"""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 ``), §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'' ) def arrow_h(x0, x1, y, color=BLUE): """Săgeată orizontală de la x0 la x1, vârful la x1.""" return ( f'' f'' ) parts = [ f'', f'', ] # ---------------------------------------------------------------- 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'' ) 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'' f'' f'' f'' ) 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'' f'{html.escape(s, quote=False)}' ) parts.append("") 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")