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
231 lines
10 KiB
Python
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")
|