vike
Strategies

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.

crates/vike-strategy/src/position_executor.rs drives ONE position's full lifecycle over a portable Broker handle. It is a plain struct with explicit step methods — start, on_bar, on_tick, on_fill, on_refresh, on_order_event, cancel — that a controller harness calls from the matching Strategy hooks. It is deliberately NOT a Strategy itself. Every order crosses the same Broker, so it still passes the live RiskGate, and all time comes from the injected Broker::now and each fill's own epoch-millisecond timestamp — never a wall clock — so the machine is deterministic and runs bit-identically live and in backtest.

The PARAM types live one layer down, in vike_model::barrier: TripleBarrier and ControllerParams are pure serde data and are transitively part of the StrategyParams payload the exec ingest lane carries, so they must sit in the bottom domain crate. The state machine imports them back down.

States and transitions

ExecutorState has four live labels — Pending (created from an intent, nothing submitted), EntryWorking (the entry is in flight or resting, driving refresh and submit-retry), Open (the position is live and the barriers armed) and Closing (a barrier fired, the close is in flight) — and three terminals: Closed (flat, records an outcome), Failed (the entry never landed, retries exhausted) and Canceled (stopped before any fill).

start submits the entry per the intent's EntryKind. A FULL entry fill — on_fill accumulates partials — records the VWAP entry price and the entry timestamp, arms the barriers, and opens the position. The FIRST barrier hit submits a market close; a full flatten produces the terminal outcome.

Closing is sticky

Once Closing, the executor NEVER re-opens or re-evaluates entry or barriers: on_bar and on_tick are no-ops until the close completes, so a late favourable tick cannot flip a closing position back to Open. A position, once told to flatten, only completes the flatten — a position must never be stranded. A rejected CLOSE resubmits the unflattened remainder and STAYS Closing.

The triple barrier

Take-profit, stop-loss and trailing are ABSOLUTE PRICE OFFSETS — a positive distance in price units — from the ENTRY FILL price; the signed barrier price is derived at check time from the entry price and the position side by take_profit_price and stop_loss_price. TripleBarrier::from_bps is a caller-side convenience that converts basis points against a reference price, so the stored shape stays absolute and unit-free. Every leg is optional and defaults to None, an un-armed position.

The third leg is the genuinely new one: time_limit_ms is a max HOLDING duration, and time_barrier_hit fires once now >= entry_ts + time_limit_ms. Nothing else in the workspace closes a position on elapsed time.

Each price barrier is detected through vike_model::order_fill_price — the same gap-open adverse-fill and trailing-ratchet oracle the conditional-order emulator and the backtest engines use — so a barrier hit behaves identically on both stacks. The stop is evaluated as a reduce-only stop, the take-profit as a reduce-only limit, and the trailing stop as a trailing order whose extreme is seeded from the entry price and ratcheted in place on every non-firing check. The exit_px on a BarrierHit is DIAGNOSTIC — the oracle's trigger price, or the current mark for a time exit; the real close fill is whatever the venue or the sim engine resolves when the market close lands.

Precedence

evaluate_barriers returns the FIRST hit under a fixed, deterministic order: stop-loss, then trailing, then take-profit, then time. Within one ambiguous bar the intrabar path is unknown, so the ADVERSE barriers are assumed to hit first — the same no-look-ahead-optimism convention the backtest engines apply to protective stops. A wide bar straddling both a stop and the target therefore resolves to the stop, and a position past its deadline that also crossed a price barrier reports the price barrier, because a real cross is more specific — and more pessimistic — than the timeout. evaluate_barriers_at_price is the tick twin: a degenerate one-price bar.

Refresh and retry

on_refresh is the harness-driven timer step, a no-op outside EntryWorking. It resubmits a backed-off retry that has come due, and reprices a still-resting, wholly-unfilled LIMIT entry to the current mark once RefreshPolicy::every_ms has elapsed since the last submit.

Both refresh modes take the same action here

The portable Broker surface has NEITHER a modify nor a cancel verb — only submit_* — so Reprice and CancelReplace both degrade to one fresh submit_limit, and the stale resting order is abandoned at this layer. Honouring the distinction needs the richer HftBroker::modify_tagged and cancel_tagged verbs and is a later live-only enhancement; the mode is preserved on the intent for it.

on_order_rejected treats a venue Rejected and a RiskGate Denied identically — the order never established, so it is retryable. An entry reject with budget remaining schedules a resubmit backoff_ms in the future and terminalizes Failed only once RetryPolicy::max_attempts is exhausted; a reject of a partially-filled entry is ignored, because there is no clean single order to retry. cancel terminalizes as Canceled before any inventory exists and FLATTENS instead once the position is even partly open, so an external stop never strands inventory.

The outcome

ExecutorOutcome records the barrier that fired, the entry and exit timestamps and prices, the realized PnL and the round-trip fees. The PnL is folded through the canonical vike_model::TradeFold — the one cost-basis primitive the account, the sim broker, the vector engine and reconciliation share — rather than a hand-rolled VWAP difference, so it inherits the contract multiplier (PositionExecutor::with_multiplier) and the GROSS convention for free. Fees are reported SEPARATELY rather than netted silently into the same field; ExecutorOutcome::net is the fee-adjusted figure.

On this page