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: signaloperations 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-CONTRACTif 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> }—idis 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'sklines1mstream;close_timeis the end of the interval (exclusive) andavailable_atwhen 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 }—Nonebounds mean the dataset's own extent (stated, not defaulted: the field must still be written).Status { Completed, StoppedByUser, StoppedByStrategy, Failed }— TTT'sRunStatus; everyFailed*status maps toFailed, andReport.detailsays 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> }—fundingisNonewhen the dataset carried no settled funding: it was not observed, which is not the same as zero (TTT'sMeasured).
| 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 isE-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 }—fractionisNonewhen 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_datasetagainst the dataset's streams) and executed byBacktestRunner(deterministic, tape-driven fills per the givenExecution). Errmeans the run could not start (invalid strategy, dataset not found, currency mismatch). A run that started and then failed or was cancelled isOk(report)withstatussaying so — TTT's rule that a failed run is a run, not a missing one.starting_balancemust be in the dataset's quote currency (E-BT-CURRENCY). It is converted to TTT'sf64accounting; money in the report is TTT'sf64value converted toDecimalby its shortest round-trip decimal form, in the dataset's quote currency.run_idist0-<execution>-<n>; the journal is written to the session's runs directory (TTT:<temp>/ttt-runs/t0/) and its path returned (Noneif the session has none).- The translation is checked by test: TTT's reference graph
strategy_graph::examples::sma_vwap_cross, written in.tzand run throughbacktest.run, has the samestrategy_identityas 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_drawdownis 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 ofvalueandunknownis set; unknown is never zero (unknownsays why: warmup with the bars needed and held, no data, invalidated by a reported gap, not a single number).available_atis never afteras_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 }—availableis whether this terminal can serve it now (withwhywhen not);grantedwhether 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> }—kindis the event's stable name (symbol_changed,app_start,drawing_trigger,manual…);sourcesays what it was attached to (hook-3,drawing-0,manual);dataiskey=valuelines 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 |
notifysounds the terminal's alert chime, speaks the title when the person has voice on, and counts one unread alert.webhookposts.kindisdiscord(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>) orjson(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 }--buyis volume that lifted the offer;max_buythe 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 iscall_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> }--unpricedoptions 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> }--bandis how wide a band is (a level's price is its floor: a trade atphit the band whose price is at mostpand abovep - band; the capture sizes it from the instrument's price); thelevelslargest bands a side (the walls), by price ascending;added/removedare 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 }--reasonisbar(a base bar of the market closed) ortimer(the time asked for passed first);nowis 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 }--targetisreplay,demo,realordatasets;stateispendinguntil the person has answered the terminal's form,insertedfor 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
DataGapwhen 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#
- Add the descriptor and handler in
t0-host(one registration). - 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. - Document it here. A new operation in an existing contract is a minor change; changing a signature or a meaning is breaking.