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
233 lines
8.8 KiB
Python
233 lines
8.8 KiB
Python
"""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")
|