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
315 lines
14 KiB
Python
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")
|