session-levels/tests/reference.py
Allan Niñal Allan Niñal 5a6e5b9b72 Session Levels: free MT5 and MT4 indicator source, Python reference, tests and README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 18:04:52 +08:00
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