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#
- Curly braces delimit blocks; every simple statement ends with
;. There is no automatic semicolon insertion and no significant whitespace. - Static typing with local inference. Functions, struct fields and strategy parameters are annotated.
- No implicit conversion between numeric kinds, no truthiness, no implicit zero for missing values, no silent error suppression.
- Value semantics: assigning or passing a value copies it (implemented with copy-on-write). There is no shared mutable state and no aliasing.
- A program has no ambient authority: everything outside pure computation
and
printis a call to a host contract the program declares.
2. Lexical structure#
- Source text is UTF-8. Newlines are
\nor\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.versionis a field access):lang host requires import(file headers) andversion 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.strategyis a field access. - Reserved for later versions and refused with
E-SYN-UNSUPPORTEDwhere an item may start:impl trait pub mod use async. - Integer literal:
[0-9][0-9_]*(underscores ignored; no leading zero unless the literal is0). The value must fiti64. - Decimal literal: digits
.digits (12.50,0.0001). At most 18 fractional digits.1.and.5are not literals. - Duration literal: an integer literal immediately followed (no space) by a
unit:
ns us ms s min h d—15min,250ms,1d.dis 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 ifchains 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_itemlang "<range>";— required, first header: the language versions the program accepts (range syntax indocs/versioning/compatibility-policy.md§2, e.g.">=0.1.0, <0.2.0").host "<name>" "<range>";— required when anyrequiresis 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 isE-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, returningUnitorResult<Unit, Error>. A program withoutmaincan 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 isE-TYPE-IMMUTABLE. Shadowing a name in an inner block is allowed. - Conditions must be
Bool(E-TYPE-MISMATCHotherwise; no truthiness). for x in erequirese : List<T>;x : Tis immutable. Iteration is over the list's value when the loop starts.- Blocks do not produce values. A function returns with
return; a non-Unitfunction whose body can reach its end isE-TYPE-MISSING-RETURN(conservative: the last statement must return on every path). - An expression statement of type
ResultorOptionproduces warningW-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 offor, or amatchscrutinee (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> = [];), otherwiseE-TYPE-CANNOT-INFER.- The
ifexpression 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,StringorBool. Arms are tried in order. - Matches on
Option,Result, enums andBoolmust be exhaustive (E-TYPE-NONEXHAUSTIVE); matches onInt/Stringneed a final_or binding arm. - Expression form: every arm is an expression of one common type.
- Statement form (a
matchthat begins a statement): arms may be blocks; every arm has typeUnit; 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 whens1 + 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;placesin0..=18, elseE-NUM-SCALE.- Display keeps the scale:
1.50prints1.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.statsoverList<Float>:median,correlation(ys, xs)andslope(ys, xs)(over two lists of one length),sum,mean,variance,stdev(sample, n−1),min,max, each returningOption<Float>:Nonefor 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 instdlib.md.
8. Temporal semantics#
Timestampis 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
strategydeclarations. A strategy body has no lists, indexing, loops or host calls — onlySignals that the host engine evaluates as-of each event (ADR-0004). It cannot read the future by construction.
- A missing observation is
Noneor anError, never0.
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>); + - * /onSignal<Float>; comparisons →Signal<Bool>;&& || !onSignal<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 sincecwas 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.protectionisProtect.Bracket(take_profit, stop_loss),Protect.TakeProfit(price)orProtect.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);Protectis 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.sizingisSizing.PercentOfPosition(percent(50))-- a share of what the position still holds, and the size of a reduction only -- orSizing.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 (acomponent riskthat definesfn 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'sauthorizeand 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 signalxwas 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):
letnames 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 afterlang): aResultorOptionthat is computed and dropped isE-UNUSED-RESULT, an error, where otherwise it is the warningW-UNUSED-RESULT. To drop one on purpose:let _ = expr;.if let PATTERN = expr { … } else { … }andwhile let PATTERN = expr { … }: amatchwith 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 theOption<T>. OnOption<T>:.ok_or(Error). OnError:.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 powtake 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).nandminutesare 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 firstorderorprotectis 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 noorderorprotectonly keeps state, and does not use up the step's order. - Add:
Intent.Addsends 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 needOrderType.Limit(price)orOrderType.StopLimit(trigger, limit), and not together -- andOrderOption.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 throughtrigger, then goes to the market) andOrderType.StopLimit(trigger, limit)(becomes a limit order atlimitonce 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 carriesProtect.…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 bookClosingwhile it does, so a rule that waits forExposure.Opendoes not stack a second exit on it; it ends when it fills, when itsOrderOption.Expirepasses, or when another rule sayscancel(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 Configis 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-DUPLICATEfor a second), a module cannot declare one (E-MOD-ITEM), and the header is language 0.2 (E-VER-FEATUREotherwise). - The artifact records the role, name, version, entry function and
Configtype (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
Allowis not the end of the judgment (the host's own limits and a reservation of the exposure still apply), aReduceis judged again from the start, aReevaluateasked twice is a refusal, and an error, a timeout or an answer that is not a decision refuses the order. A portfolioDecisionstates 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 answerstrend.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)andat_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 ofrule;rejected(rule)after the account turned it away;last_error()is0,1(refused) or2(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 anIntparameter 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).