"""Diagrama de trigger MBP (marcajul `trigger`), Pasul 3.
Două panouri de 15 minute care pornesc din exact aceeași candelă M (close
623.87 sub banda 623.90, low 623.60 — cifrele exemplului din curs), și se
despart după ea:
- DA: prima candelă de după M e verde și închide la 623.95 (Buy Momentum
din curs). Acolo intri, la close-ul ei.
- NU: cele trei candele de după M rămân roșii, fără nicio candelă de
moment. Nu intri. Cine ar fi cumpărat „că oricum era oversold" ar fi
avut stopul la 623.60 și l-ar fi văzut rupt două bare mai târziu.
Zona punctată acoperă exact ferestrele în care aștepți trigger-ul, iar
numărul din eticheta ei e numărul de bare pe care le acoperă, calculat.
Rulează: python3 gen/strategii/mbp_trigger.py
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Arrow, Chart, Level, Zone, from_closes # noqa: E402
OUT = Path(__file__).with_suffix(".svg")
RED, GREEN, BLUE, GRAY = "#c00", "#188a3e", "#1a5fb4", "#777"
BAND = 623.90 # banda inferioară din exemplul cursului
M = 10 # indexul candelei M, în ambele panouri
COMUN = [624.95, 624.80, 624.62, 624.70, 624.50, 624.36, 624.20, 624.28,
624.10, 624.02, 623.87] # 0..10, ultima = M
W_COMUN = [(0.06, 0.10)] * 10 + [(0.06, 0.27)] # M: low 623.60
DA = COMUN + [623.95, 624.12, 624.24] # 11 = trigger verde
W_DA = W_COMUN + [(0.05, 0.12), (0.06, 0.08), (0.06, 0.08)]
NU = COMUN + [623.80, 623.62, 623.45] # trei bare roșii, fără trigger
W_NU = W_COMUN + [(0.04, 0.10), (0.04, 0.10), (0.04, 0.10)]
da = from_closes(DA, wicks=W_DA, start=625.10)
nu = from_closes(NU, wicks=W_NU, start=625.10)
# ── ce afirmă textul, verificat pe serie ───────────────────────────────────
assert len(da) == len(nu) == 14
assert all(abs(a.c - b.c) < 1e-9 for a, b in zip(da[:M + 1], nu[:M + 1])), \
"panourile trebuie să pornească din aceeași candelă M"
assert abs(da[M].c - 623.87) < 1e-9 and da[M].c < BAND # M închide sub bandă
assert da[M].c < da[M].o # M e roșie
assert abs(da[M].l - 623.60) < 1e-9, da[M].l # low-ul lui M = viitorul stop
TRIG = M + 1
entry = da[TRIG].c
assert abs(entry - 623.95) < 1e-9, entry
assert da[TRIG].c > da[TRIG].o, "candela de trigger trebuie să fie verde"
assert entry > da[M].c, "trigger-ul închide peste close-ul lui M"
sl = min(c.l for c in da[TRIG - 3:TRIG + 1]) # 4 candele, trigger inclus
assert abs(sl - 623.60) < 1e-9, sl
assert sl == da[M].l
# panoul NU: nicio candelă verde după M, iar prețul rupe nivelul stopului
fereastra_nu = nu[M + 1:]
assert all(c.c < c.o for c in fereastra_nu), "în panoul NU nu are voie să apară o candelă verde"
assert min(c.l for c in fereastra_nu) < sl, "NU: prețul trebuie să rupă 623.60"
rupere = min(c.l for c in fereastra_nu)
n_nu = len(fereastra_nu)
assert n_nu == 3
pierdere = sl - rupere
SCALE, GAP, BODY, LEFT, PW = 190.0, 22, 14, 44, 372
pts_all = [p for c in (da + nu) for p in (c.h, c.l)] + [BAND]
P_LO, P_HI = min(pts_all), max(pts_all)
AER = 0.07 * (P_HI - P_LO)
SPACER = [Level(P_HI + AER, "", "none", dash=""), Level(P_LO - AER, "", "none", dash="")]
def panel(cds, levels, zones, arrows):
ch = Chart(candles=cds, levels=levels + SPACER, zones=zones, arrows=arrows,
scale=SCALE, gap=GAP, body_w=BODY, left=LEFT, width=PW)
raw = ch.render()
m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', raw)
assert m
w, h = int(m.group(1)), float(m.group(2))
return ch, w, h, raw[raw.index(">") + 1:raw.rindex("")].strip("\n")
ch_da, w_da, h_da, in_da = panel(
da,
[Level(BAND, f"bandă inferioară {BAND:.2f}", BLUE, dash="4 3", label_at="left"),
Level(sl, f"SL {sl:.2f}", RED, dash="4 3", label_at="left")],
[Zone(TRIG, TRIG, "", GREEN)],
[Arrow(TRIG, f"trigger: candelă verde,\nintri la close {entry:.2f}", where="above", color=GREEN),
Arrow(M, f"M: close {da[M].c:.2f},\nsub bandă", where="below", color=RED)])
ch_nu, w_nu, h_nu, in_nu = panel(
nu,
[Level(BAND, f"bandă inferioară {BAND:.2f}", BLUE, dash="4 3", label_at="left"),
Level(sl, f"stopul ar fi fost {sl:.2f}", RED, dash="4 3", label_at="left")],
[Zone(M + 1, M + n_nu, f"{n_nu} candele fără trigger", RED)],
[Arrow(len(nu) - 1, f"aici s-ar fi rupt stopul,\ncu {pierdere:.2f} $ sub {sl:.2f}",
where="below", color=RED)])
PH = max(h_da, h_nu)
add_da: list[str] = []
add_nu: list[str] = []
FOOTER = [
f"Aceeași candelă M în ambele panouri: close {da[M].c:.2f} sub banda {BAND:.2f}, "
f"low {sl:.2f}. Ce urmează după ea decide.",
f"DA: candela {TRIG - M} de după M e verde și închide la {entry:.2f} — ăsta e trigger-ul, "
f"iar close-ul ei e prețul de intrare.",
f"NU: {n_nu} candele roșii la rând, niciun trigger. Nu intri. Prețul coboară până la "
f"{rupere:.2f}, cu {pierdere:.2f} $ sub {sl:.2f},",
"adică exact sub stopul pe care l-ai fi avut dacă intrai „pentru că oricum era oversold\".",
"Condiția M (bandă + TICUS) nu e un ordin de cumpărare: e doar permisiunea de a aștepta trigger-ul.",
"Cifrele lui M și ale trigger-ului (623.87 / 623.90 / 623.60 / 623.95) sunt din curs; "
"restul barelor sunt construite de mine.",
]
TOP = 52
X_DA, X_NU = 8, 8 + w_da + 16
RW = max(780, X_NU + w_nu + 8)
root_h = TOP + PH + 24 + 17 * len(FOOTER) + 8
out = [f'")
svg = "\n ".join(out)
def fits(body: str, w: float, h: float, unde: str) -> None:
box: list[tuple[float, float, float, str]] = []
for mm in re.finditer(r']*)>(.*?)', body):
x, y, attrs, txt = float(mm.group(1)), float(mm.group(2)), mm.group(3), mm.group(4)
if not txt.strip():
continue
tw = len(re.sub(r"<[^>]+>", "", txt)) * (6.4 if "bold" in attrs else 5.9)
x0 = x - tw if 'anchor="end"' in attrs else (x - tw / 2 if 'anchor="middle"' in attrs else x)
assert x0 >= -2 and x0 + tw <= w + 2, f"{unde}: text tăiat: {txt!r}"
assert 0 <= y <= h + 2, f"{unde}: text în afara înălțimii: {txt!r}"
box.append((x0, x0 + tw, y, txt))
for i in range(len(box)):
for j in range(i + 1, len(box)):
(a0, a1, ay, at), (b0, b1, by, bt) = box[i], box[j]
assert not (abs(ay - by) < 13 and a0 < b1 - 4 and b0 < a1 - 4), \
f"{unde}: texte suprapuse: {at!r} / {bt!r}"
fits(in_da + "\n" + "\n".join(add_da), w_da, PH, "panoul DA")
fits(in_nu + "\n" + "\n".join(add_nu), w_nu, PH, "panoul NU")
assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
assert svg.count("") == 3
assert f"{n_nu} candele fără trigger" in svg and n_nu == len(fereastra_nu)
OUT.write_text(svg, encoding="utf-8")
print(f"scris {OUT.name}: M {da[M].c:.2f} (low {sl:.2f}), trigger {entry:.2f}; "
f"NU {n_nu} bare roșii, rupere {rupere:.2f} ({pierdere:.2f} $ sub stop)")