← Todos los ejemplos
Ejemplo 84
Ema cross big trades
follow a cross of the price over its EMA of 20 candles when the big money agrees, and take the profit in three steps. When the price crosses the EMA from below to above, and at least 4 of the last 5 large
- 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: follow a cross of the price over its EMA of 20 candles when the big money agrees, and take the profit in three steps. When the price crosses the EMA 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. // // EMA the exponential average of the closes of the last `ema_period` (20) candles of `bar_minutes` minutes (60: one-hour candles), seeded with the simple average of the first 20 and then `ema += 2 / (period + 1) x (close - ema)` // as each candle closes. The candle in progress is not in it: the average moves once an hour, the price is looked at every minute. There is no average, and no trade, until `ema_period` candles have been seen. // CROSS judged at the close of each minute: the price (that close) is above the average and the last time it was off the average it was below -> a cross up; the mirror -> a cross down. Touching the average exactly keeps the // side it had. The first minute with an average 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. // 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`); 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 EMA length 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, } // 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 above the average the last time it was off it, -1 below, 2 not known yet. `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, seen: Int, seed: Float, ema: Float, ema_ok: Bool, 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, seen: 0, seed: 0.0, ema: 0.0, ema_ok: false, 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); } // A candle has closed with `c`: the average of the first `period` closes seeds it, then each close moves it by 2 / (period + 1) of its distance. fn ema_after(m: Memory, c: Float, period: Int) -> Memory { let mut s = m; s.seen = s.seen + 1; if s.seen < period { s.seed = s.seed + c; } else if s.seen == period { s.seed = s.seed + c; s.ema = s.seed / to_float(period); s.ema_ok = true; } else { s.ema = s.ema + 2.0 / (to_float(period) + 1.0) * (c - s.ema); } return s; } // 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, large_n: Float, large_buy: Float, large_sell: Float, abs_buy: Float, abs_sell: Float, past: Float, bar_minutes: Float, ema_period: Float, big_qty: Float, big_last: Float, big_least: Float, steps: Float, trailing: Float, trail_pct: 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 period = whole(ema_period); 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; } // A new candle has begun: the one in progress has closed, and its close moves the average. let candle = (end_min - 1) / length; if candle != s.cur { if s.cur >= 0 { s = ema_after(s, s.c_close, period); } s.cur = candle; } s.c_close = close; // 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 }); } // Which side of the average the price is on, and whether it has just crossed it. let mut cross = 0; if s.ema_ok { let mut now_side = 0; if close > s.ema { now_side = 1; } if close < s.ema { now_side = -1; } if now_side != 0 { if s.side == -1 && now_side == 1 { cross = 1; } if s.side == 1 && now_side == -1 { cross = -1; } s.side = now_side; } } // 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) { s.phase = 1; s.sgn = 1; acts.push(market(handlers.Side.Buy, handlers.Intent.Open, size)); } if flat && cross == -1 && flow_agrees(s.prints, last, least, false) { s.phase = 1; s.sgn = -1; 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, size: Float) -> Step { let mut s = m; let mut acts: List<handlers.Act> = []; let n = whole(steps); 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 = entry * (1.0 - d * sl_pct / 100.0); s.peak = entry; 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 = entry * (1.0 + d * to_float(k) * step_pct / 100.0); 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, +1 % after the second... let behind = to_float(done - 1) * step_pct; let stop = s.entry * (1.0 + to_float(s.sgn) * behind / 100.0); // 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, s.entry * (1.0 + to_float(s.sgn) * to_float(n) * step_pct / 100.0), 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 ema_cross_big_trades( bar_minutes: Int, ema_period: 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: "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(); // 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, large_n, large_buy, large_sell, abs_buy, abs_sell, past, bar_minutes, ema_period, big_qty, big_last, big_least, steps, trailing, trail_pct, absorption, abs_minutes, abs_x, abs_move_pct, size); handler fill = on_fill(ctx, sl_pct, step_pct, steps, partial_pct, size); } fn main() -> Result<Unit, Error> { // The rule as asked: the price crossing its EMA 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); no absorption exit (absorption 0). let run = trade.run(ema_cross_big_trades(60, 20, 50, 5, 4, 1.0, 1.0, 3, 50, 0, 1.0, 0, 10, 2.0, 0.1, 0.01))?; print(run.name + " on " + run.target + ": " + run.state); return Ok(()); }