The controller seam
The Controller decides whether to open a position; the ControllerHarness is itself a Strategy and owns the executors, the routing and the cooldown.
crates/vike-strategy/src/controller.rs is the TOP half of a controller-to-executor split. It
carries two things: the Controller decision seam — the WHAT, emit a PositionIntent or not — and
the ControllerHarness that wraps a controller into a plain Strategy, the glue that owns the live
position executors and does the HOW.
Why it is a Strategy, not a parallel runtime
ControllerHarness<C> implements Strategy<B> for any B: Broker. It IS a strategy, so it mounts
through the existing strategy seam and runs unchanged on BOTH engines — the live single-writer
runtime over LiveBroker and the backtest engine over SimBroker. Direct-submit strategies are
untouched; a strategy adopts managed positions by mounting a harness instead of itself. Every order
an executor emits still crosses the SAME Broker handle, then the drain, then the RiskGate, then
the client, so the harness adds NO authority and cannot bypass the gate. It is additive: no serde
event or wire change, no new command verb — it is composed entirely from the existing Strategy
handlers plus the executor.
What the harness owns
The controller, asked per (venue, symbol) whether to open. The active executors, ONE per
(venue, symbol) — a Vec keyed by the pair, because there are few of them and a linear scan with
deterministic insertion order replays reproducibly, the same rationale the multi-venue paper client
uses. And a post-exit cooldown stamp per pair: the controller may not re-open a pair until
last_close_ts + cooldown_ms, with 0 meaning eligible on the very next observation.
Routing is a venue_map of symbol -> venue alongside a default venue. That is what makes a
CROSS-VENUE controller tradeable from ONE mount: each series is keyed and evaluated under its own
venue, so a controller whose decision needs two venues' data can observe them all. An empty map —
the default — makes the harness byte-identical to a single-venue mount.
The per-callback flow
On each on_bar, on_quote_tick or on_trade_tick the harness does three things in order.
Maybe-open: if there is no active executor for this pair and the cooldown has elapsed, ask
Controller::evaluate; on Some(intent) spawn an executor and start it, which submits the entry
through the same broker. Drive: advance the active executor — its refresh and retry timer, then
the barrier evaluation. Reap: when an executor reaches a terminal state, record its outcome if
it has one, stamp the cooldown from its close timestamp, and drop it.
on_fill routes the execution to the active executor for the fill's symbol. on_order_event
forwards the reject or deny an executor's retry reacts to, BROADCAST to the active executors rather
than routed, because the portable Broker surface exposes no client-order-id to route a single
order by; with one executor per pair, and typically one pair per mount, that is exact, and a
non-reject transition is a no-op inside the executor, so the broadcast is harmless.
The decision seam
Controller::evaluate reads the same market and account view a strategy gets — but through a SHARED
&B, so it can call price, bars, position and now and yet structurally cannot submit
orders, because every submit_* verb needs &mut B. A controller decides; the executor executes.
It reasons in POSITIONS — it emits a PositionIntent carrying side, quantity, entry kind, triple
barrier and the refresh and retry policies — never in raw orders, and it never sees fills. The
harness invokes it only when it is free to open, so an implementation need not re-check the
cooldown or the active-executor condition.
MomentumController is an example, not a recommendation
The shipped reference controller compares the current mark to the mark at its PREVIOUS invitation
and opens long on a move of at least threshold, short on the mirror, otherwise declining. The
first invitation has no reference, so it records the mark and declines. It is deterministic and
read-only, works identically on the bar path (price is the bar close) and the tick path, and the
harness mounting it is parity-clean.
Determinism and live re-tune
Every decision is a pure function of the portable broker reads plus the delivered fills and order
events, and ALL time comes from the injected Broker::now and each fill's own epoch-millisecond
timestamp — never a wall clock. Both runtimes deliver those identically, so a harness produces the
same fills and outcomes live and in backtest; crates/vike-backtest/tests/controller_harness.rs is
the parity gate.
A mounted harness is re-tuned live without unmounting. StrategyParams::PositionController carries
a ControllerParams bag spanning the two layers a re-tune touches — the harness cooldown, and the
controller's intent template and decision knob. ControllerHarness::on_params_updated applies the
cooldown itself and forwards the whole bag to Controller::apply_params, which keeps each
controller the sole authority on which knobs it absorbs, then bumps an observable params_epoch. A
foreign variant is a no-op and does not bump it. The policy is that a re-tune affects only FUTURE
opens: an in-flight executor keeps its armed intent and barriers and is never torn down or
retroactively resized.
Harness knobs from a profile
Mounted through the registry, the harness reads venue,
cooldown_ms and an optional [strategy.params.venues] table. Two hazards are declared rather than
hidden: the venue fallback is the literal string sim, which on a live mount is a tag no venue
answers to — the registry's own echo reports it for exactly that reason — and a venues row whose
value is not a string is dropped silently by the reader's filter_map, which is why the echo prints
the surviving map rather than the table that was typed.
The portable registry
Name to boxed strategy, generic over the broker — the one table that serves both the simulator and the live daemon, and the four refusals it makes possible.
The position executor
One position, driven from Pending to Closed over a portable Broker — the lifecycle states, the triple barrier and its fixed precedence, and the realized-PnL law.