| # SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
|
| """Python statement of the Prior Day Breakout rule, written from the rule text (README), on M1 bars.
|
|
|
| trades = run(m1, tf_minutes=60, target_r=2.0)
|
|
|
| `m1` is a pandas DataFrame indexed by server time (naive timestamps, M1 open time) with columns open, high, low, close
|
| (bid prices) and spread (in PRICE units, e.g. 0.35, not points). It returns one row per trade (gross, before swap):
|
| entry/exit time, side, fill prices, stop distance, exit reason.
|
|
|
| Rule: levels = high/low of the previous trading day (the previous day that has bars); on the chart timeframe, from the
|
| bar that opens 01:00 on, the first CLOSE above the high (buy) or below the low (sell), once per side per day; the entry is
|
| the next bar's open if it is before 20:00 of the same day; buys fill at open + spread, sells at the bid; the stop is the
|
| other side of the range; skipped when the stop distance is under 8 x the entry spread; target = target_r x the stop
|
| distance; the position is flat at 23:00 (the first M1 bar at or after it); one position at a time. Stops and targets are
|
| tested on the M1 extremes (a buy on the bid low and high, a sell on the ask = bid + spread); the stop first when both fall
|
| in one M1 bar; a gap fills the stop at the open."""
|
| import numpy as np
|
| import pandas as pd
|
|
|
| FROM_SEC = 3600 # signal bars from 01:00
|
| CUTOFF_SEC = 20 * 3600 # no entry from 20:00
|
| FLAT_SEC = 23 * 3600 # flat at 23:00
|
| MIN_SPREADS = 8.0 # skip when the stop is under 8 x the spread
|
|
|
|
|
| def run(m1: pd.DataFrame, tf_minutes: int = 60, target_r: float = 2.0) -> pd.DataFrame:
|
| t = m1.index.values.astype("datetime64[s]").astype(np.int64)
|
| o, h, l, c, sp = (m1[k].to_numpy(float) for k in ("open", "high", "low", "close", "spread"))
|
| n = len(t)
|
| day = t // 86400
|
| starts = np.concatenate(([0], np.flatnonzero(np.diff(day)) + 1))
|
| dh, dl = np.maximum.reduceat(h, starts), np.minimum.reduceat(l, starts)
|
| pdh = np.concatenate(([np.nan], dh[:-1]))
|
| pdl = np.concatenate(([np.nan], dl[:-1]))
|
| day_of_start = day[starts]
|
| gidx = np.searchsorted(day_of_start, day) # M1 index -> day group
|
| sec = tf_minutes * 60
|
| key = t // sec
|
| fm = np.concatenate(([0], np.flatnonzero(np.diff(key)) + 1)) # first M1 index of each chart bar
|
| last = np.concatenate((fm[1:] - 1, [n - 1]))
|
| bar_open, bar_close = key[fm] * sec, c[last]
|
| rows, free, cur_day, done = [], 0, -1, [False, False]
|
| for j in range(len(fm) - 1):
|
| bday, btod = bar_open[j] // 86400, bar_open[j] % 86400
|
| if bday != cur_day:
|
| cur_day, done = bday, [False, False]
|
| if btod < FROM_SEC:
|
| continue
|
| g = gidx[fm[j]]
|
| hi, lo = pdh[g], pdl[g]
|
| if np.isnan(hi) or np.isnan(lo):
|
| continue
|
| side = 0
|
| if bar_close[j] > hi and not done[0]:
|
| done[0], side = True, 1
|
| elif bar_close[j] < lo and not done[1]:
|
| done[1], side = True, -1
|
| if side == 0:
|
| continue
|
| e = fm[j + 1]
|
| if t[e] // 86400 != cur_day or t[e] % 86400 >= CUTOFF_SEC or e < free:
|
| continue
|
| fill = o[e] + sp[e] if side > 0 else o[e]
|
| sl = lo if side > 0 else hi
|
| sd = (fill - sl) * side
|
| if not (sd > 0) or sd < MIN_SPREADS * sp[e]:
|
| continue
|
| tp = fill + side * target_r * sd if target_r > 0 else np.nan
|
| fx = int(np.searchsorted(t, cur_day * 86400 + FLAT_SEC, side="left"))
|
| px, why, m_exit, nxt = (o[fx] if side > 0 else o[fx] + sp[fx]) if fx < n else c[-1], "time", min(fx, n - 1), min(fx, n - 1)
|
| for m in range(e, min(fx, n)):
|
| if side > 0:
|
| if l[m] <= sl:
|
| px, why, m_exit, nxt = min(sl, o[m]), "stop", m, m + 1
|
| break
|
| if tp == tp and h[m] >= tp:
|
| px, why, m_exit, nxt = max(tp, o[m]), "target", m, m + 1
|
| break
|
| else:
|
| if h[m] + sp[m] >= sl:
|
| px, why, m_exit, nxt = max(sl, o[m] + sp[m]), "stop", m, m + 1
|
| break
|
| if tp == tp and l[m] + sp[m] <= tp:
|
| px, why, m_exit, nxt = min(tp, o[m] + sp[m]), "target", m, m + 1
|
| break
|
| rows.append(dict(entry_t=t[e], exit_t=t[m_exit], side=side, entry=fill, exit=px, sd=sd, why=why,
|
| gross_r=(px - fill) * side / sd))
|
| free = nxt
|
| return pd.DataFrame(rows)
|