Files
atm-curs-dl/gen/strategii/selectia_funnel.py
Claude Agent f1033a916c selectia: diagramele refacute dupa sablon (textul urmeaza)
Agentul a atins pragul de context dupa diagrame; capitolul 04_selectia
e inca in formatul vechi si se reia separat.

Ce s-a schimbat: ansamblul nu mai arata ULTA mergand la TP2 — ULTA pica
chiar filtrul de selectie (RVOL 0.61 si 5.01 % de suport), deci nu putea
fi exemplul-vitrina; acum arata doar ce citeste selectia, cu granita
"aici se termina selectia" marcata. Funnel-ul trece de la 8 trepte la 6,
aliniat cu pasii nivelului 2, si fiecare treapta spune de ce. Cadranele
primesc mini-histograme RS, ca sa se recunoasca dintr-un desen, nu
dintr-o propozitie. Trimiterile fisier:linie sunt scoase de pe desene.

Noi: univers (pragurile de pret si volum), sectoare (dominantul din cele
11), proximitate (banda de 3 % peste suportul monthly, cu observatia ca
nu e pragul de 1-2 % din Buy Doji — masoara altceva).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-20 12:43:12 +00:00

278 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 6 trepte care se
îngustează de sus în jos, apoi lista scurtă; cutie separată cu ce rămâne pe
nivelul 3. Pentru `summaries/17_STRATEGII/04_selectia.md`, nivelul 1 — harta
celor șase pași din nivelul 2. Scrie `gen/strategii/selectia_funnel.svg`.
Fiecare treaptă spune pragul ȘI de ce există pragul; trimiterile la material
nu stau pe desen, ci în `summaries/17_STRATEGII/fise/04_selectia_surse.md`.
Nu folosește `svg_candles` / `pchart`: SVG-ul e construit ca string, doar
stdlib.
Fiecare treaptă are titlul, pragul și motivul pragului. Toate cifrele din
desen (pragurile, „max ~300 de simboluri”, „top 10”) vin din material; 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 = 840
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 = 30.0 # cât se strânge pe fiecare treaptă
Y0 = 100.0
HSTEP = 76.0
LIST_W, LIST_Y, LIST_H = 410.0, 566.0, 62.0
NOTE = {"x": 40.0, "y": 648.0, "w": 620.0, "h": 176.0}
# ---------------------------------------------------------------- conținut
# (titlu, subtitlu, [linii de sursă])
STEPS = [
("Universul și lichiditatea",
"close peste 30-40 $; volum mediu ≥ 500.000 de acțiuni, ideal peste 1 milion",
["de ce: pe un simbol subțire, ordinul tău mută singur prețul"]),
("Scorul fundamental",
"ATM score peste 7-8 (12-13 = spuma); F-score 6-9",
["de ce: instituționalii cumpără bilanț, nu desen"]),
("Sectorul care domină",
"numeri din ce sector vin cele mai multe semnale",
["de ce: multe semnale în același sector = bani care intră acolo"]),
("Suport sau pivot, după downtrend",
"proximitate până la 3 % (4 % „e cam departe”)",
["de ce: la un nivel, cumpărătorii au motiv să apară"]),
("Relative strength: cadranul 1 sau 2",
"simbolul se mișcă mai bine decât indicele de referință",
["de ce: cumperi ce urcă mai tare decât piața"]),
("Relative volume peste 100 %",
"candela de volum / media ultimelor 10 candele",
["de ce: confirmă că azi chiar participă lumea, nu doar tu"]),
]
LIST = ("Listă scurtă",
"câteva idei pe care le urmărești",
"nicio poziție deschisă: trigger-ul e al altui capitol")
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: 6 filtre, apoi lista scurtă."
NOTE_TITLE = "Ce nu intră în cei șase pași (rafinări, nivelul 3)"
NOTE1 = ["Sortarea finală după 3 stele (Trinity: relative strength, Power și",
"volum, toate în cadranul 1) și departajarea între doi candidați",
"rămași: trend, apoi volum, apoi fundamental."]
NOTE1_SRC = "un scor de 7 nu e motiv să-l iei, dacă ai altul mai bun"
NOTE2 = ["Radarscreen (maximum ~300 de simboluri: cele 11 sectoare × top 10):",
"același flux, rulat pe o listă fixă, nu pe tot scanerul."]
NOTE2_SRC = "după o corecție mare, cauți doji sau hammer la suporturile mari"
# ---------------------------------------------------------------- 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'<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, rx=0):
SHAPES.append((x, y, w, h, tag))
r = f' rx="{rx}"' if rx else ""
return (f'<rect x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}" '
f'fill="{fill}" stroke="{stroke}" stroke-width="{sw}"{r}/>')
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'<path d="{d}" fill="{fill}" stroke="{BLUE}" '
f'stroke-width="1.2"/>')
# =============================================================== 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: 6 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 + 60 + k * 15, 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"] + 26, NOTE_TITLE, 15, "bold", ORANGE, "start")
add(nx, NOTE["y"] + 50, NOTE1[0], 13, "normal", BLACK, "start")
add(nx, NOTE["y"] + 68, NOTE1[1], 13, "normal", BLACK, "start")
add(nx, NOTE["y"] + 86, NOTE1[2], 13, "normal", BLACK, "start")
add(nx, NOTE["y"] + 104, NOTE1_SRC, 13, "normal", GRAY, "start")
add(nx, NOTE["y"] + 130, NOTE2[0], 13, "normal", BLACK, "start")
add(nx, NOTE["y"] + 148, NOTE2[1], 13, "normal", BLACK, "start")
add(nx, NOTE["y"] + 166, NOTE2_SRC, 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" '
f'role="img" aria-label="Palnia de selectie: 6 trepte de filtrare de la '
f'scanner la lista scurta, fiecare cu pragul ei si cu motivul pentru '
f'care exista pragul, plus rafinarile de la nivelul 3">')
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. palnia are exact 8 trepte, conectate și tot mai înguste
assert len(STEPS) == 6, f"aștept 6 trepte, am {len(STEPS)}"
traps = [s for s in SHAPES if s[4].startswith("step")]
assert len(traps) == 6, f"aștept 6 trapezoide, am {len(traps)}"
assert len(WIDTHS) == 6
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 «de ce»-ul treptei {i + 1}: {src!r}"
assert surse[0].startswith("de ce:"), f"treapta {i + 1} nu spune de ce"
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(len(STEPS)):
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 + len(STEPS) * 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 "<circle" not in svg and "<line" not in svg, "doar palnie, fără axe"
# 8. singurele cifre din desen (fără referințele de sursă) sunt pragurile
# din specificație
content = [t[2] for t in TEXTS]
spec_numbers = {"1", "2", "3", "4", "5", "6", "7", "8", "9",
"30", "40", "500", "000", "11", "12", "13", "100", "10", "300"}
nums = set(re.findall(r"\d+", " ".join(content)))
assert nums <= spec_numbers, f"cifre neașteptate în desen: {nums - spec_numbers}"
OUT = Path(__file__).with_suffix(".svg")
OUT.write_text(svg, encoding="utf-8")
print(f"scris {OUT} ({len(svg)} bytes, {len(TEXTS)} etichete, "
f"{len(traps)} trepte, {W}x{H}), verificare OK")