Files
atm-curs-dl/gen/strategii/atmi_orizont.py
Claude Agent 400cd66727 strategii: textele gresite din doua SVG-uri, reparate in generatoare
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
2026-09-19 04:26:01 +00:00

231 lines
10 KiB
Python

#!/usr/bin/env python3
"""Orizontul de ținere al ATMI — două descrieri incompatibile, pe aceeași axă.
Pentru `strategii/03_atmi.md`, marcajul `<!-- DIAGRAMA: orizont -->`.
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
"""
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'<text x="{x:.1f}" y="{y:.1f}" font-size="{size}" font-weight="{weight}" '
f'fill="{fill}" text-anchor="{anchor}">{escape(s)}</text>'
)
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}/>')
def tick_x(months):
return AX_X0 + AX_W * months / 12.0
def build():
p = [
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" '
f'width="{W}" height="{H}" role="img" '
f'aria-label="Orizontul de tinere al ATMI: fisiarul principal spune luni '
f'pana la un an, adaptarea pentru prop spune doua saptamani; cele doua '
f'descrieri raman nerezolvate">',
'<g font-family="system-ui, sans-serif">',
f'<rect x="0" y="0" width="{W}" height="{H}" fill="{WHITE}"/>',
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'<rect x="{AX_X0}" y="{BAR_TOP_Y}" width="{AX_W:g}" height="{BAR_H:g}" '
f'rx="6" fill="#eef4fd" stroke="{BLUE}" stroke-width="1.5"/>')
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ă: 04_MODUL_3/03_strategia_2_atmi.md:26-29; "
":358-363", 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'<rect x="{AX_X0}" y="{BAR_PROP_Y}" width="{LEN_PROP:.1f}" '
f'height="{BAR_H:g}" rx="6" fill="#fdf0e0" stroke="{ORANGE}" '
f'stroke-width="1.5"/>')
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'<rect x="{CARD_X}" y="{CARD_Y}" width="{CARD_W}" height="{CARD_H}" '
f'rx="8" fill="{WHITE}" stroke="{ORANGE}" stroke-width="1.5"/>')
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ă: 09_QA/26_2025-06-11.md:225-227", 11, "400", GRAY))
# ------- cutia de anotare
p.append(f'<rect x="{BOX_X}" y="{BOX_Y}" width="{BOX_W}" height="{BOX_H}" '
f'rx="8" fill="#fafafa" stroke="{GRAY}" stroke-width="1.2"/>')
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ă: 09_QA/26_2025-06-11.md:216-222",
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('</g></svg>')
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",
"03_strategia_2_atmi.md:26-29",
"09_QA/26_2025-06-11.md:225-227",
"09_QA/26_2025-06-11.md:216-222"):
assert cheie in svg, f"lipsește din desen: {cheie!r}"
# 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")