From e58763d26d6ba7dc1d564a570546ca5e73c48a29 Mon Sep 17 00:00:00 2001
From: Claude Agent
Date: Sun, 20 Sep 2026 19:08:04 +0000
Subject: [PATCH] 04_selectia: ultimele doua diagrame (rvol, trinity)
Pasul 6 si cele 3 stele, singurele doua marcaje ramase LIPSA.
Ambele conceptuale, dupa tiparul lui selectia_scor / selectia_cadrane.
Singurul prag desenat la RVOL e 100 %, cat da sursa; trinity ilustreaza
doar sensul din acest capitol (RS + Power + volum in cadranul 1).
Assert-urile masoara in pixeli raportul bara/medie (2.00 si 0.61), deci
o schimbare de scara sau de serie face eticheta sa pice, nu sa minta.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
---
gen/strategii/selectia_rvol.py | 315 ++++++++++++++++++++++++
gen/strategii/selectia_rvol.spec.txt | 27 ++
gen/strategii/selectia_rvol.svg | 63 +++++
gen/strategii/selectia_trinity.py | 314 +++++++++++++++++++++++
gen/strategii/selectia_trinity.spec.txt | 24 ++
gen/strategii/selectia_trinity.svg | 120 +++++++++
summaries/17_STRATEGII/04_selectia.html | 187 +++++++++++++-
summaries/17_STRATEGII/04_selectia.md | 185 ++++++++++++++
8 files changed, 1234 insertions(+), 1 deletion(-)
create mode 100644 gen/strategii/selectia_rvol.py
create mode 100644 gen/strategii/selectia_rvol.spec.txt
create mode 100644 gen/strategii/selectia_rvol.svg
create mode 100644 gen/strategii/selectia_trinity.py
create mode 100644 gen/strategii/selectia_trinity.spec.txt
create mode 100644 gen/strategii/selectia_trinity.svg
diff --git a/gen/strategii/selectia_rvol.py b/gen/strategii/selectia_rvol.py
new file mode 100644
index 0000000..92ee850
--- /dev/null
+++ b/gen/strategii/selectia_rvol.py
@@ -0,0 +1,315 @@
+#!/usr/bin/env python3
+"""Relative volume (RVOL) — bara zilei față de media ultimelor 10 bare.
+
+Diagramă conceptuală (NU grafic de preț): pentru
+`summaries/17_STRATEGII/04_selectia.md`, Nivel 2, Pasul 6. Citește
+specificația `gen/strategii/selectia_rvol.spec.txt` și scrie
+`gen/strategii/selectia_rvol.svg`.
+
+Nu folosește `svg_candles` / `pchart`: SVG-ul e construit ca string, doar
+stdlib. Fără `` — toate coordonatele sunt absolute.
+
+Ce arată:
+ - două panouri cu ACELAȘI istoric de 10 bare de volum și aceeași medie;
+ - linia mediei = 100 %, punctată, etichetată pe ambele panouri;
+ - bara „azi": stânga de 2x media (RVOL 200 %, cazul HIG), dreapta 0.61x
+ media (RVOL 0.61, cazul ULTA);
+ - banda de jos: sub 100 % e minus, nu veto (cele trei rezultate reale ale
+ scanerului de Buy Doji: 0.86, 1.03, 0.61);
+ - nota: media e pe 10 bare, nu pe 50 ca în setările implicite.
+
+Singurul prag din material e „peste 100 %"; nu se desenează alt prag.
+
+Rulare: .venv/bin/python gen/strategii/selectia_rvol.py
+"""
+import html
+import re
+import xml.etree.ElementTree as ET
+from pathlib import Path
+
+W, H = 700, 764
+
+BLACK = "#000000"
+GRAY = "#777777"
+RED = "#c00"
+GREEN = "#188a3e"
+BLUE = "#1a5fb4"
+ORANGE = "#b06000"
+
+GREEN_BG = "#eef7f0"
+RED_BG = "#fdeeee"
+NOTE_BG = "#fdf0e0"
+GRAY_BG = "#f7f7f7"
+
+# ------------------------------------------------------- date (unități de volum)
+# Istoricul e ales ca media lui să fie exact 100 -> „media = 100 %".
+ISTORIC = [82, 95, 88, 91, 97, 140, 103, 110, 120, 74]
+MEDIA = sum(ISTORIC) / len(ISTORIC) # = 100.0
+AZI_HIG = 2.00 * MEDIA # RVOL 200 % (cazul HIG)
+AZI_ULTA = 0.61 * MEDIA # RVOL 0.61 (cazul ULTA)
+
+# ------------------------------------------------------- geometrie panouri
+PAN_Y0, PAN_Y1 = 128.0, 438.0 # cadrul panoului
+BASE = 430.0 # linia de 0 a volumului
+UNIT = 0.84 # px per unitate de volum
+PAN_W = 284.0
+PAN_AX, PAN_BX = 40.0, 376.0 # marginea stângă a fiecărui panou
+
+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((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,
+ opacity=None):
+ RECTS.append((float(x), float(y), float(w), float(h), tag))
+ d = f' stroke-dasharray="{dash}"' if dash else ""
+ o = f' fill-opacity="{opacity}"' if opacity is not None 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 arrow(x1, y1, x2, y2, stroke=BLACK, width=1.6):
+ """Săgeată verticală simplă (vârf triunghiular la capătul (x2, y2))."""
+ dy = 1 if y2 > y1 else -1
+ return [line(x1, y1, x2, y2 - dy * 7, stroke, width),
+ f'']
+
+
+# ---------------------------------------------------------------- panou
+BARS = {} # tag panou -> listă de (x_centru, y_top, înălțime, valoare)
+
+
+def panou(tag, px, azi, culoare_azi, eticheta_azi, subeticheta_azi, captura):
+ """Desenează un panou: 10 bare de istoric + linia mediei + bara „azi"."""
+ out = [rect(px, PAN_Y0, PAN_W, PAN_Y1 - PAN_Y0, f"pan_{tag}",
+ fill="#ffffff", stroke=GRAY, sw=1.2)]
+ vals = ISTORIC + [azi]
+ slot = PAN_W / len(vals)
+ bw = slot * 0.62
+ bars = []
+ for i, v in enumerate(vals):
+ cx = px + slot * (i + 0.5)
+ h = v * UNIT
+ top = BASE - h
+ ultima = (i == len(vals) - 1)
+ col = culoare_azi if ultima else "#9aa3ad"
+ out.append(f'')
+ bars.append((cx, top, h, v))
+ BARS[tag] = bars
+
+ # linia de bază (volum 0) și linia mediei
+ out.append(line(px, BASE, px + PAN_W, BASE, BLACK, 1.2))
+ y_med = BASE - MEDIA * UNIT
+ out.append(line(px, y_med, px + PAN_W, y_med, ORANGE, 2.0, "5 3"))
+ # eticheta stă în stânga, unde barele sunt sub linie, ca să nu cadă peste ele
+ add(px + 6, y_med - 6, "media = 100 %", 13, "bold", ORANGE, "start")
+
+ # eticheta barei „azi" + săgeata către vârful ei
+ cx, top, _h, _v = bars[-1]
+ add(px + PAN_W / 2, PAN_Y0 + 24, eticheta_azi, 15, "bold", culoare_azi)
+ add(px + PAN_W / 2, PAN_Y0 + 44, subeticheta_azi, 13, "normal", BLACK)
+ out += arrow(cx, PAN_Y0 + 54, cx, top - 4, culoare_azi, 1.8)
+
+ # axa de jos: ce sunt barele
+ add(px + (PAN_W - slot) / 2 - slot * 0.3, BASE + 20,
+ "ultimele 10 bare de volum", 13, "normal", GRAY)
+ add(cx, BASE + 20, "azi", 13, "bold", culoare_azi)
+
+ # captura: de ce contează
+ add(px + PAN_W / 2, BASE + 48, captura[0], 13, "bold", culoare_azi)
+ add(px + PAN_W / 2, BASE + 68, captura[1], 13, "normal", BLACK)
+ return out
+
+
+# =============================================================== CONȚINUT
+p = []
+
+add(W / 2, 34, "Relative volume (RVOL): volumul zilei față de media pe 10 bare",
+ 17, "bold")
+add(W / 2, 58, "RVOL = volumul de azi ÷ media ultimelor 10 bare de volum",
+ 14, "bold", GRAY)
+add(W / 2, 80,
+ "Măsoară dacă azi chiar participă lumea, nu doar tu. Singurul prag din curs: peste 100 %.",
+ 13, "normal", GRAY)
+
+add(PAN_AX + PAN_W / 2, 110, "Cazul HIG — peste prag", 15, "bold", GREEN)
+add(PAN_BX + PAN_W / 2, 110, "Cazul ULTA — sub prag", 15, "bold", RED)
+
+p += panou("hig", PAN_AX, AZI_HIG, GREEN,
+ "azi = 2 × media → RVOL 200 %",
+ "cel mai mare din cele 16 rezultate",
+ ("peste 100 % = confirmare",
+ "se tranzacționează dublu față de normal"))
+p += panou("ulta", PAN_BX, AZI_ULTA, RED,
+ "azi = 0,61 × media → RVOL 0.61",
+ "sub jumătate față de o zi normală",
+ ("sub 100 % = minus, NU veto",
+ "îl notezi, dar analiza continuă"))
+
+# ---- banda „minus, nu veto"
+BAND_Y = 526.0
+p.append(rect(40, BAND_Y, 620, 112, "band_veto", fill=GRAY_BG, stroke=GRAY, sw=1.2))
+add(60, BAND_Y + 26, "Sub 100 % e un minus, nu un veto", 15, "bold", BLACK, "start")
+add(60, BAND_Y + 50,
+ "Pe scanerul real de Buy Doji, cele trei rezultate aveau RVOL 0.86, 1.03 și 0.61.",
+ 13, "normal", BLACK, "start")
+add(60, BAND_Y + 70,
+ "Un prag dur de genul „RVOL ≥ 1.2” ar fi dat zero rezultate în ziua aia.",
+ 13, "normal", BLACK, "start")
+add(60, BAND_Y + 94,
+ "De aceea RVOL e o coloană pe care o citești, nu un filtru care taie automat.",
+ 13, "bold", BLACK, "start")
+
+# ---- nota: media pe 10, nu pe 50
+NOTE_Y = 654.0
+p.append(rect(40, NOTE_Y, 620, 90, "note_10", fill=NOTE_BG, stroke=ORANGE, sw=1.4))
+add(60, NOTE_Y + 26,
+ "Media e pe 10 bare, nu pe 50 (implicit în platforme).",
+ 13, "bold", BLACK, "start")
+add(60, NOTE_Y + 48,
+ "Cifra cursului e mai sensibilă la ziua curentă. Dacă vezi altă valoare în",
+ 13, "normal", GRAY, "start")
+add(60, NOTE_Y + 68,
+ "altă platformă, nu e greșeală de calcul, e altă lungime de medie.",
+ 13, "normal", GRAY, "start")
+
+# =============================================================== EMITERE
+head = (f'")
+svg = "\n".join(body) + "\n"
+
+# =============================================================== VERIFICARE
+# 1. geometria desenată chiar spune ce spun etichetele: raportul dintre
+# înălțimea barei „azi" și înălțimea liniei mediei (măsurate în px, de la
+# linia de 0) trebuie să fie 2.00, respectiv 0.61.
+h_media_px = BASE - (BASE - MEDIA * UNIT)
+for tag, rvol_cerut in (("hig", 2.00), ("ulta", 0.61)):
+ h_azi_px = BARS[tag][-1][2]
+ assert abs(h_azi_px / h_media_px - rvol_cerut) < 0.005, \
+ f"{tag}: bara desenată dă RVOL {h_azi_px / h_media_px:.3f}, nu {rvol_cerut}"
+
+# 2. istoricul e chiar de 10 bare și media lui e chiar linia trasată
+for tag in ("hig", "ulta"):
+ istoric = [b[3] for b in BARS[tag][:-1]]
+ assert len(istoric) == 10, f"{tag}: {len(istoric)} bare de istoric, nu 10"
+ assert abs(sum(istoric) / 10 - MEDIA) < 1e-9, f"{tag}: media desenată nu e media barelor"
+# istoricul e identic în ambele panouri (același simbol de referință vizual)
+assert [b[3] for b in BARS["hig"][:-1]] == [b[3] for b in BARS["ulta"][:-1]]
+
+# 3. bara „azi" din HIG e chiar cea mai înaltă, iar cea din ULTA e sub medie
+assert BARS["hig"][-1][2] == max(b[2] for b in BARS["hig"]), "bara HIG nu e cea mai înaltă"
+assert BARS["ulta"][-1][2] < h_media_px, "bara ULTA nu e sub linia mediei"
+# și barele nu ies din cadrul panoului
+for tag in ("hig", "ulta"):
+ for cx, top, h, v in BARS[tag]:
+ assert top >= PAN_Y0, f"{tag}: bară peste marginea de sus a panoului"
+ assert abs(top + h - BASE) < 1e-6, f"{tag}: bară care nu pleacă de la linia de 0"
+
+# 3b. eticheta mediei nu cade peste nicio bară (e pusă în stânga, unde barele
+# sunt sub linia mediei)
+_y_med = BASE - MEDIA * UNIT
+for tag in ("hig", "ulta"):
+ px = PAN_AX if tag == "hig" else PAN_BX
+ lat = est_w("media = 100 %", 13, "bold")
+ for cx, top, h, v in BARS[tag]:
+ if cx - 9 < px + 6 + lat and cx + 9 > px + 6:
+ assert top > _y_med - 6 + 3, \
+ f"{tag}: eticheta mediei cade peste o bară (valoare {v})"
+
+# 4. nicio cifră care nu e desenată sau care nu vine din material
+PERMISE = {"2", "200", "100", "10", "50", "0.61", "0,61", "0.86", "1.03", "1.2", "16"}
+for _x, _y, s, *_ in TEXTS:
+ for n in re.findall(r"\d+(?:[.,]\d+)?", s):
+ assert n in PERMISE, f"cifră fără acoperire în material: {n!r} în {s!r}"
+# pragul inventat „ideal > 200 %" nu are voie să apară
+assert "ideal" not in svg.lower(), "prag inventat de tip ideal peste N la suta"
+
+# 5. textele obligatorii
+for req in ("Relative volume (RVOL): volumul zilei față de media pe 10 bare",
+ "RVOL = volumul de azi ÷ media ultimelor 10 bare de volum",
+ "Singurul prag din curs: peste 100 %",
+ "media = 100 %",
+ "azi = 2 × media → RVOL 200 %",
+ "azi = 0,61 × media → RVOL 0.61",
+ "peste 100 % = confirmare",
+ "sub 100 % = minus, NU veto",
+ "Sub 100 % e un minus, nu un veto",
+ "RVOL 0.86, 1.03 și 0.61",
+ "zero rezultate în ziua aia",
+ "nu un filtru care taie automat",
+ "Media e pe 10 bare, nu pe 50"):
+ assert req in svg, f"lipsește din desen: {req!r}"
+# eticheta mediei apare o dată pe fiecare panou
+assert svg.count("media = 100 %") == 2, "linia mediei neetichetată pe ambele panouri"
+
+# 6. fiecare etichetă: mărime >= 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}")
+
+# 7. stil + XML valid, fără transformări (audit_diagrams dă fals-pozitive pe ele)
+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("
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Relative volume (RVOL): volumul zilei față de media pe 10 bare
+RVOL = volumul de azi ÷ media ultimelor 10 bare de volum
+Măsoară dacă azi chiar participă lumea, nu doar tu. Singurul prag din curs: peste 100 %.
+Cazul HIG — peste prag
+Cazul ULTA — sub prag
+media = 100 %
+azi = 2 × media → RVOL 200 %
+cel mai mare din cele 16 rezultate
+ultimele 10 bare de volum
+azi
+peste 100 % = confirmare
+se tranzacționează dublu față de normal
+media = 100 %
+azi = 0,61 × media → RVOL 0.61
+sub jumătate față de o zi normală
+ultimele 10 bare de volum
+azi
+sub 100 % = minus, NU veto
+îl notezi, dar analiza continuă
+Sub 100 % e un minus, nu un veto
+Pe scanerul real de Buy Doji, cele trei rezultate aveau RVOL 0.86, 1.03 și 0.61.
+Un prag dur de genul „RVOL ≥ 1.2” ar fi dat zero rezultate în ziua aia.
+De aceea RVOL e o coloană pe care o citești, nu un filtru care taie automat.
+Media e pe 10 bare, nu pe 50 (implicit în platforme).
+Cifra cursului e mai sensibilă la ziua curentă. Dacă vezi altă valoare în
+altă platformă, nu e greșeală de calcul, e altă lungime de medie.
+
+
Sub 100 % e un minus, nu un veto. În material analiza a continuat la
0.99, 0.96, 0.94, 0.88 și 0.74. Iar pe scanerul real de Buy Doji, cele trei
@@ -925,6 +989,127 @@ simbol. Asta e ce numește cursul „3 stele" sau „trinity" în capitolul de
selecție — și numai asta. Când toate trei arată în aceeași direcție,
simbolul are și forță relativă, și trend susținut, și participare.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Cele 3 stele (Trinity): RS, Power și volum, toate în cadranul 1
+Sortare finală, când după cei șase pași ți-au rămas prea multe simboluri.
+Cadranul 1 = ultima bară a indicatorului e peste 0 ȘI peste media lui mobilă (linia punctată).
+Simbol A — 3 stele
+toate trei arată în aceeași direcție
+Relative strength
+cadranul 1: peste 0 și peste MA
+Power
+cadranul 1: peste 0 și peste MA
+Volum
+cadranul 1: peste 0 și peste MA
+Simbol B — 2 din 3
+Power rămâne în urmă: nu e trinity
+Relative strength
+cadranul 1: peste 0 și peste MA
+Power
+cadranul 3: peste 0, dar sub MA
+Volum
+cadranul 1: peste 0 și peste MA
+3 stele → îl iei primul la analiză
+2 stele → nu e „3 stele”
+În capitolul ăsta „3 stele” / „trinity” înseamnă exact un lucru:
+relative strength, Power și volumul, toate trei în cadranul 1 pe același simbol.
+Cuvântul apare în material și cu alte sensuri (la MDSRT: trei indicatori în divergență
+simultană). Acelea sunt altceva și nu se aplică aici.
+Power e proprietar: măsoară tăria trendului, dar formula nu e publicată.
+Citești cadranul din indicator, nu îl poți reconstrui singur.
+De aceea sortarea după 3 stele e o rafinare, nu un pas obligatoriu.
+
+
Power e proprietar și nu are formulă publicată în material: știi ce măsoară
(tăria trendului), citești cadranul, dar nu îl poți reconstrui singur. De
@@ -1341,6 +1526,6 @@ selecției).
[ ] Dacă au rămas prea mulți: sortez după 3 stele (RS + Power + volum în cadranul 1).
[ ] Între doi candidați: trend, apoi volum, apoi fundamental.
[ ] Am notat simbolul pe listă fără să intru. Trigger-ul se caută separat, pe grafic.