#!/usr/bin/env python3 """Fluxul de selecție — palnia de filtrare de la scanner la lista scurtă. Diagramă conceptuală (NU grafic de preț): palnie cu 8 trepte care se îngustează de sus în jos, apoi lista scurtă; cutie separată de note laterale. Pentru `strategii/04_selectia.md`, Nivel 1, §1.2 „Fluxul de filtrare, pas cu pas". Citește specificația `gen/strategii/selectia_funnel.spec.txt` și scrie `gen/strategii/selectia_funnel.svg`. Nu folosește `svg_candles` / `pchart`: SVG-ul e construit ca string, doar stdlib. Fiecare treaptă are titlul, subtitlul cu pragul și trimiterea (sursa). Toate cifrele din desen (pragurile, „max ~300 simboluri”, „top 10”) sunt preluate textual din specificație; nu se inventează valori de RS sau de sectoare. Rulare: .venv/bin/python gen/strategii/selectia_funnel.py """ import html import re from pathlib import Path W = 700 H = 1024 BLACK = "#000000" GRAY = "#777777" RED = "#c00" GREEN = "#188a3e" BLUE = "#1a5fb4" ORANGE = "#b06000" STEP_FILL = ("#eef3fb", "#e4edf9") # alternanță foarte fină între trepte GREEN_BG = "#eef7f0" NOTE_BG = "#fdf0e0" # ---------------------------------------------------------------- geometrie CX = W / 2.0 # 350: axa palniei TOPW = 610.0 # lățimea gurii palniei NARROW = 24.0 # cât se strânge pe fiecare treaptă Y0 = 100.0 HSTEP = 76.0 LIST_W, LIST_Y, LIST_H = 410.0, 722.0, 62.0 NOTE = {"x": 40.0, "y": 802.0, "w": 620.0, "h": 200.0} # ---------------------------------------------------------------- conținut # (titlu, subtitlu, [linii de sursă]) STEPS = [ ("Scanner — multe semnale", "zeci sau sute de semnale", ["08_ANALIZA_ZILNICA/11_selectia_simbolului.md:631-637"]), ("Filtre de bază", "close > 30-40 $; volum mediu 3 luni > 1M (min 500k)", ["09_QA/28_2025-05-27.md:372-380;", "03_MODUL_2/11_construire_scanere_sf_byd_sld.md:70-73"]), ("Fundamental", "ATM score peste 7-8; 12-13 = spuma; F-score 6-9", ["08_ANALIZA_ZILNICA/11_selectia_simbolului.md:199-221"]), ("Sectorul care domină", "cele 11 sectoare S&P, sortate cadran 1 + valoarea RS", ["08_ANALIZA_ZILNICA/11_selectia_simbolului.md:639-717"]), ("Doar suport / pivot, după downtrend", "suport monthly, quarterly, anual sau pivot; proximitate ≤ 3%", ["08_ANALIZA_ZILNICA/11_selectia_simbolului.md:719-728, 787-803"]), ("Relative strength: cadran 1-2", "doar cadranele 1 și 2, RS pozitiv", ["08_ANALIZA_ZILNICA/11_selectia_simbolului.md:730-739"]), ("Relative volume peste 100%", "candela de volum / media ultimelor 10 candele", ["08_ANALIZA_ZILNICA/11_selectia_simbolului.md:741-747"]), ("Stele (Trinity)", "RS + Power + volum, cadran 1", ["08_ANALIZA_ZILNICA/11_selectia_simbolului.md:776-785"]), ] LIST = ("Listă scurtă", "câteva idei pe care le urmărești", "08_ANALIZA_ZILNICA/11_selectia_simbolului.md:42-50") TITLE = "Fluxul de selecție — de la scanner la lista scurtă" SUBTITLE = ("selecția se face înainte de semnalul tehnic; " "ea nu spune CÂND intri") CAPTION = "Palnia se îngustează de sus în jos: 8 filtre, apoi lista scurtă." NOTE_TITLE = "Note laterale" NOTE1 = ["După corecție masivă: doji / hammer la suporturi mari", "(monthly, quarterly, anual); scanner de apropiere de suport", "până la 3% (4% e cam departe)."] NOTE1_SRC = "08_ANALIZA_ZILNICA/11_selectia_simbolului.md:787-803" NOTE2 = ["Radarscreen (max ~300 simboluri): repetă fluxul — sectoare top 10,", "RS, apoi 3 stele."] NOTE2_SRC = "08_ANALIZA_ZILNICA/11_selectia_simbolului.md:815-825" # ---------------------------------------------------------------- colect TEXTS = [] # (x, y, s, size, weight, fill, anchor) SHAPES = [] # (x, y, w, h, tag) bbox-uri pentru verificare WIDTHS = [] # (w_sus, w_jos) pe fiecare treaptă, pentru verificat conectarea def add(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"): TEXTS.append((x, 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'' f'{html.escape(s, quote=False)}') def rect(x, y, w, h, tag, fill="#ffffff", stroke=GRAY, sw=1.5, rx=0): SHAPES.append((x, y, w, h, tag)) r = f' rx="{rx}"' if rx else "" return (f'') def trapezoid(y, wt, wb, h, index): """Treaptă de palnie: muchia de sus lată wt, cea de jos wb.""" xtl, xtr = CX - wt / 2.0, CX + wt / 2.0 xbl, xbr = CX - wb / 2.0, CX + wb / 2.0 SHAPES.append((xbl, y, wb, h, f"step{index}")) WIDTHS.append((wt, wb)) d = (f"M {xtl:.1f} {y:.1f} L {xtr:.1f} {y:.1f} " f"L {xbr:.1f} {y + h:.1f} L {xbl:.1f} {y + h:.1f} Z") fill = STEP_FILL[index % 2] return (f'') # =============================================================== CONȚINUT p = [] # ---- titlu / subtitlu / caption add(W / 2, 34, TITLE, 20, "bold") add(W / 2, 57, SUBTITLE, 13, "normal", GRAY) add(W / 2, 79, CAPTION, 13, "normal", GRAY) # ---- palnia: 8 trepte for i, (titlu, subtitlu, surse) in enumerate(STEPS): y = Y0 + i * HSTEP wt = TOPW - i * NARROW wb = TOPW - (i + 1) * NARROW p.append(trapezoid(y, wt, wb, HSTEP, i)) add(CX, y + 25, f"{i + 1}. {titlu}", 14.5, "bold", BLACK) add(CX, y + 45, subtitlu, 13, "normal", BLACK) for k, src in enumerate(surse): add(CX, y + 59 + k * 14, src, 13, "normal", GRAY) # ---- lista scurtă (vârful palniei, cutie separată) p.append(rect(CX - LIST_W / 2, LIST_Y, LIST_W, LIST_H, "list", fill=GREEN_BG, stroke=GREEN, sw=1.8, rx=8)) add(CX, LIST_Y + 24, LIST[0], 16, "bold", GREEN) add(CX, LIST_Y + 43, LIST[1], 13, "normal", BLACK) add(CX, LIST_Y + 57, LIST[2], 13, "normal", GRAY) # ---- cutia de note laterale p.append(rect(NOTE["x"], NOTE["y"], NOTE["w"], NOTE["h"], "note", fill=NOTE_BG, stroke=ORANGE, sw=1.6, rx=8)) nx = NOTE["x"] + 20 add(nx, NOTE["y"] + 28, NOTE_TITLE, 15, "bold", ORANGE, "start") add(nx, NOTE["y"] + 56, NOTE1[0], 13, "normal", BLACK, "start") add(nx, NOTE["y"] + 75, NOTE1[1], 13, "normal", BLACK, "start") add(nx, NOTE["y"] + 94, NOTE1[2], 13, "normal", BLACK, "start") add(nx, NOTE["y"] + 115, NOTE1_SRC, 13, "normal", GRAY, "start") add(nx, NOTE["y"] + 150, NOTE2[0], 13, "normal", BLACK, "start") add(nx, NOTE["y"] + 169, NOTE2[1], 13, "normal", BLACK, "start") add(nx, NOTE["y"] + 190, NOTE2_SRC, 13, "normal", GRAY, "start") # =============================================================== EMITERE head = (f'') body = [head, f''] 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 = "\n".join(body) + "\n" # =============================================================== VERIFICARE # 1. palnia are exact 8 trepte, conectate și tot mai înguste assert len(STEPS) == 8, f"aștept 8 trepte, am {len(STEPS)}" traps = [s for s in SHAPES if s[4].startswith("step")] assert len(traps) == 8, f"aștept 8 trapezoide, am {len(traps)}" assert len(WIDTHS) == 8 for i, (wt, wb) in enumerate(WIDTHS): assert wt > wb, f"treapta {i} nu se îngustează" if i: assert abs(WIDTHS[i][0] - WIDTHS[i - 1][1]) < 1e-9, \ f"treapta {i} nu continuă muchia treptei {i - 1}" assert WIDTHS[-1][1] < WIDTHS[0][0], "palnia nu se îngustează de sus în jos" assert LIST_W < traps[-1][2], "lista scurtă nu e mai îngustă decât vârful palniei" # 2. toate titlurile și subtitlurile din spec sunt desenate for i, (titlu, subtitlu, surse) in enumerate(STEPS): assert any(t[2] == f"{i + 1}. {titlu}" for t in TEXTS), \ f"lipsește titlul treptei {i + 1}: {titlu!r}" assert any(t[2] == subtitlu for t in TEXTS), \ f"lipsește subtitlul treptei {i + 1}: {subtitlu!r}" for src in surse: assert src in svg, f"lipsește sursa treptei {i + 1}: {src!r}" assert any(t[2] == LIST[0] for t in TEXTS), "lipsește «Listă scurtă»" assert "câteva idei pe care le urmărești" in svg # 3. titlul, subtitlul și notele din spec sunt exact cele desenate assert TITLE in svg, "lipsește titlul din spec" assert SUBTITLE in svg, "lipsește subtitlul din spec" for line in NOTE1 + [NOTE1_SRC] + NOTE2 + [NOTE2_SRC]: assert line in svg, f"lipsește din note: {line!r}" # 4. fiecare textul încape în lățimea treptei lui (muchia de jos, cea îngustă) for i in range(8): ytop = Y0 + i * HSTEP ybot = ytop + HSTEP wb = TOPW - (i + 1) * NARROW for x, y, s, size, weight, fill, anchor in TEXTS: if ytop <= y <= ybot: w = est_w(s, size, weight) assert w <= wb - 14, f"treapta {i + 1}: text prea lat: {s!r} ({w:.0f})" # textul din lista scurtă și din note încape în cutiile lor for x, y, s, size, weight, fill, anchor in TEXTS: if LIST_Y <= y <= LIST_Y + LIST_H: assert est_w(s, size, weight) <= LIST_W - 16, f"listă: text prea lat: {s!r}" if NOTE["y"] <= y <= NOTE["y"] + NOTE["h"]: assert est_w(s, size, weight) <= NOTE["w"] - 40, f"notă: text prea lat: {s!r}" # 5. toate etichetele în panză + fără suprapuneri boxes = [] for x, y, s, size, weight, fill, anchor in TEXTS: assert size >= 13, f"font sub 13px: {s!r}" 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 panzei orizontal: {s!r}" assert 0 <= top and bottom <= H, f"text în afara panzei 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}") # 6. cutiile stau una sub alta, în ordinea palnie -> listă -> note assert LIST_Y >= Y0 + 8 * HSTEP, "lista scurtă se suprapune cu palnia" assert NOTE["y"] >= LIST_Y + LIST_H, "notele se suprapun cu lista scurtă" assert NOTE["y"] + NOTE["h"] <= H, "notele ies din panză" # 7. stil: fără umbre / gradienți, fontul cerut; fără prețuri inventate assert "filter=" not in svg and "gradient" not in svg, "fără umbre/gradienti" assert 'font-family="system-ui, sans-serif"' in svg, "lipsește fontul cerut" assert "