Files
atm-curs-dl/gen/strategii/selectia_scor.py
Claude Agent ccfc4a7f7f strategii: diagramele puse in capitole
36 din 37 de diagrame substituite in .md. substituie.py accepta acum si
cheia cu slug inclus (unii agenti au scris mp2_exemplu, altii exemplu) si
marcajul indentat. A ramas mdsrt_aplus: agentul a raportat OK fara sa scrie
niciun fisier — exact capcana din CLAUDE.md, un raport de agent nu e dovada.
Relansat separat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-18 19:34:11 +00:00

287 lines
12 KiB
Python

#!/usr/bin/env python3
"""ATM score — scala 0-14 (bară cu praguri). TIP: CONCEPT.
Citește specificația `gen/strategii/selectia_scor.spec.txt` și scrie
`gen/strategii/selectia_scor.svg`. Diagramă conceptuală (NU candele): nu
folosește `svg_candles` / `pchart`, SVG-ul e construit ca string, doar stdlib.
Ce arată (toate cifrele vin din spec):
- bara 0-14: F-score 0-9 (albastru, 9/14) + value-score 0-5 (portocaliu, 5/14);
- praguri: 7 (reperul „galben”), 7-8 („intră în analiză”), 12-13 („spuma”), 14 (max);
- benzile F-score: 0-3 slab fundamental, 4-5 mixt, 6-7 solid, 8-9 foarte puternic;
- nota: scor -1 = date indisponibile în platformă, NU scor 0;
- nota: „peste 7-8" e formularea cursului, sub 7 e deducere, nu prag declarat.
Rulare: .venv/bin/python gen/strategii/selectia_scor.py
"""
import html
from pathlib import Path
W, H = 700, 610
BLACK = "#000000"
GRAY = "#777777"
RED = "#c00"
GREEN = "#188a3e"
BLUE = "#1a5fb4"
ORANGE = "#b06000"
YELLOW = "#e0a800" # „7/14 = galben" (reperul din curs)
DARK_YELLOW = "#8a6d00" # aceeași nuanță, lizibilă ca text
BLUE_BG = "#eef2fb"
ORANGE_BG = "#fdf0e0"
GREEN_BG = "#eef7f0"
RED_BG = "#fdeeee"
GRAY_BG = "#f4f4f4"
# ---------------------------------------------------------------- geometrie bară
BAR_X0, BAR_X1 = 60.0, 640.0
BAR_Y, BAR_H = 150.0, 48.0
UNIT = (BAR_X1 - BAR_X0) / 14.0 # 41.4286 px per punct de scor
SPLIT = 9 # F-score 0-9, apoi value-score 9-14
def sx(v):
"""Poziția pe bară a scorului v (0..14)."""
return BAR_X0 + v * UNIT
X_SPLIT = sx(SPLIT)
X7, X8, X12, X13, X14 = sx(7), sx(8), sx(12), sx(13), sx(14)
# ---------------------------------------------------------------- colectoare
TEXTS = [] # (x, y, s, size, weight, fill, anchor)
RECTS = [] # (x, y, w, h, tag)
def add(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"):
TEXTS.append((float(x), float(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, dash=None):
RECTS.append((float(x), float(y), float(w), float(h), tag))
d = f' stroke-dasharray="{dash}"' if dash else ""
return (f'<rect x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}" '
f'fill="{fill}" stroke="{stroke}" stroke-width="{sw}"{d}/>')
def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None):
d = f' stroke-dasharray="{dash}"' if dash else ""
return (f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
f'stroke="{stroke}" stroke-width="{width}"{d}/>')
# =============================================================== CONȚINUT
p = []
# ---- titlu + subtitlu
add(W / 2, 34, "ATM score: scala 0-14", 21, "bold")
add(W / 2, 58, "ATM score = F-score (max 9) + value-score (max 5) = max 14",
13, "normal", GRAY)
# ---- etichetele celor două componente, deasupra barei
add((BAR_X0 + X_SPLIT) / 2, 112, "F-score 0-9", 15, "bold", BLUE)
add((BAR_X0 + X_SPLIT) / 2, 131, "9/14 din bară", 13, "normal", GRAY)
add((X_SPLIT + BAR_X1) / 2, 112, "value-score 0-5", 15, "bold", ORANGE)
add((X_SPLIT + BAR_X1) / 2, 131, "5/14 din bară", 13, "normal", GRAY)
# ---- bara celor 14 puncte: partea albastră (0-9) + partea portocalie (9-14)
p.append(rect(BAR_X0, BAR_Y, X_SPLIT - BAR_X0, BAR_H, "fscore",
fill=BLUE_BG, stroke=BLUE, sw=1.8))
p.append(rect(X_SPLIT, BAR_Y, BAR_X1 - X_SPLIT, BAR_H, "vscore",
fill=ORANGE_BG, stroke=ORANGE, sw=1.8))
# diviziunea la 9
p.append(line(X_SPLIT, BAR_Y, X_SPLIT, BAR_Y + BAR_H, BLACK, 1.2, "3 3"))
# reperele peste bară: banda „spuma" 12-13 și linia galbenă de la 7
p.append(rect(X12, BAR_Y - 6, X13 - X12, BAR_H + 12, "spuma",
fill="none", stroke=RED, sw=1.4, dash="5 3"))
p.append(line(X7, BAR_Y - 10, X7, BAR_Y + BAR_H + 6, YELLOW, 2.4, "4 3"))
# în interiorul barei, numele componentelor
add((BAR_X0 + X_SPLIT) / 2, BAR_Y + BAR_H / 2 + 5, "F-score", 15, "bold", BLUE)
add((X_SPLIT + BAR_X1) / 2, BAR_Y + BAR_H / 2 + 5, "value-score", 15, "bold", ORANGE)
# ---- scale: tick-uri + numere 0..14 sub bară
for i in range(15):
x = sx(i)
p.append(line(x, BAR_Y + BAR_H, x, BAR_Y + BAR_H + 7, GRAY, 1.0))
add(x, BAR_Y + BAR_H + 26, str(i), 13, "normal", BLACK)
# ---- reperul 7/14 (galben)
add(X7 - 8, 244, "7/14 = galben", 13, "bold", DARK_YELLOW, "end")
# ---- intervalul „spuma" (12-13) și maximul 14
add(X14, 244, "12-13: spuma", 13, "bold", RED, "end")
add(X14, 264, "14 = maxim", 13, "bold", BLACK, "end")
# ---- „peste 7-8: intră în analiză" (acoladă sub scală, peste intervalul 7-8)
p.append(line(X7, 234, X7, 242, GRAY, 1.2))
p.append(line(X8, 234, X8, 242, GRAY, 1.2))
p.append(line(X7, 242, X8, 242, GRAY, 1.2))
add((X7 + X8) / 2, 264, "peste 7-8: intră în analiză", 13, "bold", BLACK)
# ---- benzile F-score, aliniate sub partea albastră (0-9)
add(BAR_X0, 300, "Benzi F-score (partea albastră, 0-9)", 14, "bold",
BLACK, "start")
BANDS = [
(0, 3, "0-3", ["slab", "fundamental"], RED, RED_BG),
(3, 5, "4-5", ["mixt"], ORANGE, GRAY_BG),
(5, 7, "6-7", ["solid"], BLUE, BLUE_BG),
(7, 9, "8-9", ["foarte", "puternic"], GREEN, GREEN_BG),
]
BAND_Y, BAND_H = 314.0, 64.0
for a, b, rng, labs, col, bg in BANDS:
x0, x1 = sx(a), sx(b)
p.append(rect(x0, BAND_Y, x1 - x0, BAND_H, f"band{a}",
fill=bg, stroke=col, sw=1.6))
cx = (x0 + x1) / 2
add(cx, 336, rng, 15, "bold", col)
add(cx, 358, labs[0], 13, "normal", BLACK)
if len(labs) > 1:
add(cx, 376, labs[1], 13, "normal", BLACK)
# ---- note (scor -1 + formularea „peste 7-8")
p.append(rect(40, 402, 620, 122, "note",
fill="#f7f7f7", stroke=GRAY, sw=1.2))
add(60, 430, "Scor -1 = date indisponibile în platformă, NU scor 0.",
14, "bold", BLACK, "start")
add(60, 450, "Scorul -1 nu se citește ca 0.", 13, "normal", GRAY, "start")
add(60, 470,
"16_ANALIZA_FUNDAMENTALA/09_implementare_f_score_tradestation.md:45-46, 268-269.",
13, "normal", GRAY, "start")
add(60, 498,
"„peste 7-8” e formularea exactă a cursului; sub 7 se deduce ca excludere,",
13, "normal", BLACK, "start")
add(60, 518,
"nu e declarat ca prag separat (11_selectia_simbolului.md:199-204).",
13, "normal", GRAY, "start")
# ---- surse
add(W / 2, 556,
"Surse: 08_ANALIZA_ZILNICA/11_selectia_simbolului.md:158-164, :199-204;",
13, "normal", GRAY)
add(W / 2, 574,
"16_ANALIZA_FUNDAMENTALA/08_piotrowski_f_score.md:70-72;",
13, "normal", GRAY)
add(W / 2, 592,
"16_ANALIZA_FUNDAMENTALA/09_implementare_f_score_tradestation.md:45-46, 268-269.",
13, "normal", GRAY)
# =============================================================== 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" role="img" '
f'aria-label="Scala ATM score 0-14: F-score 0-9 la care se adauga '
f'value-score 0-5; pragurile 7 (galben), 7-8 intrare in analiza, '
f'12-13 spuma, 14 maxim">')
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. bara are proporțiile cerute: 9/14 albastru, 5/14 portocaliu
w_f = X_SPLIT - BAR_X0
w_v = BAR_X1 - X_SPLIT
assert abs(w_f / (BAR_X1 - BAR_X0) - 9 / 14) < 1e-9, "F-score nu e 9/14 din bară"
assert abs(w_v / (BAR_X1 - BAR_X0) - 5 / 14) < 1e-9, "value-score nu e 5/14 din bară"
assert abs(X_SPLIT - sx(9)) < 1e-9 and abs(sx(0) - BAR_X0) < 1e-9, "scara nu e 0-14"
# 2. toate cifrele/etichetele din spec sunt în desen
for req in ("ATM score: scala 0-14",
"ATM score = F-score (max 9) + value-score (max 5) = max 14",
"F-score 0-9", "9/14 din bară",
"value-score 0-5", "5/14 din bară",
"7/14 = galben", "peste 7-8: intră în analiză",
"12-13: spuma", "14 = maxim",
"0-3", "slab", "fundamental",
"4-5", "mixt", "6-7", "solid", "8-9", "foarte", "puternic",
"Scor -1 = date indisponibile în platformă, NU scor 0.",
"sub 7 se deduce ca excludere",
"nu e declarat ca prag separat"):
assert any(req in t[2] for t in TEXTS), f"lipsește din desen: {req!r}"
# 3. numerele de pe scală sunt exact 0..14
nums = [t[2] for t in TEXTS if t[5] == BLACK and t[3] == 13 and t[6] == "middle"
and t[2].isdigit() and BAR_Y < t[1] < BAR_Y + BAR_H + 40]
assert nums == [str(i) for i in range(15)], nums
# 4. pozițiile relative ale pragurilor
assert abs(sx(7) - (BAR_X0 + 7 * UNIT)) < 1e-9
assert sx(12) < sx(13) < sx(14) == BAR_X1, "intervalul spuma / maximul greșit"
# reperul 7 (galben) și intervalul spuma sunt desenate ca linii/dreptunghiuri
assert any(r[4] == "spuma" for r in RECTS), "lipsește banda spuma 12-13"
assert any(r[4] == "fscore" for r in RECTS) and any(r[4] == "vscore" for r in RECTS)
# 5. benzile F-score: aliniate exact sub partea albastră, fără suprapunere
bands = [r for r in RECTS if r[4].startswith("band")]
assert len(bands) == 4, f"aștept 4 benzi F-score, am {len(bands)}"
assert abs(bands[0][0] - BAR_X0) < 1e-9, "prima bandă nu începe la 0"
assert abs((bands[-1][0] + bands[-1][2]) - X_SPLIT) < 1e-9, "ultima bandă nu se termină la 9"
for i in range(1, len(bands)):
assert abs(bands[i][0] - (bands[i - 1][0] + bands[i - 1][2])) < 1e-9, "benzi nelipite"
for i in range(len(bands)):
for j in range(i + 1, len(bands)):
x1, y1, w1, h1, t1 = bands[i]
x2, y2, w2, h2, t2 = bands[j]
assert not (x1 < x2 + w2 and x2 < x1 + w1 and y1 < y2 + h2 and y2 < y1 + h1), \
f"benzi suprapuse: {t1}/{t2}"
# 6. numerele de pe scală sunt exact cele din benzi (0,3,5,7,9)
edges = [sx(0), sx(3), sx(5), sx(7), sx(9)]
for e, b in zip(edges, bands):
assert abs(e - b[0]) < 1e-9, "marginile benzilor nu cad pe scală"
# 7. fiecare etichetă: mărime >= 13, î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} ({size})"
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}")
# 8. textele din interiorul barei și benzile încap
for tag, (rx, ry, rw, rh, _) in [(r[4], r) for r in RECTS]:
if tag in ("fscore", "vscore"):
for x, y, s, size, weight, fill, anchor in TEXTS:
if rx <= x <= rx + rw and ry <= y <= ry + rh:
assert est_w(s, size, weight) <= rw - 12, f"text prea lat în {tag}: {s!r}"
# 9. stil: fără umbre/gradienți, fontul cerut
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 'fill="#ffffff"' in svg, "fundal alb lipsă"
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(bands)} benzi F-score, F-score {w_f:.1f}px / value-score {w_v:.1f}px, "
f"{W}x{H}), verificare OK")