Skip to content

NullPointerException in HandlerManager when enabling Python addon on second instrument #36

@michaelalvarezm

Description

@michaelalvarezm

NullPointerException in HandlerManager when enabling Python addon on second instrument

Environment

  • Bookmap version: 7.6.0 build 30
  • Bookmap embedded Python: 3.9 (miniforge bm39 env)
  • OS: Windows 11 (WSL Ubuntu-24.04 used to host the addon source)
  • Data provider: Rithmic Paper
  • Instruments tested: NQM6.CME@RITHMIC, ESM6.CME@RITHMIC
  • Strategy type: Simplified L1 Python addon
  • Loader: runpy.run_path(...) from Bookmap's Python editor

Summary

A Python addon that subscribes to multiple instruments via INSTRUMENT_ALIASES
crashes Bookmap with java.lang.NullPointerException in
HandlerManager.handle() immediately after the subscribe handler
fires for the second instrument. The crash happens regardless of which
instrument is enabled first (NQ then ES, or ES then NQ), and regardless
of the Python-side state. Single-instrument operation works perfectly.

Reproduction steps

  1. Create a Python addon (full code below) that calls bm.create_addon,
    adds trades + depth + on_interval handlers, and starts with
    bm.start_addon(addon, subscribe_handler, unsubscribe_handler).
  2. Load the script in Bookmap via the Python editor:
    import runpy
    runpy.run_path(r'<path_to_script>', run_name='__main__')
  3. Enable the addon for the first instrument (e.g. NQM6.CME@RITHMIC).
    Observe: subscribe handler fires correctly, addon starts processing.
    snapshot.json is written every 100ms with valid data. ✅
  4. Enable the addon for the second instrument (e.g. ESM6.CME@RITHMIC),
    without disabling the first. Observe:
    • Python's subscribe handler IS called for the second instrument
    • STATE dict reaches size 2
    • ~2ms later, Bookmap throws NullPointerException and unloads
      the addon. ❌

The same crash happens in the reverse order (ES first, then NQ).

Stack trace

java.lang.NullPointerException: Cannot invoke
"com.bookmap.api.rpc.server.handlers.HandlerManager.handle(com.bookmap.api.rpc.server.data.utils.AbstractEvent)"
because "this.handlerManager" is null
	at com.bookmap.api.rpc.server.EventLoop.lambda$new$0(EventLoop.java:27)
	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)

After the NPE, Bookmap unloads the addon (Unloading strategy/indicator ... bm_producer.jar), the Python unsubscribe_instrument handler is
called for the first instrument, and the addon process exits. Settings
get persisted with disabling for ESM6.CME@RITHMIC flags so the
Strategy settings were saved before it was loaded last time message
appears on the next load.

Relevant Bookmap log excerpt

20260428 22:11:05.581 (AWT-EventQueue-1) Stratgies dialog: com.bookmap.api.rpc.server.addon.bm_producer true
20260428 22:11:05.581 (AWT-EventQueue-1) Enabled for NQM6.CME@RITHMIC true - started (Simplified L1)
20260428 22:11:05.613 (AWT-EventQueue-1) BrApi(...): Add-on "bm_producer" - Became available for broadcasts as a consumer.
20260428 22:11:05.614 [PYTHON-CLIENT] [bm_producer] subscribe alias=NQM6.CME@RITHMIC ... pips=0.25 size_mult=1.0
20260428 22:11:05.614 [PYTHON-CLIENT] [bm_producer]   registered short=NQ trades+depth subscribed; STATE size=2
20260428 22:11:05.614 [RPC-SERVER] Received finish initialization message for NQM6.CME@RITHMIC
20260428 22:11:05.616 (AWT-EventQueue-1) Enabled for NQM6.CME@RITHMIC true - complete (Simplified L1)
20260428 22:11:05.618 ERROR: (Global Exception Handler) Intercepted exception from strategy
java.lang.NullPointerException: Cannot invoke "com.bookmap.api.rpc.server.handlers.HandlerManager.handle(...)" because "this.handlerManager" is null
    at com.bookmap.api.rpc.server.EventLoop.lambda$new$0(EventLoop.java:27)
    ...
20260428 22:11:05.624 (AWT-EventQueue-1) Unloading strategy/indicator C:\Bookmap\Python\build\bm_producer.jar com.bookmap.api.rpc.server.addon.bm_producer

(In this excerpt, ES had been enabled first at 22:09:53 and worked
correctly for ~72 seconds. NQ was enabled at 22:11:05; the NPE
occurred at 22:11:05.618 — about 2ms after the Python-side subscribe
handler completed for NQ.)

Minimal repro script

"""Minimal reproduction of NPE when enabling addon on second instrument."""
import bookmap as bm
import threading
from datetime import datetime, timezone

def stamp():
    return datetime.now(timezone.utc).isoformat(timespec="microseconds")

def log(msg):
    print("[repro] [" + stamp() + "] " + msg, flush=True)

STATE = {}

def handle_subscribe_instrument(addon, alias, full_name, is_crypto, pips,
                                size_multiplier, instrument_multiplier,
                                supported_features):
    log("subscribe alias=" + str(alias) + " (STATE size before=" + str(len(STATE)) + ")")
    STATE[alias] = {"order_book": bm.create_order_book()}
    bm.subscribe_to_trades(addon, alias, len(STATE))
    bm.subscribe_to_depth(addon, alias, len(STATE) + 100)
    log("  subscribed; STATE size=" + str(len(STATE)))

def handle_unsubscribe_instrument(addon, alias):
    log("unsubscribe alias=" + str(alias))
    STATE.pop(alias, None)

def handle_trades(addon, alias, price, size, is_otc, is_bid,
                  is_execution_start, is_execution_end,
                  aggressor_order_id, passive_order_id):
    pass  # no-op

def handle_depth(addon, alias, is_bid, price, size):
    state = STATE.get(alias)
    if state is not None:
        bm.on_depth(state["order_book"], is_bid, price, size)

def on_interval(addon, alias):
    pass  # no-op

if __name__ == "__main__":
    log("STARTUP, thread=" + threading.current_thread().name)
    addon = bm.create_addon()
    bm.add_trades_handler(addon, handle_trades)
    bm.add_depth_handler(addon, handle_depth)
    bm.add_on_interval_handler(addon, on_interval)
    bm.start_addon(addon, handle_subscribe_instrument, handle_unsubscribe_instrument)
    log("Addon started, blocking...")
    bm.wait_until_addon_is_turned_off(addon)
    log("Addon stopped.")

To reproduce:

  1. Save as repro.py.
  2. Load in Bookmap via runpy.run_path(r'<path>', run_name='__main__').
  3. Enable for any single instrument — works fine.
  4. Enable for a second instrument (without disabling the first) — NPE.

What I expected

Per the official start_addon documentation:

handle_subscribe_instrument is a function that you should define.
It will be called each time you enable the addon in Bookmap for a
certain instrument
.

This wording suggests one Python addon should handle multiple
instruments by receiving multiple subscribe calls (one per
instrument). The official examples (mbo_test.py, cvd_addon.py,
liquidity_tracker.py, simple_market_maker.py) all use this pattern
with alias_to_* dicts, implying multi-instrument is the intended
design.

What actually happens

The subscribe handler fires correctly for the second instrument, but
Bookmap's internal EventLoop then tries to dispatch an event to a
HandlerManager that is null. This suggests the second-instrument
initialization path has a race condition or missing initialization on
the Java side.

Workaround currently in use

I've added a guard in my Python addon that refuses any subscribe call
beyond the first. This restricts the addon to one instrument per
session. The user must disable the addon entirely before switching to
a different instrument. This works but defeats the whole point of
multi-instrument addons.

def handle_subscribe_instrument(addon, alias, ...):
    if len(STATE) >= 1:
        log("REFUSED: already subscribed to " + str(list(STATE.keys())[0]))
        return
    # ... normal subscribe path ...

Questions for the team

  1. Is there a known issue with multi-instrument Simplified L1 Python
    addons in Bookmap 7.6.0?
  2. Should the Python API support multi-instrument in the same addon,
    or is it expected behavior that each instrument requires a separate
    addon load?
  3. If multi-instrument is supported, is there an initialization order
    or method I'm missing in the bootstrap?
  4. If NOT supported, what's the recommended pattern for an addon that
    needs to read data from multiple instruments simultaneously (e.g.
    to compute cross-instrument correlations)?

Happy to provide more logs, test other configurations, or run a debug
build if helpful.

Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions