#!/usr/bin/env python3 """Cele 3 stele (Trinity) — RS, Power și volum, toate în cadranul 1. Diagramă conceptuală (NU grafic de preț): pentru `summaries/17_STRATEGII/04_selectia.md`, Nivel 3, „Cele 3 stele (Trinity)". Citește specificația `gen/strategii/selectia_trinity.spec.txt` și scrie `gen/strategii/selectia_trinity.svg`. Nu folosește `svg_candles` / `pchart`: SVG-ul e construit ca string, doar stdlib. Fără `` — toate coordonatele sunt absolute. Ce arată: - stânga: un simbol cu RS, Power și volum toate în cadranul 1 -> 3 stele; - dreapta: un simbol cu doar două din trei în cadranul 1 -> nu e trinity; - fiecare indicator e o mini-histogramă cu linia de 0 și media mobilă peste ea; cadranul 1 = ultima bară peste 0 ȘI peste MA (definiția de la pasul 5 al capitolului); - nota că Power e proprietar, fără formulă publicată; - nota că „trinity" are în material și alte sensuri — aici e numai ăsta. Desenul e pur calitativ: nicio valoare de RS / Power / volum pe etichete. Rulare: .venv/bin/python gen/strategii/selectia_trinity.py """ import html import re import xml.etree.ElementTree as ET from pathlib import Path W, H = 700, 868 BLACK = "#000000" GRAY = "#777777" RED = "#c00" GREEN = "#188a3e" ORANGE = "#b06000" GREEN_BG = "#eef7f0" RED_BG = "#fdeeee" NOTE_BG = "#fdf0e0" GRAY_BG = "#f7f7f7" # ------------------------------------------------------- serii (calitative) # Cadranul 1 = ultima bară peste 0 ȘI peste media ei mobilă. IN_Q1 = [-2, -1, 0, 1, 2, 2, 3, 4, 5, 6] # urcă: ultima e peste 0 și peste MA IN_Q3 = [6, 6, 5, 5, 4, 4, 3, 3, 2, 2] # coboară: peste 0, dar SUB MA def ma4(vals): """Media mobilă pe 4, ca la indicatorii cursului (histogramă + MA peste ea).""" return [sum(vals[max(0, i - 3):i + 1]) / len(vals[max(0, i - 3):i + 1]) for i in range(len(vals))] def in_cadranul_1(vals): """Ultima bară: peste 0 ȘI peste media ei mobilă.""" return vals[-1] > 0 and vals[-1] > ma4(vals)[-1] # ------------------------------------------------------- geometrie COL_W = 300.0 COL_AX, COL_BX = 32.0, 368.0 COL_Y0 = 110.0 ROW_H = 118.0 # un indicator pe rând ROW_Y0 = COL_Y0 + 60.0 # primul rând, sub titlul coloanei COL_H = 60.0 + 3 * ROW_H + 62.0 TEXTS = [] # (x, y, s, size, weight, fill, anchor) RECTS = [] # (x, y, w, h, tag) STARS = {} # tag coloană -> câte stele s-au desenat def add(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"): TEXTS.append((float(x), float(y), s, size, weight, fill, anchor)) return s def est_w(s, size, weight): factor = 0.62 if weight == "bold" else 0.56 return len(s) * size * factor def txt(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"): return (f'' f'{html.escape(s, quote=False)}') def rect(x, y, w, h, tag, fill="#ffffff", stroke=GRAY, sw=1.5, dash=None): RECTS.append((float(x), float(y), float(w), float(h), tag)) d = f' stroke-dasharray="{dash}"' if dash else "" return (f'') def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None): d = f' stroke-dasharray="{dash}"' if dash else "" return (f'') def stea(cx, cy, r, fill, stroke): """Steluță cu 5 colțuri, desenată ca poligon (fără font de emoji).""" import math pts = [] for k in range(10): ang = -math.pi / 2 + k * math.pi / 5 rad = r if k % 2 == 0 else r * 0.42 pts.append(f"{cx + rad * math.cos(ang):.1f},{cy + rad * math.sin(ang):.1f}") return (f'') def histograma(x0, y0, w, h, vals, culoare): """Mini-histogramă: barele, linia de 0 și media mobilă punctată peste ea.""" yz = y0 + h / 2.0 unit = (h / 2.0 - 4) / max(abs(v) for v in vals) bw = w / (len(vals) * 1.6) xs = [x0 + w * (i + 0.5) / len(vals) for i in range(len(vals))] out = [f'', line(x0, yz, x0 + w, yz, BLACK, 1.0)] for i, v in enumerate(vals): top = yz - v * unit if v > 0 else yz out.append(f'') pts = " ".join(f"{xs[i]:.1f},{yz - m * unit:.1f}" for i, m in enumerate(ma4(vals))) out.append(f'') return out, xs[-1], (yz - vals[-1] * unit if vals[-1] > 0 else yz) def coloana(tag, cx0, titlu, subtitlu, serii, culoare_cadru, fundal): """O coloană: trei indicatori, fiecare cu histograma lui și verdictul lui.""" out = [rect(cx0, COL_Y0, COL_W, COL_H, f"col_{tag}", fill=fundal, stroke=culoare_cadru, sw=1.8)] add(cx0 + COL_W / 2, COL_Y0 + 26, titlu, 16, "bold", culoare_cadru) add(cx0 + COL_W / 2, COL_Y0 + 46, subtitlu, 13, "normal", BLACK) stele = 0 for i, (nume, vals) in enumerate(serii): y = ROW_Y0 + i * ROW_H q1 = in_cadranul_1(vals) col = GREEN if q1 else RED add(cx0 + 16, y + 16, nume, 14, "bold", BLACK, "start") hist, _x_ult, _y_ult = histograma(cx0 + 16, y + 26, COL_W - 90, 56, vals, col) out += hist if q1: stele += 1 out.append(stea(cx0 + COL_W - 34, y + 54, 15, "#e0a800", "#8a6d00")) add(cx0 + 16, y + 100, "cadranul 1: peste 0 și peste MA", 13, "normal", GREEN, "start") else: out.append(line(cx0 + COL_W - 46, y + 42, cx0 + COL_W - 22, y + 66, RED, 2.4)) out.append(line(cx0 + COL_W - 22, y + 42, cx0 + COL_W - 46, y + 66, RED, 2.4)) add(cx0 + 16, y + 100, "cadranul 3: peste 0, dar sub MA", 13, "normal", RED, "start") STARS[tag] = stele return out # =============================================================== CONȚINUT p = [] add(W / 2, 32, "Cele 3 stele (Trinity): RS, Power și volum, toate în cadranul 1", 17, "bold") add(W / 2, 56, "Sortare finală, când după cei șase pași ți-au rămas prea multe simboluri.", 13, "normal", GRAY) add(W / 2, 78, "Cadranul 1 = ultima bară a indicatorului e peste 0 ȘI peste media lui mobilă (linia punctată).", 13, "normal", GRAY) SERII_A = [("Relative strength", IN_Q1), ("Power", IN_Q1), ("Volum", IN_Q1)] SERII_B = [("Relative strength", IN_Q1), ("Power", IN_Q3), ("Volum", IN_Q1)] p += coloana("a", COL_AX, "Simbol A — 3 stele", "toate trei arată în aceeași direcție", SERII_A, GREEN, GREEN_BG) p += coloana("b", COL_BX, "Simbol B — 2 din 3", "Power rămâne în urmă: nu e trinity", SERII_B, RED, RED_BG) # verdictul fiecărei coloane, sub cele trei rânduri VY = ROW_Y0 + 3 * ROW_H + 24 add(COL_AX + COL_W / 2, VY, "3 stele → îl iei primul la analiză", 14, "bold", GREEN) add(COL_BX + COL_W / 2, VY, "2 stele → nu e „3 stele”", 14, "bold", RED) # ---- ce înseamnă aici, și ce NU înseamnă BAND_Y = COL_Y0 + COL_H + 22 p.append(rect(32, BAND_Y, 636, 98, "band_sens", fill=GRAY_BG, stroke=GRAY, sw=1.2)) add(50, BAND_Y + 26, "În capitolul ăsta „3 stele” / „trinity” înseamnă exact un lucru:", 14, "bold", BLACK, "start") add(50, BAND_Y + 48, "relative strength, Power și volumul, toate trei în cadranul 1 pe același simbol.", 13, "normal", BLACK, "start") add(50, BAND_Y + 74, "Cuvântul apare în material și cu alte sensuri (la MDSRT: trei indicatori în divergență", 13, "normal", GRAY, "start") add(50, BAND_Y + 92, "simultană). Acelea sunt altceva și nu se aplică aici.", 13, "normal", GRAY, "start") # ---- nota Power NOTE_Y = BAND_Y + 114 p.append(rect(32, NOTE_Y, 636, 92, "note_power", fill=NOTE_BG, stroke=ORANGE, sw=1.4)) add(50, NOTE_Y + 26, "Power e proprietar: măsoară tăria trendului, dar formula nu e publicată.", 13, "bold", BLACK, "start") add(50, NOTE_Y + 48, "Citești cadranul din indicator, nu îl poți reconstrui singur.", 13, "normal", BLACK, "start") add(50, NOTE_Y + 72, "De aceea sortarea după 3 stele e o rafinare, nu un pas obligatoriu.", 13, "normal", GRAY, "start") # =============================================================== EMITERE head = (f'') body = [head, f''] body += p for x, y, s, size, weight, fill, anchor in TEXTS: body.append(txt(x, y, s, size, weight, fill, anchor)) body.append("") svg = "\n".join(body) + "\n" # =============================================================== VERIFICARE # 1. seriile desenate chiar satisfac definiția cadranului 1 (nu e tautologic: # verifică ultima bară față de 0 și față de media mobilă calculată din serie) assert in_cadranul_1(IN_Q1), "seria „cadranul 1” nu e în cadranul 1" assert IN_Q3[-1] > 0 and IN_Q3[-1] < ma4(IN_Q3)[-1], \ "seria „cadranul 3” trebuie să fie peste 0, dar sub MA" # 2. numărul de stele desenate = numărul de indicatori în cadranul 1 assert STARS["a"] == sum(in_cadranul_1(v) for _, v in SERII_A) == 3, \ f"coloana A: {STARS['a']} stele, dar seriile dau altceva" assert STARS["b"] == sum(in_cadranul_1(v) for _, v in SERII_B) == 2, \ f"coloana B: {STARS['b']} stele, dar seriile dau altceva" assert svg.count("= 13, în pânză, fără suprapuneri boxes = [] for x, y, s, size, weight, fill, anchor in TEXTS: assert size >= 13, f"font sub 13px: {s!r} ({size})" w = est_w(s, size, weight) if anchor == "end": left = x - w elif anchor == "start": left = x else: left = x - w / 2.0 right = left + w top, bottom = y - size * 0.78, y + size * 0.22 assert -2 <= left and right <= W + 2, f"text în afara pânzei orizontal: {s!r}" assert 0 <= top and bottom <= H, f"text în afara pânzei vertical: {s!r}" boxes.append((left, right, top, bottom, s)) for i in range(len(boxes)): for j in range(i + 1, len(boxes)): l1, r1, t1, b1, s1 = boxes[i] l2, r2, t2_, b2, s2 = boxes[j] if r1 > l2 + 2 and r2 > l1 + 2 and b1 > t2_ + 2 and b2 > t1 + 2: raise AssertionError(f"etichete suprapuse: {s1!r} / {s2!r}") # 8. stil + XML valid, fără transformări assert "filter=" not in svg and "gradient" not in svg, "fără umbre/gradienți" assert "transform=" not in svg, "fără : coordonate absolute" assert 'font-family="system-ui, sans-serif"' in svg, "lipsește fontul cerut" assert svg.lstrip().startswith("" assert ET.fromstring(svg).tag.endswith("svg") OUT = Path(__file__).with_suffix(".svg") OUT.write_text(svg, encoding="utf-8") print(f"scris {OUT} ({len(svg)} bytes, {len(TEXTS)} etichete, " f"stele: A={STARS['a']}, B={STARS['b']}, {W}x{H}), verificare OK")