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