85 lines
4 KiB
Python
# SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
| |||
"""Independent pandas/numpy reference for Session Levels MT5 (buffers 0-6), written from the spec, not from the MQL code.
| |||
| |||
Assumes the server clock is New York close (server time = New York time + 7h), so the
| |||
trading day is simply the server calendar day (17:00 New York = 00:00 server) and a New York wall-clock
| |||
minute is (server seconds - 7h) mod a day. Bars are identified by their open time (epoch seconds, server time).
| |||
| |||
b0 previous-day high b1 previous-day low b2 previous-week high b3 previous-week low
| |||
b4 Asia high so far b5 Asia low so far b6 +1 first close > PDH of the day, -1 first close < PDL,
| |||
+2 first close > Asia high after Asia ended, -2 first close < Asia low
| |||
Days/weeks: week = Monday-based week of the trading day. The first day/week in the data has no "previous" (NaN).
| |||
The last bar is the live bar: callers compare closed bars only (b6 of the last bar is NaN here as well)."""
| |||
import numpy as np
| |||
import pandas as pd
| |||
| |||
NY_SHIFT = 7 * 3600
| |||
| |||
| |||
def hm(s: str) -> int:
| |||
h, m = s.split(":")
| |||
return int(h) * 60 + int(m)
| |||
| |||
| |||
def in_session(minute, s, e):
| |||
if s == e:
| |||
return np.zeros(len(minute), bool)
| |||
return (minute >= s) & (minute < e) if s < e else (minute >= s) | (minute < e)
| |||
| |||
| |||
def session_levels(t, o, h, l, c, asia_start="19:00", asia_end="04:00") -> np.ndarray:
| |||
"""Return an (n, 7) float array, NaN for empty."""
| |||
t = np.asarray(t, dtype="int64")
| |||
h, l, c = (np.asarray(x, float) for x in (h, l, c))
| |||
n = len(t)
| |||
out = np.full((n, 7), np.nan)
| |||
day = t // 86400
| |||
week = (day + 3) // 7 # 1970-01-01 was a Thursday; Monday-based week number
| |||
df = pd.DataFrame({"day": day, "week": week, "h": h, "l": l, "c": c})
| |||
# previous day / week extremes: aggregate each group, then look one group back
| |||
for key, hi_col, lo_col in (("day", 0, 1), ("week", 2, 3)):
| |||
agg = df.groupby(key).agg(hi=("h", "max"), lo=("l", "min"))
| |||
prev = agg.shift(1)
| |||
out[:, hi_col] = df[key].map(prev["hi"]).to_numpy()
| |||
out[:, lo_col] = df[key].map(prev["lo"]).to_numpy()
| |||
# Asia range so far, for the trading day the Asia session belongs to
| |||
nyw = t - NY_SHIFT
| |||
minute = (nyw % 86400) // 60
| |||
s, e = hm(asia_start), hm(asia_end)
| |||
ina = in_session(minute, s, e)
| |||
owner = nyw // 86400 + ((minute >= s) & (s > e)).astype(int)
| |||
a = pd.DataFrame({"owner": np.where(ina, owner, -1), "day": day, "h": h, "l": l})
| |||
a_in = a[ina]
| |||
run_hi = a_in.groupby("owner").h.cummax()
| |||
run_lo = a_in.groupby("owner").l.cummin()
| |||
first_day = a_in.groupby("owner").day.transform("first")
| |||
ah = pd.Series(np.nan, index=a.index); al = ah.copy(); ad = ah.copy()
| |||
ah[ina], al[ina], ad[ina] = run_hi, run_lo, first_day
| |||
ah, al, ad = ah.ffill(), al.ffill(), ad.ffill() # the latest Asia instance carries forward
| |||
valid = (ad.to_numpy() == day)
| |||
out[:, 4] = np.where(valid, ah.to_numpy(), np.nan)
| |||
out[:, 5] = np.where(valid, al.to_numpy(), np.nan)
| |||
# signals on closed bars, each kind once per trading day
| |||
pdh, pdl = out[:, 0], out[:, 1]
| |||
after_asia = valid & ~ina
| |||
for d in np.unique(day):
| |||
idx = np.flatnonzero(day == d)
| |||
idx = idx[idx < n - 1] # closed bars only
| |||
done = set()
| |||
for i in idx:
| |||
kinds = []
| |||
if not np.isnan(pdh[i]):
| |||
if 1 not in done and c[i] > pdh[i]:
| |||
kinds.append(1)
| |||
if 2 not in done and c[i] < pdl[i]:
| |||
kinds.append(2)
| |||
if after_asia[i]:
| |||
if 4 not in done and c[i] > out[i, 4]:
| |||
kinds.append(4)
| |||
if 8 not in done and c[i] < out[i, 5]:
| |||
kinds.append(8)
| |||
if kinds:
| |||
done.update(kinds)
| |||
k = kinds[0] # priority: PDH, PDL, Asia high, Asia low
| |||
out[i, 6] = {1: 1, 2: -1, 4: 2, 8: -2}[k]
| |||
return out
|