← 모든 예제
예제 83
Vwap candle big trades fade
example 82 with one more filter, the large prints before the entry. Fade a one-hour candle that reaches the THIRD band of the rolling VWAP, but only when at least `big_least` (3) of the last `big_last` (5) large prints (each of at least `big_qty`: 20 BTC, the terminal's Large trades at 20 x) were on the trade's side -- bought by the aggressor for a long, sold for a short. When a candle CLOSES and, against its own point of the bands of the VWAP of 20 candles -- what the terminal's RVWAP 20 draws at that
- 분류
- Host: 터미널의 데이터와 엔진 필요
- 권한
- orders.submit
- 실행하기 전에
- BTCUSDT aggregated prints and one-minute bars
- 하는 일
- asks the terminal to run the strategy; once running it places orders on the account the person chose.
- 실행 방법
- Insert strategy in the Backtests window (backtest and optimize); Add script on a replay (the same handlers run there).
- 언어
>=0.2.0, <0.3.0- 호스트
>=0.1.26, <0.2.0- 필요한 것
- features >=0.1.9, <0.2.0handlers >=0.1.2, <0.2.0trade >=0.1.0, <0.2.0
소스
// Purpose: example 82 with one more filter, the large prints before the entry. Fade a one-hour candle that reaches the THIRD band of the rolling VWAP, but only when at least `big_least` (3) of the last `big_last` (5) large prints (each of at least `big_qty`: 20 BTC, the terminal's Large trades at 20 x) were on the trade's side -- bought by the aggressor for a long, sold for a short. When a candle CLOSES and, against its own point of the bands of the VWAP of 20 candles -- what the terminal's RVWAP 20 draws at that // candle --, its low reached the lower band at `touch_k` deviations (3 by default), it is bought; its high reaching the upper band is sold -- against the stretch --, at the market as the candle closes, with a target // on a band of that same VWAP (`band_k` deviations on the trade's side) that FOLLOWS the latest band -- moved as each one-hour candle closes -- while the trade is open, and a stop exactly at the extreme of the signal // candle (or `stop_ratio` times the target's distance). // // BIG TRADES the filter, the one thing that is not example 82. Just before the entry, the last `big_last` large prints seen (a single print of at least `big_qty`; 5 and 20 BTC by default -- the terminal's "Large trades" mode at a // threshold of 20 x its base, floored at 1 BTC) are looked at, and at least `big_least` (3 by default) of them must be on the trade's side: BOUGHT by the aggressor (bullish) for a long, SOLD (bearish) for a short. // So the band says where the stretch ended and the big prints say who was acting there: a long at the lower band with the big money buying, a short at the upper band with it selling. If fewer agree there is no // trade: the candle is passed over, and the next one is judged on its own. The prints counted are those up to and including the minute the entry is made in (the candle's last minute for the close entry). // `big_last` 0 turns the filter off and this is example 82. Fewer than `big_last` large prints seen so far in the run: 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 (kept within what the volumes allow, as example 80 does). When the 5th-last print falls 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). // The touch of the band is example 82's, unchanged: a long needs the low of the candle at the LOWER third band, a short the high at the UPPER one. // 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`. Each bar's value is the one the line has when that bar closes, the bar included, as the chart draws it. There is no line until `vwap_window` candles have been seen: 20 hours of a run, by default. // TOUCH judged ONLY when the candle has closed, against the candle's own point of the bands: the VWAP and its deviation as the closed candle leaves them (the candle included), which the chart draws at the // candle's centre -- the visual confirmation, read once the bar is done (the entry is on the bar after it). A low at or under `vwap - touch_k x sd` buys; a high at or over `vwap + touch_k x sd` sells; // a candle that reached both bands is passed over. Checked against the terminal's RVWAP 20 on the chart's own one-hour candles: the same high and low, a line a few dollars apart at most (the volume // of the recorded bars is not the chart's to the last unit), so a candle that only grazes a band can read either way. // ENTRY at the market, as the candle closes. One position at a time; a candle that touches while a position is open or an order is working is passed over. Not taken when every minute of the candle was not seen, // nor when the band the target would sit on is not beyond the price (a collapsed band). // LIVE ENTRY `entry_mode` 1 (off by default): instead of waiting for the close, enter at the market in the FIRST minute (before the last) whose own range reaches the third band as it stands at that minute -- the // band of the 19 closed candles and the candle so far --, at that minute's close, once a candle; the stop is the candle's extreme so far plus `live_buffer` deviations of the band (the wick runs on past the extreme it has at the first touch on 74 to 80 % of them: a stop right at it would mostly be hit). // The close rule is not used in this mode. // `pace_max` (a filter, off at 0): only while the hour's volume so far, per minute, is at most that many times the mean of the 19 hours before. Why: a heavy hour pulls the band towards itself, so slow // hours end as touches more often. STUDIED (docs/research/touch-entry-btcusdt.md, pre-registered, BTC 2025-01..2026-03, 1-minute candles, no profit and loss): the live touch is seen before the close on // about 90 % of the candles that end as touches and 57 % of live touches end as touches (2.7 % of all candles do), the entry is a median 28 dollars from the band at that minute but only about a fifth // nearer the extreme of the wick than the close (the wick runs on past the band), and `pace_max` at 2.7 lifts the share to 70 % at the cost of a third to a half of the touches. It did NOT pass the // pre-registered bar on the distance to the extreme, so it is an option to be backtested, not a recommendation; whether it earns more than entering at the close is for a backtest. // 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 band of the latest closed candle, at each candle's close // (when it has moved by more than 0.01 %). It only moves while it stays beyond the entry price, so it never becomes a loss. A band that drifts to the entry's other side (a negative reward:risk): with // `neg_rr_exit` 1 (the default) the position is CLOSED at once at the market; with 0 the target stays where it was. // `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. // MIN TARGET a signal whose target is nearer to the candle's close than `min_tp_pct` per cent (0.5 by default; 0 turns it off) is not traded -- for the band target the distance to the band, for the fixed one `tp_pct`. // A target exactly `min_tp_pct` away is traded. It is checked when the signal is judged; the target that later follows the band is not held to it. // STOP `sl_mode` 1 (the default): exactly at the extreme of the candle that gave the signal -- its low for a long, its high for a short --, so the stop sits where the candle's own wick ended. It is not moved. If the // market fills past that extreme there is no room for the stop and the position is closed at once rather than left without one. Beforehand, a signal whose reward:risk would be negative or // undefined is NOT traded: the target not beyond the candle's close (a band target on the wrong side), or the close at the very extreme the stop would sit on (no room for it). // `sl_mode` 0: `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, 0.5 half of it. // // The flags (`sl_mode`, `tp_mode`, `neg_rr_exit`) are on/off switches in the terminal's Parameters panel (`@param(switch: true)`), and `min_tp_pct` is a filter with a switch beside its value (`off: 0`): the panel says what is on and what is off. // The optimizer moves four numbers (ADR-0015): the band touched (`touch_k`), 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, and `stop_ratio` counts only with `sl_mode` 0; the candle length and the window are the rule as asked, fixed. // Everything is worked out on one-hour candles (`bar_minutes` 60): the VWAP, its bands, the touch, the entry and the moving target. The handler runs on each closed minute only to build the candle. This is example 81 with the entry judged // at the candle's close instead of minute by minute, and without its ATR ceiling. Example 83 is example 82 with the large-prints filter above, in every entry mode (at the close, at the live touch, and the limit beyond the band: for the limit, the filter is read when the limit is left, every minute). // 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, } // 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, } // What the strategy remembers: the candle in progress, the closed ones of the window, 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_minutes: Int, c_hi: Float, c_lo: Float, c_close: Float, c_vol: Float, candles: List<Candle>, phase: Int, sgn: Int, extreme: Float, entered: Int, approach: Int, approach_cur: Int, target: Float, entry: Float, stop: Float, sent_tp: Float, prints: List<Flow>, } struct Step { state: Memory, actions: List<handlers.Act>, } fn begin() -> Memory { return Memory { cur: -1, c_minutes: 0, c_hi: 0.0, c_lo: 0.0, c_close: 0.0, c_vol: 0.0, candles: [], phase: 0, sgn: 0, extreme: 0.0, entered: -1, approach: 0, approach_cur: -1, target: 0.0, entry: 0.0, stop: 0.0, sent_tp: 0.0, prints: [] }; } fn whole(f: Float) -> Int { return text.parse_int(to_string(decimal_from_float(f, 0))).unwrap_or(0); } // A buy or sell limit for one minute (it expires at the end of the next minute and is left again, at the band as it then stands). fn limit_at(side: handlers.Side, price: Float, 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: Some(price), stop: None, take_profit: None, stop_loss: None, tag: 7, post_only: false, immediate: false, expire_minutes: 1, }); } 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; } fn to_count(f: Float) -> Float { return to_float(whole(f)); } // 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. `last` below 1 is the filter off. 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); } // How fast the hour is trading: its volume so far, per minute, against the mean volume per minute of the closed candles before it (1 is the usual pace). 0 when there is nothing to compare with. fn pace_of(candles: List<Candle>, vol_so_far: Float, minutes: Int, length: Int) -> Float { let mut total: Float = 0.0; for c in candles { total = total + c.vol; } if candles.len() == 0 || total <= 0.0 || minutes < 1 || length < 1 { return 0.0; } let mean = total / to_float(candles.len()); return vol_so_far / mean * to_float(length) / to_float(minutes); } // 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, large_n: Float, large_buy: Float, large_sell: Float, vwap_window: Float, bar_minutes: Float, touch_k: Float, entry_mode: Float, pace_max: Float, live_buffer: Float, limit_entry: Float, limit_depth: Float, band_k: Float, tp_mode: Float, sl_mode: Float, neg_rr_exit: Float, min_tp_pct: Float, tp_pct: Float, big_qty: Float, big_last: Float, big_least: Float, size: Float) -> Step { let last = whole(big_last); let least = whole(big_least); 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 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 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.cur = candle; s.c_minutes = 0; 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; s.c_minutes = s.c_minutes + 1; // 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 }); } let band = band_of(s.candles, (s.c_hi + s.c_lo + s.c_close) / 3.0, s.c_vol, window); if band.ok { let fixed = tp_mode >= 0.5; // A one-hour candle has just closed: everything below is worked out on closed candles, once, here. let ends = end_min % length == 0; // The candle is judged against its own point of the bands -- the VWAP and its deviation as the closed candle leaves them, the candle included --, the point the chart draws at the candle's centre. let lower = band.vwap - touch_k * band.sd; let upper = band.vwap + touch_k * band.sd; let low_touch = s.c_lo <= lower; let high_touch = s.c_hi >= upper; // **Entry at the live touch** (`entry_mode`): before the candle closes, the first minute whose own range reaches the band AS IT STANDS (the candle so far included) is entered at the market, at that minute's close. // One entry a candle; the candle's last minute is left to nobody (the close is where the other mode enters). Optionally only while the hour trades slowly (`pace_max`): a heavy hour pulls the band towards // itself, so slow hours end as touches more often. // **A limit beyond the live band** (`limit_entry`, studied in docs/research/band-flow-btcusdt.md): from the first minute the candle comes within one deviation of a band, a limit is left `limit_depth` deviations // beyond that band as it stands, for one minute, and left again every minute at the band's new place until it fills (once a candle). let limit_mode = limit_entry >= 0.5; if s.approach_cur != s.cur { s.approach_cur = s.cur; s.approach = 0; } if s.approach == 0 { let near_low = lo <= lower + band.sd; let near_high = hi >= upper - band.sd; if near_low && !near_high { s.approach = 1; } if near_high && !near_low { s.approach = -1; } } if limit_mode && s.approach != 0 && flow_agrees(s.prints, last, least, s.approach == 1) && s.c_minutes < length && s.entered != s.cur && s.phase == 0 && ctx.orders.len() == 0 && !held && band.sd > 0.0 { let d = to_float(s.approach); let price = if s.approach == 1 { lower - limit_depth * band.sd } else { upper + limit_depth * band.sd }; let aim = band.vwap + d * band_k * band.sd; let reward = (aim - price) * d; let stop_at = price - d * live_buffer * band.sd; let dist_pct = if fixed { tp_pct } else { reward / price * 100.0 }; if (fixed || reward > 0.0) && dist_pct >= min_tp_pct { s.phase = 1; s.sgn = s.approach; s.target = aim; s.extreme = stop_at; acts.push(limit_at(if s.approach == 1 { handlers.Side.Buy } else { handlers.Side.Sell }, price, size)); } } let live_mode = entry_mode >= 0.5 && !limit_mode; let live_low = lo <= lower; let live_high = hi >= upper; if live_mode && s.c_minutes < length && s.entered != s.cur && live_low != live_high && s.phase == 0 && ctx.orders.len() == 0 && !held && band.sd > 0.0 { let pace = pace_of(s.candles, s.c_vol, s.c_minutes, length); let calm = pace_max <= 0.0 || (pace > 0.0 && pace <= pace_max); if calm && live_low && flow_agrees(s.prints, last, least, true) { let aim = band.vwap + band_k * band.sd; let reward = aim - close; // The wick usually runs on past the extreme it has at the first touch (it did on 74 to 80 % of them), so the stop goes `live_buffer` deviations beyond it. let stop_at = s.c_lo - live_buffer * band.sd; let risk = close - stop_at; let dist_pct = if fixed { tp_pct } else { reward / close * 100.0 }; if (fixed || reward > 0.0) && (sl_mode < 0.5 || risk > 0.0) && dist_pct >= min_tp_pct { s.phase = 1; s.sgn = 1; s.target = aim; s.extreme = stop_at; s.entered = s.cur; acts.push(market(handlers.Side.Buy, handlers.Intent.Open, size)); } } if calm && live_high && flow_agrees(s.prints, last, least, false) { let aim = band.vwap - band_k * band.sd; let reward = close - aim; let stop_at = s.c_hi + live_buffer * band.sd; let risk = stop_at - close; let dist_pct = if fixed { tp_pct } else { reward / close * 100.0 }; if (fixed || reward > 0.0) && (sl_mode < 0.5 || risk > 0.0) && dist_pct >= min_tp_pct { s.phase = 1; s.sgn = -1; s.target = aim; s.extreme = stop_at; s.entered = s.cur; acts.push(market(handlers.Side.Sell, handlers.Intent.Open, size)); } } } if !live_mode && !limit_mode && ends && s.c_minutes == length && low_touch != high_touch && s.phase == 0 && ctx.orders.len() == 0 && !held && band.sd > 0.0 { if low_touch && flow_agrees(s.prints, last, least, true) { // It reached the lower band: buy, aiming at the band on the upside. let aim = band.vwap + band_k * band.sd; // Reward and risk from the candle's close, where the entry is made: the target above it, the stop at the candle's low below it. A stop at the extreme needs room (close above the low). let reward = aim - close; let risk = close - s.c_lo; // How far the target is, in per cent of the close: a target too near is not worth the trade. let dist_pct = if fixed { tp_pct } else { reward / close * 100.0 }; if (fixed || reward > 0.0) && (sl_mode < 0.5 || risk > 0.0) && dist_pct >= min_tp_pct { s.phase = 1; s.sgn = 1; s.target = aim; s.extreme = s.c_lo; acts.push(market(handlers.Side.Buy, handlers.Intent.Open, size)); } } if high_touch && flow_agrees(s.prints, last, least, false) { // It reached the upper band: sell, aiming at the band on the downside. let aim = band.vwap - band_k * band.sd; let reward = close - aim; let risk = s.c_hi - close; let dist_pct = if fixed { tp_pct } else { reward / close * 100.0 }; if (fixed || reward > 0.0) && (sl_mode < 0.5 || risk > 0.0) && dist_pct >= min_tp_pct { s.phase = 1; s.sgn = -1; s.target = aim; s.extreme = s.c_hi; acts.push(market(handlers.Side.Sell, handlers.Intent.Open, size)); } } } // The position is open: when a candle closes the target moves to the latest band, the one of the candle that has just closed (a fixed target does not move). A band that has drifted to the entry's other // side makes the reward negative: with `neg_rr_exit` the position is closed at once at the market, without it the target stays where it was. if ends && s.phase == 2 && held && !fixed { let want = band.vwap + to_float(s.sgn) * band_k * band.sd; let reach = (want - s.entry) * to_float(s.sgn); if reach <= 0.0 && neg_rr_exit >= 0.5 { let out = if s.sgn == 1 { handlers.Side.Sell } else { handlers.Side.Buy }; acts.push(market(out, handlers.Intent.Close, size)); } let mut gap = want - s.sent_tp; if gap < 0.0 { gap = 0.0 - gap; } if reach > 0.0 && 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)); } } } 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, sl_mode: Float, stop_ratio: Float, size: Float) -> Step { let mut s = m; if s.phase == 1 && ctx.positions.len() > 0 { s.entered = s.cur; 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; // The stop: the extreme of the signal candle, or a multiple of the target's distance. let mut stop = entry - d * reach * stop_ratio; if sl_mode >= 0.5 { stop = s.extreme; } if reach <= 0.0 || (stop - entry) * d >= 0.0 { // The market filled beyond the band it aimed at, or past the extreme the stop was to sit on: there is no target or no room for the stop, so the position is closed rather than left half protected. 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 = stop; 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_candle_big_trades_fade( bar_minutes: Int, vwap_window: Int, @param(label: "Signal band (std devs)", min: 1.0, max: 3.5, step: 0.5, group: "Signal", hint: "The candle must reach the band at this many deviations of the rolling VWAP: the lower one to buy, the upper one to sell. 3 is the chart's third band.") touch_k: Decimal, @param(switch: true, label: "Enter at the live touch", group: "Signal", hint: "On: enter at the market in the first minute whose range reaches the third band as it stands (before the candle closes), once a candle, with the stop a little beyond the candle's extreme so far (Live stop beyond the extreme). Off: wait for the close and enter if the closed candle reached its band. Measured on BTC 2025-10..2026-03: about 57 % of live touches end as touches (2.7 % of all candles do); the entry is within about 30 dollars of the band but only about a fifth nearer the wick's extreme than the close.") entry_mode: Int, @param(label: "Volume pace ceiling", min: 1.0, max: 5.0, step: 0.5, off: 0, group: "Filters", hint: "Live entry only while the hour trades at most this many times the usual pace (its volume so far per minute against the 19 hours before). Slow hours end as touches more often (AUC 0.30 on validation). Off: any pace.") pace_max: Decimal, @param(label: "Live stop beyond the extreme (std devs)", min: 0.0, max: 2.5, step: 0.25, group: "Exit", hint: "Only for a live entry with the stop at the extreme: the stop goes this many deviations of the band beyond the extreme the candle has at the entry. Measured: the wick runs on past that extreme by a median 0.4 to 0.5, a quarter of the time by more than 1, one in ten by more than 1.5 to 1.7.") live_buffer: Decimal, @param(switch: true, label: "Enter with a limit beyond the live band", group: "Signal", hint: "On: from the first minute the candle comes within one deviation of a band, a limit is left Limit depth deviations beyond the band as it stands, renewed every minute, once a candle. Studied: the entry is much nearer the level of the touch (about 0.3 deviations against 0.6 at the live touch and 0.95 at the close) but the trades lost more (2024: -26 bps a trade against -17 at the close): limits fill when the price runs through.") limit_entry: Int, @param(label: "Limit depth (std devs)", min: 0.0, max: 2.0, step: 0.25, group: "Signal", hint: "How far beyond the live band the limit is left.") limit_depth: Decimal, @param(label: "Target band (std devs)", min: -2.0, max: 3.0, step: 0.5, group: "Exit", hint: "The target sits on the band at this many deviations of the rolling VWAP on the trade's side (0 is the VWAP itself), and follows it. The chart's bands are 1, 2 and 3.") band_k: Decimal, @param(switch: true, label: "Stop at the signal candle's extreme", group: "Exit", hint: "On: the stop sits exactly at the candle's low (a long) or high (a short). Off: Stop (x target distance) times the target's distance.") sl_mode: Int, @param(label: "Stop (x target distance)", min: 0.25, max: 3.0, step: 0.25, group: "Exit", hint: "Used when the stop mode is 0: 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, @param(switch: true, label: "Close when the target turns negative", group: "Exit", hint: "On: an open position is closed at once when, at a candle's close, the band the target follows has fallen behind the entry. Off: the target stays where it was.") neg_rr_exit: Int, @param(label: "Minimum target distance (%)", min: 0.25, max: 2.0, step: 0.25, off: 0, group: "Filters", hint: "No entry when the target is nearer than this to the candle's close. Off: any distance.") min_tp_pct: 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, @param(label: "Large print (BTC)", min: 5, max: 60, step: 1, group: "Filters", hint: "A single print at least this big. The terminal's Large trades at 20x is 20 BTC.") big_qty: Decimal, @param(label: "Large prints looked at", min: 1, max: 10, step: 1, off: 0, group: "Filters", hint: "The last this many large prints before the entry are looked at. 5 is the rule as asked. Off: no filter, which is example 82.") big_last: Decimal, @param(label: "Of them, at least on the trade's side", min: 1, max: 10, step: 1, group: "Filters", hint: "At least this many of those prints must have been bought by the aggressor for a long, sold for a short. 3 of 5 is the rule as asked; more than the prints looked at never trades.") big_least: 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(); // The large prints of each minute alone, and on which side the aggressor was. 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); state = begin(); handler bar(1) = on_minute(ctx, hi, lo, close, vol, large_n, large_buy, large_sell, vwap_window, bar_minutes, touch_k, entry_mode, pace_max, live_buffer, limit_entry, limit_depth, band_k, tp_mode, sl_mode, neg_rr_exit, min_tp_pct, tp_pct, big_qty, big_last, big_least, size); handler fill = on_fill(ctx, tp_mode, tp_pct, sl_mode, stop_ratio, size); } fn main() -> Result<Unit, Error> { // The rule as asked: a closed one-hour candle that reaches the third band of the rolling VWAP of 20 candles is faded at its close, target on the first band, the stop at the extreme of that candle (sl_mode 1). It is taken only when at least 3 of the last 5 large prints (20 BTC or more) were on its side: bought for a long, sold for a short. let run = trade.run(vwap_candle_big_trades_fade(60, 20, 3.0, 0, 2.7, 1.0, 0, 0.75, 1.0, 1, 1.0, 0, 1, 0.5, 1.0, 20, 5, 3, 0.01))?; print(run.name + " on " + run.target + ": " + run.state); return Ok(()); }