Skip to content
← All examples

Example 46

Vwap 3sd reversion

a VWAP reversion strategy, described in T0 and run by TTT's engine -- the same strategy, with the same rules, in a backtest and live on the

Category
Host: needs the terminal's data and engine
Permissions
market.readbacktest.run
Before you run it
at least one published BTCUSDT dataset on this machine that holds aggregated prints and 1-minute bars (Datasets window)
What it does
starts two backtest jobs (they appear in the Jobs panel); writes their runs under the terminal's runs folder.
How to run it
grant market.read and backtest.run in the T0 Lang window, Run. "Stop job" stops only the backtest; "Stop program" stops both.
Language
>=0.1.1, <0.2.0
Host
>=0.1.8, <0.2.0
Needs
market >=0.1.1, <0.2.0features >=0.1.3, <0.2.0backtest >=0.1.1, <0.2.0

Source

// Purpose: a VWAP reversion strategy, described in T0 and run by TTT's engine -- the same strategy, with the same rules, in a backtest and live on the
// demo account (it reads only what the live feed delivers: aggregated prints and closed bars).
//
//   ENTRY   a bar CLOSES beyond the third deviation of the session VWAP (a bar closed below the lower 3-sigma band buys; one closed above the
//           upper 3-sigma band sells) -- price stretched three deviations from where the volume traded, and bet on it coming back.
//   TARGET  the second deviation (`target_k` = +2.0 puts it on the 2-sigma band FARTHEST from the entry, which is the one on the other side of the
//           VWAP; `target_k` = -2.0 puts it on the 2-sigma band nearest the entry, on the entry's own side). It is measured from the VWAP and the
//           spread AS THEY WERE WHEN THE ENTRY WAS DECIDED and held, so the target does not drift while the trade is open.
//   STOP    1:1 -- as far from the entry on the losing side as the target is on the winning one.
//
// It is run twice on the newest published BTCUSDT dataset, with the target on the far and on the near second deviation, and the two reports are
// printed side by side: which of them the data likes is the point of a backtest. Note what the far target means: from a 3-sigma entry to the opposite
// 2-sigma band is five deviations, so a 1:1 stop sits five deviations the other way -- in practice a stop that is rarely reached.
//
// The VWAP here is the engine's session VWAP: every aggregated print since the run began, weighted by volume, with the volume-weighted spread of the
// prices around it (West's update). Rules are evaluated on every print, so the target and the stop are checked at each print, and filled by the
// engine's own execution model (fees, slippage and latency in `backtest.Execution`).
// Category: host
// Permissions: market.read, backtest.run
// Preconditions: at least one published BTCUSDT dataset on this machine that holds aggregated prints and 1-minute bars (Datasets window)
// Side effects: starts two backtest jobs (they appear in the Jobs panel); writes their runs under the terminal's runs folder.
// Run: grant market.read and backtest.run in the T0 Lang window, Run. "Stop job" stops only the backtest; "Stop program" stops both.
// Live: the `vwap_3sd_reversion` strategy runs unchanged on a replay or the demo account -- see 47_live_vwap_reversion.tzs (every feature it reads is served live).

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

strategy vwap_3sd_reversion(entry_k: Decimal, target_k: Decimal, size: Quantity) version "1.0.0" {
    // What the market is doing: the last print, the last closed bar, the VWAP and its spread.
    let px = features.price(features.Source.AggTrades);
    let bar = features.bar_close();
    let vw = features.vwap(features.Source.AggTrades);
    let sd = features.vwap_spread(features.Source.AggTrades);
    let lower = vw - entry_k * sd;
    let upper = vw + entry_k * sd;

    // True on the one step where a new bar has closed -- so a bar is looked at once, not at every print until the next one.
    let new_bar = bar != prev(bar);

    // The open trade, as it was decided: which side, at what price, and where the VWAP and its spread were then.
    let held = features.position_sign();
    let entry = features.entry_price();
    let entry_vw = features.entry_vwap(features.Source.AggTrades);
    let entry_sd = features.entry_spread(features.Source.AggTrades);
    let long_target = entry_vw + target_k * entry_sd;
    let long_stop = entry - (long_target - entry);
    let short_target = entry_vw - target_k * entry_sd;
    let short_stop = entry + (entry - short_target);

    rule long_target_hit when exposure_is(Exposure.Open) && held > 0 && px >= long_target
        => order(Side.Sell, Intent.Close, OrderType.Market, Sizing.Fixed(size));
    rule long_stop_hit when exposure_is(Exposure.Open) && held > 0 && px <= long_stop
        => order(Side.Sell, Intent.Close, OrderType.Market, Sizing.Fixed(size));
    rule short_target_hit when exposure_is(Exposure.Open) && held < 0 && px <= short_target
        => order(Side.Buy, Intent.Close, OrderType.Market, Sizing.Fixed(size));
    rule short_stop_hit when exposure_is(Exposure.Open) && held < 0 && px >= short_stop
        => order(Side.Buy, Intent.Close, OrderType.Market, Sizing.Fixed(size));

    rule enter_long when exposure_is(Exposure.Flat) && new_bar && bar < lower
        => order(Side.Buy, Intent.Open, OrderType.Market, Sizing.Fixed(size));
    rule enter_short when exposure_is(Exposure.Flat) && new_bar && bar > upper
        => order(Side.Sell, Intent.Open, OrderType.Market, Sizing.Fixed(size));
    flatten_on_stop;
}

fn run(ds: market.Dataset, label: String, target_k: Decimal) -> Result<Unit, Error> {
    let job = backtest.submit(backtest.Spec {
        strategy: vwap_3sd_reversion(3.0, target_k, 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 report = backtest.wait(job)?;
    print(label + ": " + to_string(report.status) + ", " + to_string(report.trades) + " trades, realized " + to_string(report.realized_pnl) + ", fees " + to_string(report.fees_paid));
    return Ok(());
}

fn main() -> Result<Unit, Error> {
    let ds = market.datasets("BTCUSDT")?.first().expect("no published BTCUSDT dataset on this machine");
    run(ds, "target on the far 2-sigma band", 2.0)?;
    run(ds, "target on the near 2-sigma band", -2.0)?;
    return Ok(());
}