divergence-signal-alerts/tests/reference.py
175 lines
5.3 KiB
Python
# SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
"""Python twin of mql5/Include/Divergence/Div.mqh: the same two oscillators (RSI, MACD), pivots and divergence rule, operation for
operation, so MT5's output can be compared bar by bar (tests/check_parity.py) and frequencies measured in Python.
from reference import Params, run
events, osc = run(high, low, close, Params())
"""
from dataclasses import dataclass, field
import numpy as np
RSI, MACD = range(2)
NAMES = ["RSI", "MACD"]
@dataclass
class Params:
use: tuple = (True, True)
rsi_n: int = 14
macd_fast: int = 12
macd_slow: int = 26
macd_sig: int = 9
left: int = 5
right: int = 3
min_dist: int = 5
max_dist: int = 60
regular: bool = True
hidden: bool = False
clean: bool = True
min_count: int = 1
class Ema:
"""SMA-seeded EMA (DVEmaStep)."""
def __init__(self):
self.cnt, self.sum, self.v = 0, 0.0, None
def step(self, n, x):
self.cnt += 1
if self.cnt < n:
self.sum += x
self.v = None
elif self.cnt == n:
self.sum += x
self.v = self.sum / n
else:
self.v = self.v + 2.0 / (n + 1) * (x - self.v)
return self.v
def oscillators(c, p: Params):
"""Both oscillators on every bar (None until they have enough bars), as DVOscStep computes them."""
n = len(c)
out = [[None] * n for _ in range(2)]
sg = sl = gain = loss = 0.0
ef, es, esg = Ema(), Ema(), Ema()
for k in range(n):
# RSI (Wilder)
if k >= 1:
ch = c[k] - c[k - 1]
g, ls = max(ch, 0.0), max(-ch, 0.0)
m = p.rsi_n
if k < m:
sg += g
sl += ls
elif k == m:
sg += g
sl += ls
gain, loss = sg / m, sl / m
else:
gain = (gain * (m - 1) + g) / m
loss = (loss * (m - 1) + ls) / m
if k >= m:
out[RSI][k] = 100.0 if loss == 0.0 else (0.0 if gain == 0.0 else 100.0 - 100.0 / (1.0 + gain / loss))
# MACD histogram
a, b = ef.step(p.macd_fast, c[k]), es.step(p.macd_slow, c[k])
if a is not None and b is not None:
mline = a - b
s = esg.step(p.macd_sig, mline)
if s is not None:
out[MACD][k] = mline - s
return out
def pivot_low(l, p, pr):
if p - pr.left < 0:
return False
v = l[p]
return all(v < l[p - j] for j in range(1, pr.left + 1)) and all(not (v > l[p + j]) for j in range(1, pr.right + 1))
def pivot_high(h, p, pr):
if p - pr.left < 0:
return False
v = h[p]
return all(v > h[p - j] for j in range(1, pr.left + 1)) and all(not (v < h[p + j]) for j in range(1, pr.right + 1))
def clean(series, q, p, d):
a, b = series[q], series[p]
for j in range(q + 1, p):
line = a + (b - a) * (j - q) / float(p - q)
v = series[j]
if v is None:
return False
if (v < line) if d > 0 else (v > line):
return False
return True
def check(h, l, osc, pr, p, d, prev):
"""DVCheck: the first previous pivot q with enough diverging oscillators; None if none."""
price = l if d > 0 else h
for q in prev:
dist = p - q
if dist < pr.min_dist:
continue
if dist > pr.max_dist:
break
pp, pq = price[p], price[q]
if d > 0:
typ = 1 if pp < pq else (2 if pp > pq else 0)
else:
typ = 1 if pp > pq else (2 if pp < pq else 0)
if typ == 0 or (typ == 1 and not pr.regular) or (typ == 2 and not pr.hidden):
continue
if pr.clean and not clean(price, q, p, d):
continue
mask = cnt = 0
for o in range(2):
if not pr.use[o]:
continue
op, oq = osc[o][p], osc[o][q]
if op is None or oq is None:
continue
div = d * (op - oq) > 0 if typ == 1 else d * (op - oq) < 0
if not div:
continue
if pr.clean and not clean(osc[o], q, p, d):
continue
mask |= 1 << o
cnt += 1
if cnt >= max(1, pr.min_count):
return dict(dir=d, type=typ, mask=mask, count=cnt, p=p, q=q, ppx=pp, qpx=pq)
return None
def run(h, l, c, pr: Params, start: int = 0, stop: int | None = None):
"""Feed bars start..stop-1 (closed bars) as the indicator does; events keyed by the confirming bar index."""
stop = len(c) if stop is None else stop
h, l, c = (list(map(float, x[start:stop])) for x in (h, l, c))
osc = oscillators(c, pr)
pl, ph = [], []
events = {}
for k in range(len(c)):
p = k - pr.right
if p < 0:
continue
ev = None
if pivot_low(l, p, pr):
ev = check(h, l, osc, pr, p, 1, pl)
pl.insert(0, p)
del pl[4:]
is_ph = pivot_high(h, p, pr)
if ev is None and is_ph:
ev = check(h, l, osc, pr, p, -1, ph)
if is_ph:
ph.insert(0, p)
del ph[4:]
if ev is not None:
ev = {**ev, "p": ev["p"] + start, "q": ev["q"] + start}
events[k + start] = ev
full = [[None] * start + o for o in osc]
return events, full