# SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE) """Independent Python reference for SMC Zones, bar by bar. Swings, sweeps, BOS/CHoCH, order block and FVG written as a closed-bar state machine. Everything at bar i is decided from bars <= i only; the last bar of the array is the forming bar and is never used. A zone's creation bar and mitigation bar, once set, never change. Rules (dir +1 = bullish, -1 = bearish): ATR Wilder (RMA) of the true range, seeded with the SMA of the first `atr` values. swing bar p is a swing high (low) if each of the `swing` bars to its left has a strictly lower high (higher low) and each of the `swing` bars to its right a high not above (low not below). Known at bar p + swing. BOS/CHoCH a close beyond the newest unbroken swing high (low). CHoCH when the previous break went the other way, otherwise BOS. The first break ever is a BOS. order block the zone left by a break: org = lowest low (highest high) bar from the broken swing to the break bar. If org is a candle against the move (bearish candle for a bullish break), the OB candle is the last of the run of against-candles starting at org, else the nearest against-candle among the 3 bars before org, else org. Bullish zone: top = high[ob], bot = min(low[ob], low[org]); bearish: top = max(high[ob], high[org]), bot = low[ob]. Drawn from the earlier of ob/org. FVG bars i-2, i, gap = low[i] - high[i-2] (bullish) or low[i-2] - high[i] (bearish) >= fvg_min * ATR[i]. sweep a bar wicks beyond an unswept swing (confirmed, at most `sweep_look` bars old) by >= sweep_min * ATR and closes back inside: the lowest low (highest high) such swing is reported. Every swing wicked through is marked taken whether or not the bar closed back inside. mitigation (bars after the zone's creation bar), modes: 0 touch (wick reaches the near edge), 1 midpoint (wick reaches the middle), 2 far-edge wick, 3 far-edge close. Midpoint is inclusive within a relative 1e-10. touch a bar whose wick reaches the near edge of a live zone that was not inside on the bar before. session optional: zones and sweeps are only created on bars whose open hour (server time) is inside [start, end). Per bar the order is: mitigation/touch of older zones, sweeps, FVG, break + order block, then the swing `swing` bars back.""" import numpy as np NAN = np.nan def true_range(h, l, c): tr = h - l tr[1:] = np.maximum(tr[1:], np.maximum(np.abs(h[1:] - c[:-1]), np.abs(l[1:] - c[:-1]))) return tr def rma(x, n): out = np.full(len(x), NAN) if len(x) < n: return out out[n - 1] = x[:n].mean() for i in range(n, len(x)): out[i] = (out[i - 1] * (n - 1) + x[i]) / n return out def in_session(t, start, end): hr = (int(t) // 3600) % 24 if start == end: return True return start <= hr < end if start < end else (hr >= start or hr < end) def mitigated(z, mode, o, h, l, c): """Does this bar mitigate zone z?""" top, bot = z["top"], z["bot"] mid = (top + bot) / 2.0 tol = abs(mid) * 1e-10 # a wick exactly on the midpoint counts; (top+bot)/2 can be 1 ulp off in decimals if z["dir"] > 0: return {0: l <= top, 1: l <= mid + tol, 2: l <= bot, 3: c < bot}[mode] return {0: h >= bot, 1: h >= mid - tol, 2: h >= top, 3: c > top}[mode] def smc_zones(t, o, h, l, c, swing=3, atr=14, fvg_min=0.20, ob_mit=3, fvg_mit=2, sweep_look=60, sweep_min=0.10, use_session=False, sess_start=7, sess_end=17): n = len(c) A = rma(true_range(h, l, c), atr) nan = lambda: np.full(n, NAN) out = {k: nan() for k in ("ob_bull_top", "ob_bull_bot", "ob_bear_top", "ob_bear_bot", "fvg_bull_top", "fvg_bull_bot", "fvg_bear_top", "fvg_bear_bot", "struct", "level", "sweep_bull", "sweep_bear", "mitigated", "touched", "active_bull", "active_bear")} zones, swings, breaks, sweeps, live = [], [], [], [], [] newest = {1: None, -1: None} # newest swing per side (side +1 = high) trend = 0 swept_at = {1: -1, -1: -1} def is_swing(p, side): for k in range(1, swing + 1): if side > 0 and (h[p - k] >= h[p] or h[p + k] > h[p]): return False if side < 0 and (l[p - k] <= l[p] or l[p + k] < l[p]): return False return True def far(i, d): return l[i] if d > 0 else h[i] def near(i, d): return h[i] if d > 0 else l[i] def against(i, d): return c[i] < o[i] if d > 0 else c[i] > o[i] for i in range(n - 1): # the last bar is still forming ok_atr = not np.isnan(A[i]) and A[i] > 0 if ok_atr: a = A[i] sess = (not use_session) or in_session(t[i], sess_start, sess_end) # 1. older zones: mitigated or touched n_mit = n_touch = 0 for z in live: if z["born"] >= i: continue mode = ob_mit if z["kind"] == "ob" else fvg_mit if mitigated(z, mode, o[i], h[i], l[i], c[i]): z["end"] = i n_mit += 1 continue d = z["dir"] inside = (l[i] <= z["top"]) if d > 0 else (h[i] >= z["bot"]) if inside and not z["inside"]: z["touches"] += 1 n_touch += 1 z["inside"] = inside live[:] = [z for z in live if z["end"] is None] out["mitigated"][i], out["touched"][i] = n_mit, n_touch # 2. sweeps for d in (1, -1): side, best = -d, None for s in reversed(swings): if i - s["i"] > sweep_look: break if s["side"] != side or s["swept"]: continue if (far(i, d) - (s["p"] - d * sweep_min * a)) * d < 0: s["swept"] = True if (c[i] - s["p"]) * d > 0 and (best is None or s["p"] * d < best["p"] * d): best = s if best is not None: swept_at[d] = i if sess: out["sweep_bull" if d > 0 else "sweep_bear"][i] = best["p"] sweeps.append(dict(dir=d, level=best["p"], at=i, frm=best["i"])) # 3. fair value gap if i >= 2: for d in (1, -1): gap = (l[i] - h[i - 2]) if d > 0 else (l[i - 2] - h[i]) if gap >= fvg_min * a and sess: top, bot = (l[i], h[i - 2]) if d > 0 else (l[i - 2], h[i]) zones.append(dict(kind="fvg", dir=d, top=top, bot=bot, t0=i - 1, born=i, end=None, touches=0, inside=False)) live.append(zones[-1]) k = "fvg_bull" if d > 0 else "fvg_bear" out[k + "_top"][i], out[k + "_bot"][i] = top, bot # 4. structure break and order block for d in (1, -1): s = newest[d] if s is None or s["broken"] or (c[i] - s["p"]) * d <= 0: continue s["broken"] = True choch = trend == -d trend = d org = s["i"] for j in range(s["i"], i + 1): if (far(j, d) - far(org, d)) * d < 0: org = j ob = -1 if against(org, d): ob = org while ob + 1 < i and against(ob + 1, d): ob += 1 else: for j in range(org - 1, max(0, org - 3) - 1, -1): if against(j, d): ob = j break if ob < 0: ob = org if d > 0: top, bot = h[ob], min(l[ob], l[org]) else: top, bot = max(h[ob], h[org]), l[ob] if sess: zones.append(dict(kind="ob", dir=d, top=top, bot=bot, t0=min(ob, org), born=i, end=None, touches=0, inside=False)) live.append(zones[-1]) k = "ob_bull" if d > 0 else "ob_bear" out[k + "_top"][i], out[k + "_bot"][i] = top, bot out["struct"][i] = d * (2 if choch else 1) out["level"][i] = s["p"] breaks.append(dict(dir=d, choch=choch, level=s["p"], frm=s["i"], at=i)) out["active_bull"][i] = sum(1 for z in live if z["dir"] > 0) out["active_bear"][i] = sum(1 for z in live if z["dir"] < 0) # 5. the swing `swing` bars back is known from the next bar on p = i - swing if p >= swing: for side in (1, -1): if is_swing(p, side): s = dict(i=p, p=h[p] if side > 0 else l[p], side=side, swept=False, broken=False) swings.append(s) newest[side] = s return out, zones, breaks, sweeps