Files
atm-curs-dl/gen/strategii/atmi_ansamblu.py
Claude Agent 43e6fbcd32 audit: pragul de rand derivat din font-size; si cele doua diagrame chiar stricate
Al doilea defect al uneltei: inaltimea de rand era constanta LINE_H=13,
calibrata pe 12 px, deci la etichetele de 10 px cu pas de 12 px credea ca
randurile se ating. Acum e LINE_H_RATIO x font-size-ul efectiv, mostenit
corect de pe <g>. Factorul e 1.07, nu 1.15: agentul a incercat valorile mari,
a masurat cu metrice reale de glif si a aratat ca scot cinci fantome noi.

Cele doua probleme reale pe care unealta reparata le-a scos la iveala:
atmi_ansamblu avea un caption de 792 px intr-un viewBox de 720, deci se taia
la margine; la selectia_proximitate se calcau eticheta sagetii si titlul.
Ambele reparate si privite la 1:1.

Cele 92 de diagrame: 0 probleme. Selftest-ul trece, si prinde in continuare o
suprapunere fabricata deliberat - unealta tace pentru ca e curat, nu pentru ca
a orbit.

Ramane, raportat si nereparat: CHAR_W = 5.7 e tot calibrat pe 12 px, deci
latimea textelor mici e supraestimata. Aceeasi clasa de bug, n-a produs inca
raportari gresite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-22 20:45:50 +00:00

175 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Diagrama de ansamblu ATMI (strategii/05_atmi.md, marcajul `ansamblu`).
„SPY — daily: corecție ≥4 % → zona PPQ → BIM + verde → intrare → TP +10 %".
Structura seriei și nivelurile vin din spec-ul
`gen/strategii/atmi_ansamblu.spec.txt`. Toate cifrele desenate (100.00 swing
high, 96.00 intrare, 105.60 = +10 %) sunt din exemplul „DA" al cursului
(`summaries/04_MODUL_3/03_strategia_2_atmi.md:185-247`). Benzile PPQ/SQ sunt
schematice: cursul spune explicit că nu dă cifrele lor
(`...:143`), deci nu poartă prețuri.
Rulează:
python3 gen/strategii/atmi_ansamblu.py
Scrie lângă el `atmi_ansamblu.svg`.
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402
OUT = Path(__file__).resolve().parent / "atmi_ansamblu.svg"
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
PURPLE = "#7b2fbf" # folosit în materialul de curs pentru PPQ
# ---------------------------------------------------------------- seria (33 bare)
# 15 bare de urcare ~88 -> 100, 8 de corecție 100 -> 96 (ultima = intrarea),
# 10 de urcare 96 -> 105.60. open-ul fiecărei bare = close-ul precedentei.
UP_A = [96.60, 97.10, 97.60, 98.00, 98.50, 98.20, 98.70, 99.20,
99.60, 99.30, 99.70, 100.00] # 0-11
CORR = [99.30, 98.50, 97.80, 97.20, 96.70, 96.30, 95.90, 96.00] # 12-19 (18 = BIM)
UP_B = [96.60, 97.20, 97.90, 98.60, 99.40, 100.30, 101.40, 102.60,
103.40, 104.20, 104.90, 105.60] # 20-31
CLOSES = UP_A + CORR + UP_B
assert len(CLOSES) == 32, len(CLOSES)
PEAK, BIM, ENTRY, LAST = 11, 18, 19, 31
# fitile (sus, jos) per bară; jos pe corecție ținut ≥ 0 ca minimul să fie fix 96.00
WICKS = ([(0.25, 0.25)] * 11
+ [(0.00, 0.25)] # 11: vârful atinge exact 100.00
+ [(0.00, 0.20), (0.05, 0.20), (0.05, 0.20), (0.05, 0.20),
(0.05, 0.20), (0.05, 0.20),
(0.05, 0.35), # 18: bara de BIM, fundul la 95.55
(0.15, 0.05)] # 19: candela verde de intrare
+ [(0.25, 0.00)] + [(0.25, 0.25)] * 10
+ [(0.00, 0.20)]) # 31: închide/atinge exact 105.60
assert len(WICKS) == len(CLOSES), (len(WICKS), len(CLOSES))
VOLS = ([44, 47, 42, 50, 45, 41, 48, 43, 46, 51, 44, 58] # 0-11 urcare
+ [64, 70, 66, 61, 58, 74, 96, 88] # 12-19 corecție + BIM/intrare
+ [55, 58, 52, 60, 57, 63, 59, 66, 70, 74, 78, 84]) # 20-31 urcare spre TP
assert len(VOLS) == len(CLOSES), (len(VOLS), len(CLOSES))
cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=96.20)
# ---------------------------------------------------------------- assert-urile din spec
high = 100.00
entry = cds[ENTRY].c
tp = cds[LAST].h
assert abs((high - 96.00) / high * 100 - 4.0) < 0.05 # corecția e 4.0 %
assert abs(entry - 96.00) < 0.05 # intrarea = close-ul candelei verzi
assert abs(tp / entry - 1.10) < 0.005 # TP = +10 %
assert abs(tp - max(c.h for c in cds[-12:])) < 1e-9 # TP atins la final, e maximul barelor
assert BIM < ENTRY # BIM înainte de candela verde
assert abs(min(c.l for c in cds[12:20]) - 95.55) < 1e-9 # fundul corecției = 95.55
assert cds[ENTRY].c >= cds[ENTRY].o # candela de intrare e verde
assert abs(cds[PEAK].h - high) < 1e-9 # swing high = 100.00
assert abs(cds[LAST].c - tp) < 1e-9 # ultima bară închide pe TP
levels = [
Level(high, "100.00 — swing high recent", "#333", label_at="left"),
Level(entry, f"intrare {entry:.2f}", GREEN, label_at="right"),
Level(tp, f"TP +10 % = {tp:.2f}", BLUE, label_at="right"),
]
# nicio linie de stop-loss și niciun nivel sub intrare (absența SL e regula)
assert all("stop" not in lv.text.lower() for lv in levels)
assert all(lv.price >= entry - 1e-9 for lv in levels)
marks = [
Mark(13, "corecție 4,0 %", ORANGE, "below", dy=26),
Mark(BIM, "BIM", "#0a7f9e", "above"),
Mark(ENTRY, "prima candelă verde → intrare", GREEN, "below", dy=30),
Mark(LAST, "ieși cu tot", BLUE, "below", dy=22),
]
caption = ("100.00 / 96.00 / 105.60 sunt cifrele exemplului din curs; PPQ 95.80 e calculat "
"în diagrama pasului 1 (cursul nu dă valorile pivoților).")
ch = Chart(candles=cds, levels=levels, marks=marks,
scale=22.0, gap=17, body_w=9, left=44, width=720, vol_h=70, vol_gap=72, top=88,
vol_label="volum",
caption=caption)
svg = ch.render()
def band(price: float, half: float, color: str, label: str) -> str:
"""Bandă schematică orizontală, desenată în spatele candelelor.
Se injectează în SVG după randare, pentru că `Chart` nu are benzi; y-ul
se calculează cu aceeași scară (`ch._y`), deci banda e aliniată corect.
"""
y_top, y_bot = ch._y(price + half), ch._y(price - half)
return (f'<rect x="24" y="{y_top:.1f}" width="{ch.width - 38}" '
f'height="{y_bot - y_top:.1f}" fill="{color}" opacity="0.10"/>'
f'<text x="28" y="{y_bot + 14:.1f}" fill="{color}">{label}</text>')
# PPQ 95.80 e pivotul trimestrial calculat în diagrama pasului 1; banda e
# zona de cumpărare din jurul lui. SQ 90.80 e sub scara acestui desen, deci
# nu se desenează aici — apare pe diagrama pasului 1.
PPQ = 95.80
assert abs(min(c.l for c in cds) - 95.55) < 1e-9
assert PPQ - 0.30 <= min(c.l for c in cds) <= PPQ + 0.30, "fundul nu cade în banda PPQ"
assert entry > PPQ, "candela verde trebuie să închidă peste pivot"
ppq = band(PPQ, 0.30, PURPLE, f"zona PPQ {PPQ:.2f} — aici cumperi")
sq = ""
# „fără stop-loss standard" stă sub zona de intrare, unde nu are candele
no_sl = (f'<text x="{ch._cx(ENTRY):.0f}" y="{ch._y(entry) + 46:.1f}" fill="{RED}" '
f'text-anchor="middle">fără stop-loss standard — nicio linie sub intrare</text>')
# titlul e injectat deasupra, ca rând separat de eticheta TP (care stă chiar
# sub marginea de sus a panoului); altfel cele două texte se calcă la dreapta
titlu = ('<text x="24" y="26" fill="#333" style="font-weight:bold">'
'SPY — daily: corecție ≥4 % → zona PPQ → BIM + verde → intrare → TP +10 %</text>')
i = svg.index(">") + 1
svg = svg[:i] + "\n " + "\n ".join([ppq, no_sl, titlu]) + svg[i:]
# ---------------------------------------------------------------- caption pe două rânduri
# Captionul are 135 de caractere, adică ~794 px de la x=24 într-o pânză lată de
# 720: se rupe pe două rânduri, ca în selectia_ansamblu.py.
def _wrap(text: str, max_chars: int) -> list[str]:
lines, cur = [], ""
for w in text.split():
if cur and len(cur) + 1 + len(w) > max_chars:
lines.append(cur)
cur = w
else:
cur = f"{cur} {w}".strip()
if cur:
lines.append(cur)
return lines
cap_lines = _wrap(caption, 80)
assert len(cap_lines) == 2, cap_lines
m = re.search(r'<text x="24" y="(\d+)" fill="#555">' + re.escape(caption) + r"</text>", svg)
assert m, "caption-ul nu a fost găsit în SVG"
y0 = int(m.group(1))
wrapped = "\n ".join(f'<text x="24" y="{y0 + 17 * i}" fill="#555">{ln}</text>'
for i, ln in enumerate(cap_lines))
svg = svg[:m.start()] + wrapped + svg[m.end():]
vm = re.search(r'viewBox="0 0 (\d+) (\d+)"', svg)
assert vm, "nu găsesc viewBox-ul"
svg = svg.replace(vm.group(0),
f'viewBox="0 0 {vm.group(1)} {int(vm.group(2)) + 17 * (len(cap_lines) - 1)}"', 1)
# fiecare rând de caption CHIAR DESENAT trebuie să încapă în lățimea pânzei.
# CHAR_W = 5.7 e lățimea medie per caracter calibrată de audit_diagrams.py;
# assertul prinde exact defectul vechi, când captionul era un singur text de
# 135 de caractere: 24 + 135 × 5.7 = 794 > 720.
CHAR_W = 5.7
vw = int(vm.group(1))
for m_cap in re.finditer(r'<text x="24" y="[\d.]+" fill="#555">([^<]*)</text>', svg):
ln = m_cap.group(1)
x1 = 24 + len(ln) * CHAR_W
assert x1 <= vw, f"rând de caption tăiat la marginea dreaptă ({x1:.0f} > {vw}): {ln!r}"
assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
assert ".md:" not in svg, "trimitere fișier:linie în SVG"
OUT.write_text(svg)
print(f"scris {OUT.name}: {len(cds)} candele, intrare {entry:.2f}, TP {tp:.2f}")