#!/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'' f'{html.escape(s, quote=False)}') 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'') def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None): d = f' stroke-dasharray="{dash}"' if dash else "" return (f'') # =============================================================== 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'') 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. 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")