I'm looking to create a Cumulative Volume Delta indicator, except instead of using Market Orders as Volume, I'm trying to use Filled Limit orders as Volume. This entails getting the best bid and ask price, and recording the size of the orderbook event when the bid or ask is on the same tick as the limit order. Here is my Code: ## 1. Import Libraries and Setup import bookmap as bm from typing import Dict, Any # # Initialize the addon - this creates our connection to Bookmap addon = bm.create_addon() # What this does: Think of this like opening a phone line to Bookmap. The addon object is your communication channel. # # ## 2. Global Variables for Data Storage # Storage for our indicator data indicators: Dict[int, int] = {} # Maps request_id to indicator_id limit_order_delta: Dict[str, float] = {} # Running delta per instrument instrument_data: Dict[str, Dict[str, Any]] = {} # Instrument details order_books: Dict[str, Any] = {} # Order book objects previous_bbo: Dict[str, tuple] = {} # Previous best bid/offer # What this does: # indicators - Like a phone book matching our requests to Bookmap's responses # limit_order_delta - The running total of our indicator for each trading instrument # instrument_data - Important details about each instrument (like price formatting) # order_books - Live order book data for each instrument # previous_bbo - The last best bid/offer prices we saw # # ## 3. Instrument Subscription Handler def handle_subscribe_instrument(addon, alias, full_name, is_crypto, pips, size_multiplier, instrument_multiplier, supported_features): """ Called when we start tracking a new instrument Think of this like subscribing to a specific stock's data feed """ print(f"Starting Limit Order Delta for {alias}", flush=True) # Store instrument details for price/size calculations instrument_data[alias] = { 'pips': pips, 'size_multiplier': size_multiplier, 'instrument_multiplier': instrument_multiplier } # Initialize our running delta to zero limit_order_delta[alias] = 0.0 # Create order book tracker for this instrument order_books[alias] = bm.create_order_book() # Initialize previous BBO previous_bbo[alias] = (None, None) # Register our indicator with Bookmap bm.register_indicator( addon, alias, 1, # request_id - like a tracking number "Limit Order Volume Delta", # Name shown in Bookmap "BOTTOM", # Display in subchart below main chart color=(0, 150, 255), # Blue color line_style="SOLID", initial_value=0.0 ) # Subscribe to order book updates bm.subscribe_to_depth(addon, alias, 1) # What this does: When you enable the indicator on a trading instrument, this function sets up everything needed to track that instrument's data. It's like tuning into a specific radio station. # # ## 4. Indicator Responce Handler def handle_indicator_response(addon, request_id, indicator_id): """ Bookmap calls this to give us the indicator ID we need for updates Think of this like getting a tracking number for a package """ indicators[request_id] = indicator_id print(f"Indicator registered with ID: {indicator_id}", flush=True) # What this does: After we request to create an indicator, Bookmap assigns it a unique ID and tells us what it is through this function. # # ## 5. Main Logic - Depth Data Handler def handle_depth(addon, alias, is_bid, price_level, size_level): # Update order book with new depth event bm.on_depth(order_books[alias], is_bid, price_level, size_level) # Get best bid and offer safely bbo = bm.get_bbo(order_books[alias]) # Note: get_bbos(), not get_bbo() if bbo is None: # Order book empty or not ready yet, skip processing return (bid_pl, bid_sl), (ask_pl, ask_sl) = bbo # Extract only price levels for delta logic current_best_bid = bid_pl current_best_ask = ask_pl # Retrieve previous price levels prev_bid, prev_ask = previous_bbo[alias] # Initialize previous on first tick if prev_bid is None or prev_ask is None: previous_bbo[alias] = (current_best_bid, current_best_ask) return volume_delta = 0.0 # Detect limit‐sell fills when bid moves up if current_best_bid > prev_bid: hit_volume = detect_limit_sells_hit( order_books[alias], prev_bid, current_best_bid, instrument_data[alias]['size_multiplier'] ) volume_delta -= hit_volume # Detect limit‐buy fills when ask moves down if current_best_ask < prev_ask: hit_volume = detect_limit_buys_hit( order_books[alias], prev_ask, current_best_ask, instrument_data[alias]['size_multiplier'] ) volume_delta += hit_volume # Update indicator if any volume change occurred if volume_delta != 0.0: limit_order_delta[alias] += volume_delta ind_id = indicators.get(1) if ind_id is not None: bm.add_point(addon, alias, ind_id, limit_order_delta[alias]) # Save current price levels for next update previous_bbo[alias] = (current_best_bid, current_best_ask) # What this does: This is the main engine of our indicator. Every time the order book changes, this function: # Checks if the best bid moved up (potentially hitting limit sell orders) # Checks if the best ask moved down (potentially hitting limit buy orders) # Calculates the volume of orders that got hit # Updates our running delta # Displays the new value on the chart # # ## 6. Helper Functions for Volume Detection def detect_limit_sells_hit(order_book, old_bid, new_bid, size_multiplier): """ Calculate volume of limit sell orders that got hit when bid moved up """ if new_bid <= old_bid: return 0.0 hit_volume = 0.0 # Check each price level between old and new bid for price_level in range(old_bid + 1, new_bid + 1): # Get the size at this price level on the ask side size_level = bm.get_size_by_price(order_book, False, price_level) # False = ask side if size_level > 0: # Convert size level to actual volume actual_volume = size_level / size_multiplier hit_volume += actual_volume return hit_volume def detect_limit_buys_hit(order_book, old_ask, new_ask, size_multiplier): """ Calculate volume of limit buy orders that got hit when ask moved down """ if new_ask >= old_ask: return 0.0 hit_volume = 0.0 # Check each price level between old and new ask for price_level in range(new_ask, old_ask): # Get the size at this price level on the bid side size_level = bm.get_size_by_price(order_book, True, price_level) # True = bid side if size_level > 0: # Convert size level to actual volume actual_volume = size_level / size_multiplier hit_volume += actual_volume return hit_volume # What this does: These helper functions calculate exactly how much volume got "hit" when prices moved. They look at the order book levels between the old and new prices to see what orders were there. # # ## 7. Unsubscribe Handler def handle_unsubscribe_instrument(addon, alias): """ Clean up when we stop tracking an instrument """ print(f"Stopping Limit Order Delta for {alias}", flush=True) # Clean up our data if alias in limit_order_delta: del limit_order_delta[alias] if alias in instrument_data: del instrument_data[alias] if alias in order_books: del order_books[alias] if alias in previous_bbo: del previous_bbo[alias] # What this does: When you disable the indicator, this cleans up all the data we were storing for that instrument. # # ## 8. Final Setup and Startup # Register all our event handlers bm.add_indicator_response_handler(addon, handle_indicator_response) bm.add_depth_handler(addon, handle_depth) # Start the addon bm.start_addon(addon, handle_subscribe_instrument, handle_unsubscribe_instrument) # Keep the addon running bm.wait_until_addon_is_turned_off(addon) # What this does: This connects all our functions to Bookmap and starts the addon running. And this is the error message I am getting: java.lang.RuntimeException: Last msg - 8\ue000BTC-USDT:MBERP@BMD\ue0001\ue00053550\ue00011 Traceback (most recent call last): File "C:\Bookmap\Python\tmp\volume_pro\bookmap\bookmap.py", line 138, in _process_event handler(addon, *params) ~~~~~~~^^^^^^^^^^^^^^^^ File "C:\Bookmap\Python\tmp\volume_pro\script.py", line 88, in handle_depth (bid_pl, bid_sl), (ask_pl, ask_sl) = bbo ^^^^^^^^^^^^^^^^ TypeError: cannot unpack non-iterable NoneType object Reading task stopped at com.bookmap.api.rpc.server.addon.volume_pro.lambda$initialize$1(RpcServerAddon.java:116) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) at java.base/java.lang.Thread.run(Unknown Source) Thanks for your help, this website is a great example of how great the world can be Continue reading...