prior-day-breakout/tests/reference.py
Allan Niñal Allan Niñal 4b48564cd6 Prior Day Breakout: free MT5 and MT4 EA source, Python reference of the rule, tests and README
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 16:07:51 +08:00
89 lines
4.4 KiB
Python
# 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)