selectia_funnel: "saptamanal" nu reiese din curs, doar din tabelul sumarului. atmi_orizont: sursa spune ca unui participant i s-au inchis pozitii, nu ca firma le inchide automat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
283 lines
12 KiB
Python
283 lines
12 KiB
Python
#!/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'<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: 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'<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: 8 trepte de filtrare de la '
|
|
f'scanner la lista scurta, fiecare cu pragul si sursa ei, plus note '
|
|
f'laterale despre suporturi si Radarscreen">')
|
|
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) == 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 "<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 if ".md:" not in t[2]]
|
|
spec_numbers = {"1", "2", "3", "4", "5", "6", "7", "8", "9",
|
|
"30", "40", "500", "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")
|