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)
|