cele 7 diagrame din 11_selectia

Sunt diagrame conceptuale - funnel, bara de scor, matrice, cele patru
cadrane, Trinity, sectoare - nu grafice de pret, deci nu trec prin
svg_candles / pchart, care raman obligatorii doar pentru candele si
entry/SL/TP. Agent nou, diagramist, cate unul per diagrama, fiecare cu
generatorul lui in gen/selectia/; substitutia in .md o face
gen/az_selectia.py, ca sa nu scrie sapte agenti in acelasi fisier.

Regula de continut a agentului: fiecare cifra si eticheta trebuie sa
existe in document sau in citate. Ce nu reiese din sursa e scris pe
desen ca atare - ordinea celor 7 sectoare, ce se intampla sub 100%
relative volume. Fara semafor rosu/verde, care nu e in sursa.

Verificat cu ochii pe doua diagrame randate in browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
This commit is contained in:
Claude Agent
2026-09-18 16:52:01 +00:00
parent 7873f3253a
commit 3a6da7a916
28 changed files with 2131 additions and 14 deletions

232
gen/selectia/sectoare.py Normal file
View File

@@ -0,0 +1,232 @@
"""Clasamentul celor 11 sectoare S&P: sortare după cadranul 1 + valoarea RS,
cu rotația banilor între sectoare.
Diagramă conceptuală pentru `summaries/08_ANALIZA_ZILNICA/11_selectia_simbolului.md`
(marcaj `<!--SVG:sectoare-->`), §4.1 „Sectorul care domină".
Ce susține textul din jurul marcajului:
- „cele 11 sectoare S&P" (rândul 439) și „clasamentul sectoarelor | 11 rânduri,
sortate săptămânal" (rândul 88);
- sortarea după „cadranul 1 + valoarea relativ strength"
(`54_08062026.txt:14`, citat la rândul 441-443);
- top 4-ul din citatul sursă: XLV primul, apoi energia, financiarul, real
estate (`54_08062026.txt:9-10`);
- rotația sectorială: „banii migrează dintr-un sector în altul" (rândul 71),
„se mută bani [...] rotație a banilor în acel sector"
(`32_15072026.txt:43-49`), „uită-te ce predomină, în ce sectoare"
(`148_11122025.txt:725-727`).
CE NU AM PUS, pentru că sursa nu-l susține:
- valorile numerice de RS: textul/citatul dă doar regula de sortare
(„cu cât este mai mare histograma de relativ strength și cadranul 1"),
nu și cifre concrete. Desenul redă deci ordinea, nu valori inventate;
- săgeți cu schimbări concrete „săptămâna trecută X, săptămâna asta Y":
nu există asemenea date în sursă. Săgețile arată doar conceptul de rotație
ciclică (semnale în același sector -> bani acolo -> sortare refăcută
săptămânal);
- ordinea celor 7 sectoare din afara top-ului: sursa enumeră doar primele 4.
Nu e grafic de preț, deci NU folosește `svg_candles` / `pchart`: SVG-ul e
construit ca string, fără dependințe. Rulează:
.venv/bin/python gen/selectia/sectoare.py
Scrie `gen/selectia/sectoare.svg`.
Fără semafor: textul nu leagă cadranul 1 de verde sau restul de roșu. Accentele
sunt albastru (informativ) și gri (secundar).
"""
import html
from pathlib import Path
W, H = 700, 640
BLACK = "#000000"
GRAY = "#777777"
BLUE = "#1a5fb4"
texts = [] # (x, y, text, size, weight, fill, w_est, anchor)
def add(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"):
factor = 0.62 if weight == "bold" else 0.56
w = len(s) * size * factor
texts.append((x, y, s, size, weight, fill, w, anchor))
return texts[-1]
def box(x, y, w, h, stroke=GRAY):
return (
f'<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="#ffffff" '
f'stroke="{stroke}" stroke-width="1.5"/>'
)
def arrow_h(x0, x1, y, color=BLUE):
"""Săgeată orizontală de la x0 la x1, vârful la x1."""
return (
f'<line x1="{x0}" y1="{y}" x2="{x1 - 12}" y2="{y}" '
f'stroke="{color}" stroke-width="2"/>'
f'<path d="M {x1} {y} L {x1 - 14} {y - 7} L {x1 - 14} {y + 7} Z" '
f'fill="{color}"/>'
)
parts = [
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" '
f'width="{W}" height="{H}" role="img" '
f'aria-label="Clasamentul celor 11 sectoare S&P sortate după cadranul 1 și '
f'valoarea relative strength, cu rotația banilor între sectoare">',
f'<rect x="0" y="0" width="{W}" height="{H}" fill="#ffffff"/>',
]
# ---------------------------------------------------------------- titluri
add(W / 2, 34, "Sectoare S&P 500 — cadranul 1 + valoarea RS", 20, "bold")
add(W / 2, 58,
"Sortare după cadranul 1 + valoarea RS; clasamentul se reface săptămânal.",
13, "normal", GRAY)
add(W / 2, 76,
"Ordinea primelor 4 sectoare e dată în sursă; restul nu au ordine.",
13, "normal", GRAY)
# ---------------------------------------------------------------- panoul listei
P_X, P_Y, P_W, P_H = 40, 92, 620, 328
parts.append(box(P_X, P_Y, P_W, P_H))
# primele 4 rânduri, sortate (ordinea din 54_08062026.txt:9-10)
TOP4 = [
("1", "Healthcare (XLV)", "cadran 1"),
("2", "Energie (XLE)", "cadran 1"),
("3", "Financiar (XLF)", "cadran 1"),
("4", "Imobiliare (XLRE)", "locul 4"),
]
ROW_X, ROW_W, ROW_H = 56, 588, 34
ROW_Y0, ROW_STEP = 110, 40
for i, (num, name, badge) in enumerate(TOP4):
ry = ROW_Y0 + i * ROW_STEP
parts.append(box(ROW_X, ry, ROW_W, ROW_H, stroke="#cccccc"))
add(ROW_X + 18, ry + 23, num, 16, "bold", BLUE)
add(ROW_X + 44, ry + 23, name, 14, anchor="start")
# badge: dreptunghi + text
bx, bw = 540, 92
parts.append(box(bx, ry + 5, bw, 24, stroke=BLUE))
add(bx + bw / 2.0, ry + 22, badge, 13, "bold", BLUE)
# separator + restul de 7 sectoare
parts.append(
f'<line x1="{ROW_X}" y1="276" x2="{ROW_X + ROW_W}" y2="276" '
f'stroke="{GRAY}" stroke-width="1"/>'
)
add(64, 296, "celelalte 7 sectoare S&P — ordinea nu reiese din sursă",
13, "normal", GRAY, anchor="start")
REST = [
"Tehnologie (XLK)",
"Consum discreționar (XLY)",
"Comunicații (XLC)",
"Industrii (XLI)",
"Consum de bază (XLP)",
"Utilități (XLU)",
"Materiale (XLB)",
]
COLS = [64, 380]
for idx, name in enumerate(REST):
col = idx // 4
row = idx % 4
add(COLS[col], 322 + row * 26, name, 13, "normal", BLACK, anchor="start")
# ---------------------------------------------------------------- rotația
add(W / 2, 438,
"rotația banilor: banii migrează dintr-un sector în altul",
13, "bold", BLUE)
CYCLE = [
(40, ["Multe semnale din", "același sector"]),
(260, ["Banii se mută,", "este rotație"]),
(480, ["Sortarea se reface", "săptămânal"]),
]
CB_Y, CB_W, CB_H = 448, 180, 58
for cx, lines in CYCLE:
parts.append(box(cx, CB_Y, CB_W, CB_H))
for k, line in enumerate(lines):
add(cx + CB_W / 2.0, CB_Y + 24 + k * 20, line, 13)
# săgeți 1->2, 2->3
parts.append(arrow_h(222, 256, 477))
parts.append(arrow_h(442, 476, 477))
# săgeată de revenire 3->1 (pe sub cutii)
ret = (
f'<line x1="570" y1="506" x2="570" y2="536" stroke="{BLUE}" stroke-width="2"/>'
f'<line x1="570" y1="536" x2="130" y2="536" stroke="{BLUE}" stroke-width="2"/>'
f'<line x1="130" y1="536" x2="130" y2="512" stroke="{BLUE}" stroke-width="2"/>'
f'<path d="M 130 506 L 123 520 L 137 520 Z" fill="{BLUE}"/>'
)
parts.append(ret)
add(W / 2, 556, "săptămâna următoare: rotația continuă", 13, "normal", GRAY)
# ---------------------------------------------------------------- sursă
add(W / 2, 586,
"Sursa: 11_selectia_simbolului.md §4.1 „Sectorul care domină”",
13, "normal", GRAY)
add(W / 2, 604,
"(54_08062026.txt:9-10,14; 32_15072026.txt:43-49; 148_11122025.txt:725-727).",
13, "normal", GRAY)
add(W / 2, 622,
"Sursa dă doar ordinea, nu și valori numerice de RS — desenul nu inventează cifre.",
13, "normal", GRAY)
# ---------------------------------------------------------------- emitere
for x, y, s, size, weight, fill, _, anchor in texts:
parts.append(
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>'
)
parts.append("</svg>")
svg = "\n".join(parts) + "\n"
# ---------------------------------------------------------------- verificare
assert "filter=" not in svg and "gradient" not in svg, "fără umbre/gradienti"
boxes = []
for x, y, s, size, weight, fill, w, anchor in texts:
assert size >= 13, f"font sub 13px: {s!r} ({size})"
if anchor == "start":
left, right = x, x + w
elif anchor == "end":
left, right = x - w, x
else:
left, right = x - w / 2.0, x + w / 2.0
top, bottom = y - size * 0.78, y + size * 0.22
assert 0 <= left and right <= W, (
f"etichetă în afara panzei pe orizontală: {s!r} ({left:.0f}..{right:.0f})"
)
assert 0 <= top and bottom <= H, (
f"etichetă în afara panzei pe verticală: {s!r} ({top:.0f}..{bottom:.0f})"
)
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 and r2 > l1 and b1 > t2 and b2 > t1:
raise AssertionError(f"etichete suprapuse: {s1!r} / {s2!r}")
for required in ("Sectoare S&P 500", "cadranul 1", "valoarea RS",
"Healthcare (XLV)", "Energie (XLE)", "Financiar (XLF)",
"Imobiliare (XLRE)", "Tehnologie (XLK)",
"Consum discreționar (XLY)", "Comunicații (XLC)",
"Industrii (XLI)", "Consum de bază (XLP)",
"Utilități (XLU)", "Materiale (XLB)",
"rotația banilor", "săptămânal", "11 sectoare"):
assert html.escape(required, quote=False) in svg, \
f"lipsește din desen: {required!r}"
# cele 11 sectoare apar exact o dată fiecare
for _, name, _ in TOP4:
assert svg.count(name) == 1, f"sector duplicat sau lipsă: {name!r}"
for name in REST:
assert svg.count(name) == 1, f"sector duplicat sau lipsă: {name!r}"
OUT = Path(__file__).with_suffix(".svg")
OUT.write_text(svg, encoding="utf-8")
print(f"scris {OUT} ({len(svg)} bytes, {len(texts)} etichete, {W}x{H}), verificare OK")