#!/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")