Files
atm-curs-dl/gen/strategii/selectia_trinity.py
Claude Agent e58763d26d 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-20 19:08:04 +00:00

315 lines
14 KiB
Python

#!/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ă `<g transform>` — 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'<text x="{x:.1f}" y="{y:.1f}" font-size="{size}" '
f'font-weight="{weight}" fill="{fill}" text-anchor="{anchor}">'
f'{html.escape(s, quote=False)}</text>')
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'<rect x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}" '
f'fill="{fill}" stroke="{stroke}" stroke-width="{sw}"{d}/>')
def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None):
d = f' stroke-dasharray="{dash}"' if dash else ""
return (f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
f'stroke="{stroke}" stroke-width="{width}"{d}/>')
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'<polygon points="{" ".join(pts)}" fill="{fill}" stroke="{stroke}" '
f'stroke-width="1.2"/>')
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'<rect x="{x0:.1f}" y="{y0:.1f}" width="{w:.1f}" height="{h:.1f}" '
f'fill="#ffffff" stroke="{GRAY}" stroke-width="0.8"/>',
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'<rect x="{xs[i] - bw / 2:.1f}" y="{top:.1f}" width="{bw:.1f}" '
f'height="{abs(v) * unit:.1f}" fill="{culoare}" '
f'fill-opacity="{1.0 if i == len(vals) - 1 else 0.35}"/>')
pts = " ".join(f"{xs[i]:.1f},{yz - m * unit:.1f}" for i, m in enumerate(ma4(vals)))
out.append(f'<polyline points="{pts}" fill="none" stroke="{ORANGE}" '
f'stroke-width="1.6" stroke-dasharray="3 2"/>')
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'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" '
f'width="{W}" height="{H}" font-family="system-ui, sans-serif" role="img" '
f'aria-label="Cele 3 stele: relative strength, Power si volum, toate in '
f'cadranul 1 pe acelasi simbol; in dreapta un simbol cu doar doua din '
f'trei, care nu e trinity">')
body = [head, f'<rect x="0" y="0" width="{W}" height="{H}" fill="#ffffff"/>']
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>")
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("<polygon") == STARS["a"] + STARS["b"], \
"numărul de steluțe desenate nu e cel calculat din serii"
# 3. cei trei indicatori sunt exact cei din definiția capitolului
assert [n for n, _ in SERII_A] == ["Relative strength", "Power", "Volum"]
assert [n for n, _ in SERII_A] == [n for n, _ in SERII_B], \
"cele două coloane compară alți indicatori"
# 4. textele obligatorii
for req in ("Cele 3 stele (Trinity): RS, Power și volum, toate în cadranul 1",
"Cadranul 1 = ultima bară a indicatorului e peste 0 ȘI peste media lui mobilă",
"Simbol A — 3 stele", "Simbol B — 2 din 3",
"cadranul 1: peste 0 și peste MA",
"cadranul 3: peste 0, dar sub MA",
"3 stele → îl iei primul la analiză",
"relative strength, Power și volumul, toate trei în cadranul 1 pe același simbol",
"Cuvântul apare în material și cu alte sensuri",
"formula nu e publicată",
"o rafinare, nu un pas obligatoriu"):
assert req in svg, f"lipsește din desen: {req!r}"
# „cadranul 1” marcat de 3 ori în stânga + 2 în dreapta = 5
assert svg.count("cadranul 1: peste 0 și peste MA") == 5
assert svg.count("cadranul 3: peste 0, dar sub MA") == 1
# 5. desen pur calitativ: nicio valoare de RS / Power / volum
PERMISE = {"0", "1", "2", "3"} # doar praguri și numărători de stele
for _x, _y, s, *_ in TEXTS:
for n in re.findall(r"\d+(?:[.,]\d+)?", s):
assert n in PERMISE, f"cifră inventată de indicator: {n!r} în {s!r}"
assert "%" not in s, f"procent inventat: {s!r}"
# 6. coloanele nu se suprapun și încap în pânză
cols = [r for r in RECTS if r[4].startswith("col_")]
assert len(cols) == 2
(ax, ay, aw, ah, _), (bx, by, bw, bh, _) = cols
assert ax + aw < bx, "coloanele se suprapun"
assert bx + bw <= W - 20, "coloana din dreapta iese din pânză"
assert ay + ah <= H and by + bh <= H
# 7. 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}")
# 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ă <g transform>: coordonate absolute"
assert 'font-family="system-ui, sans-serif"' in svg, "lipsește fontul cerut"
assert svg.lstrip().startswith("<svg "), "rădăcina nu e <svg>"
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")