Skip to content
← All examples

Example 08

Backtest job

A backtest as a job: submit it, look at it while it runs, wait for its

Category
Host: needs the terminal's data and engine
Permissions
market.readbacktest.run
Language
>=0.1.1, <0.2.0
Host
>=0.1.1, <0.2.0
Needs
market >=0.1.1, <0.2.0features >=0.1.0, <0.2.0backtest >=0.1.1, <0.2.0

Source

// Category: host
// Permissions: market.read, backtest.run
// A backtest as a job: submit it, look at it while it runs, wait for its
// report, then read its closed trades from the run's own journal. The
// strategy runs on TTT's engine; T0 only describes it.
//
// Grant in the T0 Lang window: market.read, backtest.run.
// While it runs, "Stop job" in the window stops only the backtest (this
// program carries on and sees StoppedByUser); "Stop program" stops both.

lang ">=0.1.1, <0.2.0";
host "ttt" ">=0.1.1, <0.2.0";
requires "market" ">=0.1.1, <0.2.0";
requires "features" ">=0.1.0, <0.2.0";
requires "backtest" ">=0.1.1, <0.2.0";

strategy sma_breakout(window: Int, size: Quantity) version "1.0.0" {
    let average = features.sma(window);
    let px = features.price(features.Source.Trades);
    rule enter when cross_above(px, average) && exposure_is(Exposure.Flat)
        => order(Side.Buy, Intent.Open, OrderType.Market, Sizing.Fixed(size));
    rule exit when cross_below(px, average) && exposure_is(Exposure.Open)
        => order(Side.Sell, Intent.Close, OrderType.Market, Sizing.Fixed(size));
    flatten_on_stop;
}

fn main() -> Result<Unit, Error> {
    let ds = market.datasets("BTCUSDT")?.first().expect("no published BTCUSDT dataset on this machine");
    let job = backtest.submit(backtest.Spec {
        strategy: sma_breakout(30, qty(0.01)),
        dataset: ds,
        start: None,
        end: None,
        starting_balance: money(10000, USDT),
        execution: backtest.Execution {
            maker_fee: bps(2),
            taker_fee: bps(5),
            slippage: backtest.Slippage.Fraction(bps(1)),
            latency: 50ms,
            market_fill: backtest.MarketFill.NextPrint,
            limit_fill: backtest.LimitFill.TradeThrough,
        },
    })?;
    let first = backtest.status(job)?;
    print("submitted " + first.run_id + ", " + to_string(first.state) + " (" + first.phase + ")");

    let report = backtest.wait(job)?;
    print(to_string(report.status) + ": " + to_string(report.trades) + " trades, realized " + to_string(report.realized_pnl) + ", fees " + to_string(report.fees_paid));

    let trades = backtest.trades(report.run_id)?;
    let mut winners = 0;
    for t in trades {
        if t.realized_pnl.amount() > 0.0 {
            winners = winners + 1;
        }
    }
    print(to_string(winners) + " of " + to_string(trades.len()) + " trades made money");
    match trades.first() {
        Some(t) => print("first: " + t.side + " " + to_string(t.qty) + " from " + to_string(t.entry_price) + " to " + to_string(t.exit_price) + " (" + t.reason + ")"),
        None => print("no closed trades"),
    }
    return Ok(());
}