Ir al contenido

API del host

T0 Host API — contract model, capability system, and the TTT host contracts

Estas páginas están en inglés. Una especificación en un solo idioma dice una sola cosa.

This document defines (1) the host-independent contract model every host implements, (2) the capability/status model, and (3) the contracts TTT exposes in host API 0.1.0. The machine-readable form of (3) is the manifest TTT publishes; its committed snapshot is t0-lang/contracts/ttt-host-0.1.json, and a test in t0-host fails when the two differ.

1. Contract model (t0-contract)#

HostManifest
  manifest_format: 1
  host:        { name: "ttt", version: "<TTT build>", api_version: "0.1.0" }
  language:    VersionReq   -- language versions this host accepts
  ir:          VersionReq   -- IR versions this host's runtime executes
  contracts:   [ContractDesc]
  status:      { "<contract>.<op>": OperationStatus }

ContractDesc
  id: "backtest"             -- the namespace in source
  version: "0.1.0"
  doc
  types:      [TypeDef]      -- Record{name, fields} | Enum{name, variants} | Handle{name}
  operations: [OperationDesc]

OperationDesc
  name: "run"
  doc
  kind:        call | signal
  params:      [{ name, type: TypeDesc }]
  returns:     TypeDesc
  effect:      pure | read | compute | workspace_write | external | live_trading
  permission:  Permission | none
  execution:   inline | ui_thread | worker | remote
  cancellable: bool
  progress:    bool
  idempotent:  bool
  errors:      [code]        -- codes this operation may return besides the generic ones
  since:       version
  deprecated:  { replacement, removal } | none

TypeDesc  = Unit | Bool | Int | Float | Decimal | String | Price | Quantity | Money
          | Ratio | Currency | Timestamp | Duration | Error | Strategy
          | List(T) | Option(T) | Result(T, E) | Signal(T)
          | Named("<contract>.<Type>")
  • kind: signal operations are never invoked by the interpreter: they are vocabulary for strategy graphs, interpreted by the host's strategy engine (ADR-0004). Their parameters must be constant at instantiation.
  • Values cross the boundary as HostValue (owned, Send, serde): scalars, decimal strings, records as ordered field lists, enums by variant name, handles as { type, epoch, id }, strategies as the IR signal graph plus bound parameters. The runtime checks every value the host returns against the declared return type (E-RT-HOST-CONTRACT if it does not match: a host bug is reported, never trusted).

2. Capability system: six separate questions#

question answered by where it is visible
1. Known to the SDK the committed contract snapshot contracts/*.json
2. Implemented by the runtime the runtime supports the contract's types and execution class the compiler/runtime refuse unknown TypeDesc
3. Exposed by this host the operation is in the host's manifest HostManifest.contracts
4. Available here its dependencies exist (a catalogue, a datasets directory) status.available + detail
5. Operational now measured health; null = not measured, never "fine" status.healthy
6. Authorised for this program declared by requires and granted to the session E-CAP-UNDECLARED at compile time; E-PERM-DENIED before and while running

Presence in a document never implies 3–6. A program that uses an operation the manifest lacks does not compile against that manifest; one whose operation is exposed but unavailable compiles and fails that call with E-CAP-UNAVAILABLE and the host's reason.

3. Permissions#

market.read, analytics.run, research.submit, backtest.run, workspace.read, workspace.write, account.read, orders.prepare, orders.submit, risk.modify, external.access (ADR-0008) and script.run (ADR-0013, API 0.1.23). In TTT the grants are toggles in the T0 window, all off at start, held in memory only.

4. Generic host error codes#

E-HOST-STALE-HANDLE, E-HOST-UI-TIMEOUT, E-HOST-CANCELLED, E-HOST-BAD-ARGUMENT, E-HOST-FAILED, E-CAP-UNAVAILABLE, E-PERM-DENIED (meanings in ../language/diagnostics.md).

5. TTT host API 0.1.13#

API 0.1.26 (2026-10-22), additive: features 0.1.9 -- the rolling signals rolling_vwap, rolling_poc, rolling_value_area_high, rolling_value_area_low, rolling_node_below and rolling_node_above (§5.3a).

API 0.1.25 (2026-10-21), additive: the component role manager and its contract (§5.20).

API 0.1.30 (2026-11-01), additive: features 0.1.12 -- signals that compare the markets of a portfolio: funding_rank, return_rank, risk_rank, volume_rank, -1 the lowest to +1 the highest, and breadth_down; funding_mean, reference_return, six more features.Market. A settlement or a day counts five minutes after it, in a backtest and live alike; a comparison is refused on a replay. handlers 0.1.4 -- Act.Size(Float): an entry's size as a multiple of the portfolio's weight.

API 0.1.29 (2026-10-31), additive: features 0.1.11 -- what the venue's closed candle says beyond its prices: bar_trades, bar_buy_volume, bar_quote_volume. From the dataset stream klinesFlow1m, and live from the candles a portfolio reads; a chart refuses a strategy that reads them. bar_trades is every execution, not flow_trades over aggregated prints.

API 0.1.24 (2026-10-21), additive: handlers 0.1.2 -- the action Act.Notify(handlers.Notice { name, text }) (§5.16).

API 0.1.23 (2026-10-21), additive: the component role indicator (§5.18) and the operation script.run with its permission (§5.19).

API 0.1.13 (2026-10-18), additive: a strategy reads the market beyond its candles -- features 0.1.8 (§5.3a): funding, open interest, forced closes and the resting book, the levels of the UTC day, and the large prints of the tape. API 0.1.12 (features 0.1.7) added the footprint of a bar of any length; 0.1.11 (features 0.1.6) studies on longer bars; 0.1.10 (features 0.1.5) close_ago. All of them are the same figures the chart and the research contracts show, and an unknown is never a zero (§5.9a).

5.0 TTT host API 0.1.9#

API 0.1.9 (2026-10-14), additive: a strategy reads the bar still forming. features 0.1.4 adds forming_open, forming_high, forming_low, forming_volume and forming_delta (§5.3): the one-minute bar in progress, made of the prints the strategy is given, so a rule can decide inside a bar instead of at its close.

One correction in 0.1.9: quantities are in units of the instrument. features.volume, cvd and bar_delta, and the min_volume of inferred_absorption_sell_side, were read and returned in the strategy engine's internal integer scale (10^-8 of the instrument), which no document stated and which an order's size -- written in units of the instrument in the same strategy -- contradicted: cvd > 5 meant five hundred-millionths, and a min_volume of 5.0 was a threshold of nothing (the shipped 45_orderflow_strategy.tzs passed 1.0 and 5.0 meaning bitcoin). They are now in units of the instrument, like every other quantity a strategy states. A program that compared one of them against a number written in the old scale reads a different thing now and has to divide that number by 10^8; nothing else changed, and a program written against 0.1.8 loads as it did.

API 0.1.8, additive: a program hands over a strategy. One new contract, trade 0.1.0 (§5.15): run(strategy) puts a strategy to work where the person started the program -- a market replay, the live market in real time, or the Backtests window's backtests.

API 0.1.8 (2026-10-13), additive: a program that watches. One new contract, live 0.1.0 (§5.14): wait (a program rests until a bar of the market closes or a time passes) and now (the market's clock). With it a script is no longer only something run from the top each time: started once as an "Always on" hook (Script Hooks), it keeps its state in its own variables and loops around live.wait, so it can hold a running total, a last swing, a list of zones. The runtime renews a monitor's work budgets (evaluation steps, host calls, output, wall clock) at every rest -- limits are per activation, not for its life; its memory budget is never renewed.

API 0.1.7 (2026-10-13), additive: the market a program reads, beyond candles -- market 0.1.3 adds coverage (§5.2), and four new contracts: derivs 0.1.0 (§5.10: funding, open interest, mark and index price, liquidations), flow 0.1.0 (§5.11: footprint cells, the tape, the spot tape), options 0.1.0 (§5.12: dealer gamma by strike, the walls, the flip) and book 0.1.0 (§5.13: the resting book in bands). They all follow §5.9a: unknown is never a number.

API 0.1.6 (2026-10-13), additive: what a script needs to be seen and heard. market 0.1.2 adds chart_candles (§5.2): the candles a chart shows, which is the input of a custom indicator. Two new contracts: plot 0.1.0 (§5.8), a program draws on a chart; and alert 0.1.0 (§5.9), a program raises an alert on the terminal or posts to a Discord / Telegram / URL webhook. external.access may now be given to a script hook (still never an order or a backtest).

API 0.1.22 (2026-10-21), additive: backtest 0.1.5 adds run_with_risk and submit_with_risk (§5.4, Sweeping the risk policy): a backtest under the risk policy of the bindings with other numbers.

API 0.1.21 (2026-10-21), additive: risk 0.1.4 (§5.17): Context.group_others, Context.group_max_gross.

API 0.1.20 (2026-10-21), additive: risk 0.1.3 (§5.17): Intent.liquidation, SizeRequest.liquidation, Context.slippage.

API 0.1.19 (2026-10-21), additive: backtest 0.1.4 adds run_group (§5.4, Groups): one strategy over several instruments as a group of accounts on one clock.

API 0.1.5 (2026-10-10), additive: a new contract event 0.1.0 (§5.7): what set the program off. It is what a script hook or a trigger line starts a program with; run by hand it says manual.

API 0.1.4 (2026-10-10), additive: workspace 0.1.3 adds close_chart and close_panel (marked ‡‡ below). The terminal always keeps one chart open: closing the last is refused with E-WS-LAST-CHART, never done and never undone by the terminal opening a chart the program did not choose.

API 0.1.3 (2026-10-10), additive: workspace 0.1.2 (inspect, open and focus charts; the application's panels), backtest 0.1.3 (trades_page; trades read as a stream), research 0.1.2 (market_state cancellable), and a new capabilities 0.1.0. Operations new in 0.1.3 are marked ‡.

API 0.1.2 (2026-10-10): backtest 0.1.2 and research 0.1.1 add explicit limits as errors (E-BT-CAPACITY, E-BT-TOO-MANY, at most 64 research features); no signature or meaning changed. A program checked against 0.1.0 or 0.1.1 runs unchanged.

API 0.1.1 (2026-10-09) adds operations and one contract to 0.1.0 without changing any existing signature or meaning: workspace, market and backtest are at 0.1.1, features stays 0.1.0, research is new at 0.1.0. A program checked against a 0.1.0 contract runs on 0.1.1 (same minor, newer patch: docs/versioning/compatibility-policy.md); a program using a 0.1.1 operation declares >=0.1.1. Operations new in 0.1.1 are marked †. The machine-readable form is the snapshot contracts/ttt-host-0.1.json, regenerated and compared by test.

Host name ttt. Accepts language >=0.1.0, <0.2.0 and IR >=0.1.0, <0.2.0.

5.1 workspace 0.1.2#

Types:

  • ChartRef — handle. Operations of this contract are unavailable until a terminal window has serviced the session's UI queue (a session without a window — a test, a future headless host — reports why).
  • Chart { chart: ChartRef, symbol: String, timeframe: String, active: Bool }
operation signature effect / permission / execution errors
charts () -> List<Chart> read / workspace.read / ui_thread —
active_chart () -> Option<Chart> read / workspace.read / ui_thread —
set_symbol (ChartRef, String) -> Result<Unit, Error> workspace_write / workspace.write / ui_thread E-HOST-STALE-HANDLE, E-WS-UNKNOWN-SYMBOL
set_timeframe (ChartRef, String) -> Result<Unit, Error> workspace_write / workspace.write / ui_thread E-HOST-STALE-HANDLE, E-WS-BAD-TIMEFRAME
symbols † () -> List<String> read / workspace.read / ui_thread —
indicators † (ChartRef) -> Result<List<String>, Error> read / workspace.read / ui_thread E-HOST-STALE-HANDLE
set_indicator † (ChartRef, String, Bool) -> Result<Unit, Error> workspace_write / workspace.write / ui_thread E-HOST-STALE-HANDLE, E-WS-UNKNOWN-INDICATOR

| chart ‡ | (ChartRef) -> Result<Chart, Error> | read / workspace.read / ui_thread | E-HOST-STALE-HANDLE | | open_chart ‡ | (String, String) -> Result<Chart, Error> | workspace_write / workspace.write / ui_thread | E-WS-UNKNOWN-SYMBOL, E-WS-BAD-TIMEFRAME | | focus_chart ‡ | (ChartRef) -> Result<Unit, Error> | workspace_write / workspace.write / ui_thread | E-HOST-STALE-HANDLE | | panel_kinds ‡ | () -> List<String> | read / workspace.read / ui_thread | — | | panels ‡ | () -> List<Panel> | read / workspace.read / ui_thread | — | | open_panel ‡ | (String) -> Result<Panel, Error> | workspace_write / workspace.write / ui_thread | E-WS-UNKNOWN-PANEL | | focus_panel ‡ | (PanelRef) -> Result<Unit, Error> | workspace_write / workspace.write / ui_thread | E-HOST-STALE-HANDLE | | close_chart ‡‡ | (ChartRef) -> Result<Unit, Error> | workspace_write / workspace.write / ui_thread | E-HOST-STALE-HANDLE, E-WS-LAST-CHART | | close_panel ‡‡ | (PanelRef) -> Result<Unit, Error> | workspace_write / workspace.write / ui_thread | E-HOST-STALE-HANDLE, E-WS-LAST-CHART |

Types new in 0.1.2: PanelRef (handle) and Panel { panel: PanelRef, kind: String, title: String, focused: Bool, chart: Option<ChartRef> }.

Charts and panels are the application's own (2026-10-10). A panel is a pane of the terminal's workspace. open_chart checks the symbol (one the terminal offers) and the timeframe first — nothing is created when either is refused — then opens the chart exactly as the terminal's own "new chart" does (terminal_app::t0::spawn_chart, the one function both use): a new chart, a pane placed by the workspace's rule, in focus. panel_kinds is the terminal's own list of panels a person can open (its Workspace window's list, besides charts; today Diagnostics), so a program opens exactly what a person can and no invented kind; open_panel focuses a one-of-a-kind panel that is already open instead of opening a second. A PanelRef is the pane's identity: a pane closed and opened again is a new one, and the old handle is stale. Closing charts or panels is not offered. Every operation is a request the terminal's UI thread answers between frames (ADR-0003); a program never touches the interface directly.

Every operation here can also fail with E-HOST-UI-TIMEOUT when no frame services the request in time. indicators lists the indicators drawn on the chart by their names in the terminal's indicator list, a name once for each of its kind the chart holds (a chart with an EMA 20 and an EMA 50 lists the name twice); set_indicator takes that name or its short name, in any case, and turns it on or off on that chart's own settings: on means the chart has one of that kind (it never adds a further one), off takes every one of that kind off (an unknown name is refused with examples, never ignored).

Timeframe strings are the labels TTT's charts use; the accepted set is listed in the message of E-WS-BAD-TIMEFRAME. A symbol must be one of the terminal's available symbols. Chart identities are TTT's ChartId, never reused within one process; the handle's epoch makes them process-local (audit §2.2).

5.2 market 0.1.3#

Types:

  • Dataset { id: String, symbol: String, quote: Currency, streams: List<String>, first: Option<Timestamp>, last: Option<Timestamp> } — id is the dataset's content root hash, which is what makes a result reproducible on another machine. Never a path.
  • Candle { open_time: Timestamp, close_time: Timestamp, available_at: Timestamp, open: Price, high: Price, low: Price, close: Price, volume: Quantity } — a closed 1-minute candle from the dataset's klines1m stream; close_time is the end of the interval (exclusive) and available_at when the candle became knowable (its close). Prices and volumes are exact: TTT stores ticks of 0.01 and quantities of 1e-8.
operation signature effect / permission / execution errors
datasets (String) -> Result<List<Dataset>, Error> read / market.read / worker E-MKT-CATALOG-UNAVAILABLE
candles (Dataset, Timestamp, Timestamp) -> Result<List<Candle>, Error> read / market.read / worker, cancellable E-MKT-DATASET-NOT-FOUND, E-MKT-NO-STREAM, E-MKT-TOO-MANY, E-HOST-BAD-ARGUMENT
dataset † (String) -> Result<Dataset, Error> read / market.read / worker E-MKT-DATASET-NOT-FOUND, E-MKT-CATALOG-UNAVAILABLE

dataset(id) finds one dataset by its root hash, so a program (or a result) can name exactly the data it used.

datasets(symbol) lists the published datasets for a symbol: from the backend catalogue when the terminal has one (only datasets present on this machine and whose manifest holds the hash the catalogue names), and from the local datasets directories ($XDG_CONFIG_HOME/ttt/datasets and every path in TTT_T0_DATASETS). Every manifest is verified against its own hash before it is listed or read. candles(ds, from, to) returns the candles with from <= available_at <= to, in order, at most 200 000 (more is E-MKT-TOO-MANY, never a truncated list). Since 0.1.3 a range that has room for more than 200 000 one-minute candles (after clamping it to the dataset's own extent) is refused before anything is read. A dataset without the klines1m stream is E-MKT-NO-STREAM. The quote currency is derived from the symbol's suffix (USDT, USDC, BUSD, USD); a symbol with no known suffix is E-MKT-NO-STREAM for prices, with the reason.

chart_candles(symbol, timeframe, limit) -> Result<List<Candle>, Error> (0.1.2, market.read, worker): the latest limit (1 to 5 000) closed candles of a symbol at a timeframe, oldest first -- what a chart shows, and the input of a custom indicator (examples/29_custom_indicator.tz). The newest candle, which may still be forming, is dropped, so a program never reads a bar that can still change: an indicator computed here equals the one computed in a backtest over the same closed bars. The timeframes are the ones the backend keeps candles for (1m 5m 15m 30m 1h 4h 1D, and seconds); another is E-WS-BAD-TIMEFRAME, an unknown symbol E-WS-UNKNOWN-SYMBOL, no backend E-MKT-NO-FEED. It needs no dataset.

coverage(symbol, from, to, stream) -> Result<List<Hole>, Error> (0.1.3, market.read, worker): the holes of one stream of the live capture of a symbol in a range, in time order -- Hole { from: Timestamp, to: Timestamp, reason: String }, reason being tape_disconnected, capture_silent, stream_disconnected or stream_unmonitored (§5.9a). stream is tape, liquidations or spot. An empty list is a range the stream covered. A program that wants to know before it computes asks this; the data reads of §5.10 to §5.13 carry the same holes with their answers.

5.3 features 0.1.4 (signal vocabulary)#

Types: Source { Trades, AggTrades } — which prints a feature reads: per-execution prints or aggregated prints (TTT keeps them distinct on purpose, audit §2.3).

All operations are kind: signal, effect: pure, no permission:

operation signature TTT node
price (Source) -> Signal<Float> Price{source}
volume (Source) -> Signal<Float> Volume{source}
cvd (Source) -> Signal<Float> Cvd{source}
vwap (Source) -> Signal<Float> Vwap{source}
sma (Int) -> Signal<Float> Sma{period} (closed-bar closes)
ema (Int) -> Signal<Float> Ema{period}
bar_close () -> Signal<Float> LastClosedBarClose
bar_delta (Source) -> Signal<Float> BarDelta{source}
bar_imbalance (Source) -> Signal<Float> BarImbalance{source}
inferred_absorption_sell_side (Source, Decimal, Decimal) -> Signal<Bool> InferredAbsorptionIsSellSide{source, min_volume, min_imbalance_bps} — inferred, not observed
vwap_spread (0.1.3) (Source) -> Signal<Float> VwapSpread{source} — the volume-weighted standard deviation of the prices around vwap, in dollars (West's update). A band at k deviations is vwap + k * vwap_spread.
entry_price (0.1.3) () -> Signal<Float> EntryPrice — the open position's average entry price; unknown while none is open.
position_sign (0.1.3) () -> Signal<Float> PositionSign — +1 for an open long, -1 for an open short; unknown while none is open.
entry_vwap / entry_spread (0.1.3) (Source) -> Signal<Float> EntryVwap{source} / EntrySpread{source} — the VWAP and its spread as they were when the entry was decided, held while the position is open, so a target or stop defined from them does not drift. Unknown while none is open.
forming_open / forming_high / forming_low (0.1.4) (Source) -> Signal<Float> FormingBarOpen{source} / FormingBarHigh{source} / FormingBarLow{source} — the open, the highest and the lowest price of the bar still forming. Its close is price(source).
forming_volume / forming_delta (0.1.4) (Source) -> Signal<Float> FormingBarVolume{source} / FormingBarDelta{source} — what has traded in the bar still forming, and its buying minus its selling, in units of the instrument (the unit of an order's size: 0.5 is half a bitcoin).

| bar_open / bar_high / bar_low / bar_volume (0.1.4) | () -> Signal<Float> | LastClosedBarOpen / …High / …Low / …Volume — the rest of the last closed bar, beside bar_close; the volume in units of the instrument. | | highest_high / lowest_low (0.1.4) | (Int, Int) -> Signal<Float> | HighestHigh{period, offset} / LowestLow{period, offset} — the highest high (lowest low) of period closed bars, the newest of them offset bars before the last closed one: offset 0 includes the last closed bar, 1 is the window before it, which is what a break of that range is measured against. Up to 1440 bars; unknown until period + offset have closed, and after a hole in them. A strategy started on a market is given that many bars of history. | | close_ago (0.1.5) | (Int) -> Signal<Float> | CloseAgo{offset} -- the close of the closed bar offset bars before the last closed one (close_ago(0) is bar_close()). With minute_of_day, the price at a moment of the day. Up to 1439 bars back; unknown until offset + 1 bars have closed, and after a hole in them. A negative offset is refused when compiled (looks_ahead): it would be a bar that has not closed, known to a backtest and never live -- the same for highest_high/lowest_low. | | position_size / position_fraction / position_minutes (0.1.4) | () -> Signal<Float> | PositionSize / PositionFraction / PositionMinutes — what the open position still holds (units of the instrument), that as a share of what it was opened with (1 until it is reduced), and the minutes since it opened. Unknown while none is open. | | stop_price / target_price (0.1.4) | () -> Signal<Float> | StopPrice / TargetPrice — where the stop-loss and the take-profit resting on the open position are now. Unknown while there is none, and a comparison with an unknown does not hold. | | flat_minutes (0.1.4) | () -> Signal<Float> | FlatMinutes — minutes since the last position was closed in full, while nothing is held; a very large number for a strategy that has not traded yet. | | minute_of_day (0.1.4) | () -> Signal<Float> | MinuteOfDay — the minute of the UTC day, 0 to 1439. |

The bar still forming is the one-minute bar of the newest print, made of the prints of source so far. It is made by the strategy engine from the prints the strategy is given, never read from what a feed publishes of the venue's bar -- a dataset holds no such thing, and a replay and the live feed publish it each at their own pace -- so it is the same bar in a backtest, in a replay and live, and it changes at each print, at that print's instant. It is unknown, and a rule that reads it does not fire:

  • in a minute the strategy did not see from its start (a part of a bar is another bar) -- the first minute of a run; a strategy started on a market is given the prints from the minute before, so it has it from its first print;
  • in a minute a hole in the prints reaches into;
  • from a bar's close until the next print: nothing is forming.

A print that arrives late, behind the first print of the minute after its own, changes neither bar.

Values are TTT's: prices in units of the quote as binary64, as the strategy engine computes them. Every quantity is in units of the instrument -- what an order's size is written in, so 0.5 is half a bitcoin on BTCUSDT: volume, cvd, bar_delta, forming_volume, forming_delta, and the min_volume given to inferred_absorption_sell_side (corrected in 0.1.9; before, the first three and min_volume were in 10^-8 of the instrument).

5.3a features 0.1.5 to 0.1.8: studies, the footprint, facts, levels and the tape#

Everything here is kind: signal, effect: pure, no permission. Arguments that size what the engine keeps -- minutes, bands, a size -- are fixed when the strategy is built: a literal, or one of the strategy's parameters (never a signal).

Footprint (0.1.7). flow_<figure>(source, minutes, tick) of the last closed bar of minutes (a whole number a day divides into), made of the prints of source on rows of tick dollars, with the chart's own definitions (strategy_core::flow, checked against aggregation): delta, volume, buy_volume, sell_volume, trades, poc, poc_delta, max_level_delta, min_level_delta, delta_at_high, delta_at_low, unfinished_high, unfinished_low (1 or 0), value_area_high, value_area_low (70 %), rows; and flow_stacked_buy / flow_stacked_sell(source, minutes, tick, ratio) (the length of the longest run of adjacent rows whose buying is at least ratio times the selling one row below -- the chart's diagonal imbalance). forming_flow_<figure> reads the bar still forming. A bar not seen from its start, or one a hole reaches into, is unknown.

Facts (0.1.8) -- strategy_core::facts:

operation signature meaning
funding_rate / funding_realized () -> Signal<Float> the funding rate the venue says comes next (an estimate), and the rate actually charged at the last settlement
open_interest / open_interest_change () -> … / (Int) -> … contracts outstanding, and now minus minutes ago (unknown until the stream was watched that long)
liquidated / liquidation_count (Int, LiquidatedSide) -> Signal<Float> volume forced closed / how many, in the last minutes; LiquidatedSide { Both, Longs, Shorts }. Zero only if the stream was watched for the whole window
book_imbalance (Int) -> Signal<Float> (bids - asks) / (bids + asks) over the bands bands nearest the touch
book_wall (BookSide, Int) -> Signal<Float> the price of the nearest band on a side that holds at least a size; BookSide { Bids, Asks }
best_bid / best_ask / spread_ticks () -> Signal<Float> the touch of the last book frame

Where facts are served. A dataset or recording holds funding, settled funding and liquidations; a backtest refuses, in words and before it runs, a dataset that lacks the stream a strategy reads (dataset cannot serve …). No dataset holds the book: the book signals are refused by a backtest. Live they are served from the capture's heat columns (market_feed::HeatColumn: the touch and the bands, a second apart, the same the depth ladder is drawn from -- terminal_app::book_feed): the terminal reads them while a running strategy reads the book, and gives the strategy each as a book frame stamped with the moment it arrived. The book is the one of the instrument asked for, never the merged one in its place. A frame more than ten seconds old is not the book now and the signals read unknown. A replay refuses them: it would not give the same run twice (what the strategy saw would depend on how fast the book arrived), until a replay can hold its clock for the columns. They are the capture's aggregated bands, not the raw book: no queue position can be told from them. Live, funding_rate, open_interest and open_interest_change are served: they are one sampler's reading, which the terminal polls from the backend about once a minute while a running strategy reads them and gives the strategy as the same events a dataset holds (market_model::funding_sample_events, one definition for both), stamped with the moment they arrived -- a strategy running now could not have known them earlier. They are unknown until the first reading, and a change over a window is unknown until the run has watched that window in full (the deployment check says so as a warning). funding_realized, liquidated and liquidation_count are served live from the capture's own rows -- the ones a dataset is imported from: the data engine serves the rows of a window as the same text (/v2/api/reference-rows), and the terminal makes events of them with the importer's own code (storage::import::reference_events, terminal_app::reference_feed), so a live run and a backtest are given the same events and only the source differs. A live run knows a row from the moment it arrives (a few seconds after it happened); a dataset's forced close has no receive time, so a backtest knows it at its own instant.

The gamma (0.1.8, on this branch) -- gamma_flip(days), gamma_call_wall(days), gamma_put_wall(days), gamma_max_pain(days) (prices) and gamma_net(days), from the options listed on the asset of the instrument, made by the gamma ladder's own functions (aggregation::gamma). days is written in the strategy: 0 counts every expiry, n only those that settle within n days of the reading. One event, two sources: the strategy is given MarketEvent::OptionsChain readings, each made by market_model::OptionsSample::of from a chain. A backtest has them from the data engine's history of the chain (/v2/api/options-history, read back before the run in pieces that keep the capture's five minutes: Services::options_history), so the same range gives the same run; at most ten days at a time, and a range with no history is refused in words. Live, the terminal reads the chain every five minutes (terminal_app::options_feed) and gives the same event, known from the moment it arrived. A reading older than twenty minutes is unknown, and a level the chain does not have is unknown, never zero. A replay refuses it for now (it would not give the same run twice).

Levels of the UTC day (0.1.8) -- strategy_core::levels, made of the prints of source: session_open, session_high, session_low, prior_day_high, prior_day_low, prior_day_close (all (Source) -> Signal<Float>) and opening_range_high / opening_range_low(source, minutes). A day counts only if it was seen from its open (the first print within a minute of it): a run that began at noon has half a day, and the level is unknown rather than the afternoon's. A hole in the prints invalidates the days it reaches into. The opening range is unknown until its minutes have passed. Live, only the day in progress is preloaded, so the prior-day levels are unknown until the first UTC midnight the script sees.

The profile of the day (0.1.8) -- strategy_core::profile: session_poc, session_value_area_high, session_value_area_low and prior_day_poc, prior_day_value_area_high, prior_day_value_area_low, each (source, tick) -> Signal<Float>. Made of the prints of source on rows of tick dollars with the chart's own definitions (the footprint's and aggregation::VolumeProfile's: the POC is the highest row among equals, the value area holds 70 % and grows from the POC one row at a time). Same rule as the levels of the day: known only for a day seen from its open, a hole invalidates the days it reaches into, and live only the day in progress is preloaded.

Rolling VWAP and profile (0.1.9) -- strategy_core::rolling: the VWAP and the volume profile of the last days days up to the latest print, a window whose back end moves with its front -- a fixed range volume profile that follows the market -- where the signals above start from an open. rolling_vwap(source, days, bands) is the VWAP plus bands volume-weighted standard deviations (0 the VWAP, -2 the lower second band); rolling_poc, rolling_value_area_high and rolling_value_area_low(source, days, tick) are the chart's own figures of the profile of the window on rows of tick dollars; and rolling_node_below / rolling_node_above(source, days, tick, volume_pct, radius, min_gap_pct, max_gap_pct) give the nearest high-volume node under (over) the price: a row that holds at least volume_pct per cent of the volume of the point of control and is the heaviest within radius rows either side of it (of equals, the lower), no nearer to the price than min_gap_pct per cent of it and no farther than max_gap_pct. A node signal is 0 -- known, not unknown -- where the window has no such node, so a strategy can fall back to a fixed distance with if node > 0 { node } else { .. }. days is a whole number from 1 to 30 and, like tick and the rest, is fixed when the strategy is built. When it is made again. The VWAP and its bands include every print up to the one just seen; the profile and its nodes are worked out when each minute ends (the minute in progress is not in them), which for three days is a part in four thousand. A read is a lookup, so a rule that reads them on every step costs nothing. What it holds. Live, the terminal seeds the window from the recorded footprint of the previous days (one-minute bars, rows of tick; a VWAP from rows of a dollar) when the strategy is started with its session loaded, and the prints of the minute in progress and of the session come as prints, so no print is in it twice; with "start now, values beginning now" it begins empty. In a backtest or a replay it holds what the run has seen: it says nothing for its first half hour and then reads the last min(days, what was seen) days -- a backtest of a short dataset is therefore not the live strategy with a full window, and says so. A minute with no prints adds nothing and nothing is invented for it; a hole is not bridged.

Anchored VWAP (0.1.8) -- strategy_core::anchored: anchored_vwap(source, anchor, bands) -> Signal<Float>, with Anchor { Day, Week, Month } (UTC; the week begins on Monday, the month on the 1st). The volume-weighted average price of the prints of source since the start of the period, plus bands volume-weighted standard deviations (0 is the VWAP; -2 the lower second band). A period counts only if it was seen from its start, so a weekly or monthly anchor is unknown until the first boundary a run sees unless the data starts on one; a hole invalidates the period it reaches into; a new period starts from nothing.

The tape (0.1.8) -- strategy_core::tape: large_buy_volume, large_sell_volume, large_delta and large_count(source, min_qty, minutes): the prints of source of at least min_qty (units of the instrument) in the last minutes (1 to 1440). Unknown until the window was watched in full; a hole in the prints inside the window invalidates it, and leaving the window heals it.

5.16 handlers 0.1.2 (types only, API 0.1.24)#

What the functions a strategy calls on events return (handler bar(5) = on_bar(px, avg);, language spec §9 "Handlers"). No operations: a handler computes and the strategy's engine acts. Types: Side { Buy, Sell }; Intent { Open, Add, Close, Reduce } (Close leaves every position of the book, Reduce takes qty off the oldest first); Order { book: String, side, intent, qty: Quantity, limit: Option<Float>, stop: Option<Float>, take_profit: Option<Float>, stop_loss: Option<Float>, tag: Int, post_only: Bool, immediate: Bool, expire_minutes: Int } (post_only/immediate need a limit; expire_minutes is 0 for never; a stop is a trigger; with a limit too, a stop-limit -- on an entry a breakout, on an exit (Close, Reduce) a limit rests until the market reaches it and a stop is a way out through a price, linked to the position, withdrawn if it is closed first, and post_only/immediate/expire_minutes apply to it; a target and a stop rest on the position the entry opens, a stop entry's included; tag is yours, 0 or more); Protection { book, take_profit: Option<Float>, stop_loss: Option<Float> }; Act { Submit(Order), Protect(Protection), Cancel(Int), Stop(String), Notify(Notice) } (Protect puts or moves them on the open positions of a book; Cancel(tag) withdraws the orders still resting that were sent with that tag; Notify is below). book is main or a book the strategy declared with position. A handler that returns something the host cannot act on (an unknown book, a malformed order) ends the run as StoppedByStrategy with the reason. The program travels inside the strategy definition (StrategyDefinition.script, part of its identity), so a backtest, a replay and the live demo run the same handlers; the engine that runs them is installed with the host (TttHost::new).

Notify (0.1.2). Notice { name: String, text: String } -- Act.Notify(handlers.Notice { name: "above-average", text: "…" }) tells the world something with no order: it never trades and never stops the run. A strategy running live hands each notice to the terminal, which makes it the event strategy_signal (signal is the name, detail the text; the hooks attached to it -- a webhook, a control script -- do the rest, see ../tooling/hooks.md); a backtest has nobody to tell and ignores it. A name is cut to 60 characters and a text to 300, and a handler's notices past eight in one event are not kept (counted, not told). A program compiled before 0.1.2 has no such variant and loads as it did. Example: 73_notify_on_cross.tzs.

ctx (0.1.1). A handler may be given the account's side of the moment by writing ctx among its arguments (handler bar(5) = on_bar(px, ctx, avg);), at the place of a parameter of type handlers.Context: Context { now: Timestamp, balance: Float, equity: Float, realized_pnl: Float, unrealized_pnl: Float, positions: List<Position>, orders: List<Resting> }; Position { book: String, side, qty: Quantity, entry_price: Float, opened_at: Timestamp, stop_loss: Option<Float>, take_profit: Option<Float>, exiting: Bool } (every open position of every book, oldest first; the stop and target are the ones resting on its book); Resting { book: String, side, qty: Quantity, exit: Bool, tag: Int } (every order of the strategy still working; tag is the one the handler sent it with, -1 for an order a rule sent). It is read from the same context the rules read, as of the event that called the handler, so it shows nothing later. The market itself comes only through signals (let …), so the handler stays a function of what the engine computed: a book, a footprint or a gamma level is a signal first. ctx is not a signal and never makes a handler wait; a program that names a signal or a parameter ctx keeps that meaning.

5.17 risk 0.1.4 and portfolio 0.1.0 (types only, API 0.1.21)#

The contracts of the component roles (language spec §12 "Components"). No operations: a component is a function the host calls, and the host binds it to an account. Units are the language's own -- money is Money, sizes Quantity, prices Price, shares and drawdowns Ratio -- and a value the host could not establish is None, never a zero and never the last value seen.

risk. fn authorize([cfg: Config,] intent: risk.Intent, ctx: risk.Context) -> risk.Decision, called for every order that would open exposure (a plain entry, from a strategy or from the trader's hand) before it can fill or reach a venue. Types: Side, Kind { Market, Limit, Stop, StopLimit }, Mode { Backtest, Demo, Real }; Intent { id, account, origin, symbol, side, kind, qty, price, trigger, leverage, post_only, immediate, notional, margin, cost } (the last three are Option<Money>: None when the order cannot be valued); Context { account, mode, symbol, quote, now, sequence, equity, balance, available, used_margin, reserved_margin, unrealized_pnl, day_pnl, peak_equity, drawdown, mark, mark_age, taker_fee, liquidity, positions, orders, gross, net, pending, origins, orders_last_minute, added_last_minute, halted } -- positions, orders (every order still working, with what it holds in reserve) and origins (exposure by strategy) are lists; liquidity is always None (the account holds no order-book depth); sequence is a stamp of the state that only grows; Decision { Allow, Reject(Refusal), Reduce(Reduction), Reevaluate(Reason) } with Reason { code, message, limit, observed }, Refusal { reasons }, Reduction { max_qty, reasons }. The host's answer to each: Allow still faces the host's own limits (the system invariants, the margin that must cover the exposure) and a reservation of the exposure; Reduce must be more than zero and less than asked and the reduced order is judged again from the start; Reevaluate makes the host rebuild the context and ask once more, and twice is a refusal; a timeout, a crash, an invalid answer, a missing policy and stale data are refusals that are reported as failures of the protection. Exits (an order linked to a position) and liquidations are never judged by the policy and a halt does not block them; a strategy may only take risk off from what it opened.

risk 0.1.4 -- the group. Context.group_others (Money) is what the other accounts of the group this account belongs to hold together, gross (zero for an account on its own), and Context.group_max_gross (Option<Money>) is the group's limit on all of them together. The host refuses an entry that would pass it by itself (group_max_gross_exposure); a policy that reads them can reduce an entry to the group's room instead of having it refused.

risk 0.1.3 -- liquidation and slippage. Intent.liquidation and SizeRequest.liquidation are Option<Price>: where the entry would be liquidated with isolated margin at its leverage (None for cross margin, or when its price is not known). A stop on the far side of it -- below it for a long, above it for a short -- is never reached, so the host itself refuses such an entry (system_stop_beyond_liquidation, a limit no policy lifts); a policy that sizes by risk can use the price to keep a stop within reach. Context.slippage is a Ratio: what the account's execution model charges a market order, as a fraction of the price (a tick count taken at the mark) -- an estimate for a size that counts the cost of getting in and out together with Context.taker_fee. A policy compiled against an older risk keeps running: the host fills only the fields its types have.

risk 0.1.2 -- the day's history. Context.history and every Exposure.history (the origin's own) are a History { trades_today: Int, loss_streak: Int, last_loss: Option<Timestamp>, realized_today: Money }: the positions closed today (UTC day of the account's clock; a partial close counts as one), the run of consecutive losing closes counted back from the latest and only today's (a win, or no close today, makes it 0), when the latest loss was, and the day's result, net of fees and funding. A policy is a function of what it is given and has no memory of its own, so this is what lets it stop after losses, cool off after one or limit the trades of a day (example 66). An origin that closed something today appears in origins even if it holds nothing now. A policy compiled against 0.1.0 or 0.1.1 keeps running: the host fills only the fields its types have.

risk 0.1.1 -- sizing. A risk policy may also define fn size([cfg: Config,] request: risk.SizeRequest, ctx: risk.Context) -> risk.Sizing, with the same Config as authorize. The host calls it, before authorize, for an entry a strategy placed without a size (Sizing.ByRisk). SizeRequest { id, account, origin, symbol, side, kind, price, trigger, entry, stop, target, leverage, post_only, immediate } (entry is the price the entry is expected to trade near -- its own, or the mark; stop and target are the ones the strategy put on the entry, None when it put none); Sizing { Size(Sized), Decline(Refusal) } with Sized { qty, reasons }. The host's answer to each: Size must be more than zero (rounded down to the venue's lot) and the sized order is then judged from the start by authorize and the host's own limits, so a size is a proposal and can still be reduced or refused; Decline is no order and not an error (the strategy goes on); a timeout, a crash, a size of zero or a policy that defines no fn size are refusals reported as failures of the protection. Every sizing question and its answer are in the audit record of the order and can be replayed. size is a reserved name in a risk component: it has the signature above or the program is refused (E-COMP-ENTRY).

portfolio. fn allocate([cfg: Config,] snapshot: portfolio.Snapshot, limits: portfolio.Constraints) -> portfolio.Decision, called when the host rebalances. Types: Sleeve { name, gross, net, pnl, positions, orders, returns: List<Float>, budget } (the share of the account run by one strategy; returns is empty when the host has not tracked them); Snapshot { account, quote, now, sequence, equity, gross, net, pending, sleeves }; Limit { sleeve, min_gross, max_gross }; Constraints { max_gross, max_net, max_turnover, min_trade, limits }; Target { sleeve, gross, why }; Allocation { targets }; Decision { Targets(Allocation), Hold(String), Infeasible(String) }. A decision states the desired allocation and grants no permission to trade: the host clamps it to the constraints, turns the difference into proposals, and every order that follows goes through the risk firewall.

5.18 indicator 0.1.0 (types only, API 0.1.23)#

The contract of the component role indicator (file .tzi). fn compute([cfg: Config,] input: indicator.Input) -> indicator.Output. Input { symbol, now, price, previous: Option<Float>, bars }: the last price, the price at the previous look (None at the first, so nothing is seen to cross) and the closed candles, oldest first, at most the last 500 (Bar { time, open, high, low, close, volume }; the candle still forming is never given). Output { values, emits }: values is the line to show (empty draws nothing) and emits the signals given now (Emit { name, value, why }, at most 16 per look; a name is required). The terminal looks at each indicator on a thread of its own about once a second, on the market in hand, with candles read from the backend every 20 seconds; a look that fails (a timeout, an invalid answer) gives no signal and five in a row switch the indicator off with the reason. Each Emit becomes the indicator_signal event (data: indicator, signal, value, why; symbol and price), at most 30 a minute per indicator; the hooks attached to it do the rest. An indicator keeps no state between calls and has no operations: it cannot read the market, run a script or touch an order. The terminal draws its values on the charts of the instrument and timeframe it reads (one drawing of its own that every look replaces; a switch per indicator).

5.19 script 0.1.0 (API 0.1.23)#

script.run(path: String, args: List<String>) -> Result<Unit, Error>, permission script.run, effect workspace.write. Starts a script of the scripts folder -- a .tzc control program or a plain .tz one -- and returns at once; it does not wait and its end is not told to the caller. Err means it was not started: E-SCRIPT-NOT-FOUND (not in the scripts folder), E-SCRIPT-KIND (a risk, portfolio, indicator or strategy file cannot be started), E-SCRIPT-DEPTH (past three deep), E-SCRIPT-NOT-STARTED (the terminal's limits: 30 starts a minute, 16 waiting), E-HOST-BAD-ARGUMENT (more than 16 arguments or one past 200 characters; caller, depth and grants are the terminal's own). The started script holds at most the permissions of the one that started it, cut by what a hook may hold, and reads its arguments in event.current() (kind script_run, data the key=value words plus caller and depth).

5.20 manager 0.1.0 (types only, API 0.1.25)#

The contract of the component role manager (file .tzm), the script of a playbook. fn decide([cfg: Config,] input: manager.Input) -> manager.Decision, called when a candle of the playbook's timeframe closes -- never on a tick -- in a backtest and live alike. Input { symbol, now, price, previous: Option<Decision>, bars, members }: the closed candles up to the one that just closed (Bar { time, open, high, low, close, volume }, at most 500), the price then, the decision it gave last time (None at the first call and after a gap in the candles; a manager keeps no other memory) and the members it can name (Member { name, kind }, kind one of strategy, risk, portfolio). Decision { active, risk, portfolio, state, why, values }: active is the complete list of strategies that stay on (every other strategy is suspended: no new entries, what it holds keeps its stops and targets), risk and portfolio name the member in force (None leaves the account's own), state labels the situation (1 to 60 characters; it appears on the screen, in the log and in the event playbook_changed), why is a reason (cut at 300 characters) and values an optional line to draw, one number per candle ending at the last. A name that is not a member of the right kind, a duplicate, more than 24 names, a state outside 1-60 characters, or a line of more than 500 numbers is an invalid answer: the decision is refused whole, the host keeps what was on and counts the failure, and three in a row apply the playbook's fault policy (by default every strategy is suspended). The manager reads the market only -- no account, no profit of a member -- so that a backtest and the live terminal give the same decisions; it has no operations and cannot call the host. In a backtest it runs on fuel alone, with no wall clock, so a run is reproducible.

5.4 backtest 0.1.4#

Types:

  • Slippage { None, Fraction(Ratio), Ticks(Int) } — adverse slippage on market fills; Fraction(bps(1)) is 1 bp.
  • MarketFill { NextPrint, Immediate }, LimitFill { TradeThrough, AtTouch }.
  • Execution { maker_fee: Ratio, taker_fee: Ratio, slippage: Slippage, latency: Duration, market_fill: MarketFill, limit_fill: LimitFill } — every field required: fees, slippage, latency and fill rules change results materially, so none is a hidden default.
  • Spec { strategy: Strategy, dataset: market.Dataset, start: Option<Timestamp>, end: Option<Timestamp>, starting_balance: Money, execution: Execution } — None bounds mean the dataset's own extent (stated, not defaulted: the field must still be written).
  • Status { Completed, StoppedByUser, StoppedByStrategy, Failed } — TTT's RunStatus; every Failed* status maps to Failed, and Report.detail says which (FailedDataset: …, FailedMissingFeature: …).
  • Report { run_id: String, status: Status, detail: String, events: Int, dataset_id: String, strategy_identity: String, execution_prints: String, trades: Int, realized_pnl: Money, fees_paid: Money, unrealized_pnl: Money, funding: Option<Money>, max_drawdown: Money, final_equity: Money, open_positions_at_end: Int, orders_submitted: Int, orders_filled: Int, orders_rejected: Int, orders_working: Int, journal: Option<String> } — funding is None when the dataset carried no settled funding: it was not observed, which is not the same as zero (TTT's Measured).
operation signature effect / permission / execution errors
run (Spec) -> Result<Report, Error> compute / backtest.run / worker, cancellable, progress E-BT-INVALID-STRATEGY, E-BT-UNSUPPORTED, E-BT-CURRENCY, E-BT-RANGE, E-MKT-DATASET-NOT-FOUND
submit † (Spec) -> Result<Job, Error> compute / backtest.run / worker as run, and E-BT-CAPACITY
status † (Job) -> Result<JobStatus, Error> read / backtest.run / inline E-HOST-STALE-HANDLE
wait † (Job) -> Result<Report, Error> read / backtest.run / worker, cancellable, progress E-HOST-STALE-HANDLE
cancel † (Job) -> Result<Unit, Error> read / backtest.run / inline (it only stops what this execution started) E-HOST-STALE-HANDLE
trades † (String) -> Result<List<Trade>, Error> read / backtest.run / worker E-BT-UNKNOWN-RUN, E-BT-NO-JOURNAL, E-BT-TOO-MANY (past 200 000 trades: never a cut list)

Types new in 0.1.1:

  • Job — handle to a backtest this execution submitted; in another execution it is E-HOST-STALE-HANDLE.
  • JobState { Running, Cancelling, Completed, StoppedByUser, StoppedByStrategy, Failed }.
  • JobStatus { run_id: String, state: JobState, phase: String, fraction: Option<Ratio>, events: Int, fills: Int, elapsed: Duration } — fraction is None when the dataset's event total is unknown: never invented.
  • Trade { position: Int, side: String, opened_at: Timestamp, closed_at: Timestamp, entry_price: Price, exit_price: Price, qty: Quantity, realized_pnl: Money, reason: String } — a closed position from the run's journal; a partial close is one trade.

Jobs: submit starts the same run run would and returns at once; run is submit + wait. A job runs on a host thread. Three ways it can stop, all ending StoppedByUser: the program calls cancel(job); the person presses Stop job in the terminal (the program keeps running and its wait returns the report saying so); or the execution ends or is stopped (every job it still has running is stopped with it, so nothing outlives the program that started it). cancel on a finished job changes nothing. wait forwards the job's progress as the execution's progress; cancelling the execution while it waits returns E-HOST-CANCELLED and stops the job.

Capacity (0.1.2, ADR-0010 §4). run and submit start a job only if a slot is free at all three levels: per program execution (4), per T0 session (6), per terminal process (cores − 1, from 1 to 8, shared by every session). Otherwise they return Err with E-BT-CAPACITY, whose message names the full level; nothing is queued and nothing runs past a limit. A job's slot is returned whenever it ends — completed, failed, stopped by the program, by the person, or with its execution, the engine failing included — and before its outcome is visible, so wait then submit is never refused because of the job just waited for. A job is Running, Cancelling (a stop was asked; the engine stops at its next event), then one of Completed, StoppedByUser, StoppedByStrategy, Failed; a refused submit is not a job. The terminal lists the last 50 finished jobs; trades reads the last 1 000 runs.

Large runs (0.1.3). trades reads the run's journal as a stream (storage::run::RunJournal::visit) — never the whole journal into memory — and stops as soon as it passes 200 000 trades, answering E-BT-TOO-MANY without having built them. trades_page(run_id, from, limit) -> Result<TradePage, Error> ‡ reads up to limit (1 to 10 000) trades starting at position from, stopping one trade past the page; TradePage { trades: List<Trade>, from: Int, next: Option<Int> }, where next is where the following page starts and None means this page ends the run. Both look at cancellation every 4 096 journal events.

Groups (0.1.4, API 0.1.19). run_group(spec) -> Result<GroupReport, Error> runs one strategy over two to eight datasets of different instruments (all quoted in the same currency) as a group: each instrument has an account of its own, protected as the terminal's risk bindings say (the backtest entry, else demo), and they run on one clock -- over the days every dataset holds, never letting one run ahead of another by a millisecond -- under the limits and the portfolio controller of the group entry of the bindings (max_gross_exposure, max_daily_loss, max_drawdown_pct on the sum of the accounts; the controller names its strategies strategy@SYMBOL). GroupSpec { strategy, datasets: List<Dataset>, start, end, starting_balance (of each account), execution }; GroupReport { status, detail, members: List<Report>, common_days, barriers, rebalances, halted_barriers }, each member a Report like a run of its own (and its trades read by trades(run_id)). Completed only if every member did; a member that fails stops the group, and detail says which. Err: E-BT-GROUP (fewer than two or more than eight datasets, two of one instrument, no day in common, a hole, accounts with no risk policy bound: a group is of protected accounts), E-BT-RISK (the limits of the group cannot be had), E-BT-CAPACITY (each member counts as a backtest at all three levels, and the group is refused whole, holding none), and the errors of run. The program waits for the whole group; cancelling the execution stops every member. The result does not depend on how fast each member computes.

Sweeping the risk policy (0.1.5, API 0.1.22). run_with_risk(spec, risk_config: String) -> Result<Report, Error> is run under the risk policy the bindings bind to the backtest entry (else demo), but with its configuration replaced by risk_config (JSON, the shape of that policy's Config): the same script, pinned by the same hash, with other numbers. Run it once per value to see how a parameter (the share to risk per trade, a limit on an order) changes the result; example 69 does. submit_with_risk starts the run and returns a Job like submit. The bindings file is not changed. Err E-BT-RISK: the text is not JSON, the configuration does not fit the policy, or the bindings bind no policy to give other numbers to; and the errors of run. A sweep is as many backtests as values and each counts against the limits.

trades(run_id) reads only runs this terminal session started (the journal under the session's runs directory); any other id, including a path or the id of a run started elsewhere, is E-BT-UNKNOWN-RUN. A program cannot use trades to read arbitrary files. A run without a journal is E-BT-NO-JOURNAL.

Semantics (TTT):

  • The strategy is translated to strategy_graph::StrategyDefinition, validated by TTT (validate, check_dataset against the dataset's streams) and executed by BacktestRunner (deterministic, tape-driven fills per the given Execution).
  • Err means the run could not start (invalid strategy, dataset not found, currency mismatch). A run that started and then failed or was cancelled is Ok(report) with status saying so — TTT's rule that a failed run is a run, not a missing one.
  • starting_balance must be in the dataset's quote currency (E-BT-CURRENCY). It is converted to TTT's f64 accounting; money in the report is TTT's f64 value converted to Decimal by its shortest round-trip decimal form, in the dataset's quote currency.
  • run_id is t0-<execution>-<n>; the journal is written to the session's runs directory (TTT: <temp>/ttt-runs/t0/) and its path returned (None if the session has none).
  • The translation is checked by test: TTT's reference graph strategy_graph::examples::sma_vwap_cross, written in .tz and run through backtest.run, has the same strategy_identity as the graph built directly and produces a byte-identical journal after the header (trading-terminal/crates/t0-host/tests/integration.rs).
  • Progress (phase, events consumed/total, simulated time, orders, fills) is published to the session while the run is in flight; cancelling the execution makes the run end StoppedByUser.
  • max_drawdown is TTT's realized-equity drawdown (SessionAnalytics).

5.5 research 0.1.2 †#

TTT's research API (research_api::market_state_at): what a strategy could have known at an instant, computed by the same features the strategy engine uses. Available when datasets are.

  • Reading { feature: String, value: Option<Float>, unit: String, available_at: Option<Timestamp>, unknown: Option<String>, quality: String } — exactly one of value and unknown is set; unknown is never zero (unknown says why: warmup with the bars needed and held, no data, invalidated by a reported gap, not a single number). available_at is never after as_of.
  • State { dataset_id: String, symbol: String, as_of: Timestamp, events_applied: Int, readings: List<Reading>, gaps: Int }.
operation signature effect / permission / execution errors
market_state (market.Dataset, Timestamp, List<String>) -> Result<State, Error> read / research.submit / worker E-RS-FEATURE, E-RS-FAILED, E-MKT-DATASET-NOT-FOUND

Feature names: price(trades|agg_trades), session_volume(…), cvd(…), vwap(…), bar_flow(…), last_closed_bar, sma(N), ema(N); an unknown one, or more than 64 in one call, is E-RS-FEATURE with the reason. Cancelling the execution stops the replay (it looks every 1 024 events, research_api::market_state_at_until) and the call fails E-HOST-CANCELLED; opening and verifying the dataset before the replay is not interruptible. Values are Float (statistics computed by TTT in f64); a non-finite one is E-HOST-FAILED.

5.6 capabilities 0.1.0 ‡#

What the terminal offers programs, as it is now. Needs no permission: it describes and grants nothing.

  • Operation { contract: String, operation: String, version: String, permission: Option<String>, available: Bool, why: Option<String>, granted: Bool } — available is whether this terminal can serve it now (with why when not); granted whether this session has granted its permission (true when it needs none). Available is not authorized.
operation signature effect / permission / execution errors
operations () -> List<Operation> read / none / inline —

Built from the same manifest the compiler, completion, t0 manifest and the editor read, and from the session's live grants.

5.7 event 0.1.0 ‡‡‡#

What set this execution off. Needs no permission: it describes, and grants nothing. A program the person starts by hand reads kind = "manual"; one a script hook or a trigger line starts (../tooling/hooks.md) reads the event that did.

  • Event { kind: String, source: String, symbol: Option<String>, timeframe: Option<String>, price: Option<Price>, time: Timestamp, data: List<String> } — kind is the event's stable name (symbol_changed, app_start, drawing_trigger, manual …); source says what it was attached to (hook-3, drawing-0, manual); data is key=value lines specific to the kind (from=BTCUSDT).
operation signature effect / permission / execution errors
current () -> Event read / none / inline —

The event belongs to the execution: it is set before the program starts and forgotten when it ends.

5.8 plot 0.1.0 ‡‡‡‡#

A program draws on a chart. It draws with the chart's own drawings (a level is a horizontal line, a zone a rectangle, a line a polyline, a marker a note), so they look and behave like anything a person draws, and a person can select, restyle or delete them. Each carries the name of the program (owner): clear(chart, owner) removes that owner's drawings and nobody else's, so a program replaces its own drawing on each run and never touches a person's. They are locked against an accidental drag. At most 500 drawings per owner per chart (E-PLOT-TOO-MANY), at most 5 000 points in a line.

A color is #RRGGBB or green, red, blue, yellow, orange, purple, white, gray, cyan; an empty one is the chart's default (E-PLOT-BAD-COLOR otherwise). Prices are placed to the hundredth, the chart's own unit.

  • Point { time: Timestamp, price: Price }
operation signature effect / permission / execution errors
clear (ChartRef, owner: String) -> Result<Int, Error> workspace_write / workspace.write / ui_thread E-HOST-STALE-HANDLE
level (ChartRef, owner, price: Price, label: String, color: String) -> Result<Unit, Error> same + E-PLOT-TOO-MANY, E-PLOT-BAD-COLOR, E-HOST-BAD-ARGUMENT
zone (ChartRef, owner, from: Timestamp, to: Timestamp, low: Price, high: Price, label, color) -> Result<Unit, Error> same same
line (ChartRef, owner, points: List<Point>, label, color) -> Result<Unit, Error> same same
marker (ChartRef, owner, at: Timestamp, price: Price, text, color) -> Result<Unit, Error> same same

5.9 alert 0.1.0 ‡‡‡‡#

A program raises an alert. Messages are at most 1 900 characters (Discord's limit is 2 000).

operation signature effect / permission / execution errors
notify (title: String, message: String) -> Result<Unit, Error> workspace_write / workspace.write / ui_thread E-HOST-BAD-ARGUMENT
webhook (kind: String, url: String, message: String) -> Result<Unit, Error> external / external.access / worker E-ALERT-BAD-URL, E-ALERT-RATE, E-ALERT-FAILED, E-HOST-BAD-ARGUMENT
  • notify sounds the terminal's alert chime, speaks the title when the person has voice on, and counts one unread alert.
  • webhook posts. kind is discord (https://discord.com/api/webhooks/<id>/<token>; the message cannot mention @everyone or a role), telegram (https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>) or json (any https address; it receives {"text": …, "source": "ttt"}). What a script can reach is narrow: https only, no credentials in the address, nothing on this machine or a private network (including a name that resolves to one), no redirects, a 10 s timeout. At most one send a second and 20 a minute per address (E-ALERT-RATE). An error never repeats the address, which carries the token.

market.chart_candles (§5.2) and these are served by what the application lends the host: without a market feed the former is E-CAP-UNAVAILABLE, the manifest says so, and capabilities.operations shows it.

5.9a Reading the market: unknown is never a number, and nothing is read from the future#

Everything in §5.10 to §5.13 (and market.coverage) follows the same rules, enforced in one place (t0-host/src/data.rs, coverage.rs) and tested against a capture with holes in it (t0-host/tests/market_data.rs).

1. A hole is said, never filled. Every series comes with the stretches the capture has nothing for (market.Hole { from, to, reason }), and what falls in one is None / known: false:

what when nothing happened when nobody was recording
a liquidation bucket known: true, longs: Some(0) known: false, longs: None -- even if part of it was recorded
open interest at an instant the sampler's last word, if it is under 3 min old None, with unknown saying why; never the last value carried forward
a footprint bar known: true, no cells known: false
the options walls from a chain read in the 15 min before the instant None, with unknown; never the last walls seen
the book at an instant the column recorded in the 10 s before it known: false; in a history, an unknown step, never the previous one repeated
a value the venue did not publish (open interest, mark, a funding rate) -- None: the capture's zero for "not given" is never reported as a zero
spot, on a deployment that does not record it -- the call fails with E-DATA-NOT-CAPTURED, instead of answering with a market in which nobody traded

2. What a hole is known from. Four kinds of evidence, and none is assumed where it is absent:

  • tape_disconnected -- the capture was up and not connected to the trade stream. Recorded by the daemon (ttt.capture_gaps).
  • stream_disconnected -- the socket of a stream of its own (liquidations, spot) was away. Recorded by the daemon per stream (ttt.capture_gaps, reason = the stream's name; the tape's rows keep the empty reason, so what marks candles and repairs days never sees them). With it a liquidation bucket is judged by its socket, and the tape's gaps are not used for it.
  • stream_unmonitored -- the daemon was not yet recording that stream's gaps: a stretch before its earliest row (all of the range, if it never wrote one) has none on record, which is not the same as none, and is unknown. The earliest row is the start of recording: the daemon writes one for the moment between starting and being connected.
  • capture_silent -- the capture's once-a-minute sampler (funding, open interest) wrote nothing for more than three minutes: it was not running. Read from the samples themselves, because a process that is down writes no gap. (Measured on the live capture over 14 days to 2026-10-13: the sampler's median gap is 60.3 s and its 99th percentile 68 s; there were 61 silences over three minutes, one of twelve hours -- and the gap table held 26 gaps adding up to 5.4 hours. The rule finds what the table cannot.)

A bar of the tape that overlaps a capture_silent stretch is unknown only if it is empty: trades inside one mean the tape was repaired from the venue's own record afterwards, and are kept. A liquidation or spot bucket that overlaps a hole is unknown.

When the backend cannot say a socket's own gaps (an engine older than the parameter, or a source that does not keep them) the answer is the earlier one: the sampler's silence and the tape's gaps stand in, and a drop of only that socket is not detected. The feed says so (Ok(None)), nothing is guessed, and the answer carries no stream_* hole. market.coverage(symbol, from, to, stream) shows what a stream is judged by (tape, liquidations, spot).

3. No lookahead. A read returns nothing after the to or the instant asked about, and only buckets and bars that had closed by then (on a grid aligned to the epoch). "Now" is the market's clock: in a replay it is the instant being replayed, so a program run during a replay reads what was knowable then. event.current().time of a program started by hand is that same clock.

4. Bounded. A range is at most 31 days (E-DATA-RANGE); a dense series at most 5 000 buckets, a footprint 2 000 bars and 200 000 cells, the tape 10 000 prints, a book history 600 columns of 50 bands a side (E-DATA-TOO-MANY: ask for a shorter range or a wider step, never a truncated answer). Without a market feed the operations are E-CAP-UNAVAILABLE at compile time.

All are read / market.read / worker. Sizes are in the base asset, prices in the symbol's quote currency (options strikes in USD).

5.10 derivs 0.1.0#

Of the same venue and instrument as the symbol's trades (DATA_CONTRACT.md, rule 6).

  • Sample { time, funding_rate: Option<Ratio>, next_funding: Option<Timestamp>, open_interest: Option<Quantity>, mark: Option<Price>, index: Option<Price> }
  • Series { samples: List<Sample>, holes: List<market.Hole> }
  • Reading { as_of, sample_time: Option<Timestamp>, funding_rate, next_funding, open_interest, mark, index, unknown: Option<String> }
  • Level { price, longs: Quantity, shorts: Quantity }, Bucket { time, known: Bool, longs: Option<Quantity>, shorts: Option<Quantity>, levels: List<Level> }, Liquidations { buckets, holes }
operation signature notes
funding (symbol, from, to) -> Result<Series, Error> what was really sampled, oldest first; the holes are where the sampler was silent
at (symbol, time) -> Result<Reading, Error> the last sample at or before time, if under 3 min old; never one after it
liquidations (symbol, from, to, bucket: Duration, tick: Price) -> Result<Liquidations, Error> bucket 1 min to 1 h; one row per closed bucket; longs are longs that were closed (forced sells)

The funding rate is the venue's estimate for the interval (a signal); what a position pays is the settled rate, which a backtest reads from its dataset.

5.11 flow 0.1.0#

  • Cell { price, buy: Quantity, sell: Quantity, buy_trades: Int, sell_trades: Int, max_buy: Quantity, max_sell: Quantity } -- buy is volume that lifted the offer; max_buy the largest single aggressive buy in the band.
  • Bar { open_time, known: Bool, cells: List<Cell> }, Footprint { bars, holes }
  • Print { time, price, qty, side: String } (buy / sell: the aggressor's), Prints { prints, truncated: Bool, holes }
  • SpotBar { open_time, known: Bool, buy: Option<Quantity>, sell: Option<Quantity> }, Spot { bars, holes }
operation signature notes
footprint (symbol, timeframe, from, to, tick: Price) -> Result<Footprint, Error> every closed bar of the range, cells in bands of tick
prints (symbol, from, to, limit: Int) -> Result<Prints, Error> the newest limit (1 to 10 000) of the range, oldest first; truncated when older ones were left out
spot (symbol, timeframe, from, to) -> Result<Spot, Error> the spot market beside the perpetual; E-DATA-NOT-CAPTURED where spot is not recorded

5.12 options 0.1.0#

Dealer gamma of the listed options of an asset (BTC, ETH), with the definitions the terminal's gamma ladder draws (aggregation::gamma), and an option's time to expiry measured from when the chain was read.

  • Strike { strike: Price, call_gamma: Float, put_gamma: Float } -- both magnitudes, in quote currency per 1 % move; under the dealer convention (long calls, short puts) the net is call_gamma - put_gamma.
  • Gamma { currency, as_of, read_at: Option<Timestamp>, index: Option<Price>, strikes, call_wall, put_wall, flip, max_pain: Option<Price>, unpriced: Int, unknown: Option<String> } -- unpriced options had no implied volatility and are left out: the totals are short by them, and the number says so.
operation signature notes
gamma (currency) -> Result<Gamma, Error> the chain as it is now
gamma_at (currency, time) -> Result<Gamma, Error> the chain the capture read at or before time (it reads one every 5 min)

5.13 book 0.1.0#

The resting book as the capture recorded it, a column a second, in price bands -- the history behind the heatmap.

  • Level { price, size: Quantity }, Snapshot { time, known: Bool, read_at: Option<Timestamp>, best_bid, best_ask, band: Option<Price>, bids, asks: List<Level>, added, removed: Option<Quantity> } -- band is how wide a band is (a level's price is its floor: a trade at p hit the band whose price is at most p and above p - band; the capture sizes it from the instrument's price); the levels largest bands a side (the walls), by price ascending; added / removed are one-way totals of size that arrived at and left the near book since the previous column.
  • History { columns: List<Snapshot>, missing: Int }
operation signature notes
at (symbol, time, levels: Int) -> Result<Snapshot, Error> levels 1 to 50
history (symbol, from, to, step: Duration, levels: Int) -> Result<History, Error> step 1 s or more; one snapshot per step, every one

The book that was asked for, or an error. The data engine builds the books it is told to; where it builds only the merged one (BTCUSDT.X), book.at("BTCUSDT", …) fails with E-DATA-NOT-CAPTURED and names the merged symbol -- it does not answer with another instrument's book in the venue's name (DATA_CONTRACT.md, rule 1). Ask for BTCUSDT.X to read the merged book.

5.14 live 0.1.0#

A program that watches. A monitor is a script started once (Script Hooks -> "Always on (monitor)") that loops:

while true {
    let wake = live.wait(30s)?;          // rests here
    ... read what is new since its own cursor, update its state, draw, alert ...
}
  • Wake { reason: String, now: Timestamp } -- reason is bar (a base bar of the market closed) or timer (the time asked for passed first); now is the market's clock at the wake.
operation signature notes
wait (max: Duration) -> Result<Wake, Error> market.read. Rests until a bar closes that the program has not been woken for, or max passes (held to 1 s ... 5 min). Ends at once, as cancelled, if the program is stopped.
now () -> Timestamp the wall clock, or the instant being replayed during a replay

Beats are merged, never lost. Several bars closing while the program computes are one wake; the program asks the market what is new since its own cursor (flow.footprint(symbol, "1m", cursor, wake.now, …)), so it does not matter how many wakes it slept through. The cursor advances only to the close of the last whole bar: the bar still forming is read when it has closed.

Limits of a monitor. No wall-clock limit; Limits::renew_on = "live.wait" renews steps, host calls and printed output at every rest (an activation that runs past them still fails: the renewal is at the rest, not a bigger allowance); memory is a limit for its whole life. The hook rules apply otherwise: the permission cap, Live / Replay / Both (a live-only monitor is stopped when a replay starts and comes back after it), at most four at once beside the four hooks, a failure waits 30 s before the next start, and five in a row switch it off. A script that returns on its own is finished until it is switched on again.

5.15 trade 0.1.0#

A program hands the terminal a strategy to run. Where it runs is decided by where the person started the program, never by the program: on a market replay, on the live market in real time, or -- added in the Backtests window -- as backtests over the datasets the person picks. The same strategy, with the same rules, in all three.

  • Run { name: String, target: String, state: String } -- target is replay, demo, real or datasets; state is pending until the person has answered the terminal's form, inserted for a strategy handed to the Backtests window.
operation signature notes
run (strategy: Strategy) -> Result<Run, Error> orders.submit. Returns at once. A program may call it several times: each call is one more strategy, and any number of strategies run side by side.

On a market (replay or live): what the program decides and what it does not. The program says which strategy. The person says the rest in a form, one form per strategy: which account (the replay's practice account, the demo account of the live market -- also while a replay is on screen -- or the real account, which is listed and refused until it has been switched on), what to load and when to close. A strategy that reads something built up over the session (the VWAP, the cumulative delta, an average) is offered Load and start: the session so far is read from the data engine and calculated on this machine (ComputeSite::Local; the servers are another site, later) and folded in before the strategy starts, so the VWAP is the day's and not a VWAP that began when the script did. No dataset is involved; what was not recorded is declared as a gap, never filled in.

Its data is kept whole, and nothing is waited for.

  • Live. The terminal keeps the last minutes of prints its own feed delivered. A strategy that starts is loaded from the data engine only up to where those begin -- a stretch the engine already has whole -- and they are folded in after it as history (the strategy is not called on them), so it starts with the session complete to the instant and with no wait. A terminal that has only just connected reads the newest seconds from what the capture has written (a second or two behind the market), never from a minute the engine has yet to call settled. There is no hole on the live market unless the connection drops; when it does, what was missed is read the same way on the first print after it, in a second or two.
  • When it decides. On the live market a strategy is given each event as it arrives, on the thread that received it and before the interface draws it: the book, then the account (an order fills), then the strategy, whole, before the next event. No frame is involved, so it goes on deciding with the window hidden. The account is one lock shared with the interface, which holds it while it builds the panels that show it: an event that arrives then waits a few milliseconds for it.
  • Replay. The replay moves one event at a time, and the account and the strategies move with it: the account is given an event, then the strategy, and only then the next. While a strategy is reading its data nothing more is taken from the replay and its clock stands. The replay reads ahead of its clock, so whether the recording is whole is known beforehand -- a minute its bar says traded, with no print, is delivered as a DataGap when the clock reaches it -- but nothing from ahead of the clock is delivered, folded in or decided on.
  • What it is given. Prints, closed bars, and a hole when there is one -- and it decides at every print. The bar still forming it reads from its own prints (features.forming_*, §5.3), not from what a feed publishes of it: the venue's candle and the footprint of the minute in progress are sent after every print, at each feed's own pace, and the footprint is dated at the end of a minute that has not ended. Neither is given to a strategy and neither moves its clock. A feed is silent when nothing of it arrives, not when what arrives cannot be used.
  • Bars. The live feed sends only the bar still forming, so the bar a strategy reads is closed from the prints it is given -- by the first print of the next minute, so that it holds every print of its own: the venue's next bar is often heard before the last of them. An average is given its history when it starts, live or in a replay: a simple one its period, an exponential one four of its periods (up to sixty days of minutes), read backwards from where it starts. Where the bars run out it starts from the nearest point that has them, and says with how many.
  • When the data is really not there -- a minute never recorded that a bar cannot stand for, a drop the capture missed too -- the form's rules apply, and only then: close on a hole, do not trade on data that is not whole. A minute whose prints were not recorded is rebuilt from the venue's own bar (its whole volume at its average price, exact for the VWAP and the volume) when the strategy reads nothing a bar cannot say.

A strategy's target and stop are orders of the account. An entry written order(…, Protect.Bracket(take_profit, stop_loss)) (language 0.1.2) is sent with them: the moment it fills they are attached to the position it opened as resting orders -- a limit and a stop, the same ones a person attaches by hand -- listed and drawn with every other order, on a replay's practice account and on the demo account alike. The account fills the one the price reaches (a target at its own price; a stop the price goes straight through, at the print that reached it) and withdraws the other; closing the position any other way withdraws both, and a strategy that is stopped without closing leaves them where they are, still protecting it. A target on the losing side of its stop, or of a limit entry's own price, is refused with the entry rather than placed. An entry whose target or stop cannot be read on the step it fires is not sent.

A position is managed by rules that read it. A part of it is closed with Intent.Reduce (at the market; the stop and the target stay on what is left, for what is left), and its stop or its target is moved with protect(…). Each is done once by reading what it changed: features.position_fraction() says how much of the position is still there, features.stop_price() where the stop is, so "half off at one risk" and "then the stop to the entry price" are two rules that stop being true once they have acted. at_entry(x) holds a value from the step the entry was decided -- the risk the trade was taken with -- unchanged by anything done to the position afterwards (50_sweep_fade_managed.tzs).

When a strategy's trades are closed is the person's choice in the form: a gap in its data, a market gone quiet, the replay ending, the script stopped, a loss limit; and whether it may trade at all when its data could not be loaded in full. Only the trades the strategy itself opened are closed: several strategies and the person share one account, and none touches what another opened.

In the Backtests window. Add strategy… runs the script once; each trade.run leaves one strategy on the bench (state: inserted), and nothing is traded. For every strategy and every dataset the window says whether the dataset holds what the strategy reads -- per-execution prints, aggregated prints, 1-minute bars -- and, where it does not, exactly what is missing and what would fix it; Run backtests starts every pair that can be served, as many at a time as the window is told (two to start with), and never one that cannot. A dataset holds its whole range, so these runs go as fast as the machine reads it; a replay asks for its data as it plays.

A script that was written as a backtest, put somewhere. Where a script is put is what its strategies run on, whatever its text says: a replay reads its market on demand, and a dataset is a different thing. So for a script started from the replay window, the title bar or the Datasets window, backtest.submit(spec) and backtest.run(spec) hand spec.strategy to the terminal exactly as trade.run does -- the dataset the spec names is not opened, and its costs and balance are the form's or the window's. market.datasets answers even on a terminal with no dataset (one entry standing for the market, id: "placed:<symbol>", which names no dataset), so the script's own lookup does not stop it. The job it is given has no report: backtest.wait, status and cancel answer E-BT-PLACED, and a script that ends on it has ended as it should -- which also means that what such a script would have submitted after waiting for its first report is not handed over. backtest.run is granted to these scripts without asking, because for them it starts no backtest. Run from the T0 Editor the same file is a program of its own, and a backtest is a backtest over a dataset.

Any script, not only a strategy script. A script started from the replay window or the title bar is granted what a hook may hold plus orders.submit, and may stay: one that watches (it draws, it alerts, it rests in live.wait) is listed beside the strategies as running, until it is stopped.

Errors: E-TRADE-NO-TARGET (no chart, so no market), E-TRADE-BUSY (too many requests are still waiting for the person's answers), E-TRADE-UNSUPPORTED (on a market: it reads per-execution prints, which the live feed does not deliver -- use AggTrades; a dataset may hold them, which the Datasets window checks dataset by dataset).

6. Adding a contract or an operation#

  1. Add the descriptor and handler in t0-host (one registration).
  2. Regenerate the snapshot (T0_UPDATE_SNAPSHOT=1 cargo test -p t0-host manifest), review the diff against the compatibility policy, and bump the contract version accordingly.
  3. Document it here. A new operation in an existing contract is a minor change; changing a signature or a meaning is breaking.