"""Diagrama de stop MBP (marcajul `stop`), Pasul 4.
Fereastra de 4 candele din care iese stopul, mărită: candela de trigger și
cele 3 dinaintea ei (deci candela M intră în fereastră). Cel mai jos minim
din ele e 623.60 — low-ul lui M — iar stopul stă fix acolo, ca în exemplul
din curs (fără marjă adăugată; e aceeași regulă în toate diagramele
capitolului).
Pe desen: zona punctată peste exact cele 4 candele (numărul din etichetă e
numărat, nu scris de mână), linia de intrare la 623.95, linia de stop la
623.60 și măsura 1R = 0.35 $ între ele, cu care se calculează mărimea
poziției.
Cifrele din curs (`05_MODUL_4/03_strategia_6_mbp_mbappe.md`): M close 623.87
sub banda 623.90, trigger → intrare 623.95, SL 623.60 = lowest low din 4
candele, trigger inclus. Structura barelor e construită de mine.
Rulează: python3 gen/strategii/mbp_stop.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
M = 11
CLOSES = [624.90, 624.75, 624.60, 624.68, 624.50, 624.38, 624.22, 624.30,
624.12, 624.05, 623.98, 623.87, 623.95, 624.08, 624.02, 624.15]
WICKS = ([(0.06, 0.10)] * 9
+ [(0.05, 0.20), # 9: low 623.85
(0.05, 0.16), # 10: low 623.82
(0.06, 0.27), # 11 = M: low 623.60
(0.05, 0.12), # 12 = trigger: low 623.75
(0.06, 0.08), (0.06, 0.08), (0.06, 0.08)])
assert len(CLOSES) == len(WICKS) == 16
cds = from_closes(CLOSES, wicks=WICKS, start=625.02)
TRIG = M + 1
I0 = TRIG - 3 # prima candelă din fereastră
# ── ce afirmă textul, verificat pe serie ───────────────────────────────────
entry = cds[TRIG].c
assert abs(entry - 623.95) < 1e-9, entry
assert cds[TRIG].c > cds[TRIG].o # trigger verde
assert abs(cds[M].c - 623.87) < 1e-9 and cds[M].c < BAND
assert cds[M].c < cds[M].o # M roșie
fereastra = cds[I0:TRIG + 1]
N_F = len(fereastra)
assert N_F == 4, N_F
sl = min(c.l for c in fereastra)
assert abs(sl - 623.60) < 1e-9, sl
assert sl == cds[M].l, "cel mai jos minim din fereastră e low-ul lui M"
assert all(c.l > sl for c in fereastra if c is not cds[M]), \
"nicio altă candelă din fereastră nu coboară sub low-ul lui M"
risc = entry - sl
assert abs(risc - 0.35) < 1e-9, risc
# stopul nu are marjă adăugată: e fix pe minim, ca în cifra cursului
assert sl == min(c.l for c in fereastra)
# candela dinaintea ferestrei are minim peste stop — altfel fereastra de 4 n-ar conta
assert cds[I0 - 1].l > sl, cds[I0 - 1].l
# mărimea poziției, calculată din risc (cifrele contului sunt alegerea mea)
CONT, RISC_PCT = 50_000.0, 0.5
risc_bani = CONT * RISC_PCT / 100
unitati = int(risc_bani / risc)
notional = unitati * entry
assert unitati == 714, unitati
assert abs(notional / CONT - 8.9) < 0.1, notional / CONT
SCALE, GAP, BODY, LEFT, W = 300.0, 30, 18, 64, 756
AER = 0.07 * (max(c.h for c in cds) - min(c.l for c in cds))
ch = Chart(
candles=cds,
levels=[Level(BAND, f"banda inferioară {BAND:.2f}", BLUE, dash="4 3", label_at="left"),
Level(entry, f"intrare {entry:.2f} = close-ul trigger-ului", GREEN, dash="2 3",
label_at="right"),
Level(sl, f"SL {sl:.2f}", RED, dash="4 3", label_at="left"),
Level(max(c.h for c in cds) + AER, "", "none", dash=""),
Level(sl - AER, "", "none", dash="")],
zones=[Zone(I0, TRIG, f"{N_F} candele: trigger-ul și cele 3 dinaintea lui", RED)],
arrows=[Arrow(M, f"cel mai jos minim din fereastră: {sl:.2f}\nsub el, ideea de epuizare "
f"e moartă — acolo stă stopul", where="below", color=RED),
Arrow(TRIG, "trigger: aici intri, la close", where="above", color=GREEN, dx=52, dy=-24)],
scale=SCALE, gap=GAP, body_w=BODY, left=LEFT, width=W,
title="SPY — 15 min: de unde iese stopul (fereastra de 4 candele)")
svg = ch.render()
m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg)
assert m
W2, H = int(m.group(1)), float(m.group(2))
# ── măsura 1R între intrare și stop, lipită de liniile ei ──────────────────
xr = ch._cx(len(cds) - 1) + 26
y_e, y_s = ch._y(entry), ch._y(sl)
assert y_s > y_e
masura = [
f'',
f'',
f'',
f'1R = {risc:.2f} $',
]
W3 = max(W2, xr + 8 + len(f"1R = {risc:.2f} $") * 6.4 + 8)
LEG = [
f"Numeri {N_F} candele înapoi începând cu cea de trigger (trigger, M și cele două "
f"dinaintea lui M) și iei cel mai jos minim:",
f"{sl:.2f}. Acolo pui stopul, fix pe minim, fără marjă adăugată — asta e cifra cursului.",
f"Riscul pe unitate iese din desen: {entry:.2f} − {sl:.2f} = {risc:.2f} $ = 1R. Din el "
f"calculezi câte unități cumperi.",
f"Completare proprie (cifrele contului sunt ale mele): cont {CONT:,.0f} $, risc "
f"{RISC_PCT} % = {risc_bani:.0f} $ → {risc_bani:.0f} / {risc:.2f} = {unitati} unități,".replace(",", "."),
f"adică un notional de {notional:,.0f} $ ≈ {notional / CONT:.1f}× contul. "
f"Un stop strâns nu înseamnă un trade mic.".replace(",", "."),
]
W3 = max(W3, max(len(t) for t in LEG) * 5.9 + 32)
ly = H + 6
svg = svg.replace("\n", "\n " + "\n ".join(
masura + [f'{t}'
for i, t in enumerate(LEG)]) + "\n")
H2 = ly + 17 * len(LEG) + 6
svg = re.sub(r'(viewBox="0 0 )\d+ [0-9.]+(")', lambda mm: mm.group(1) + f"{W3:.0f} {H2:.0f}" + mm.group(2),
svg, count=1)
# ── verificări pe SVG-ul final ─────────────────────────────────────────────
assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
assert f"{N_F} candele" in svg and N_F == TRIG - I0 + 1
# liniile nu cad pe marginea cadrului
assert ch._y(sl) < H2 - 20 and ch._y(max(c.h for c in cds)) > 14
box = []
for mm in re.finditer(r']*)>(.*?)', svg):
x, y, attrs, txt = float(mm.group(1)), float(mm.group(2)), mm.group(3), mm.group(4)
tw = len(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 <= W3 + 2, f"text tăiat: {txt!r}"
assert 0 <= y <= H2 + 2, f"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"texte suprapuse: {at!r} / {bt!r}"
# etichetele goale (nivelurile-distanțier) nu au ce căuta în SVG: auditul le
# citește ca text suprapus peste ce e sub ele
svg = re.sub(r"\n\s*]*>\s*", "", svg)
assert svg.count("\n\n") == 0
OUT.write_text(svg, encoding="utf-8")
print(f"scris {OUT.name}: fereastră {N_F} candele [{I0}..{TRIG}], SL {sl:.2f} (= low M), "
f"intrare {entry:.2f}, 1R {risc:.2f}, {unitati} unități, notional {notional:,.0f}")