Message bus
RealTyped, dot-separated topics with wildcards. Messages reach subscribers in subscription order; anything a handler publishes is delivered after the current message has reached everyone.
The parts that exist, how they fit, and what each one is labelled. Everything on this page is in the source repository and covered by its test suite unless it says Planned or Proposed.
One ordered event loop is what makes a run repeatable. Strategy commands and order events go through one first-in, first-out work queue, so a handler that reacts to an event can never reorder events already queued.
Typed, dot-separated topics with wildcards. Messages reach subscribers in subscription order; anything a handler publishes is delivered after the current message has reached everyone.
Instruments and market data. Every write is checked: known instrument, correct precision, on tick, quotes not crossed, time never going backwards. A rejected write changes nothing.
Nanosecond timestamps. The test clock fires timers in time order, then registration order, so two runs on the same input fire identically. A live clock reads real time.
Market, limit, stop-market and stop-limit orders, time in force GTC or IOC, reduce-only, and brackets whose two exits cancel each other. An order changes state only through an allowed event.
Every order is checked before it can reach a venue: a maximum order quantity and a halt switch that rejects all new orders. A failed check becomes a rejection with the reason.
Positions and profit and loss derived from fills only, matched first in, first out in exact integer arithmetic. Commissions are tracked separately. Unrealised profit and loss is marked on each quote.
Value types: Price and Quantity are a 128-bit integer plus a decimal precision of 0 to 9. They are built from exact decimal strings or raw integers, never from floating-point numbers. Mismatched precision and overflow are errors, not silent rounding.
The engine is Engine<V: Venue>. A backtest is the same engine with the simulated venue. It runs incrementally: start, then step for each market data item, idle when time passes with no data, and finish.
A whole backtest (run) is made of exactly those calls, and it gives the same digest as before the change. A live session drives the same calls with real time, so backtest and live run the same code path.
pub trait Venue { // hand a command to the venue at engine time `now` fn send(&mut self, cmd: VenueCommand, now: UnixNanos); // acks, fills, cancels and rejects that happened by `now` fn process(&mut self, now: UnixNanos) -> Vec<OrderEvent>; // a simulated venue matches; a live venue does nothing fn on_quote(&mut self, q: &QuoteTick) -> Vec<OrderEvent>; fn inflight_count(&self) -> usize; // open orders imported at restart, all or nothing fn adopt(&mut self, orders: &[Order]) -> Result<(), String>; }
Before the first strategy callback, the engine takes a report of the venue's open orders and positions and imports it. Open orders come in as accepted orders; positions come in as an opening fill at the venue's average price.
The engine's order id counter moves past every imported id, and a new order with an id that already exists is refused.
The venue must be able to reach every imported order, or it refuses the whole set and the reconcile fails, leaving the venue tracking none of them. Duplicate ids in one report are refused.
The XRPL venue builds the report from the account's resting offers on the ledger, so offers left open before a restart are followed again.
The bus keeps a delivery log of every message, in order. Its digest identifies a run. A backtest read from the data catalog gives the same digest as the same data held in memory.
Recording a live session's inputs so it can be replayed through the same path is supported by the design; a recorder is not built yet.
A backtest writes a single HTML file: inline styles and an inline SVG equity curve, no scripts, no external requests, every piece of text escaped. Run statistics use integer arithmetic only. The file can be diffed run to run.
Market data lives in Apache Parquet files, one per data kind, instrument and UTC day, with prices stored as raw integers. A manifest lists each file's rows, time range and SHA-256; a read refuses any file whose hash does not match.
fth-catalog <root> instrument add <id> <price_precision> <size_precision> <tick>
fth-catalog <root> import quotes <id> <file.csv>
fth-catalog <root> import trades <id> <file.csv>
fth-catalog <root> list
fth-catalog <root> verify # exit 2 if any file is mismatched, missing or unlisted
Strategies are written in Rust against the Strategy trait: on_start, on_quote, on_trade, on_bar, on_timer, on_order_event, on_stop.
A Python layer for research and strategy code is proposed. It is not built, and the snippet beside this text does not run.
from fth_engine import Strategy, StrategyConfig, Bar, OrderSide from fth_engine.indicators import EMA class EmaCrossConfig(StrategyConfig): instrument: str fast: int = 10 slow: int = 30 qty: str = "1" # decimal string, never float class EmaCross(Strategy): def on_start(self): self.fast = EMA(self.config.fast) self.slow = EMA(self.config.slow) self.subscribe_bars(self.config.instrument, "1-MINUTE-LAST") def on_bar(self, bar: Bar): self.fast.update(bar.close); self.slow.update(bar.close) if not self.slow.ready: return if self.fast.crossed_above(self.slow): self.submit_market(OrderSide.BUY, self.config.qty) elif self.fast.crossed_below(self.slow): self.close_all_positions(self.config.instrument)
The docs site covers each component, its tests, the review findings and what is still open.