# SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE) """Python twin of Cdl.mqh: 13 candlestick patterns on closed bars (the rules are in the README). This file is the exact rule book; the MQL5 and MQL4 engines are checked against it bar by bar (tests/check_parity.py). from reference import run, NAMES rows = run(o, h, l, c) # rows[k] = dict(ev=[(pattern, dir), ...], mask=bit mask, ema=EMA(20) or None) """ NAMES = ["Bullish Engulfing", "Bearish Engulfing", "Hammer", "Hanging Man", "Inverted Hammer", "Shooting Star", "Doji", "Morning Star", "Evening Star", "Bullish Harami", "Bearish Harami", "Piercing Line", "Dark Cloud Cover"] NP = len(NAMES) ALL = (True,) * NP # fixed direction per pattern (Doji: by the trend before it, 0 here) DIR = [1, -1, 1, -1, 1, -1, 0, 1, -1, 1, -1, 1, -1] EMA_N, AVG_N = 20, 10 def run(o, h, l, c, use=ALL): n = len(c) ema = [None] * n s, cnt = 0.0, 0 for k in range(n): # EMA(20) of closes, SMA-seeded cnt += 1 if cnt < EMA_N: s += c[k] elif cnt == EMA_N: s += c[k] ema[k] = s / EMA_N else: ema[k] = ema[k - 1] + 2.0 / (EMA_N + 1) * (c[k] - ema[k - 1]) body = [abs(c[k] - o[k]) for k in range(n)] rng = [h[k] - l[k] for k in range(n)] up = [h[k] - max(o[k], c[k]) for k in range(n)] lo = [min(o[k], c[k]) - l[k] for k in range(n)] bull = [c[k] > o[k] for k in range(n)] bear = [c[k] < o[k] for k in range(n)] def avg(arr, k): # SMA of the AVG_N bars before k return sum(arr[k - AVG_N:k]) / AVG_N if k >= AVG_N else None rows = [] for k in range(n): ev = [] # (pattern, dir) ab, ar = avg(body, k), avg(rng, k) if ab is not None and k >= 3: b, r, u, w = body[k], rng[k], up[k], lo[k] tol = 1e-9 * abs(c[k]) # ties within 1e-9 x price count as equal (rounding noise decides nothing) def GE(x, y): return x >= y - tol def LE(x, y): return x <= y + tol def GT(x, y): return x > y + tol def LT(x, y): return x < y - tol def tr(j): # +1 up / -1 down / 0 unknown, at bar j if j < 0 or ema[j] is None: return 0 return 1 if GT(c[j], ema[j]) else (-1 if LT(c[j], ema[j]) else 0) t1, t2, t3 = tr(k - 1), tr(k - 2), tr(k - 3) p = k - 1 # 0/1 engulfing if bear[p] and bull[k] and LE(o[k], c[p]) and GE(c[k], o[p]) and GT(b, body[p]) and t2 < 0: ev.append((0, 1)) if bull[p] and bear[k] and GE(o[k], c[p]) and LE(c[k], o[p]) and GT(b, body[p]) and t2 > 0: ev.append((1, -1)) if GT(r, 0): hammer = GE(w, 2 * b) and LE(u, 0.1 * r) and GE(w, 0.6 * r) inv = GE(u, 2 * b) and LE(w, 0.1 * r) and GE(u, 0.6 * r) if hammer and t1 < 0: ev.append((2, 1)) if hammer and t1 > 0: ev.append((3, -1)) if inv and t1 < 0: ev.append((4, 1)) if inv and t1 > 0: ev.append((5, -1)) if LE(b, 0.05 * r) and GE(r, 0.5 * ar) and t1 != 0: ev.append((6, -t1)) # 7/8 stars q = k - 2 if bear[q] and GE(body[q], ab) and LE(body[p], 0.3 * body[q]) and LE(min(o[p], c[p]), c[q]) and bull[k] \ and GT(c[k], (o[q] + c[q]) / 2) and t3 < 0: ev.append((7, 1)) if bull[q] and GE(body[q], ab) and LE(body[p], 0.3 * body[q]) and GE(max(o[p], c[p]), c[q]) and bear[k] \ and LT(c[k], (o[q] + c[q]) / 2) and t3 > 0: ev.append((8, -1)) # 9/10 harami if bear[p] and GE(body[p], ab) and bull[k] and GE(o[k], c[p]) and LE(c[k], o[p]) and LT(b, body[p]) and t2 < 0: ev.append((9, 1)) if bull[p] and GE(body[p], ab) and bear[k] and LE(o[k], c[p]) and GE(c[k], o[p]) and LT(b, body[p]) and t2 > 0: ev.append((10, -1)) # 11/12 piercing, dark cloud if bear[p] and GE(body[p], ab) and bull[k] and LE(o[k], c[p]) and GT(c[k], (o[p] + c[p]) / 2) and LT(c[k], o[p]) and t2 < 0: ev.append((11, 1)) if bull[p] and GE(body[p], ab) and bear[k] and GE(o[k], c[p]) and LT(c[k], (o[p] + c[p]) / 2) and GT(c[k], o[p]) and t2 > 0: ev.append((12, -1)) ev = [e for e in ev if use[e[0]]] ev.sort() rows.append(dict(ev=ev, mask=sum(1 << pt for pt, _ in ev), ema=ema[k])) return rows