Ir al contenido
← Todos los ejemplos

Ejemplo 81

Vwap touch fade

fade the touch of the rolling VWAP. When the price reaches the rolling VWAP of 20 candles it is bought if it came down onto the line and sold if it came up to it -- against the cross --, with a target on a

Categoría
Host: necesita los datos y el motor de la terminal
Permisos
orders.submit
Antes de ejecutarlo
BTCUSDT aggregated prints and one-minute bars
Qué hace
asks the terminal to run the strategy; once running it places orders on the account the person chose.
Cómo ejecutarlo
Insert strategy in the Backtests window (backtest and optimize); Add script on a replay (the same handlers run there).
Lenguaje
>=0.2.0, <0.3.0
Host
>=0.1.26, <0.2.0
Necesita
features >=0.1.9, <0.2.0handlers >=0.1.2, <0.2.0trade >=0.1.0, <0.2.0

Código

// Purpose: fade the touch of the rolling VWAP. When the price reaches the rolling VWAP of 20 candles it is bought if it came down onto the line and sold if it came up to it -- against the cross --, with a target on a
// band of that same VWAP (`band_k` deviations, upper for a long, lower for a short) that FOLLOWS the latest band while the trade is open, and a stop as far from the entry as the target was when it was entered (1:1).
//
//   ROLLING VWAP  the chart's own "Rolling VWAP": over the last `vwap_window` candles of `bar_minutes` minutes (20 one-hour candles by default), each at its typical price (high + low + close) / 3 weighted by its volume,
//                 the mean and the volume-weighted standard deviation of those prices; a band is `vwap ± k x sd`. The candle in progress is in it, as on the chart, so the line moves every minute.
//                 There is no line until `vwap_window` candles (counting the one in progress) have been seen: 20 hours of a run, by default.
//   TOUCH         the engine runs a handler on each closed minute, so a touch is seen at a minute's resolution: the minute's low-to-high range contains the line. The side the price came from is where the previous
//                 minute closed (above or under the line): a touch from above buys, a touch from below sells. A strategy that has not seen a close on either side yet waits for one.
//   ATR FILTER    no entry while the ATR is above `atr_max` (600 by default; 0 turns it off): the chart's ATR of `atr_period` candles (14), the plain mean of their true ranges -- the largest of high - low and the
//                 gaps from the previous close -- in price units, taken over the CLOSED candles, so it does not move within the candle. None until `atr_period` candles have closed. Exits are not affected.
//   ENTRY         at the market, once the minute that touched has closed. One position at a time. Not taken when the band the target would sit on is not beyond the price (a collapsed band).
//   TARGET        `tp_mode` 0 (the default): the band at `band_k` deviations on the side of the trade (a long aims at the upper band, a short at the lower), put on the position when the entry fills and then MOVED to the latest band each minute
//                 (when it has moved by more than 0.01 %). It only moves while it stays beyond the entry price: a band that drifts to the entry's other side leaves the target where it was, and never turns a target into a loss.
//                 `tp_mode` 1: a fixed distance, `tp_pct` per cent of the real entry price in the direction of the trade; it does not move, and `band_k` is not used.
//   STOP          `stop_ratio` times the distance from the entry price to the target (the band's or the fixed one) when the entry filled, on the losing side: 1.0 is 1:1 (as far as the target), 0.5 half of it. It does not move.
//
// `tp_mode` is an on/off switch in the terminal's Parameters panel (`@param(switch: true)`) and `atr_max` a filter with a switch beside its value (`off: 0`): the panel says what is on and what is off.
// The optimizer moves three numbers (ADR-0015): the target band (`band_k`), the fixed target (`tp_pct`) and the stop (`stop_ratio`) -- with `tp_mode` 0 only the band counts, with 1 only the fixed target; the candle length and the window 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 target is moved only when the latest band is this many per cent of the price away from where it rests.
const RETARGET_PCT: Float = 0.01;

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

// The rolling VWAP and its volume-weighted deviation; `ok` is false while the window is not full.
struct Band {
    vwap: Float,
    sd: Float,
    ok: Bool,
}

// What the strategy remembers: the candle in progress, the closed ones of the window, the side of the line the last minute closed on, and the position.
// `phase`: 0 flat, 1 entry sent, 2 position open and protected. `sgn` is 1 for a long, -1 for a short. `target` is the band the entry aimed at, `sent_tp` the one resting on the position, `stop` the stop beside it.
struct Memory {
    cur: Int,
    c_hi: Float,
    c_lo: Float,
    c_close: Float,
    c_vol: Float,
    candles: List<Candle>,
    ranges: List<Float>,
    prev_close: Float,
    side: Int,
    phase: Int,
    sgn: Int,
    target: Float,
    entry: Float,
    stop: Float,
    sent_tp: Float,
}

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

fn begin() -> Memory {
    return Memory { cur: -1, c_hi: 0.0, c_lo: 0.0, c_close: 0.0, c_vol: 0.0, candles: [], ranges: [], prev_close: 0.0, side: 0, phase: 0, sgn: 0, target: 0.0, entry: 0.0, stop: 0.0, sent_tp: 0.0 };
}

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

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

// The last `n` closed candles.
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;
}

// The true range of a candle: its own range or the gap from the previous close, whichever is larger (no previous close yet: its own range).
fn true_range(hi: Float, lo: Float, prev_close: Float) -> Float {
    let mut tr = hi - lo;
    if prev_close > 0.0 {
        let up = hi - prev_close;
        let down = prev_close - lo;
        if up > tr {
            tr = up;
        }
        if down > tr {
            tr = down;
        }
    }
    return tr;
}

// The last `n` true ranges.
fn last_ranges(ranges: List<Float>, n: Int) -> List<Float> {
    if n > 0 && ranges.len() > n {
        match list.slice(ranges, ranges.len() - n, ranges.len()) {
            Some(tail) => {
                return tail;
            },
            None => {},
        }
    }
    return ranges;
}

// The ATR: the mean of the last `period` true ranges, or -1 while fewer candles than that have closed.
fn atr_of(ranges: List<Float>, period: Int) -> Float {
    if period < 1 || ranges.len() < period {
        return -1.0;
    }
    let mut total: Float = 0.0;
    for r in ranges {
        total = total + r;
    }
    return total / to_float(ranges.len());
}

// The rolling VWAP over the closed candles and the one in progress (`tp`, `vol`), as the chart draws it: the mean of the typical prices weighted by volume and their weighted deviation.
fn band_of(candles: List<Candle>, tp: Float, vol: Float, window: Int) -> Band {
    let mut total: Float = vol;
    let mut weighted: Float = tp * vol;
    for c in candles {
        total = total + c.vol;
        weighted = weighted + c.tp * c.vol;
    }
    if candles.len() + 1 < window || total <= 0.0 {
        return Band { vwap: 0.0, sd: 0.0, ok: false };
    }
    let mean = weighted / total;
    let mut spread: Float = vol * (tp - mean) * (tp - mean);
    for c in candles {
        spread = spread + c.vol * (c.tp - mean) * (c.tp - mean);
    }
    return Band { vwap: mean, sd: math.sqrt(spread / total), ok: true };
}

fn on_minute(m: Memory, ctx: handlers.Context, hi: Float, lo: Float, close: Float, vol: Float, vwap_window: Float, bar_minutes: Float, band_k: Float, tp_mode: Float, atr_period: Float, atr_max: 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 window = whole(vwap_window);
    let mut length = whole(bar_minutes);
    if length < 1 {
        length = 1;
    }
    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 touch.
    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 candle of this minute: a new one closes the previous into the window.
    let candle = (end_min - 1) / length;
    if candle != s.cur {
        if s.cur >= 0 && s.c_vol > 0.0 {
            s.candles.push(Candle { tp: (s.c_hi + s.c_lo + s.c_close) / 3.0, vol: s.c_vol });
            s.candles = last_candles(s.candles, window - 1);
            s.ranges.push(true_range(s.c_hi, s.c_lo, s.prev_close));
            s.ranges = last_ranges(s.ranges, whole(atr_period));
            s.prev_close = s.c_close;
        }
        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;

    let band = band_of(s.candles, (s.c_hi + s.c_lo + s.c_close) / 3.0, s.c_vol, window);
    if band.ok {
        // The touch, from the side the previous minute closed on: the line is inside this minute's range.
        let touched = lo <= band.vwap && hi >= band.vwap;
        let fixed = tp_mode >= 0.5;
        let calm = atr_max <= 0.0 || (atr_of(s.ranges, whole(atr_period)) >= 0.0 && atr_of(s.ranges, whole(atr_period)) <= atr_max);
        if s.phase == 0 && ctx.orders.len() == 0 && !held && touched && band.sd > 0.0 && calm {
            if s.side == 1 {
                // Came down onto the line: buy, aiming at the upper band.
                let aim = band.vwap + band_k * band.sd;
                if fixed || aim > close {
                    s.phase = 1;
                    s.sgn = 1;
                    s.target = aim;
                    acts.push(market(handlers.Side.Buy, handlers.Intent.Open, size));
                }
            }
            if s.side == -1 {
                // Came up to the line: sell, aiming at the lower band.
                let aim = band.vwap - band_k * band.sd;
                if fixed || aim < close {
                    s.phase = 1;
                    s.sgn = -1;
                    s.target = aim;
                    acts.push(market(handlers.Side.Sell, handlers.Intent.Open, size));
                }
            }
        }
        // The position is open: the target follows the latest band, while it stays beyond the entry (a fixed target does not move).
        if s.phase == 2 && held && !fixed {
            let want = band.vwap + to_float(s.sgn) * band_k * band.sd;
            let beyond = if s.sgn == 1 { want > s.entry } else { want < s.entry };
            let mut gap = want - s.sent_tp;
            if gap < 0.0 {
                gap = 0.0 - gap;
            }
            if beyond && gap > s.sent_tp * RETARGET_PCT / 100.0 {
                s.sent_tp = want;
                let protection = handlers.Protection { book: "main", take_profit: Some(want), stop_loss: Some(s.stop) };
                acts.push(handlers.Act.Protect(protection));
            }
        }
        if close > band.vwap {
            s.side = 1;
        }
        if close < band.vwap {
            s.side = -1;
        }
    }
    return Step { state: s, actions: acts };
}

// The entry filled: the target (the band the entry aimed at, or `tp_pct` per cent of the entry price) and a stop on the other side go on the position, measured from its real entry price.
fn on_fill(m: Memory, ctx: handlers.Context, tp_mode: Float, tp_pct: Float, stop_ratio: Float, size: 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);
        if tp_mode >= 0.5 {
            s.target = entry * (1.0 + d * tp_pct / 100.0);
        }
        let reach = (s.target - entry) * d;
        s.entry = entry;
        if reach <= 0.0 {
            // The market filled beyond the band it aimed at: there is no target to speak of, so the position is closed rather than left with a stop only.
            s.phase = 0;
            let out = if s.sgn == 1 { handlers.Side.Sell } else { handlers.Side.Buy };
            return Step { state: s, actions: [market(out, handlers.Intent.Close, size)] };
        }
        s.phase = 2;
        s.stop = entry - d * reach * stop_ratio;
        s.sent_tp = s.target;
        let protection = handlers.Protection { book: "main", take_profit: Some(s.target), stop_loss: Some(s.stop) };
        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 vwap_touch_fade(
    bar_minutes: Int,
    vwap_window: Int,
    @param(label: "Target band (std devs)", min: 0.5, max: 3.0, step: 0.5, group: "Exit", hint: "The target sits on the band at this many deviations of the rolling VWAP, and follows it. The chart's bands are 1, 2 and 3.") band_k: Decimal,
    @param(label: "Stop (x target distance)", min: 0.25, max: 3.0, step: 0.25, group: "Exit", hint: "1 is a 1:1 stop, as far from the entry as the target was.") stop_ratio: Decimal,
    @param(switch: true, label: "Fixed-distance target", group: "Exit", hint: "On: the target is Fixed target (%) from the entry and does not move. Off: the band at Target band, which follows the VWAP.") tp_mode: Int,
    atr_period: Int,
    @param(label: "ATR ceiling", min: 100, max: 2000, step: 100, off: 0, group: "Filters", hint: "No entry while the ATR of 14 candles is above this, in price units. Off: no ceiling.") atr_max: Decimal,
    @param(label: "Fixed target (%)", min: 0.25, max: 4.0, step: 0.25, group: "Exit", hint: "Used when the target mode is 1: this per cent of the entry price, and it does not move.") tp_pct: Decimal,
    size: Decimal,
) version "1.0.0" {
    let hi = features.bar_high();
    let lo = features.bar_low();
    let close = features.bar_close();
    let vol = features.bar_volume();
    state = begin();
    handler bar(1) = on_minute(ctx, hi, lo, close, vol, vwap_window, bar_minutes, band_k, tp_mode, atr_period, atr_max, size);
    handler fill = on_fill(ctx, tp_mode, tp_pct, stop_ratio, size);
}

fn main() -> Result<Unit, Error> {
    // The rule as asked: the rolling VWAP of 20 one-hour candles, fade its touch, target on the first band and a 1:1 stop (stop_ratio 1).
    let run = trade.run(vwap_touch_fade(60, 20, 1.0, 1.0, 0, 14, 600, 1.0, 0.01))?;
    print(run.name + " on " + run.target + ": " + run.state);
    return Ok(());
}