← Todos los ejemplos
Ejemplo 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
- 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 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(()); }