#!/usr/bin/env python3 """Relative strength — cele 4 cadrane (matrice 2x2). Diagramă conceptuală (NU grafic de preț): pentru `strategii/04_selectia.md`, Nivel 1, §1.2 F4 „Relative strength în cadranul 1-2". Citește specificația `gen/strategii/selectia_cadrane.spec.txt` și scrie `gen/strategii/selectia_cadrane.svg`. Nu folosește `svg_candles` / `pchart`: SVG-ul e construit ca string, doar stdlib. Axele (așa cum le dă spec-ul): - vertical: histograma RS față de linia de 0 (peste / sub 0); - orizontal: histograma RS față de MA (peste / sub MA). Cadranele (definițiile din spec, preluate din 03_MODUL_2/06_indicatorul_de_performanta.md:31-39): 1 supraperformanță peste 0 ȘI peste MA -> LONG 2 îmbunătățire sub 0, dar peste MA -> LONG (zona de cumpărat) 3 înrăutățire peste 0, dar sub MA -> SHORT 4 subperformanță sub 0 ȘI sub MA -> SHORT / stai pe margine Banda de jos e avertismentul despre cadranul 4 (08_ANALIZA_ZILNICA/ 11_selectia_simbolului.md:493-508), plus nota mică despre eroarea de transcriere (03_MODUL_2/06 ...:41-48) — marcată explicit ca NOTĂ, nu ca definiție a cursului. Fără cifre de RS inventate: desenul e pur calitativ. Rulare: .venv/bin/python gen/strategii/selectia_cadrane.py """ import html from pathlib import Path W, H = 700, 812 BLACK = "#000000" GRAY = "#777777" RED = "#c00" GREEN = "#188a3e" BLUE = "#1a5fb4" ORANGE = "#b06000" GREEN_BG = "#eef7f0" RED_BG = "#fdeeee" # ---------------------------------------------------------------- geometrie PLOT_X0, PLOT_X1 = 130.0, 570.0 PLOT_Y0, PLOT_Y1 = 134.0, 502.0 CX = (PLOT_X0 + PLOT_X1) / 2.0 # 350 CY = (PLOT_Y0 + PLOT_Y1) / 2.0 # 266 GUT = 4.0 CW = (PLOT_X1 - PLOT_X0 - GUT) / 2.0 # 218 CH = (PLOT_Y1 - PLOT_Y0 - GUT) / 2.0 # 138 Q3 = {"x": PLOT_X0, "y": PLOT_Y0} # peste 0, sub MA Q1 = {"x": CX + GUT / 2, "y": PLOT_Y0} # peste 0, peste MA Q4 = {"x": PLOT_X0, "y": CY + GUT / 2} # sub 0, sub MA Q2 = {"x": CX + GUT / 2, "y": CY + GUT / 2} # sub 0, peste MA for _q in (Q1, Q2, Q3, Q4): _q["w"], _q["h"] = CW, CH BAND = {"x": 40.0, "y": 556.0, "w": 620.0, "h": 196.0} # ---------------------------------------------------------------- collect TEXTS = [] # (x, y, s, size, weight, fill, anchor) RECTS = [] # (x, y, w, h, tag) def add(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"): TEXTS.append((x, 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((x, y, w, 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 ma4(vals): """Media mobilă pe 4, ca în indicatorul 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 sketch(q, dy, vals, color): """Mini-histogramă RS cu linia de 0 și media mobilă peste ea, în celula q. Ultima bară e cea care decide cadranul, așa că e desenată plină, iar poziția ei față de 0 și față de MA e verificată prin assert de mai jos. """ w, h = 158.0, 46.0 x0 = q["x"] + (q["w"] - w) / 2.0 y0 = q["y"] + dy yz = y0 + h / 2.0 # linia de 0, la mijloc unit = (h / 2.0 - 3) / max(abs(v) for v in vals) bw = w / (len(vals) * 1.6) out = [f'', line(x0, yz, x0 + w, yz, BLACK, 1.0)] xs = [x0 + w * (i + 0.5) / len(vals) for i in range(len(vals))] 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 def cell_text(q, dy, s, *a, **k): add(q["x"] + q["w"] / 2.0, q["y"] + dy, s, *a, **k) # =============================================================== CONȚINUT p = [] # ---- titlu + subtitlu + legenda axelor add(W / 2, 34, "Relative strength — cele 4 cadrane", 20, "bold") add(W / 2, 58, "RS = cât de bine se mișcă simbolul față de benchmark (ex. SPY)", 13, "normal", GRAY) add(W / 2, 80, "Axa verticală: histograma RS față de 0 (peste / sub 0)", 13, "normal", GRAY) add(W / 2, 98, "Axa orizontală: histograma RS față de MA (peste / sub MA)", 13, "normal", GRAY) # ---- etichetele coloanelor / rândurilor (axe) add(PLOT_X0 + CW / 2 + GUT / 2, 122, "sub MA", 14, "bold", ORANGE) add(CX + GUT / 2 + CW / 2, 122, "peste MA", 14, "bold", BLUE) add(PLOT_X0 - 8, Q3["y"] + CH / 2, "peste 0", 14, "bold", GREEN, "end") add(PLOT_X0 - 8, Q4["y"] + CH / 2, "sub 0", 14, "bold", RED, "end") # ---- liniile de 0 (orizontală) și de MA (verticală), în spatele cadranelor p.append(rect(PLOT_X0, PLOT_Y0, PLOT_X1 - PLOT_X0, PLOT_Y1 - PLOT_Y0, "plot", fill="#ffffff", stroke=GRAY, sw=1.0)) p.append(line(PLOT_X0, CY, PLOT_X1, CY, GRAY, 1.0, "4 4")) p.append(line(CX, PLOT_Y0, CX, PLOT_Y1, GRAY, 1.0, "4 4")) # ---- cadrane p.append(rect(Q3["x"], Q3["y"], CW, CH, "Q3", fill=RED_BG, stroke=RED, sw=1.6)) p.append(rect(Q1["x"], Q1["y"], CW, CH, "Q1", fill=GREEN_BG, stroke=GREEN, sw=1.6)) p.append(rect(Q4["x"], Q4["y"], CW, CH, "Q4", fill=RED_BG, stroke=RED, sw=1.6, dash="6 4")) p.append(rect(Q2["x"], Q2["y"], CW, CH, "Q2", fill=GREEN_BG, stroke=GREEN, sw=1.6)) # seriile mini-histogramelor: ultima bară decide cadranul H3 = [6, 6, 5, 5, 4, 3, 3, 2, 1] # peste 0, dar sub MA H1 = [-1, 0, 1, 2, 2, 3, 4, 5, 6] # peste 0 și peste MA H4 = [1, 0, -1, -2, -2, -3, -4, -5, -6] # sub 0 și sub MA H2 = [-6, -6, -5, -5, -4, -3, -3, -2, -1] # sub 0, dar peste MA # Cadran 3 — top-left: peste 0, sub MA -> SHORT cell_text(Q3, 26, "Cadran 3", 16, "bold", BLACK) cell_text(Q3, 48, "înrăutățire", 14, "bold", RED) cell_text(Q3, 70, "peste 0, dar sub MA", 13, "normal", BLACK) cell_text(Q3, 94, "SHORT", 16, "bold", RED) p += sketch(Q3, 106, H3, RED) # Cadran 1 — top-right: peste 0, peste MA -> LONG cell_text(Q1, 26, "Cadran 1", 16, "bold", BLACK) cell_text(Q1, 48, "supraperformanță", 14, "bold", GREEN) cell_text(Q1, 70, "peste 0 și peste MA", 13, "normal", BLACK) cell_text(Q1, 94, "LONG", 16, "bold", GREEN) p += sketch(Q1, 106, H1, GREEN) # Cadran 4 — bottom-left: sub 0, sub MA -> SHORT / stai pe margine cell_text(Q4, 24, "Cadran 4", 16, "bold", BLACK) cell_text(Q4, 46, "subperformanță", 14, "bold", RED) cell_text(Q4, 68, "sub 0 și sub MA", 13, "normal", BLACK) cell_text(Q4, 90, "SHORT — stai pe margine", 14, "bold", RED) p += sketch(Q4, 102, H4, RED) # Cadran 2 — bottom-right: sub 0, peste MA -> LONG (zona de cumpărat) cell_text(Q2, 24, "Cadran 2", 16, "bold", BLACK) cell_text(Q2, 46, "îmbunătățire", 14, "bold", GREEN) cell_text(Q2, 68, "sub 0, dar peste MA", 13, "normal", BLACK) cell_text(Q2, 90, "LONG — zona de cumpărat", 14, "bold", GREEN) p += sketch(Q2, 102, H2, GREEN) # fiecare schiță trebuie să arate exact cadranul pe care îl ilustrează for _nume, _h, _peste0, _pesteMA in (("3", H3, True, False), ("1", H1, True, True), ("4", H4, False, False), ("2", H2, False, True)): _last, _ma = _h[-1], ma4(_h)[-1] assert (_last > 0) == _peste0, f"schița cadranului {_nume}: ultima bară față de 0" assert (_last > _ma) == _pesteMA, f"schița cadranului {_nume}: ultima bară față de MA" # ---- concluzia de sub matrice add(W / 2, 530, "Long = cadranele 1-2 · Short = cadranele 3-4", 14, "bold", BLACK) add(W / 2, 548, "în fiecare cadran: histograma RS (barele), linia de 0 și media mobilă (punctat portocaliu)", 13, "normal", GRAY) # ---- banda de avertisment (cadranul 4) p.append(rect(BAND["x"], BAND["y"], BAND["w"], BAND["h"], "band", fill="#fdf0e0", stroke=ORANGE, sw=1.6)) add(BAND["x"] + 20, 584, "Avertisment — cadranul 4 nu se tranzacționează pe orb", 15, "bold", RED, "start") add(BAND["x"] + 20, 612, "„Cadranul 4 nu se tranzacționează pe orb: aștepți trecerea în 2 sau 1,", 13, "normal", BLACK, "start") add(BAND["x"] + 20, 632, "ori un MP4-1 / MBT-10 long ca semn de schimbare.”", 13, "normal", BLACK, "start") add(BAND["x"] + 20, 662, "„Un setup de bottom cu RS fără îmbunătățire are șanse mai mici.”", 13, "bold", BLACK, "start") add(BAND["x"] + 20, 692, "Notă: transcrierea descrierii cadranului 4 are o eroare (îl descrie ca pe", 13, "normal", GRAY, "start") add(BAND["x"] + 20, 712, "cadranul 3, care e altceva). Definiția din tabloul de mai sus e cea", 13, "normal", GRAY, "start") add(BAND["x"] + 20, 732, "corectă și e singura folosită în tot capitolul.", 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. geometrie: 4 cadrane egale, fără suprapunere, în interiorul plot-ului assert len(RECTS) == 6, f"aștept 6 dreptunghiuri, am {len(RECTS)}" quads = [r for r in RECTS if r[4] in ("Q1", "Q2", "Q3", "Q4")] assert len(quads) == 4, f"aștept 4 cadrane, am {len(quads)}" for x, y, w, h, tag in quads: assert abs(w - CW) < 1e-9 and abs(h - CH) < 1e-9, f"{tag}: dimensiune greșită" assert PLOT_X0 - 0.5 <= x and x + w <= PLOT_X1 + 0.5, f"{tag}: iese orizontal" assert PLOT_Y0 - 0.5 <= y and y + h <= PLOT_Y1 + 0.5, f"{tag}: iese vertical" for i in range(len(quads)): for j in range(i + 1, len(quads)): x1, y1, w1, h1, t1 = quads[i] x2, y2, w2, h2, t2 = quads[j] assert not (x1 < x2 + w2 and x2 < x1 + w1 and y1 < y2 + h2 and y2 < y1 + h1), \ f"cadrane suprapuse: {t1} / {t2}" # 2. pozițiile relative: 1 sus-dreapta, 2 jos-dreapta, 3 sus-stânga, 4 jos-stânga by_tag = {r[4]: r for r in quads} assert by_tag["Q1"][0] > CX and by_tag["Q1"][1] < CY, "cadranul 1 nu e sus-dreapta" assert by_tag["Q2"][0] > CX and by_tag["Q2"][1] > CY, "cadranul 2 nu e jos-dreapta" assert by_tag["Q3"][0] < CX and by_tag["Q3"][1] < CY, "cadranul 3 nu e sus-stânga" assert by_tag["Q4"][0] < CX and by_tag["Q4"][1] > CY, "cadranul 4 nu e jos-stânga" # 3. definițiile din spec sunt exact cele desenate for req in ("Cadran 1", "Cadran 2", "Cadran 3", "Cadran 4", "supraperformanță", "îmbunătățire", "înrăutățire", "subperformanță"): assert any(t[2] == req for t in TEXTS), f"lipsește din desen: {req!r}" # condiția fiecărui cadran assert any(t[2] == "peste 0 și peste MA" for t in TEXTS) assert any(t[2] == "sub 0, dar peste MA" for t in TEXTS) assert any(t[2] == "peste 0, dar sub MA" for t in TEXTS) assert any(t[2] == "sub 0 și sub MA" for t in TEXTS) # direcția: LONG pe 1-2, SHORT pe 3-4 assert sum(t[2].startswith("LONG") for t in TEXTS) == 2, "LONG trebuie să apară de 2 ori" assert sum(t[2].startswith("SHORT") for t in TEXTS) == 2, "SHORT trebuie să apară de 2 ori" assert "stai pe margine" in svg, "lipsește «stai pe margine» din cadranul 4" assert "zona de cumpărat" in svg, "lipsește «zona de cumpărat» din cadranul 2" assert "Long = cadranele 1-2" in svg and "Short = cadranele 3-4" in svg # 4. titlu, subtitlu, legenda axelor, avertismentul, nota for req in ("Relative strength — cele 4 cadrane", "RS = cât de bine se mișcă simbolul față de benchmark (ex. SPY)", "Axa verticală: histograma RS față de 0 (peste / sub 0)", "Axa orizontală: histograma RS față de MA (peste / sub MA)", "Cadranul 4 nu se tranzacționează pe orb", "aștepți trecerea în 2 sau 1", "MP4-1 / MBT-10 long", "Un setup de bottom cu RS fără îmbunătățire are șanse mai mici", "transcrierea descrierii cadranului 4 are o eroare", "histograma RS (barele), linia de 0 și media mobilă"): assert req in svg, f"lipsește din desen: {req!r}" # 5. etichetele cadranului 2 și 3 care se pot încurca: verifică asocierea t2 = [t for t in TEXTS if t[0] > CX and t[1] > CY] assert any("îmbunătățire" == t[2] for t in t2), "cadranul 2 nu e marcat corect" t3 = [t for t in TEXTS if t[0] < CX and t[1] < CY] assert any("înrăutățire" == t[2] for t in t3), "cadranul 3 nu e marcat corect" # 6. textul fiecărui cadran încape în cadranul lui for tag, q in (("Q1", Q1), ("Q2", Q2), ("Q3", Q3), ("Q4", Q4)): for x, y, s, size, weight, fill, anchor in TEXTS: if q["x"] <= x <= q["x"] + q["w"] and q["y"] <= y <= q["y"] + q["h"]: w = est_w(s, size, weight) assert w <= q["w"] - 16, f"{tag}: text prea lat: {s!r} ({w:.0f})" # 7. toate etichetele în panză + 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 panzei orizontal: {s!r}" assert 0 <= top and bottom <= H, f"text în afara panzei 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: fără umbre / gradienți, fontul cerut 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" # 8b. SVG-ul e XML valid, cu o singură rădăcină (bug prins la render: # un rect scris înaintea rădăcinii => „extra content at the end”) import xml.etree.ElementTree as _ET assert svg.lstrip().startswith("" _root = _ET.fromstring(svg) assert _root.tag.endswith("svg"), f"rădăcină neașteptată: {_root.tag}" # 9. fără cifre de RS inventate: niciun text nu conține numere zecimale sau # procente (axele folosesc doar „0”, ca prag, nu valori de RS) import re for _x, _y, _s, _size, _weight, _fill, _anchor in TEXTS: assert not re.search(r"\d+[.,]\d+", _s), f"cifră de RS inventată: {_s!r}" assert "%" not in _s, f"procent inventat: {_s!r}" assert "score" not in svg and "scor" not in 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"{len(quads)} cadrane, {W}x{H}), verificare OK")