Skip to content
← All examples

Example 80

Large trade wick fade

fade a one-hour candle whose wick is longer than its body and holds large prints. If the candle's upper wick is longer than its body and at least `min_prints` large AGGRESSIVE BUYS traded in that

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: fade a one-hour candle whose wick is longer than its body and holds large prints. If the candle's upper wick is longer than its body and at least `min_prints` large AGGRESSIVE BUYS traded in that
// wick, the buyers were rejected there: sell, with a target `tp_pct` and a stop `sl_pct` from the entry. The mirror: a lower wick longer than the body with at least `min_prints` large aggressive SELLS in it
// is bought. Prints of the opposite direction are not a condition. A candle with both wicks qualifying trades the one with more prints (none when they tie: the two signals contradict each other).
//
//   LARGE PRINT   a single print of at least `large_qty` of the instrument (the terminal's "Large trades" mode, threshold 18 x its base, is 18 BTC on BTC: the base is the chart's reference, floored at 1 BTC).
//   WICK          the upper wick is the high above the body's top (the larger of open and close), the lower wick the body's bottom under the low. It must exceed `wick_mult` times the body.
//   IN THE WICK   the engine tells how many large prints a minute held, how much of it was bought and sold, and the minute's own VWAP -- not the price of each print. A minute's large prints are therefore
//                 placed by that minute's VWAP (above the body's top: in the upper wick). Prints of the other side do not matter: the upper wick needs `min_prints` buys, whatever sold in it. A minute with
//                 only one side is exact; a minute with both is shared by volume (within what the volumes allow). Prints are few, so this is close to the exact count; it is not the exact count.
//   BAND          the candle must also touch the second band of the rolling VWAP of 20 candles, the upper or the lower: any part of it, wick or body (its low-to-high range), reaches `vwap ± band_k x sd`.
//                 It is the chart's own "Rolling VWAP" (window 20, band 2): over the last `vwap_window` one-hour candles, each at its typical price (high + low + close) / 3 weighted by its volume, the mean
//                 and the volume-weighted standard deviation of those prices, the candle being judged included. Until 20 candles have closed there is no band and no trade.
//   DIRECTION     `candle_filter` 0 (the default): the colour of the candle does not matter -- a short needs the long upper wick with its buys, and the candle may close up or down. 1: only against the candle,
//                 a short on a candle that closed up (close above open), a long on one that closed down.
//   ENTRY         at the market, when the hour closes. One position at a time: a candle that qualifies while a position is open or an order is working is passed over.
//   EXIT          the target and the stop, set on the position when the entry fills, from its real entry price. Nothing else closes it.
//
// `candle_filter` is an on/off switch in the terminal's Parameters panel (`@param(switch: true)`).
// The optimizer moves three numbers (ADR-0015): the size of a large print (`large_qty`), the target and the stop. The prints needed (`min_prints`), the wick over the body (`wick_mult`) and the band
// (`vwap_window`, `band_k`) are the rule as asked, fixed; give them an `@param` to optimize them too. With the defaults the rule is the one asked for: 18 BTC prints, three of them, a wick over the body,
// touching band 2 of the 20-candle rolling VWAP, a 1.5 % target, a 1 % stop.
// 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;
const HOUR_MINUTES: Int = 60;

// One minute that held large prints: where it traded (its VWAP) and how many were bought and how many sold by the aggressor.
struct Print {
    vwap: Float,
    buys: Float,
    sells: Float,
}

// One closed one-hour candle as the rolling VWAP sees it: its typical price and its volume.
struct Candle {
    tp: Float,
    vol: Float,
}

// What the strategy remembers: the minute-to-minute running totals of the session (for each minute's own VWAP), the hour being built, the closed candles of the VWAP window, and where the position is.
// `phase`: 0 flat, 1 entry sent, 2 position open and protected. `sgn` is 1 for a long, -1 for a short.
struct Memory {
    prev_cv: Float,
    prev_pv: Float,
    hour: Int,
    minutes: Int,
    open: Float,
    high: Float,
    low: Float,
    close: Float,
    hour_vol: Float,
    prints: List<Print>,
    candles: List<Candle>,
    phase: Int,
    sgn: Int,
}

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

fn begin() -> Memory {
    return Memory { prev_cv: 0.0, prev_pv: 0.0, hour: -1, minutes: 0, open: 0.0, high: 0.0, low: 0.0, close: 0.0, hour_vol: 0.0, prints: [], candles: [], phase: 0, sgn: 0 };
}

fn market(side: handlers.Side, size: Float) -> handlers.Act {
    return handlers.Act.Submit(handlers.Order {
        book: "main",
        side: side,
        intent: handlers.Intent.Open,
        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,
    });
}

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));
}

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

// Whether the candle (its low-to-high range, wick and body) reaches the band at `k` standard deviations of the rolling VWAP of `candles`, above or below it: the chart's "Rolling VWAP" band, from the
// typical price of each candle weighted by its volume.
fn touches_band(candles: List<Candle>, k: Float, hi: Float, lo: Float) -> Bool {
    let mut total: Float = 0.0;
    let mut weighted: Float = 0.0;
    for c in candles {
        total = total + c.vol;
        weighted = weighted + c.tp * c.vol;
    }
    if total <= 0.0 {
        return false;
    }
    let mean = weighted / total;
    let mut spread: Float = 0.0;
    for c in candles {
        spread = spread + c.vol * (c.tp - mean) * (c.tp - mean);
    }
    let sd = math.sqrt(spread / total);
    let upper = mean + k * sd;
    let lower = mean - k * sd;
    return (lo <= upper && hi >= upper) || (lo <= lower && hi >= lower);
}

// 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 large prints the candle's wick held: buys above the body's top, or sells under its bottom. The other side's prints do not matter.
fn prints_in_wick(prints: List<Print>, buys: Bool, edge: Float) -> Float {
    let mut n: Float = 0.0;
    for p in prints {
        if buys && p.vwap > edge {
            n = n + p.buys;
        }
        if !buys && p.vwap < edge {
            n = n + p.sells;
        }
    }
    return n;
}

// The side to trade for a finished candle: 1 long, -1 short, 0 none.
fn verdict(s: Memory, wick_mult: Float, min_prints: Float, against: Bool) -> Int {
    let top = if s.open > s.close { s.open } else { s.close };
    let bottom = if s.open > s.close { s.close } else { s.open };
    let body = top - bottom;
    let upper = s.high - top;
    let lower = bottom - s.low;
    let mut up_prints: Float = 0.0;
    let mut down_prints: Float = 0.0;
    if upper > wick_mult * body && upper > 0.0 {
        up_prints = prints_in_wick(s.prints, true, top);
    }
    if lower > wick_mult * body && lower > 0.0 {
        down_prints = prints_in_wick(s.prints, false, bottom);
    }
    let mut sell_ok = up_prints >= min_prints;
    let mut buy_ok = down_prints >= min_prints;
    if against {
        sell_ok = sell_ok && s.close > s.open;
        buy_ok = buy_ok && s.close < s.open;
    }
    if sell_ok && (!buy_ok || up_prints > down_prints) {
        return -1;
    }
    if buy_ok && (!sell_ok || down_prints > up_prints) {
        return 1;
    }
    return 0;
}

fn on_minute(m: Memory, ctx: handlers.Context, op: Float, hi: Float, lo: Float, close: Float, cumvol: Float, cumvwap: Float,
    large_buy: Float, large_sell: Float, large_n: Float, large_qty: Float, wick_mult: Float, min_prints: Float, vwap_window: Float, band_k: Float, candle_filter: 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, and the hour closed when the instant is a boundary of the hour.
    let end_min = now_ms / MINUTE_MS;
    let mut s = m;
    let mut acts: List<handlers.Act> = [];

    // This minute's own volume-weighted average price, from what the session had traded before it and after it.
    let pv = cumvol * cumvwap;
    let moved = cumvol - s.prev_cv;
    let mut mv = close;
    let mut minute_vol: Float = 0.0;
    if s.prev_cv > 0.0 && moved > 0.0 && cumvol >= s.prev_cv {
        mv = (pv - s.prev_pv) / moved;
        minute_vol = moved;
    }
    s.prev_cv = cumvol;
    s.prev_pv = pv;

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

    // The hour of the closed minute: a new one starts its candle, minute by minute.
    let hour = (end_min - 1) / HOUR_MINUTES;
    if hour != s.hour {
        s.hour = hour;
        s.minutes = 0;
        s.open = op;
        s.high = hi;
        s.low = lo;
        s.hour_vol = 0.0;
        s.prints = [];
    }
    s.minutes = s.minutes + 1;
    s.hour_vol = s.hour_vol + minute_vol;
    if hi > s.high {
        s.high = hi;
    }
    if lo < s.low {
        s.low = lo;
    }
    s.close = close;
    if large_n > 0.0 {
        let bought = bought_of(large_n, large_buy, large_sell, large_qty);
        s.prints.push(Print { vwap: mv, buys: bought, sells: large_n - bought });
    }

    // The hour is over: judge the candle, if every one of its minutes was seen.
    if end_min % HOUR_MINUTES == 0 && s.minutes == HOUR_MINUTES {
        // The candle joins the window of the rolling VWAP, whatever the strategy is doing; the band it is judged against includes it.
        let typical = (s.high + s.low + s.close) / 3.0;
        s.candles.push(Candle { tp: typical, vol: s.hour_vol });
        s.candles = last_candles(s.candles, whole(vwap_window));
        if s.phase == 0 && ctx.orders.len() == 0 {
            let mut side = verdict(s, wick_mult, min_prints, candle_filter >= 0.5);
            if to_float(s.candles.len()) < vwap_window || !touches_band(s.candles, band_k, s.high, s.low) {
                side = 0;
            }
            if side == 1 {
                s.phase = 1;
                s.sgn = 1;
                acts.push(market(handlers.Side.Buy, size));
            }
            if side == -1 {
                s.phase = 1;
                s.sgn = -1;
                acts.push(market(handlers.Side.Sell, size));
            }
        }
    }
    return Step { state: s, actions: acts };
}

// The entry filled: the target and the stop go on the position, measured from its real entry price.
fn on_fill(m: Memory, ctx: handlers.Context, tp_pct: Float, sl_pct: Float) -> Step {
    let mut s = m;
    if s.phase == 1 && ctx.positions.len() > 0 {
        let entry = ctx.positions[0].entry_price;
        let d = to_float(s.sgn);
        s.phase = 2;
        let protection = handlers.Protection {
            book: "main",
            take_profit: Some(entry * (1.0 + d * tp_pct / 100.0)),
            stop_loss: Some(entry * (1.0 - d * sl_pct / 100.0)),
        };
        return Step { state: s, actions: [handlers.Act.Protect(protection)] };
    }
    if s.phase == 2 && ctx.positions.len() == 0 {
        s.phase = 0;
    }
    return Step { state: s, actions: [] };
}

strategy large_trade_wick_fade(
    @param(label: "Large print (BTC)", min: 5, max: 60, step: 1, group: "Signal", hint: "A single print at least this big. The terminal's Large trades at 18x is 18 BTC.") large_qty: Decimal,
    min_prints: Int,
    wick_mult: Decimal,
    vwap_window: Int,
    band_k: Decimal,
    @param(switch: true, label: "Only against the candle's colour", group: "Signal", hint: "On: a short needs a candle that closed up and a long one that closed down. Off: any colour.") candle_filter: Int,
    @param(label: "Target (%)", min: 0.5, max: 4.0, step: 0.25, group: "Exit", hint: "1.5 is 1.5 % of the entry price.") tp_pct: Decimal,
    @param(label: "Stop (%)", min: 0.25, max: 3.0, step: 0.25, group: "Exit", hint: "1 is 1 % of the entry price.") sl_pct: Decimal,
    size: Decimal,
) version "1.0.0" {
    let src = features.Source.AggTrades;
    // The closed minute, and what the session had traded by its close: the handler works out the minute's own VWAP from the cumulative pair.
    let op = features.bar_open();
    let hi = features.bar_high();
    let lo = features.bar_low();
    let close = features.bar_close();
    let cumvol = features.volume(src);
    let cumvwap = features.vwap(src);
    // The large prints of that minute alone, and on which side the aggressor was.
    let large_buy = features.large_buy_volume(src, large_qty, 1);
    let large_sell = features.large_sell_volume(src, large_qty, 1);
    let large_n = features.large_count(src, large_qty, 1);
    state = begin();
    handler bar(1) = on_minute(ctx, op, hi, lo, close, cumvol, cumvwap, large_buy, large_sell, large_n, large_qty, wick_mult, min_prints, vwap_window, band_k, candle_filter, size);
    handler fill = on_fill(ctx, tp_pct, sl_pct);
}

fn main() -> Result<Unit, Error> {
    // The rule as asked: 18 BTC prints, three in the wick, a wick over the body, touching band 2 of the 20-candle rolling VWAP, any candle colour (candle_filter 0), a 1.5 % target and a 1 % stop.
    let run = trade.run(large_trade_wick_fade(18, 3, 1.0, 20, 2.0, 0, 1.5, 1.0, 0.01))?;
    print(run.name + " on " + run.target + ": " + run.state);
    return Ok(());
}