本文へ移動
← すべてのサンプル

サンプル 87

Turtle breakout

a long-only Turtle breakout on daily bands of the New York trading day, with an optional day-trading mode. The trading day runs from 17:00 to 17:00 New York (summer time included); the bands and the ATR are made from

カテゴリ
Host:ターミナルのデータとエンジンが必要
権限
orders.submit
実行する前に
BTCUSDT 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: a long-only Turtle breakout on daily bands of the New York trading day, with an optional day-trading mode. The trading day runs from 17:00 to 17:00 New York (summer time included); the bands and the ATR are made from
// COMPLETED days only, so they change once a day, at 17:00. It buys when the price goes above the highest high of the last `entry_days` (20) days; the stop loss is `atr_mult` (2) ATR under the entry price, fixed for the whole
// trade; the trailing exit is the lowest low of the last `exit_days` (10) days; the trade leaves at whichever of the two stops is the higher. One position at a time, long only.
//
//   DAYS          the engine's one-minute bars are grouped into trading days: a day is every minute from 17:00 New York to the next 17:00 (America/New_York, summer time included, from `time.utc_offset_minutes`). A day's high and low are those of its minutes. Nothing is read from a day until it has ended.
//   ENTRY BAND    the highest high of the last `entry_days` completed days (the green line). There is no band until that many days have been seen.
//   EXIT BAND     the lowest low of the last `exit_days` completed days (the orange line).
//   ATR           the standard ATR of `atr_days` (20) completed days, Wilder's smoothing: the first value is the mean of the first `atr_days` true ranges, then (previous x (n - 1) + true range) / n. A day's true range is the
//                 greatest of high - low, high - the previous close and the previous close - low (the first day, which has no previous close, has high - low). There is no entry until the ATR has `atr_days` days.
//   ENTRY         the first minute whose high is above the entry band, with the position flat: bought at the market for `size`, as that minute closes (the band is known the evening before, so a stop-buy order at the band would
//                 do the same on a venue that fills it at the band; here the market order fills a little above it).
//   STOP LOSS     `atr_mult` x the ATR under the REAL entry price of the trade, put on the position when the entry fills and fixed for the whole trade (the ATR is the one of the last completed day).
//   EXIT          the stop that stands is the higher of the stop loss and the exit band (the red line). It is a resting stop of the account, and it is moved at 17:00, when the exit band changes, to wherever it now is. If the
//                 exit band has come above the price at that moment, the position is closed at the market instead.
//   DAY TRADING   `daytrade` 1 (off by default): the position is closed at the market in the first minute from 16:30 New York, and the trade stays alive while it is flat by schedule. From the first minute from 18:00 New York it is
//                 bought again if the price is still above the stop that stands (the higher of the stop loss and the exit band, as the 17:00 change left it); if the price has fallen to the stop in between, the trade is over (the
//                 stop was hit while flat). No NEW trade starts between 16:30 and 18:00. The stop loss does not change at the re-entry: it stays where the first entry put it. This is done every day of the week, weekends included
//                 (the market it runs on does not close).
//
// ASSUMPTIONS to be changed if they are not what was meant: the entry is a market order when a minute's high goes above the band (not a resting stop-buy at the band), so it fills a little above the band; the stop that stands is the higher of the stop loss and the exit band as of the
// last 17:00 (it is not only ever raised: it follows the exit band); the re-entry looks at that same stop (the spec says "the stop loss"); the first day of a dataset may be a partial day and counts as a day; the 16:30 close and the 18:00 return are every day, weekends included.
// HONEST NOTE: written to the specification and checked that it compiles and that the days, the bands, the stops and the day-trading schedule do what they say on made-up data; its edge has not been measured. The `@param` ranges are for the optimizer; the 20-day entry, the 10-day
// exit, the 20-day ATR and the 2 ATR stop are the rule as asked.
// Category: host
// Permissions: orders.submit
// Preconditions: BTCUSDT 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.list;
import std.time;
import std.text;

const MINUTE_MS: Int = 60000;
// The wall clock the trading day is kept on.
const ZONE: String = "America/New_York";
// The minute of the New York day (0..1440) at which the trading day turns over, the one at which the position is closed in day-trading mode and the one from which it is entered again.
const ROLLOVER_MIN: Int = 1020;
const CLOSE_MIN: Int = 990;
const REENTER_MIN: Int = 1080;
// The stop is moved only when it would change by at least this per cent of its price.
const MOVE_MIN_PCT: Float = 0.01;

// One completed trading day: its high and its low.
struct Day {
    hi: Float,
    lo: Float,
}

// What the strategy remembers: the day in progress and the completed ones, the ATR, the two bands, and the trade.
// `day`: the number of the trading day in progress (-1 none yet). `prev_close` is the close of the last completed day. `atr_n` counts the days the ATR has seen, `tr_sum` adds the first `atr_days` true ranges.
// `upper_ok` / `lower_ok` say whether the entry band / the exit band exist. `phase`: 0 flat, 1 an entry sent, 2 open. `alive`: a trade is on, open or flat by schedule (`paused`). `fresh`: the entry that is sent is the
// trade's first (it sets the stop loss), not a return after a pause. `sl`: the stop loss. `stop`: the stop on the account now. `sched`: the strategy has sent the 16:30 close. `pause_cal`: the New York calendar day of
// that close. `leaving`: the strategy has sent a close of its own (because the exit band came over the price).
struct Memory {
    day: Int,
    d_hi: Float,
    d_lo: Float,
    d_close: Float,
    days: List<Day>,
    has_prev: Bool,
    prev_close: Float,
    atr: Float,
    atr_n: Int,
    tr_sum: Float,
    upper: Float,
    lower: Float,
    upper_ok: Bool,
    lower_ok: Bool,
    phase: Int,
    alive: Bool,
    paused: Bool,
    fresh: Bool,
    entry: Float,
    sl: Float,
    stop: Float,
    sched: Bool,
    pause_cal: Int,
    leaving: Bool,
}

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

fn begin() -> Memory {
    return Memory { day: -1, d_hi: 0.0, d_lo: 0.0, d_close: 0.0, days: [], has_prev: false, prev_close: 0.0, atr: 0.0, atr_n: 0, tr_sum: 0.0, upper: 0.0, lower: 0.0, upper_ok: false, lower_ok: false, phase: 0, alive: false, paused: false, fresh: true, entry: 0.0, sl: 0.0, stop: 0.0, sched: false, pause_cal: 0, leaving: false };
}

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

fn protect(stop: Float) -> handlers.Act {
    return handlers.Act.Protect(handlers.Protection { book: "main", take_profit: None, stop_loss: Some(stop) });
}

// The last `keep` completed days.
fn last_days(days: List<Day>, keep: Int) -> List<Day> {
    if keep > 0 && days.len() > keep {
        match list.slice(days, days.len() - keep, days.len()) {
            Some(tail) => {
                return tail;
            },
            None => {},
        }
    }
    return days;
}

// The stop that stands: the higher of the stop loss and the exit band (when there is one).
fn standing_stop(s: Memory) -> Float {
    if s.lower_ok && s.lower > s.sl {
        return s.lower;
    }
    return s.sl;
}

// A trading day has ended with the `d_hi`, `d_lo` and `d_close` of the memory: it joins the completed days, the ATR takes its true range, and the bands are worked out again.
fn day_closed(m: Memory, entry_days: Int, exit_days: Int, atr_days: Int) -> Memory {
    let mut s = m;
    s.days.push(Day { hi: s.d_hi, lo: s.d_lo });
    s.days = last_days(s.days, if entry_days > exit_days { entry_days } else { exit_days });
    let mut tr = s.d_hi - s.d_lo;
    if s.has_prev {
        let up = s.d_hi - s.prev_close;
        let down = s.prev_close - s.d_lo;
        if up > tr {
            tr = up;
        }
        if down > tr {
            tr = down;
        }
    }
    if atr_days >= 1 {
        if s.atr_n < atr_days {
            s.tr_sum = s.tr_sum + tr;
            s.atr_n = s.atr_n + 1;
            if s.atr_n == atr_days {
                s.atr = s.tr_sum / to_float(atr_days);
            }
        } else {
            s.atr = (s.atr * to_float(atr_days - 1) + tr) / to_float(atr_days);
            s.atr_n = s.atr_n + 1;
        }
    }
    s.prev_close = s.d_close;
    s.has_prev = true;
    let count = s.days.len();
    s.upper_ok = entry_days >= 1 && count >= entry_days;
    if s.upper_ok {
        let mut i = count - entry_days;
        let first = s.days[i];
        let mut best = first.hi;
        while i < count {
            let d = s.days[i];
            if d.hi > best {
                best = d.hi;
            }
            i = i + 1;
        }
        s.upper = best;
    }
    s.lower_ok = exit_days >= 1 && count >= exit_days;
    if s.lower_ok {
        let mut j = count - exit_days;
        let first_low = s.days[j];
        let mut worst = first_low.lo;
        while j < count {
            let d = s.days[j];
            if d.lo < worst {
                worst = d.lo;
            }
            j = j + 1;
        }
        s.lower = worst;
    }
    return s;
}

// A position that is gone: if the strategy took it out by schedule the trade is only paused, otherwise it is over.
fn position_gone(m: Memory) -> Memory {
    let mut s = m;
    s.phase = 0;
    s.leaving = false;
    if s.sched {
        s.paused = true;
    } else {
        s.alive = false;
        s.paused = false;
    }
    s.sched = false;
    return s;
}

fn on_minute(m: Memory, ctx: handlers.Context, close: Float, hi: Float, lo: Float, entry_days: Float, exit_days: Float, atr_days: Float, daytrade: 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 start_min = now_ms / MINUTE_MS - 1;
    // The minute in New York: the minutes since the epoch of New York's wall clock, the minute of its day, its calendar day and its trading day (turning over at 17:00).
    let local = start_min + time.utc_offset_minutes(ZONE, ctx.now);
    let tod = local % 1440;
    let cal = local / 1440;
    let day = (local - ROLLOVER_MIN) / 1440;
    let scheduled = daytrade >= 0.5;
    let mut s = m;
    let mut acts: List<handlers.Act> = [];

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

    // The trading day: a minute of a new one closes the one before it (its bands and its ATR change here, once a day) and starts the next.
    let mut turned = false;
    if day != s.day {
        if s.day >= 0 {
            s = day_closed(s, whole(entry_days), whole(exit_days), whole(atr_days));
            turned = true;
        }
        s.day = day;
        s.d_hi = hi;
        s.d_lo = lo;
    }
    if hi > s.d_hi {
        s.d_hi = hi;
    }
    if lo < s.d_lo {
        s.d_lo = lo;
    }
    s.d_close = close;

    // The stop follows the exit band: it is moved when the band changes. If the band has come over the price, the position is closed instead.
    if turned && s.phase == 2 && held && !s.leaving {
        let want = standing_stop(s);
        if want >= close {
            acts.push(market(handlers.Side.Sell, handlers.Intent.Close, size));
            s.leaving = true;
        } else if (want - s.stop) / s.stop * 100.0 >= MOVE_MIN_PCT || (s.stop - want) / s.stop * 100.0 >= MOVE_MIN_PCT {
            s.stop = want;
            acts.push(protect(want));
        }
    }

    // A trade flat by schedule: if the price has fallen to the stop that stands, it is over.
    if s.alive && s.paused && s.phase == 0 && lo <= standing_stop(s) {
        s.alive = false;
        s.paused = false;
    }

    // Day trading: out at 16:30 (the first minute from it).
    if scheduled && s.phase == 2 && held && !s.sched && !s.leaving && tod >= CLOSE_MIN && tod < REENTER_MIN {
        acts.push(market(handlers.Side.Sell, handlers.Intent.Close, size));
        s.sched = true;
        s.pause_cal = cal;
    }

    let flat = s.phase == 0 && ctx.orders.len() == 0 && !held;

    // Day trading: back in from 18:00 (the first minute from it), if the price is still above the stop that stands.
    if flat && s.alive && s.paused && (cal > s.pause_cal || tod >= REENTER_MIN) {
        if close > standing_stop(s) {
            s.phase = 1;
            s.fresh = false;
            acts.push(market(handlers.Side.Buy, handlers.Intent.Open, size));
        } else {
            s.alive = false;
            s.paused = false;
        }
    }

    // A new trade: the price goes above the entry band, with the ATR known for the stop loss. Not while the schedule keeps the market closed to it.
    let closed_window = scheduled && tod >= CLOSE_MIN && tod < REENTER_MIN;
    if flat && s.phase == 0 && !s.alive && !closed_window && s.upper_ok && s.atr_n >= whole(atr_days) && hi > s.upper {
        s.phase = 1;
        s.fresh = true;
        acts.push(market(handlers.Side.Buy, handlers.Intent.Open, size));
    }
    return Step { state: s, actions: acts };
}

// A fill. The entry: the stop loss goes under the real entry price (the first entry of a trade) and the stop that stands is put on the position. A position that is gone: the trade is paused or over.
fn on_fill(m: Memory, ctx: handlers.Context, atr_mult: Float, size: Float) -> Step {
    let mut s = m;
    if s.phase == 1 && ctx.positions.len() > 0 {
        let entry = ctx.positions[0].entry_price;
        s.phase = 2;
        s.alive = true;
        s.paused = false;
        s.sched = false;
        s.leaving = false;
        if s.fresh {
            s.entry = entry;
            s.sl = entry - atr_mult * s.atr;
        }
        s.stop = standing_stop(s);
        if s.stop >= entry {
            // The fill came in at or under the stop that stands: there is no room for the stop, so the position is closed rather than left unprotected.
            s.leaving = true;
            return Step { state: s, actions: [market(handlers.Side.Sell, handlers.Intent.Close, size)] };
        }
        return Step { state: s, actions: [protect(s.stop)] };
    }
    if s.phase == 2 && ctx.positions.len() == 0 {
        return Step { state: position_gone(s), actions: [] };
    }
    return Step { state: s, actions: [] };
}

strategy turtle_breakout(
    @param(label: "Entry breakout (days)", min: 10, max: 55, step: 5, group: "Signal", hint: "The price must go above the highest high of this many completed New York trading days (17:00 to 17:00). 20 is the classic.") entry_days: Int,
    @param(label: "Exit low (days)", min: 5, max: 20, step: 5, group: "Exit", hint: "The trailing exit is the lowest low of this many completed days. The trade leaves at the higher of this and the stop loss. 10 is the classic.") exit_days: Int,
    atr_days: Int,
    @param(label: "Stop loss (ATR)", min: 1.0, max: 4.0, step: 0.5, group: "Exit", hint: "The stop loss sits this many ATRs under the entry price and stays there for the whole trade. 2 is the classic.") atr_mult: Decimal,
    @param(switch: true, label: "Day-trading mode", group: "Schedule", hint: "On: the position is closed at 16:30 New York and bought again from 18:00 if the price is still above the stop that stands; a trade whose stop was reached in between is over. Off: the position is held through the nights and weekends.") daytrade: Int,
    size: Decimal,
) version "1.0.0" {
    let close = features.bar_close();
    let hi = features.bar_high();
    let lo = features.bar_low();
    state = begin();
    handler bar(1) = on_minute(ctx, close, hi, lo, entry_days, exit_days, atr_days, daytrade, size);
    handler fill = on_fill(ctx, atr_mult, size);
}

fn main() -> Result<Unit, Error> {
    // The rule as asked: a breakout of the highest high of 20 days, an exit at the lowest low of 10 days or at 2 ATR (of 20 days) under the entry, whichever is higher; the day-trading mode off (daytrade 0).
    let run = trade.run(turtle_breakout(20, 10, 20, 2.0, 0, 0.01))?;
    print(run.name + " on " + run.target + ": " + run.state);
    return Ok(());
}