143 lines
7.2 KiB
Python
# SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
| |||
"""Independent Python reference of the Displacement Order Blocks engine (mql5/.../DzEngine.mqh), bar by bar.
| |||
| |||
Everything at bar i is decided from bars <= i; the last bar of the arrays is the forming bar and is never used. The
| |||
rules are the ones in the DzEngine.mqh header (d = +1 demand, -1 supply):
| |||
ATR Wilder (RMA) of the true range, seeded with the SMA of the first `atr` values (summed in order, as MQL5);
| |||
the impulse test of bar i uses the ATR after bar i-1.
| |||
swing left `swing` bars strictly lower highs (higher lows), right `swing` bars not above (not below); known at
| |||
p + swing; a new swing replaces the previous one of its side.
| |||
break the first close beyond the newest swing; it uses the swing up whether or not a zone is made.
| |||
impulse range >= disp_mult x ATR, body >= body_pct % of the range (within 1e-9 x range, for exact decimal ties),
| |||
closing in the break's direction.
| |||
base base_mode 0: nearest candle of the other colour among the `search` bars before the break bar; base_mode 1: from
| |||
the lowest low (demand; supply: highest high, latest bar on a tie) of those bars back, the nearest candle of the
| |||
other colour.
| |||
zone mode 0 full candle, 1 body, 2 mixed.
| |||
states per bar for every live zone made before it: broken (checked first), then tested / retested.
| |||
cap max_live live zones per side (0 = off): the oldest live one of that side is dropped (state 3).
| |||
"""
| |||
import numpy as np
| |||
| |||
FRESH, TESTED, BROKEN, DROPPED = 0, 1, 2, 3
| |||
KEYS = ["dem_top", "dem_bot", "sup_top", "sup_bot", "break_up", "break_dn", "tested", "retests", "broken",
| |||
"live_dem", "live_sup", "dem_base", "sup_base"]
| |||
DEFAULTS = dict(swing=3, atr=14, search=12, zone_mode=0, test_mode=0, break_mode=0, max_live=12, base_mode=0,
| |||
disp_mult=1.0, body_pct=55.0)
| |||
| |||
| |||
def dz_zones(t, o, h, l, c, on_bar=None, **kw):
| |||
"""-> (buffers, zones, breaks, live zones, ATR after the last closed bar). on_bar(i, zones) runs after each bar."""
| |||
p = {**DEFAULTS, **kw}
| |||
n = len(c)
| |||
out = {k: np.full(n, np.nan) for k in KEYS}
| |||
zones, breaks, live = [], [], []
| |||
sw = [dict(i=-1, p=0.0, broken=True), dict(i=-1, p=0.0, broken=True)]
| |||
active = [0, 0]
| |||
atr, atr_sum = None, 0.0
| |||
A, S = p["atr"], p["swing"]
| |||
| |||
def reaches(z, hi, lo):
| |||
tol = abs(z["mid"]) * 1e-10
| |||
m = p["test_mode"]
| |||
if z["dir"] > 0:
| |||
return lo <= z["top"] if m == 0 else (lo <= z["mid"] + tol if m == 1 else lo <= z["bot"])
| |||
return hi >= z["bot"] if m == 0 else (hi >= z["mid"] - tol if m == 1 else hi >= z["top"])
| |||
| |||
def breaks_zone(z, hi, lo, cl):
| |||
if z["dir"] > 0:
| |||
return cl < z["bot"] if p["break_mode"] == 0 else lo < z["bot"]
| |||
return cl > z["top"] if p["break_mode"] == 0 else hi > z["top"]
| |||
| |||
def is_swing(q, side):
| |||
for k in range(1, S + 1):
| |||
if side > 0 and (h[q - k] >= h[q] or h[q + k] > h[q]):
| |||
return False
| |||
if side < 0 and (l[q - k] <= l[q] or l[q + k] < l[q]):
| |||
return False
| |||
return True
| |||
| |||
for i in range(n - 1):
| |||
prev = atr
| |||
tr = h[i] - l[i] if i == 0 else max(h[i] - l[i], max(abs(h[i] - c[i - 1]), abs(l[i] - c[i - 1])))
| |||
if i < A - 1:
| |||
atr_sum += tr
| |||
elif i == A - 1:
| |||
atr_sum += tr
| |||
atr = atr_sum / A
| |||
else:
| |||
atr = (atr * (A - 1) + tr) / A
| |||
if prev is not None and prev > 0:
| |||
n_test = n_re = n_brk = 0
| |||
keep = []
| |||
for z in live:
| |||
reach = reaches(z, h[i], l[i])
| |||
if breaks_zone(z, h[i], l[i], c[i]):
| |||
z["state"], z["end"] = BROKEN, i
| |||
n_brk += 1
| |||
active[0 if z["dir"] > 0 else 1] -= 1
| |||
continue
| |||
if reach:
| |||
if z["state"] == FRESH:
| |||
z["state"], z["retests"] = TESTED, 1
| |||
n_test += 1
| |||
elif not z["inside"]:
| |||
z["retests"] += 1
| |||
n_re += 1
| |||
z["inside"] = reach
| |||
keep.append(z)
| |||
live = keep
| |||
out["tested"][i], out["retests"][i], out["broken"][i] = n_test, n_re, n_brk
| |||
for d in (1, -1):
| |||
s = 0 if d > 0 else 1
| |||
if sw[s]["broken"] or c[i] * d <= sw[s]["p"] * d:
| |||
continue
| |||
sw[s]["broken"] = True
| |||
out["break_up" if d > 0 else "break_dn"][i] = sw[s]["p"]
| |||
rng, body = h[i] - l[i], abs(c[i] - o[i])
| |||
impulse = rng > 0 and rng >= p["disp_mult"] * prev and body >= p["body_pct"] / 100.0 * rng - rng * 1e-9 \
| |||
and (c[i] - o[i]) * d > 0
| |||
base, lo_j, from_j = -1, max(0, i - p["search"]), i - 1
| |||
if impulse and p["base_mode"] == 1:
| |||
for j in range(i - 2, lo_j - 1, -1): # the move's start: the lowest low (highest high), latest on a tie
| |||
if (l[j] < l[from_j]) if d > 0 else (h[j] > h[from_j]):
| |||
from_j = j
| |||
if impulse:
| |||
for j in range(from_j, lo_j - 1, -1):
| |||
if (c[j] - o[j]) * d < 0:
| |||
base = j
| |||
break
| |||
zi = -1
| |||
if base >= 0:
| |||
bt, bb = max(o[base], c[base]), min(o[base], c[base])
| |||
zm = p["zone_mode"]
| |||
top = bt if zm == 1 else (bt if zm == 2 and d > 0 else h[base])
| |||
bot = bb if zm == 1 else (bb if zm == 2 and d < 0 else l[base])
| |||
if top - bot > 0:
| |||
z = dict(dir=d, top=top, bot=bot, mid=(top + bot) / 2.0, t0=base, born=i, state=FRESH,
| |||
end=-1, retests=0, inside=False)
| |||
zi = len(zones)
| |||
zones.append(z)
| |||
live.append(z)
| |||
side = 0 if d > 0 else 1
| |||
active[side] += 1
| |||
if p["max_live"] > 0 and active[side] > p["max_live"]:
| |||
for k, y in enumerate(live):
| |||
if (0 if y["dir"] > 0 else 1) == side:
| |||
y["state"], y["end"] = DROPPED, i
| |||
del live[k]
| |||
active[side] -= 1
| |||
break
| |||
out["dem_top" if d > 0 else "sup_top"][i] = top
| |||
out["dem_bot" if d > 0 else "sup_bot"][i] = bot
| |||
out["dem_base" if d > 0 else "sup_base"][i] = i - base
| |||
breaks.append(dict(dir=d, level=sw[s]["p"], frm=sw[s]["i"], at=i, zone=zi))
| |||
out["live_dem"][i], out["live_sup"][i] = active[0], active[1]
| |||
q = i - S
| |||
if q >= S:
| |||
for side in (1, -1):
| |||
if is_swing(q, side):
| |||
sw[0 if side > 0 else 1] = dict(i=q, p=h[q] if side > 0 else l[q], broken=False)
| |||
if on_bar is not None:
| |||
on_bar(i, zones)
| |||
return out, zones, breaks, live, atr
|