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
|