#!/usr/bin/env python3 """Secvența M → A a strategiei M2D — schemă conceptuală pe două panouri. Pentru `strategii/03_m2d.md`, marcajul ``. Nu e grafic de preț (spec: „schemă pe două panouri, nu candele de trade”): SVG scris de mână, doar stdlib — nu se folosește svg_candles / pchart. Ce arată (toate etichetele vin din spec; nicio cifră de preț nu se inventează): - panoul de sus, LONG: pe TF mare (15 min) polilinia urcă și „rămâne în Buy pe tot parcursul”; pe TF mic (3 min) urcă → pullback (M: Sell Momentum) → urcă peste vârful anterior (A: Buy Momentum — TRIGGER, intri long); - panoul de jos, SHORT: oglindit — TF mare coboară („rămâne în Sell”), TF mic coboară → pullback în sus (M: Buy Momentum) → coboară sub minimul anterior (A: Sell Momentum — TRIGGER, intri short); - două linii verticale punctate leagă momentul M și momentul A de rândul de sus (verificarea că TF mare rămâne în aceeași direcție); - textul de subsol: „Secvența contează, nu starea...”. Rulare: python3 gen/strategii/m2d_secventa.py -> gen/strategii/m2d_secventa.svg """ from pathlib import Path from xml.sax.saxutils import escape OUT = Path(__file__).with_suffix(".svg") W, H = 700, 780 BLACK, GRAY, RED, GREEN, BLUE, CYAN, WHITE = ( "#000000", "#777777", "#c00", "#188a3e", "#1a5fb4", "#00b8c4", "#ffffff") PANEL_BG, PANEL_EDGE = "#fafafa", "#ddd" # ---------------------------------------------------------------- geometrie # Fiecare polilinie: listă de (x, y). y mic = sus. # Index: 0 = start, 1 = vârf/trough anterior, 2 = M, 3 = A, 4 = continuare. LONG_TOP = [(40, 162), (150, 148), (250, 138), (430, 128), (660, 118)] LONG_BOT = [(40, 255), (180, 225), (250, 282), (430, 210), (660, 198)] SHORT_TOP = [(40, 430), (150, 444), (250, 454), (430, 464), (660, 474)] SHORT_BOT = [(40, 527), (180, 562), (250, 500), (430, 574), (660, 588)] L_M, L_A, S_M, S_A = 2, 3, 2, 3 # indicii momentelor M și A PANELS = { "LONG": (14, 92, 672, 250), "SHORT": (14, 362, 672, 270), } TEXTS = [] # (x, y, text, size, weight, anchor, color) pentru verificare def txt(x, y, s, size=13, weight="400", fill=BLACK, anchor="middle"): TEXTS.append((x, y, s, size, weight, anchor, fill)) return (f'' 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 poly(pts, stroke, width=2.0): p = " ".join(f"{x:.1f},{y:.1f}" for x, y in pts) return f'' def dot(x, y, fill, r=5.0): return f'' def build(): p = [ f'', '', f'', txt(W / 2, 30, "Secvența M → A: nu e o stare, e o secvență", 19, "700", BLACK), txt(W / 2, 52, "M2D — același moment, două time frame-uri: " "TF mare dă direcția, TF mic dă trigger-ul", 13, "400", GRAY), txt(W / 2, 72, "sus = LONG; jos = SHORT — aceleași momente, în oglindă", 13, "400", GRAY), ] # ------------------------------------------------------ panouri for name, (px, py, pw, ph) in PANELS.items(): p.append(f'') # ------------------------------------------------------ panoul LONG p.append(txt(30, 116, "LONG", 15, "700", GREEN, "start")) p.append(txt(30, 140, "TF mare (ex. 15 min)", 13, "400", GRAY, "start")) p.append(txt(660, 114, "rămâne în Buy pe tot parcursul", 13, "400", GREEN, "end")) for x, y_top, y_bot in ((LONG_TOP[L_M][0], LONG_TOP[L_M][1], LONG_BOT[L_M][1]), (LONG_TOP[L_A][0], LONG_TOP[L_A][1], LONG_BOT[L_A][1])): p.append(line(x, y_top, x, y_bot, "#999999", 1.2, "4 4")) p.append(poly(LONG_TOP, GREEN)) p.append(dot(*LONG_TOP[1], CYAN)) p.append(txt(185, 126, "Buy Momentum", 13, "400", CYAN, "start")) p.append(txt(30, 202, "TF mic (ex. 3 min)", 13, "400", GRAY, "start")) p.append(poly(LONG_BOT, "#333333")) p.append(dot(*LONG_BOT[L_M], RED)) p.append(txt(262, 296, "M: Sell Momentum (pullback)", 13, "400", RED, "start")) p.append(dot(*LONG_BOT[L_A], GREEN, r=6)) p.append(txt(660, 240, "A: Buy Momentum — TRIGGER, intri long", 13, "400", GREEN, "end")) # ------------------------------------------------------ panoul SHORT p.append(txt(30, 386, "SHORT", 15, "700", RED, "start")) p.append(txt(30, 410, "TF mare (ex. 15 min)", 13, "400", GRAY, "start")) p.append(txt(660, 494, "rămâne în Sell pe tot parcursul", 13, "400", RED, "end")) for x, y_top, y_bot in ((SHORT_TOP[S_M][0], SHORT_TOP[S_M][1], SHORT_BOT[S_M][1]), (SHORT_TOP[S_A][0], SHORT_TOP[S_A][1], SHORT_BOT[S_A][1])): p.append(line(x, y_top, x, y_bot, "#999999", 1.2, "4 4")) p.append(poly(SHORT_TOP, RED)) p.append(dot(*SHORT_TOP[1], RED)) p.append(txt(168, 432, "Sell Momentum", 13, "400", RED, "start")) p.append(txt(30, 470, "TF mic (ex. 3 min)", 13, "400", GRAY, "start")) p.append(poly(SHORT_BOT, "#333333")) p.append(dot(*SHORT_BOT[S_M], CYAN)) p.append(txt(238, 494, "M: Buy Momentum (pullback)", 13, "400", CYAN, "end")) p.append(dot(*SHORT_BOT[S_A], RED, r=6)) p.append(txt(660, 610, "A: Sell Momentum — TRIGGER, intri short", 13, "400", RED, "end")) # ------------------------------------------------------ subsol p.append(f'') p.append(txt(W / 2, 676, "Secvența contează, nu starea: mare în Buy → mic în Sell → " "mic în Buy", 13, "400", BLACK)) p.append(txt(W / 2, 696, "(cu mare încă în Buy). Un Buy direct pe TF mic, fără Sell " "între ele,", 13, "400", BLACK)) p.append(txt(W / 2, 716, "NU e trigger (vezi 1.3).", 13, "400", BLACK)) p.append(txt(W / 2, 746, "Sursă: 14_strategia_1_m2d.md:30-38, 172-180; " "09_QA/08_2026-01-20.md:65-67", 13, "400", GRAY)) 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: panourile nu se suprapun, sunt în panză long_ = PANELS["LONG"] short_ = PANELS["SHORT"] assert long_[1] + long_[3] < short_[1], "panourile LONG/SHORT se suprapun" assert short_[1] + short_[3] < 648, "panoul SHORT intră în subsol" for _, (px, py, pw, ph) in PANELS.items(): assert px >= 0 and py >= 0 and px + pw <= W and py + ph <= H # 2. secvența LONG: TF mare rămâne în Buy (urcă monoton: y strict descrescător) ys = [y for _, y in LONG_TOP] assert all(ys[i] > ys[i + 1] for i in range(len(ys) - 1)), \ "LONG: TF mare nu urcă monoton (nu rămâne în Buy)" # M = pullback (minim local), A = reluare peste vârful anterior assert LONG_BOT[L_M][1] > LONG_BOT[L_M - 1][1] and \ LONG_BOT[L_M][1] > LONG_BOT[L_M + 1][1], "LONG: M nu e un minim local" assert LONG_BOT[L_A][1] < LONG_BOT[1][1], \ "LONG: A nu urcă peste vârful anterior (pullback-ul)" assert LONG_TOP[L_M][0] < LONG_TOP[L_A][0], "LONG: M trebuie înainte de A" assert LONG_TOP[L_M][0] == LONG_BOT[L_M][0] and \ LONG_TOP[L_A][0] == LONG_BOT[L_A][0], \ "LONG: liniile verticale nu leagă exact M și A de rândul de sus" # 3. secvența SHORT: oglindit ys = [y for _, y in SHORT_TOP] assert all(ys[i] < ys[i + 1] for i in range(len(ys) - 1)), \ "SHORT: TF mare nu coboară monoton (nu rămâne în Sell)" assert SHORT_BOT[S_M][1] < SHORT_BOT[S_M - 1][1] and \ SHORT_BOT[S_M][1] < SHORT_BOT[S_M + 1][1], "SHORT: M nu e un maxim local" assert SHORT_BOT[S_A][1] > SHORT_BOT[1][1], \ "SHORT: A nu coboară sub minimul anterior" assert SHORT_TOP[S_M][0] < SHORT_TOP[S_A][0], "SHORT: M trebuie înainte de A" assert SHORT_TOP[S_M][0] == SHORT_BOT[S_M][0] and \ SHORT_TOP[S_A][0] == SHORT_BOT[S_A][0], \ "SHORT: liniile verticale nu leagă exact M și A de rândul de sus" # 4. verticalele chiar traversează de la rândul de sus până la moment assert LONG_TOP[2][1] < LONG_BOT[L_M][1] assert LONG_TOP[3][1] < LONG_BOT[L_A][1] assert SHORT_TOP[2][1] < SHORT_BOT[S_M][1] assert SHORT_TOP[3][1] < SHORT_BOT[S_A][1] # 5. 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})" # 6. 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}") # 7. niciun text nu se tastează peste o polilinie (coliziune text-linie) segments = [(LONG_TOP, PANELS["LONG"]), (LONG_BOT, PANELS["LONG"]), (SHORT_TOP, PANELS["SHORT"]), (SHORT_BOT, PANELS["SHORT"])] 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 top, bot = y - size + 1, y for pts, _ in segments: for (x1, y1), (x2, y2) in zip(pts, pts[1:]): xa, xb = min(x1, x2), max(x1, x2) if xb < left - 2 or xa > right + 2: continue for xc in (max(xa, left), min(xb, right)): t = 0 if x2 == x1 else (xc - x1) / (x2 - x1) yc = y1 + t * (y2 - y1) if top - 1 <= yc <= bot + 1: raise AssertionError( f"text peste polilinie: {s!r} (y={y}, linia y={yc:.0f})") # 8. etichetele obligatorii din spec sunt prezente for cheie in ("LONG", "SHORT", "TF mare (ex. 15 min)", "TF mic (ex. 3 min)", "rămâne în Buy pe tot parcursul", "rămâne în Sell pe tot parcursul", "Buy Momentum", "Sell Momentum", "M: Sell Momentum (pullback)", "A: Buy Momentum — TRIGGER, intri long", "M: Buy Momentum (pullback)", "A: Sell Momentum — TRIGGER, intri short", "Secvența contează, nu starea", "NU e trigger (vezi 1.3)"): assert cheie in svg, f"lipsește din desen: {cheie!r}" # 9. fără cifre de preț inventate (spec: diagramă fără niveluri) for interzis in ("$", "%", "€", "intrare ", "SL ", "TP"): 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"{len(LONG_TOP) + len(LONG_BOT) + len(SHORT_TOP) + len(SHORT_BOT)} puncte")