Skip to content

Run slow methods beside hot hooks

Slow methods are plain Python methods of a hot strategy that read the engine's state and publish new parameter values for the hot hooks. Reference: Slow methods.

Declare the strategy

Write the quoting as hot hooks over parameters and the model as an @fastmm.every method. The engine does not quote before the first publish, so on_start publishes the starting values:

class HotSlowMM(fastmm.Strategy):
    half_spread_bps = Param(0.1, min=0.0, max=1000.0, doc="half spread around the fair value, bps")
    fair_offset_bps = Param(0.0, min=-20.0, max=20.0, doc="fair value minus mid, bps")
    quote_qty = Param(0.001, min=0.0, max=1.0, doc="quantity per side, base units")
    horizon_rows = Param(20, min=1, max=1000, doc="book changes ahead that the model predicts")
    fair = State(0.0, doc="the last fair value quoted around")

    @fastmm.hot
    def on_book(self, ctx, book):
        quote(self, ctx, book)

    @fastmm.hot
    def on_params(self, ctx, book):
        quote(self, ctx, book)  # requote as soon as the offset changes

    def on_start(self, ctx):
        self.fits = 0
        ctx.publish(fair_offset_bps=0.0)  # quoting starts with the first publish

    @fastmm.every("1s")
    def refit(self, ctx):
        rows = ctx.recent(0).rows
        book = rows[rows["kind"] == 0]
        depth = book["bid_qty"] + book["ask_qty"]
        book = book[(depth > 0) & np.isfinite(book["mid"])]
        h = self.horizon_rows
        if len(book) < h + 50:
            ctx.publish()  # no new values; keeps the parameters within max_param_age_ms
            return
        imbalance = (book["bid_qty"] - book["ask_qty"]) / (book["bid_qty"] + book["ask_qty"])
        move_bps = (book["mid"][h:] / book["mid"][:-h] - 1.0) * 1e4
        beta = np.linalg.lstsq(imbalance[:-h, None], move_bps, rcond=None)[0][0]
        offset = float(np.clip(beta * imbalance[-1], -20.0, 20.0))
        self.fits += 1
        ctx.publish(fair_offset_bps=offset if np.isfinite(offset) else 0.0)

The hot on_params hook requotes when an update arrives. Both hooks call a numba.njit helper:

@numba.njit
def quote(self, ctx, book):
    if not book.valid:
        ctx.pull()
        return
    fair = book.mid * (1.0 + self.fair_offset_bps * 1e-4)
    half = fair * self.half_spread_bps * 1e-4
    ctx.quote(fair - half, fair + half, self.quote_qty)
    ctx.keep_passive()
    self.fair = fair

Read the engine's state

ctx.recent(inst).rows holds recent top-of-book changes and trades as a numpy array. ctx.snapshot() holds each instrument's book, position, PnL, quoting state and parameter age. ctx.fills() returns the fills since the previous call.

Both lag the engine: snapshot().age_ms and each instrument's param_age_ms give the age in ms.

Publish

ctx.publish(fair_offset_bps=offset) changes the named parameters for every instrument; inst= limits the update to one instrument. An unknown name or an invalid value raises ValueError in the slow method, and nothing is sent.

The engine stops quoting when no update has applied for max_param_age_ms, which defaults to 3 periods of the fastest slow method. A method with nothing to change calls ctx.publish() to renew it.

Run a backtest and replay it

Set slow_delay_ms to about the wall time the model takes, record a journal and replay it:

cfg.journal_out = str(Path(tmp) / "hot_slow_mm.fmj")
result = fastmm.run_backtest(cfg, data="synthetic", strategy=HotSlowMM, slow_delay_ms=5)
refit = result.slow_methods["refit"]
print(f"{result.strategy}: {len(result.fills['ts'])} fills, {result.outbound_messages} "
      f"messages, net PnL {result.stats()['net_pnl']:.4f}")
print(f"refit: {refit['calls']} calls, wall p50 {refit['p50_ms']:.3f} ms, "
      f"p99 {refit['p99_ms']:.3f} ms")
replayed = fastmm.replay(cfg.journal_out, HotSlowMM)
print(f"replay: {'identical' if replayed.ok else 'different'} outbound hash, "
      f"what-if: {replayed.what_if}")
python examples/python/strategies/hot_slow_mm.py

result.slow_methods reports each slow method's wall time, and run_backtest warns when the median is above slow_delay_ms. The replay does not run slow methods; the parameter updates come from the journal.

Run it live

python -m fastmm run hot_slow_mm:HotSlowMM --config <file> runs the same class against venues (Run a Python strategy live). Live, timeout applies: a call that runs past it stops the session with exit code 7.

Fix what the checks report

Error Cause
TypeError when the class is defined a slow method with another signature or a hook's name, @fastmm.every without hot hooks, or more than 128 parameters
ValueError from ctx.publish an unknown or State name, a value of the wrong type or out of range, a failed validate(), or more than 32 names
StrategyError with slow_failure == "exception" a slow method raised; hook names it
StrategyError with slow_failure == "fills overflow" more fills arrived between two runs of the slow methods than fills_capacity
exit code 7 and fastmm-live: slow tier failed (<cause>) in a live session a slow method raised or ran past its timeout, the fills ring was full, or the slow thread ended (Slow methods live)
ReplayResult.what_if is True the class, hot-hook source, fastmm or numba version, or parameters differ from the recording (what_if_reasons)