Skip to content

Language

Language Specification

Language version 0.2.0 (draft; 0.1 programs compile unchanged -- see §12). Source files use the extension .tz. This document is normative for the compiler (t0-compiler), the IR validator (t0-ir) and the reference runtime (t0-runtime). Where the implementation differs, either the implementation is wrong or this document must be revised first (with an ADR when the change is material).

Status markers: [v0] implemented in 0.1; [planned] specified direction, not implemented; writing it is rejected with E-SYN-UNSUPPORTED or a resolution error.


1. Design rules#

  1. Curly braces delimit blocks; every simple statement ends with ;. There is no automatic semicolon insertion and no significant whitespace.
  2. Static typing with local inference. Functions, struct fields and strategy parameters are annotated.
  3. No implicit conversion between numeric kinds, no truthiness, no implicit zero for missing values, no silent error suppression.
  4. Value semantics: assigning or passing a value copies it (implemented with copy-on-write). There is no shared mutable state and no aliasing.
  5. A program has no ambient authority: everything outside pure computation and print is a call to a host contract the program declares.

2. Lexical structure#

  • Source text is UTF-8. Newlines are \n or \r\n.
  • Comments: // … to end of line, /* … */ (nesting allowed).
  • Identifiers: [A-Za-z_][A-Za-z0-9_]*. _ alone is the wildcard pattern.
  • Reserved keywords: fn let mut const struct enum strategy if else while for in match return break continue true false.
  • Contextual keywords, special only where the grammar expects them and ordinary identifiers elsewhere (so report.version is a field access): lang host requires import (file headers) and version rule when flatten_on_stop (inside a strategy declaration).
  • A keyword may be used as a field name (after ., in a struct literal, in a struct declaration): spec.strategy is a field access.
  • Reserved for later versions and refused with E-SYN-UNSUPPORTED where an item may start: impl trait pub mod use async.
  • Integer literal: [0-9][0-9_]* (underscores ignored; no leading zero unless the literal is 0). The value must fit i64.
  • Decimal literal: digits . digits (12.50, 0.0001). At most 18 fractional digits. 1. and .5 are not literals.
  • Duration literal: an integer literal immediately followed (no space) by a unit: ns us ms s min h d — 15min, 250ms, 1d. d is exactly 86 400 s (no calendar).
  • String literal: "…" with escapes \" \\ \n \r \t \u{HEX}. No raw newline inside a string.
  • Punctuation: { } ( ) [ ] ; , : . -> => = == != < <= > >= + - * / % ! && || ?.
  • Nesting: expressions, blocks, types, patterns and else if chains nest at most 128 levels deep (E-SYN-TOO-DEEP). Every later stage recurses over the tree; the parser bounds it once for all of them.

3. Program structure#

program   = header* item* EOF
header    = "lang" STRING ";"
          | "host" STRING STRING ";"
          | "requires" STRING STRING ";"
          | "import" IDENT ("." IDENT)* ";"
item      = fn_item | struct_item | enum_item | const_item | strategy_item
  • lang "<range>"; — required, first header: the language versions the program accepts (range syntax in docs/versioning/compatibility-policy.md §2, e.g. ">=0.1.0, <0.2.0").
  • host "<name>" "<range>"; — required when any requires is present: the host and the host-API versions the program accepts.
  • requires "<contract>" "<range>"; — one per host contract used. Calling an operation of an undeclared contract is E-CAP-UNDECLARED.
  • import std.<module>; — makes a standard-library module's functions available as <module>.<fn>(…). v0 modules: std.math, std.stats.
  • Items may appear in any order; names resolve over the whole file. A file is one module. [planned] multi-file modules, importing user modules.
  • Entry point: fn main() with no parameters, returning Unit or Result<Unit, Error>. A program without main can be checked and compiled but not run (E-RT-NO-ENTRY).

3.1 Items#

fn_item     = "fn" IDENT "(" params? ")" ("->" type)? block
params      = param ("," param)* ","?
param       = IDENT ":" type
struct_item = "struct" IDENT "{" (field ("," field)* ","?)? "}"
field       = IDENT ":" type
enum_item   = "enum" IDENT "{" variant ("," variant)* ","? "}"
variant     = IDENT ("(" type ")")?
const_item  = "const" IDENT ":" type "=" expr ";"
type        = IDENT ("." IDENT)? ("<" type ("," type)* ">")?

A function without -> type returns Unit. A const initializer may use only literals, operators, other consts and prelude constructors. Recursion is allowed (bounded by the call-depth limit). Duplicate item names are E-RES-DUPLICATE.

4. Statements and blocks#

block = "{" stmt* "}"
stmt  = "let" "mut"? IDENT (":" type)? "=" expr ";"
      | place "=" expr ";"
      | "if" expr block ("else" (block | if_stmt))?
      | "while" expr block
      | "for" IDENT "in" expr block
      | "match" expr match_body          (statement form, §5.4)
      | "return" expr? ";"
      | "break" ";" | "continue" ";"
      | expr ";"
place = IDENT ("." IDENT)*               (a mutable local, or a field path of one)
  • Bindings are immutable unless declared mut; assigning to an immutable binding is E-TYPE-IMMUTABLE. Shadowing a name in an inner block is allowed.
  • Conditions must be Bool (E-TYPE-MISMATCH otherwise; no truthiness).
  • for x in e requires e : List<T>; x : T is immutable. Iteration is over the list's value when the loop starts.
  • Blocks do not produce values. A function returns with return; a non-Unit function whose body can reach its end is E-TYPE-MISSING-RETURN (conservative: the last statement must return on every path).
  • An expression statement of type Result or Option produces warning W-UNUSED-RESULT: an ignored error stays visible.

5. Expressions#

5.1 Precedence (lowest to highest)#

level operators associativity
1 || left
2 && left
3 == != < <= > >= none (a < b < c is E-SYN-CHAINED-COMPARISON)
4 + - left
5 * / % left
6 unary - ! prefix
7 call f(…), field .x, method .m(…), index [i], try ? postfix

&& and || short-circuit.

5.2 Primary expressions#

primary = literal | IDENT | "(" ")" | "(" expr ")" | "[" (expr ("," expr)* ","?)? "]"
        | path "{" (IDENT ":" expr ("," IDENT ":" expr)* ","?)? "}"   (struct literal)
        | "if" expr "{" expr "}" "else" "{" expr "}"                   (if expression)
        | "match" expr match_body
  • A struct literal is not allowed directly in the condition of if/while, the iterable of for, or a match scrutinee (parenthesise it).
  • Every field must be given exactly once (E-TYPE-MISSING-FIELD, E-TYPE-UNKNOWN-FIELD). There are no default field values: a backtest specification that omits its fees does not compile.
  • [] needs its element type from context (let xs: List<Int> = [];), otherwise E-TYPE-CANNOT-INFER.
  • The if expression takes exactly one expression per branch; both branches must have the same type.

5.3 Paths and names#

a.b.c resolves left to right: a local or const → field access; a declared or prelude enum → variant; an imported std module → function; a declared host contract → operation or contract type. An unresolvable first segment is E-RES-UNKNOWN-NAME, with a suggestion when a similar name exists.

5.4 match#

match_body = "{" arm ("," arm)* ","? "}"
arm        = pattern "=>" (expr | block)
pattern    = "_" | IDENT | INT | STRING | "true" | "false" | "None"
           | ("Some" | "Ok" | "Err") "(" pattern ")"
           | path ("(" pattern ")")?
  • The scrutinee may be Option, Result, an enum, Int, String or Bool. Arms are tried in order.
  • Matches on Option, Result, enums and Bool must be exhaustive (E-TYPE-NONEXHAUSTIVE); matches on Int/String need a final _ or binding arm.
  • Expression form: every arm is an expression of one common type.
  • Statement form (a match that begins a statement): arms may be blocks; every arm has type Unit; no trailing ; is needed.

5.5 Error propagation#

e? with e : Result<T, E> evaluates to T or returns Err(e) from the enclosing function, whose return type must be Result<_, E> with the same E. e? with e : Option<T> requires the function to return Option<_>. Anything else is E-TYPE-TRY.

6. Types#

6.1 Core types [v0]#

type values
Unit the single value, written () (e.g. return Ok(());)
Bool true, false
Int signed 64-bit; overflow is a runtime error
Float IEEE 754 binary64; never NaN or infinite (§7.3)
Decimal exact decimal, 128-bit mantissa, scale 0..=18 (§7.2)
String immutable UTF-8 text
List<T> ordered, value semantics
Option<T> Some(x) / None
Result<T, E> Ok(x) / Err(e)
Error record { code: String, message: String }; host failures arrive as Error
Timestamp instant, nanoseconds since 1970-01-01T00:00:00Z (i64)
Duration signed span in nanoseconds (i64)
Currency an upper-case currency or asset code (USDT, BTC)
Price Decimal amount of quote currency per one unit of base asset, tagged with the quote currency
Quantity Decimal amount of base asset (untagged in v0; ADR-0005)
Money Decimal amount tagged with a currency
Ratio dimensionless Decimal (percent(1.5) is 0.015)

User types: struct (fields by name) and enum (variants with at most one payload). Host contracts contribute records, enums and handles (opaque references, §10.3) under their namespace: market.Dataset, backtest.Report.

Strategy types (only inside strategy, §9): Signal<Float>, Signal<Bool>, Action; and the value type Strategy.

[planned] user generics, closures, methods on user types, maps.

6.2 Literal typing#

An integer literal becomes Int, Decimal or Float from context, defaulting to Int. A decimal literal becomes Decimal or Float from context, defaulting to Decimal; it never becomes Int. Context is the other operand of a binary operator, the declared type of a binding, parameter or field, or the expected type of an argument or return value. let x = 1.5; is Decimal; let f: Float = 1.5; is Float.

6.3 Equality and ordering#

==/!= compare two values of the same type, structurally; Decimal equality is numeric (1.50 == 1.5). Ordering operators are defined for Int, Float, Decimal, Price, Quantity, Money, Ratio, Timestamp, Duration, String (byte-wise). Comparing Money/Price of different currencies is a runtime error (E-FIN-CURRENCY-MISMATCH), never false.

7. Numeric and financial semantics#

7.1 Int#

+ - * / %; / truncates toward zero; % takes the sign of the dividend. Overflow → E-NUM-OVERFLOW; division or remainder by zero → E-NUM-DIV-ZERO.

7.2 Decimal#

  • Representation m × 10^-s, m : i128, 0 ≤ s ≤ 18.
  • +, -: exact (scales aligned to the larger). Overflow → error.
  • *: exact when s1 + s2 ≤ 18; otherwise rounded to 18 places, half to even.
  • /: the exact quotient when it has at most 18 places — written with no trailing zeros beyond the dividend's own scale (1 / 4 = 0.25, 3.00 / 1 = 3.00, 10.0 / 4 = 2.5) — otherwise rounded to 18 places, half to even. Division by zero → E-NUM-DIV-ZERO.
  • round(x, places) rounds half to even; trunc(x, places) rounds toward zero; places in 0..=18, else E-NUM-SCALE.
  • Display keeps the scale: 1.50 prints 1.50.
  • Serialisation (IR, host boundary): the decimal string, e.g. "1.50".

7.3 Float#

+ - * / on Float. A result that is NaN or ±infinity is E-NUM-NONFINITE (this includes division by zero). to_float(x) converts Int/Decimal (possibly inexact). decimal_from_float(f, places) rounds half to even and is the only Float → Decimal route.

7.4 Dimension table#

The only arithmetic defined on financial and temporal types. Anything not listed is E-FIN-DIMENSION, naming both operand types. The table is the single source in t0-ir::ops, used by both the compiler and the IR validator.

left op right result notes
Price + - Price Price same currency (runtime)
Price * Quantity Money currency of the price; also Quantity * Price
Price * / Decimal Price also Decimal * Price
Price * Ratio Price also Ratio * Price
Price / Price Ratio same currency
Quantity + - Quantity Quantity
Quantity * / Decimal Quantity also Decimal * Quantity
Quantity * Ratio Quantity also Ratio * Quantity
Quantity / Quantity Ratio
Money + - Money Money same currency
Money * / Decimal Money also Decimal * Money
Money * Ratio Money also Ratio * Money (fees)
Money / Money Ratio same currency
Money / Quantity Price currency of the money
Money / Price Quantity same currency
Ratio + - * / Ratio Ratio
Ratio + - * / Decimal Ratio also Decimal op Ratio
Timestamp - Timestamp Duration
Timestamp + - Duration Timestamp also Duration + Timestamp
Duration + - Duration Duration
Duration * / Int Duration / truncates toward zero
Duration / Duration Decimal

Explicitly refused (each one a conformance test): Price + Quantity, Price + Money, Price * Price, Money * Money, Money + Decimal, Timestamp + Timestamp, Int + Decimal (non-literal), Float + Decimal.

Unary - is defined on Int Float Decimal Price Quantity Money Ratio Duration.

7.5 Prelude functions and constants#

function signature
price (Decimal, Currency) -> Price
qty (Decimal) -> Quantity
money (Decimal, Currency) -> Money
ratio, percent, bps (Decimal) -> Ratio (percent(x) = x/100, bps(x) = x/10000)
currency (String) -> Currency; code must match [A-Z0-9]{2,12} (E-FIN-CURRENCY-CODE)
timestamp (String) -> Timestamp; RFC 3339 UTC (2024-01-01T00:00:00Z, fraction up to 9 digits); a literal argument is checked at compile time
to_float (Int) -> Float, (Decimal) -> Float
to_decimal (Int) -> Decimal
decimal_from_float (Float, Int) -> Decimal
round, trunc (Decimal, Int) -> Decimal
to_string (T) -> String
print (T) -> Unit; writes the display form to the execution's output
error (String, String) -> Error

Constants of type Currency: USD USDT USDC BTC ETH EUR.

Built-in methods: .amount() -> Decimal and .currency() -> Currency on Price/Money; .value() -> Decimal on Quantity/Ratio; .ns() -> Int on Timestamp/Duration. List<T>: .len(), .is_empty(), .get(Int) -> Option<T>, .first(), .last(), .push(T) (on a mutable place only); xs[i] (out of range → E-RT-INDEX). Option<T>: .is_some(), .is_none(), .unwrap_or(T), .expect(String) (None → E-RT-EXPECT carrying the message). Result<T, E>: .is_ok(), .is_err(), .unwrap_or(T), .expect(String). String: .len(), + concatenation.

7.6 Standard library [v0]#

  • std.math: abs (Int/Float/Decimal), min, max (two values of one ordered numeric type), sqrt(Float) (negative → E-NUM-DOMAIN), ln(Float) (non-positive → E-NUM-DOMAIN), exp(Float), pow(Float, Float).
  • std.stats over List<Float>: median, correlation(ys, xs) and slope(ys, xs) (over two lists of one length), sum, mean, variance, stdev (sample, n−1), min, max, each returning Option<Float>: None for an input too short (empty; fewer than two for variance/stdev). Never zero for "no data".
  • std.text, std.list, std.decimal, std.time, std.fin, std.ind: every function with its signature, behaviour and errors is in stdlib.md.

8. Temporal semantics#

  • Timestamp is an instant in UTC with nanosecond resolution. There are no local times or calendars in v0.
  • Every observation a host returns with a time carries both its event time and the time it became available (available_at). Host contracts state which one their bounds use (TTT: availability).
  • Lookahead. v0 has two kinds of data access:
    • descriptive research: a host operation returns observations over a closed historical range, all in the program's past; the program may compute anything from them;
    • decisions inside a simulation, which exist only inside strategy declarations. A strategy body has no lists, indexing, loops or host calls — only Signals that the host engine evaluates as-of each event (ADR-0004). It cannot read the future by construction.
  • A missing observation is None or an Error, never 0.

9. Strategies#

strategy_item = "strategy" IDENT "(" sparams? ")" "version" STRING "{" sitem* "}"
sparams       = sparam ("," sparam)* ","?
sparam        = decorator* IDENT ":" ("Int" | "Decimal" | "Quantity" | "Ratio")
decorator     = "@" IDENT ( "(" ( IDENT ":" expr ("," IDENT ":" expr)* ","? )? ")" )?        (0.2)
sitem         = "let" IDENT "=" expr ";"
              | "rule" IDENT ("for" IDENT)? "when" expr "=>" expr ";"
              | "position" IDENT ";"                                                          (0.2)
              | "var" IDENT "=" expr ";"                                                       (0.2)
              | "handler" ("bar" "(" expr ")" | "fill" | "order" | "start") "=" expr ";"       (0.2)
              | "state" "=" expr ";"                                                           (0.2)
              | "halt_on_error" ";"                                                            (0.2)
              | "warm_up" expr "days" ";"                                                       (0.2)
              | "flatten_on_stop" ";"

Decorators (0.2). A strategy parameter may be preceded by @param(min: …, max: …, step: …, …), which says that a host's optimizer may move it, in what range and on what grid. The compiler checks the decorator (E-DECO-*, diagnostics) and carries it on the signal graph; it never changes what the strategy does. @param is the only decorator, and only a strategy's parameters take one (script-kinds). The tokens @ and : after a key are the whole of the syntax: the values are literals (numbers, strings, true/false) or, for above/below, the name of another parameter. Two keys say how a host shows the parameter rather than how far it moves: switch: true (an Int that is 1 or 0, with no range of its own) and off: N (the value that means a filter is off); ADR-0016.

Inside a strategy body expressions denote signals, values the host engine recomputes at each evaluation step. Allowed:

  • literals and strategy parameters (lifted to Signal<Float>);
  • + - * / on Signal<Float>; comparisons → Signal<Bool>; && || ! on Signal<Bool>;
  • operations of a host contract whose descriptor has kind: signal (features.sma(slow)); their arguments must be literals, parameters or enum values, never signals;
  • transitions: cross_above(a, b), cross_below(a, b) (true only on the step where the relation becomes true), rising(x), falling(x) (strictly versus the previous step), prev(x) (the previous step's value), bars_since(c) (steps since c was last true; unknown until it has been true once);
  • exposure_is(Exposure.Flat) (Flat | Entering | Open | Closing);
  • order(side, intent, order_type, sizing) → Action, only as the right-hand side of a rule;
  • order(side, Intent.Open, order_type, sizing, protection) (0.1.2): an entry that carries its own way out. protection is Protect.Bracket(take_profit, stop_loss), Protect.TakeProfit(price) or Protect.StopLoss(price); each price is a number signal, read on the step the rule fires. They are orders the host is left with, not conditions the strategy watches: once the entry fills they rest on the position it opened, the host fills the one the price reaches and withdraws the other. Only on an entry (Intent.Open); Protect is not a type and names nothing anywhere else;
  • order(side, Intent.Reduce, OrderType.Market, sizing) (0.1.2): closes a part of the open position at the market and leaves the rest, and what protects it, where it is. sizing is Sizing.PercentOfPosition(percent(50)) -- a share of what the position still holds, and the size of a reduction only -- or Sizing.Fixed(qty);
  • Sizing.ByRisk (0.2) is the size of an entry that the strategy does not decide: order(Side.Buy, Intent.Open, OrderType.Market, Sizing.ByRisk, Protect.StopLoss(stop)). It takes no argument. The account's risk policy (a component risk that defines fn size) answers the quantity, reading the request -- which way, how the entry is priced, the stop and the target the entry carries -- and the account; or it declines the signal, which is no order and not an error: the strategy goes on to the next signal. The sized order is then judged by the policy's authorize and the host's own limits like any other. An account whose policy does not size, or that has none, refuses the entry: a size nobody decided is never guessed. Not for an order that closes;
  • protect(protection) → Action (0.1.2): sets, or moves, the take-profit and the stop-loss resting on the open position; a side not given is left as it is. A rule whose condition goes on holding does not have it done again: what already rests where the rule would put it is left there;
  • at_entry(x) (0.1.2): what the number signal x was on the step the open position's entry was decided, held for as long as the position is there and unknown while flat -- the risk a trade was taken with, the level it was taken from.

Prelude enums used by strategies: Side { Buy, Sell }, Intent { Open, Close }, OrderType { Market, Limit(Signal<Float>) }, Sizing { Fixed(Quantity), PercentOfEquity(Ratio), PercentOfAvailable(Ratio) }, Exposure { Flat, Entering, Open, Closing }. Sizing payloads must be literals or parameters.

Semantics owned by the host engine (contract ttt.strategy-ir@1 for TTT):

  • let names become node names in the host's decision explanations; rule names become the decision code of the orders they send.
  • Rules are evaluated in written order; the first whose condition holds and whose action the host accepts acts.
  • An unknown value (warm-up, missing data) makes a comparison unknown and a rule not fire; it is never 0.

Calling a strategy like a function outside any strategy, sma_cross(3, 8), produces a Strategy value: the signal graph plus bound parameter values. It does nothing by itself; it is handed to a host operation (backtest.run). Inside a strategy body, lists, loops, print, user functions and non-signal host operations are E-STRAT-NOT-SIGNAL.

10. Host contracts in the language#

10.1 Calls#

contract.operation(args) calls a host operation. The compiler checks it against the manifest it was given (ADR-0006): the contract must be declared with requires (E-CAP-UNDECLARED), present in the manifest (E-CAP-UNKNOWN) at a version satisfying the declared range (E-CAP-VERSION); the operation must exist (E-CAP-UNKNOWN-OP) and the arguments must match its parameter types.

10.2 Results#

A host operation that can fail returns Result<T, Error>. The Error code is a stable identifier from the contract's documented error list or a generic host code (E-HOST-*, E-PERM-*, E-CAP-UNAVAILABLE).

10.3 Handles#

A handle (workspace.ChartRef, market.Dataset) is an opaque reference issued by the host. It can be stored, compared with == and passed back to the host; its contents cannot be read or constructed by a program. A handle is valid only in the host process that issued it; using one after the object is gone, or in another process, fails with E-HOST-STALE-HANDLE.

11. Diagnostics#

Every compile-time and runtime problem has a stable code from the registry in docs/language/diagnostics.md. A code is never reused for a different meaning.

12. Language 0.2#

A program says lang ">=0.2.0, <0.3.0"; to use what follows. A program whose lang range stops short of 0.2 is compiled as 0.1.2 and means exactly what it meant. A construct of 0.2 in such a program is E-VER-FEATURE.

12.1 Handling errors#

  • mode "strict"; (a header after lang): a Result or Option that is computed and dropped is E-UNUSED-RESULT, an error, where otherwise it is the warning W-UNUSED-RESULT. To drop one on purpose: let _ = expr;.
  • if let PATTERN = expr { … } else { … } and while let PATTERN = expr { … }: a match with the pattern and a wildcard.
  • On Result<T, Error>: .context(String) prepends a line to the error's message and keeps its code; .map_err_code(String) replaces the code; .ok() is the Option<T>. On Option<T>: .ok_or(Error). On Error: .is(code), and .is_in("E-HOST") for the family of codes under a prefix.

12.2 Strategies#

Signals gain (all inside a strategy):

  • Math on number signals: abs sign sqrt ln exp min max clamp pow round_to. sqrt ln exp pow take dimensionless numbers; the rest keep the kind of value they are given (a price stays a price). Outside its domain a function has no value (unknown), it never errors or becomes zero.
  • Windows over any signal, sampled at the close of each bar of minutes (one, unless a third argument says): prev_n(x, n), change(x, n), sum_over, mean_over, stdev_over, highest_over, lowest_over, zscore, ema_of, count_true(cond, n). n and minutes are fixed for the run. A window describes closed bars only: it does not change inside a bar. It is unknown until it holds enough bars, and starts again after a bar in which its input was unknown or in which nothing arrived -- a hole is never bridged.
  • if cond { a } else { b } on number signals.
  • State: var armed = 0; declares a number the strategy remembers; reading it by name gives its value at the start of the step; set(armed, x) in a rule changes it for the steps after. It keeps its value when a position closes.
  • Several actions: rule r when c => [order(…), set(v, x), protect(…), cancel(other), stop("reason")];. The first order or protect is the rule's own; the others follow it, in order. A further action is never an entry. cancel(rule) withdraws the order that rule left resting. stop(reason) ends the run deliberately (stopped by the strategy). A rule with no order or protect only keeps state, and does not use up the step's order.
  • Add: Intent.Add sends another entry in a book that is already open (pyramiding). Refused while it is flat.
  • Order options (0.2): after the sizing, an order (an entry, or an exit that has a price) may say how it behaves, in any order and beside its protection: OrderOption.PostOnly (refused, not filled, if it would take liquidity when placed), OrderOption.ImmediateOrCancel (fills what it can at once and does not rest) -- both need OrderType.Limit(price) or OrderType.StopLimit(trigger, limit), and not together -- and OrderOption.Expire(minutes) (withdrawn if still working that many minutes after it was placed; a whole number written in the strategy; not on a market order). The market's clock decides, so a backtest withdraws it at the same event live would. They are part of the strategy's identity. Only on a rule's first order, not on the further actions of a rule.
  • Order types (0.2): OrderType.Market, OrderType.Limit(price), OrderType.Stop(trigger) (rests until the price trades through trigger, then goes to the market) and OrderType.StopLimit(trigger, limit) (becomes a limit order at limit once triggered). The prices are number signals read when the rule fires. They serve an entry (Intent.Open / Intent.Add) and an exit (Intent.Close / Intent.Reduce: a limit to take profit at a price, a stop to get out through one; a reduction may rest at a price too). A stop entry carries Protect.… like any entry: its target and stop are judged against the trigger (the limit, for a stop-limit) and attach to the position when the trigger fills. An exit that rests keeps the book Closing while it does, so a rule that waits for Exposure.Open does not stack a second exit on it; it ends when it fills, when its OrderOption.Expire passes, or when another rule says cancel(rule).

Components (0.2). A program says what it is for the host with a header after lang, host and requires:

component risk "limits" version "1.0.0";

A component has a role, a name (lowercase letters, digits, -, _; at most 64) and a major.minor.patch version. The host binds components to accounts by name and version; nothing a component says changes what it is bound to. There are three roles, and the compiler checks what the host needs of each, so a component that compiles is one the host can bind:

role the host calls requires has
strategy the strategy's signals and handlers, as before whatever it needs a strategy
risk fn authorize([cfg: Config,] intent: risk.Intent, ctx: risk.Context) -> risk.Decision risk only no main, no strategy
portfolio fn allocate([cfg: Config,] snapshot: portfolio.Snapshot, limits: portfolio.Constraints) -> portfolio.Decision portfolio only no main, no strategy
  • A risk or portfolio component is a function of what it is given. It requires only the contract of its role -- which has types and no operations -- so it cannot ask for the market, a file, the network or an order (E-COMP-CAPABILITY); the language has no clock and no randomness, so the same inputs give the same answer, which is what lets a decision be replayed. It cannot import or call another component: the host connects them.
  • struct Config is optional: the settings of the component, filled by the host from the configuration of a binding. When present it is the first parameter of the entry point, and it holds plain values only (Int, Float, Decimal, Bool, String, Money, Quantity, Price, Ratio, Currency, Duration, and lists and options of those: E-COMP-CONFIG). An unknown or missing field in a binding is an error when the host binds it, never a default.
  • A program is one component (E-RES-DUPLICATE for a second), a module cannot declare one (E-MOD-ITEM), and the header is language 0.2 (E-VER-FEATURE otherwise).
  • The artifact records the role, name, version, entry function and Config type (Body.component, IR 0.1.4). A program with no role serialises exactly as before.
  • What the host does with the answer is fixed by the contract, not by the component: a risk Allow is not the end of the judgment (the host's own limits and a reservation of the exposure still apply), a Reduce is judged again from the start, a Reevaluate asked twice is a refusal, and an error, a timeout or an answer that is not a decision refuses the order. A portfolio Decision states desired allocations and grants no permission to trade.

Modules (0.2). A program can be made of several files: import "lib/levels.tz" as levels; in the headers (after lang). The path is relative to the file that imports it, with forward slashes, ends in .tz, and stays inside the program's folder (never absolute, .. only back to that folder). The module is a file with its own lang header and functions, structs, enums and constants -- and its own imports; it cannot hold a strategy or say host, requires or mode (it uses the contracts its program requires). Its names are reached through the alias -- levels.clamp(x), levels.Point { x: 1, y: 2 }, levels.Dir.Buy, levels.LIMIT, the type levels.Point -- and inside it they are written bare; a module sees its own names and the prelude, nothing of the file that imports it. The same file imported twice is one module (so levels.Point is one type however it is reached); two modules that import each other are E-MOD-CYCLE; the alias may not be a standard module (list, math, …) or a contract. Limits: sixteen levels of imports, sixty-four modules, one MiB of source in all. The compiler is given the files by its host (t0 check/build/run read them from the program's folder; the terminal's editor does for a saved file); a host that lends none refuses an import in words (E-MOD-LOAD). What is compiled is one program: the artifact's source_sha256 covers the modules, a change in a module is a different program, and a diagnostic inside a module is shown at the import that brought it in, naming file:line:column. Strategies and their handler functions are written in the program's own file; they may call the modules' functions.

Maps (0.2). Map<K, V> holds pairs in key order; the key is a String, an Int or a Timestamp. Map.new() makes an empty one and needs its type from where it is written (let mut levels: Map<Int, Float> = Map.new();). Methods: len(), is_empty(), get(k) -> Option<V>, contains(k), keys(), values() (in key order), and insert(k, v) / remove(k), which as a statement on a variable or a field change the map in place (levels.insert(30, 1.5);) and as an expression give the changed map. Maps are values like lists (assigning one copies it on its next change), two maps are equal when their pairs are, and a map is bounded by the same collection and memory limits as a list. for (key, value) in map { … } walks the pairs in key order, as the map was when the loop began (changing the map inside the body does not change the walk); break and continue work as in any loop. A map does not cross to the host. A 0.1 program that declares its own Map type keeps it.

Handlers (0.2). A strategy keeps its signals (let …, evaluated by the host's engine) and may name functions of the program to call on events:

struct Memory { bars: Int, holding: Bool }
struct Step { state: Memory, actions: List<handlers.Act> }
fn begin() -> Memory { return Memory { bars: 0, holding: false }; }
fn on_bar(m: Memory, px: Float, avg: Float) -> Step { … }

strategy follower() version "1" {
    let px = features.price(features.Source.AggTrades);
    let avg = features.sma(3);
    state = begin();                  // makes the state the handlers carry
    handler bar(5) = on_bar(px, avg); // on a closed bar of 5 minutes (1 to 1440)
}

handler bar(N) = f(signals…);, handler fill = …; (an order was filled), handler order = …; (an order was refused or rejected) and handler start = …;. With state = init(); (a function of no parameters) the handler's function takes the state first; it returns a record with the next state (same type) and actions, a list of the host contract's action type (handlers.Act: Submit(Order) or Stop(reason)). A handler is called only when every signal it names is known, at the instant the event is known: it sees nothing later and reads the market only through the signals it names. Functions run on a deterministic runtime instance with a fuel budget per call and no wall clock, so a backtest repeats to the byte. The program travels inside the strategy definition, so the same handlers run in a backtest, in a replay and on the live feed (in the demo account). The compiler checks the shape (E-STRAT-HANDLER) and that each signal reaches a parameter of its type (E-TYPE-MISMATCH). A handler that needs to know what the strategy holds (its positions, the orders still working, its equity) writes ctx among the arguments -- handler bar(5) = on_bar(px, ctx, avg); -- and takes a handlers.Context at that place, so it need not keep that in its own state; the market still comes only through signals.

  • Books: position trend; declares a named position beside the main one. rule go for trend when … => order(…) works in it; the book answers trend.exposure_is(Exposure.Flat), entry_price() (the average of its entries), sign(), size(), fraction(), minutes(), stop_price(), target_price(), flat_minutes(), entries(), entry_vwap(source), entry_spread(source) and at_entry(x). Each book places one order a step; two books can act on the same step. Reducing, closing and protecting a book act on every position in it.
  • Refusals: refused(rule) is true after the runtime declined to place the order of rule; rejected(rule) after the account turned it away; last_error() is 0, 1 (refused) or 2 (rejected). halt_on_error; stops the strategy at the first of either. warm_up 220 days; declares the history, in whole days from 1 to 3660, the strategy must have run over before its first order -- a number written in the strategy or an Int parameter of it (warm_up entry_days days;, so a search can move it). The handlers are called on that history with every order refused, as a backtest does before its first day and a live run does before it starts to trade; a host that cannot provide it says so. Declared once (E-STRAT-WARM-UP).