Skip to content
← All examples

Example 86

Vwap cross big trades

follow the price crossing the middle line of the rolling VWAP (the VWAP itself, the middle band of the chart's Rolling VWAP) when the big money agrees, and take the profit in three steps. When the price crosses the VWAP from below to above, and at least 4 of the last 5 large

Category
Host: needs the terminal's data and engine
Permissions
orders.submit
Before you run it
BTCUSDT aggregated prints and one-minute bars
What it does
asks the terminal to run the strategy; once running it places orders on the account the person chose.
How to run it
Insert strategy in the Backtests window (backtest and optimize); Add script on a replay (the same handlers run there).
Language
>=0.2.0, <0.3.0
Host
>=0.1.26, <0.2.0
Needs
features >=0.1.9, <0.2.0handlers >=0.1.2, <0.2.0trade >=0.1.0, <0.2.0

Source

// Purpose: follow the price crossing the middle line of the rolling VWAP (the VWAP itself, the middle band of the chart's Rolling VWAP) when the big money agrees, and take the profit in three steps. When the price crosses the VWAP from below to above, and at least 4 of the last 5 large
// prints (each of at least `big_qty`: 50 BTC, the terminal's Large trades at 50 x) were BOUGHT by the aggressor, it buys; the mirror -- a cross from above to below with at least 4 of the last 5 SOLD -- sells. One position at
// a time. The stop is `sl_pct` (1 %) from the entry; each time the price goes `step_pct` (1 %) further in the trade's favour a share of what is left (`partial_pct`, 50 %) is taken at that exact price and the stop moves up behind it,
// three times (`steps`), the last one taking everything that is left. Optionally the position is closed when the big prints on its own side meet an absorbing market: they come and the price does not move on.
//
//   VWAP          the chart's own "Rolling VWAP" (as in example 82): over the last `vwap_window` (20) candles of `bar_minutes` minutes (60: one-hour candles), each at its typical price (high + low + close) / 3 weighted by its volume, the mean
//                 is the VWAP -- the MIDDLE band -- and the volume-weighted standard deviation of those prices is what the other bands are made of: `vwap +/- k x sd`. A candle's value is the one the line has when that candle closes,
//                 the candle included, as the chart draws it. There is no line, and no trade, until `vwap_window` candles have been seen (20 hours, by default).
//   CROSS         judged when a candle closes and entered at that very close: the candle's close is above the VWAP and the last time a close was off the line it was below -> a cross up; the mirror -> a cross down. A close exactly on the
//                 line keeps the side it had. The first candle with a line only says which side the price is on: it is not a cross. A cross while a position is open or an order is working is passed over.
//   BIG TRADES    a large print is a single print of at least `big_qty` (the terminal's "Large trades" mode at a threshold of 50 x its base, floored at 1 BTC: 50 BTC on BTC). Of the last `big_last` (5) seen up to and including the minute of
//                 the cross, at least `big_least` (4) must be on the trade's side -- bought by the aggressor (bullish) for a long, sold (bearish) for a short; if fewer are, there is no trade. Fewer than 5 seen so far: no trade.
//                 READ HONESTLY: the engine tells, for each minute, how many large prints it held and the volume the aggressor bought and sold in them -- not the side or the order of each print inside the minute. A minute with one side
//                 only is exact; a minute with both has its buys counted by their share of the volume (as example 80 does); when the count of 5 ends inside such a minute only some of its prints are among the last 5 and their order is
//                 not known: the ones that DISAGREE are taken as the latest (the safe side -- a doubtful read counts against the trade).
//   ENTRY         at the market, as the minute closes, for `size`.
//   STOP          `sl_pct` per cent of the real entry price on the losing side, put on the position when the entry fills. It is a resting stop of the account.
//   STEPS         when the entry fills, the first `steps` - 1 (2) limit orders are left, at `step_pct` (1 %) and twice that from the entry price, in the trade's favour. Each only REDUCES the position, taking `partial_pct` (50 %)
//                 of what is left when it is its turn: 50 % of the position, then 25 %. The LAST step, at three times `step_pct`, is the full take-profit: it is left when the step before it has filled, for whatever the position holds
//                 THEN (so it always closes everything, and never lingers half-filled), and nothing is left running. When a step has filled completely the stop moves to the price of the step BEFORE it: the first step puts it on the entry (break-even), the second at +1 %. After the last there is no position.
//   BANDS         `bands_exit` 1 (off by default): the stop and the steps sit on the bands of the same VWAP instead of at per cents of the entry. The bands are those of the candle that gave the signal (the VWAP and its deviation as that candle
//                 leaves them, fixed for the trade: they do not move afterwards). The STOP is the band `sl_band` (1) deviations on the losing side -- under the VWAP for a long, over it for a short --; the STEPS are the bands `band_step`,
//                 twice that and three times that (1, 2 and 3) on the trade's side, the last the full take-profit. The stop still moves up behind each step -- to the entry after the first, to the price of the step before after the others.
//                 A signal is not traded when the stop would have no room under (over) the close or the first step would not be beyond it (a collapsed band), and an entry that fills past either is closed at once.
//   TRAILING      `trailing` 1 (off by default): the stop also FOLLOWS the price. At the close of each minute the best price the position has seen since its entry (the highest high of a long, the lowest low of a short) is taken, and
//                 the stop is put `trail_pct` (1 %) behind it. It only ever moves the trade's way -- up for a long, down for a short -- and stands still when the price goes against the trade; a move smaller than 0.02 % of the
//                 price is not made. It starts as the stop at `sl_pct` (the best price is the entry), and lives beside the steps' stops: of the two, the one nearer the price stands. It is read once a minute, so a stop that the
//                 price runs through inside a minute is the one of the minute before.
//   ABSORPTION    `absorption` 1 (off by default): once the position has lived `abs_minutes` minutes (so the window is all inside the trade), it is closed at the market when, over the last `abs_minutes` minutes, the large prints
//                 on ITS side -- bought for a long, sold for a short -- add up to at least `abs_x` times `big_qty` AND the price has gone no more than `abs_move_pct` per cent in the trade's favour over the same minutes. Many
//                 big buys that do not lift the price are being sold into (absorbed); many big sells that do not push it down are being bought. The numbers (10 minutes, 2 prints' worth, 0.10 %) are a first guess to be tuned,
//                 not a measured rule.
//
// ASSUMPTIONS to be changed if they are not what was meant: the candles are one hour long (`bar_minutes`) and the cross is the close of a one-hour candle against the VWAP (not the price minute by minute); the bands are fixed at the signal and do not follow the VWAP afterwards; the second step takes half of what is LEFT, not half of the original position (half of the original twice would leave nothing for a third); the third and last step is a full take-profit.
// HONEST NOTE: written to the specification and checked that it compiles and that the filter and the steps do what they say on made-up data; its edge has not been measured. The `@param` ranges are for the optimizer; the
// 5 prints, the 4 that must agree, the three steps and the 20 candles of the VWAP are the rule as asked, fixed.
// Category: host
// Permissions: orders.submit
// Preconditions: BTCUSDT aggregated prints and one-minute bars
// Side effects: asks the terminal to run the strategy; once running it places orders on the account the person chose.
// Run: Insert strategy in the Backtests window (backtest and optimize); Add script on a replay (the same handlers run there).

lang ">=0.2.0, <0.3.0";
host "ttt" ">=0.1.26, <0.2.0";
requires "features" ">=0.1.9, <0.2.0";
requires "handlers" ">=0.1.2, <0.2.0";
requires "trade" ">=0.1.0, <0.2.0";

import std.math;
import std.list;
import std.time;
import std.text;

const MINUTE_MS: Int = 60000;
// The tags of the orders that take a step: 11 is the first, 12 the second...
const STEP_TAG: Int = 10;
// The trailing stop is moved only when it would gain at least this per cent of the price: a stop moved every minute by a few cents is a stream of orders for nothing.
const TRAIL_MIN_PCT: Float = 0.02;

// One minute that held large prints: how many, and how many of them were bought by the aggressor (the rest were sold).
struct Flow {
    n: Float,
    buys: Float,
}

// One closed candle of the rolling VWAP: its typical price and its volume.
struct Candle {
    tp: Float,
    vol: Float,
}

// NOTE: `exiting` of a position is NOT used to know whether the strategy is already taking it out: the steps are exit orders of the strategy that sit waiting, so it is true for almost the whole of a trade.
// `leaving` is the strategy's own: set when it sends the close on absorption.
// What the strategy remembers: the candle in progress and the average of the closed ones, which side of it the price was last on, the large prints, and the position.
// `side`: 1 the price above the VWAP the last time it was off it, -1 below, 2 not known yet. `lvl_vwap` and `lvl_sd` are the VWAP and its deviation at the signal, which the bands of the trade are made from. `cross` is the cross a closed candle has just made (1 up, -1 down, 0 none). `phase`: 0 flat, 1 entry sent, 2 open and protected. `sgn` is 1 for a long, -1 for a short. `done` is how many steps have filled.
struct Memory {
    cur: Int,
    c_close: Float,
    c_hi: Float,
    c_lo: Float,
    closed: Int,
    c_vol: Float,
    candles: List<Candle>,
    vwap: Float,
    sd: Float,
    lvl_vwap: Float,
    lvl_sd: Float,
    cross: Int,
    side: Int,
    prints: List<Flow>,
    phase: Int,
    sgn: Int,
    entry: Float,
    opened_ms: Int,
    done: Int,
    stop: Float,
    peak: Float,
    leaving: Bool,
}

struct Step {
    state: Memory,
    actions: List<handlers.Act>,
}

fn begin() -> Memory {
    return Memory { cur: -1, c_close: 0.0, c_hi: 0.0, c_lo: 0.0, closed: -1, c_vol: 0.0, candles: [], vwap: 0.0, sd: 0.0, lvl_vwap: 0.0, lvl_sd: 0.0, cross: 0, side: 2, prints: [], phase: 0, sgn: 0, entry: 0.0, opened_ms: 0, done: 0, stop: 0.0, peak: 0.0, leaving: false };
}

fn whole(f: Float) -> Int {
    return text.parse_int(to_string(decimal_from_float(f, 0))).unwrap_or(0);
}

fn to_count(f: Float) -> Float {
    return to_float(whole(f));
}

fn market(side: handlers.Side, intent: handlers.Intent, size: Float) -> handlers.Act {
    return handlers.Act.Submit(handlers.Order {
        book: "main",
        side: side,
        intent: intent,
        qty: qty(decimal_from_float(size, 6)),
        limit: None,
        stop: None,
        take_profit: None,
        stop_loss: None,
        tag: 1,
        post_only: false,
        immediate: false,
        expire_minutes: 0,
    });
}

// A limit that only takes `size` off the position, resting at `price` until the market reaches it.
fn reduce_at(side: handlers.Side, price: Float, size: Float, tag: Int) -> handlers.Act {
    return handlers.Act.Submit(handlers.Order {
        book: "main",
        side: side,
        intent: handlers.Intent.Reduce,
        qty: qty(decimal_from_float(size, 6)),
        limit: Some(price),
        stop: None,
        take_profit: None,
        stop_loss: None,
        tag: tag,
        post_only: false,
        immediate: false,
        expire_minutes: 0,
    });
}

// A limit that takes the whole position out at `price` (a full take-profit): the quantity is ignored by the engine, `size` only has to be a valid one.
fn close_at(side: handlers.Side, price: Float, size: Float, tag: Int) -> handlers.Act {
    return handlers.Act.Submit(handlers.Order {
        book: "main",
        side: side,
        intent: handlers.Intent.Close,
        qty: qty(decimal_from_float(size, 6)),
        limit: Some(price),
        stop: None,
        take_profit: None,
        stop_loss: None,
        tag: tag,
        post_only: false,
        immediate: false,
        expire_minutes: 0,
    });
}

// How many of a minute's `n` large prints were bought. The engine gives the count and the volume each side bought and sold in large prints, not the side of each print: a minute with one side only is
// exact, and a mixed one is shared by volume, kept within what the volumes allow (each print is at least `min_qty`, and each side with volume has at least one).
fn bought_of(n: Float, bought: Float, sold: Float, min_qty: Float) -> Float {
    if n <= 0.0 {
        return 0.0;
    }
    if sold <= 0.0 {
        return n;
    }
    if bought <= 0.0 {
        return 0.0;
    }
    let mut lo: Float = 1.0;
    let mut hi: Float = n - 1.0;
    if n - sold / min_qty > lo {
        lo = n - sold / min_qty;
    }
    if bought / min_qty < hi {
        hi = bought / min_qty;
    }
    let mut x = to_count(n * bought / (bought + sold));
    if hi >= lo {
        if x < lo {
            x = to_count(lo + 0.5);
        }
        if x > hi {
            x = to_count(hi - 0.5);
        }
    }
    return x;
}

// The last `keep` minutes that held large prints (each holds at least one, so they always cover the last `keep` prints).
fn last_flows(flows: List<Flow>, keep: Int) -> List<Flow> {
    if keep > 0 && flows.len() > keep {
        match list.slice(flows, flows.len() - keep, flows.len()) {
            Some(tail) => {
                return tail;
            },
            None => {},
        }
    }
    return flows;
}

// The filter: of the last `last` large prints, counted back from the newest minute that held any, are at least `least` on the trade's side -- bought (`longs`, a long) or sold (a short)?
// A minute whose prints all lie among the last `last` counts its own; the minute the count ends in may have only some of its prints there, and in what order is not known: the prints that disagree are taken
// as the latest, so a doubtful read counts against the trade. Fewer than `last` large prints seen: no.
fn flow_agrees(prints: List<Flow>, last: Int, least: Int, longs: Bool) -> Bool {
    if last < 1 {
        return true;
    }
    let mut left = to_float(last);
    let mut agree: Float = 0.0;
    let mut i = prints.len();
    while i > 0 && left > 0.0 {
        i = i - 1;
        let p = prints[i];
        let yes = if longs { p.buys } else { p.n - p.buys };
        let no = p.n - yes;
        if p.n <= left {
            agree = agree + yes;
        } else if left > no {
            agree = agree + (left - no);
        }
        left = left - p.n;
    }
    return left <= 0.0 && agree >= to_float(least);
}

// The last `keep` closed candles.
fn last_candles(candles: List<Candle>, keep: Int) -> List<Candle> {
    if keep > 0 && candles.len() > keep {
        match list.slice(candles, candles.len() - keep, candles.len()) {
            Some(tail) => {
                return tail;
            },
            None => {},
        }
    }
    return candles;
}

// A candle has closed with this `hi`, `lo`, `close` and `vol`: it joins the window of the rolling VWAP -- the last `window` closed candles, each at its typical price weighted by its volume --, the VWAP and its deviation
// are worked out with it in, and `cross` says whether its close has gone over the VWAP (1) or under it (-1) from the other side.
fn candle_closed(m: Memory, hi: Float, lo: Float, close: Float, vol: Float, window: Int) -> Memory {
    let mut s = m;
    if vol > 0.0 {
        s.candles.push(Candle { tp: (hi + lo + close) / 3.0, vol: vol });
        s.candles = last_candles(s.candles, window);
    }
    if window >= 2 && s.candles.len() >= window {
        let mut total: Float = 0.0;
        let mut weighted: Float = 0.0;
        for c in s.candles {
            total = total + c.vol;
            weighted = weighted + c.tp * c.vol;
        }
        let mean = weighted / total;
        let mut spread: Float = 0.0;
        for c in s.candles {
            spread = spread + c.vol * (c.tp - mean) * (c.tp - mean);
        }
        s.vwap = mean;
        s.sd = math.sqrt(spread / total);
        let mut now_side = 0;
        if close > mean {
            now_side = 1;
        }
        if close < mean {
            now_side = -1;
        }
        if now_side != 0 {
            if s.side == -1 && now_side == 1 {
                s.cross = 1;
            }
            if s.side == 1 && now_side == -1 {
                s.cross = -1;
            }
            s.side = now_side;
        }
    }
    return s;
}

// The price of step `k` (1 is the first): `k` x `step_pct` per cent from the entry or, on the VWAP's bands, the band `k` x `band_step` deviations from the VWAP as it stood at the signal, on the trade's side. Step 0 is the entry.
fn level(s: Memory, k: Int, step_pct: Float, band_step: Float, bands: Bool) -> Float {
    let d = to_float(s.sgn);
    if k < 1 {
        return s.entry;
    }
    if bands {
        return s.lvl_vwap + d * to_float(k) * band_step * s.lvl_sd;
    }
    return s.entry * (1.0 + d * to_float(k) * step_pct / 100.0);
}

// On the bands, whether a signal has room: the stop (`sl_band` deviations on the losing side of the VWAP) under (over) the close, the first step (`band_step` on the other) beyond it, and a line with a width at all.
fn bands_ok(vwap: Float, sd: Float, close: Float, sgn: Int, sl_band: Float, band_step: Float) -> Bool {
    let d = to_float(sgn);
    return sd > 0.0 && (close - (vwap - d * sl_band * sd)) * d > 0.0 && (vwap + d * band_step * sd - close) * d > 0.0;
}

// Withdraws the orders that were waiting to take a step.
fn cancel_steps(steps: Int) -> List<handlers.Act> {
    let mut acts: List<handlers.Act> = [];
    let mut k = 1;
    while k <= steps {
        acts.push(handlers.Act.Cancel(STEP_TAG + k));
        k = k + 1;
    }
    return acts;
}

fn on_minute(m: Memory, ctx: handlers.Context, close: Float, hi: Float, lo: Float, vol: Float, large_n: Float, large_buy: Float, large_sell: Float, abs_buy: Float, abs_sell: Float, past: Float, bar_minutes: Float, vwap_window: Float, big_qty: Float, big_last: Float, big_least: Float, steps: Float, trailing: Float, trail_pct: Float, bands_exit: Float, sl_band: Float, band_step: Float, absorption: Float, abs_minutes: Float, abs_x: Float, abs_move_pct: Float, size: Float) -> Step {
    let now_ms = time.unix_ms(ctx.now);
    // The event is the close of a minute: its number is the one before the instant.
    let end_min = now_ms / MINUTE_MS;
    let mut length = whole(bar_minutes);
    if length < 1 {
        length = 1;
    }
    let window = whole(vwap_window);
    let last = whole(big_last);
    let least = whole(big_least);
    let mut s = m;
    let mut acts: List<handlers.Act> = [];

    // A position that is gone, or an entry that never filled, frees the strategy for the next cross.
    let held = ctx.positions.len() > 0;
    if s.phase == 2 && !held {
        s.phase = 0;
        s.done = 0;
        s.leaving = false;
    }
    if s.phase == 1 && !held && ctx.orders.len() == 0 {
        s.phase = 0;
    }

    // The candle of this minute. When a new one has begun and the one before it was not closed at its own last minute (a minute was not seen), it is closed now.
    s.cross = 0;
    let candle = (end_min - 1) / length;
    if candle != s.cur {
        if s.cur >= 0 && s.closed != s.cur {
            s = candle_closed(s, s.c_hi, s.c_lo, s.c_close, s.c_vol, window);
        }
        s.cur = candle;
        s.c_hi = hi;
        s.c_lo = lo;
        s.c_vol = 0.0;
    }
    if hi > s.c_hi {
        s.c_hi = hi;
    }
    if lo < s.c_lo {
        s.c_lo = lo;
    }
    s.c_close = close;
    s.c_vol = s.c_vol + vol;
    // The candle's last minute: it closes here, with this minute in it, and the entry is made at this very close.
    if end_min % length == 0 {
        s = candle_closed(s, s.c_hi, s.c_lo, s.c_close, s.c_vol, window);
        s.closed = s.cur;
    }

    // The minute's large prints, kept for the filter.
    if large_n > 0.0 {
        s.prints.push(Flow { n: large_n, buys: bought_of(large_n, large_buy, large_sell, big_qty) });
        s.prints = last_flows(s.prints, if last < 1 { 1 } else { last });
    }

    // The cross of the VWAP, if a candle has just closed on one. On the bands, only a signal that has room is traded.
    let on_bands = bands_exit >= 0.5;
    let cross = s.cross;

    // The entry: a cross, with the big prints on its side.
    let flat = s.phase == 0 && ctx.orders.len() == 0 && !held;
    if flat && cross == 1 && flow_agrees(s.prints, last, least, true) && (!on_bands || bands_ok(s.vwap, s.sd, close, 1, sl_band, band_step)) {
        s.phase = 1;
        s.sgn = 1;
        s.lvl_vwap = s.vwap;
        s.lvl_sd = s.sd;
        acts.push(market(handlers.Side.Buy, handlers.Intent.Open, size));
    }
    if flat && cross == -1 && flow_agrees(s.prints, last, least, false) && (!on_bands || bands_ok(s.vwap, s.sd, close, -1, sl_band, band_step)) {
        s.phase = 1;
        s.sgn = -1;
        s.lvl_vwap = s.vwap;
        s.lvl_sd = s.sd;
        acts.push(market(handlers.Side.Sell, handlers.Intent.Open, size));
    }

    // The trailing stop (optional): it follows the best price the position has seen, `trail_pct` behind it, and never goes back -- it moves with the price when it goes the trade's way and stands still when it goes against it.
    // It lives beside the steps' stops: whichever is nearer the price is the one that stands.
    if trailing >= 0.5 && s.phase == 2 && held && !s.leaving {
        let d = to_float(s.sgn);
        if s.sgn == 1 && hi > s.peak {
            s.peak = hi;
        }
        if s.sgn == -1 && lo < s.peak {
            s.peak = lo;
        }
        let want = s.peak * (1.0 - d * trail_pct / 100.0);
        if (want - s.stop) * d > s.stop * TRAIL_MIN_PCT / 100.0 {
            s.stop = want;
            let protection = handlers.Protection { book: "main", take_profit: None, stop_loss: Some(want) };
            acts.push(handlers.Act.Protect(protection));
        }
    }

    // Absorption: the big prints on the position's side keep coming and the price does not follow. Only judged once the position has lived the whole window.
    if absorption >= 0.5 && s.phase == 2 && held && !s.leaving && past > 0.0 {
        let lived = now_ms - s.opened_ms >= whole(abs_minutes) * MINUTE_MS;
        let on_its_side = if s.sgn == 1 { abs_buy } else { abs_sell };
        let progress = (close - past) / past * 100.0 * to_float(s.sgn);
        if lived && on_its_side >= abs_x * big_qty && progress < abs_move_pct {
            let out = if s.sgn == 1 { handlers.Side.Sell } else { handlers.Side.Buy };
            acts.push(market(out, handlers.Intent.Close, size));
            s.leaving = true;
            for cancel in cancel_steps(whole(steps)) {
                acts.push(cancel);
            }
        }
    }
    return Step { state: s, actions: acts };
}

// A fill. The entry: the stop goes on from the real entry price and the steps are left as resting limits. Later fills: when a step has filled completely the stop moves up behind it. A position that is gone: forget it.
fn on_fill(m: Memory, ctx: handlers.Context, sl_pct: Float, step_pct: Float, steps: Float, partial_pct: Float, bands_exit: Float, sl_band: Float, band_step: Float, size: Float) -> Step {
    let mut s = m;
    let mut acts: List<handlers.Act> = [];
    let n = whole(steps);
    let bands = bands_exit >= 0.5;
    if s.phase == 1 && ctx.positions.len() > 0 {
        let entry = ctx.positions[0].entry_price;
        let d = to_float(s.sgn);
        let out = if s.sgn == 1 { handlers.Side.Sell } else { handlers.Side.Buy };
        s.phase = 2;
        s.entry = entry;
        s.done = 0;
        s.leaving = false;
        s.opened_ms = time.unix_ms(ctx.now);
        s.stop = if bands { s.lvl_vwap - d * sl_band * s.lvl_sd } else { entry * (1.0 - d * sl_pct / 100.0) };
        s.peak = entry;
        if bands && ((entry - s.stop) * d <= 0.0 || (level(s, 1, step_pct, band_step, true) - entry) * d <= 0.0) {
            // The market filled past the band the stop or the first step were to sit on: there is no room for them, so the position is closed rather than left half protected.
            s.phase = 0;
            return Step { state: s, actions: [market(out, handlers.Intent.Close, size)] };
        }
        let protection = handlers.Protection { book: "main", take_profit: None, stop_loss: Some(s.stop) };
        acts.push(handlers.Act.Protect(protection));
        // The steps: each at its price from the entry. All but the last take their share of what is left when their turn comes; the last is the full take-profit and closes whatever is left.
        let mut left = size;
        let mut k = 1;
        while k <= n {
            let at = level(s, k, step_pct, band_step, bands);
            if k == n {
                // The full take-profit is left later, when the step before it has filled (see the next fills): an order to close the whole position sent now would be for all of it, and once the steps
                // had taken their share it would fill only what is left, stay half-filled and alive for ever -- and the strategy waits for no order to be working before it enters again. With a single step there is
                // nothing before it.
                if n == 1 {
                    acts.push(close_at(out, at, left, STEP_TAG + k));
                }
            } else {
                let part = left * partial_pct / 100.0;
                acts.push(reduce_at(out, at, part, STEP_TAG + k));
                left = left - part;
            }
            k = k + 1;
        }
        return Step { state: s, actions: acts };
    }
    if s.phase == 2 && ctx.positions.len() > 0 {
        // A step is done when its order is no longer working: the ones still resting are the steps not taken. Only the steps before the last are counted; the last is not left until they are done.
        let mut waiting = 0;
        for o in ctx.orders {
            if o.tag > STEP_TAG && o.tag < STEP_TAG + n {
                waiting = waiting + 1;
            }
        }
        let done = n - 1 - waiting;
        if done > s.done {
            s.done = done;
            // The stop goes to the price of the step before the one that has just filled: the entry after the first, the first step's price after the second...
            let stop = level(s, done - 1, step_pct, band_step, bands);
            // Stops only come nearer: one the trailing stop has already passed stays where it is.
            if (stop - s.stop) * to_float(s.sgn) > 0.0 {
                s.stop = stop;
                let protection = handlers.Protection { book: "main", take_profit: None, stop_loss: Some(stop) };
                acts.push(handlers.Act.Protect(protection));
            }
            // The steps before the last are all taken: the full take-profit is left now, for whatever the position holds.
            if done == n - 1 {
                let out = if s.sgn == 1 { handlers.Side.Sell } else { handlers.Side.Buy };
                acts.push(close_at(out, level(s, n, step_pct, band_step, bands), size, STEP_TAG + n));
            }
        }
        return Step { state: s, actions: acts };
    }
    if s.phase == 2 && ctx.positions.len() == 0 {
        s.phase = 0;
        s.done = 0;
        s.leaving = false;
        return Step { state: s, actions: cancel_steps(n) };
    }
    return Step { state: s, actions: [] };
}

strategy vwap_cross_big_trades(
    bar_minutes: Int,
    vwap_window: Int,
    @param(label: "Large print (BTC)", min: 20, max: 100, step: 5, group: "Signal", hint: "A single print at least this big. The terminal's Large trades at 50x is 50 BTC.") big_qty: Decimal,
    big_last: Int,
    big_least: Int,
    @param(label: "Stop (%)", min: 0.5, max: 3.0, step: 0.25, group: "Exit", hint: "The stop is this per cent of the entry price on the losing side. 1 is 1 %.") sl_pct: Decimal,
    @param(label: "Step (%)", min: 0.5, max: 3.0, step: 0.25, group: "Exit", hint: "Each step is this per cent further from the entry in the trade's favour: the first at 1x, the second at 2x, the third at 3x.") step_pct: Decimal,
    steps: Int,
    @param(label: "Taken at each step (%)", min: 25, max: 75, step: 5, group: "Exit", hint: "The share of what is left that each step takes, except the last, which takes all of it. 50 takes half, then half of the rest, then the rest.") partial_pct: Decimal,
    @param(switch: true, label: "Trailing stop", group: "Exit", hint: "On: the stop follows the best price the trade has reached, Trail distance behind it. It moves up (a long) or down (a short) as the price goes the trade's way and stays where it is when the price goes against it; the steps' stops still apply, and the nearer one stands. Off: only the stop and the steps.") trailing: Int,
    @param(label: "Trail distance (%)", min: 0.5, max: 3.0, step: 0.25, group: "Exit", hint: "How far behind the best price the trailing stop stays. 1 is 1 % of that price.") trail_pct: Decimal,
    @param(switch: true, label: "TP and SL on the VWAP bands", group: "Bands", hint: "On: the stop and the three steps sit on the bands of the rolling VWAP, as they stood at the signal, instead of at per cents of the entry. The stop is the band Stop band on the losing side; the steps are the bands Step band, twice and three times that on the trade's side, the last the full take-profit. Off: Stop (%) and Step (%).") bands_exit: Int,
    @param(label: "Stop band (std devs)", min: 0.5, max: 3.0, step: 0.5, group: "Bands", hint: "The stop sits this many deviations of the VWAP on the losing side of it: under the line for a long, over it for a short. 1 is the chart's first band.") sl_band: Decimal,
    @param(label: "Step band (std devs)", min: 0.5, max: 2.0, step: 0.5, group: "Bands", hint: "The steps sit at this many deviations of the VWAP on the trade's side, twice and three times that: 1 puts them on the chart's bands 1, 2 and 3.") band_step: Decimal,
    @param(switch: true, label: "Close on absorption", group: "Absorption", hint: "On: close the position when the large prints on its own side add up to the volume below and the price has not moved on, which reads as the other side absorbing them. Off: only the stop and the steps.") absorption: Int,
    @param(label: "Absorption window (minutes)", min: 2, max: 30, step: 1, group: "Absorption", hint: "Looked at only once the position has lived this long, over this many minutes.") abs_minutes: Int,
    @param(label: "Large volume on its side (x the print)", min: 1.0, max: 6.0, step: 0.5, group: "Absorption", hint: "At least this many times the large print size, bought for a long and sold for a short, in the window.") abs_x: Decimal,
    @param(label: "Price progress under (%)", min: 0.02, max: 0.5, step: 0.02, group: "Absorption", hint: "The price has gone less than this per cent in the trade's favour over the window.") abs_move_pct: Decimal,
    size: Decimal,
) version "1.0.0" {
    let close = features.bar_close();
    let hi = features.bar_high();
    let lo = features.bar_low();
    let vol = features.bar_volume();
    // The large prints of each minute alone, and on which side the aggressor was; and the same over the absorption window.
    let src = features.Source.AggTrades;
    let large_n = features.large_count(src, big_qty, 1);
    let large_buy = features.large_buy_volume(src, big_qty, 1);
    let large_sell = features.large_sell_volume(src, big_qty, 1);
    let abs_buy = features.large_buy_volume(src, big_qty, abs_minutes);
    let abs_sell = features.large_sell_volume(src, big_qty, abs_minutes);
    let past = features.close_ago(abs_minutes);
    state = begin();
    handler bar(1) = on_minute(ctx, close, hi, lo, vol, large_n, large_buy, large_sell, abs_buy, abs_sell, past, bar_minutes, vwap_window, big_qty, big_last, big_least, steps, trailing, trail_pct, bands_exit, sl_band, band_step, absorption, abs_minutes, abs_x, abs_move_pct, size);
    handler fill = on_fill(ctx, sl_pct, step_pct, steps, partial_pct, bands_exit, sl_band, band_step, size);
}

fn main() -> Result<Unit, Error> {
    // The rule as asked: the price crossing the VWAP of 20 one-hour candles, with at least 4 of the last 5 large prints (50 BTC or more) on its side; a 1 % stop; three steps of 1 %, the first two taking half of what is left and the last everything, the
    // stop following behind; no trailing stop (trailing 0); the stop and the steps in per cents, not on the bands (bands_exit 0); no absorption exit (absorption 0).
    let run = trade.run(vwap_cross_big_trades(60, 20, 50, 5, 4, 1.0, 1.0, 3, 50, 0, 1.0, 0, 1.0, 1.0, 0, 10, 2.0, 0.1, 0.01))?;
    print(run.name + " on " + run.target + ": " + run.state);
    return Ok(());
}