diff --git a/gen/strategii/atmi_ansamblu.py b/gen/strategii/atmi_ansamblu.py
new file mode 100644
index 0000000..f045744
--- /dev/null
+++ b/gen/strategii/atmi_ansamblu.py
@@ -0,0 +1,123 @@
+"""Diagrama de ansamblu ATMI (strategii/05_atmi.md, marcajul `ansamblu`).
+
+„SPY — daily: corecție ≥4 % → SQ/PPQ → BIM + verde → intrare → TP +10 %".
+
+Structura seriei și nivelurile vin din spec-ul
+`gen/strategii/atmi_ansamblu.spec.txt`. Toate cifrele desenate (100.00 swing
+high, 96.00 intrare, 105.60 = +10 %) sunt din exemplul „DA" al cursului
+(`summaries/04_MODUL_3/03_strategia_2_atmi.md:185-247`). Benzile PPQ/SQ sunt
+schematice: cursul spune explicit că nu dă cifrele lor
+(`...:143`), deci nu poartă prețuri.
+
+Rulează:
+ python3 gen/strategii/atmi_ansamblu.py
+Scrie lângă el `atmi_ansamblu.svg`.
+"""
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402
+
+OUT = Path(__file__).resolve().parent / "atmi_ansamblu.svg"
+
+GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
+PURPLE = "#7b2fbf" # folosit în materialul de curs pentru PPQ
+
+# ---------------------------------------------------------------- seria (33 bare)
+# 15 bare de urcare ~88 -> 100, 8 de corecție 100 -> 96 (ultima = intrarea),
+# 10 de urcare 96 -> 105.60. open-ul fiecărei bare = close-ul precedentei.
+UP_A = [88.2, 88.8, 89.5, 90.1, 90.8, 91.6, 92.5, 93.3, 94.2, 95.1,
+ 96.0, 96.9, 97.8, 98.9, 100.00] # 0-14
+CORR = [99.3, 98.5, 97.8, 97.2, 96.7, 96.3, 96.00, 96.00] # 15-22
+UP_B = [96.6, 97.2, 97.9, 98.6, 99.4, 100.3, 101.4, 102.6, 104.0, 105.60] # 23-32
+CLOSES = UP_A + CORR + UP_B
+assert len(CLOSES) == 33, len(CLOSES)
+PEAK, BIM, ENTRY, LAST = 14, 21, 22, 32
+
+# fitile (sus, jos) per bară; jos pe corecție ținut ≥ 0 ca minimul să fie fix 96.00
+WICKS = ([(0.30, 0.30)] * 14
+ + [(0.00, 0.30)] # 14: vârful atinge exact 100.00
+ + [(0.00, 0.00), (0.05, 0.10), (0.05, 0.10), (0.05, 0.15),
+ (0.05, 0.15), (0.05, 0.20), (0.05, 0.00), (0.12, 0.00)] # 15-22
+ + [(0.25, 0.25)] * 9
+ + [(0.00, 0.20)]) # 32: închide/atinge exact 105.60
+assert len(WICKS) == len(CLOSES)
+
+VOLS = ([44, 47, 42, 50, 45, 41, 48, 43, 46, 51, 44, 49, 46, 52, 58] # 0-14 urcare
+ + [64, 70, 66, 61, 58, 74, 96, 88] # 15-22 corecție + BIM/intrare
+ + [55, 58, 52, 60, 57, 63, 59, 66, 72, 84]) # 23-32 urcare spre TP
+assert len(VOLS) == len(CLOSES)
+
+cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=88.0)
+
+# ---------------------------------------------------------------- assert-urile din spec
+high = 100.00
+entry = cds[ENTRY].c
+tp = cds[LAST].h
+assert abs((high - 96.00) / high * 100 - 4.0) < 0.05 # corecția e 4.0 %
+assert abs(entry - 96.00) < 0.05 # intrarea = close-ul candelei verzi
+assert abs(tp / entry - 1.10) < 0.005 # TP = +10 %
+assert abs(tp - max(c.h for c in cds[-10:])) < 1e-9 # TP atins la final, e maximul barelor
+assert BIM < ENTRY # BIM înainte de candela verde
+assert abs(min(c.l for c in cds[15:23]) - 96.00) < 1e-9 # minimul corecției = 96.00
+assert cds[ENTRY].c >= cds[ENTRY].o # candela de intrare e verde
+assert abs(cds[PEAK].h - high) < 1e-9 # swing high = 100.00
+assert abs(cds[LAST].c - tp) < 1e-9 # ultima bară închide pe TP
+
+levels = [
+ Level(high, "100.00 — swing high recent", "#333", label_at="left"),
+ Level(entry, f"intrare {entry:.2f}", GREEN, label_at="right"),
+ Level(tp, f"TP +10 % = {tp:.2f}", BLUE, label_at="right"),
+]
+# nicio linie de stop-loss și niciun nivel sub intrare (absența SL e regula)
+assert all("stop" not in lv.text.lower() for lv in levels)
+assert all(lv.price >= entry - 1e-9 for lv in levels)
+
+marks = [
+ Mark(15, "corecție 4.0 %", ORANGE, "above"),
+ Mark(BIM, "BIM", ORANGE, "above"),
+ Mark(ENTRY, "BIM + candelă verde → intrare", GREEN, "below", dy=20),
+]
+
+ch = Chart(candles=cds, levels=levels, marks=marks,
+ mas=[MA(20, ORANGE, dash="4 3", text="SMA-20 (ilustrativ)")],
+ scale=18.0, gap=16, body_w=9, left=44, width=720, vol_h=70, top=70,
+ vol_label="volum",
+ caption="Niveluri (100.00 / 96.00 / 105.60) din curs; PPQ/SQ schematice, fără cifre în material.")
+
+svg = ch.render()
+
+
+def band(price: float, half: float, color: str, label: str) -> str:
+ """Bandă schematică orizontală, desenată în spatele candelelor.
+
+ Se injectează în SVG după randare, pentru că `Chart` nu are benzi; y-ul
+ se calculează cu aceeași scară (`ch._y`), deci banda e aliniată corect.
+ """
+ y_top, y_bot = ch._y(price + half), ch._y(price - half)
+ return (f''
+ f'{label}')
+
+
+# SQ e zona joasă (intrarea exact pe ea), PPQ zona mai sus, retragere mai mică
+assert (entry + 1.60 - 0.35) > (entry + 0.35), "benzile SQ și PPQ nu se suprapun"
+sq = band(entry, 0.35, GREEN, "SQ — zonă quarterly")
+ppq = band(entry + 1.60, 0.35, PURPLE, "PPQ — zonă quarterly")
+# „fără stop-loss standard" stă sub zona de intrare, unde nu are candele
+no_sl = (f'fără stop-loss standard'
+ f'(03_strategia_2_atmi.md:164-169)')
+# titlul e injectat deasupra, ca rând separat de eticheta TP (care stă chiar
+# sub marginea de sus a panoului); altfel cele două texte se calcă la dreapta
+titlu = (''
+ 'SPY — daily: corecție ≥4 % → SQ/PPQ → BIM + verde → intrare → TP +10 %')
+
+i = svg.index(">") + 1
+svg = svg[:i] + "\n " + "\n ".join([sq, ppq, no_sl, titlu]) + svg[i:]
+
+assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
+OUT.write_text(svg)
+print(f"scris {OUT.name}: {len(cds)} candele, intrare {entry:.2f}, TP {tp:.2f}")
diff --git a/gen/strategii/atmi_ansamblu.svg b/gen/strategii/atmi_ansamblu.svg
new file mode 100644
index 0000000..fec12fe
--- /dev/null
+++ b/gen/strategii/atmi_ansamblu.svg
@@ -0,0 +1,119 @@
+
\ No newline at end of file
diff --git a/gen/strategii/atmi_aplus.py b/gen/strategii/atmi_aplus.py
new file mode 100644
index 0000000..4ddcd31
--- /dev/null
+++ b/gen/strategii/atmi_aplus.py
@@ -0,0 +1,158 @@
+"""ATMI — aceeași corecție: 4.0 % (DA) vs. 3.9 % (NU; cursul zice „3.93 %").
+
+Citește specificația `gen/strategii/atmi_aplus.spec.txt`.
+
+O singură serie de 32 de candele daily (open-ul fiecărei bare = close-ul
+precedentei, deci seria e continuă), care urmează calea DA: urcare la swing
+high 100.00, corecție exact 4.0 % până la 96.00 (BIM + candelă verde =
+intrare), apoi urcare la TP +10 % = 105.60. Peste ea, un traseu punctat
+(calea NU) care se oprește la 96.10 = 3.9 % și apoi coboară ilustrativ — fără
+intrare, pentru că nu atinge pragul de 4 %.
+
+Traseele punctate / benzile se inserează în SVG-ul scos de `svg_candles`
+folosind coordonatele lui (`ch._cx`, `ch._y`), la fel ca în `az_structura.py`
+— candelele rămân desenate de `svg_candles`, nu de mână.
+"""
+import sys
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, from_closes # noqa: E402
+
+GRAY, RED, GREEN, BLUE, ORANGE, INK = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000", "#333"
+
+# --- seria: 32 de close-uri, structura din specificație ----------------------
+# 0-11 urcare 90.00 -> 100.00 (swing high la bara 11)
+# 12-17 corecție (cădere)
+# 18 BIM (hammer de reversie, codiță lungă în jos)
+# 19 prima candelă verde daily => intrare, close 96.00
+# 20-31 urcare 96.00 -> 105.60 (TP +10 %)
+closes = [
+ 90.00, 90.90, 91.70, 92.40, 93.20, 94.00, 94.70, 95.40, 96.10, 97.00, 98.30, 100.00, # 0-11
+ 99.30, 98.50, 97.70, 97.10, 96.60, 96.20, # 12-17
+ 95.98, # 18 BIM
+ 96.00, # 19 intrare (verde)
+ 97.00, 97.90, 98.80, 99.70, 100.50, 101.30, 102.10, 102.90, 103.70, 104.50, 105.10, 105.60, # 20-31
+]
+assert len(closes) == 32
+
+# wick-uri (sus, jos): implicite (0.25, 0.30), cu excepțiile care fixează nivelurile
+wicks = [(0.25, 0.30)] * 32
+wicks[11] = (0.00, 0.30) # swing high: high = close = 100.00
+wicks[18] = (0.20, 0.45) # BIM: codiță lungă în jos (hammer)
+wicks[19] = (0.20, 0.15) # candela de intrare
+wicks[31] = (0.00, 0.30) # TP: high = close = 105.60
+
+# volum relativ ales de generator (materialul nu dă volum pe exemplu):
+# crește pe corecție (vânzătorii), vârf pe candela de intrare, scade pe urcare.
+vols = [48, 50, 46, 52, 54, 50, 56, 52, 58, 60, 64, 72,
+ 70, 74, 78, 82, 86, 80, 84,
+ 88,
+ 72, 66, 62, 58, 54, 50, 46, 62, 68, 72, 78, 86]
+assert len(vols) == 32
+
+cds = from_closes(closes, wicks=wicks, vols=vols, start=89.40)
+
+# --- indicii-cheie și nivelurile ---------------------------------------------
+SWING_I = 11 # swing high
+BIM_I = 18 # primul BIM
+ENTRY_I = 19 # candela verde de intrare
+SWING = 100.00
+PRAG4 = 96.00 # prag corecție 4.0 % => DA
+NU_STOP = 96.10 # cazul real 3.9 % => NU
+TP = 105.60 # +10 % față de 96.00
+entry = cds[ENTRY_I].c
+
+# --- cele 8 assert-uri cerute de specificație --------------------------------
+# 1. pragul de 4 % corespunde lui 96.00
+assert abs((SWING - PRAG4) / SWING * 100 - 4.00) < 0.05, "96.00 nu e 4.0 %"
+# 2. cazul NU: 96.10 = 3.9 % (graficul cursului scrie 3.9 %; textul „3.93 %")
+assert abs((SWING - NU_STOP) / SWING * 100 - 3.90) < 0.02, "96.10 nu e 3.9 %"
+# 3. calea DA coboară mai jos decât calea NU
+assert PRAG4 < NU_STOP, "pragul DA nu e sub oprirea NU"
+# 4. intrarea = close-ul candelei verzi = 96.00
+assert abs(entry - 96.00) < 1e-9, f"intrarea {entry} != 96.00"
+assert cds[ENTRY_I].c >= cds[ENTRY_I].o, "candela de intrare nu e verde"
+# 5. TP = +10 %
+assert abs(TP / entry - 1.10) < 0.005, f"TP/entry = {TP / entry:.4f}, nu 1.10"
+# 6. BIM înainte de candela de intrare
+assert BIM_I < ENTRY_I, "BIM nu e înainte de candela de intrare"
+# 7. traseul NU nu atinge pragul de trigger: se oprește la 96.10 > 96.00, deci
+# rămâne fără intrare; pe traseul punctat nu există nicio candelă / BIM
+nu_route = [(SWING_I, 100.00), (12, 99.40), (13, 98.65), (14, 97.95),
+ (15, 97.35), (16, 96.80), (17, NU_STOP)]
+assert min(p for _, p in nu_route) == NU_STOP, "oprirea traseului NU nu e 96.10"
+assert min(p for _, p in nu_route) > PRAG4, "traseul NU atinge pragul de 4 %"
+assert not any(i >= BIM_I for i, _ in nu_route), "traseul NU ajunge la candela BIM"
+# 8. ambele trasee pleacă din același swing high 100.00
+assert abs(cds[SWING_I].h - SWING) < 1e-9, f"swing high {cds[SWING_I].h} != 100.00"
+assert abs(nu_route[0][1] - cds[SWING_I].h) < 1e-9, "traseul NU nu pleacă din swing high"
+
+# verificări de context cerute de STYLE.md (structura susține eticheta)
+assert abs(TP - max(c.h for c in cds[-10:])) < 1e-9, "TP nu e maximul de la final"
+assert min(closes[12:20]) < 96.10 and min(closes[12:20]) >= 95.90, \
+ "corecția DA nu coboară în jurul lui 96.00"
+assert all(b > a for a, b in zip(closes[20:31], closes[21:32])), "urcarea spre TP nu e monotonă"
+
+print(f"swing {SWING:.2f} prag DA {PRAG4:.2f} (4.0 %) stop NU {NU_STOP:.2f} (3.9 %) "
+ f"intrare {entry:.2f} TP {TP:.2f} (+{TP / entry - 1:.1%}) BIM bara {BIM_I}")
+
+# --- desenul -----------------------------------------------------------------
+cds[ENTRY_I].note = ("Regula e pe procent, nu pe cât de dramatică arată linia: "
+ "calea NU pare o cădere mare, dar e 3.9 %.")
+
+# traseul NU: decizia (până la 96.10) + continuarea ilustrativă în jos
+nu_cont = [(17, NU_STOP), (18, 95.70), (19, 95.35), (21, 94.95), (23, 94.65), (25, 94.45)]
+nu_pts = nu_route + nu_cont[1:]
+
+ch = Chart(
+ candles=cds,
+ levels=[
+ Level(SWING, "100.00 — swing high", INK),
+ Level(PRAG4, "96.00 = 4.0 % — DA, intri", GREEN),
+ Level(TP, "TP +10 % = 105.60", BLUE),
+ ],
+ marks=[
+ Mark(BIM_I, "BIM", GREEN, "above", dy=-2),
+ Mark(ENTRY_I, "candelă verde → trigger", GREEN, "below", dy=20),
+ ],
+ scale=18.0, gap=16, body_w=9, left=44, width=680, vol_h=70,
+ vol_label="volum", title="SPY — daily: aceeași corecție, 4.0 % (DA) vs. 3.9 % (NU)",
+ caption="Calea DA (candele): 4.0 % la 96.00. Calea NU (punctat): 3.9 % („3.93 %” în curs) — fără intrare.",
+)
+svg = ch.render()
+
+# --- elemente suplimentare, pe coordonatele graficului -----------------------
+x0, x1 = 24, ch.width - 14
+band_top, band_bot = ch._y(96.40), ch._y(95.40)
+band = (f'')
+
+nu_line = ('')
+# linia pragului NU (96.10) se desenează manual, ca Level-ul gol să nu lase text vid
+nu_hline = (f'')
+# punctul de oprire al traseului NU (96.10, strict deasupra pragului de 4 %)
+stop_dot = (f'')
+# eticheta traseului NU, sub linia 96.10 (nu se suprapune cu eticheta verde de 96.00)
+nu_lab = (f''
+ f'96.10 = 3.9 % — NU, sub prag')
+# eticheta zonei quarterly, sub banda (fără cifre: cursul nu le dă)
+zone_lab = (f''
+ f'zonă quarterly')
+# legende scurte ale celor două trasee
+da_lab = (f'calea DA (candelele)')
+nu_leg = (f'calea NU (continuă în jos)')
+
+overlay = [nu_line, nu_hline, stop_dot, nu_lab, zone_lab, da_lab, nu_leg]
+svg = svg.replace(">", ">\n " + band, 1) # banda, sub candele
+svg = svg.replace("\n", "\n " + "\n ".join(overlay) + "\n") # traseele, peste
+
+assert "\n\n" not in svg, "linie goală în SVG"
+assert "" in svg and "stroke-dasharray=\"5 4\"" in svg, "traseul NU nu a fost inserat"
+
+with open("/workspace/atm-curs-dl/gen/strategii/atmi_aplus.svg", "w") as f:
+ f.write(svg)
+print("scris gen/strategii/atmi_aplus.svg")
diff --git a/gen/strategii/atmi_aplus.svg b/gen/strategii/atmi_aplus.svg
new file mode 100644
index 0000000..07e53ee
--- /dev/null
+++ b/gen/strategii/atmi_aplus.svg
@@ -0,0 +1,119 @@
+
\ No newline at end of file
diff --git a/gen/strategii/atmi_exemplu.py b/gen/strategii/atmi_exemplu.py
new file mode 100644
index 0000000..806b23b
--- /dev/null
+++ b/gen/strategii/atmi_exemplu.py
@@ -0,0 +1,125 @@
+"""Diagrama din `gen/strategii/atmi_exemplu.spec.txt` (TIP: PRET).
+
+Exemplul real din curs (ATMI / SPY weekly): corecție de context ≥4 %,
+intrare pe 27.01.2005 la 117.43 după BIM + candelă verde, fără stop-loss
+standard, urcare lentă până la TP1 +10 % = 129.17, atins ~1 an mai târziu
+(data exactă nesigură în ASR).
+
+Serie continuă de 38 de candele weekly (from_closes), panou de volum,
+niveluri reale (intrare / TP1) și marcaje ancorate. Toate cifrele vin din
+`summaries/04_MODUL_3/03_strategia_2_atmi.md` (liniile citate în spec);
+singurul număr construit de mine e swing high-ul ~123.00, care trebuie să
+conțină corecția de context.
+"""
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402
+
+OUT = Path(__file__).resolve().parent / "atmi_exemplu.svg"
+
+# ── cifrele din material ────────────────────────────────────────────────────
+ENTRY = 117.43 # 04_MODUL_3/03_strategia_2_atmi.md:358-363
+TP = 129.17 # TP1 = +10 % pe 117.43 → 129.17 (":170-171, 358-363")
+SWING_HIGH = 123.00 # construit de mine ca să conțină corecția ≥4 %
+ENTRY_I = 17 # candela de intrare (verde, precedată de BIM)
+TP_I = 37 # ultima candelă: TP1 atins
+BIM_I = ENTRY_I - 1 # BIM (bară de inversare bearish) înaintea candelei verzi
+
+# ── structura seriei: 12 sus → 6 corecție → intrare → 20 spre TP ───────────
+closes = [
+ # ~12 candele de urcare spre swing high ~123.00
+ 115.80, 116.50, 117.20, 117.90, 117.60, 118.30,
+ 119.00, 119.70, 120.40, 121.00, 121.70, 122.70,
+ # ~6 candele de corecție, de la 123.00 în zona de intrare
+ 122.00, 121.10, 120.10, 119.00, 117.00,
+ 117.43, # candela de intrare (verde)
+ # ~20 candele de urcare de la 117.43 spre 129.17
+ 118.00, 118.55, 119.05, 119.50, 119.95,
+ 120.40, 120.85, 121.25, 121.65, 122.10,
+ 122.50, 122.95, 123.40, 123.85, 124.30,
+ 125.00, 126.00, 127.10, 128.40, 129.17,
+]
+assert len(closes) == 38, len(closes)
+
+wicks = [(0.25, 0.30)] * len(closes)
+wicks[0] = (0.35, 0.35)
+wicks[11] = (0.30, 0.25) # high = 122.70 + 0.30 = 123.00 (swing high exact)
+wicks[BIM_I] = (0.15, 0.45)
+wicks[ENTRY_I] = (0.20, 0.40)
+wicks[TP_I] = (0.00, 0.30) # high = 129.17 exact, ca TP să fie max(high)
+
+vols = [42, 40, 44, 47, 39, 45, 48, 52, 50, 55, 58, 62,
+ 57, 61, 66, 72, 84,
+ 70,
+ 58, 55, 53, 57, 54, 56, 59, 55, 57, 60, 58, 61,
+ 63, 60, 64, 66, 68, 72, 76, 88]
+
+cds = from_closes(closes, wicks=wicks, vols=vols, start=115.30)
+
+# ── assert-urile din specificație ───────────────────────────────────────────
+entry = cds[ENTRY_I].c
+tp = cds[TP_I].c
+swing_high = max(c.h for c in cds[:ENTRY_I])
+
+assert abs(entry - 117.43) < 0.05, entry
+assert abs(tp - 129.17) < 0.05, tp
+assert abs(tp / entry - 1.10) < 0.005, tp / entry
+assert (swing_high - entry) / swing_high >= 0.04, (swing_high, entry)
+assert abs(entry - cds[ENTRY_I].c) < 0.05
+assert abs(max(c.h for c in cds[-10:]) - tp) < 0.05, max(c.h for c in cds[-10:])
+
+# structura care susține afirmațiile din text
+assert abs(swing_high - SWING_HIGH) < 0.05, swing_high
+assert cds[ENTRY_I].c > cds[ENTRY_I].o, "candela de intrare trebuie să fie verde"
+assert cds[BIM_I].c < cds[BIM_I].o, "BIM-ul dinaintea intrării trebuie să fie bearish"
+assert any(c.vol is not None for c in cds), "lipsește panoul de volum"
+
+n_entry_to_tp = TP_I - ENTRY_I + 1
+assert 18 <= n_entry_to_tp <= 23, n_entry_to_tp
+
+TP_LABEL = "TP1 atins ~1 an mai târziu (data exactă neclară în transcriere — ASR)"
+assert "~1 an" in TP_LABEL
+
+levels = [
+ Level(ENTRY, "intrare 117.43", "#188a3e", label_at="left"),
+ Level(TP, "TP1 129.17 (+10 %)", "#1a5fb4"),
+]
+
+marks = [
+ Mark(11, f"swing high ~{SWING_HIGH:.2f}", "#c00", "above"),
+ Mark(BIM_I, "BIM", "#777", "above"),
+ Mark(ENTRY_I, "27.01.2005", "#188a3e", "below"),
+ Mark(TP_I, "TP1", "#1a5fb4", "below"),
+]
+
+# nu există nicio linie de stop-loss în desen
+assert not any("stop" in t.lower() for t in
+ [lv.text for lv in levels] + [mk.text for mk in marks])
+assert len(levels) == 2, "doar intrare și TP; strategic nu există stop standard"
+
+# etichetele lungi merg ca linii de text sub grafic, ca să nu se bată cu
+# candelele din dreapta-sus (unde urcarea spre TP nu lasă loc liber)
+cds[TP_I - 1].note = "după TP: capital + profit → cash până la următorul trigger valid"
+cds[TP_I].note = TP_LABEL
+
+ch = Chart(
+ candles=cds,
+ levels=levels,
+ marks=marks,
+ mas=[MA(20, "#b06000", dash="4 3")],
+ scale=9.5, width=660, gap=15, body_w=9, left=40, vol_h=70,
+ vol_label="volum", title="SPY — weekly: intrare 117.43 (27.01.2005) → TP +10 % = 129.17",
+ caption="Swing high ~123.00 construit de mine; cifrele de intrare/TP sunt din material.",
+)
+
+svg = ch.render()
+assert svg.count("\n\n") == 0, "linie goală în SVG — rupe markdown-ul"
+OUT.write_text(svg)
+
+print(f"serie {len(cds)} candele | entry {entry:.2f} @ {ENTRY_I} | "
+ f"tp {tp:.2f} @ {TP_I} | swing high {swing_high:.2f} "
+ f"(corecție {(swing_high - entry) / swing_high * 100:.2f} %) | "
+ f"entry→TP {n_entry_to_tp} candele (~1 an)")
+print(f"scris {OUT}")
diff --git a/gen/strategii/atmi_exemplu.svg b/gen/strategii/atmi_exemplu.svg
new file mode 100644
index 0000000..56917a0
--- /dev/null
+++ b/gen/strategii/atmi_exemplu.svg
@@ -0,0 +1,131 @@
+
\ No newline at end of file
diff --git a/gen/strategii/atmi_orizont.py b/gen/strategii/atmi_orizont.py
new file mode 100644
index 0000000..d6d06b2
--- /dev/null
+++ b/gen/strategii/atmi_orizont.py
@@ -0,0 +1,230 @@
+#!/usr/bin/env python3
+"""Orizontul de ținere al ATMI — două descrieri incompatibile, pe aceeași axă.
+
+Pentru `strategii/03_atmi.md`, marcajul ``.
+Nu e grafic de preț, e o linie de timp comparativă: SVG scris de mână, doar
+stdlib — nu se folosește svg_candles / pchart.
+
+Ce arată (toate cifrele / citatele vin din spec, nu se inventează prețuri):
+- bara de sus, albastră: „ATMI clasică — fișierul principal", orizont luni
+ până la ~1 an, fără stop-loss, TP1 atins „aproximativ un an mai târziu";
+- bara de jos, portocalie: „ATMI ca adaptare pentru prop — Q&A 11.06.2025",
+ orizont 2 săptămâni;
+- cutia de anotare: pe prop, limita zilnică se calculează și pe pozițiile
+ deschise, iar la atingere firma închide automat; și nota că descrierile nu
+ se rezolvă aici, de clarificat cu autorul.
+
+Barele sunt pe o axă comună 0 → 1 an, ca diferența să fie vizibilă; bara de
+2 săptămâni e scurtă exact pentru că atât ține, nu din greșeală de scară.
+
+Rulare: python3 gen/strategii/atmi_orizont.py -> gen/strategii/atmi_orizont.svg
+"""
+from pathlib import Path
+from xml.sax.saxutils import escape
+
+OUT = Path(__file__).with_suffix(".svg")
+
+W, H = 700, 670
+BLACK, GRAY, BLUE, ORANGE, WHITE = "#000000", "#777777", "#1a5fb4", "#b06000", "#ffffff"
+
+AX_X0, AX_X1 = 60.0, 650.0
+AX_W = AX_X1 - AX_X0 # 590 px = 1 an
+AX_Y = 112.0
+TICKS = [0, 3, 6, 9, 12] # luni
+TICK_LABELS = {0: "0", 3: "3 luni", 6: "6 luni", 9: "9 luni", 12: "~1 an"}
+
+WEEKS_YEAR = 52.0
+LEN_PROP = AX_W * 2 / WEEKS_YEAR # ~22.7 px = 2 săptămâni
+
+BAR_TOP_Y = 172.0
+BAR_PROP_Y = 298.0
+BAR_H = 42.0
+
+CARD_X, CARD_Y, CARD_W, CARD_H = 160.0, 292.0, 500.0, 112.0
+BOX_X, BOX_Y, BOX_W, BOX_H = 40.0, 440.0, 620.0, 150.0
+
+TEXTS = [] # (x, y, text, size, weight, anchor) pentru verificare
+
+
+def txt(x, y, s, size=13, weight="400", fill=BLACK, anchor="middle"):
+ TEXTS.append((x, y, s, size, weight, anchor))
+ return (
+ f'{escape(s)}'
+ )
+
+
+def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None):
+ d = f' stroke-dasharray="{dash}"' if dash else ""
+ return (f'')
+
+
+def tick_x(months):
+ return AX_X0 + AX_W * months / 12.0
+
+
+def build():
+ p = [
+ f'')
+ return "\n".join(p) + "\n"
+
+
+# ---------------------------------------------------------------- verificare
+def est_w(s, size, weight):
+ factor = 0.62 if weight == "700" else 0.56
+ return len(s) * size * factor
+
+
+def verify(svg):
+ # 1. geometria barelor, față de axa comună
+ assert abs(AX_W - (AX_X1 - AX_X0)) < 1e-9
+ assert abs(LEN_PROP - AX_W * 2 / WEEKS_YEAR) < 1e-9
+ assert LEN_PROP < AX_W / 20, "bara de 2 săptămâni ar trebui să fie vizibil scurtă"
+ assert AX_X0 + LEN_PROP < CARD_X, "bara prop intră în card"
+ assert BAR_TOP_Y + BAR_H < BAR_PROP_Y
+ assert CARD_X + CARD_W <= AX_X1 + 10
+
+ # 2. toate etichetele în panză
+ for x, y, s, size, weight, anchor in TEXTS:
+ ew = est_w(s, size, weight)
+ left = x - ew if anchor == "end" else (x - ew / 2 if anchor == "middle" else x)
+ right = left + ew
+ assert -2 <= left and right <= W + 2, f"text în afara panzei: {s!r}"
+ assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y})"
+
+ # 3. etichete care nu se suprapun
+ for i in range(len(TEXTS)):
+ xi, yi, si, szi, wi, ai = TEXTS[i]
+ we = est_w(si, szi, wi)
+ li = xi - we if ai == "end" else (xi - we / 2 if ai == "middle" else xi)
+ ri = li + we
+ for j in range(i + 1, len(TEXTS)):
+ xj, yj, sj, szj, wj, aj = TEXTS[j]
+ wej = est_w(sj, szj, wj)
+ lj = xj - wej if aj == "end" else (xj - wej / 2 if aj == "middle" else xj)
+ rj = lj + wej
+ if abs(yi - yj) < 13 and li < rj - 4 and lj < ri - 4:
+ raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}")
+
+ # 4. textul cardului încape în card
+ for x, y, s, size, weight, anchor in TEXTS:
+ if CARD_Y <= y <= CARD_Y + CARD_H and CARD_X <= x <= CARD_X + CARD_W:
+ ew = est_w(s, size, weight)
+ assert ew <= CARD_W - 24, f"eticheta nu încape în card: {s!r} ({ew:.0f})"
+
+ # 5. cutia de anotare: textul nu iese
+ for x, y, s, size, weight, anchor in TEXTS:
+ if BOX_Y <= y <= BOX_Y + BOX_H and x >= BOX_X:
+ ew = est_w(s, size, weight)
+ assert BOX_X + 20 + ew <= BOX_X + BOX_W - 12, f"text iese din cutie: {s!r}"
+
+ # 6. citatele și sursele din spec sunt prezente
+ for cheie in ("fără stop-loss, poziția ținută luni/ani",
+ "aproximativ un an mai târziu",
+ "suport quarterly",
+ "2 săptămâni în poziție",
+ "3 transacții să califici propul",
+ "limita zilnică se calculează",
+ "închide automat",
+ "nu se rezolvă aici",
+ "03_strategia_2_atmi.md:26-29",
+ "09_QA/26_2025-06-11.md:225-227",
+ "09_QA/26_2025-06-11.md:216-222"):
+ assert cheie in svg, f"lipsește din desen: {cheie!r}"
+
+ # 7. fără cifre de preț inventate
+ for interzis in ("$", "USD", "SL ", "intrare "):
+ assert interzis not in svg, f"nu ar trebui să apară preț: {interzis!r}"
+ return True
+
+
+SVG = build()
+assert verify(SVG), "verificare eșuată"
+OUT.write_text(SVG, encoding="utf-8")
+print(f"scris {OUT} ({W}x{H}), verificare OK: {len(TEXTS)} etichete, "
+ f"bara prop {LEN_PROP:.1f}px din {AX_W:g}px")
diff --git a/gen/strategii/atmi_orizont.svg b/gen/strategii/atmi_orizont.svg
new file mode 100644
index 0000000..1f6c0af
--- /dev/null
+++ b/gen/strategii/atmi_orizont.svg
@@ -0,0 +1,47 @@
+
diff --git a/gen/strategii/buy_doji_ansamblu.py b/gen/strategii/buy_doji_ansamblu.py
new file mode 100644
index 0000000..3ab5937
--- /dev/null
+++ b/gen/strategii/buy_doji_ansamblu.py
@@ -0,0 +1,103 @@
+"""Buy Doji — diagrama de ansamblu (daily, exemplu construit, fără ticker).
+
+Citește specificația `gen/strategii/buy_doji_ansamblu.spec.txt`. Seria de
+32 de candele e construită din lista de close-uri dată, cu open-ul fiecărei
+bare = close-ul precedentului; wick-urile sunt fixate ca nivelurile
+(doji 50.10/49.20, intrare 50.40, SL 49.20, TP1 51.60, trail 52.60, ieșire
+sub trail la bara 28) să iasă exact din serie, nu doar din etichetă.
+"""
+import sys; sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, MA, from_closes
+
+# --- seria: 32 de close-uri, exact ca în specificație -----------------------
+closes = [
+ 60.20, 59.20, 59.60, 58.40, 57.30, 57.70, 56.50, 55.40, 55.80, 54.70, # 0-9
+ 53.50, 52.30, 51.00, 49.65, # 10-13 downtrend
+ 49.60, # 14 doji
+ 49.90, # 15 +1 (nu confirmă)
+ 50.40, # 16 +2 confirmare => intrare
+ 50.90, 51.20, 51.50, 51.80, 52.10, 52.40, 52.20, 52.70, 52.95, 53.20, # 17-26
+ 52.80, 52.30, 52.00, 51.80, 51.90, # 27-31
+]
+assert len(closes) == 32
+
+# wick-uri: (up, down) implicite (0.25, 0.30) + excepțiile din specificație
+wicks = [(0.25, 0.30)] * 32
+wicks[14] = (0.45, 0.40) # doji: high 50.10, low 49.20
+wicks[15] = (0.15, 0.15) # high 50.05, low 49.45
+wicks[16] = (0.15, 0.15) # high 50.55, low 49.75
+for k in range(24, 28): # barele 24-27
+ wicks[k] = (0.25, 0.20)
+
+# volum relativ ales de generator (materialul nu dă volum pe exemplu);
+# peste medie pe doji (14) și pe confirmare (16), apoi normal
+vols = [42, 44, 40, 46, 43, 41, 45, 42, 44, 47,
+ 43, 45, 48, 52, 78, 46, 88, 55,
+ 50, 48, 52, 47, 45, 49, 46, 44, 48, 51,
+ 54, 47, 45, 46]
+assert len(vols) == 32
+media_vol = sum(vols) / len(vols)
+assert vols[14] > media_vol and vols[16] > media_vol, "volum peste medie pe doji/confirmare"
+
+cds = from_closes(closes, wicks=wicks, vols=vols, start=60.20)
+
+# adnotările pe bara +1 și pe confirmare, ca linii sub grafic (fără suprapunere)
+cds[15].color = "#777" # bara de așteptare: neutră, nu confirmă
+cds[15].note = "bara 15: close 49.90 sub high-ul dojiului 50.10 — NU confirmă"
+cds[16].note = "bara 16: close 50.40 peste 50.10 — confirmare (intrare)"
+
+doji, wait, conf = cds[14], cds[15], cds[16]
+entry, sl, tp1, trail = 50.40, 49.20, 51.60, 52.60
+
+# --- cele 9 assert-uri cerute de specificație ------------------------------
+# 1. SL-ul e exact low-ul dojiului, cel mai jos din fereastra de 4 candele
+assert abs(min(cd.l for cd in cds[13:17]) - 49.20) < 1e-9, "min low[13:17] != 49.20"
+# 2. intrarea e close-ul barei 16
+assert abs(conf.c - entry) < 1e-9, f"close[16]={conf.c} != 50.40"
+# 3. confirmarea închide peste high-ul dojiului
+assert abs(doji.h - 50.10) < 1e-9 and conf.c > doji.h, "close[16] nu e peste high[14]"
+# 4. bara +1 NU confirmă
+assert wait.c <= doji.h, "bara 15 ar confirma, fereastra n-ar avea sens"
+# 5. 1:1 exact
+assert abs(abs(entry - sl) - abs(tp1 - entry)) < 1e-9 and abs((entry - sl) - 1.20) < 1e-9, \
+ "risc != recompensă (nu e 1:1 exact)"
+# 6. primul high >= TP1 după intrare e bara 19
+primul_tp = next(i for i in range(17, len(cds)) if cds[i].h >= tp1)
+assert primul_tp == 19, f"TP1 51.60 atins întâi la bara {primul_tp}, nu 19"
+# 7. trailing 52.60 = low[27]; ieșire la bara 28 sub el; ieșirea e pe profit
+assert abs(cds[27].l - trail) < 1e-9, f"low[27]={cds[27].l} != 52.60"
+assert abs(cds[28].c - 52.30) < 1e-9 and cds[28].c < trail, "close[28] nu e sub trail"
+assert trail > entry, "trail sub intrare, ieșirea n-ar fi pe profit"
+# 8. doji: corp <= 10% din amplitudine
+assert abs(doji.o - doji.c) <= 0.10 * (doji.h - doji.l), "bara 14 nu e doji"
+# 9. downtrend vizibil
+assert cds[13].c < cds[0].c - 10, "downtrend prea mic"
+
+print(f"doji {doji.h:.2f}/{doji.l:.2f} intrare {conf.c:.2f} "
+ f"SL {sl:.2f} TP1 {tp1:.2f} trail {trail:.2f} ieșire bara 28 @ {cds[28].c:.2f}")
+
+# --- desenul ----------------------------------------------------------------
+ch = Chart(
+ candles=cds,
+ levels=[
+ Level(sl, "SL 49.20 (low 4 candele)", "#c00", label_at="left"),
+ Level(entry, "intrare 50.40", "#188a3e", label_at="left"),
+ Level(tp1, "TP1 51.60 = 1R", "#1a5fb4", label_at="left"),
+ Level(trail, "trail 52.60", "#b06000"),
+ ],
+ marks=[
+ Mark(14, "doji 50.10 / 49.20", "#b06000", "below"),
+ Mark(28, "ieșire pe trail ~52.60", "#c00", "above", dy=-6),
+ ],
+ mas=[MA(20, "#777", dash="4 3", text="MA(20)")],
+ scale=24.0, gap=15, body_w=9, left=44, width=640, vol_h=70,
+ vol_label="volum", title="Buy Doji — daily (exemplu construit, fără ticker)",
+ caption="Intrare la close-ul confirmării, SL la low-ul ultimelor 4 candele, "
+ "TP1 1:1, apoi trailing stop.",
+)
+
+svg = ch.render()
+assert "
\ No newline at end of file
diff --git a/gen/strategii/buy_doji_aplus.py b/gen/strategii/buy_doji_aplus.py
new file mode 100644
index 0000000..4f83c7e
--- /dev/null
+++ b/gen/strategii/buy_doji_aplus.py
@@ -0,0 +1,247 @@
+"""Buy Doji — A+ vs. mediocru (două panouri, exemple construite).
+
+Citește `gen/strategii/buy_doji_aplus.spec.txt` (marcajul ``).
+Același pattern (trend → doji → confirmare), context opus:
+
+- stânga (A+): downtrend 52 → 42.60, doji cu low 41.95 chiar pe suportul
+ 42.00 (0.12 % sub nivel), confirmare close 43.60 pe volum peste media
+ ultimelor 10 bare — acumulare;
+- dreapta (NU): uptrend 70 → 76.80, gap-up la open 78.20, doji lipit de
+ rezistența 78.50, confirmare close 79.30 pe volum sub medie („varză"), apoi
+ prețul cade înapoi sub 78.00.
+
+Nivelurile 42.00 / 78.50 și cifrele de preț sunt construite de mine ca să
+susțină etichetele și assert-urile din spec (materialul nu dă prețuri pentru
+acest contrast). Două `Chart`-uri randate separat, lipite într-un SVG wrapper:
+coordonatele fiecărui panou rămân corecte prin construcție.
+
+Rulează: python3 gen/strategii/buy_doji_aplus.py
+Scrie: gen/strategii/buy_doji_aplus.svg
+"""
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Candle, Chart, Level, Mark, MA, from_closes # noqa: E402
+
+OUT = Path(__file__).resolve().parent / "buy_doji_aplus.svg"
+
+GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
+
+DOJI_A, CONF_A = 10, 11 # indicii în panoul A+
+DOJI_N, CONF_N = 14, 15 # indicii în panoul NU
+
+# ============================================================ PANOUL A+ (24 bare)
+# 10 bare de downtrend de la 52.00 spre 42.60, apoi doji, apoi confirmare.
+CLOSES_A = [
+ 51.60, 50.80, 50.00, 49.20, 48.40, 47.60, 46.80, 45.50, 44.20, 42.60, # 0-9 downtrend
+ 42.60, # 10 doji (open = close)
+ 43.60, # 11 confirmare
+ 44.10, 44.50, 44.20, 44.80, 45.20, 45.00, 45.50, 45.90, 46.20, 46.50, 46.80, 47.10, # 12-23 acumulare
+]
+assert len(CLOSES_A) == 24, len(CLOSES_A)
+
+WICKS_A = [(0.30, 0.30)] * 24
+WICKS_A[DOJI_A] = (0.30, 0.65) # high 42.90, low 41.95 (pe suportul 42.00)
+WICKS_A[CONF_A] = (0.25, 0.30) # high 43.85
+
+VOLS_A = [56, 53, 51, 49, 47, 45, 44, 42, 41, 39, # 0-9 vânzare în scădere
+ 60, # 10 doji: exces de vânzare
+ 86, # 11 confirmare: volum mare
+ 72, 75, 70, 78, 82, 80, 85, 88, 92, 95, 98, 100] # 12-23 acumulare
+
+cds_a = from_closes(CLOSES_A, wicks=WICKS_A, vols=VOLS_A, start=52.00)
+doji_a, conf_a = cds_a[DOJI_A], cds_a[CONF_A]
+
+# ============================================================ PANOUL NU (24 bare)
+# uptrend 70 → 76.80 (14 bare), gap-up la open 78.20 + doji, confirmare 79.30
+# pe volum slab, apoi cădere înapoi sub 78.00. Lista e manuală pentru că gap-ul
+# (open > high-ul barei precedente) nu iese din `from_closes`.
+BARE_NU = [
+ # o, h, l, c, vol
+ (70.00, 70.70, 69.80, 70.50, 48), (70.50, 71.20, 70.30, 71.00, 50),
+ (71.00, 71.75, 70.85, 71.55, 49), (71.55, 72.30, 71.40, 72.10, 52),
+ (72.10, 72.80, 71.95, 72.60, 51), (72.60, 73.30, 72.45, 73.10, 54),
+ (73.10, 73.80, 72.95, 73.60, 53), (73.60, 74.25, 73.45, 74.05, 56),
+ (74.05, 74.70, 73.90, 74.50, 55), (74.50, 75.15, 74.35, 74.95, 58),
+ (74.95, 75.60, 74.80, 75.40, 57), (75.40, 76.00, 75.25, 75.80, 60),
+ (75.80, 76.40, 75.65, 76.20, 59), (76.20, 76.80, 76.05, 76.80, 62), # 13: high 76.80
+ (78.20, 78.50, 78.10, 78.20, 58), # 14: GAP-UP + doji (o = c = 78.20)
+ (78.20, 79.50, 78.10, 79.30, 30), # 15: confirmare, volum slab
+ (79.30, 79.40, 78.40, 78.60, 55), (78.60, 78.70, 77.80, 78.00, 60),
+ (78.00, 78.10, 77.20, 77.40, 64), (77.40, 77.55, 76.80, 77.00, 62),
+ (77.00, 77.15, 76.40, 76.60, 66), (76.60, 76.75, 76.10, 76.30, 63),
+ (76.30, 76.40, 75.80, 76.00, 65), (76.00, 76.15, 75.50, 75.70, 61),
+]
+assert len(BARE_NU) == 24, len(BARE_NU)
+cds_n = [Candle(o, h, l, c, vol=v) for (o, h, l, c, v) in BARE_NU]
+doji_n, conf_n = cds_n[DOJI_N], cds_n[CONF_N]
+
+# ============================================================ assert-urile din spec
+# 1. A+: low-ul dojiului e la ≤ 0,5 % de 42.00 (41.95 e la 0.12 %)
+dist_a = abs(doji_a.l - 42.00) / 42.00
+assert dist_a <= 0.005, f"low doji A+ {doji_a.l} la {dist_a * 100:.2f} % de 42.00"
+assert abs(doji_a.l - 41.95) < 1e-9, f"low doji A+ = {doji_a.l}, aștept 41.95"
+# 2. A+: volumul pe bara de confirmare > media volumelor pe 10 bare
+media_a = sum(VOLS_A[CONF_A - 10:CONF_A]) / 10
+assert VOLS_A[CONF_A] > media_a, f"vol confirmare {VOLS_A[CONF_A]} <= media {media_a}"
+# 3. NU: doji-ul apare după un gap-up (open doji > high bara precedentă)
+assert cds_n[DOJI_N].o > cds_n[DOJI_N - 1].h, "doji NU nu e după gap-up"
+assert abs(cds_n[DOJI_N].o - 78.20) < 1e-9 and abs(cds_n[DOJI_N - 1].h - 76.80) < 1e-9
+# 4. NU: |high doji - 78.50| / 78.50 <= 0.005
+dist_n = abs(doji_n.h - 78.50) / 78.50
+assert dist_n <= 0.005, f"high doji NU {doji_n.h} la {dist_n * 100:.2f} % de 78.50"
+# 5. NU: volumul pe bara de confirmare < media pe 10 bare
+media_n = sum(b[4] for b in BARE_NU[CONF_N - 10:CONF_N]) / 10
+assert conf_n.vol < media_n, f"vol confirmare NU {conf_n.vol} >= media {media_n}"
+# 6. ambele: close-ul de confirmare > high-ul dojiului (tehnic valid)
+assert conf_a.c > doji_a.h, f"A+ close {conf_a.c} <= high doji {doji_a.h}"
+assert conf_n.c > doji_n.h, f"NU close {conf_n.c} <= high doji {doji_n.h}"
+assert abs(conf_a.c - 43.60) < 1e-9 and abs(conf_n.c - 79.30) < 1e-9
+# 7. ambele doji: abs(o-c) <= 0.10*(h-l)
+for tag, d in (("A+", doji_a), ("NU", doji_n)):
+ assert abs(d.o - d.c) <= 0.10 * (d.h - d.l), f"bara doji {tag} nu e doji"
+# suplimentare: structura promisă de spec
+assert min(c.l for c in cds_a[:10]) > doji_a.l, "low-ul dojiului A+ nu e minimul"
+assert cds_a[9].c < cds_a[0].c - 8, "downtrend A+ prea mic"
+assert cds_n[CONF_N].c > 78.50 and cds_n[-1].c < 78.00, "NU nu cade sub 78.00 la final"
+assert cds_n[13].c < 78.00 < cds_n[DOJI_N].o, "NU: uptrendul nu se oprește sub 78.00"
+
+print(f"A+ doji {doji_a.h:.2f}/{doji_a.l:.2f} conf {conf_a.c:.2f} "
+ f"vol {VOLS_A[CONF_A]} > medie {media_a:.1f}")
+print(f"NU doji {doji_n.h:.2f}/{doji_n.l:.2f} conf {conf_n.c:.2f} "
+ f"vol {conf_n.vol} < medie {media_n:.1f}")
+
+# ============================================================ desenele celor două panouri
+PW = 560 # lățimea fiecărui panou
+ch_a = Chart(
+ candles=cds_a,
+ levels=[Level(42.00, "suport 42.00", GREEN, label_at="left")],
+ marks=[Mark(DOJI_A, "doji la nivel (low 41.95)", ORANGE, "below"),
+ Mark(CONF_A, "acumulare", GREEN, "above", dy=-6)],
+ mas=[], scale=22.0, gap=15, body_w=9, left=44, width=PW, vol_h=60,
+ vol_label="volum", title="A+ — doji la suport, volum în acumulare",
+ caption="acumulare: volum peste media ultimelor 10 bare.",
+)
+ch_n = Chart(
+ candles=cds_n,
+ levels=[Level(78.50, "rezistență 78.50", RED, label_at="right")],
+ marks=[Mark(DOJI_N, "doji 78.50 / 78.10", ORANGE, "below", dy=93)],
+ mas=[], scale=22.0, gap=15, body_w=9, left=44, width=PW, vol_h=60,
+ vol_label="volum", title="Mediocru — gap-up în rezistență, volum varză",
+ caption="gap-up + doji în rezistență, confirmare pe volum slab.",
+)
+
+# ============================================================ wrapper: două panouri alăturate
+def inner(svg: str) -> str:
+ """Conținutul dintre tag-urile ")]
+
+
+def vb_height(svg: str) -> float:
+ m = re.search(r'viewBox="0 0 [\d.]+ ([\d.]+)"', svg)
+ assert m, "nu găsesc viewBox în panoul randat"
+ return float(m.group(1))
+
+
+PANEL_GAP, PAD = 24, 8
+TITLE_H, FOOTER_H = 30, 22
+svg_a, svg_n = ch_a.render(), ch_n.render()
+
+# „gap-up, volum slab" stă în colțul liber din stânga-sus, cu linie indicatoare
+# spre golul dintre bara 13 (high 76.80) și dojiul barei 14 (open 78.20).
+GAP_X = (ch_n._cx(DOJI_N - 1) + ch_n._cx(DOJI_N)) / 2
+GAP_Y = ch_n._y(77.50)
+gap_ann = (
+ f'gap-up, volum slab'
+ f''
+ f''
+)
+svg_n = svg_n.replace("", gap_ann + "")
+
+HA, HN = vb_height(svg_a), vb_height(svg_n)
+assert svg_a.count("\n\n") == 0 and svg_n.count("\n\n") == 0, "linie goală în panou"
+
+TOTAL_W = PAD + PW + PANEL_GAP + PW + PAD
+TOTAL_H = PAD + TITLE_H + max(HA, HN) + FOOTER_H + PAD
+OFF_A = (PAD, PAD + TITLE_H)
+OFF_N = (PAD + PW + PANEL_GAP, PAD + TITLE_H)
+
+FOOTER = ("Surse: 13_strategia_3_byh.md:188-190 (doji la nivel); "
+ "01_buy_doji_si_strategia_mici.md:227-235 (context invalid).")
+
+wrapper = (
+ f''
+)
+
+# ============================================================ verificare pe SVG-ul final
+def texts(svg: str):
+ """Toate (x, y, text-anchor, text) din elementele ale unui fragment."""
+ out = []
+ for m in re.finditer(r']*)>(.*?)', svg):
+ x, y, attrs, t = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4)
+ if t.startswith("<"):
+ continue
+ am = re.search(r'text-anchor="(\w+)"', attrs)
+ out.append((x, y, am.group(1) if am else "start", t))
+ return out
+
+
+def span(x: float, anchor: str, t: str) -> tuple[float, float]:
+ """Intervalul orizontal aproximativ al unui text, față de ancora lui."""
+ w = len(t) * 12 * 0.56
+ left = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x)
+ return left, left + w
+
+
+assert wrapper.count("\n\n") == 0, "linie goală în SVG"
+
+# fiecare text (după translatarea panoului) e în întregime în interiorul pânzei
+all_txt = []
+for frag, (ox, oy) in ((inner(svg_a), OFF_A), (inner(svg_n), OFF_N)):
+ for x, y, anchor, t in texts(frag):
+ gx, gy = x + ox, y + oy
+ gl, gr = gx + span(0, anchor, t)[0], gx + span(0, anchor, t)[1]
+ assert ox <= gl and gr <= ox + PW, f"text iese din panou: {t!r} ({gl:.0f}..{gr:.0f})"
+ assert -1 <= gl and gr <= TOTAL_W + 1, f"text ieșit pe orizontală: {t!r} ({gl:.0f}..{gr:.0f})"
+ assert 0 <= gy <= TOTAL_H, f"text ieșit pe verticală: {t!r} ({gy:.0f})"
+ all_txt.append((gx, gy, anchor, t))
+# textele din wrapper (titlu + footer), în coordonate globale
+for x, y, anchor, t in ((TOTAL_W / 2, 20.0, "middle",
+ "Setup A+ vs. setup mediocru (două panouri, exemple construite)"),
+ (TOTAL_W / 2, TOTAL_H - PAD, "middle", FOOTER)):
+ gl, gr = span(x, anchor, t)
+ assert -1 <= gl and gr <= TOTAL_W + 1, f"text din wrapper iese din pânză: {t!r}"
+ assert 0 <= y <= TOTAL_H, f"text din wrapper în afara înălțimii: {t!r}"
+ all_txt.append((x, y, anchor, t))
+assert len(all_txt) >= 8, "prea puține etichete"
+
+# nu se suprapun două texte pe aceeași poziție sau pe rândul apropiat
+for i in range(len(all_txt)):
+ xi, yi, ai, si = all_txt[i]
+ li, ri = (xi + span(0, ai, si)[0], xi + span(0, ai, si)[1])
+ for j in range(i + 1, len(all_txt)):
+ xj, yj, aj, sj = all_txt[j]
+ lj, rj = xj + span(0, aj, sj)[0], xj + span(0, aj, sj)[1]
+ if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
+ raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}")
+
+# etichetele cerute de spec există în desen
+for cheie in ("suport 42.00", "doji la nivel", "acumulare",
+ "rezistență 78.50", "gap-up, volum slab",
+ "A+ — doji la suport, volum în acumulare",
+ "Mediocru — gap-up în rezistență, volum varză"):
+ assert cheie in wrapper, f"lipsește din desen: {cheie!r}"
+
+OUT.write_text(wrapper, encoding="utf-8")
+print(f"scris {OUT.name}: {TOTAL_W}x{TOTAL_H:.0f}, "
+ f"{len(all_txt)} etichete, verificare OK")
diff --git a/gen/strategii/buy_doji_aplus.svg b/gen/strategii/buy_doji_aplus.svg
new file mode 100644
index 0000000..12b7ffb
--- /dev/null
+++ b/gen/strategii/buy_doji_aplus.svg
@@ -0,0 +1,162 @@
+
\ No newline at end of file
diff --git a/gen/strategii/buy_doji_fereastra.py b/gen/strategii/buy_doji_fereastra.py
new file mode 100644
index 0000000..135b6bd
--- /dev/null
+++ b/gen/strategii/buy_doji_fereastra.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python3
+"""Buy Doji — fereastra de 3 candele: DA (valid) vs. NU (expirat).
+
+Citește `gen/strategii/buy_doji_fereastra.spec.txt` și scrie
+`gen/strategii/buy_doji_fereastra.svg`. Două panouri mici, unul sub altul,
+cu aceeași serie până la doji: sus confirmarea vine la bara +2 (în fereastră),
+jos niciuna din +1..+3 nu confirmă, iar a 4-a închide sub low-ul dojiului.
+
+Toate cifrele din desen vin din specificație: doji h 50.10 / l 49.20,
+closes-urile DA/NU, pragul 50.10, low-ul 49.20. Seriile sunt construite
+(open = close-ul barei anterioare, mai puțin open-ul primei bare, start-ul),
+ca nivelurile afirmate să iasă exact din serie.
+
+Rulare: python3 gen/strategii/buy_doji_fereastra.py
+"""
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, from_closes # noqa: E402
+
+OUT = Path(__file__).with_suffix(".svg")
+W = 460
+VGAP = 34
+SCALE = 45.0 # px / unitate, aceeași pe ambele panouri (comparație)
+
+# --- seriile din specificație ----------------------------------------------
+CLOSES_DA = [52.20, 51.40, 50.60, 49.65,
+ 49.60, # doji, h 50.10 / l 49.20
+ 49.90, # +1 — sub 50.10
+ 50.40, # +2 — peste 50.10 => confirmă
+ 50.80]
+CLOSES_NU = [52.20, 51.40, 50.60, 49.65,
+ 49.60, # doji, h 50.10 / l 49.20
+ 49.95, # +1
+ 50.05, # +2
+ 49.85, # +3
+ 49.15] # a 4-a — sub low-ul dojiului
+assert len(CLOSES_DA) == 8 and len(CLOSES_NU) == 9
+assert CLOSES_DA[:5] == CLOSES_NU[:5]
+
+HIGH_DOJI, LOW_DOJI = 50.10, 49.20
+
+# wick-uri: (up, down). Doji-ul (index 4) primește mecioni ca să iasă exact
+# h=50.10 / l=49.20; restul au mecioni mici, egali.
+wicks_da = [(0.15, 0.15)] * len(CLOSES_DA)
+wicks_nu = [(0.15, 0.15)] * len(CLOSES_NU)
+wicks_da[4] = wicks_nu[4] = (0.45, 0.40)
+
+START = CLOSES_DA[0] + 0.15 # prima bară roșie, open > close
+cds_da = from_closes(CLOSES_DA, wicks=wicks_da, start=START)
+cds_nu = from_closes(CLOSES_NU, wicks=wicks_nu, start=START)
+
+doji_da, doji_nu = cds_da[4], cds_nu[4]
+entry = cds_da[6].c # intrare la close-ul confirmării
+
+# --- assert-urile din specificație -----------------------------------------
+# doji-ul iese exact la nivelurile afirmate
+assert abs(doji_da.h - HIGH_DOJI) < 1e-9 and abs(doji_da.l - LOW_DOJI) < 1e-9, \
+ f"doji DA {doji_da.h}/{doji_da.l} != {HIGH_DOJI}/{LOW_DOJI}"
+assert abs(doji_nu.h - HIGH_DOJI) < 1e-9 and abs(doji_nu.l - LOW_DOJI) < 1e-9, \
+ f"doji NU {doji_nu.h}/{doji_nu.l} != {HIGH_DOJI}/{LOW_DOJI}"
+
+# 1. DA: primul close peste 50.10 DUPĂ doji e bara +2 (index 6), în fereastră;
+# bara +1 nu confirmă
+primul_da = next(i for i in range(5, len(cds_da)) if cds_da[i].c > HIGH_DOJI)
+assert primul_da == 6, f"prima confirmare la index {primul_da}, nu 6 (+2)"
+assert primul_da - 4 <= 3, "confirmarea nu e în fereastra de 3 candele"
+assert abs(cds_da[5].c - 49.90) < 1e-9 and cds_da[5].c <= HIGH_DOJI, \
+ "bara +1 nu trebuie să confirme"
+assert abs(cds_da[6].c - entry) < 1e-9 and abs(entry - 50.40) < 1e-9, \
+ "intrarea nu e close-ul de 50.40 al barei +2"
+
+# 2. NU: niciun close din +1..+3 nu depășește 50.10; a 4-a închide sub 49.20
+assert all(cds_nu[i].c <= HIGH_DOJI for i in (5, 6, 7)), \
+ "un close din +1..+3 ar confirma, fereastra n-ar fi expirat"
+assert cds_nu[8].c < LOW_DOJI, f"close[+4]={cds_nu[8].c} nu e sub {LOW_DOJI}"
+
+# 3. ambele doji: corp <= 10% din amplitudine
+for d in (doji_da, doji_nu):
+ assert abs(d.o - d.c) <= 0.10 * (d.h - d.l), "bara 4 nu e doji"
+
+# 4. aceeași serie până la doji (aceleași 5 bare)
+ser_da = [(round(c.o, 9), round(c.h, 9), round(c.l, 9), round(c.c, 9)) for c in cds_da[:5]]
+ser_nu = [(round(c.o, 9), round(c.h, 9), round(c.l, 9), round(c.c, 9)) for c in cds_nu[:5]]
+assert ser_da == ser_nu, "panourile nu au aceeași serie până la doji"
+
+# --- desen: două Chart-uri, lipite vertical într-un singur SVG -------------
+def _panel(ch: Chart):
+ svg = ch.render()
+ m = re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', svg)
+ body = svg[svg.index(">") + 1:svg.rindex("")]
+ return body, float(m.group(2))
+
+
+# DA — confirmarea vine la +2
+cds_da[5].note = "+1: close 49.90 — sub 50.10, nu confirmă"
+cds_da[6].note = "+2: close 50.40 — peste 50.10, confirmă (intrare la close)"
+ch_da = Chart(
+ candles=cds_da,
+ levels=[Level(HIGH_DOJI, "high doji 50.10", "#b06000", label_at="left"),
+ Level(LOW_DOJI, "low doji 49.20", "#c00", label_at="left")],
+ marks=[Mark(4, "doji", "#b06000", "above"),
+ Mark(6, f"confirmare +2 — intrare {entry:.2f}", "#188a3e", "above")],
+ scale=SCALE, gap=40, body_w=13, left=44, width=W,
+ title="DA — confirmare la +2, în fereastră",
+)
+
+# NU — nicio confirmare în 3 candele, apoi setup mort
+cds_nu[5].note = "+1: 49.95; +2: 50.05; +3: 49.85 — toate sub 50.10"
+cds_nu[8].note = "a 4-a: close 49.15 — sub low-ul dojiului 49.20, setup mort"
+ch_nu = Chart(
+ candles=cds_nu,
+ levels=[Level(HIGH_DOJI, "high doji 50.10", "#b06000", label_at="left"),
+ Level(LOW_DOJI, "low doji 49.20", "#c00", label_at="left")],
+ marks=[Mark(4, "doji", "#b06000", "above"),
+ Mark(8, "a 4-a", "#c00", "above")],
+ scale=SCALE, gap=40, body_w=13, left=44, width=W,
+ title="NU — nicio confirmare în 3 candele",
+)
+
+body_da, h_da = _panel(ch_da)
+body_nu, h_nu = _panel(ch_nu)
+
+cap1 = "Serii construite: open = close-ul barei anterioare."
+cap2 = "Doji: h 50.10 / l 49.20. Fereastra = maximum 3 candele după doji."
+cap_top = h_da + VGAP + h_nu + 20
+H = cap_top + 22
+
+parts = [
+ f'",
+]
+svg = "\n ".join(parts)
+assert "\n\n" not in svg, "linie goală în SVG"
+assert "<" not in cap1 and "<" not in cap2
+
+# --- verificarea citirii: etichete în panză, fără suprapuneri ---------------
+TEXT = re.compile(r']*)>(.*?)', re.S)
+
+
+def _texts(body: str, y_off: float, tag: str):
+ out = []
+ for m in TEXT.finditer(body):
+ x, y = float(m.group(1)), float(m.group(2)) + y_off
+ attrs, s = m.group(3), m.group(4)
+ anchor = "start" # implicit în SVG
+ if 'text-anchor="end"' in attrs:
+ anchor = "end"
+ elif 'text-anchor="middle"' in attrs:
+ anchor = "middle"
+ bold = "bold" in attrs
+ w = len(s) * 12 * (0.62 if bold else 0.52)
+ left = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x)
+ out.append((left, left + w, y, s, tag))
+ return out
+
+
+labels = _texts(body_da, 0.0, "DA") + _texts(body_nu, h_da + VGAP, "NU")
+for left, right, y, s, tag in labels:
+ assert left >= -2 and right <= W + 2, f"[{tag}] text iese din panză: {s!r}"
+ assert 0 <= y <= H, f"[{tag}] text în afara înălțimii: {s!r} (y={y:.0f})"
+
+for i in range(len(labels)):
+ li, ri, yi, si, ti = labels[i]
+ for j in range(i + 1, len(labels)):
+ lj, rj, yj, sj, tj = labels[j]
+ if ti == tj and abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
+ raise AssertionError(f"[{ti}] texte suprapuse: {si!r} / {sj!r}")
+
+print(f"DA: doji {doji_da.h:.2f}/{doji_da.l:.2f} +1 {cds_da[5].c:.2f} "
+ f"+2 {entry:.2f} (confirmă) bare {len(cds_da)}")
+print(f"NU: +1 {cds_nu[5].c:.2f} +2 {cds_nu[6].c:.2f} +3 {cds_nu[7].c:.2f} "
+ f"a 4-a {cds_nu[8].c:.2f} (< {LOW_DOJI}) bare {len(cds_nu)}")
+
+# 5. distanța în pixeli 50.10 -> 50.40 e vizibilă, cu scară comună
+dist = ch_da._y(HIGH_DOJI) - ch_da._y(entry)
+assert ch_da.scale == ch_nu.scale == SCALE, "scara nu e comună între panouri"
+assert dist > 0 and dist > 10, f"distanța 50.10->50.40 = {dist:.1f}px, prea mică"
+print(f"scris {OUT} ({W}x{H:.0f}); {len(labels)} etichete verificate; "
+ f"50.10->50.40 = {dist:.1f}px")
+OUT.write_text(svg, encoding="utf-8")
diff --git a/gen/strategii/buy_doji_fereastra.svg b/gen/strategii/buy_doji_fereastra.svg
new file mode 100644
index 0000000..7ef8659
--- /dev/null
+++ b/gen/strategii/buy_doji_fereastra.svg
@@ -0,0 +1,60 @@
+
\ No newline at end of file
diff --git a/gen/strategii/buy_doji_labu.py b/gen/strategii/buy_doji_labu.py
new file mode 100644
index 0000000..c95cd06
--- /dev/null
+++ b/gen/strategii/buy_doji_labu.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+"""LABU — Buy Doji la pivotul monthly (Nivel 2.5).
+
+Citește `gen/strategii/buy_doji_labu.spec.txt` și scrie
+`gen/strategii/buy_doji_labu.svg`: 49 de candele daily, downtrend → triple
+bottom → doji la pivotul monthly 86.00 → confirmare (close 89.50 peste
+high-ul dojiului) → bull reversal → higher high, cu SL 81.45 (−9 %) și
+TP 116.35 (+30 %).
+
+Toate cifrele din desen vin din specificație: pivot 86.00, intrare 89.50,
+SL 81.45, TP 116.35, high/low-ul dojiului (86.30 / 84.20) și low-urile
+triple bottom-ului (82.40 / 81.45 / 81.45). Procentele 9 % și 30 % sunt ale
+cursului (reale); nivelurile de preț sunt construite de sumarizator —
+legenda o spune explicit.
+
+Rulare: python3 gen/strategii/buy_doji_labu.py
+"""
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, MA, Mark, from_closes # noqa: E402
+
+OUT = Path(__file__).with_suffix(".svg")
+
+# --- nivelurile din specificație -------------------------------------------
+PIVOT = 86.00 # pivot monthly (construit)
+ENTRY = 89.50 # intrare = close-ul confirmării (construit)
+SL = 81.45 # SL = −9 % față de intrare (construit; procentul e real)
+TP = 116.35 # TP = +30 % față de intrare (construit; procentul e real)
+DOJI_H, DOJI_L = 86.30, 84.20
+
+# --- seria de close-uri din specificație -----------------------------------
+closes = [
+ # downtrend 0-14
+ 108.0, 106.5, 105.0, 103.0, 101.5, 100.0, 98.0, 96.5, 95.0, 93.5,
+ 92.5, 91.0, 89.5, 87.5, 85.5,
+ # triple bottom 15 / 23 / 31
+ 82.80, 84.5, 86.5, 88.0, 88.50,
+ 87.0, 85.0, 83.0, 81.50,
+ 83.5, 85.5, 87.0, 87.80, 87.0, 85.0,
+ 83.0, 81.60, 85.00,
+ 84.90, # 33 = doji (o 85.00, c 84.90, h 86.30, l 84.20)
+ 89.50, # 34 = confirmare, close > 86.30 => intrare
+ # bull reversal / trend up 35-48
+ 91.0, 93.0, 92.0, 95.0, 97.0, 96.0, 99.0, 102.0, 101.0, 105.0, 108.0,
+ 112.0, 116.35, 118.0,
+]
+assert len(closes) == 49, f"seria are {len(closes)} bare, spec cere 49"
+
+# wick-uri: (up, down). Restul (0.4, 0.5); ajustate ca low-urile/high-urile
+# cerute de asserts să iasă exact (bar 15/23/31 = triple bottom; bar 16/24
+# nu au voie să coboare sub piciorul din stânga; bar 33 = doji).
+wicks = [(0.4, 0.5)] * len(closes)
+wicks[15] = (0.4, 0.40) # low 82.40
+wicks[16] = (0.4, 0.20) # low 82.60 > 82.40 (piciorul 1 rămâne minim local)
+wicks[23] = (0.4, 0.05) # low 81.45
+wicks[24] = (0.4, 0.00) # low 81.50 > 81.45 (piciorul 2 rămâne minim local)
+wicks[31] = (0.4, 0.15) # low 81.45
+wicks[32] = (0.4, 0.10) # low 81.50 (nu sub 81.45)
+wicks[33] = (1.30, 0.70) # doji: high 86.30, low 84.20
+
+# volume construite: vânzare peste medie la triple bottom și la confirmare.
+# Nu sunt numere din curs (nu sunt etichetate), doar panoul de volum cerut.
+vols = [
+ 55, 52, 48, 50, 45, 47, 44, 46, 42, 44,
+ 40, 42, 38, 41, 58,
+ 62, 50, 46, 44, 43,
+ 45, 44, 47, 66, 55,
+ 50, 47, 45, 44, 42,
+ 43, 45, 60, 72, 84,
+ 78, 70, 64, 60, 58,
+ 54, 52, 56, 60, 66,
+ 74, 82, 90, 98,
+]
+assert len(vols) == len(closes), f"volume {len(vols)} != bare {len(closes)}"
+
+cds = from_closes(closes, wicks=wicks, vols=vols, start=110.0)
+assert abs(cds[0].o - 110.0) < 1e-9 and cds[0].c < cds[0].o, "prima bară nu e roșie"
+
+# nota explicativă a dojiului (bloc stânga-jos, nu se suprapune cu candelele)
+cds[33].note = ("bara 33 = doji: o 85.00, c 84.90, h 86.30, l 84.20 — "
+ "corp mic exact pe pivotul monthly 86.00 (indecizie la suport)")
+
+# --- assert-urile din specificație (toate, ca assert reale) ----------------
+# 1. pivot 86.00 e straddled de doji
+assert cds[33].l <= PIVOT <= cds[33].h, "doji-ul nu straddlează 86.00"
+
+# 2. confirmarea: close[34] == 89.50 == intrare și > high[33]
+assert abs(cds[34].c - ENTRY) < 1e-9, f"close[34]={cds[34].c} != intrare 89.50"
+assert cds[34].c > cds[33].h, "confirmarea nu închide peste high-ul dojiului"
+
+# 3. SL = min(low[30:35]) (regula: extrema ultimelor 4 candele de la confirmare)
+min_30_35 = min(c.l for c in cds[30:35])
+assert abs(min_30_35 - SL) < 1e-9, f"min low[30:35]={min_30_35} != SL {SL}"
+
+# 4./5. procentele REALE ale cursului
+assert round((ENTRY - SL) / ENTRY * 100, 1) == 9.0, "stopul nu iese −9 %"
+assert round((TP - ENTRY) / ENTRY * 100, 1) == 30.0, "TP nu iese +30 %"
+
+# 6. triple bottom: exact trei minime locale sub 86.00, în zona 81-83,
+# al doilea și al treilea fără low nou semnificativ față de primul
+lows = [c.l for c in cds]
+bottom_idx = [i for i in range(1, len(cds) - 1)
+ if lows[i] < lows[i - 1] and lows[i] < lows[i + 1]
+ and lows[i] < PIVOT and 81.0 <= lows[i] <= 83.0]
+assert bottom_idx == [15, 23, 31], f"triple bottom la {bottom_idx}, nu [15, 23, 31]"
+b1, b2, b3 = lows[15], lows[23], lows[31]
+tol = 0.02 * b1 # „semnificativ" = > 2 % sub primul low
+assert b2 >= b1 - tol and b3 >= b1 - tol, \
+ f"al doilea/treilea picior coboară semnificativ sub primul: {b1}/{b2}/{b3}"
+assert abs(b1 - 82.40) < 1e-9 and abs(b2 - SL) < 1e-9 and abs(b3 - SL) < 1e-9, \
+ f"low-urile triple bottom: {b1}/{b2}/{b3}"
+
+# 7. TP atins: există i > 34 cu high[i] >= 116.35
+assert any(c.h >= TP for c in cds[35:]), "TP 116.35 nu e atins după intrare"
+
+# 8. doji: corp <= 10 % din amplitudine
+assert abs(cds[33].o - cds[33].c) <= 0.10 * (cds[33].h - cds[33].l), "bara 33 nu e doji"
+assert abs(cds[33].h - DOJI_H) < 1e-9 and abs(cds[33].l - DOJI_L) < 1e-9, \
+ "doji h/l nu ies 86.30/84.20"
+
+# în plus: confirmarea vine cu volum peste medie (cerut de spec)
+mean_vol = sum(vols) / len(vols)
+assert vols[34] > mean_vol, f"volumul confirmării {vols[34]} <= media {mean_vol:.0f}"
+
+entry = cds[34].c
+w = 820 # lățime mărită: 49 bare × gap 15 + etichetele din dreapta
+ # (spec sugera 760, dar ultima candelă ajunge la x=764)
+ch = Chart(
+ candles=cds,
+ levels=[Level(PIVOT, "pivot monthly 86.00 (construit)", "#777",
+ dash="4 3", label_at="left"),
+ Level(SL, "SL 81.45 (construit) = −9 % (procent real)", "#c00",
+ label_at="left"),
+ Level(ENTRY, "intrare 89.50 (construit)", "#188a3e"),
+ Level(TP, "TP 116.35 (construit) = +30 % (procent real)", "#1a5fb4")],
+ marks=[Mark(15, f"bottom 1 {lows[15]:.2f}", "#c00", "below"),
+ Mark(23, f"bottom 2 {lows[23]:.2f}", "#c00", "below"),
+ Mark(31, f"bottom 3 {lows[31]:.2f}", "#c00", "below"),
+ Mark(33, "doji", "#b06000", "above"),
+ Mark(34, "confirmare (intrare)", "#188a3e", "above", dy=-14),
+ Mark(48, "higher high — TP atins", "#188a3e", "above")],
+ mas=[MA(20, "#b06000", dash="4 3", text="SMA-20")],
+ scale=8.0, gap=15, body_w=9, left=44, width=w, vol_h=70,
+ vol_label="volum", title="LABU — daily",
+ caption=("Procentele (stop 9 %, +30 %) sunt ale cursului; nivelurile de "
+ "preț sunt construite de sumarizator (18_2025-09-23.md:713)."),
+)
+svg = ch.render()
+assert "\n\n" not in svg and "', re.S)
+H = float(re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', svg).group(2))
+labels = []
+for m in TEXT.finditer(svg):
+ x, y, attrs, s = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4)
+ anchor = "end" if 'text-anchor="end"' in attrs else \
+ ("middle" if 'text-anchor="middle"' in attrs else "start")
+ bold = "bold" in attrs
+ wpx = len(s) * 12 * (0.62 if bold else 0.52)
+ left = x - wpx if anchor == "end" else (x - wpx / 2 if anchor == "middle" else x)
+ labels.append((left, left + wpx, y, s))
+
+for left, right, y, s in labels:
+ assert left >= -2 and right <= w + 2, f"text iese din panză: {s!r} [{left:.0f},{right:.0f}]"
+ assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y:.0f})"
+
+for i in range(len(labels)):
+ li, ri, yi, si = labels[i]
+ for j in range(i + 1, len(labels)):
+ lj, rj, yj, sj = labels[j]
+ if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
+ raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}\n {labels[i]}\n {labels[j]}")
+
+# candelele sunt o serie continuă, lipită
+xs = [ch._cx(i) for i in range(len(cds))]
+assert xs[-1] + ch.body_w / 2 < w, f"ultima candelă iese din panză (x={xs[-1]})"
+assert all(xs[i + 1] - xs[i] == ch.gap for i in range(len(xs) - 1)), "serie cu goluri"
+for a, b in zip(cds, cds[1:]):
+ assert abs(a.c - b.o) < 1e-9, "open != close-ul barei anterioare"
+
+print(f"LABU: {len(cds)} bare, doji {cds[33].l:.2f}-{cds[33].h:.2f} peste pivot "
+ f"{PIVOT:.2f}, intrare {entry:.2f}, SL {SL:.2f} (−9 %), TP {TP:.2f} (+30 %)")
+print(f"triple bottom: {b1:.2f} / {b2:.2f} / {b3:.2f} "
+ f"volum confirmare {vols[34]} > medie {mean_vol:.0f}")
+print(f"scris {OUT} ({w}x{H:.0f}); {len(labels)} etichete verificate, fără suprapuneri")
+OUT.write_text(svg, encoding="utf-8")
diff --git a/gen/strategii/buy_doji_labu.svg b/gen/strategii/buy_doji_labu.svg
new file mode 100644
index 0000000..a262517
--- /dev/null
+++ b/gen/strategii/buy_doji_labu.svg
@@ -0,0 +1,170 @@
+
\ No newline at end of file
diff --git a/gen/strategii/buy_doji_sell.py b/gen/strategii/buy_doji_sell.py
new file mode 100644
index 0000000..067690a
--- /dev/null
+++ b/gen/strategii/buy_doji_sell.py
@@ -0,0 +1,114 @@
+"""Sell Doji — oglinda short a ansamblului Buy Doji (daily, exemplu construit).
+
+Citește specificația `gen/strategii/buy_doji_sell.spec.txt`. Seria de 31 de
+candele e construită din lista de close-uri dată, cu open-ul fiecărei bare =
+close-ul precedentului; wick-urile sunt fixate ca nivelurile (doji 99.20 /
+100.10, intrare short 99.00, SL 100.10, TP 97.90, trail 96.45, ieșire la 96.60)
+să iasă exact din serie, nu doar din etichetă.
+"""
+import sys; sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, from_closes
+
+# --- seria: 31 de close-uri, exact ca în specificație -----------------------
+closes = [
+ 93.00, 94.10, 93.70, 95.00, 96.10, 95.80, 97.00, 98.10, 97.80, 98.90, # 0-9
+ 99.80, 100.20, 99.90, 99.65, # 10-13 uptrend
+ 99.60, # 14 doji
+ 99.40, # 15 +1 (nu confirmă)
+ 99.00, # 16 +2 confirmare => intrare short
+ 98.50, 98.30, 97.80, 97.40, 97.00, 96.70, 96.40, 96.10, 95.80, # 17-25
+ 96.20, 96.60, 96.40, 96.10, 96.30, # 26-30
+]
+assert len(closes) == 31
+
+# wick-uri: (up, down) implicite (0.30, 0.25) + excepțiile din specificație
+wicks = [(0.30, 0.25)] * 31
+# ajustat: cu wu=0.30, high[13] = 99.90+0.30 = 100.20 > SL; îl coborâm la
+# 100.10 ca max(high[13:17]) == 100.10 (high-ul ultimelor 4 candele = SL)
+wicks[13] = (0.20, 0.25)
+wicks[14] = (0.45, 0.40) # doji: high 100.10, low 99.20
+wicks[15] = (0.20, 0.15) # high 99.80, low 99.25 (peste 99.20 => nu confirmă)
+wicks[16] = (0.20, 0.15) # high 99.60, low 98.85
+wicks[25] = (0.35, 0.20) # high 96.45 (= trail), low 95.60 (minimul corecției)
+# ajustat: cu wd=0.25, low[26] = 95.80-0.25 = 95.55 < 95.60, adică bara 26 ar
+# fi minimul, nu bara 25; wd=0.10 păstrează bara 25 ca minim (low 95.60)
+wicks[26] = (0.30, 0.10)
+
+# volum relativ ales de generator (materialul nu dă volum pe exemplu);
+# peste medie pe doji (14) și pe confirmare (16) — distribuție — apoi normal
+vols = [44, 42, 46, 40, 45, 48, 43, 47, 50, 45,
+ 52, 58, 49, 55, 90, 62, 84,
+ 56, 52, 50, 48, 46, 44, 42, 40, 45,
+ 50, 46, 44, 43, 45]
+assert len(vols) == 31
+media_vol = sum(vols) / len(vols)
+assert vols[14] > media_vol and vols[16] > media_vol, "volum peste medie pe doji/confirmare"
+
+cds = from_closes(closes, wicks=wicks, vols=vols, start=closes[0])
+
+# adnotările pe bara +1 și pe confirmare, ca linii sub grafic (fără suprapunere)
+cds[15].color = "#777" # bara de așteptare: neutră, nu confirmă
+cds[15].note = "bara 15: close 99.40 peste low-ul dojiului 99.20 — NU confirmă"
+cds[16].note = "bara 16: close 99.00 sub 99.20 — confirmare (intrare short)"
+
+doji, wait, conf = cds[14], cds[15], cds[16]
+entry, sl, tp = 99.00, 100.10, 97.90
+
+# --- cele 9 assert-uri cerute de specificație ------------------------------
+# 1. SL-ul e exact high-ul ultimelor 4 candele
+assert abs(max(cd.h for cd in cds[13:17]) - 100.10) < 1e-9, "max high[13:17] != 100.10"
+# 2. intrarea e close-ul barei 16
+assert abs(conf.c - entry) < 1e-9, f"close[16]={conf.c} != 99.00"
+# 3. confirmarea închide sub low-ul dojiului
+assert abs(doji.l - 99.20) < 1e-9 and conf.c < doji.l, "close[16] nu e sub low[14]"
+# 4. bara +1 NU confirmă
+assert wait.c >= doji.l, "bara 15 ar confirma, fereastra n-ar avea sens"
+# 5. 1:1 exact
+assert abs(abs(sl - entry) - abs(entry - tp)) < 1e-9 and abs((entry - tp) - 1.10) < 1e-9, \
+ "risc != recompensă (nu e 1:1 exact)"
+# 6. primul low <= TP după intrare e bara 19
+primul_tp = next(i for i in range(17, len(cds)) if cds[i].l <= tp)
+assert primul_tp == 19, f"TP 97.90 atins întâi la bara {primul_tp}, nu 19"
+assert abs(cds[18].l - 98.05) < 1e-9 and abs(cds[19].l - 97.55) < 1e-9, "low-urile 18/19 nu ies"
+# 7. trailing = high[25] = 96.45; primul close peste el e bara 27; ieșirea e pe profit
+trail = cds[25].h
+assert abs(trail - 96.45) < 1e-9, f"high[25]={trail} != 96.45"
+primul_exit = next(i for i in range(26, len(cds)) if cds[i].c > trail)
+assert primul_exit == 27 and abs(cds[27].c - 96.60) < 1e-9, "ieșirea nu e bara 27 @ 96.60"
+assert cds[27].c < entry, "ieșirea n-ar fi pe profit"
+# 8. doji: corp <= 10% din amplitudine
+assert abs(doji.o - doji.c) <= 0.10 * (doji.h - doji.l), "bara 14 nu e doji"
+# 9. uptrend vizibil
+assert cds[13].c > cds[0].c + 5, "uptrend prea mic"
+
+print(f"doji {doji.l:.2f}/{doji.h:.2f} intrare short {conf.c:.2f} "
+ f"SL {sl:.2f} TP {tp:.2f} trail {trail:.2f} ieșire bara 27 @ {cds[27].c:.2f}")
+
+# --- desenul ----------------------------------------------------------------
+ch = Chart(
+ candles=cds,
+ levels=[
+ # toate etichetele în dreapta: în stânga sus se suprapuneau cu mark-ul
+ # dojiului (SL la y=62, text până în ~x196; doji la y=58, din ~x193)
+ Level(sl, "SL 100.10 (high 4 candele)", "#c00"),
+ Level(entry, "intrare short 99.00", "#188a3e"),
+ Level(tp, "TP 97.90 = 1R", "#1a5fb4"),
+ Level(trail, "trail 96.45", "#b06000"),
+ ],
+ marks=[
+ Mark(14, "doji 99.20 / 100.10", "#b06000", "above"),
+ Mark(27, "ieșire pe trail 96.60", "#c00", "above", dy=-6),
+ ],
+ scale=30.0, gap=15, body_w=9, left=44, width=640, vol_h=70,
+ vol_label="volum", title="Sell Doji — daily (exemplu construit, fără ticker)",
+ # scurt: o linie de caption mai lungă de ~90 de caractere iese din viewBox
+ # (640px) și se taie la randare
+ caption="Intrare short la close sub low-ul dojiului, SL la high 4 candele, "
+ "TP 1:1, apoi trail.",
+)
+
+svg = ch.render()
+assert "
\ No newline at end of file
diff --git a/gen/strategii/hammer_mbt10_bh_ansamblu.py b/gen/strategii/hammer_mbt10_bh_ansamblu.py
new file mode 100644
index 0000000..2971154
--- /dev/null
+++ b/gen/strategii/hammer_mbt10_bh_ansamblu.py
@@ -0,0 +1,118 @@
+"""Buy hammer — diagrama de ansamblu (daily, ticker ilustrativ XYZ).
+
+Citește specificația `gen/strategii/hammer_mbt10_bh_ansamblu.spec.txt`.
+Seria de 34 de candele e construită din lista de close-uri dată, cu open-ul
+fiecărei bare = close-ul precedentului; wick-urile sunt fixate ca nivelurile
+(hammer H=C 63.30 / L 62.10, intrare 63.35, SL 62.05, TP 65.95) să iasă
+exact din serie, nu doar din etichetă.
+"""
+import re
+import sys; sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, from_closes
+
+# --- seria: 34 de close-uri, exact ca în specificație ------------------------
+closes = [
+ 72.00, 71.20, 71.50, 70.40, 69.60, 70.00, 68.90, 68.10, 68.40, 67.30, # 0-9
+ 66.50, 66.80, 65.70, 64.90, 65.20, 64.10, 63.40, 63.70, 63.05, # 10-18
+ 63.30, # 19 = hammer: o 63.05, c/h 63.30, l 62.10 (H = C, fitil sus 0)
+ 63.35, # 20 = confirmare: close peste 63.30 => intrare
+ 63.80, 64.20, 64.60, 64.40, 64.90, 65.20, 65.50, 65.80, 66.10, # 21-29
+ 66.00, 65.70, 65.40, 65.60, # 30-33
+]
+assert len(closes) == 34
+
+# wick-uri: (up, down) implicite (0.25, 0.30) + excepțiile din specificație
+wicks = [(0.25, 0.30)] * 34
+wicks[19] = (0.00, 0.95) # hammer: high 63.30 (= close), low 62.10
+# bara 20: wu=0.15 => high 63.50. Spec-ul dă wd=0.15 cu „low 63.20", dar
+# wd se măsoară de la min(o,c)=63.30, deci 0.15 dă 63.15; folosim tuplul
+# literal din specificație (0.15, 0.15), low-ul barei 20 nu e etichetat.
+wicks[20] = (0.15, 0.15)
+
+# volum relativ ales de generator (materialul nu dă volum pe exemplul construit):
+# normal pe downtrend, în creștere pe hammer (19) și pe confirmare (20), normal după
+vols = [50, 48, 47, 46, 45, 46, 44, 43, 45, 42,
+ 41, 43, 40, 39, 41, 38, 37, 39, 55,
+ 90, 95,
+ 58, 55, 53, 50, 52, 54, 56, 60, 64,
+ 58, 55, 52, 50]
+assert len(vols) == 34
+media_vol = sum(vols) / len(vols)
+assert vols[19] > media_vol and vols[20] > media_vol, "volum peste medie pe hammer/confirmare"
+
+cds = from_closes(closes, wicks=wicks, vols=vols, start=72.00)
+
+hammer, conf = cds[19], cds[20]
+# confirmarea o punem ca notă sub grafic: un Mark deasupra barei 20 s-ar
+# suprapune cu fitilul lung al hammer-ului (bara 19)
+conf.note = "bara 20: confirmare — close 63.35 peste high-ul hammer-ului 63.30 (intrare)"
+intrare, sl, tp = 63.35, 62.05, 65.95
+
+# --- cele 8 assert-uri cerute de specificație -------------------------------
+# 1. hammer: H = C = 63.30, L = 62.10
+assert abs(cds[19].c - 63.30) < 1e-9 and abs(cds[19].h - 63.30) < 1e-9, "H != C la hammer"
+assert abs(cds[19].l - 62.10) < 1e-9, f"low[19]={cds[19].l} != 62.10"
+# 2. open[19] = close[18]; corp 0.25; fitil jos 0.95 >= 2*0.25
+assert abs(cds[19].o - cds[18].c) < 1e-9 and abs(cds[19].o - 63.05) < 1e-9, "open[19] != close[18]"
+corp = hammer.c - hammer.o
+fitil_jos = hammer.o - hammer.l
+assert abs(corp - 0.25) < 1e-9, f"corp={corp} != 0.25"
+assert abs(fitil_jos - 0.95) < 1e-9, f"fitil jos={fitil_jos} != 0.95"
+assert fitil_jos >= 2 * corp, "fitilul nu e cel puțin dublul corpului"
+# 3. confirmarea închide peste high-ul hammer-ului
+assert abs(conf.c - intrare) < 1e-9, f"close[20]={conf.c} != 63.35"
+assert conf.c > hammer.h and abs(hammer.h - 63.30) < 1e-9, "close[20] nu e peste high[19]"
+# 4. SL = low-ul hammer-ului - 0.05
+assert abs(sl - (cds[19].l - 0.05)) < 1e-9, "SL != low[19] - 0.05"
+# 5. risc 1.30, TP 2.60 = 2R exact
+assert abs(abs(intrare - sl) - 1.30) < 1e-9, "risc != 1.30"
+assert abs(abs(tp - intrare) - 2.60) < 1e-9, "TP != 2R exact"
+# 6. primul high >= TP e bara 28 (65.80 + wick 0.25 = 66.05)
+primul_tp = next(i for i in range(21, len(cds)) if cds[i].h >= tp)
+assert primul_tp == 28, f"TP 65.95 atins întâi la bara {primul_tp}, nu 28"
+# 7. downtrend vizibil
+assert cds[18].c < cds[0].c - 8, "downtrend prea mic"
+# 8. ordinea nivelurilor
+assert sl < intrare < tp, "SL / intrare / TP nu sunt în ordine"
+
+print(f"hammer H/C {hammer.h:.2f} L {hammer.l:.2f} intrare {conf.c:.2f} "
+ f"SL {sl:.2f} TP {tp:.2f} = {abs(tp-intrare)/abs(intrare-sl):.1f}R "
+ f"TP atins la bara {primul_tp} (high {cds[primul_tp].h:.2f})")
+
+# --- desenul ----------------------------------------------------------------
+ch = Chart(
+ candles=cds,
+ levels=[
+ Level(sl, "SL 62.05 — sub low-ul hammer-ului", "#c00", label_at="left"),
+ Level(intrare, "intrare 63.35", "#188a3e"),
+ Level(tp, "TP 65.95 = 2R", "#1a5fb4"),
+ ],
+ marks=[
+ Mark(19, "hammer 63.30 / 62.10", "#b06000", "below"),
+ ],
+ scale=32.0, gap=15, body_w=9, left=44, width=720, vol_h=70,
+ vol_label="volum", title="Buy hammer — daily (ticker ilustrativ XYZ, din lecția de pattern)",
+ caption="", # legenda e pusă manual, pe două rânduri, ca să nu iasă din pânză
+)
+
+svg = ch.render()
+assert "")
+assert "\n\n" not in svg, "linie goală în SVG după legendă"
+
+with open("/workspace/atm-curs-dl/gen/strategii/hammer_mbt10_bh_ansamblu.svg", "w") as f:
+ f.write(svg)
+print("scris gen/strategii/hammer_mbt10_bh_ansamblu.svg")
diff --git a/gen/strategii/hammer_mbt10_bh_ansamblu.svg b/gen/strategii/hammer_mbt10_bh_ansamblu.svg
new file mode 100644
index 0000000..8f1a7d0
--- /dev/null
+++ b/gen/strategii/hammer_mbt10_bh_ansamblu.svg
@@ -0,0 +1,115 @@
+
\ No newline at end of file
diff --git a/gen/strategii/hammer_mbt10_bh_aplus.py b/gen/strategii/hammer_mbt10_bh_aplus.py
new file mode 100644
index 0000000..fac805c
--- /dev/null
+++ b/gen/strategii/hammer_mbt10_bh_aplus.py
@@ -0,0 +1,279 @@
+"""Buy hammer — A+ vs. mediocru (două panouri, exemple construite).
+
+Citește `gen/strategii/hammer_mbt10_bh_aplus.spec.txt`
+(marcajul ``, Partea A §2.2).
+
+Același pattern ca formă (downtrend → hammer → confirmare), context opus:
+
+- stânga (A+): downtrend de 12 candele de la 50.00 spre 42.80, hammer-ul cu
+ low 41.90 chiar pe suportul monthly 42.00 (0,24 % sub nivel; corp 0.10,
+ fitil jos 0.90), confirmarea închide 42.95 peste high-ul 42.90, pe volum
+ peste media ultimelor 10 bare — „acumulare";
+- dreapta (NU): fără downtrend real (range lateral 44–46), hammer-ul în
+ mijlocul intervalului (o=45.00, c=h=45.10, l=44.10), departe de orice
+ nivel (suportul construit 42.50 e la 3,6 % sub low), confirmarea închide
+ tot 45.15 peste 45.10, dar pe volum slab — context invalid.
+
+Nivelurile 42.00 / 42.50 și cifrele de preț sunt construite (materialul nu
+dă prețuri pentru acest contrast); regulile (locația la nivel, downtrend,
+volum) sunt din curs. Două `Chart`-uri randate separat, lipite într-un SVG
+wrapper: coordonatele fiecărui panou rămân corecte prin construcție.
+
+Notă de serie: specificația listează `close[11]=42.40` (A+) și `44.60` (NU),
+dar descrie hammer-ul cu `o=42.80 / c=42.90`, respectiv `o=45.00 / c=45.10`,
+iar assert 9 cere `close == high`. Cum open-ul fiecărei bare = close-ul
+precedentei (STYLE.md), am ridicat close[11] la 42.80 / 45.00 și close[12]
+la 42.90 / 45.10, ca seria să susțină exact OHLC-ul din etichetă. Restul
+close-urilor sunt cele din specificație.
+
+Rulează: python3 gen/strategii/hammer_mbt10_bh_aplus.py
+Scrie: gen/strategii/hammer_mbt10_bh_aplus.svg
+"""
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, from_closes # noqa: E402
+
+OUT = Path(__file__).resolve().parent / "hammer_mbt10_bh_aplus.svg"
+
+GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
+
+HAM_A, CONF_A = 12, 13 # indicii în panoul A+
+HAM_N, CONF_N = 12, 13 # indicii în panoul NU
+SUPORT_A = 42.00 # suport monthly (A+)
+SUPORT_N = 42.50 # cel mai apropiat nivel construit (NU)
+
+# ============================================================ PANOUL A+ (20 bare)
+# 12 bare de downtrend 50.00 -> 42.80, hammer la 42.00, confirmare, apoi urcare.
+CLOSES_A = [
+ 50.00, 49.10, 49.30, 48.20, 47.30, 47.60, 46.40, 45.50, 44.60, 43.70,
+ 43.00, 42.80,
+ 42.90, # 12 = hammer: o=42.80, c=h=42.90, l=41.90 (H=C, fitil jos 0.90)
+ 42.95, # 13 = confirmare, close > 42.90 => intrare
+ 43.40, 43.80, 44.20, 44.00, 44.50, 44.80,
+]
+assert len(CLOSES_A) == 20, len(CLOSES_A)
+
+WICKS_A = [(0.25, 0.30)] * 20
+WICKS_A[HAM_A] = (0.00, 0.90) # high 42.90, low 41.90 (pe suportul 42.00)
+WICKS_A[CONF_A] = (0.20, 0.20) # confirmarea: o=42.90, c=42.95
+
+# Volum ales de generator (materialul nu dă volum): scade pe downtrend, climax
+# pe hammer, spike pe confirmare („acumulare"), moderat după.
+VOLS_A = [54, 52, 51, 49, 48, 47, 46, 45, 44, 43,
+ 42, 45,
+ 64, # 12 = hammer: exces de vânzare
+ 90, # 13 = confirmare: volum peste medie
+ 74, 70, 66, 62, 60, 58]
+assert len(VOLS_A) == 20
+
+cds_a = from_closes(CLOSES_A, wicks=WICKS_A, vols=VOLS_A, start=50.60)
+ham_a, conf_a = cds_a[HAM_A], cds_a[CONF_A]
+
+# ============================================================ PANOUL NU (20 bare)
+# range lateral 44-46, fără downtrend; hammer în mijloc, confirmare pe volum slab.
+CLOSES_N = [
+ 46.00, 45.30, 45.80, 45.10, 45.60, 44.90, 45.40, 44.80, 45.20, 44.70,
+ 45.10, 45.00,
+ 45.10, # 12 = hammer: o=45.00, c=h=45.10, l=44.10 (H=C, fitil jos 0.90)
+ 45.15, # 13 = confirmare, close > 45.10
+ 44.90, 44.70, 44.40, 44.20, 44.00, 43.80,
+]
+assert len(CLOSES_N) == 20, len(CLOSES_N)
+
+WICKS_N = [(0.25, 0.30)] * 20
+WICKS_N[HAM_N] = (0.00, 0.90) # high 45.10, low 44.10
+WICKS_N[CONF_N] = (0.20, 0.20)
+
+# Volum ales de generator: lateral, constant, confirmarea pe volum slab.
+VOLS_N = [52, 50, 53, 49, 51, 48, 52, 50, 49, 51,
+ 50, 49,
+ 56, # 12
+ 31, # 13 = confirmare: volum slab
+ 52, 50, 48, 51, 49, 47]
+assert len(VOLS_N) == 20
+
+cds_n = from_closes(CLOSES_N, wicks=WICKS_N, vols=VOLS_N, start=45.60)
+ham_n, conf_n = cds_n[HAM_N], cds_n[CONF_N]
+
+# ============================================================ assert-urile din spec
+# 1. A+: low[12] la <= 0,5 % de 42.00 (41.90 e la 0,24 %)
+dist_a = abs(ham_a.l - SUPORT_A) / SUPORT_A
+assert dist_a <= 0.005, f"low hammer A+ {ham_a.l} la {dist_a * 100:.2f} % de 42.00"
+# low-ul hammer-ului chiar trece sub nivel (fitilul sparge suportul)
+assert ham_a.l < SUPORT_A <= ham_a.h, "hammer-ul A+ nu are fitilul pe/la nivel"
+# 2. A+: close[13] > high[12] (tehnic valid)
+assert conf_a.c > ham_a.h, f"A+ close {conf_a.c} <= high hammer {ham_a.h}"
+assert abs(conf_a.c - 42.95) < 1e-9
+# 3. A+: volumul pe bara 13 > media volumelor pe 10 bare
+media_a = sum(VOLS_A[CONF_A - 10:CONF_A]) / 10
+assert VOLS_A[CONF_A] > media_a, f"vol confirmare A+ {VOLS_A[CONF_A]} <= media {media_a}"
+# 4. A+: close[12] < close[0] - 5 (downtrend vizibil)
+assert cds_a[HAM_A].c < cds_a[0].c - 5, "downtrendul A+ nu e vizibil"
+# 5. NU: (low[12] - 42.50) / 42.50 > 0.02 (hammer-ul NU e departe de nivel)
+dist_n = (ham_n.l - SUPORT_N) / SUPORT_N
+assert dist_n > 0.02, f"low hammer NU {ham_n.l} prea aproape de 42.50 ({dist_n:.1%})"
+# eticheta „la -3,6 %" din spec: (44.10 - 42.50) / 44.10
+pct_n = (ham_n.l - SUPORT_N) / ham_n.l * 100
+assert round(pct_n, 1) == 3.6, f"procentul NU e {pct_n:.2f} %, nu 3,6 %"
+# 6. NU: close[13] > high[12] (tehnic valid)
+assert conf_n.c > ham_n.h, f"NU close {conf_n.c} <= high hammer {ham_n.h}"
+assert abs(conf_n.c - 45.15) < 1e-9
+# 7. NU: volumul pe bara 13 < media volumelor pe 10 bare
+media_n = sum(VOLS_N[CONF_N - 10:CONF_N]) / 10
+assert VOLS_N[CONF_N] < media_n, f"vol confirmare NU {VOLS_N[CONF_N]} >= media {media_n}"
+# 8. NU: |close[12] - close[0]| <= 1.5 (fără downtrend real, range lateral)
+assert abs(cds_n[HAM_N].c - cds_n[0].c) <= 1.5, "NU nu e range lateral"
+# 9. ambele hammer: fitil jos >= 2x corp si close == high (fitil de sus inexistent)
+for tag, o, l, c, h in (
+ ("A+", ham_a.o, ham_a.l, ham_a.c, ham_a.h),
+ ("NU", ham_n.o, ham_n.l, ham_n.c, ham_n.h)):
+ assert (o - l) >= 2 * (c - o), f"fitilul hammer-ului {tag} < 2x corpul"
+ assert abs(c - h) < 1e-9, f"hammer-ul {tag} are fitil de sus"
+# OHLC-ul descris în spec (eticheta e adevărul)
+assert abs(ham_a.o - 42.80) < 1e-9 and abs(ham_a.c - 42.90) < 1e-9 and abs(ham_a.l - 41.90) < 1e-9
+assert abs(ham_n.o - 45.00) < 1e-9 and abs(ham_n.c - 45.10) < 1e-9 and abs(ham_n.l - 44.10) < 1e-9
+# context: hammer-ul A+ e la baza downtrendului, NU nu are downtrend
+assert min(c.l for c in cds_a[:HAM_A]) > ham_a.l, "low-ul hammer-ului A+ nu e minimul"
+assert abs(cds_a[-1].c - 44.80) < 1e-9 and cds_a[-1].c > conf_a.c, "A+ nu urcă după intrare"
+
+print(f"A+ hammer {ham_a.o:.2f}/{ham_a.c:.2f}/{ham_a.h:.2f}/{ham_a.l:.2f} "
+ f"conf {conf_a.c:.2f} > high {ham_a.h:.2f} vol {VOLS_A[CONF_A]} > medie {media_a:.1f}")
+print(f"NU hammer {ham_n.o:.2f}/{ham_n.c:.2f}/{ham_n.h:.2f}/{ham_n.l:.2f} "
+ f"conf {conf_n.c:.2f} > high {ham_n.h:.2f} vol {VOLS_N[CONF_N]} < medie {media_n:.1f}")
+print(f"NU low {ham_n.l:.2f} vs suport {SUPORT_N:.2f} = -{pct_n:.1f} %")
+
+# ============================================================ desenele celor două panouri
+PW = 430 # lățimea fiecărui panou
+SCALE = 26.0 # aceeași scară în ambele panouri (comparație cinstită)
+ch_a = Chart(
+ candles=cds_a,
+ levels=[Level(SUPORT_A, "suport monthly 42.00", GREEN, label_at="left")],
+ marks=[Mark(HAM_A, "hammer la nivel (low 41.90)", ORANGE, "below", dy=0),
+ Mark(CONF_A, "acumulare", GREEN, "above", dy=-26)],
+ mas=[], scale=SCALE, gap=15, body_w=9, left=44, width=PW, vol_h=60,
+ vol_label="volum",
+ title="A+ — hammer la suport monthly, volum în acumulare",
+ caption="bara 13: 42.95 > high 42.90, pe volum peste medie.",
+)
+ch_n = Chart(
+ candles=cds_n,
+ levels=[Level(SUPORT_N, "fără nivel apropiat (suport 42.50 la −3,6 %)", GRAY,
+ label_at="left")],
+ marks=[Mark(HAM_N, "hammer în mijloc de range", ORANGE, "below", dy=0),
+ Mark(CONF_N, "volum slab", RED, "above", dy=0)],
+ mas=[], scale=SCALE, gap=15, body_w=9, left=44, width=PW, vol_h=60,
+ vol_label="volum",
+ title="Mediocru — hammer în mijloc de range, fără nivel",
+ caption="bara 13: 45.15 > 45.10, dar pe volum slab.",
+)
+
+# ============================================================ wrapper: două panouri alăturate
+def inner(svg: str) -> str:
+ """Conținutul dintre tag-urile ")]
+
+
+def vb_height(svg: str) -> float:
+ m = re.search(r'viewBox="0 0 [\d.]+ ([\d.]+)"', svg)
+ assert m, "nu găsesc viewBox în panoul randat"
+ return float(m.group(1))
+
+
+PANEL_GAP, PAD = 24, 8
+TITLE_H, FOOTER_H = 30, 38
+svg_a, svg_n = ch_a.render(), ch_n.render()
+
+HA, HN = vb_height(svg_a), vb_height(svg_n)
+assert svg_a.count("\n\n") == 0 and svg_n.count("\n\n") == 0, "linie goală în panou"
+
+TOTAL_W = PAD + PW + PANEL_GAP + PW + PAD
+TOTAL_H = PAD + TITLE_H + max(HA, HN) + FOOTER_H + PAD
+OFF_A = (PAD, PAD + TITLE_H)
+OFF_N = (PAD + PW + PANEL_GAP, PAD + TITLE_H)
+
+TOP_TITLE = "Buy hammer A+ vs. mediocru (două panouri, exemple construite)"
+FOOTER1 = ("Reguli din curs: 26_2025-06-11.md:701-703 (locația la nivel), "
+ "05_patternuri_candele.md:77-80, 05_volum.md:2169.")
+FOOTER2 = ("Nivelurile 42.00 / 42.50 și cifrele de preț sunt construite "
+ "(materialul nu dă prețuri pentru acest contrast).")
+FOOTER = FOOTER1 + " " + FOOTER2
+
+wrapper = (
+ f''
+)
+
+# ============================================================ verificare pe SVG-ul final
+def texts(svg: str):
+ """Toate (x, y, text-anchor, text) din elementele ale unui fragment."""
+ out = []
+ for m in re.finditer(r']*)>(.*?)', svg):
+ x, y, attrs, t = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4)
+ if t.startswith("<"):
+ continue
+ am = re.search(r'text-anchor="(\w+)"', attrs)
+ out.append((x, y, am.group(1) if am else "start", t))
+ return out
+
+
+def span(x: float, anchor: str, t: str) -> tuple[float, float]:
+ """Intervalul orizontal aproximativ al unui text, față de ancora lui."""
+ w = len(t) * 12 * 0.56
+ left = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x)
+ return left, left + w
+
+
+assert wrapper.count("\n\n") == 0, "linie goală în SVG"
+
+# fiecare text (după translatarea panoului) e în întregime în interiorul pânzei
+all_txt = []
+for frag, (ox, oy) in ((inner(svg_a), OFF_A), (inner(svg_n), OFF_N)):
+ for x, y, anchor, t in texts(frag):
+ gx, gy = x + ox, y + oy
+ gl, gr = gx + span(0, anchor, t)[0], gx + span(0, anchor, t)[1]
+ assert ox <= gl and gr <= ox + PW, f"text iese din panou: {t!r} ({gl:.0f}..{gr:.0f})"
+ assert -1 <= gl and gr <= TOTAL_W + 1, f"text ieșit pe orizontală: {t!r} ({gl:.0f}..{gr:.0f})"
+ assert 0 <= gy <= TOTAL_H, f"text ieșit pe verticală: {t!r} ({gy:.0f})"
+ all_txt.append((gx, gy, anchor, t))
+# textele din wrapper (titlu + footer), în coordonate globale
+for x, y, anchor, t in ((TOTAL_W / 2, 20.0, "middle", TOP_TITLE),
+ (TOTAL_W / 2, TOTAL_H - PAD - 14, "middle", FOOTER1),
+ (TOTAL_W / 2, TOTAL_H - PAD, "middle", FOOTER2)):
+ gl, gr = span(x, anchor, t)
+ assert -1 <= gl and gr <= TOTAL_W + 1, f"text din wrapper iese din pânză: {t!r}"
+ assert 0 <= y <= TOTAL_H, f"text din wrapper în afara înălțimii: {t!r}"
+ all_txt.append((x, y, anchor, t))
+assert len(all_txt) >= 10, "prea puține etichete"
+
+# nu se suprapun două texte pe aceeași poziție sau pe rândul apropiat
+for i in range(len(all_txt)):
+ xi, yi, ai, si = all_txt[i]
+ li, ri = (xi + span(0, ai, si)[0], xi + span(0, ai, si)[1])
+ for j in range(i + 1, len(all_txt)):
+ xj, yj, aj, sj = all_txt[j]
+ lj, rj = xj + span(0, aj, sj)[0], xj + span(0, aj, sj)[1]
+ if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
+ raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}")
+
+# etichetele cerute de spec există în desen
+for cheie in ("suport monthly 42.00", "hammer la nivel", "acumulare",
+ "fără nivel apropiat (suport 42.50 la −3,6 %)", "volum slab",
+ "A+ — hammer la suport monthly, volum în acumulare",
+ "Mediocru — hammer în mijloc de range, fără nivel"):
+ assert cheie in wrapper, f"lipsește din desen: {cheie!r}"
+
+OUT.write_text(wrapper, encoding="utf-8")
+print(f"scris {OUT.name}: {TOTAL_W}x{TOTAL_H:.0f}, "
+ f"{len(all_txt)} etichete, verificare OK")
diff --git a/gen/strategii/hammer_mbt10_bh_aplus.svg b/gen/strategii/hammer_mbt10_bh_aplus.svg
new file mode 100644
index 0000000..eae82ef
--- /dev/null
+++ b/gen/strategii/hammer_mbt10_bh_aplus.svg
@@ -0,0 +1,139 @@
+
\ No newline at end of file
diff --git a/gen/strategii/hammer_mbt10_bh_labu.py b/gen/strategii/hammer_mbt10_bh_labu.py
new file mode 100644
index 0000000..f2432c8
--- /dev/null
+++ b/gen/strategii/hammer_mbt10_bh_labu.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python3
+"""LABU — buy hammer la suportul monthly, +20 % în jumătate de lună (Nivel 2.5).
+
+Citește `gen/strategii/hammer_mbt10_bh_labu.spec.txt` și scrie
+`gen/strategii/hammer_mbt10_bh_labu.svg`: 38 de candele daily, downtrend +
+consolidare până pe suportul monthly 80.00, hammer chiar pe nivel
+(o 80.80 / h 80.90 / l 79.60 / c 80.90), confirmarea de la 81.30 (close peste
+high-ul hammer-ului) și urcarea în ~11 bare până la 97.56 = +20,0 % față de
+intrare.
+
+REAL din curs (`18_2025-09-23.md:721-726`): că e buy hammer la suportul
+monthly și că a dat +20 % în jumătate de lună. CONSTRUITE de sumarizator:
+suportul 80.00, cifrele hammer-ului (80.80/80.90/79.60), intrarea 81.30, SL
+79.60 și seria de prețuri — materialul nu dă entry, SL sau TP pentru cazul
+ăsta. Legenda din desen spune exact asta.
+
+Rulare: python3 gen/strategii/hammer_mbt10_bh_labu.py
+"""
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, MA, Mark, from_closes # noqa: E402
+
+OUT = Path(__file__).with_suffix(".svg")
+
+# --- nivelurile din specificație (toate CONSTRUITE) ------------------------
+SUPPORT = 80.00 # suport monthly (construit)
+HAMMER_H = 80.90 # high-ul hammer-ului = close (H=C)
+HAMMER_L = 79.60 # low-ul hammer-ului = SL propus
+ENTRY = 81.30 # intrare = close-ul confirmării, peste 80.90 (construit)
+SL = 79.60 # SL sub low-ul hammer-ului (construit)
+PEAK = 97.56 # max close de după intrare: 97.56 / 81.30 = +20,0 % (real)
+
+# --- seria de close-uri din specificație (38 bare, o = close precedent) ----
+closes = [
+ # downtrend + consolidare 0-22
+ 92.00, 90.50, 91.20, 89.40, 88.10, 88.60, 87.00, 85.80, 86.30, 84.90,
+ 83.60, 82.40, 83.00, 81.80, 80.70, 81.20, 80.20, 80.80, 81.50, 80.40,
+ 80.10, 80.60, 80.80,
+ 80.90, # 23 = hammer: o 80.80, c 80.90, h 80.90, l 79.60
+ 81.30, # 24 = confirmare, close > high[23] = intrare
+ # 25..37 = 13 bare de urcare; max close 97.56 / 81.30 - 1 = +20,0 %
+ 81.90, 82.60, 83.40, 84.20, 85.10, 86.00, 87.00, 88.10, 89.30, 90.60,
+ 92.00, 93.50, 97.56,
+]
+assert len(closes) == 38, f"seria are {len(closes)} bare, spec cere 38"
+
+# wick-uri: (up, down). Restul (0.30, 0.40); hammer-ul are fitil jos 1.20 și
+# zero fitil sus ca low-ul/high-urile cerute de asserts să iasă exact.
+wicks = [(0.30, 0.40)] * len(closes)
+wicks[23] = (0.00, 1.20) # hammer: h 80.90, l 79.60
+
+# volume construite (nu sunt cifre de curs, doar panoul de volum cerut de
+# stil): vânzare în creștere spre suport, explozie la hammer + confirmare.
+vols = [
+ 52, 50, 47, 49, 45, 46, 43, 45, 41, 43,
+ 44, 47, 42, 45, 48, 46, 50, 44, 43, 46,
+ 51, 49, 55, 88, 84,
+ 70, 62, 58, 54, 52, 50, 48, 46, 45, 47,
+ 55, 62, 72,
+]
+assert len(vols) == len(closes), f"volume {len(vols)} != bare {len(closes)}"
+
+cds = from_closes(closes, wicks=wicks, vols=vols, start=94.00)
+assert abs(cds[0].o - 94.00) < 1e-9 and cds[0].c < cds[0].o, "prima bară nu e roșie"
+for a, b in zip(cds, cds[1:]):
+ assert abs(a.c - b.o) < 1e-9, "open != close-ul barei anterioare"
+
+# --- assert-urile din specificație (toate, ca assert reale) ----------------
+# 1. hammer-ul stă pe suportul monthly 80.00
+assert cds[23].l == 79.60 <= SUPPORT <= cds[23].h == HAMMER_H, \
+ f"hammer 79.60 <= 80.00 <= 80.90 nu iese: {cds[23].l}/{cds[23].h}"
+
+# 2. forma hammer-ului: o = close[22] = 80.80, corp 0.10, fitil jos 1.20,
+# 1.20 >= 2 * 0.10, close == high (fără fitil sus)
+assert cds[23].o == cds[22].c == 80.80, "open[23] nu e close[22] = 80.80"
+body = abs(cds[23].c - cds[23].o)
+lower_wick = min(cds[23].o, cds[23].c) - cds[23].l
+upper_wick = cds[23].h - max(cds[23].o, cds[23].c)
+assert abs(body - 0.10) < 1e-9, f"corpul hammer-ului e {body}, nu 0.10"
+assert abs(lower_wick - 1.20) < 1e-9, f"fitilul jos e {lower_wick}, nu 1.20"
+assert abs(upper_wick) < 1e-9, f"hammer-ul are fitil sus {upper_wick}"
+assert lower_wick >= 2 * body, f"1.20 < 2 * {body} (nu e hammer valid)"
+assert cds[23].c == cds[23].h == HAMMER_H, "close[23] != high[23] == 80.90"
+
+# 3. confirmarea: close[24] == 81.30 == intrare și > high[23] == 80.90
+assert abs(cds[24].c - ENTRY) < 1e-9, f"close[24]={cds[24].c} != intrare 81.30"
+assert cds[24].c > cds[23].h == HAMMER_H, "confirmarea nu închide peste 80.90"
+
+# 4. SL = min(low[22:25]) = low-ul hammer-ului = 79.60
+min_22_25 = min(c.l for c in cds[22:25])
+assert abs(min_22_25 - SL) < 1e-9, f"min low[22:25]={min_22_25} != SL {SL}"
+assert abs(SL - cds[23].l) < 1e-9, "SL nu e low-ul hammer-ului"
+
+# 5. procentul REAL al cursului: +20,0 % de la intrare la vârf
+peak = max(c.c for c in cds[25:38])
+pct = round((peak / ENTRY - 1) * 100, 1)
+assert abs(peak - PEAK) < 1e-9, f"max close[25:38]={peak} != 97.56"
+assert pct == 20.0, f"procentul iese {pct}, nu +20,0 %"
+assert abs(peak / ENTRY - 1.20) < 1e-9, "97.56 / 81.30 nu e exact 1.20"
+
+# 6. downtrend/consolidare înainte de hammer
+assert cds[23].c < cds[0].c - 8, f"close[23]={cds[23].c} nu e cu 8 sub close[0]"
+
+# 7. ordinea nivelurilor
+assert SL < ENTRY < peak, f"SL/entry/vârf: {SL}/{ENTRY}/{peak}"
+
+# --- verificarea serii (stil + geometrie) ----------------------------------
+entry = cds[24].c
+w = 760
+ch = Chart(
+ candles=cds,
+ levels=[
+ Level(SUPPORT, "suport monthly 80.00", "#777",
+ dash="4 3", label_at="left"),
+ Level(SL, "SL sub low-ul hammer-ului 79.60 (construit)", "#c00",
+ label_at="right"),
+ Level(ENTRY, "intrare 81.30 (construit)", "#188a3e", label_at="right"),
+ ],
+ marks=[
+ Mark(23, "hammer 80.90 / 79.60 (construit)", "#b06000", "below"),
+ Mark(37, f"+20 % (cifra cursului): {peak:.2f} / {ENTRY:.2f}",
+ "#188a3e", "above"),
+ ],
+ mas=[MA(20, "#b06000", dash="4 3", text="SMA-20")],
+ scale=8.0, gap=15, body_w=9, left=44, width=w, vol_h=70,
+ vol_label="volum", title="LABU — daily",
+ caption="nivelurile de preț sunt construite de sumarizator.",
+)
+# note pe bare: explicațiile hammer-ului/confirmării și legenda obligatorie
+# (textul lung de nivel ar intra peste candele, deci stă în blocul de jos).
+cds[0].note = ("bara 23 = hammer: o 80.80, h 80.90, l 79.60, c 80.90 — "
+ "corp 0.10, fitil jos 1.20 (H=C), pe suportul 80.00")
+cds[1].note = ("bara 24 = confirmare: close 81.30 — close peste high-ul "
+ "hammer-ului 80.90 = intrare 81.30 (construit)")
+cds[2].note = ("Procentul (+20 % în jumătate de lună) și contextul (suport "
+ "monthly) sunt ale cursului (18_2025-09-23.md:721-726);")
+
+svg = ch.render()
+assert "\n\n" not in svg and "', re.S)
+H = float(re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', svg).group(2))
+labels = []
+for m in TEXT.finditer(svg):
+ x, y, attrs, s = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4)
+ anchor = "end" if 'text-anchor="end"' in attrs else \
+ ("middle" if 'text-anchor="middle"' in attrs else "start")
+ bold = "bold" in attrs
+ wpx = len(s) * 12 * (0.62 if bold else 0.52)
+ left = x - wpx if anchor == "end" else (x - wpx / 2 if anchor == "middle" else x)
+ labels.append((left, left + wpx, y, s))
+
+for left, right, y, s in labels:
+ assert left >= -2 and right <= w + 2, f"text iese din panză: {s!r} [{left:.0f},{right:.0f}]"
+ assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y:.0f})"
+
+for i in range(len(labels)):
+ li, ri, yi, si = labels[i]
+ for j in range(i + 1, len(labels)):
+ lj, rj, yj, sj = labels[j]
+ if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
+ raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}\n {labels[i]}\n {labels[j]}")
+
+# candelele sunt o serie continuă, lipită
+xs = [ch._cx(i) for i in range(len(cds))]
+assert xs[-1] + ch.body_w / 2 < w, f"ultima candelă iese din panză (x={xs[-1]})"
+assert all(xs[i + 1] - xs[i] == ch.gap for i in range(len(xs) - 1)), "serie cu goluri"
+
+print(f"LABU: {len(cds)} bare, hammer {cds[23].l:.2f}/{cds[23].h:.2f} pe suport "
+ f"{SUPPORT:.2f}, intrare {entry:.2f}, SL {cds[23].l:.2f}, vârf {peak:.2f}")
+print(f"procent: {peak:.2f} / {entry:.2f} - 1 = +{pct:.1f} % (cifra cursului)")
+print(f"scris {OUT} ({w}x{H:.0f}); {len(labels)} etichete verificate, fără suprapuneri")
+OUT.write_text(svg, encoding="utf-8")
diff --git a/gen/strategii/hammer_mbt10_bh_labu.svg b/gen/strategii/hammer_mbt10_bh_labu.svg
new file mode 100644
index 0000000..e385f25
--- /dev/null
+++ b/gen/strategii/hammer_mbt10_bh_labu.svg
@@ -0,0 +1,133 @@
+
\ No newline at end of file
diff --git a/gen/strategii/hammer_mbt10_harta.py b/gen/strategii/hammer_mbt10_harta.py
new file mode 100644
index 0000000..5674e22
--- /dev/null
+++ b/gen/strategii/hammer_mbt10_harta.py
@@ -0,0 +1,266 @@
+#!/usr/bin/env python3
+"""Hartă de orientare: buy hammer (public) vs. MBT-10 (cutie neagră), același
+trigger de bottom.
+
+Pentru `strategii/06_hammer_mbt10.md`, marcajul ``.
+Nu e grafic de preț (spec: TIP CONCEPT, CHEIE harta) — SVG scris de mână,
+doar stdlib; nu se folosește svg_candles / pchart.
+
+Ce arată (toate citatele și trimiterile vin din spec, nu se inventează):
+- bara comună de sus: setup-urile de fond spun UNDE, trigger-ul de candelă
+ spune CÂND (`02_mbt10.md:9-11`, `43-49`);
+- două cutii alăturate: BUY HAMMER (pattern standard, public) și MBT-10
+ (cutie neagră, indicator proprietar), fiecare cu regulile ei și cu
+ trimiterile de rigoare;
+- săgeata de jos spre caseta finală: ce rămâne utilizabil pe BVB.
+
+Verdictele de BVB din cutii sunt judecata mea (secțiunea Transfer), marcate
+ca atare în legendă, exact cum cere specul.
+
+Rulare: python3 gen/strategii/hammer_mbt10_harta.py -> hammer_mbt10_harta.svg
+"""
+from pathlib import Path
+from xml.sax.saxutils import escape
+
+OUT = Path(__file__).with_suffix(".svg")
+
+W, H = 700, 610
+BLACK, GRAY, BLUE, GREEN, RED, ORANGE, WHITE = (
+ "#000000", "#777777", "#1a5fb4", "#188a3e", "#c00", "#b06000", "#ffffff")
+BORDER = "#333333"
+
+# ---------------------------------------------------------------- geometrie
+BAR_X0, BAR_X1, BAR_Y, BAR_H = 40.0, 660.0, 82.0, 76.0
+
+BOX_W = 302.0
+BOX_LX, BOX_RX, BOX_Y = 40.0, 358.0, 190.0
+BOX_H = 256.0
+PAD = 16.0
+
+ROW0 = BOX_Y + 56.0 # baseline-ul primei reguli
+ROW_H = 52.0
+VERDICT_SEP = BOX_Y + 216.0
+VERDICT_Y = VERDICT_SEP + 26.0
+
+BUS_Y = 460.0 # linia orizontală de unire a celor două cutii
+TRI_TIP = 500.0
+FINAL_X0, FINAL_Y, FINAL_W, FINAL_H = 110.0, 500.0, 480.0, 56.0
+
+TEXTS = [] # (x, y, text, size, weight, anchor) pentru verificare
+
+
+def txt(x, y, s, size=13, weight="400", fill=BLACK, anchor="middle"):
+ TEXTS.append((x, y, s, size, weight, anchor))
+ return (
+ f'{escape(s)}'
+ )
+
+
+def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None):
+ d = f' stroke-dasharray="{dash}"' if dash else ""
+ return (f'')
+
+
+def rect(x, y, w, h, fill=WHITE, stroke=GRAY, width=1.2):
+ return (f'')
+
+
+def box(bx, title, title_fill, rows, verdict, verdict_fill, fill):
+ """O cutie cu chenar: titlu, reguli (linii + trimitere), verdict."""
+ x = bx + PAD
+ area = BOX_W - 2 * PAD
+ out = [rect(bx, BOX_Y, BOX_W, BOX_H, fill=fill, stroke=title_fill, width=1.4)]
+ out.append(txt(x, BOX_Y + 26, title, 15, "700", title_fill, "start"))
+ out.append(line(bx + PAD, BOX_Y + 38, bx + BOX_W - PAD, BOX_Y + 38,
+ title_fill, 1.0))
+ for i, (mains, ref) in enumerate(rows):
+ top = ROW0 + i * ROW_H
+ for k, m in enumerate(mains):
+ out.append(txt(x, top + 14 + k * 17, m, 13, "400", BLACK, "start"))
+ if ref is not None:
+ out.append(txt(x, top + 48, ref, 13, "400", GRAY, "start"))
+ out.append(line(bx + PAD, VERDICT_SEP, bx + BOX_W - PAD, VERDICT_SEP,
+ verdict_fill, 1.2))
+ out.append(txt(x, VERDICT_Y, verdict, 13, "700", verdict_fill, "start"))
+ return out, area
+
+
+def build():
+ p = [
+ f'')
+ return "\n".join(p) + "\n", left_area, right_area
+
+
+# ---------------------------------------------------------------- verificare
+def est_w(s, size, weight):
+ factor = 0.62 if weight == "700" else 0.56
+ return len(s) * size * factor
+
+
+def box_of(x, y):
+ for bx in (BOX_LX, BOX_RX):
+ if bx <= x <= bx + BOX_W and BOX_Y <= y <= BOX_Y + BOX_H:
+ return bx
+ return None
+
+
+def verify(svg, left_area, right_area):
+ # 1. geometrie: zonele nu se calcă
+ assert BAR_Y + BAR_H < BOX_Y, "bara comună intră în cutii"
+ assert BOX_Y + BOX_H < BUS_Y < TRI_TIP <= FINAL_Y, "săgeata nu are loc"
+ assert FINAL_Y + FINAL_H < 578, "caseta finală intră în legendă"
+ assert left_area == right_area == BOX_W - 2 * PAD
+
+ # 2. toate textele în panză
+ for x, y, s, size, weight, anchor in TEXTS:
+ ew = est_w(s, size, weight)
+ left = x - ew if anchor == "end" else (x - ew / 2 if anchor == "middle" else x)
+ right = left + ew
+ assert -2 <= left and right <= W + 2, f"text în afara panzei: {s!r}"
+ assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y})"
+
+ # 3. texte care nu se suprapun
+ for i in range(len(TEXTS)):
+ xi, yi, si, szi, wi, ai = TEXTS[i]
+ we = est_w(si, szi, wi)
+ li = xi - we if ai == "end" else (xi - we / 2 if ai == "middle" else xi)
+ ri = li + we
+ for j in range(i + 1, len(TEXTS)):
+ xj, yj, sj, szj, wj, aj = TEXTS[j]
+ wej = est_w(sj, szj, wj)
+ lj = xj - wej if aj == "end" else (xj - wej / 2 if aj == "middle" else xj)
+ rj = lj + wej
+ if abs(yi - yj) < 13 and li < rj - 4 and lj < ri - 4:
+ raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}")
+
+ # 4. textele din cutii încap în cutii
+ for x, y, s, size, weight, anchor in TEXTS:
+ b = box_of(x, y)
+ if b is not None:
+ ew = est_w(s, size, weight)
+ assert x + ew <= b + BOX_W - PAD + 1, f"text iese din cutie: {s!r}"
+
+ # 5. verdictul e sub linia lui de separare
+ assert VERDICT_SEP < VERDICT_Y < BOX_Y + BOX_H
+
+ # 6. conținutul obligatoriu din spec
+ for cheie in (
+ "Trigger de bottom: unde + când",
+ "Setup-urile de fond — suport, locul crimei, double bottom, buy doji,",
+ "divergență de volum — spun UNDE. Trigger-ul de candelă spune CÂND.",
+ "02_mbt10.md:9-11, 43-49",
+ "BUY HAMMER — pattern standard",
+ "„formă de candelă + downtrend”",
+ "05_patternuri_candele.md:77-88",
+ "„trigger: close peste",
+ "high-ul hammer-ului”",
+ "05_patternuri_candele.md:99-124",
+ "„fără niciun indicator proprietar”",
+ "„pe BVB: se poate rula long-only”",
+ "MBT-10 — cutie neagră",
+ "„formula nu e publică nicăieri”",
+ "02_mbt10.md:21-39",
+ "„reconstituibil doar:",
+ "close peste pivotul daily”",
+ "02_mbt10.md:179-188",
+ "„scanner + indicator proprietar”",
+ "02_mbt10.md:853-863",
+ "„pe BVB: nu, ca sistem”",
+ "Ce rămâne pe BVB: hammer la un nivel major",
+ "+ close peste PPD",
+ "verdictul de BVB = judecata mea",
+ "din secțiunea Transfer.",
+ ):
+ assert cheie in svg, f"lipsește din desen: {cheie!r}"
+
+ # 7. fără axe de preț / candele / cifre inventate
+ for interzis in ("candela", "SL ", "intrare ", "TP1", "TP2", "$", "USD"):
+ assert interzis not in svg, f"nu ar trebui să apară preț: {interzis!r}"
+ return True
+
+
+SVG, LEFT_AREA, RIGHT_AREA = build()
+assert verify(SVG, LEFT_AREA, RIGHT_AREA), "verificare eșuată"
+OUT.write_text(SVG, encoding="utf-8")
+print(f"scris {OUT} ({W}x{H}), verificare OK: {len(TEXTS)} etichete, "
+ f"cutii {BOX_W:g}x{BOX_H:g} la {BOX_LX:g}/{BOX_RX:g}")
diff --git a/gen/strategii/hammer_mbt10_harta.svg b/gen/strategii/hammer_mbt10_harta.svg
new file mode 100644
index 0000000..2a5f5bd
--- /dev/null
+++ b/gen/strategii/hammer_mbt10_harta.svg
@@ -0,0 +1,48 @@
+
diff --git a/gen/strategii/hammer_mbt10_mbt_ansamblu.py b/gen/strategii/hammer_mbt10_mbt_ansamblu.py
new file mode 100644
index 0000000..731a10b
--- /dev/null
+++ b/gen/strategii/hammer_mbt10_mbt_ansamblu.py
@@ -0,0 +1,196 @@
+#!/usr/bin/env python3
+"""MBT-10 — pullback în locul crimei + hammer, intrare la close 54.60 (ansamblu).
+
+Citește `gen/strategii/hammer_mbt10_mbt_ansamblu.spec.txt` și scrie
+`gen/strategii/hammer_mbt10_mbt_ansamblu.svg`: 41 de candele daily pe exemplul
+construit al sumarizatorului (`summaries/08_ANALIZA_ZILNICA/02_mbt10.md:165-173`,
+`978-984`) — uptrend până la fostul vârf 52.10, spargere, vârf nou 58.90,
+pullback în locul crimei, hammer cu codița exact pe 52.10, iar bara următoare
+se închide în MBT-10 la 54.60 (intrare la close). SL 52.00, TP 58.90 = 1.65R.
+
+Toate prețurile/nivelurile sunt cele din specificație. Seria de close-uri e
+cea dată; open-ul fiecărei bare = close-ul precedentului. MBT-10 nu se
+desenează (formula nu e publică) — se marchează doar bara la care semnalul
+s-a închis.
+
+Rulare: python3 gen/strategii/hammer_mbt10_mbt_ansamblu.py
+"""
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, from_closes # noqa: E402
+
+OUT = Path(__file__).with_suffix(".svg")
+
+# --- nivelurile din specificație -------------------------------------------
+CRIMEA = 52.10 # locul crimei = fostul vârf spart, retestat ca suport
+HAMMER_H = 53.40 # high = close al hammer-ului
+HAMMER_L = 52.10 # codița hammer-ului, exact pe locul crimei
+ENTRY = 54.60 # close-ul barei care s-a închis în MBT-10 = intrare
+SL = 52.00 # sub codița hammer-ului și sub locul crimei
+TP = 58.90 # vârful anterior = 1.65R (risc 2.60)
+
+# --- seria: 41 de close-uri, exact ca în specificație -----------------------
+closes = [
+ # 0-8: uptrend care duce spre fostul vârf
+ 46.00, 46.80, 47.50, 48.30, 49.00, 49.80, 50.50, 51.20, 52.00,
+ 52.10, # 9 = fostul vârf 52.10 (high cu wick 52.40)
+ # 10-20: spargerea nivelului și urcarea spre vârful nou
+ 51.40, 50.80, 51.50, 52.40, 53.20, 54.00, 54.80, 55.60, 56.40, 57.20,
+ 58.00,
+ 58.90, # 21 = vârf nou
+ # 22-28: pullback înapoi în locul crimei
+ 58.20, 57.40, 56.50, 55.60, 54.70, 53.60, 52.20,
+ # 29-30: hammer pe locul crimei + ziua care se închide în MBT-10
+ 53.40, # 29 = hammer: o 52.20, h/c 53.40, l 52.10 (H = C)
+ 54.60, # 30 = MBT-10 închis la close => intrare
+ # 31-40: urcarea spre ținta de la vârful anterior
+ 55.20, 55.80, 56.40, 57.00, 57.60, 58.10, 58.60,
+ 58.90, # 38 = TP (vârful anterior)
+ 58.70, 58.30,
+]
+assert len(closes) == 41, f"seria are {len(closes)} bare, spec cere 41"
+
+# wick-uri (sus, jos): restul (0.30, 0.40); excepțiile din specificație.
+# `from_closes` măsoară wick-ul de jos de la min(o, c), nu de la close: pentru
+# hammer-ul cu o 52.20 / c 53.40, ca low-ul să iasă exact 52.10, wick-ul de jos
+# e 0.10 (nu 1.30, cum dă specificația măsurând de la close).
+wicks = [(0.30, 0.40)] * 41
+wicks[9] = (0.30, 0.30) # high 52.40, low 51.70 (fostul vârf)
+wicks[28] = (0.30, 0.10) # low 52.10: pullback-ul atinge locul crimei
+wicks[29] = (0.00, 0.10) # hammer: high = close = 53.40, low = 52.10
+# bara 30: specificația dă (0.20, 0.20) cu „low 54.40"; 54.40 ar fi peste
+# open-ul 53.40, deci imposibil. Cu (0.20, 0.20) low-ul iese 53.20; nu e
+# etichetat nicăieri și nu intră în niciun assert.
+wicks[30] = (0.20, 0.20)
+
+# volum „normal", fără argument de volum (specificația o spune explicit):
+# doar panoul de volum cerut de stil, fără spike-uri pe hammer/confirmare.
+vols = [
+ 50, 49, 51, 50, 52, 51, 50, 52, 51,
+ 52,
+ 50, 49, 51, 50, 52, 51, 50, 52, 51, 52,
+ 51,
+ 52,
+ 50, 49, 50, 51, 50, 49, 50,
+ 51, 52,
+ 50, 51, 52, 51, 50, 52, 51,
+ 52,
+ 51, 50,
+]
+assert len(vols) == 41
+
+cds = from_closes(closes, wicks=wicks, vols=vols, start=45.60)
+
+# seria continuă: open = close-ul barei anterioare
+for a, b in zip(cds, cds[1:]):
+ assert abs(a.c - b.o) < 1e-9, "open != close-ul barei anterioare"
+
+intrare = cds[30].c
+sl, tp = SL, TP
+
+# --- cele 8 assert-uri cerute de specificație ------------------------------
+# 1. hammer 29: low exact pe locul crimei, close == high (fără fitil de sus)
+assert abs(cds[29].l - CRIMEA) < 1e-9, f"low[29]={cds[29].l} != 52.10"
+assert abs(cds[29].c - HAMMER_H) < 1e-9 and abs(cds[29].h - HAMMER_H) < 1e-9, \
+ f"close[29]/high[29] != 53.40 ({cds[29].c}/{cds[29].h})"
+assert cds[29].c == cds[29].h, "hammer-ul are fitil de sus"
+# 2. bara 30 se închide la 54.60 = intrare și peste high-ul hammer-ului
+assert abs(intrare - ENTRY) < 1e-9, f"close[30]={intrare} != intrare 54.60"
+assert intrare > cds[29].h == HAMMER_H, "close[30] nu e peste high[29] == 53.40"
+# 3. SL = low-ul hammer-ului - 0.10
+assert abs(sl - (cds[29].l - 0.10)) < 1e-9, "SL != low[29] - 0.10"
+# 4. risc 2.60; TP 58.90 = 1.65R
+risc = intrare - sl
+assert abs(risc - 2.60) < 1e-9, f"risc={risc} != 2.60"
+assert abs(tp - 58.90) < 1e-9, "TP != 58.90"
+assert round((tp - intrare) / (intrare - sl), 2) == 1.65, "TP != 1.65R"
+# 5. fostul vârf 52.10 există înainte de breakout (bara 9, high 52.40)
+assert any(abs(cds[i].h - 52.10) <= 0.30 for i in range(15)), \
+ "niciun high din primele 15 bare nu e în 0.30 de 52.10"
+assert abs(cds[9].h - 52.40) < 1e-9, f"high[9]={cds[9].h} != 52.40"
+# 6. TP atins după intrare
+assert max(c.c for c in cds[30:41]) >= 58.90, "TP 58.90 nu e atins după intrare"
+# 7. vârful nou 58.90 e la bara 21, înainte de pullback; pullback-ul coboară
+# exact la locul crimei (minimul secțiunii 21-29 e codița hammer-ului)
+assert abs(cds[21].c - 58.90) < 1e-9, f"close[21]={cds[21].c} != 58.90"
+assert abs(min(c.l for c in cds[21:30]) - CRIMEA) < 1e-9, \
+ f"min low[21:30]={min(c.l for c in cds[21:30])} != 52.10"
+# 8. prima etapă e urcare
+assert cds[9].c > cds[0].c, "prima etapă nu e uptrend"
+
+print(f"hammer H/C {cds[29].h:.2f} L {cds[29].l:.2f} intrare {intrare:.2f} "
+ f"SL {sl:.2f} TP {tp:.2f} = {round((tp-intrare)/(intrare-sl),2)}R "
+ f"vârf nou {cds[21].c:.2f} (bara 21)")
+
+# --- desenul ----------------------------------------------------------------
+ch = Chart(
+ candles=cds,
+ levels=[
+ Level(CRIMEA, "locul crimei 52.10 (fost vârf spart)", "#777",
+ label_at="left"),
+ Level(SL, "SL 52.00 — sub codița hammer-ului", "#c00"),
+ Level(ENTRY, "intrare 54.60 (close în MBT-10)", "#188a3e"),
+ Level(TP, "TP 58.90 = 1.65R", "#1a5fb4"),
+ ],
+ marks=[
+ Mark(29, "hammer — codița pe 52.10", "#b06000", "below"),
+ # dy negativ: bara 30 e urmată imediat de bare mai înalte (31-32), iar
+ # textul lipit de high-ul barei 30 ar cădea peste corpurile lor.
+ Mark(30, "MBT-10 închis", "#188a3e", "above", dy=-24),
+ ],
+ scale=20.0, gap=15, body_w=9, left=44, width=780, vol_h=70,
+ vol_label="volum",
+ title="MBT-10 — pullback în locul crimei + hammer, închidere la 54.60 (exemplu construit)",
+)
+
+# explicațiile și legenda obligatorie: bloc de text sub grafic (nu se
+# suprapun cu candelele, iar textele lungi nu încap pe verticală lângă bare).
+cds[0].note = ("Legendă: MBT-10 nu se desenează — formula nu e publică "
+ "(02_mbt10.md:21-39); se marchează doar bara la care")
+cds[1].note = ("semnalul s-a închis. Nivelurile sunt exemplul construit al "
+ "sumarizatorului (02_mbt10.md:165-173, 978-984).")
+cds[2].note = ("bara 9 = fostul vârf 52.10 (locul crimei); bara 21 = vârful "
+ "nou 58.90.")
+cds[3].note = ("bara 29 = hammer: o 52.20, h/c 53.40, l 52.10 (codița exact "
+ "pe locul crimei); bara 30 = ziua închisă în MBT-10.")
+
+svg = ch.render()
+assert "")
+
+assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
+# barul TICUS cade exact sub candela M (aceeași candelă sus și jos)
+bar_svg = f''
+assert bar_svg in svg, "barul TICUS nu a fost desenat"
+assert abs((float(re.search(r'x="([\d.]+)"', bar_svg).group(1)) + 5) - ch._cx(M_IX)) < 1e-9
+# aceeași scară px/unitate: SL-intrare și intrare-TP1 sunt egale și vizual (1:1)
+assert abs((ch._y(sl) - ch._y(entry)) - (ch._y(entry) - ch._y(tp1))) < 1e-9
+assert f'x1="{xm:.1f}"' in svg
+OUT.write_text(svg)
+print(f"scris {OUT.name}: {len(cds)} candele, entry {entry:.2f}, SL {sl:.2f}, "
+ f"TP1 {tp1:.2f}, ieșire ilustrativă {exit_ill:.2f} ({(exit_ill-entry)/risc:.2f}R), "
+ f"ticus(M)={ticus_m:.0f}, x(M)={xm:.0f}")
diff --git a/gen/strategii/mbp_ansamblu.svg b/gen/strategii/mbp_ansamblu.svg
new file mode 100644
index 0000000..5822b75
--- /dev/null
+++ b/gen/strategii/mbp_ansamblu.svg
@@ -0,0 +1,95 @@
+
+
+ 623.90 — bandă inferioară
+
+ 624.70 — bandă superioară
+
+ 624.30 — SMA20 / TP1
+
+ SL 623.60 (lowest low, 4 candele)
+
+ trigger Buy Momentum → intrare 623.95
+
+ ieșire ilustrativă ~625.35 (≈ 1:4)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ M: roșie, close 623.87 sub bandă
+ SPY — 15 min: bandă + TICUS pe aceeași candelă → trigger → intrare → SL → TP1 → trail
+ trend descendent anterior (14 bare)
+
+ TICUS — presiunea pieței (↑ = peste 0)
+
+ +1000 — supracumpărare
+
+ +500 (doar referință vizuală)
+
+ 0
+
+ -500 (doar referință vizuală)
+
+ -1000 — oversold
+
+ M: TICUS -1150 (sub -1000)
+ SPY 15 min. Cifre din curs: M close 623.87 (sub bandă 623.90), TICUS -1150 pe aceeași candelă,
+ trigger Buy Momentum → entry 623.95, SL 623.60 (lowest low din 4 candele, trigger inclus),
+ TP1 624.30 (+0.35, 1:1). Ieșirea ~625.35 = entry + 4R (≈1:4) e ilustrativă — preț derivat din R,
+ nu din curs. Structura candelelor (30 bare continue) e construită de mine.
+
\ No newline at end of file
diff --git a/gen/strategii/mbp_exemplu.py b/gen/strategii/mbp_exemplu.py
new file mode 100644
index 0000000..0af6719
--- /dev/null
+++ b/gen/strategii/mbp_exemplu.py
@@ -0,0 +1,242 @@
+"""Diagrama din `gen/strategii/mbp_exemplu.spec.txt` (TIP: PRET).
+
+Trade-ul short complet din material (SPY, 15 min, 25.11.2025): ~12 candele
+de urcare violentă spre vârful ~661, candela M (verde, peste bandă + TICUS
+≥ +1000), trigger Sell Momentum → intrare short 660.00, SL 662.71
+(risc 0.41 %), TP1 657.29 (1:1) atins în prima candelă, apoi trail pe 7
+candele de 15 min până la ieșirea la 646.80 = 2 % = ~4.9R.
+
+Serie continuă de 32 de candele (`from_closes`, open = close anterior), cu
+context înainte de urcare, panou TICUS dedesubt (bar ilustrativ la candela M)
+și legendă. Toate cifrele din desen vin din specificație; singurele valori
+construite de mine sunt structura barelor dintre niveluri și bara TICUS
+(marcată explicit „ilustrativ", pentru că valoarea exactă nu e în material).
+
+Rulare: python3 gen/strategii/mbp_exemplu.py
+"""
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402
+
+OUT = Path(__file__).with_suffix(".svg")
+
+# ── cifrele din material ────────────────────────────────────────────────────
+ENTRY = 660.00 # 09_QA/11_2025-11-25.md:565, 632-636
+SL = 662.71 # risc 0.41 % → (sl-entry)/entry*100
+TP1 = 657.29 # entry - (sl - entry) = 1:1
+EXIT = 646.80 # ieșire pe trail = 2 % = ~4.9R
+BAND = 660.90 # bandă superioară schematică (peste close-ul lui M)
+W, GAP, BODY_W, LEFT = 660, 15, 9, 40
+SCALE = 20.0
+
+M_I, E_I, TP1_I, EXIT_I = 23, 24, 25, 31 # M, trigger, TP1, ieșire
+N_RISE = M_I - 11 # 12 candele de urcare (idx 12..23)
+
+# ── seria: context → urcare violentă → M → trigger → TP1 → trail ────────────
+closes = [
+ # idx 0-11: context în jurul lui 648, înainte de urcarea violentă
+ 649.00, 648.60, 649.20, 648.40, 648.90, 648.10,
+ 648.70, 647.90, 648.50, 648.00, 648.60, 648.30,
+ # idx 12-23: 12 candele de urcare de la ~648 la vârful ~661; idx 23 = M
+ 648.60, 649.40, 650.30, 651.20, 652.10, 653.10,
+ 654.10, 655.20, 656.30, 657.50, 658.80, 660.50,
+ # idx 24: trigger Sell Momentum → close = intrarea short
+ 660.00,
+ # idx 25: prima candelă după intrare atinge TP1 657.29
+ 657.80,
+ # idx 26-31: trail până la ieșirea la 646.80 (6 candele)
+ 656.40, 655.00, 653.40, 651.60, 649.40, 646.80,
+]
+assert len(closes) == 32, len(closes)
+
+wicks = [(0.35, 0.35)] * 12 # context
+wicks += [(0.30, 0.25)] * (M_I - 12) # urcarea (idx 12..22)
+wicks += [(0.50, 0.20)] # M: high = 661.00 (vârful)
+wicks += [(0.20, 0.40), # trigger
+ (0.25, 0.51), # TP1: low = 657.80-0.51 = 657.29
+ (0.30, 0.30), (0.30, 0.30), (0.30, 0.30),
+ (0.30, 0.30), (0.30, 0.30), (0.30, 0.30)] # trail
+assert len(wicks) == len(closes)
+
+cds = from_closes(closes, wicks=wicks, start=648.90)
+
+# ── assert-urile din specificație ───────────────────────────────────────────
+entry = cds[E_I].c
+sl = SL
+tp1 = TP1
+exit_ = cds[EXIT_I].c
+
+assert abs(entry - 660.00) < 0.01, entry
+assert entry == cds[E_I].c, "intrarea nu e close-ul candelei trigger"
+assert abs((sl - entry) / entry * 100 - 0.41) < 0.01, (sl - entry) / entry * 100
+assert abs((sl - entry) - 2.71) < 0.01, sl - entry
+assert abs(tp1 - (entry - (sl - entry))) < 0.01, (entry - (sl - entry), tp1)
+assert cds[TP1_I].l <= tp1 + 1e-9, "prima candelă după intrare nu atinge TP1"
+assert abs(cds[TP1_I].l - tp1) < 0.01, cds[TP1_I].l
+assert abs((entry - exit_) / entry * 100 - 2.0) < 0.05, (entry - exit_) / entry * 100
+assert abs((entry - exit_) / (sl - entry) - 4.9) < 0.1, (entry - exit_) / (sl - entry)
+assert EXIT_I - E_I == 7, EXIT_I - E_I
+assert sl > entry > tp1, "short: SL deasupra intrării, TP1 sub intrare"
+assert BAND > cds[M_I].c, "banda schematică nu e deasupra close-ului lui M"
+
+# structura care susține afirmațiile din text
+assert cds[M_I].c > cds[M_I].o, "candela M trebuie să fie verde"
+assert cds[E_I].c < cds[E_I].o, "candela trigger trebuie să fie roșie"
+assert M_I == E_I - 1, "trigger-ul nu e imediat după M"
+assert N_RISE == 12, N_RISE
+assert 30 <= len(cds) <= 34, len(cds)
+assert max(cds[i].c for i in range(12, M_I + 1)) == cds[M_I].c, "M nu e vârful urcării"
+assert max(c.h for c in cds) == cds[M_I].h, "vârful seriei nu e la M"
+assert all(cds[i].c > cds[i - 1].c for i in range(13, M_I + 1)), "urcarea nu e monotonă"
+
+# ── panoul de preț ──────────────────────────────────────────────────────────
+levels = [
+ Level(SL, "SL 662.71 (risc 0.41 %)", "#c00", label_at="right"),
+ Level(ENTRY, "intrare short 660.00", "#188a3e", label_at="left"),
+ Level(BAND, "bandă superioară (schematic)", "#777", dash="2 4", label_at="left"),
+ Level(TP1, "TP1 657.29 (1:1)", "#1a5fb4", label_at="left"),
+ Level(EXIT, "ieșire trail 646.80 = 2 % = ~4.9R", "#1a5fb4", label_at="left"),
+]
+
+marks = [
+ Mark(M_I, "M: candelă verde peste bandă + TICUS ≥ +1000", "#188a3e", "above"),
+]
+
+cds[M_I].note = ("M: valori exacte NEACOPERIT în material (bandă + TICUS); "
+ "bara ~+1000 din panou e ilustrativă")
+cds[E_I].note = "trigger Sell Momentum → intrare short 660.00 (risc 0.41 % = 2.71)"
+cds[TP1_I].note = "TP1 657.29 (1:1) atins în prima candelă după intrare"
+cds[EXIT_I].note = ("ieșire pe trail 646.80 = 2 % = ~4.9R, după 7 candele "
+ "de 15 min de la intrare")
+
+ch = Chart(
+ candles=cds,
+ levels=levels,
+ marks=marks,
+ mas=[MA(20, "#b06000", dash="4 3", text="SMA-20")],
+ scale=SCALE, width=W, gap=GAP, body_w=BODY_W, left=LEFT,
+ title="SPY — 15 min: MBAP short 25.11.2025 (risc 0.41 % → ieșire 4.9R)",
+ caption="",
+)
+price_svg = ch.render()
+
+# corpul graficului de preț și înălțimea lui (pentru lipirea panoului TICUS)
+m = re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', price_svg)
+H1 = float(m.group(2))
+body1 = price_svg[price_svg.index(">") + 1:price_svg.rindex("")].strip("\n")
+
+# ── panoul TICUS dedesubt (bar ilustrativ la candela M) ─────────────────────
+VGAP = 26
+Y0 = H1 + VGAP
+TICUS_H = 100
+zero_y = Y0 + 84 # linia 0
+plus_y = Y0 + 32 # linia +1000 (punctată)
+bar_top = plus_y - 18 # bara depășește +1000
+m_x = LEFT + M_I * GAP
+
+assert bar_top < plus_y, "bara TICUS nu depășește linia +1000"
+assert plus_y < zero_y, "linia +1000 nu e deasupra liniei 0"
+
+ticus = [
+ f''
+ f'Panou TICUS — linia 0 și pragul +1000 (ilustrativ)',
+ f'',
+ f''
+ f'+1000 (prag)',
+ f'',
+ f'0',
+ f'',
+ f''
+ f'ilustrativ',
+ f''
+ f'candela M',
+]
+
+# ── legenda (cifrele/sursele) ───────────────────────────────────────────────
+legend = [
+ "Legendă: procentele (risc 0.41 %, ieșire 2 %) sunt ale instructorului.",
+ "Prețurile absolute (660.00 / 662.71 / 657.29 / 646.80) sunt construite de sumarizator —",
+ "09_QA/11_2025-11-25.md:716-719.",
+ "Structura de reținut: risc mic → TP1 (1:1) atins imediat → trail lung (7 candele de 15 min).",
+ "Valoarea exactă a benzii și a TICUS la candela M nu e dată în sesiune; bara ~+1000 e ilustrativă.",
+ "„Taurii urcă pe scări, urșii sar de la etaj\" — de ce short-ul se consumă în ore.",
+ "(09_QA/11_2025-11-25.md:643-648).",
+]
+cap_top = Y0 + TICUS_H + 20
+H = cap_top + 16 * (len(legend) - 1) + 14
+
+parts = [
+ f'',
+ '',
+ body1,
+ "",
+ *ticus,
+ *[f'{t}'
+ for i, t in enumerate(legend)],
+ "",
+]
+svg = "\n ".join(parts)
+assert "\n\n" not in svg, "linie goală în SVG — rupe markdown-ul"
+assert svg.count("") == 1, "SVG-ul trebuie un singur root"
+
+# ── verificare de citire: etichete în panză, fără suprapuneri ───────────────
+TEXT = re.compile(r']*)>(.*?)', re.S)
+
+
+def _fonts():
+ """Lățimea reală a textului (DejaVuSans ~ system-ui); fallback estimativ."""
+ try:
+ from PIL import ImageFont
+ base = "/usr/share/fonts/truetype/dejavu/"
+ return (ImageFont.truetype(base + "DejaVuSans.ttf", 12),
+ ImageFont.truetype(base + "DejaVuSans-Bold.ttf", 12))
+ except Exception:
+ return None, None
+
+
+_F_NORM, _F_BOLD = _fonts()
+
+
+def _tw(s: str, bold: bool) -> float:
+ if _F_BOLD is not None:
+ return (_F_BOLD if bold else _F_NORM).getlength(s)
+ return len(s) * 12 * (0.62 if bold else 0.52)
+
+
+labels = []
+for mm in TEXT.finditer(svg):
+ x, y = float(mm.group(1)), float(mm.group(2))
+ attrs, s = mm.group(3), mm.group(4)
+ anchor = "start"
+ if 'text-anchor="end"' in attrs:
+ anchor = "end"
+ elif 'text-anchor="middle"' in attrs:
+ anchor = "middle"
+ w = _tw(s, "bold" in attrs)
+ left_ = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x)
+ labels.append((left_, left_ + w, y, s))
+
+for left_, right_, y, s in labels:
+ assert left_ >= -2 and right_ <= W + 2, f"text iese din panză: {s!r}"
+ assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y:.0f})"
+
+for i in range(len(labels)):
+ li, ri, yi, si = labels[i]
+ for j in range(i + 1, len(labels)):
+ lj, rj, yj, sj = labels[j]
+ if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
+ raise AssertionError(f"texte suprapuse: {si!r} / {sj!r} (y={yi:.0f}/{yj:.0f})")
+
+OUT.write_text(svg, encoding="utf-8")
+
+print(f"serie {len(cds)} candele | M@{M_I} {cds[M_I].c:.2f} (h {cds[M_I].h:.2f}) | "
+ f"trigger@{E_I} {entry:.2f} | SL {sl:.2f} ({(sl-entry)/entry*100:.2f} %) | "
+ f"TP1 {tp1:.2f} (low {cds[TP1_I].l:.2f}) | ieșire@{EXIT_I} {exit_:.2f} "
+ f"({(entry-exit_)/entry*100:.2f} %, {(entry-exit_)/(sl-entry):.2f}R)")
+print(f"scris {OUT} ({W}x{H:.0f}); {len(labels)} etichete verificate, fără suprapuneri")
diff --git a/gen/strategii/mbp_exemplu.svg b/gen/strategii/mbp_exemplu.svg
new file mode 100644
index 0000000..525a83a
--- /dev/null
+++ b/gen/strategii/mbp_exemplu.svg
@@ -0,0 +1,101 @@
+
+
+ SPY — 15 min: MBAP short 25.11.2025 (risc 0.41 % → ieșire 4.9R)
+
+ SL 662.71 (risc 0.41 %)
+
+ intrare short 660.00
+
+ bandă superioară (schematic)
+
+ TP1 657.29 (1:1)
+
+ ieșire trail 646.80 = 2 % = ~4.9R
+
+ SMA-20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ M: candelă verde peste bandă + TICUS ≥ +1000
+ M: valori exacte NEACOPERIT în material (bandă + TICUS); bara ~+1000 din panou e ilustrativă
+ trigger Sell Momentum → intrare short 660.00 (risc 0.41 % = 2.71)
+ TP1 657.29 (1:1) atins în prima candelă după intrare
+ ieșire pe trail 646.80 = 2 % = ~4.9R, după 7 candele de 15 min de la intrare
+
+ Panou TICUS — linia 0 și pragul +1000 (ilustrativ)
+
+ +1000 (prag)
+
+ 0
+
+ ilustrativ
+ candela M
+ Legendă: procentele (risc 0.41 %, ieșire 2 %) sunt ale instructorului.
+ Prețurile absolute (660.00 / 662.71 / 657.29 / 646.80) sunt construite de sumarizator —
+ 09_QA/11_2025-11-25.md:716-719.
+ Structura de reținut: risc mic → TP1 (1:1) atins imediat → trail lung (7 candele de 15 min).
+ Valoarea exactă a benzii și a TICUS la candela M nu e dată în sesiune; bara ~+1000 e ilustrativă.
+ „Taurii urcă pe scări, urșii sar de la etaj" — de ce short-ul se consumă în ore.
+ (09_QA/11_2025-11-25.md:643-648).
+
\ No newline at end of file
diff --git a/gen/strategii/mbp_samecandle.py b/gen/strategii/mbp_samecandle.py
new file mode 100644
index 0000000..712d5c3
--- /dev/null
+++ b/gen/strategii/mbp_samecandle.py
@@ -0,0 +1,204 @@
+"""Diagrama „aceeași candelă" MBP / MBPA++ (marcajul `samecandle`).
+
+Două panouri alături, fiecare cu un grafic de 10 candele de 15 min și un panou
+TICUS dedesubt, legate printr-o linie punctată verticală de la candela M la
+valoarea ei TICUS. Singura diferență dintre panouri: pe care candelă e TICUS
+extrem.
+
+ - Panoul DA: M (roșie, index 5) închide 623.87 sub banda inferioară 623.90
+ ȘI TICUS(M) = -1150 (≤ -1000) — pe EXACT aceeași candelă. M îndeplinit.
+ - Panoul NU: aceeași structură de preț, dar TICUS(M) = -600 (peste prag),
+ iar extremul de -1200 e pe candela m-1, a cărei închidere e peste bandă.
+ M neîndeplinit, deși vizual „pare" același setup.
+
+Regula și cifrele vin din `summaries/05_MODUL_4/03_strategia_6_mbp_mbappe.md`:
+ - regula (bandă și TICUS pe ACEEAȘI candelă) — liniile 179-221, 439-442;
+ - verificarea prin click pe candelă, nu din ochi — liniile 217-221, 436-438;
+ - pragurile ±1000, ±500 doar referință vizuală — liniile 56-64;
+ - M close 623.87 sub banda 623.90 și TICUS(M) -1150 (exemplul real) —
+ liniile 336-342.
+Cifrele din panoul NU (TICUS(M) = -600, TICUS(m-1) = -1200) sunt construite de
+mine ca ilustrare a cazului „aproape, dar nu" (spec, rândurile 27-31).
+Structura candelelor e construită de mine, ca fiecare nivel afirmat să iasă
+exact din serie (assert-urile de mai jos).
+
+Rulează:
+ python3 gen/strategii/mbp_samecandle.py
+Scrie lângă el `mbp_samecandle.svg`.
+"""
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, from_closes # noqa: E402
+
+OUT = Path(__file__).resolve().parent / "mbp_samecandle.svg"
+
+GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
+
+# ---------------------------------------------------------------- seria (10 bare)
+# 5 bare de coborâre până la M (index 5), apoi 4 bare care își revin. Open-ul
+# fiecărei bare = close-ul precedentei. Aceeași serie în ambele panouri.
+CLOSES = [625.35, 625.10, 624.90, 624.65, 624.30, # 0-4 downtrend
+ 623.87, # 5 = M, roșie, sub bandă
+ 623.98, 624.12, 624.28, 624.45] # 6-9 revenire
+WICKS = [(0.08, 0.08), (0.10, 0.10), (0.08, 0.10), (0.10, 0.10), (0.08, 0.12),
+ (0.06, 0.10), # M: high 624.36, low 623.77
+ (0.06, 0.06), (0.08, 0.06), (0.06, 0.06), (0.08, 0.05)]
+assert len(CLOSES) == 10 and len(WICKS) == 10, (len(CLOSES), len(WICKS))
+
+BAND = 623.90
+M = 5
+
+cds = from_closes(CLOSES, wicks=WICKS, start=625.55)
+
+# ---------------------------------------------------------------- assert-urile din spec
+assert abs(cds[M].c - 623.87) < 0.005, cds[M].c # exemplul real din curs
+assert cds[M].c < BAND, cds[M].c # M închide sub banda inferioară
+assert cds[M].c < cds[M].o, (cds[M].o, cds[M].c) # M e roșie
+assert abs(cds[M].l - 623.77) < 0.005, cds[M].l
+assert cds[M].l == min(c.l for c in cds) # M are minimul seriei
+assert cds[M - 1].c > BAND, cds[M - 1].c # candela m-1 închide PESTE bandă
+assert [round(c.c, 2) for c in cds] == CLOSES # close-urile sunt respectate
+
+# TICUS: valoarea pe candelă. DA: doar M, sub prag. NU: extremul pe m-1.
+TICUS_DA = {M: -1150.0}
+TICUS_NU = {M - 1: -1200.0, M: -600.0}
+
+assert TICUS_DA[M] <= -1000, TICUS_DA[M] # DA: ticus(M) under -1000
+assert TICUS_NU[M] > -1000, TICUS_NU[M] # NU: ticus(M) peste prag
+assert TICUS_NU[M - 1] <= -1000, TICUS_NU[M - 1] # NU: extremul e pe m-1
+assert not any(i == M and v <= -1000 for i, v in TICUS_NU.items()), \
+ "NU: nu are voie să existe bar TICUS sub -1000 aliniat cu candela M"
+
+# ---------------------------------------------------------------- construcția panourilor
+SCALE, GAP, BODY, LEFT, W = 85.0, 16, 10, 44, 380
+
+
+def _ref_line(Wv, y, color, dash, text, lab_dy):
+ dash_attr = f' stroke-dasharray="{dash}"' if dash else ""
+ return (f''
+ f'{text}')
+
+
+def build_panel(ticus, labels):
+ """Un panou = grafic de preț (svg_candles) + panou TICUS dedesubt."""
+ ch = Chart(candles=cds,
+ levels=[Level(BAND, "bandă 623.90", BLUE, dash="4 3", label_at="right")],
+ scale=SCALE, gap=GAP, body_w=BODY, left=LEFT, width=W)
+ raw = ch.render()
+ mm = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', raw)
+ Wv, H0 = int(mm.group(1)), float(mm.group(2))
+ inner = raw[raw.index(">") + 1: raw.rindex("")].strip("\n")
+
+ ticus_top = H0 + 30
+ y0 = H0 + 100 # linia 0 a TICUS
+ PX = 44.0 # 1000 unități TICUS = 44 px
+ bar_label_x = ch._cx(M) + 16 # etichetele barelor stau în coloană, la dreapta
+
+ def ty(v):
+ return y0 - v / 1000.0 * PX
+
+ total_h = y0 + 96
+ parts = []
+
+ # linia punctată verticală: aceeași candelă sus și jos (desenată prima, ca
+ # etichetele să vină peste ea)
+ for i, v in sorted(ticus.items()):
+ xb = ch._cx(i)
+ assert -1 <= xb <= Wv + 1, xb
+ parts.append(f'')
+
+ parts.append(f'TICUS')
+ # praguri: 0 (linie plină), ±1000 (punctate), ±500 (fine, doar referință)
+ parts.append(_ref_line(Wv, ty(1000), RED, "4 3", "+1000 (prag)", -5))
+ parts.append(_ref_line(Wv, ty(500), "#caa000", "2 2", "+500", -5))
+ parts.append(_ref_line(Wv, ty(0), "#888", "", "0", 4))
+ parts.append(_ref_line(Wv, ty(-500), "#caa000", "2 2", "-500", 13))
+ parts.append(_ref_line(Wv, ty(-1000), GREEN, "4 3", "-1000 (prag)", 13))
+
+ # barele TICUS (centrate pe x-ul candelei)
+ for i, v in sorted(ticus.items()):
+ xb = ch._cx(i)
+ yv = ty(v)
+ col = RED if v <= -1000 else ORANGE
+ parts.append(f'')
+
+ # eticheta de valoare lângă fiecare bar (aceeași coloană x, rânduri diferite)
+ for i, (txt, dy) in sorted(labels.items()):
+ col = RED if ticus[i] <= -1000 else ORANGE
+ parts.append(f'{txt}')
+
+ # adnotarea candelei M, în zona de preț, sub bară (zona e liberă)
+ parts.append(f''
+ f'M: close 623.87 < bandă 623.90')
+
+ # alinierea TICUS(M) cu candela M: barul se centrează exact pe x-ul candelei
+ xm = ch._cx(M)
+ x_bar_m = ch._cx(M) # centrul barei TICUS a lui M
+ assert abs(x_bar_m - xm) < 1e-9
+ assert ticus[M] <= -1000 if ticus is TICUS_DA else ticus[M] > -1000
+
+ body = inner + "\n " + "\n ".join(parts)
+ # bara TICUS a lui M chiar a fost scrisă cu centrul pe x-ul candelei M
+ assert f' aceleași x-uri ale candelelor în ambele panouri
+assert all(abs(ch_da._cx(i) - ch_nu._cx(i)) < 1e-9 for i in range(len(cds)))
+
+# ---------------------------------------------------------------- compunerea SVG-ului
+TOP = 48
+FOOTER = [
+ "Regula: banda (close sub 623.90) și TICUS ≤ -1000 trebuie pe ACEEAȘI candelă.",
+ "Pragurile TICUS sunt ±1000 (overbought / oversold); ±500 sunt doar referință vizuală.",
+ "Verificarea se face prin click pe candelă, nu din ochi — TICUS(M) se citește exact pe candela M.",
+ "DA: TICUS(M) = -1150 (exemplul real din curs).",
+ "NU: TICUS(M) = -600, TICUS(m-1) = -1200 — cifre construite de mine ca ilustrare.",
+ "Structura candelelor (10 bare de 15 min, aceeași în ambele panouri) e construită de mine.",
+]
+root_h = TOP + PH + 22 + 17 * len(FOOTER) + 8
+
+out = [f'',
+ f'{inner_da}',
+ f'{inner_nu}',
+ f'DA — aceeași candelă, M îndeplinit',
+ f'NU — TICUS extrem pe altă candelă,',
+ f'M neîndeplinit']
+fy = TOP + PH + 22
+for i, line in enumerate(FOOTER):
+ out.append(f'{line}')
+out.append("")
+svg = "\n ".join(out)
+
+# ---------------------------------------------------------------- verificări pe SVG-ul final
+assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
+assert "DA — aceeași candelă, M îndeplinit" in svg
+assert "NU — TICUS extrem pe altă candelă," in svg
+assert "M neîndeplinit" in svg
+assert "click pe candelă, nu din ochi" in svg
+assert svg.count("") == 3, "cele două panouri + rădăcina"
+
+OUT.write_text(svg)
+print(f"scris {OUT.name}: {len(cds)} candele/panou, M close {cds[M].c:.2f} "
+ f"(< {BAND}), TICUS DA {TICUS_DA[M]:.0f} / NU {TICUS_NU[M]:.0f} "
+ f"(extrem {TICUS_NU[M - 1]:.0f} pe m-1), x(M)={ch_da._cx(M):.0f}, "
+ f"înălțime {root_h:.0f}px")
diff --git a/gen/strategii/mbp_samecandle.svg b/gen/strategii/mbp_samecandle.svg
new file mode 100644
index 0000000..fa27641
--- /dev/null
+++ b/gen/strategii/mbp_samecandle.svg
@@ -0,0 +1,78 @@
+
+
+ bandă 623.90
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TICUS
+ +1000 (prag)
+ +500
+ 0
+ -500
+ -1000 (prag)
+
+ M: TICUS -1150 (sub -1000)
+ M: close 623.87 < bandă 623.90
+
+ bandă 623.90
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TICUS
+ +1000 (prag)
+ +500
+ 0
+ -500
+ -1000 (prag)
+
+
+ TICUS(m-1) = -1200 (extrem)
+ TICUS(M) = -600 (nu atinge -1000)
+ M: close 623.87 < bandă 623.90
+ DA — aceeași candelă, M îndeplinit
+ NU — TICUS extrem pe altă candelă,
+ M neîndeplinit
+ Regula: banda (close sub 623.90) și TICUS ≤ -1000 trebuie pe ACEEAȘI candelă.
+ Pragurile TICUS sunt ±1000 (overbought / oversold); ±500 sunt doar referință vizuală.
+ Verificarea se face prin click pe candelă, nu din ochi — TICUS(M) se citește exact pe candela M.
+ DA: TICUS(M) = -1150 (exemplul real din curs).
+ NU: TICUS(M) = -600, TICUS(m-1) = -1200 — cifre construite de mine ca ilustrare.
+ Structura candelelor (10 bare de 15 min, aceeași în ambele panouri) e construită de mine.
+
\ No newline at end of file
diff --git a/gen/strategii/mdsrt_ansamblu.py b/gen/strategii/mdsrt_ansamblu.py
new file mode 100644
index 0000000..4114e65
--- /dev/null
+++ b/gen/strategii/mdsrt_ansamblu.py
@@ -0,0 +1,166 @@
+"""Diagrama de ansamblu MDSRT long (strategii, marcajul `ansamblu`).
+
+„SPY — 5 min: al doilea picior mai jos la 589.80, sub suportul 590.00 →
+divergență bull (M) preț-vs-volum → candela-trigger Buy Hammer/Doji →
+intrare 590.20 → SL 589.40 (risc 0.80) → TP1 591.00 (1:1) → poziția rămasă
+pe trail, ieșire la close 592.40 (≈ 2.75R)."
+
+Toate close-urile, wick-urile speciale și nivelurile vin din
+`gen/strategii/mdsrt_ansamblu.spec.txt`. Structura M→A→P, suportul 590.00,
+minimul 589.80 și riscul 0.80 sunt din exemplul cursului
+(`05_MODUL_4/02_strategia_5_mdsrt.md:159-177, 246, 308-311`). Intrare/SL/TP
+sunt exemplul construit al autorului sumarului, marcat explicit ca NU citat
+din curs (`...:166-177, 439-445`); transcrierea se întrerupe înainte de
+planul numeric. Volumele sunt relative, alese de generator (materialul nu dă
+volum pentru acest exemplu).
+
+Rulează:
+ python3 gen/strategii/mdsrt_ansamblu.py
+Scrie lângă el `mdsrt_ansamblu.svg`.
+"""
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402
+
+OUT = Path(__file__).resolve().parent / "mdsrt_ansamblu.svg"
+
+GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
+
+# ---------------------------------------------------------------- seria (30 bare)
+CLOSES = [
+ 596.00, 595.20, 595.60, 594.80, 594.10, 593.40, 593.70, 592.90, # 0-7
+ 592.20, 591.60, 591.90, 591.30, 590.80, 590.30, 590.55, 590.10, # 8-15
+ 589.85, 590.05, # 16-17
+ 589.80, # 18 = al doilea picior mai jos + candela-trigger (hammer)
+ 590.20, # 19 = intrare (close = entry; sparge high-ul trigger-ului)
+ 590.60, 590.90, 591.00, # 20-22 (22 = TP1 1:1)
+ 591.30, 591.10, 591.45, 591.70, 591.95, 592.15, # 23-28
+ 592.40, # 29 = ieșire pe trail (close)
+]
+assert len(CLOSES) == 30, len(CLOSES)
+
+# fitile (sus, jos): (0.25, 0.30) peste tot, cu excepțiile din spec.
+WICKS = [(0.25, 0.30)] * 30
+WICKS[18] = (0.10, 0.40) # hammer: high 590.15, low 589.40 (= SL)
+WICKS[19] = (0.10, 0.05) # intrare: high 590.30, low 589.75
+# bara 21 coborâtă sub TP1 (high 590.95 < 591.00), ca TP1 să fie atins exact
+# prima dată la bara 22 (altfel assert-ul 5 ar pica pe „primul high").
+WICKS[21] = (0.05, 0.30)
+
+# volum relativ (ales de generator): 16-19 crescător, 18-19 peste medie,
+# vol[18] > vol[10] (higher low pe volum la lower low pe preț).
+VOLS = [
+ 48, 46, 44, 45, 43, 41, 40, 42, 44, 46, # 0-9
+ 52, 38, 40, 42, 39, 44, # 10-15
+ 50, 58, 72, 85, # 16-19 absorbție la minim
+ 70, 64, 58, 60, 55, 52, 50, 48, 46, 44, # 20-29
+]
+assert len(VOLS) == 30, len(VOLS)
+
+cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=596.60)
+
+# ---------------------------------------------------------------- assert-urile din spec
+entry = cds[19].c
+sl = cds[18].l
+tp1 = cds[22].c
+
+# 1. minimul și SL-ul pe bara 18
+assert cds[18].c == 589.80, cds[18].c
+assert cds[18].l == 589.40, cds[18].l
+# 2. al doilea picior rupe suportul/pivotul 590.00
+assert cds[18].l < 590.00, cds[18].l
+# 3. intrarea = close-ul barei 19 și sparge high-ul trigger-ului
+assert cds[19].c == 590.20, cds[19].c
+assert cds[19].c == entry
+assert cds[19].c > cds[18].h, (cds[19].c, cds[18].h)
+# 4. TP1 exact 1:1
+assert abs((entry - sl) - (tp1 - entry)) < 1e-9, (entry - sl, tp1 - entry)
+assert abs((entry - sl) - 0.80) < 1e-9
+# 5. TP1 atins la bara 22 (și nu mai devreme)
+assert cds[22].c == 591.00, cds[22].c
+assert cds[22].c == tp1
+assert max(c.h for c in cds[20:22]) < tp1, [c.h for c in cds[20:22]]
+# 6. ieșirea pe trail e pe profit
+assert cds[29].c == 592.40, cds[29].c
+assert cds[29].c > entry, (cds[29].c, entry)
+# 7. hammer: corp mic, coadă lungă în jos
+body = abs(cds[18].o - cds[18].c)
+rng = cds[18].h - cds[18].l
+tail = min(cds[18].o, cds[18].c) - cds[18].l
+assert body == 0.25, body
+assert body <= 0.5 * rng, (body, rng)
+assert tail > body, (tail, body)
+# 8. downtrend vizibil
+assert cds[13].c < cds[0].c - 5, (cds[13].c, cds[0].c)
+# 9. divergență pe volum: lower low pe preț, higher low pe volum
+assert cds[18].l < cds[10].l, (cds[18].l, cds[10].l)
+assert VOLS[18] > VOLS[10], (VOLS[18], VOLS[10])
+# barele 16-19 au volum crescător; 18-19 peste media seriei
+assert VOLS[16] < VOLS[17] < VOLS[18] < VOLS[19], VOLS[16:20]
+avg_vol = sum(VOLS) / len(VOLS)
+assert VOLS[18] > avg_vol and VOLS[19] > avg_vol, (VOLS[18], VOLS[19], avg_vol)
+# 10. fiecare open == close-ul barei anterioare
+for a, b in zip(cds, cds[1:]):
+ assert a.c == b.o, (a.c, b.o)
+
+r_mult = (cds[29].c - entry) / (entry - sl)
+assert abs(r_mult - 2.75) < 1e-9, r_mult
+
+# ---------------------------------------------------------------- niveluri, marks
+levels = [
+ Level(590.00, "suport / pivot 590.00", BLUE, label_at="left"),
+ Level(sl, f"SL {sl:.2f} — risc 0.80", RED, label_at="left"),
+ # stânga: altfel eticheta din dreapta se calcă pe textul „SMA-20" al mediei
+ Level(tp1, f"TP1 {tp1:.2f} = 1:1", GREEN, label_at="left"),
+]
+
+marks = [
+ Mark(18, f"minim 589.80 — divergență (M)", RED, "below"),
+ # ridicat deasupra vârfurilor candelelor de urcare (altfel textul cade peste
+ # fitilele barelor 20-22) — bara 19 e totuși exact sub el
+ Mark(19, f"intrare {entry:.2f}", GREEN, "above", dy=-46),
+ Mark(29, f"ieșire trail {cds[29].c:.2f}", GREEN, "above"),
+]
+
+# titlul e injectat pe două linii: textul din spec (82 caractere) nu încape
+# pe un singur rând în 640 px fără să fie tăiat la margine
+TITLE = ["SPY — 5 min (exemplu construit pe scara lecției;",
+ "cifrele planului nu vin din curs)"]
+
+ch = Chart(
+ candles=cds, levels=levels, marks=marks,
+ mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")],
+ scale=48.0, gap=15, body_w=9, left=44, width=640, top=64, vol_h=70,
+ vol_label="volum",
+ title="", caption="",
+)
+svg = ch.render()
+title_svg = "\n ".join(
+ f'{t}'
+ for i, t in enumerate(TITLE))
+i = svg.index(">") + 1
+svg = svg[:i] + "\n " + title_svg + svg[i:]
+
+# ---------------------------------------------------------------- legenda (onestitate)
+m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg)
+W, H0 = int(m.group(1)), float(m.group(2))
+legend = [
+ "Legendă: entry/SL/TP/trail sunt exemplul construit al autorului sumarului,",
+ "nu citate din curs (transcrierea se întrerupe înainte de planul numeric).",
+]
+legend_svg = [f'{t}'
+ for i, t in enumerate(legend)]
+new_h = H0 + 16 + 17 * (len(legend) - 1) + 14
+svg = re.sub(r'(viewBox="0 0 \d+ )[0-9.]+(")',
+ lambda mm: mm.group(1) + f"{new_h:.0f}" + mm.group(2), svg, count=1)
+svg = svg.replace("\n", "\n " + "\n ".join(legend_svg) + "\n")
+
+assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
+assert f"TP1 {tp1:.2f}" in svg and f"intrare {entry:.2f}" in svg
+OUT.write_text(svg)
+print(f"scris {OUT.name}: {len(cds)} candele, suport 590.00, minim {cds[18].l:.2f}, "
+ f"intrare {entry:.2f}, SL {sl:.2f} (risc 0.80), TP1 {tp1:.2f} (1:1), "
+ f"ieșire {cds[29].c:.2f} = {r_mult:.2f}R, vol[18]={VOLS[18]} > vol[10]={VOLS[10]}")
diff --git a/gen/strategii/mdsrt_ansamblu.svg b/gen/strategii/mdsrt_ansamblu.svg
new file mode 100644
index 0000000..0718c56
--- /dev/null
+++ b/gen/strategii/mdsrt_ansamblu.svg
@@ -0,0 +1,109 @@
+
+ SPY — 5 min (exemplu construit pe scara lecției;
+ cifrele planului nu vin din curs)
+
+ suport / pivot 590.00
+
+ SL 589.40 — risc 0.80
+
+ TP1 591.00 = 1:1
+
+ SMA-20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ volum
+ minim 589.80 — divergență (M)
+ intrare 590.20
+ ieșire trail 592.40
+ Legendă: entry/SL/TP/trail sunt exemplul construit al autorului sumarului,
+ nu citate din curs (transcrierea se întrerupe înainte de planul numeric).
+
\ No newline at end of file
diff --git a/gen/strategii/mdsrt_exemplu.py b/gen/strategii/mdsrt_exemplu.py
new file mode 100644
index 0000000..8423bf2
--- /dev/null
+++ b/gen/strategii/mdsrt_exemplu.py
@@ -0,0 +1,222 @@
+"""Diagrama din `gen/strategii/mdsrt_exemplu.spec.txt` (TIP: PRET).
+
+Exemplul real din Q&A 15.07.2025 (`09_QA/24_2025-07-15.md`): SPY pe 5 min,
+double bottom cu al doilea picior mai jos (MDSRT pe 5 min, suportul = pivotul
+daily). Downtrend → picior 1 la 622.40 → revenire → picior 2 la 621.40 (rupe
+pivotul 622.00) → Buy Hammer care închide la 622.60, înapoi peste pivot. După
+trigger, un plan construit de mine (marcat) duce trade-ul până la TP1.
+
+Reale din curs: nivelurile 622.40 / 622.00 / 621.40 / 622.60 și cele 8 bare
+dintre picioare. Construcția mea: entry 622.60, SL 621.30, TP1 623.90 (1:1) —
+materialul nu le dă (transcrierea planului se întrerupe). Volumele sunt
+relative, alese de generator.
+
+Rulează:
+ python3 gen/strategii/mdsrt_exemplu.py
+Scrie lângă el `mdsrt_exemplu.svg`.
+"""
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402
+
+OUT = Path(__file__).resolve().parent / "mdsrt_exemplu.svg"
+
+GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
+
+# ── nivelurile reale din curs + planul construit (marcat) ──────────────────
+PIVOT = 622.00 # pivot daily, suportul
+LEG1 = 622.40 # swing low picior 1
+LEG2 = 621.40 # picior 2 (mai jos, rupe pivotul)
+ENTRY = 622.60 # close-ul Buy Hammer (construit)
+SL = 621.30 # 0.10 sub low-ul piciorului 2 (construit)
+TP1 = 623.90 # entry + risc = 1:1 (construit)
+
+# ── seria: 30 de close-uri, exact ca în specificație ───────────────────────
+CLOSES = [
+ 627.00, 626.40, 626.70, 626.00, 625.40, 624.80, 624.40, 623.80, # 0-7
+ 623.20, 622.80, # 8-9
+ 622.40, # 10 = picior 1 (swing low 622.40)
+ 622.70, 623.10, 623.50, 623.20, 622.90, 622.60, 622.30, # 11-17
+ 622.60, # 18 = picior 2: low 621.40, Buy Hammer care închide 622.60
+ 623.00, 623.40, 623.90, # 19-21, 21 = TP1 1:1 (construit)
+ 624.20, 624.00, 624.40, 624.80, 625.20, 625.60, # 22-27
+ 625.30, 625.70, # 28-29
+]
+assert len(CLOSES) == 30, len(CLOSES)
+
+# wick-uri (sus, jos): alternanță naturală; excepțiile sunt piciorul 1 și
+# hammer-ul de la piciorul 2 (cerute exact de specificație).
+WICKS = [
+ (0.10, 0.10), (0.12, 0.14), (0.15, 0.12), (0.18, 0.15), (0.20, 0.16),
+ (0.18, 0.14), (0.16, 0.13), (0.15, 0.12), (0.14, 0.12), (0.12, 0.10),
+ (0.15, 0.05), # 10 = picior 1: low 622.40 - 0.05 = 622.35
+ (0.12, 0.12), (0.15, 0.14), (0.18, 0.15), (0.16, 0.13), (0.14, 0.12),
+ (0.12, 0.10), (0.10, 0.12),
+ (0.10, 0.90), # 18 = hammer: high 622.70, low 621.40
+ (0.12, 0.12), (0.14, 0.12), (0.15, 0.13),
+ (0.16, 0.15), (0.14, 0.12), (0.15, 0.13), (0.16, 0.14), (0.17, 0.14),
+ (0.18, 0.15), (0.14, 0.12), (0.15, 0.13),
+]
+assert len(WICKS) == 30, len(WICKS)
+
+# volum relativ (ales de generator; materialul nu dă volumul pentru exemplu):
+# scade pe downtrend, spike de absorbție pe hammer, higher low vs piciorul 1.
+VOLS = [
+ 58, 56, 54, 52, 50, 49, 48, 47, 46, 45, # 0-9
+ 44, # 10 = picior 1
+ 43, 42, 41, 40, 39, 38, 39, # 11-17
+ 100, # 18 = hammer, maximul seriei
+ 62, 58, 54, # 19-21
+ 52, 50, 49, 48, 47, 46, 45, 44, # 22-29
+]
+assert len(VOLS) == 30, len(VOLS)
+
+cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=627.00)
+
+# ── assert-urile din specificație ──────────────────────────────────────────
+# 1. picior 1: low 622.35, close 622.40
+assert abs(cds[10].l - 622.35) < 1e-9, cds[10].l
+assert cds[10].c == 622.40, cds[10].c
+
+# 2. picior 2 mai jos (621.40 < 622.35) și rupe pivotul daily 622.00
+assert abs(cds[18].l - LEG2) < 1e-9, cds[18].l
+assert cds[18].l < cds[10].l, (cds[18].l, cds[10].l)
+assert cds[18].l < PIVOT, cds[18].l
+
+# 3. trigger-ul închide înapoi peste pivot
+assert cds[18].c == 622.60, cds[18].c
+assert cds[18].c > PIVOT, cds[18].c
+
+# 4. 8 candele între picioare (peste minimul de 5)
+assert 18 - 10 == 8
+
+# 5. hammer: coadă lungă în jos (0.90) > corp (0.30)
+corp = abs(cds[18].o - cds[18].c)
+coada = min(cds[18].o, cds[18].c) - cds[18].l
+assert abs(corp - 0.30) < 1e-9, corp
+assert abs(coada - 0.90) < 1e-9, coada
+assert coada > corp, (coada, corp)
+
+# 6. entry = close-ul trigger-ului (construit), SL 0.10 sub low-ul piciorului 2
+assert cds[18].c == ENTRY
+assert abs(cds[18].l - SL - 0.10) < 1e-9, (cds[18].l, SL)
+risk = ENTRY - SL
+assert abs(risk - 1.30) < 1e-9, risk
+
+# 7. TP1 simetric 1:1
+assert abs((ENTRY - SL) - (TP1 - ENTRY)) < 1e-9
+
+# 8. după trigger, primul close care atinge 623.90 e bara 21 (nu mai devreme).
+# Seria pornește la 627.00, deci „atinge" se citește de jos în sus, din zona
+# picioarelor: barele 10-20 rămân sub TP1, bara 21 îl atinge.
+assert all(c.c < TP1 for c in cds[10:21]), [c.c for c in cds[10:21]]
+assert max(c.h for c in cds[19:21]) < TP1, [c.h for c in cds[19:21]]
+assert cds[21].c == TP1, cds[21].c
+
+# 9. divergență: lower low pe preț, higher low pe volum
+assert cds[18].l < cds[10].l, (cds[18].l, cds[10].l)
+assert VOLS[18] > VOLS[10], (VOLS[18], VOLS[10])
+assert VOLS[18] == max(VOLS)
+
+# 10. fiecare open == close-ul barei anterioare
+for a, b in zip(cds, cds[1:]):
+ assert a.c == b.o, (a.c, b.o)
+
+# ── geometria, ca să așez marks-urile fără suprapunere de text ─────────────
+SCALE, GAP, LEFT, WIDTH, BODY_W, VOL_H = 62, 15, 44, 640, 9, 70
+
+
+def cx(i: int) -> float:
+ return LEFT + i * GAP
+
+
+levels = [
+ Level(PIVOT, "pivot daily 622.00", ORANGE, label_at="left"),
+ Level(SL, "SL 621.30 (construit)", RED, label_at="left"),
+ Level(ENTRY, "intrare 622.60 (construit)", GREEN, label_at="left"),
+ Level(TP1, "TP1 623.90 = 1:1 (construit)", GREEN, label_at="left"),
+ Level(LEG1, "picior 1 = 622.40", BLUE, label_at="right"),
+]
+
+# dy-uri calculate manual ca etichetele „below" să nu calce linia pivotului
+# (bara 10) și ca „hammer" să nu calce eticheta intrării (bara 18).
+marks = [
+ Mark(10, "picior 1 = 622.40", BLUE, "below", dy=15),
+ Mark(18, "picior 2 = 621.40 (mai jos) · Buy Hammer", RED, "below"),
+ Mark(18, "hammer închide 622.60, înapoi peste pivot", GREEN, "above", dy=-20),
+]
+
+ch = Chart(
+ candles=cds, levels=levels, marks=marks,
+ mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")],
+ scale=SCALE, gap=GAP, body_w=BODY_W, left=LEFT, width=WIDTH, vol_h=VOL_H,
+ vol_label="volum",
+ title="SPY — 5 min (Q&A 15.07.2025; intrare/SL/TP construite)",
+ caption="",
+)
+svg = ch.render()
+
+# ── legenda de onestitate (assert 11): ce e real, ce e construit ───────────
+legend = [
+ "Legendă: nivelurile 622.40 / 622.00 / 621.40 / 622.60 și cele 8 candele dintre",
+ "picioare sunt reale, din curs. Intrarea 622.60, SL-ul 621.30 și TP-ul 623.90",
+ "sunt construcția mea — materialul nu le dă.",
+]
+m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg)
+W, H0 = int(m.group(1)), float(m.group(2))
+legend_svg = [f'{t}'
+ for i, t in enumerate(legend)]
+new_h = H0 + 16 + 17 * (len(legend) - 1) + 14
+svg = re.sub(r'(viewBox="0 0 \d+ )[0-9.]+(")',
+ lambda mm: mm.group(1) + f"{new_h:.0f}" + mm.group(2), svg, count=1)
+svg = svg.replace("\n", "\n " + "\n ".join(legend_svg) + "\n")
+
+assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
+assert "sunt reale, din curs" in svg and "construcția mea" in svg, \
+ "legenda nu spune explicit ce e real și ce e construit"
+
+# toate etichetele cerute de specificație sunt prezente
+for eticheta in ("pivot daily 622.00", "picior 1 = 622.40",
+ "picior 2 = 621.40 (mai jos) · Buy Hammer",
+ "hammer închide 622.60, înapoi peste pivot",
+ "intrare 622.60 (construit)", "SL 621.30 (construit)",
+ "TP1 623.90 = 1:1 (construit)"):
+ assert eticheta in svg, f"lipsește eticheta: {eticheta!r}"
+assert "SPY — 5 min" in svg, "lipsește titlul"
+
+# 1:1 desenat la 1:1 (STYLE.md: raportul din preț = raportul din pixeli)
+ys = {p: ch._y(p) for p in (SL, ENTRY, TP1)}
+assert abs((ys[SL] - ys[ENTRY]) - (ys[ENTRY] - ys[TP1])) < 1e-6, ys
+
+# ── verificare: fiecare etichetă e în pânză și nu se suprapune alta ────────
+m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg)
+W, H = int(m.group(1)), float(m.group(2))
+texte = re.findall(r']*)>([^<]*)', svg)
+cutii = []
+for xs, ys, attrs, continut in texte:
+ x, yv = float(xs), float(ys)
+ w = len(continut) * (7.44 if "bold" in attrs else 6.72)
+ if 'text-anchor="end"' in attrs:
+ x0 = x - w
+ elif 'text-anchor="middle"' in attrs:
+ x0 = x - w / 2
+ else:
+ x0 = x
+ assert -2 <= x0 and x0 + w <= W + 2, f"etichetă în afara pânzei: {continut!r}"
+ assert 0 <= yv <= H, f"etichetă în afara înălțimii: {continut!r}"
+ cutii.append((x0, x0 + w, yv, continut))
+for i in range(len(cutii)):
+ for j in range(i + 1, len(cutii)):
+ a, b = cutii[i], cutii[j]
+ if abs(a[2] - b[2]) < 13 and a[0] < b[1] - 2 and b[0] < a[1] - 2:
+ raise AssertionError(f"texte suprapuse: {a[3]!r} / {b[3]!r}")
+
+OUT.write_text(svg, encoding="utf-8")
+print(f"scris {OUT.name}: {len(cds)} candele | picior 1 {cds[10].l:.2f} (close "
+ f"{cds[10].c:.2f}) | picior 2 {cds[18].l:.2f} (mai jos) | hammer închide "
+ f"{cds[18].c:.2f} | intrare {ENTRY:.2f} | SL {SL:.2f} (risc {risk:.2f}) | "
+ f"TP1 {TP1:.2f} = {(TP1 - ENTRY) / risk:.2f}:1 la bara 21 | "
+ f"vol[18]={VOLS[18]} > vol[10]={VOLS[10]}")
diff --git a/gen/strategii/mdsrt_exemplu.svg b/gen/strategii/mdsrt_exemplu.svg
new file mode 100644
index 0000000..5e14419
--- /dev/null
+++ b/gen/strategii/mdsrt_exemplu.svg
@@ -0,0 +1,113 @@
+
+ SPY — 5 min (Q&A 15.07.2025; intrare/SL/TP construite)
+
+ pivot daily 622.00
+
+ SL 621.30 (construit)
+
+ intrare 622.60 (construit)
+
+ TP1 623.90 = 1:1 (construit)
+
+ picior 1 = 622.40
+
+ SMA-20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ volum
+ picior 1 = 622.40
+ picior 2 = 621.40 (mai jos) · Buy Hammer
+ hammer închide 622.60, înapoi peste pivot
+ Legendă: nivelurile 622.40 / 622.00 / 621.40 / 622.60 și cele 8 candele dintre
+ picioare sunt reale, din curs. Intrarea 622.60, SL-ul 621.30 și TP-ul 623.90
+ sunt construcția mea — materialul nu le dă.
+
\ No newline at end of file
diff --git a/gen/strategii/mdsrt_masurare.py b/gen/strategii/mdsrt_masurare.py
new file mode 100644
index 0000000..5e856c3
--- /dev/null
+++ b/gen/strategii/mdsrt_masurare.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python3
+"""Cum se masoara divergenta MDSRT — schema pe doua panouri (pret + indicator).
+
+Pentru `strategii/07_mdsrt.md`, marcajul `` (Nivel 1,
+§1.2 „Setup", dupa pasul 5). NU e grafic de pret cu candele: spec-ul cere o
+schema de regula, cu linii, fara axe de pret reale, fara ticker, fara cifre de
+pret. De aceea se scrie SVG de mana, doar stdlib — nu se folosesc
+svg_candles / pchart.
+
+Ce arata (toate etichetele si pragurile vin din spec / din Q&A):
+- panoul de sus: linia pretului intr-un downtrend; L1 primul swing low
+ important, L2 al doilea, mai jos → lower low;
+- panoul de jos: linia indicatorului (Power / volum / RS) cu I1 si I2, al
+ doilea mai sus → higher low = divergenta bull;
+- bracket orizontal intre L1 si L2: „minim 5 candele intre puncte";
+- bracket orizontal in stanga lui L2, pana la 60 de pasi in spate:
+ „maxim 60 de candele in spate";
+- al treilea swing low mic, la 2-3 pasi de L2, taiat cu cruce rosie si
+ etichetat „2-3 candele → NU e divergenta".
+
+Pragurile, cu sursa:
+- „ultimele 2 swing low-uri importante" + „60 de candele in spate si minim 5
+ candele intre divergente": `09_QA/24_2025-07-15.md:151-155`;
+- „Daca aveti pe 3 candele divergenta, nu e": `09_QA/23_2025-07-29.md:873-874`;
+- mecanismul pret-vs-volum (lower low pe pret, higher low pe indicator):
+ `09_QA/19_2025-09-08.md:168-177`;
+- perechea de low-uri se muta pe masura ce apare un low nou, mai jos:
+ `09_QA/24_2025-07-15.md:159-162`.
+
+Ruleaza: .venv/bin/python gen/strategii/mdsrt_masurare.py
+ -> gen/strategii/mdsrt_masurare.svg
+"""
+from pathlib import Path
+from xml.sax.saxutils import escape
+
+OUT = Path(__file__).with_suffix(".svg")
+
+W, H = 700, 620
+BLACK, GRAY, RED, GREEN, BLUE, ORANGE, WHITE = (
+ "#000000", "#777777", "#c00", "#188a3e", "#1a5fb4", "#b06000", "#ffffff")
+
+# ---- grila de timp: un pas = o candela. L2 e la 60 de pasi de la marginea
+# stanga a panoului, ca bracket-ul „maxim 60 de candele" sa aiba exact 60 de
+# pasi spre stanga si sa acopere si L1.
+X0, STEPW = 60.0, 7.0
+L1_STEP, L2_STEP, L3_STEP = 54.0, 60.0, 62.5
+X_L1, X_L2, X_L3 = X0 + L1_STEP * STEPW, X0 + L2_STEP * STEPW, X0 + L3_STEP * STEPW
+
+# ---- coordonatele verticale ale punctelor
+Y_L1, Y_L2, Y_L3 = 252.0, 278.0, 272.0 # pret: L2 mai jos (lower low)
+Y_I1, Y_I2 = 486.0, 468.0 # indicator: I2 mai sus (higher low)
+
+PRICE_BOX = (40.0, 76.0, 620.0, 250.0) # y 76 .. 326
+IND_BOX = (40.0, 344.0, 620.0, 200.0) # y 344 .. 544
+
+BR60_Y = 134.0 # bracket-ul de 60 de candele, sus
+BR5_Y = 300.0 # bracket-ul de 5 candele, sub cele doua low-uri
+
+TEXTS = [] # (x, y, text, size, weight, anchor) pentru verificarea finala
+
+
+def txt(x, y, s, size=13, weight="400", fill=BLACK, anchor="middle"):
+ TEXTS.append((x, y, s, size, weight, anchor))
+ return (f''
+ f'{escape(s)}')
+
+
+def line(x1, y1, x2, y2, stroke=BLACK, width=1.4, dash=None):
+ d = f' stroke-dasharray="{dash}"' if dash else ""
+ return (f'')
+
+
+def poly(points, stroke=BLACK, width=1.8):
+ pts = " ".join(f"{x:.1f},{y:.1f}" for x, y in points)
+ return (f'')
+
+
+def dot(x, y, color=BLACK, r=3.6):
+ return f''
+
+
+def hbracket(x0, x1, y, color, tick=7.0):
+ """Bracket orizontal de dimensiune: linie + capete verticale."""
+ return [
+ line(x0, y, x1, y, color, 1.4),
+ line(x0, y - tick, x0, y + tick, color, 1.4),
+ line(x1, y - tick, x1, y + tick, color, 1.4),
+ ]
+
+
+def cross(cx, cy, color=RED, arm=10.0):
+ """Crucea rosie care marcheaza al treilea low (NU e divergenta)."""
+ return [
+ line(cx - arm, cy - arm, cx + arm, cy + arm, color, 2.0),
+ line(cx - arm, cy + arm, cx + arm, cy - arm, color, 2.0),
+ ]
+
+
+def build():
+ p = [
+ f'',
+ '',
+ f'',
+ txt(W / 2, 34, "Divergența: cum o măsori corect", 19, "700", BLACK),
+ txt(W / 2, 58, "Aceeași structură pe preț și pe indicator: două swing "
+ "low-uri, două minime", 13, "400", GRAY),
+ ]
+
+ # ---------------- panoul de sus: pretul
+ bx, by, bw, bh = PRICE_BOX
+ p.append(f'')
+ p.append(txt(bx + 16, by + 20, "PREȚ (schematic) — downtrend, lower low",
+ 14, "700", BLACK, "start"))
+
+ # bracket „maxim 60 de candele in spate": de la L2 spre stanga, 60 de pasi
+ x60_left, x60_right = X0, X_L2
+ p.extend(hbracket(x60_left, x60_right, BR60_Y, BLACK))
+ p.append(txt((x60_left + x60_right) / 2, BR60_Y - 12,
+ "maxim 60 de candele în spate", 13, "400", BLACK))
+
+ # linia pretului: downtrend cu L1, apoi L2 mai jos, apoi un low mic L3
+ price = [(0, 158), (6, 178), (12, 166), (18, 192), (24, 180), (30, 206),
+ (36, 194), (42, 218), (48, 232), (L1_STEP, Y_L1), (57, 226),
+ (L2_STEP, Y_L2), (61.3, 248), (L3_STEP, Y_L3), (65, 250)]
+ p.append(poly([(X0 + s * STEPW, y) for s, y in price], BLACK, 1.8))
+ p.extend([dot(X_L1, Y_L1, BLACK), dot(X_L2, Y_L2, BLACK), dot(X_L3, Y_L3, BLACK)])
+ p.append(txt(X_L1 - 10, Y_L1 + 6, "L1", 13, "700", BLACK, "end"))
+ p.append(txt(X_L2 - 12, Y_L2 + 8, "L2", 13, "700", BLACK, "end"))
+
+ # bracket „minim 5 candele intre puncte": intre L1 si L2
+ p.extend(hbracket(X_L1, X_L2, BR5_Y, BLACK))
+ p.append(txt((X_L1 + X_L2) / 2, BR5_Y + 18,
+ "minim 5 candele între puncte", 13, "400", BLACK))
+
+ # al treilea low: cruce rosie + „NU" + eticheta
+ p.extend(cross(X_L3, Y_L3, RED, arm=10.0))
+ p.append(txt(X_L3, 234, "NU", 14, "700", RED))
+ p.append(txt(525, 246, "2-3 candele", 13, "700", RED, "start"))
+ p.append(txt(525, 266, "NU e divergență", 13, "700", RED, "start"))
+
+ # ---------------- panoul de jos: indicatorul
+ bx, by, bw, bh = IND_BOX
+ p.append(f'')
+ p.append(txt(bx + 16, by + 20,
+ "INDICATOR (Power / volum / RS) — higher low",
+ 14, "700", BLUE, "start"))
+
+ ind = [(0, 400), (6, 424), (12, 410), (18, 436), (24, 422), (30, 448),
+ (36, 434), (42, 458), (48, 466), (L1_STEP, Y_I1), (57, 458),
+ (L2_STEP, Y_I2), (65, 440)]
+ p.append(poly([(X0 + s * STEPW, y) for s, y in ind], BLUE, 1.8))
+ p.extend([dot(X_L1, Y_I1, BLUE), dot(X_L2, Y_I2, BLUE)])
+ p.append(txt(X_L1 - 10, Y_I1 + 6, "I1", 13, "700", BLUE, "end"))
+ p.append(txt(X_L2 + 10, Y_I2 + 6, "I2", 13, "700", BLUE, "start"))
+
+ # ---------------- legenda de jos
+ p.append(txt(W / 2, 570, "Prețul face un lower low (L2 sub L1), indicatorul "
+ "face un higher low (I2 peste I1):", 13, "400", BLACK))
+ p.append(txt(W / 2, 590, "divergență bull. Dar dacă al doilea punct e la "
+ "doar 2-3 candele, nu se numără.", 13, "400", BLACK))
+
+ p.append('')
+ return "\n".join(p) + "\n"
+
+
+# ---------------------------------------------------------------- verificare
+def est_w(s, size, weight):
+ factor = 0.62 if weight == "700" else 0.56
+ return len(s) * size * factor
+
+
+def verify(svg):
+ # (1) geometria schemei, exact ce cere spec-ul
+ assert Y_L2 > Y_L1, "L2 trebuie desenat mai jos decat L1 (lower low)"
+ assert Y_I2 < Y_I1, "I2 trebuie desenat mai sus decat I1 (higher low)"
+
+ step_l1_l2 = (X_L2 - X_L1) / STEPW
+ step_l2_l3 = (X_L3 - X_L2) / STEPW
+ assert step_l1_l2 >= 5.0 - 1e-9, f"prea putine candele L1-L2: {step_l1_l2}"
+ assert 2.0 - 1e-9 <= step_l2_l3 <= 3.0 + 1e-9, f"L3 nu e la 2-3 pasi: {step_l2_l3}"
+
+ # (4) bracket-ul de 60 de candele acopera de la L2 spre stanga si trece de L1
+ x60_left, x60_right = X0, X_L2
+ assert x60_right == X_L2, "bracket-ul de 60 porneste din L2"
+ assert x60_left <= X_L1, "bracket-ul de 60 nu ajunge pana la L1"
+ assert x60_right - x60_left >= (X_L2 - X_L1), "bracket prea scurt fata de L1"
+ assert (x60_right - x60_left) / STEPW == 60.0, "nu sunt exact 60 de pasi"
+
+ # (5) „NU" si crucea rosie chiar pe al treilea low
+ assert "NU" in svg
+ assert RED in svg and "L3" not in svg # L3 e marcat doar prin cruce, nu cu eticheta
+ cross_lines = cross(X_L3, Y_L3)
+ for ln in cross_lines:
+ assert f'x1="{X_L3 - 10:.1f}"' in ln or f'x2="{X_L3 - 10:.1f}"' in ln
+ assert f'y1="{Y_L3 - 10:.1f}"' in ln or f'y2="{Y_L3 - 10:.1f}"' in ln
+
+ # etichetele obligatorii din spec sunt in desen
+ for cheie in ("maxim 60 de candele în spate", "minim 5 candele între puncte",
+ "2-3 candele", "NU e divergență", "lower low", "higher low",
+ "L1", "L2", "I1", "I2"):
+ assert cheie in svg, f"lipsește din desen: {cheie!r}"
+
+ # (2) toate etichetele in panza
+ for x, y, s, size, weight, anchor in TEXTS:
+ ew = est_w(s, size, weight)
+ left = x - ew if anchor == "end" else (x - ew / 2 if anchor == "middle" else x)
+ right = left + ew
+ assert -2 <= left and right <= W + 2, f"text in afara panzei: {s!r}"
+ assert 0 <= y <= H, f"text in afara inaltimii: {s!r} (y={y})"
+
+ # (3) fara doua texte pe aceeasi pozitie
+ for i in range(len(TEXTS)):
+ xi, yi, si, szi, wi, ai = TEXTS[i]
+ we = est_w(si, szi, wi)
+ li = xi - we if ai == "end" else (xi - we / 2 if ai == "middle" else xi)
+ ri = li + we
+ for j in range(i + 1, len(TEXTS)):
+ xj, yj, sj, szj, wj, aj = TEXTS[j]
+ wej = est_w(sj, szj, wj)
+ lj = xj - wej if aj == "end" else (xj - wej / 2 if aj == "middle" else xj)
+ rj = lj + wej
+ if abs(yi - yj) < 13 and li < rj - 4 and lj < ri - 4:
+ raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}")
+
+ # etichetele panourilor incap in panouri
+ for x, y, s, size, weight, anchor in TEXTS:
+ for (px, py, pw, ph) in (PRICE_BOX, IND_BOX):
+ if py <= y <= py + ph and px - 2 <= x <= px + pw + 2:
+ assert x >= px + 12, f"eticheta lipita de margine: {s!r}"
+ assert est_w(s, size, weight) <= pw - 24, f"prea lata in panou: {s!r}"
+
+ # fara linie goala in SVG
+ assert "" not in svg.split("\n")[:-1], "SVG-ul are linie goala"
+ assert "\n\n" not in svg, "SVG-ul are linie goala"
+ return True
+
+
+SVG = build()
+assert verify(SVG), "verificare esuata"
+OUT.write_text(SVG, encoding="utf-8")
+print(f"scris {OUT} ({W}x{H}), verificare OK: {len(TEXTS)} etichete, "
+ f"L1-L2={(X_L2 - X_L1) / STEPW:.1f} pasi, "
+ f"L2-L3={(X_L3 - X_L2) / STEPW:.1f} pasi, "
+ f"fereastra 60 = {(X_L2 - X0) / STEPW:.0f} pasi")
diff --git a/gen/strategii/mdsrt_masurare.svg b/gen/strategii/mdsrt_masurare.svg
new file mode 100644
index 0000000..719fcee
--- /dev/null
+++ b/gen/strategii/mdsrt_masurare.svg
@@ -0,0 +1,36 @@
+
+
+
+Divergența: cum o măsori corect
+Aceeași structură pe preț și pe indicator: două swing low-uri, două minime
+
+PREȚ (schematic) — downtrend, lower low
+
+
+
+maxim 60 de candele în spate
+
+
+
+
+L1
+L2
+
+
+
+minim 5 candele între puncte
+
+
+NU
+2-3 candele
+NU e divergență
+
+INDICATOR (Power / volum / RS) — higher low
+
+
+
+I1
+I2
+Prețul face un lower low (L2 sub L1), indicatorul face un higher low (I2 peste I1):
+divergență bull. Dar dacă al doilea punct e la doar 2-3 candele, nu se numără.
+
diff --git a/gen/strategii/mdsrt_trinity.py b/gen/strategii/mdsrt_trinity.py
new file mode 100644
index 0000000..5987e46
--- /dev/null
+++ b/gen/strategii/mdsrt_trinity.py
@@ -0,0 +1,199 @@
+#!/usr/bin/env python3
+"""MDSRT — minimul obligatoriu (M complet) vs. varianta Trinity.
+
+Diagramă conceptuală (nu grafic de preț): pentru
+`summaries/strategii/07_mdsrt.md`, Nivel 2, §2.1 „Ce dă edge", după tabel
+(marcajul ``). Citește specificația
+`gen/strategii/mdsrt_trinity.spec.txt` și scrie
+`gen/strategii/mdsrt_trinity.svg`.
+
+NU folosește `svg_candles`: SVG-ul e construit ca string, fără dependințe.
+
+Conținutul, cu sursa fiecărei afirmații:
+ - Power obligatoriu; dacă nu e în divergență, setup-ul nu se califică:
+ `05_MODUL_4/02_strategia_5_mdsrt.md:59-61, 432-434`.
+ - Minim 2 indicatori: Power + (Volume SAU Relative Strength):
+ `05_MODUL_4/02_strategia_5_mdsrt.md:85-88`.
+ - Trinity = toți trei în divergență simultan, „setup de 5 stele":
+ `05_MODUL_4/02_strategia_5_mdsrt.md:6-8, 34-42, 87-88`.
+ - Sensul din selecția simbolului (aliniere în cadranul 1; trei divergențe
+ simultane NU sunt Trinity): `08_ANALIZA_ZILNICA/11_selectia_simbolului.md:612-627`.
+
+Rulează: .venv/bin/python gen/strategii/mdsrt_trinity.py
+"""
+import html
+from pathlib import Path
+
+W, H = 700, 550
+
+BLACK = "#000000"
+GRAY = "#777777"
+RED = "#c00"
+GREEN = "#188a3e"
+BLUE = "#1a5fb4"
+
+texts = [] # (x, y, text, size, weight, fill, w_est)
+boxes = [] # dict-uri: doar casetele, pentru numărătoarea din assert-uri
+
+
+def add(x, y, s, size=13, weight="normal", fill=BLACK):
+ factor = 0.62 if weight == "bold" else 0.56
+ w = len(s) * size * factor
+ texts.append((x, y, s, size, weight, fill, w))
+ return (x, y, s, size, weight, fill, w)
+
+
+def box(x, y, w, h, *, fill="#ffffff", stroke=GRAY, sw=1.5, dash=None, row=None):
+ b = {"x": x, "y": y, "w": w, "h": h, "row": row, "lines": []}
+ boxes.append(b)
+ return b
+
+
+def box_line(b, dx, dy, s, size=14, weight="normal", fill=BLACK):
+ cx = b["x"] + b["w"] / 2.0 + dx if dx is not None else b["x"] + b["w"] / 2.0
+ add(cx, b["y"] + dy, s, size, weight, fill)
+ b["lines"].append(s)
+
+
+parts = []
+
+
+def rect(b, fill, stroke, sw=1.5, dash=None):
+ d = f' stroke-dasharray="{dash}"' if dash else ""
+ return (f'')
+
+
+# --------------------------------------------------------------- titlu
+add(W / 2, 34, "MDSRT — minimul obligatoriu vs. Trinity", 20, "bold")
+add(W / 2, 58,
+ "Minim Power + unul din ceilalți e obligatoriu; toți trei în divergență = Trinity.",
+ 13, "normal", GRAY)
+
+# ------------------------------------------------- rând 1 — minim obligatoriu
+add(W / 2, 100, "Minimul obligatoriu (M complet)", 16, "bold")
+
+B1 = box(40, 112, 250, 56, row=1)
+B2 = box(310, 112, 350, 56, row=1)
+parts.append(rect(B1, GREEN, GREEN))
+parts.append(rect(B2, "#ffffff", GREEN, sw=2, dash="5 3"))
+
+box_line(B1, None, 31, "Power (obligatoriu)", 15, "bold", "#ffffff")
+box_line(B2, None, 22, "Volume SAU Relative Strength", 14, "normal", GREEN)
+box_line(B2, None, 44, "— oricare una", 13, "normal", GREEN)
+
+add(W / 2, 196, "Power lipsește → setup-ul NU se califică", 14, "bold", RED)
+
+# ------------------------------------------------- rând 2 — Trinity (opțional)
+add(W / 2, 250, "Trinity (opțional, semnal mai puternic)", 16, "bold")
+
+TITLES = ["Power (obligatoriu)", "Volume", "Relative Strength"]
+B3 = box(30, 262, 200, 76, row=2)
+B4 = box(250, 262, 200, 76, row=2)
+B5 = box(470, 262, 200, 76, row=2)
+for b in (B3, B4, B5):
+ parts.append(rect(b, GREEN, GREEN))
+for b, t in zip((B3, B4, B5), TITLES):
+ box_line(b, None, 34, t, 14, "bold", "#ffffff")
+ box_line(b, None, 56, "divergență", 14, "bold", "#ffffff")
+
+add(W / 2, 360, "toți trei în divergență simultan, pe aceeași zonă de preț",
+ 14, "normal", BLACK)
+add(W / 2, 386, "comparat în curs cu un «setup de 5 stele»", 14, "bold", GREEN)
+
+# --------------------------------------------------------------- subsol
+parts.append(f'')
+add(W / 2, 448,
+ "La MDSRT, „Trinity” se folosește local cu sensul „toți trei în divergență”.",
+ 13, "normal", GRAY)
+add(W / 2, 468,
+ "Lecția de selecție dă aceluiași cuvânt alt sens:", 13, "normal", GRAY)
+add(W / 2, 486,
+ "aliniere în cadranul 1, nu divergență (contradicția 2.4).",
+ 13, "normal", GRAY)
+add(W / 2, 512,
+ "Sursa: 05_MODUL_4/02_strategia_5_mdsrt.md:6-8, 34-42, 59-61, 85-88;",
+ 13, "normal", GRAY)
+add(W / 2, 530,
+ "08_ANALIZA_ZILNICA/11_selectia_simbolului.md:612-627.",
+ 13, "normal", GRAY)
+
+# --------------------------------------------------------------- emitere
+head = (f'')
+parts.insert(0, f'')
+parts.insert(0, head)
+
+body = []
+for x, y, s, size, weight, fill, _ in texts:
+ body.append(
+ f''
+ f'{html.escape(s, quote=False)}')
+svg = "\n".join(parts + body + [""]) + "\n"
+
+# =============================================================== VERIFICARE
+# 1. Rândul 1 conține exact 2 casete (Power + „Volume SAU Relative Strength"),
+# nu 3.
+row1 = [b for b in boxes if b["row"] == 1]
+assert len(row1) == 2, f"rândul 1 are {len(row1)} casete, trebuie exact 2"
+
+# 2. Rândul 2 conține exact 3 casete (Power, Volume, Relative Strength).
+row2 = [b for b in boxes if b["row"] == 2]
+assert len(row2) == 3, f"rândul 2 are {len(row2)} casete, trebuie exact 3"
+assert [b["lines"][0] for b in row2] == TITLES, "titlurile din rândul 2 diferă"
+
+# 3. Textul „Power (obligatoriu)" apare în ambele rânduri.
+r1_labels = [s for b in row1 for s in b["lines"]]
+r2_labels = [s for b in row2 for s in b["lines"]]
+assert "Power (obligatoriu)" in r1_labels, "lipsește din rândul 1"
+assert "Power (obligatoriu)" in r2_labels, "lipsește din rândul 2"
+assert svg.count(">Power (obligatoriu)") == 2, \
+ "„Power (obligatoriu)” nu apare exact de două ori"
+
+# 4. Eticheta „Power lipsește → setup-ul NU se califică" e lângă rândul 1.
+assert "Power lipsește → setup-ul NU se califică" in svg, "lipsește eticheta rând 1"
+
+# 5. Nota de subsol despre cele două sensuri ale lui „Trinity" e prezentă.
+assert "se folosește local cu sensul" in svg and "alt sens" in svg \
+ and "cadranul 1, nu divergență" in svg and "contradicția 2.4" in svg, \
+ "lipsește nota despre cele două sensuri ale lui Trinity"
+
+# stil: fără umbre/gradienti, text >= 13px, totul în panză, fără suprapuneri
+assert "filter=" not in svg and "gradient" not in svg, "fără umbre/gradienti"
+assert 'font-family="system-ui, sans-serif"' in svg, "lipsește fontul cerut"
+
+rects_collide = []
+for x, y, s, size, weight, fill, w in texts:
+ assert size >= 13, f"font sub 13px: {s!r} ({size})"
+ left, right = x - w / 2.0, x + w / 2.0
+ top, bottom = y - size * 0.78, y + size * 0.22
+ assert 0 <= left and right <= W, f"etichetă în afara panzei (orizontal): {s!r}"
+ assert 0 <= top and bottom <= H, f"etichetă în afara panzei (vertical): {s!r}"
+ rects_collide.append((left, right, top, bottom, s))
+
+for i in range(len(rects_collide)):
+ for j in range(i + 1, len(rects_collide)):
+ l1, r1, t1, b1, s1 = rects_collide[i]
+ l2, r2, t2, b2, s2 = rects_collide[j]
+ if r1 > l2 and r2 > l1 and b1 > t2 and b2 > t1:
+ raise AssertionError(f"etichete suprapuse: {s1!r} / {s2!r}")
+
+for required in ("Minimul obligatoriu (M complet)",
+ "Volume SAU Relative Strength",
+ "oricare una",
+ "Trinity (opțional, semnal mai puternic)",
+ "divergență",
+ "pe aceeași zonă de preț",
+ "setup de 5 stele"):
+ assert required in svg, f"lipsește din desen: {required!r}"
+
+OUT = Path(__file__).with_suffix(".svg")
+OUT.write_text(svg, encoding="utf-8")
+print(f"scris {OUT} ({len(svg)} bytes, {len(texts)} etichete, "
+ f"{len(boxes)} casete, {W}x{H}), verificare OK")
diff --git a/gen/strategii/mdsrt_trinity.svg b/gen/strategii/mdsrt_trinity.svg
new file mode 100644
index 0000000..5268a12
--- /dev/null
+++ b/gen/strategii/mdsrt_trinity.svg
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+MDSRT — minimul obligatoriu vs. Trinity
+Minim Power + unul din ceilalți e obligatoriu; toți trei în divergență = Trinity.
+Minimul obligatoriu (M complet)
+Power (obligatoriu)
+Volume SAU Relative Strength
+— oricare una
+Power lipsește → setup-ul NU se califică
+Trinity (opțional, semnal mai puternic)
+Power (obligatoriu)
+divergență
+Volume
+divergență
+Relative Strength
+divergență
+toți trei în divergență simultan, pe aceeași zonă de preț
+comparat în curs cu un «setup de 5 stele»
+La MDSRT, „Trinity” se folosește local cu sensul „toți trei în divergență”.
+Lecția de selecție dă aceluiași cuvânt alt sens:
+aliniere în cadranul 1, nu divergență (contradicția 2.4).
+Sursa: 05_MODUL_4/02_strategia_5_mdsrt.md:6-8, 34-42, 59-61, 85-88;
+08_ANALIZA_ZILNICA/11_selectia_simbolului.md:612-627.
+
diff --git a/gen/strategii/mp2_ansamblu.py b/gen/strategii/mp2_ansamblu.py
new file mode 100644
index 0000000..aca5334
--- /dev/null
+++ b/gen/strategii/mp2_ansamblu.py
@@ -0,0 +1,139 @@
+"""Diagrama de ansamblu MP² — bottom major NVDA (strategii/02_mp2.md, Nivel 0).
+
+„NVDA — weekly (pre-split 1:10)": downtrend masiv → bază/bottom oct. 2022 →
+crossing-ul pivotului weekly (mic) peste cel monthly (mare) → candela de
+trigger care închide peste pivotul mic (M+A) → intrare 13.80 → SL la swing
+low-ul absolut 10.63 (−23 %) → urcare spre TP 17.25 (+25 %, ~9 săptămâni).
+
+Cifrele (13.80 / 10.63 / 17.25 / −23 % / +25 %) sunt din
+`summaries/04_MODUL_3/04_strategia_2_mp_patrat.md:341-357,435`.
+Traseul de preț și cele două linii de pivot sunt schematice: cursul nu dă
+valorile pivoților, doar faptul că micul trece deasupra marelui la crossing
+(`...:326-335`).
+
+Rulează:
+ python3 gen/strategii/mp2_ansamblu.py
+Scrie lângă el `mp2_ansamblu.svg`.
+"""
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, from_closes # noqa: E402
+
+OUT = Path(__file__).resolve().parent / "mp2_ansamblu.svg"
+
+GRAY, RED, GREEN, BLUE = "#777", "#c00", "#188a3e", "#1a5fb4"
+
+# ------------------------------------------------------------------ seria (38 bare)
+# 16 bare de downtrend 32.00 -> 11.40 (minimul absolut 10.63 e fitilul barei 15),
+# 10 bare de bază/recuperare 11.80 -> 13.80 (bara 25 = intrarea, după M+A),
+# 12 bare de urcare 14.10 -> 17.25 (ținta). open-ul = close-ul precedentului.
+DOWN = [32.00, 30.80, 29.60, 28.70, 27.40, 26.50, 25.30, 24.20, 23.60,
+ 22.30, 21.10, 19.90, 18.50, 16.90, 15.20, 11.40] # 0-15
+BASE = [11.80, 12.10, 12.35, 12.20, 12.55, 12.85, 13.05, 13.35, 13.55,
+ 13.80] # 16-25
+UP = [14.10, 14.45, 14.80, 15.10, 15.45, 15.80, 16.10, 15.90, 16.30,
+ 16.60, 16.95, 17.25] # 26-37
+CLOSES = DOWN + BASE + UP
+assert len(CLOSES) == 38, len(CLOSES)
+
+CAPIT, ENTRY, LAST = 15, 25, 37
+
+# fitile (sus, jos) per bară; bara 15 are fitilul de jos până la minimul absolut
+WICKS = ([(0.35, 0.35)] * 14
+ + [(0.30, 0.30)] # 14
+ + [(0.20, 0.77)] # 15: low = 11.40 - 0.77 = 10.63
+ + [(0.30, 0.30)] * 9 # 16-24
+ + [(0.25, 0.20)] # 25: candela de intrare
+ + [(0.30, 0.30)] * 12) # 26-37
+assert len(WICKS) == len(CLOSES)
+
+VOLS = ([52, 55, 50, 58, 54, 49, 56, 52, 60, 57, 63, 68, 72, 80, 90, 120] # 0-15
+ + [100, 72, 60, 55, 58, 62, 66, 70, 78, 95] # 16-25
+ + [88, 80, 74, 70, 68, 72, 76, 70, 74, 78, 84, 92]) # 26-37
+assert len(VOLS) == len(CLOSES)
+
+cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=32.60)
+
+# ------------------------------------------------- liniile de pivot (trepte)
+# Stepped, schematice (cursul nu dă valorile): monthly „mare" se recalculează
+# rar, weekly „mic" mai des. Crossing: micul trece deasupra marelui exact pe
+# candela de intrare.
+PMARE = [31.00] * 10 + [21.00] * 10 + [13.55] * 8 + [13.20] * 10 # monthly
+PMIC = ([30.50] * 5 + [29.00] * 5 + [20.00] * 4 + [17.00] * 4
+ + [12.60] * 3 + [12.95] * 2 + [13.20] * 2 + [13.70] * 3
+ + [14.20] * 4 + [14.60] * 6) # weekly
+assert len(PMARE) == len(PMIC) == len(CLOSES)
+
+# ---------------------------------------------------------- assert-urile din spec
+entry = cds[ENTRY].c
+sl, tp = 10.63, cds[LAST].c
+
+assert abs(entry - 13.80) < 0.05 # close-ul intrării == 13.80
+assert abs(sl - 10.63) < 0.05 and sl < entry # SL = swing low absolut, sub entry
+assert abs(tp - 17.25) < 0.10 and tp > entry # TP = 17.25, peste entry
+# spec: `abs((entry/sl - 1)*100 - 23)`; formula din spec e inversată (dă +29.8,
+# nu −23). Pierderea corectă din entry până la SL e (sl/entry - 1), deci o scriu așa:
+loss_pct = (sl / entry - 1) * 100
+assert abs(loss_pct + 23) < 1.5, loss_pct # SL ≈ −23 %
+assert abs((tp / entry - 1) * 100 - 25) < 1.5 # TP ≈ +25 %
+assert abs(min(c.l for c in cds) - 10.63) < 0.01 # minimul absolut al seriei = SL
+assert min(c.l for c in cds[-5:-1]) > 10.63 # stopul de 4 bare ar fi mult mai sus
+# crossing-ul pivoților, înainte/după candela de intrare
+assert all(PMIC[i] < PMARE[i] for i in range(ENTRY - 4, ENTRY)), "mic < mare înainte de intrare"
+assert all(PMIC[i] > PMARE[i] for i in range(ENTRY, ENTRY + 4)), "mic > mare după intrare"
+assert cds[ENTRY].c > PMIC[ENTRY], "candela de trigger nu închide peste pivotul mic"
+assert abs(cds[CAPIT].l - sl) < 1e-9, "fitilul barei 15 nu atinge exact SL-ul"
+
+levels = [
+ Level(13.80, "entry ~13.80 (după M+A)", GREEN, label_at="left"),
+ Level(sl, "SL swing low absolut 10.63, −23%", RED, label_at="left"),
+ Level(tp, "TP ~+25% (17.25), ~9 săpt.", BLUE, label_at="left"),
+]
+marks = [
+ Mark(CAPIT, f"minim absolut {sl:.2f}", RED, "below", dy=-6),
+ Mark(ENTRY, "M+A", BLUE, "above"),
+]
+
+ch = Chart(candles=cds, levels=levels, marks=marks,
+ scale=17.0, gap=15, body_w=9, left=44, width=720, top=64, vol_h=70,
+ vol_label="volum", title="NVDA — weekly (pre-split 1:10)",
+ caption="Bottom oct. 2022: entry ~13.80, −23%, ~+25% în ~9 săpt. "
+ "Traseu și pivoți construiți (cursul nu dă valorile lor).")
+
+svg = ch.render()
+
+
+def step_points(vals: list[float]) -> str:
+ """Polilinie în trepte: orizontal pe fiecare palier, vertical la schimbare."""
+ pts: list[tuple[float, float]] = []
+ for i, v in enumerate(vals):
+ x, y = ch._cx(i), ch._y(v)
+ if i == 0:
+ pts.append((x, y))
+ elif v != vals[i - 1]:
+ pts.append((x, ch._y(vals[i - 1]))) # orizontal până la bara nouă
+ pts.append((x, y)) # salt vertical la nivelul nou
+ else:
+ pts.append((x, y))
+ return " ".join(f"{x:.1f},{y:.1f}" for x, y in pts)
+
+
+# injectate imediat după tag-ul de deschidere => desenate în spatele candelelor
+inject = [
+ f'',
+ f'',
+ f''
+ f'pivot weekly (mic)',
+ f''
+ f'pivot monthly (mare)',
+]
+i = svg.index(">") + 1
+svg = svg[:i] + "\n " + "\n ".join(inject) + svg[i:]
+
+assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
+OUT.write_text(svg)
+print(f"scris {OUT.name}: {len(cds)} candele, entry {entry:.2f}, "
+ f"SL {sl:.2f} ({loss_pct:+.1f}%), TP {tp:.2f} "
+ f"({(tp/entry - 1) * 100:+.1f}%), crossing la bara {ENTRY}")
diff --git a/gen/strategii/mp2_ansamblu.svg b/gen/strategii/mp2_ansamblu.svg
new file mode 100644
index 0000000..e602d3d
--- /dev/null
+++ b/gen/strategii/mp2_ansamblu.svg
@@ -0,0 +1,132 @@
+
+
+
+ pivot weekly (mic)
+ pivot monthly (mare)
+ NVDA — weekly (pre-split 1:10)
+
+ entry ~13.80 (după M+A)
+
+ SL swing low absolut 10.63, −23%
+
+ TP ~+25% (17.25), ~9 săpt.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ volum
+ minim absolut 10.63
+ M+A
+ Bottom oct. 2022: entry ~13.80, −23%, ~+25% în ~9 săpt. Traseu și pivoți construiți (cursul nu dă valorile lor).
+
\ No newline at end of file
diff --git a/gen/strategii/mp2_aplus.py b/gen/strategii/mp2_aplus.py
new file mode 100644
index 0000000..e1fa0c1
--- /dev/null
+++ b/gen/strategii/mp2_aplus.py
@@ -0,0 +1,272 @@
+"""MP2 crossing long — setup A+ vs. setup mediocru (două panouri).
+
+Citește `gen/strategii/mp2_aplus.spec.txt` (TIP: PRET).
+
+Panoul stânga (A+): SPY, aprilie — downtrend spre suportul quarterly 554.00,
+bază cu Inverted Head & Shoulders (umăr stâng 566, head 555, umăr drept 568,
+neckline 578.00, adică și un double bottom la ~554.5), intrare 577.00 pe prima
+candelă verde după umărul drept, SL 554.40 chiar peste suport, 1R = 22.60,
+TP1 599.60 (1:1).
+
+Panoul dreapta (Mediocru): integral construit (cifre rotunde), NU din
+material — range 100.00–110.00, crossing în mijlocul range-ului la 105.00
+fără niciun pivot/suport dedesubte, intrare 105.00, SL 103.00, TP1 107.00.
+Eticheta „exemplu construit, nu date reale" e cerută explicit de spec.
+
+Două `Chart`-uri randate separat, lipite într-un SVG wrapper: fiecare panou
+are scara lui, deci coordonatele rămân corecte prin construcție.
+
+Rulează: python3 gen/strategii/mp2_aplus.py
+Scrie: gen/strategii/mp2_aplus.svg
+"""
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Candle, Chart, Level, Mark, from_closes # noqa: E402
+
+OUT = Path(__file__).resolve().parent / "mp2_aplus.svg"
+
+GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
+
+# ============================================================ PANOUL A+ (33 bare)
+# 12 bare de cădere, 10 bare de bază (IHS), candela de intrare, 10 bare de urcare.
+ENTRY_A = 22 # prima candelă verde după umărul drept
+LS_A, HEAD_A, RS_A = 14, 17, 20 # umăr stâng / head / umăr drept
+SUP_A, SL_A, NECK_A, TP1_A = 554.00, 554.40, 578.00, 599.60
+
+CLOSES_A = [
+ 596.0, 592.5, 589.0, 585.5, 582.5, 579.5, 576.5, 573.5, 571.0, 568.5, 566.5, 564.0, # 0-11 cădere
+ 561.0, 559.5, # 12-13
+ 566.0, # 14 umăr stâng (566)
+ 561.5, # 15
+ 555.8, 555.0, # 16-17 double bottom + head (555)
+ 558.5, 563.0, # 18-19
+ 568.0, # 20 umăr drept (568)
+ 566.5, # 21 pullback
+ 577.0, # 22 INTRARE 577.00
+ 580.5, 583.5, 585.0, 583.5, 587.0, 590.0, 592.0, 594.0, 596.0, 597.5, # 23-32 urcare
+]
+assert len(CLOSES_A) == 33, len(CLOSES_A)
+
+WICKS_A = [(0.30, 0.30)] * 33
+WICKS_A[LS_A] = (0.00, 0.30) # high exact 566.00
+WICKS_A[16] = (0.30, 1.20) # low 554.60 (primul picior al double bottom)
+WICKS_A[HEAD_A] = (0.30, 0.55) # low 554.45, deasupra suportului 554.00
+WICKS_A[RS_A] = (0.00, 0.30) # high exact 568.00
+WICKS_A[ENTRY_A] = (0.25, 0.25)
+
+VOLS_A = [58, 56, 54, 52, 50, 48, 47, 45, 44, 42, 41, 40,
+ 38, 44, 52, 46, 61, 66, 58, 54, 62, 50,
+ 88,
+ 74, 70, 66, 61, 68, 63, 59, 57, 55, 60]
+
+cds_a = from_closes(CLOSES_A, wicks=WICKS_A, vols=VOLS_A, start=598.0)
+
+# ============================================================ PANOUL MEDIOCRU (28 bare)
+# Integral construit. Range 100.00–110.00, crossing în mijloc la 105.00.
+ENTRY_M = 7
+SL_M, TP1_M = 103.00, 107.00
+RANGE_LO, RANGE_HI = 100.00, 110.00
+
+CLOSES_M = [
+ 100.5, 101.1, 101.8, 102.5, 103.2, 103.9, 104.6, # 0-6 urcare spre 105
+ 105.0, # 7 crossing / intrare 105.00
+ 105.8, 106.5, 107.0, 106.2, 104.8, 103.4, 102.0, # 8-14 oscilație în range
+ 103.0, 104.5, 106.0, 107.5, 108.8, 109.5, 108.6, # 15-21
+ 107.2, 105.8, 104.4, 103.2, 104.5, 106.0, # 22-27
+]
+assert len(CLOSES_M) == 28, len(CLOSES_M)
+
+WICKS_M = [(0.30, 0.30)] * 28
+WICKS_M[ENTRY_M] = (0.30, 0.20)
+WICKS_M[21] = (0.30, 0.30) # vârf 109.80
+
+VOLS_M = [42, 44, 46, 48, 50, 52, 55, 72,
+ 64, 60, 58, 56, 60, 54, 52,
+ 50, 53, 56, 58, 62, 60, 58,
+ 55, 52, 50, 48, 51, 54]
+
+cds_m = from_closes(CLOSES_M, wicks=WICKS_M, vols=VOLS_M, start=100.5)
+
+# ============================================================ assert-urile din spec
+entry_a = cds_a[ENTRY_A].c
+# A+: intrare 577.00
+assert abs(entry_a - 577.00) < 0.05, f"intrare A+ = {entry_a}, aștept 577.00"
+# A+: SL 554.40, peste suportul quarterly 554.00 (nu sub el)
+assert abs(SL_A - 554.40) < 0.05 and SL_A > SUP_A, f"SL A+ {SL_A} vs suport {SUP_A}"
+# A+: 1R = 22.60
+r_a = entry_a - SL_A
+assert abs(r_a - 22.60) < 0.05, f"1R A+ = {r_a}, aștept 22.60"
+# A+: head 555 < umărul stâng 566 și < umărul drept 568
+head_a, ls_a, rs_a = cds_a[HEAD_A].c, cds_a[LS_A].h, cds_a[RS_A].h
+assert head_a < ls_a and head_a < rs_a, f"head {head_a} vs umeri {ls_a}/{rs_a}"
+# A+: TP1 = entry + 1R = 599.60
+assert abs(TP1_A - (entry_a + r_a)) < 0.05, f"TP1 A+ {TP1_A}, entry+1R {entry_a + r_a:.2f}"
+# suplimentar: structura promisă de spec
+assert cds_a[HEAD_A].l > SUP_A, f"low head {cds_a[HEAD_A].l} a spart suportul {SUP_A}"
+assert cds_a[HEAD_A].l == min(c.l for c in cds_a), "head-ul nu e minimul seriei"
+assert CLOSES_A[0] - CLOSES_A[HEAD_A] > 35, "downtrendul A+ e prea mic"
+assert entry_a > cds_a[RS_A].c, "intrarea nu e deasupra umărului drept"
+
+# Mediocru: intrare 105.00, SL 103.00, TP1 107.00
+entry_m = cds_m[ENTRY_M].c
+assert abs(entry_m - 105.00) < 0.05, f"intrare mediocru = {entry_m}, aștept 105.00"
+assert abs(SL_M - 103.00) < 0.05 and abs(TP1_M - 107.00) < 0.05, "SL/TP mediocru greșite"
+# Mediocru: niciun nivel de suport între SL 103.00 și intrarea 105.00
+levels_m = [RANGE_LO, RANGE_HI, entry_m, SL_M, TP1_M]
+assert not any(SL_M < p < entry_m for p in levels_m), \
+ f"există suport între {SL_M} și {entry_m}: {levels_m}"
+# Mediocru: oscilația stă în range
+assert all(RANGE_LO <= c.c <= RANGE_HI for c in cds_m), "bare în afara range-ului"
+assert min(c.l for c in cds_m) >= RANGE_LO, "low sub range"
+
+print(f"A+ : intrare {entry_a:.2f} SL {SL_A:.2f} 1R {r_a:.2f} "
+ f"head {head_a:.0f} < umeri {ls_a:.0f}/{rs_a:.0f} TP1 {TP1_A:.2f}")
+print(f"MED: intrare {entry_m:.2f} SL {SL_M:.2f} TP1 {TP1_M:.2f} "
+ f"niveluri între SL și intrare: {[p for p in levels_m if SL_M < p < entry_m]}")
+
+# ============================================================ desenele celor două panouri
+PW = 560
+ch_a = Chart(
+ candles=cds_a,
+ levels=[Level(SUP_A, f"suport quarterly {SUP_A:.2f}", GRAY, label_at="left"),
+ Level(SL_A, f"SL {SL_A:.2f}", RED, label_at="right"),
+ Level(entry_a, f"intrare {entry_a:.2f}", GREEN, label_at="left"),
+ Level(NECK_A, f"neckline {NECK_A:.2f}", ORANGE, label_at="right"),
+ Level(TP1_A, f"TP1 {TP1_A:.2f} = 1:1", BLUE, label_at="right")],
+ marks=[Mark(LS_A, f"umăr stâng {ls_a:.0f}", GRAY, "above", dy=-4),
+ Mark(HEAD_A, f"head {head_a:.0f} / double bottom", RED, "below", dy=8),
+ Mark(RS_A, f"umăr drept {rs_a:.0f}", GRAY, "above", dy=-4),
+ Mark(ENTRY_A, "crossing", GREEN, "above", dy=-6)],
+ scale=7.5, gap=15, body_w=9, left=44, width=PW, vol_h=64,
+ vol_label="volum", title="A+ — crossing la confluență (SPY, aprilie)",
+ trend="down",
+ caption="Confluență: suport 554.00 + double bottom 554.45 + neckline 578.00",
+)
+ch_m = Chart(
+ candles=cds_m,
+ levels=[Level(RANGE_HI, f"range {RANGE_HI:.2f}", GRAY, label_at="right"),
+ Level(RANGE_LO, f"range {RANGE_LO:.2f}", GRAY, label_at="left"),
+ Level(SL_M, f"SL {SL_M:.2f}", RED, label_at="right"),
+ Level(entry_m, f"intrare {entry_m:.2f}", GREEN, label_at="right"),
+ Level(TP1_M, f"TP1 {TP1_M:.2f}", BLUE, label_at="right")],
+ marks=[Mark(ENTRY_M, "crossing (mijloc)", GREEN, "above", dy=-4)],
+ scale=34.0, gap=15, body_w=9, left=44, width=PW, vol_h=64,
+ vol_label="volum", title="Mediocru — crossing în mijlocul range-ului (construit)",
+ caption="exemplu construit, nu date reale — SL nu stă pe niciun pivot",
+)
+
+# ============================================================ wrapper: două panouri alăturate
+def inner(svg: str) -> str:
+ """Conținutul dintre tag-urile ale unui panou, fără elementul rădăcină."""
+ return svg[svg.index(">") + 1:svg.rindex("")]
+
+
+def vb_height(svg: str) -> float:
+ m = re.search(r'viewBox="0 0 [\d.]+ ([\d.]+)"', svg)
+ assert m, "nu găsesc viewBox în panoul randat"
+ return float(m.group(1))
+
+
+PANEL_GAP, PAD = 24, 8
+TITLE_H, FOOTER_H = 30, 62
+svg_a, svg_m = ch_a.render(), ch_m.render()
+HA, HM = vb_height(svg_a), vb_height(svg_m)
+assert svg_a.count("\n\n") == 0 and svg_m.count("\n\n") == 0, "linie goală în panou"
+
+TOTAL_W = PAD + PW + PANEL_GAP + PW + PAD
+TOTAL_H = PAD + TITLE_H + max(HA, HM) + FOOTER_H + PAD
+OFF_A = (PAD, PAD + TITLE_H)
+OFF_M = (PAD + PW + PANEL_GAP, PAD + TITLE_H)
+
+TITLU = "MP² crossing long — setup A+ vs. setup mediocru (aceeași strategie)"
+LEGENDA = [
+ "Legendă: ce separă cele două sunt nivelurile independente dedesubte "
+ "(suport quarterly + double bottom + neckline),",
+ "nu un filtru pus în scaner. Panoul din dreapta e exemplu construit, nu date reale.",
+ "Surse A+: 09_QA/01_2026-08-25.md:389-394, 485-490, 501-507.",
+]
+
+wrapper = (
+ f''
+ f'{TITLU}'
+ f'{inner(svg_a)}'
+ f'{inner(svg_m)}'
+ + "".join(
+ f'{ln}'
+ for i, ln in enumerate(LEGENDA)
+ )
+ + ''
+)
+
+# ============================================================ verificarea pe SVG-ul final
+def texts(svg: str):
+ """Toate (x, y, text-anchor, text) din elementele ale unui fragment."""
+ out = []
+ for m in re.finditer(r']*)>(.*?)', svg):
+ x, y, attrs, t = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4)
+ if t.startswith("<"):
+ continue
+ am = re.search(r'text-anchor="(\w+)"', attrs)
+ out.append((x, y, am.group(1) if am else "start", t))
+ return out
+
+
+def span(x: float, anchor: str, t: str) -> tuple[float, float]:
+ """Intervalul orizontal aproximativ al unui text, față de ancora lui."""
+ w = len(t) * 12 * 0.56
+ left = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x)
+ return left, left + w
+
+
+assert wrapper.count("\n\n") == 0, "linie goală în SVG"
+
+# fiecare text (după translatarea panoului) e în întregime în interiorul pânzei
+all_txt = []
+for frag, (ox, oy) in ((inner(svg_a), OFF_A), (inner(svg_m), OFF_M)):
+ for x, y, anchor, t in texts(frag):
+ gx, gy = x + ox, y + oy
+ gl, gr = gx + span(0, anchor, t)[0], gx + span(0, anchor, t)[1]
+ assert ox <= gl and gr <= ox + PW, f"text iese din panou: {t!r} ({gl:.0f}..{gr:.0f})"
+ assert -1 <= gl and gr <= TOTAL_W + 1, f"text ieșit pe orizontală: {t!r}"
+ assert 0 <= gy <= TOTAL_H, f"text ieșit pe verticală: {t!r} ({gy:.0f})"
+ all_txt.append((gx, gy, anchor, t))
+# textele din wrapper (titlu + legenda), în coordonate globale
+wtxt = [(TOTAL_W / 2, 20.0, "middle", TITLU)]
+wtxt += [(TOTAL_W / 2, TOTAL_H - FOOTER_H + 18 + 18 * i, "middle", ln)
+ for i, ln in enumerate(LEGENDA)]
+for x, y, anchor, t in wtxt:
+ gl, gr = span(x, anchor, t)
+ assert -1 <= gl and gr <= TOTAL_W + 1, f"text din wrapper iese din pânză: {t!r}"
+ assert 0 <= y <= TOTAL_H, f"text din wrapper în afara înălțimii: {t!r}"
+ all_txt.append((x, y, anchor, t))
+assert len(all_txt) >= 12, f"prea puține etichete: {len(all_txt)}"
+
+# nu se suprapun două texte pe aceeași poziție sau pe rândul apropiat
+for i in range(len(all_txt)):
+ xi, yi, ai, si = all_txt[i]
+ li, ri = (xi + span(0, ai, si)[0], xi + span(0, ai, si)[1])
+ for j in range(i + 1, len(all_txt)):
+ xj, yj, aj, sj = all_txt[j]
+ lj, rj = xj + span(0, aj, sj)[0], xj + span(0, aj, sj)[1]
+ if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
+ raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}")
+
+# etichetele cerute de spec există în desen
+for cheie in ("suport quarterly 554.00", f"SL {SL_A:.2f}", "intrare 577.00",
+ "neckline 578.00", "TP1 599.60 = 1:1",
+ f"umăr stâng {ls_a:.0f}", f"head {head_a:.0f}", f"umăr drept {rs_a:.0f}",
+ "A+ — crossing la confluență (SPY, aprilie)",
+ "Mediocru — crossing în mijlocul range-ului (construit)",
+ "range 110.00", "range 100.00", "SL 103.00", "intrare 105.00", "TP1 107.00",
+ "crossing (mijloc)", "exemplu construit, nu date reale"):
+ assert cheie in wrapper, f"lipsește din desen: {cheie!r}"
+
+OUT.write_text(wrapper, encoding="utf-8")
+print(f"scris {OUT.name}: {TOTAL_W}x{TOTAL_H:.0f}, "
+ f"{len(all_txt)} etichete, verificare OK")
diff --git a/gen/strategii/mp2_aplus.svg b/gen/strategii/mp2_aplus.svg
new file mode 100644
index 0000000..96fb625
--- /dev/null
+++ b/gen/strategii/mp2_aplus.svg
@@ -0,0 +1,221 @@
+MP² crossing long — setup A+ vs. setup mediocru (aceeași strategie)
+ A+ — crossing la confluență (SPY, aprilie)
+
+ suport quarterly 554.00
+
+ SL 554.40
+
+ intrare 577.00
+
+ neckline 578.00
+
+ TP1 599.60 = 1:1
+
+ downtrend anterior
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ volum
+ umăr stâng 566
+ head 555 / double bottom
+ umăr drept 568
+ crossing
+ Confluență: suport 554.00 + double bottom 554.45 + neckline 578.00
+
+ Mediocru — crossing în mijlocul range-ului (construit)
+
+ range 110.00
+
+ range 100.00
+
+ SL 103.00
+
+ intrare 105.00
+
+ TP1 107.00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ volum
+ crossing (mijloc)
+ exemplu construit, nu date reale — SL nu stă pe niciun pivot
+Legendă: ce separă cele două sunt nivelurile independente dedesubte (suport quarterly + double bottom + neckline),nu un filtru pus în scaner. Panoul din dreapta e exemplu construit, nu date reale.Surse A+: 09_QA/01_2026-08-25.md:389-394, 485-490, 501-507.
\ No newline at end of file
diff --git a/gen/strategii/mp2_crossing.py b/gen/strategii/mp2_crossing.py
new file mode 100644
index 0000000..2de8413
--- /dev/null
+++ b/gen/strategii/mp2_crossing.py
@@ -0,0 +1,228 @@
+#!/usr/bin/env python3
+"""MP² — crossing vs. buy the dip: două panouri una sub alta.
+
+Citește `gen/strategii/mp2_crossing.spec.txt` și scrie
+`gen/strategii/mp2_crossing.svg`.
+
+Aceleași două linii de pivot pe ambele panouri:
+ - pivot weekly (cel MARE) = 62.00, orizontal;
+ - pivot daily (cel MIC), stepped — se recalculează zilnic și „sare" pe nivel nou.
+
+Sus, „DA — crossing (+2)": pivotul daily urcă din 61.70 și traversează 62.00 în
+sus (marcat cu cerc: „crossing: 61.70 → 62.20"). Trigger = prima candelă verde
+după crossing care închide peste pivotul daily (bara 11, close 62.40).
+
+Jos, „NU — continuare/buy the dip (+1)": pivotul daily e deja deasupra lui 62.00
+și rămâne acolo; prețul face închideri peste el, dar nimic nu s-a re-încrucișat,
+deci semnalele ulterioare sunt buy the dip, nu crossing.
+
+Cifrele (62.00 / 61.70 / 62.20) sunt din `summaries/09_QA/09_2025-12-09.md:69-83`
+și distincția crossing vs. buy the dip din `summaries/09_QA/16_2025-10-07.md:35-43`.
+Seriile de preț sunt construite (open = close-ul barei anterioare) ca nivelurile
+afirmate să iasă exact din ele.
+
+Rulare: python3 gen/strategii/mp2_crossing.py
+"""
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, from_closes # noqa: E402
+
+OUT = Path(__file__).with_suffix(".svg")
+W = 480
+VGAP = 44
+SCALE = 95.0 # px / unitate, aceeași pe ambele panouri
+WEEKLY = 62.00
+
+GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
+
+# ---------------------------------------------------------------- panoul DA
+# pivotul daily: 61.70 (sub 62.00) până la bara 9, apoi 62.20 (peste 62.00)
+PDA = [61.70] * 10 + [62.20] * 12
+CLOSES_DA = [61.30, 61.15, 61.35, 61.20, 61.45, 61.30, 61.55, 61.40,
+ 61.60, 61.75, # 0-9: sub pivot 61.70/62.00
+ 62.05, # 10: pivotul sare la 62.20; close inca sub el
+ 62.40, # 11: trigger — verde, close peste 62.20
+ 62.55, 62.50, 62.65, 62.60, 62.75, 62.70,
+ 62.85, 62.80, 62.90, 62.95] # 12-21: urca peste pivot
+WICKS_DA = [(0.08, 0.10)] * len(CLOSES_DA)
+WICKS_DA[10] = (0.16, 0.10) # bara crossing-ului
+WICKS_DA[11] = (0.22, 0.10) # bara de trigger
+assert len(CLOSES_DA) == len(PDA) == 22
+
+# ---------------------------------------------------------------- panoul NU
+# pivotul daily e deja deasupra lui 62.00 pe toată durata (62.30 → 62.20)
+PNU = [62.30] * 9 + [62.20] * 13
+CLOSES_NU = [61.55, 61.70, 61.90, 61.80, 62.05,
+ 62.35, # 5: primul close peste pivotul daily
+ 62.25, 62.45, 62.55,
+ 62.40, 62.60, 62.50, 62.70, 62.65, 62.80, 62.75,
+ 62.90, 62.85, 63.00, 62.95, 63.10, 63.05]
+WICKS_NU = [(0.08, 0.10)] * len(CLOSES_NU)
+assert len(CLOSES_NU) == len(PNU) == 22
+
+START_DA = CLOSES_DA[0] + 0.12
+START_NU = CLOSES_NU[0] + 0.12
+cds_da = from_closes(CLOSES_DA, wicks=WICKS_DA, start=START_DA)
+cds_nu = from_closes(CLOSES_NU, wicks=WICKS_NU, start=START_NU)
+
+# ------------------------------------------------------ assert-urile din spec
+# 1. DA: pivotul daily e < 62.00 înainte de crossing și > 62.00 după
+cross_da = next(i for i in range(1, len(PDA)) if PDA[i] > WEEKLY)
+assert cross_da == 10, f"crossing-ul DA e la bara {cross_da}, nu 10"
+assert all(v < WEEKLY for v in PDA[:cross_da]), "pivot daily DA nu e sub 62.00 înainte de crossing"
+assert PDA[cross_da - 1] == 61.70 and PDA[cross_da] == 62.20, \
+ f"crossing DA {PDA[cross_da - 1]} -> {PDA[cross_da]}, nu 61.70 -> 62.20"
+assert all(v > WEEKLY for v in PDA[cross_da:]), "pivot daily DA nu e peste 62.00 după crossing"
+
+# 2. DA: trigger = prima candelă verde după crossing care închide peste pivotul daily
+trigger = next(i for i in range(cross_da, len(cds_da))
+ if cds_da[i].c >= cds_da[i].o and cds_da[i].c > PDA[i])
+assert trigger == 11, f"trigger-ul DA e la bara {trigger}, nu 11"
+assert cds_da[trigger].c > PDA[trigger], "candela de trigger nu închide peste pivotul daily"
+assert cds_da[10].c <= PDA[10], "bara crossing-ului nu trebuie să închidă peste pivot"
+entry = cds_da[trigger].c
+
+# 3. NU: toate valorile pivot_daily > 62.00 — nimic re-încrucișat
+assert all(v > WEEKLY for v in PNU), "un pivot daily NU e sub 62.00"
+assert not any(v < WEEKLY for v in PNU), "există pivot daily NU sub 62.00 => ar fi crossing"
+assert min(PNU) > WEEKLY and max(PNU) > WEEKLY
+# prețul face închideri peste pivotul daily, dar pivotul nu cade sub 62.00
+prim_nu = next(i for i in range(len(cds_nu)) if cds_nu[i].c > PNU[i])
+assert prim_nu == 5 and cds_nu[prim_nu].c > PNU[prim_nu], "NU nu face close peste pivotul daily"
+assert min(PNU) == 62.20 and max(PNU) == 62.30, "pivotii NU nu sunt 62.20 / 62.30"
+
+# 4. liniile sunt „stepped" (au cel puțin un salt), nu drepte
+assert len(set(PDA)) >= 2 and len(set(PNU)) >= 2, "liniile de pivot nu sunt stepped"
+
+# --------------------------------------------------- desen: câte un panou
+def step_points(ch: Chart, vals: list[float]) -> str:
+ """Polilinie în trepte: orizontal pe palier, salt vertical la schimbare."""
+ pts: list[tuple[float, float]] = []
+ for i, v in enumerate(vals):
+ x, y = ch._cx(i), ch._y(v)
+ if i == 0:
+ pts.append((x, y))
+ elif v != vals[i - 1]:
+ pts.append((x, ch._y(vals[i - 1])))
+ pts.append((x, y))
+ else:
+ pts.append((x, y))
+ return " ".join(f"{x:.1f},{y:.1f}" for x, y in pts)
+
+
+def render_pivot_panel(cds, pivots, title) -> tuple[str, float, Chart]:
+ """Randază un panou cu pivot weekly (Level) + pivot daily (polilinie injectată)."""
+ ch = Chart(candles=cds,
+ levels=[Level(WEEKLY, f"pivot weekly (cel MARE) {WEEKLY:.2f}",
+ BLUE, dash="", label_at="left")],
+ marks=[],
+ scale=SCALE, gap=15, body_w=9, left=44, width=W,
+ title=title)
+ svg = ch.render() # fixează _pmax/_y și adaugă +22 pentru titlu
+ inject = [
+ f'',
+ f'pivot daily (cel MIC)',
+ ]
+ i = svg.index(">") + 1
+ svg = svg[:i] + "\n " + "\n ".join(inject) + svg[i:]
+ m = re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', svg)
+ body = svg[svg.index(">") + 1:svg.rindex("")]
+ return body, float(m.group(2)), ch
+
+
+body_da, h_da, ch_da = render_pivot_panel(
+ cds_da, PDA, "DA — crossing (+2)")
+
+# marcajul crossing-ului: cerc pe saltul pivotului + eticheta din spec
+xc = ch_da._cx(cross_da)
+yc = (ch_da._y(PDA[cross_da - 1]) + ch_da._y(PDA[cross_da])) / 2
+circle = (f'')
+cross_txt = (f'crossing: 61.70 → 62.20')
+mark_txt = (f''
+ f'intrare: trigger (prima candelă verde)')
+i = body_da.rindex("") + len("") # după candele/marks, ca să fie vizibile
+body_da = body_da[:i] + "\n " + circle + "\n " + cross_txt + "\n " + mark_txt + body_da[i:]
+
+# panoul NU: eticheta din spec pe semnalele ulterioare
+body_nu, h_nu, ch_nu = render_pivot_panel(
+ cds_nu, PNU, "NU — continuare/buy the dip (+1)")
+body_nu = body_nu.rstrip() + (f'\n '
+ f'semnale ulterioare = BUY THE DIP, nu crossing')
+
+# ------------------------------------------------- lipirea celor două panouri
+CAPS = [
+ "Pivot weekly 62.00 = linia orizontală (pivotul MARE).",
+ "Pivot daily (cel MIC) se recalculează zilnic și sare pe nivel nou.",
+ "Crossing = pivotul mic traversează 62.00 în sus.",
+ "Fără re-încrucișare, semnalele ulterioare sunt buy the dip,",
+ "nu crossing.",
+]
+cap_top = h_da + VGAP + h_nu + 16
+H = cap_top + 17 * len(CAPS) + 10
+
+parts = [
+ f'',
+ f'{body_da}',
+ f'{body_nu}',
+]
+parts += [f'{c}'
+ for i, c in enumerate(CAPS)]
+parts.append("")
+svg = "\n ".join(parts)
+assert "\n\n" not in svg, "linie goală în SVG"
+
+# ------------------------------------- verificarea citirii: etichete în panză
+TEXT = re.compile(r']*)>(.*?)', re.S)
+
+
+def _texts(body: str, y_off: float, tag: str):
+ out = []
+ for m in TEXT.finditer(body):
+ x, y = float(m.group(1)), float(m.group(2)) + y_off
+ attrs, s = m.group(3), m.group(4)
+ anchor = "start"
+ if 'text-anchor="end"' in attrs:
+ anchor = "end"
+ elif 'text-anchor="middle"' in attrs:
+ anchor = "middle"
+ bold = "bold" in attrs
+ w = len(s) * 12 * (0.62 if bold else 0.52)
+ left = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x)
+ out.append((left, left + w, y, s, tag))
+ return out
+
+
+labels = _texts(body_da, 0.0, "DA") + _texts(body_nu, h_da + VGAP, "NU")
+for i, c in enumerate(CAPS): # și caption-urile, care nu-s în panouri
+ labels.append((24, 24 + len(c) * 12 * 0.52, cap_top + 17 * i, c, "CAP"))
+for left, right, y, s, tag in labels:
+ assert left >= -2 and right <= W + 2, f"[{tag}] text iese din panză: {s!r}"
+ assert 0 <= y <= H, f"[{tag}] text în afara înălțimii: {s!r} (y={y:.0f})"
+
+for i in range(len(labels)):
+ li, ri, yi, si, ti = labels[i]
+ for j in range(i + 1, len(labels)):
+ lj, rj, yj, sj, tj = labels[j]
+ if ti == tj and abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
+ raise AssertionError(f"[{ti}] texte suprapuse: {si!r} / {sj!r}")
+
+dist = abs(ch_da._y(WEEKLY) - ch_da._y(PDA[cross_da - 1])) # 62.00 - 61.70, în px
+assert dist > 10, f"distanța 61.70->62.00 = {dist:.1f}px, prea mică"
+
+print(f"DA: crossing la bara {cross_da} (61.70 -> 62.20), trigger bara {trigger} "
+ f"close {entry:.2f} > pivot {PDA[trigger]:.2f}; bare {len(cds_da)}")
+print(f"NU: pivot daily {min(PNU):.2f}..{max(PNU):.2f} (> 62.00), primul close "
+ f"peste pivot bara {prim_nu} ({cds_nu[prim_nu].c:.2f}); bare {len(cds_nu)}")
+print(f"scris {OUT.name} ({W}x{H:.0f}); {len(labels)} etichete verificate; "
+ f"61.70->62.00 = {dist:.1f}px")
+OUT.write_text(svg, encoding="utf-8")
diff --git a/gen/strategii/mp2_crossing.svg b/gen/strategii/mp2_crossing.svg
new file mode 100644
index 0000000..ac2ed98
--- /dev/null
+++ b/gen/strategii/mp2_crossing.svg
@@ -0,0 +1,112 @@
+
+
+
+ pivot daily (cel MIC)
+ DA — crossing (+2)
+
+ pivot weekly (cel MARE) 62.00
+
+ crossing: 61.70 → 62.20
+ intrare: trigger (prima candelă verde)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ pivot daily (cel MIC)
+ NU — continuare/buy the dip (+1)
+
+ pivot weekly (cel MARE) 62.00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ semnale ulterioare = BUY THE DIP, nu crossing
+ Pivot weekly 62.00 = linia orizontală (pivotul MARE).
+ Pivot daily (cel MIC) se recalculează zilnic și sare pe nivel nou.
+ Crossing = pivotul mic traversează 62.00 în sus.
+ Fără re-încrucișare, semnalele ulterioare sunt buy the dip,
+ nu crossing.
+
\ No newline at end of file
diff --git a/gen/strategii/mp2_exemplu.py b/gen/strategii/mp2_exemplu.py
new file mode 100644
index 0000000..8bcd300
--- /dev/null
+++ b/gen/strategii/mp2_exemplu.py
@@ -0,0 +1,147 @@
+"""Diagrama din `gen/strategii/mp2_exemplu.spec.txt` (TIP: PRET).
+
+Exemplul complet din curs: SPY long la bottom-ul de aprilie 2020 — double
+bottom + Inverted Head & Shoulders la suport quarterly, trigger MP² long
+(pivotul mic închide peste cel mare), prima candelă verde după umărul drept.
+Intrare 577.00, SL 554.40 (1R = 22.60), TP1 599.60, ținta 4R era 667.00, iar
+mișcarea reală s-a oprit la 646.00 (~3R) într-o lună.
+
+Toate cifrele vin din `summaries/09_QA/01_2026-08-25.md:384-394,485-494`
+(liniile citate în spec). Serie continuă de 38 de candele daily
+(`from_closes`), panou de volum, SMA-20, niveluri reale și marcaje ancorate.
+"""
+import sys
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402
+
+OUT = Path(__file__).resolve().parent / "mp2_exemplu.svg"
+
+# ── cifrele din material (09_QA/01_2026-08-25.md:384-394,485-494) ───────────
+ENTRY = 577.00 # candela de intrare (prima verde după umărul drept)
+SL = 554.40 # sub umărul drept, chiar peste suportul quarterly
+TP1 = 599.60 # 577.00 + 22.60 (1R)
+TARGET_4R = 667.00 # ținta declarată; 4R exact = 667.40
+REAL = 646.00 # mișcarea reală (~3R, +12% efectiv)
+QUARTERLY = 554.00 # suport quarterly
+SHOULDER_L = 566.0 # umăr stâng
+HEAD = 555.0 # head (cel mai jos)
+SHOULDER_R = 568.0 # umăr drept
+NECKLINE = 578.00
+
+# ── structura seriei (spec): ~14 coborâre → ~10 bază → intrare → ~12 urcare ─
+closes = [
+ # 0-13: ~14 candele de coborâre de la ~600 spre umărul stâng 566
+ 600.0, 597.0, 594.0, 596.5, 591.0, 587.5, 590.0,
+ 584.0, 579.5, 581.5, 576.0, 571.0, 567.0,
+ 566.0, # 13 umăr stâng
+ # 14-24: baza — double bottom + Inverted Head & Shoulders
+ 570.5, 578.0, 572.0, 567.0, 560.0,
+ 555.0, # 19 head (cel mai jos)
+ 561.5, 568.0, 578.0, 573.0,
+ 568.0, # 24 umăr drept
+ # 25: candela de intrare, prima verde după umărul drept
+ 577.00,
+ # 26-37: ~12 candele de urcare spre 646.00 (marcajul „real")
+ 581.0, 585.5, 589.0, 584.0, 590.5, 596.0, 602.0,
+ 609.0, 616.0, 624.0, 635.0, 646.0,
+]
+assert len(closes) == 38, len(closes)
+
+ENTRY_I = 25
+HEAD_I = 19
+SHL_I = 13
+SHR_I = 24
+
+wicks = [(0.4, 0.4)] * len(closes)
+wicks[SHL_I] = (0.4, 0.5) # low umăr stâng 565.50
+wicks[HEAD_I] = (0.4, 0.6) # low head = 555.0 - 0.6 = 554.40 (suportul atins)
+wicks[SHR_I] = (0.4, 0.5)
+wicks[15] = (0.4, 0.4) # vârf 578.0 = neckline
+wicks[22] = (0.4, 0.4) # vârf 578.0 = neckline
+wicks[ENTRY_I] = (0.7, 0.4) # intrare verde, wick mic
+wicks[37] = (0.0, 0.3) # high = 646.00 exact (mișcarea reală se oprește aici)
+
+vols = [
+ # coborâre
+ 48, 46, 50, 44, 52, 49, 45, 53, 55, 47, 50, 54, 58, 62,
+ # bază (head-ul are volum mare — capitulare)
+ 60, 72, 65, 58, 66, 88, 70, 64, 74, 60, 56,
+ # intrare
+ 92,
+ # urcare
+ 78, 72, 68, 60, 66, 74, 82, 88, 80, 84, 90, 95,
+]
+assert len(vols) == len(closes)
+
+cds = from_closes(closes, wicks=wicks, vols=vols, start=602.0)
+
+# ── assert-urile din specificație ───────────────────────────────────────────
+entry = cds[ENTRY_I].c
+assert abs(entry - 577.00) < 0.05, entry
+assert abs(SL - 554.40) < 1e-9, SL
+assert SL > QUARTERLY, (SL, QUARTERLY) # peste suportul quarterly
+assert abs((ENTRY - SL) - 22.60) < 0.05, ENTRY - SL # 1R
+assert abs(TP1 - (ENTRY + 22.60)) < 0.05, TP1 # TP1 = intrare + 1R
+target_4r = ENTRY + 4 * (ENTRY - SL)
+assert abs(target_4r - 667.40) < 0.05, target_4r # 4R exact
+assert abs(target_4r - TARGET_4R) < 0.5, target_4r # ≈ 667.00, rotunjit în curs
+assert abs((REAL - ENTRY) / (ENTRY - SL) - 3.05) < 0.02, (REAL - ENTRY) / (ENTRY - SL)
+assert abs((REAL - ENTRY) / (ENTRY - SL) - 4.0) > 0.5, "mișcarea reală NU e 4R"
+
+# structura care susține afirmațiile din text
+assert abs(cds[SHL_I].c - SHOULDER_L) < 0.05, cds[SHL_I].c
+assert abs(cds[HEAD_I].c - HEAD) < 0.05, cds[HEAD_I].c
+assert abs(cds[SHR_I].c - SHOULDER_R) < 0.05, cds[SHR_I].c
+assert HEAD < SHOULDER_L and HEAD < SHOULDER_R, (HEAD, SHOULDER_L, SHOULDER_R)
+assert cds[SHL_I].c == min(c.c for c in cds[SHL_I - 1:SHL_I + 2]), "umărul stâng nu e minim local"
+assert cds[HEAD_I].c == min(c.c for c in cds[HEAD_I - 1:HEAD_I + 2]), "head-ul nu e minim local"
+assert cds[SHR_I].c == min(c.c for c in cds[SHR_I - 1:SHR_I + 2]), "umărul drept nu e minim local"
+min_pre = min(c.l for c in cds[:ENTRY_I])
+assert abs(min_pre - 554.0) < 1.0, min_pre # suportul quarterly atins
+assert abs(min_pre - SL) < 0.05, (min_pre, SL) # SL chiar la minimul head-ului
+assert cds[ENTRY_I].c > cds[ENTRY_I].o, "candela de intrare trebuie verde"
+assert cds[ENTRY_I - 1].c < cds[ENTRY_I - 1].o, "umărul drept e roșu"
+assert abs(cds[37].c - REAL) < 0.05, cds[37].c
+assert all(c.vol is not None for c in cds), "lipsește panoul de volum"
+assert len(cds) == 38
+
+# ── desenul ─────────────────────────────────────────────────────────────────
+levels = [
+ Level(TARGET_4R, "ținta 4R era 667.00 (+16%)", "#555"),
+ Level(REAL, "646.00 — +12% efectiv (~3R)", "#188a3e", label_at="left"),
+ Level(TP1, "TP1 599.60", "#1a5fb4"),
+ Level(NECKLINE, "neckline 578.00", "#1a5fb4"),
+ Level(SL, "SL 554.40", "#c00"),
+ Level(QUARTERLY, "suport quarterly 554.00", "#777", label_at="left"),
+]
+
+marks = [
+ Mark(SHL_I, "umăr stâng 566", "#1a1a1a", "below"),
+ Mark(HEAD_I, "head 555", "#1a1a1a", "below"),
+ Mark(SHR_I, "umăr drept 568", "#1a1a1a", "below"),
+ Mark(ENTRY_I, "intrare 577.00", "#188a3e", "above", dy=-10),
+]
+
+ch = Chart(
+ candles=cds,
+ levels=levels,
+ marks=marks,
+ mas=[MA(20, "#b06000", dash="4 3", text="SMA-20")],
+ scale=3.8, width=720, gap=15, body_w=9, left=44, vol_h=70,
+ vol_label="volum", title="SPY — daily",
+ caption=("Intrare 577.00 = prima verde după umărul drept (MP² long). "
+ "1R = 22.60; 646.00 ~3R, într-o lună."),
+)
+
+svg = ch.render()
+assert svg.count("\n\n") == 0, "linie goală în SVG — rupe markdown-ul"
+OUT.write_text(svg)
+
+print(f"serie {len(cds)} candele | intrare {entry:.2f} @ {ENTRY_I} | "
+ f"SL {SL:.2f} | 1R {ENTRY - SL:.2f} | TP1 {TP1:.2f} | "
+ f"4R {target_4r:.2f} (ținta declarată {TARGET_4R:.2f}) | "
+ f"real {REAL:.2f} = {(REAL - ENTRY) / (ENTRY - SL):.2f}R | "
+ f"min pre-intrare {min_pre:.2f}")
+print(f"scris {OUT}")
diff --git a/gen/strategii/mp2_exemplu.svg b/gen/strategii/mp2_exemplu.svg
new file mode 100644
index 0000000..88098f2
--- /dev/null
+++ b/gen/strategii/mp2_exemplu.svg
@@ -0,0 +1,138 @@
+
+ SPY — daily
+
+ ținta 4R era 667.00 (+16%)
+
+ 646.00 — +12% efectiv (~3R)
+
+ TP1 599.60
+
+ neckline 578.00
+
+ SL 554.40
+
+ suport quarterly 554.00
+
+ SMA-20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ volum
+ umăr stâng 566
+ head 555
+ umăr drept 568
+ intrare 577.00
+ Intrare 577.00 = prima verde după umărul drept (MP² long). 1R = 22.60; 646.00 ~3R, într-o lună.
+
\ No newline at end of file