Platform

How the engine works.

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.

Real runs and is tested Simulation simulated venue only Testnet XRPL testnet, no real value Planned not built Proposed design only
Architecture

A single-threaded kernel.

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.

Message bus

Real

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.

Cache

Real

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.

Clock and timers

Real

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.

Orders

Real

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.

Risk checks

Real

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.

Portfolio

Real

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.

Backtest to live

The venue is the only thing that changes.

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.

Real
src/venue.rs (abridged)Real
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>;
}
Reconciliation

A restart resumes instead of trading blind.

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.

No id collisions

Real

The engine's order id counter moves past every imported id, and a new order with an id that already exists is refused.

All or nothing

Real

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.

On the XRPL

Testnet

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.

Deterministic replay

The same data gives the same run.

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.

Digest: RealLive recorder: Planned
Quotes, trades, orders and fills are delivered in sequence; the delivery log produces a run digest. QuoteSubmitAcceptFill seq 1seq 2seq 3seq 4 Run digest same input, same value DELIVERY LOG ema_cross d0ddcad5f5f532cc breakout_bracket 0790db61a817d1d2 64-bit FNV-1a: for comparing runs, not tamper evidence
Tear sheets

One static file per run.

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.

  • Equity curve marked to market on every quote
  • Maximum drawdown, wins and losses, win rate in basis points
  • Every order fill, listed in a table
  • The sample comes from a synthetic random walk. It is not a trading result and says nothing about future results.
Data catalog

Columnar files you can check.

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-catalogReal
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
Strategy API

Rust today. Python proposed.

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.

Rust API: RealPython API: Proposed
ema_cross.py · proposed API, does not runProposed
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)

Every part has a docs page.

The docs site covers each component, its tests, the review findings and what is still open.