diff --git a/gen/strategii/m2d_ansamblu.py b/gen/strategii/m2d_ansamblu.py
index 1bf4f40..f6af71d 100644
--- a/gen/strategii/m2d_ansamblu.py
+++ b/gen/strategii/m2d_ansamblu.py
@@ -1,136 +1,390 @@
-"""Diagrama de ansamblu M2D (strategii/03_m2d.md, marcajul `ansamblu`).
+#!/usr/bin/env python3
+"""M2D — tot trade-ul, pe perechea 15 min → 3 min, cu lupă.
-„Setup M2D LONG complet pe TF mic (3 min), cu TF mare (15 min) în Buy":
-urcare -> pullback (M = Sell Momentum) -> trigger (A = primul Buy) -> intrare
--> SL de 4 candele -> TP1 la 1:1 -> continuare -> vârf -> ieșire pe bull trail.
+Pentru `summaries/17_STRATEGII/03_m2d.md`, marcajul ``.
-Toate close-urile, wick-urile, volumele și nivelurile vin din
-`gen/strategii/m2d_ansamblu.spec.txt`. Seria e construită (exemplu generic,
-fără ticker), la scara 1.00 $ risc / 2.2R randament — nu e un trade real.
+Tiparul e cel din `buy_doji_perechi_tf.py`: panoul din dreapta NU e o serie
+care vine după cea din stânga — e **descompunerea reală** a ultimelor 4
+candele de 15 minute. O candelă de 15 min = k = 5 candele de 3 min, cu
+open, close, max(high) și min(low) care coincid, verificat bară cu bară.
-Rulează:
- python3 gen/strategii/m2d_ansamblu.py
-Scrie lângă el `m2d_ansamblu.svg`.
+Sursa de adevăr e seria fină (`FINE`, 3 min). Candelele de 15 min din zona
+mărită se CALCULEAZĂ din ea prin agregare; barele de dinainte (`BASE`) sunt
+date separat și se leagă de lupă prin close == open.
+
+Toate cifrele din desen ies din serii. Regula de stop din tot capitolul:
+stopul stă FIX pe extrema ultimelor 4 candele de pe TF mic, fără marjă.
+
+Rulare: python3 gen/strategii/m2d_ansamblu.py
"""
-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")
+W = 720
+K = 5 # 5 candele de 3 min într-una de 15 min
+NZ = 4 # câte candele de 15 min sunt mărite
+STOP_BARS = 4 # stopul = extrema ultimelor 4 bare de pe TF mic
+PULLBACK_N = 9 # barele 0..8 = pullback-ul (M)
+TRIG = 9 # bara de trigger (A) pe TF mic
-OUT = Path(__file__).resolve().parent / "m2d_ansamblu.svg"
-
-GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
-
-# ---------------------------------------------------------------- seria (32 bare)
-CLOSES = [
- 96.8, 97.2, 97.7, 98.1, 98.6, 99.1, 99.5, 100.0, # 0-7 urcare
- 99.8, 99.4, 99.0, 99.2, 98.9, 99.15, 99.3, # 8-14 pullback (M)
- 99.8, 100.2, # 15-16 trigger (A)
- 100.7, 101.2, 101.6, 102.0, 102.4, 102.7, 103.0, # 17-23 continuare
- 103.3, 103.1, 102.8, 102.7, 102.9, 102.5, 102.2, 102.0, # 24-31 vârf + violare
-]
-assert len(CLOSES) == 32, len(CLOSES)
-
-# fitile (sus, jos); jos mai mari pe 10-12, ca minimul de 4 candele (11:15) să
-# fie clar sub intrare, la exact 98.80 (candela 12).
-WICKS = (
- [(0.15, 0.10)] * 8 # 0-7
- + [(0.10, 0.15), (0.10, 0.20), (0.10, 0.15), # 8-10
- (0.10, 0.15), (0.10, 0.10), (0.10, 0.05), (0.10, 0.10)] # 11-14
- + [(0.15, 0.10), (0.15, 0.10)] # 15-16
- + [(0.15, 0.10)] * 7 # 17-23
- + [(0.20, 0.10), (0.10, 0.15), (0.10, 0.15), (0.12, 0.15), # 24-27
- (0.10, 0.15), (0.10, 0.15), (0.10, 0.15), (0.10, 0.20)] # 28-31
-)
-assert len(WICKS) == 32, len(WICKS)
-
-VOLS = (
- [62, 66, 60, 68, 64, 70, 66, 74] # 0-7 trend
- + [44, 40, 38, 36, 34, 33, 35] # 8-14 pullback mic
- + [82, 88] # 15-16 trigger
- + [72, 70, 74, 68, 72, 66, 70] # 17-23 trend
- + [76, 64, 60, 58, 62, 56, 54, 66] # 24-31 vârf + ieșire
-)
-assert len(VOLS) == 32, len(VOLS)
-
-cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=96.4)
-
-# ---------------------------------------------------------------- assert-urile din spec
-entry = cds[15].c # 1. intrarea = close-ul trigger-ului
-SL = min(c.l for c in cds[11:15]) # 2. minimul ultimelor 4 dinainte
-TP1 = entry + (entry - SL) # 4. TP1 exact 1:1
-
-assert entry == round(cds[15].c, 2), (entry, cds[15].c)
-assert SL == min(c.l for c in cds[11:15]), SL
-assert SL < entry, (SL, entry)
-assert abs(TP1 - (entry + (entry - SL))) < 1e-6
-for a, b in zip(cds, cds[1:]): # 5. serie continuă, fără goluri
- assert a.c == b.o, (a.c, b.o)
-assert cds[15].c > cds[14].c # 6. trigger în sus după pullback
-assert any(c.c < c.o for c in cds[8:15]) # 7. pullback = Sell
-assert any(c.c > c.o for c in cds[0:8]) # context în sus
-exit_price = cds[31].c
-assert exit_price > entry # 8. ieșirea e peste intrare
-
-# bull trail = low-ul ultimei candele care a închis peste high-ul precedentei
-qual = [i for i in range(1, 31) if cds[i].c > cds[i - 1].h]
-assert qual and qual[-1] == 24, qual
-bull = cds[24].l
-assert cds[31].c < bull, (cds[31].c, bull) # finala închide sub linie = ieșire aici
-
-r_mult = (exit_price - entry) / (entry - SL) # 9. R-ul afișat
-assert round(r_mult, 1) == 2.2, r_mult
-
-# ---------------------------------------------------------------- niveluri, etichete, marks
-levels = [
- Level(entry, f"intrare {entry:.2f}", GREEN, label_at="left"),
- Level(SL, f"SL {SL:.2f} (4 candele)", RED, label_at="right"),
- Level(TP1, f"TP1 {TP1:.2f} = 1:1", BLUE, label_at="right"),
-]
-assert [round(lv.price, 2) for lv in levels] == [99.80, 98.80, 100.80]
-
-exit_text = f"ieșire {exit_price:.2f} pe bull trail ({r_mult:.1f}R)"
-assert f"{r_mult:.1f}R" in exit_text
-
-marks = [
- Mark(0, "TF mare (15 min) în Buy", BLUE, "above", dy=-227),
- Mark(11, "M: Sell Momentum (pullback)", RED, "below"),
- Mark(15, "A: primul Buy = trigger", GREEN, "above", dy=-122),
- # „vârf" stă sub candela 24: deasupra ei, orice text de la y-ul vârfului se
- # calcă pe titlul din stânga-sus (ambele pornesc din `top`).
- Mark(24, "vârf 103.30", GRAY, "below", dy=25),
- Mark(31, "close sub bull trail = ieși", RED, "below", dy=55),
- Mark(31, exit_text, RED, "below", dy=73),
+# --- seria fină (3 min), 20 de bare: (open, high, low, close) --------------
+FINE = [
+ (100.60, 100.70, 100.40, 100.45), (100.45, 100.50, 100.25, 100.30),
+ (100.30, 100.35, 100.15, 100.20), (100.20, 100.25, 100.05, 100.10),
+ (100.10, 100.15, 100.00, 100.05), (100.05, 100.10, 99.95, 100.00),
+ (100.00, 100.05, 99.92, 99.95), (99.95, 100.00, 99.90, 99.98),
+ (99.98, 100.08, 99.94, 100.02), # 8: capătul pullback-ului
+ (100.02, 100.45, 100.00, 100.40), # 9: A = trigger, intri
+ (100.40, 100.60, 100.35, 100.55), (100.55, 100.75, 100.50, 100.70),
+ (100.70, 101.00, 100.65, 100.95), # 12: TP1 atins
+ (100.95, 101.10, 100.88, 101.05), (101.05, 101.15, 100.95, 101.00),
+ (101.00, 101.40, 100.95, 101.35), (101.35, 101.55, 101.25, 101.45),
+ (101.45, 101.50, 101.20, 101.25),
+ (101.25, 101.30, 101.05, 101.10), # 18: close sub trail = ieși
+ (101.10, 101.15, 100.90, 100.95),
]
-caption = (f"Serie construită (32 bare, 3 min). R = ({exit_price:.2f} − {entry:.2f}) / "
- f"{entry - SL:.2f} = {r_mult:.1f}R.")
-assert f"{r_mult:.1f}R" in caption
+# --- barele de 15 min dinaintea lupei; ultima poartă semnalul Buy Momentum --
+BASE = [
+ (98.00, 98.35, 97.85, 98.25), (98.25, 98.70, 98.15, 98.60),
+ (98.60, 99.10, 98.50, 99.00), (99.00, 99.45, 98.90, 99.30),
+ (99.30, 99.95, 99.20, 99.85),
+ (99.85, 100.75, 99.80, 100.60), # 5: Buy Momentum pe 15 min
+]
+SIG_B = 5 # candela de 15 min cu semnalul
-ch = Chart(
- candles=cds, levels=levels, marks=marks,
- mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")],
- scale=52.0, gap=16, body_w=9, left=44, width=720, vol_h=70,
- vol_label="volum",
- title="Simbol construit — 3 min (TF mare 15 min în Buy)",
- caption=caption,
-)
-svg = ch.render()
-# bull trail: nu e un `Level` (spec-ul cere exact trei niveluri), deci se
-# injectează ca linie separată, la aceeași scară (`ch._y`).
-yb = ch._y(bull)
-bull_layer = (
- f''
- f'bull trail {bull:.2f}'
-)
-i = svg.index(">") + 1
-svg = svg[:i] + "\n " + bull_layer + svg[i:]
+def aggregate(fine, k, n):
+ out_ = []
+ for j in range(n):
+ part = fine[j * k:(j + 1) * k]
+ out_.append((part[0][0], max(b[1] for b in part),
+ min(b[2] for b in part), part[-1][3]))
+ return out_
-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}, SL {SL:.2f}, "
- f"TP1 {TP1:.2f}, ieșire {exit_price:.2f} = {r_mult:.1f}R, bull trail {bull:.2f}")
+
+def eq(a, b):
+ return abs(a - b) < 0.005
+
+
+ZOOM = aggregate(FINE, K, NZ)
+BIG = BASE + ZOOM
+
+# ---------------------------------------------------------------- verificări
+for name, bars in (("15 min", BIG), ("3 min", FINE)):
+ for i, (o, h, l, c) in enumerate(bars):
+ assert h >= max(o, c) and l <= min(o, c), f"{name}[{i}]: high/low incoerent"
+ if i:
+ assert eq(o, bars[i - 1][3]), f"{name}[{i}]: open != close precedent"
+assert len(FINE) == K * NZ, len(FINE)
+
+# (1) unde cad granițele candelelor de 15 min peste poveste — exact ce spune
+# textul capitolului, și poate fi fals
+span = len({i // K for i in range(PULLBACK_N)})
+assert span == 2, f"pullback-ul se întinde peste {span} candele de 15 min, nu 2"
+trig_big = TRIG // K
+assert trig_big == 1, f"trigger-ul cade în candela mare {trig_big}"
+assert eq(FINE[0][0], BASE[SIG_B][3]), "open-ul primei bare fine != close-ul semnalului"
+
+# (2) pullback-ul (M): coboară, dar NU rupe low-ul candelei de 15 min din care
+# a pornit mișcarea — definiția ancorată din mini-glosar
+PULL = FINE[:PULLBACK_N]
+ref_low = BASE[SIG_B][2]
+pull_low = min(b[2] for b in PULL)
+assert PULL[0][3] > PULL[-1][3], "pullback-ul nu coboară"
+assert pull_low > ref_low, f"pullback-ul rupe low-ul de referință ({pull_low} <= {ref_low})"
+
+# (3) trigger-ul (A). Săgeata vine dintr-un indicator proprietar, deci forma
+# ei pe desen e construită de mine: bara de trigger e PRIMA din pullback
+# care se închide peste high-urile ultimelor LOOK bare. Verific că e
+# într-adevăr prima — altfel desenul ar contrazice regula „primul semnal".
+LOOK = 3
+
+
+def rupe(i):
+ return i >= LOOK and FINE[i][3] > max(b[1] for b in FINE[i - LOOK:i])
+
+
+assert rupe(TRIG), "bara de trigger nu rupe high-urile barelor dinainte"
+assert not any(rupe(i) for i in range(LOOK, TRIG)), \
+ "o bară dinaintea trigger-ului rupea deja — nu mai e primul semnal"
+
+# (4) trade-ul
+ENTRY = FINE[TRIG][3]
+SL = min(b[2] for b in FINE[TRIG - STOP_BARS + 1:TRIG + 1])
+RISK = round(ENTRY - SL, 2)
+TP1 = round(ENTRY + RISK, 2)
+assert eq(SL, pull_low), "stopul nu e minimul pullback-ului"
+assert eq(SL, min(b[2] for b in FINE)), "stopul nu e minimul zonei mărite"
+TP_BAR = next(i for i in range(TRIG + 1, len(FINE)) if FINE[i][1] >= TP1 - 1e-9)
+assert TP_BAR == 12, TP_BAR
+
+# (5) bull trail: low-ul ultimei bare care s-a închis peste high-ul precedentei;
+# ieșirea = prima bară care se ÎNCHIDE sub linie
+trail, EXIT_BAR = None, None
+for i in range(1, len(FINE)):
+ if trail is not None and FINE[i][3] < trail:
+ EXIT_BAR = i
+ break
+ if i > TRIG and FINE[i][3] > FINE[i - 1][1]:
+ trail = FINE[i][2]
+TRAIL = trail
+EXIT_BAR = EXIT_BAR if EXIT_BAR is not None else len(FINE) - 1
+EXIT = FINE[EXIT_BAR][3]
+assert (TRAIL, EXIT_BAR) == (101.25, 18), (TRAIL, EXIT_BAR)
+assert EXIT > ENTRY, "ieșirea pe trail e sub intrare"
+R = (EXIT - ENTRY) / RISK
+assert round(R, 1) == 1.4, R
+# wick-uri sub linie nu scot: bara 17 coboară sub trail, dar se închide deasupra
+assert FINE[17][2] < TRAIL <= FINE[17][3], "bara 17 nu mai ilustrează wick sub trail"
+
+LEVEL = BASE[SIG_B][3] # nivelul comun celor două panouri
+assert eq(LEVEL, FINE[0][0])
+
+# ---------------------------------------------------------------- desen
+GREEN, RED, INK, BLUE, GRAY = "#188a3e", "#c00", "#333", "#1a5fb4", "#888"
+LINK, CYAN = "#aaa", "#0891b2"
+PLOT_H = 168
+BX0, BGAP, BW = 104, 15, 9
+SX0, SGAP, SW = 372, 11, 7
+FRAME_L = SX0 - 8
+COL_X = 600
+MIDX = FRAME_L - 20
+PAD = 0.06
+
+out, LABELS = [], []
+
+
+def text(x, y, s, fill=INK, anchor="start", bold=False, size=12):
+ st = [f"font-size:{size}px"] + (["font-weight:bold"] if bold else [])
+ a = f' text-anchor="{anchor}"' if anchor != "start" else ""
+ w = len(s) * (5.7 if size >= 12 else size * 0.5)
+ x0 = x if anchor == "start" else (x - w / 2 if anchor == "middle" else x - w)
+ assert 4 <= x0 and x0 + w <= W - 4, f"text iese din viewBox: {s!r} ({x0:.0f}..{x0 + w:.0f})"
+ LABELS.append((x0, x0 + w, y, s))
+ out.append(f'{s}')
+
+
+def hline(x1, x2, y, color, dash, width=1.5):
+ out.append(f'')
+
+
+def line(x1, y1, x2, y2, color, dash="", width=1.2, marker=""):
+ d = f' stroke-dasharray="{dash}"' if dash else ""
+ m = f' marker-end="url(#{marker})"' if marker else ""
+ out.append(f'')
+
+
+def rect(x0_, y0_, x1, y1, color, fill_op=0.10, dash=""):
+ d = f' stroke-dasharray="{dash}"' if dash else ""
+ out.append(f'')
+
+
+def candles(bars, x0, gap, yf, bw):
+ for i, (o, h, l, c) in enumerate(bars):
+ cx = x0 + i * gap
+ color = GREEN if c >= o else RED
+ line(cx, yf(h), cx, yf(l), "#1a1a1a", width=1.2)
+ top_ = yf(max(o, c))
+ hh = max(yf(min(o, c)) - top_, 1.6)
+ out.append(f'')
+
+
+def stack(items, x_line):
+ y_prev = None
+ for y, lines, fill in sorted(items, key=lambda t: t[0]):
+ y_lab = y if y_prev is None or y >= y_prev + 16 else y_prev + 16
+ if y_lab - y > 2:
+ out.append(f'')
+ else:
+ line(x_line + 2, y - 4, COL_X - 4, y_lab - 4, fill, width=0.8)
+ for j, s in enumerate(lines):
+ text(COL_X, y_lab + j * 14, s, fill=fill, size=10)
+ y_prev = y_lab + (len(lines) - 1) * 14
+
+
+def seg_dist(p, a, b):
+ (px, py), (ax, ay), (bx, by) = p, a, b
+ dx, dy = bx - ax, by - ay
+ t = 0.0 if dx == dy == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy)
+ / (dx * dx + dy * dy)))
+ return ((px - ax - t * dx) ** 2 + (py - ay - t * dy) ** 2) ** 0.5
+
+
+out.append(''
+ ''
+ '')
+text(20, 24, "M2D long, tot trade-ul: 15 min dă direcția, 3 min dă intrarea",
+ bold=True, size=14)
+text(20, 42, "Graficul din dreapta nu e altă bucată de piață: e aceeași, mărită. "
+ "O candelă de 15 min = 5 candele de 3 min.", fill="#555", size=11)
+
+top = 80
+text(20, top - 12, "15 min: direcția", fill="#555", size=11)
+text(FRAME_L, top - 12, f"3 min: descompunerea ultimelor {NZ} candele de 15 min",
+ fill="#555", size=11)
+
+zlo_, zhi_ = SL, max(b[1] for b in FINE)
+pad = PAD * (zhi_ - zlo_)
+lo, hi = zlo_ - pad, zhi_ + pad
+
+bmax = max(b[1] for b in BIG)
+bmin = min(min(b[2] for b in BIG), lo)
+bs, ss = PLOT_H / (bmax - bmin), PLOT_H / (hi - lo)
+yb = lambda p: top + (bmax - p) * bs # noqa: E731
+ys = lambda p: top + (hi - p) * ss # noqa: E731
+
+# ---- panoul de 15 min ------------------------------------------------------
+sx_a, sx_b = BX0 + SIG_B * BGAP - 8, BX0 + SIG_B * BGAP + 8
+rect(sx_a, yb(BASE[SIG_B][1]) - 5, sx_b, yb(BASE[SIG_B][2]) + 5, CYAN)
+line(BX0 + SIG_B * BGAP, yb(BASE[SIG_B][2]) + 6, BX0 + SIG_B * BGAP, top + PLOT_H + 12,
+ CYAN, width=1)
+text(BX0 + SIG_B * BGAP - 30, top + PLOT_H + 26, "Buy Momentum pe 15 min:",
+ fill=CYAN, anchor="middle", bold=True, size=11)
+text(BX0 + SIG_B * BGAP - 30, top + PLOT_H + 39, "de aici ai voie doar long",
+ fill=CYAN, anchor="middle", size=11)
+
+# caseta de lupă peste exact candelele de 15 min care se sparg în dreapta
+lx0, lx1 = BX0 + len(BASE) * BGAP - 7, BX0 + (len(BIG) - 1) * BGAP + 8
+ly0, ly1 = yb(hi), yb(lo)
+rect(lx0, ly0, lx1, ly1, BLUE, fill_op=0.06, dash="4 3")
+
+# minimul de referință al pullback-ului: trăiește pe TF-ul mare (pe panoul mic
+# e sub scară), deci se marchează aici și se trimite acolo din etichetă
+hline(BX0 - 14, lx1, yb(ref_low), "#555", "2 3", width=1.2)
+text(24, yb(ref_low) + 12, "minimul din care a pornit", fill="#555", size=10)
+text(24, yb(ref_low) + 26, f"mișcarea: {ref_low:.2f}", fill="#555", size=10)
+
+hline(BX0 - 14, lx1, yb(LEVEL), LINK, "3 3", width=1.0)
+candles(BIG, BX0, BGAP, yb, BW)
+
+# ---- ghidajele de lupă -----------------------------------------------------
+guides = [((lx1, ly0), (FRAME_L, top)), ((lx1, ly1), (FRAME_L, top + PLOT_H))]
+for a_, b_ in guides:
+ line(a_[0], a_[1], b_[0], b_[1], BLUE, dash="5 3", width=1.1)
+
+# ---- nivelul comun: conector între panouri, numărul scris o singură dată ---
+ycb, ycs = yb(LEVEL), ys(LEVEL)
+
+
+def clearance(mx):
+ conn = [((lx1, ycb), (mx, ycb)), ((mx, ycb), (mx, ycs)), ((mx, ycs), (FRAME_L, ycs))]
+ pts = [(sx + (ex - sx) * t / 24, sy + (ey - sy) * t / 24)
+ for (sx, sy), (ex, ey) in conn for t in range(25)]
+ return min(seg_dist(q, ga, gb) for q in pts if q[0] >= lx1 + 10 for ga, gb in guides)
+
+
+mid = max(range(int(lx1) + 14, int(FRAME_L) - 13), key=clearance)
+dmin = clearance(mid)
+assert dmin > 10, f"conectorul trece la {dmin:.1f} px de un ghidaj"
+out.append(f'')
+tx = min(mid + 5, FRAME_L - 60)
+text(tx, (ycb + ycs) / 2 + 4, f"{LEVEL:.2f}", fill="#777", size=10)
+text(tx, (ycb + ycs) / 2 + 17, "același preț", fill="#999", size=10)
+
+# ---- panoul de 3 min -------------------------------------------------------
+y_e, y_sl, y_tp, y_tr = ys(ENTRY), ys(SL), ys(TP1), ys(TRAIL)
+assert abs((y_sl - y_e) / (y_e - y_tp) - 1) < 1e-6, "SL-intrare != intrare-TP1 în px"
+assert top + PLOT_H - y_sl > 6, "linia de stop e lipită de marginea cadrului"
+frame_r = SX0 + (len(FINE) - 1) * SGAP + 8
+rect(FRAME_L, top, frame_r, top + PLOT_H, BLUE, fill_op=0.03)
+
+# zona de evidențiere peste pullback + trigger: acolo se decide intrarea
+zx0 = SX0 - 7
+zx1 = SX0 + TRIG * SGAP + 7
+rect(zx0, ys(max(b[1] for b in FINE[:TRIG + 1])) - 5, zx1, ys(SL) + 5, GREEN)
+
+hline(FRAME_L, frame_r, ycs, LINK, "3 3", width=1.0)
+hline(FRAME_L, frame_r, y_tp, GREEN, "2 3")
+hline(FRAME_L, frame_r, y_e, GREEN, "6 3")
+hline(FRAME_L, frame_r, y_sl, RED, "6 3")
+hline(FRAME_L, frame_r, y_tr, "#b06000", "2 2", width=1.2)
+candles(FINE, SX0, SGAP, ys, SW)
+
+stack([(y_tp + 4, [f"TP1 {TP1:.2f} = 1:1", "iese prima tranșă"], GREEN),
+ (y_tr + 4, [f"bull trail {TRAIL:.2f}", "urcă, nu coboară"], "#b06000"),
+ (y_e + 4, [f"intrare {ENTRY:.2f}", "close-ul trigger-ului"], GREEN),
+ (y_sl + 4, [f"stop {SL:.2f}", f"minimul a {STOP_BARS} bare"], RED)],
+ frame_r)
+
+# paranteza pullback-ului, sub cadru
+pxa, pxb = SX0 - 5, SX0 + (PULLBACK_N - 1) * SGAP + 5
+out.append(f'')
+text((pxa + pxb) / 2, top + PLOT_H + 22, f"M: pullback, {PULLBACK_N} bare",
+ fill=RED, anchor="middle", bold=True, size=11)
+text((pxa + pxb) / 2, top + PLOT_H + 36, "fără să rupă minimul",
+ fill=RED, anchor="middle", size=11)
+
+# ieșirea: săgeată în sus spre bara 18, cu explicația sub cadru, la dreapta
+exx = SX0 + EXIT_BAR * SGAP
+line(exx, top + PLOT_H + 14, exx, ys(FINE[EXIT_BAR][2]) + 5, "#b06000",
+ dash="3 3", width=1.1, marker="pt-o")
+text(frame_r, top + PLOT_H + 22, "close sub trail",
+ fill="#b06000", anchor="end", bold=True, size=11)
+text(frame_r, top + PLOT_H + 36, f"ieșire {EXIT:.2f} = {R:.1f}R",
+ fill="#b06000", anchor="end", size=11)
+
+# trigger-ul: săgeată în sus spre bara 9, cu explicația dedesubt
+tgx = SX0 + TRIG * SGAP
+line(tgx, top + PLOT_H + 50, tgx, ys(FINE[TRIG][2]) + 5, GREEN, width=1.6, marker="pt")
+text(pxa, top + PLOT_H + 58, "A: primul Buy după pullback — aici intri,",
+ fill=GREEN, bold=True, size=11)
+text(pxa, top + PLOT_H + 72, "pentru că 15 min e încă în Buy.", fill=GREEN, size=11)
+text(pxa, top + PLOT_H + 86, "Wick-urile nu contează: se citește close-ul.",
+ fill="#555", size=11)
+
+LAB_N = len(LABELS) # etichetele de pe panouri; ce urmează e subsol
+H = top + PLOT_H + 108
+text(20, H - 6, f"Serie construită, fără ticker. Risc = {ENTRY:.2f} − {SL:.2f} = {RISK:.2f}; "
+ f"TP1 la aceeași distanță în sus; ieșirea pe trail la {R:.1f}R.",
+ fill="#555", size=11)
+H += 12
+
+# ---- prețul fiecărui panou rămâne pe panoul lui ---------------------------
+COMUNE = {round(LEVEL, 2)}
+MARE = {"ref_low": ref_low}
+MIC = {k_: v for k_, v in (("entry", ENTRY), ("sl", SL), ("tp", TP1), ("trail", TRAIL))
+ if round(v, 2) not in COMUNE}
+for x0_, _x1, _y, s_ in LABELS[:LAB_N]:
+ rau = MARE if x0_ >= MIDX else MIC
+ for kk, v in rau.items():
+ assert f"{v:.2f}" not in s_, \
+ f"eticheta {s_!r} de pe panoul {'mic' if x0_ >= MIDX else 'mare'} " \
+ f"conține prețul {kk}={v:.2f} al celuilalt panou"
+cx_ = [x0_ for x0_, _x1, _y, s_ in LABELS[:LAB_N] if f"{LEVEL:.2f}" in s_]
+assert len(cx_) == 1 and lx1 < cx_[0] < FRAME_L, \
+ f"{LEVEL:.2f} nu e scris exact o dată, în cotul conectorului ({cx_})"
+for nume, v in (("intrarea", ENTRY), ("stopul", SL), ("TP1", TP1), ("trail-ul", TRAIL)):
+ assert any(f"{v:.2f}" in s_ and x0_ >= MIDX for x0_, _x1, _y, s_ in LABELS[:LAB_N]), \
+ f"{nume} nu apare pe panoul mic"
+
+for a in range(len(LABELS)):
+ assert 0 < LABELS[a][2] < H, f"etichetă în afara înălțimii: {LABELS[a][3]!r}"
+ for b in range(a + 1, len(LABELS)):
+ xa0, xa1, ya_, sa = LABELS[a]
+ xb0, xb1, yb_, sb = LABELS[b]
+ assert not (abs(ya_ - yb_) < 13 and xa0 < xb1 - 4 and xb0 < xa1 - 4), \
+ f"etichete suprapuse: {sa!r} / {sb!r}"
+
+svg = (f'")
+assert "\n\n" not in svg
+assert ".md:" not in svg, "trimiteri la surse în SVG"
+OUT.write_text(svg, encoding="utf-8")
+print(f"scris {OUT} ({W}x{H:.0f}) | 15 min: {len(BIG)} candele, lupă pe ultimele {NZ} "
+ f"= {len(FINE)} bare de 3 min | nivel comun {LEVEL:.2f} | "
+ f"intrare {ENTRY:.2f} SL {SL:.2f} risc {RISK:.2f} TP1 {TP1:.2f} la bara {TP_BAR} | "
+ f"trail {TRAIL:.2f}, ieșire bara {EXIT_BAR} la {EXIT:.2f} = {R:.2f}R | "
+ f"pullback peste {span} candele de 15 min, trigger în candela mare {trig_big}")
diff --git a/gen/strategii/m2d_ansamblu.svg b/gen/strategii/m2d_ansamblu.svg
index 47c536d..6333415 100644
--- a/gen/strategii/m2d_ansamblu.svg
+++ b/gen/strategii/m2d_ansamblu.svg
@@ -1,117 +1,111 @@
-