#!/usr/bin/env python3 """Orizontul de ținere al ATMI — două descrieri incompatibile, pe aceeași axă. Pentru `strategii/05_atmi.md`, marcajul ``. Nu e grafic de preț, e o linie de timp comparativă: SVG scris de mână, doar stdlib — nu se folosește svg_candles / pchart. Ce arată (toate cifrele / citatele vin din spec, nu se inventează prețuri): - bara de sus, albastră: „ATMI clasică — fișierul principal", orizont luni până la ~1 an, fără stop-loss, TP1 atins „aproximativ un an mai târziu"; - bara de jos, portocalie: „ATMI ca adaptare pentru prop — Q&A 11.06.2025", orizont 2 săptămâni; - cutia de anotare: pe prop, limita zilnică se calculează și pe pozițiile deschise, iar la atingere pozițiile se pot închide; și nota că descrierile nu se rezolvă aici, de clarificat cu autorul. Barele sunt pe o axă comună 0 → 1 an, ca diferența să fie vizibilă; bara de 2 săptămâni e scurtă exact pentru că atât ține, nu din greșeală de scară. Rulare: python3 gen/strategii/atmi_orizont.py -> gen/strategii/atmi_orizont.svg """ import re from pathlib import Path from xml.sax.saxutils import escape OUT = Path(__file__).with_suffix(".svg") W, H = 700, 670 BLACK, GRAY, BLUE, ORANGE, WHITE = "#000000", "#777777", "#1a5fb4", "#b06000", "#ffffff" AX_X0, AX_X1 = 60.0, 650.0 AX_W = AX_X1 - AX_X0 # 590 px = 1 an AX_Y = 112.0 TICKS = [0, 3, 6, 9, 12] # luni TICK_LABELS = {0: "0", 3: "3 luni", 6: "6 luni", 9: "9 luni", 12: "~1 an"} WEEKS_YEAR = 52.0 LEN_PROP = AX_W * 2 / WEEKS_YEAR # ~22.7 px = 2 săptămâni BAR_TOP_Y = 172.0 BAR_PROP_Y = 298.0 BAR_H = 42.0 CARD_X, CARD_Y, CARD_W, CARD_H = 160.0, 292.0, 500.0, 112.0 BOX_X, BOX_Y, BOX_W, BOX_H = 40.0, 440.0, 620.0, 150.0 TEXTS = [] # (x, y, text, size, weight, anchor) pentru verificare def txt(x, y, s, size=13, weight="400", fill=BLACK, anchor="middle"): TEXTS.append((x, y, s, size, weight, anchor)) return ( f'{escape(s)}' ) def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None): d = f' stroke-dasharray="{dash}"' if dash else "" return (f'') def tick_x(months): return AX_X0 + AX_W * months / 12.0 def build(): p = [ f'', '', f'', txt(W / 2, 34, "Cât ții poziția? Două descrieri, același nume", 19, "700", BLACK), txt(W / 2, 58, "fișierul principal vs. sfatul din Q&A pentru calificarea " "pe prop", 13, "400", GRAY), txt(W / 2, 84, "Aceeași axă de timp pentru ambele bare (0 → 1 an); " "fără axe de preț.", 12, "400", GRAY), ] # grid vertical, în spatele barelor for m in TICKS: x = tick_x(m) p.append(line(x, 120, x, 425, "#ddd", 1.0, "3 4")) # axa de timp p.append(line(AX_X0, AX_Y, AX_X1, AX_Y, GRAY, 1.2)) for m in TICKS: x = tick_x(m) p.append(line(x, AX_Y - 6, x, AX_Y + 6, GRAY, 1.2)) p.append(txt(x, 132, TICK_LABELS[m], 12, "400", GRAY)) # ------- bara de sus: ATMI clasică p.append(txt(AX_X0, 162, "ATMI clasică — fișierul principal", 15, "700", BLUE, "start")) p.append(f'') p.append(txt(W / 2, BAR_TOP_Y + 26, "„fără stop-loss, poziția ținută luni/ani”", 13, "400", BLACK)) p.append(line(AX_X1, 164, AX_X1, 226, BLUE, 1.2, "4 3")) p.append(txt(AX_X1, 244, "TP1 atins — aproximativ un an mai târziu", 13, "400", BLUE, "end")) p.append(txt(AX_X0, 262, "Sursă: lecția de ATMI", 11, "400", GRAY, "start")) # ------- bara de jos: adaptarea pentru prop p.append(txt(AX_X0, 282, "ATMI ca adaptare pentru prop — Q&A 11.06.2025", 15, "700", ORANGE, "start")) p.append(f'') p.append(line(AX_X0 + LEN_PROP, 290, AX_X0 + LEN_PROP, 360, ORANGE, 1.2, "4 3")) p.append(line(AX_X0 + LEN_PROP, BAR_PROP_Y + BAR_H / 2, CARD_X, BAR_PROP_Y + BAR_H / 2, ORANGE, 1.0, "3 3")) p.append(txt(AX_X0 + 30, 312, "2 săptămâni", 11, "700", ORANGE, "start")) p.append(f'') p.append(txt(CARD_X + CARD_W / 2, 316, "„aștepți o corecție și intri pe ATMI la suport quarterly,", 13, "400", BLACK)) p.append(txt(CARD_X + CARD_W / 2, 334, "swing, 2 săptămâni în poziție”", 13, "400", BLACK)) p.append(txt(CARD_X + CARD_W / 2, 358, "„Ai nevoie de 3 transacții să califici propul.”", 13, "400", BLACK)) p.append(txt(CARD_X + CARD_W / 2, 382, "Sursă: Q&A 11.06.2025", 11, "400", GRAY)) # ------- cutia de anotare p.append(f'') p.append(txt(BOX_X + 20, 466, "Anotare separată — mecanismul care face diferența", 14, "700", BLACK, "start")) p.append(txt(BOX_X + 20, 492, "Pe prop, limita zilnică se calculează și pe pozițiile deschise,", 13, "400", BLACK, "start")) p.append(txt(BOX_X + 20, 510, "iar la atingere pozițiile se pot închide — chiar în profit.", 13, "400", BLACK, "start")) p.append(txt(BOX_X + 20, 530, "Sursă: Q&A 11.06.2025", 11, "400", GRAY, "start")) p.append(txt(BOX_X + 20, 560, "Notă: cele două descrieri nu se rezolvă aici — de clarificat " "cu autorul.", 13, "700", BLACK, "start")) p.append(txt(W / 2, 626, "Fără axe de preț și fără să alegem una dintre " "variante.", 12, "400", GRAY)) p.append(txt(W / 2, 646, "Bara de 2 săptămâni e scurtă exact pentru că " "atât ține, pe aceeași scară.", 12, "400", GRAY)) p.append('') return "\n".join(p) + "\n" # ---------------------------------------------------------------- verificare def est_w(s, size, weight): factor = 0.62 if weight == "700" else 0.56 return len(s) * size * factor def verify(svg): # 1. geometria barelor, față de axa comună assert abs(AX_W - (AX_X1 - AX_X0)) < 1e-9 assert abs(LEN_PROP - AX_W * 2 / WEEKS_YEAR) < 1e-9 assert LEN_PROP < AX_W / 20, "bara de 2 săptămâni ar trebui să fie vizibil scurtă" assert AX_X0 + LEN_PROP < CARD_X, "bara prop intră în card" assert BAR_TOP_Y + BAR_H < BAR_PROP_Y assert CARD_X + CARD_W <= AX_X1 + 10 # 2. toate etichetele în panză for x, y, s, size, weight, anchor in TEXTS: ew = est_w(s, size, weight) left = x - ew if anchor == "end" else (x - ew / 2 if anchor == "middle" else x) right = left + ew assert -2 <= left and right <= W + 2, f"text în afara panzei: {s!r}" assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y})" # 3. etichete care nu se suprapun for i in range(len(TEXTS)): xi, yi, si, szi, wi, ai = TEXTS[i] we = est_w(si, szi, wi) li = xi - we if ai == "end" else (xi - we / 2 if ai == "middle" else xi) ri = li + we for j in range(i + 1, len(TEXTS)): xj, yj, sj, szj, wj, aj = TEXTS[j] wej = est_w(sj, szj, wj) lj = xj - wej if aj == "end" else (xj - wej / 2 if aj == "middle" else xj) rj = lj + wej if abs(yi - yj) < 13 and li < rj - 4 and lj < ri - 4: raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}") # 4. textul cardului încape în card for x, y, s, size, weight, anchor in TEXTS: if CARD_Y <= y <= CARD_Y + CARD_H and CARD_X <= x <= CARD_X + CARD_W: ew = est_w(s, size, weight) assert ew <= CARD_W - 24, f"eticheta nu încape în card: {s!r} ({ew:.0f})" # 5. cutia de anotare: textul nu iese for x, y, s, size, weight, anchor in TEXTS: if BOX_Y <= y <= BOX_Y + BOX_H and x >= BOX_X: ew = est_w(s, size, weight) assert BOX_X + 20 + ew <= BOX_X + BOX_W - 12, f"text iese din cutie: {s!r}" # 6. citatele și sursele din spec sunt prezente for cheie in ("fără stop-loss, poziția ținută luni/ani", "aproximativ un an mai târziu", "suport quarterly", "2 săptămâni în poziție", "3 transacții să califici propul", "limita zilnică se calculează", "pozițiile se pot închide", "nu se rezolvă aici", "Sursă: lecția de ATMI", escape("Sursă: Q&A 11.06.2025")): assert cheie in svg, f"lipsește din desen: {cheie!r}" # 6b. nicio trimitere „fișier:linie” în etichetele SVG: sursele trăiesc # doar în fișa capitolului (regulă din șablon, curățată și la cap. 06) assert not re.search(r"\.(md|txt):\d", svg), \ "eticheta SVG nu are voie să conțină trimitere fișier:linie" # 7. fără cifre de preț inventate for interzis in ("$", "USD", "SL ", "intrare "): assert interzis not in svg, f"nu ar trebui să apară preț: {interzis!r}" return True SVG = build() assert verify(SVG), "verificare eșuată" OUT.write_text(SVG, encoding="utf-8") print(f"scris {OUT} ({W}x{H}), verificare OK: {len(TEXTS)} etichete, " f"bara prop {LEN_PROP:.1f}px din {AX_W:g}px")