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