audit_diagrams: intelege <g transform=translate>, si nu mai inventeaza SCARA GRESITA

Cauza celor 46 de raportari: unealta citea coordonatele brute ale etichetelor,
deci compara panouri stivuite ca si cum ar fi in acelasi sistem. Acum tine o
stiva de translatii la parsare si scoate coordonate absolute, inclusiv pentru
<g>-uri imbricate. Si verificarea de scara ruleaza doar pe un triplet real
SL/intrare/TP, nu pe orice trei perechi linie+eticheta.

46 -> 7 pe cele 92 de diagrame, fara nicio raportare noua. Are --selftest, cu
cazul care picase si cu unul care trebuie sa ramana prins.

Doua dintre cele 7 ramase sunt probleme reale, nestiute pana acum:
atmi_ansamblu are un caption de 792 px intr-un viewBox de 720, iar la
selectia_proximitate se calca eticheta sagetii cu titlul. Cinci sunt tot
fantome, din alt defect (pragul de inaltime ignora font-size) - raportat de
agent, nereparat inca.

Spec-ul si docstring-ul lui atmi_exemplu erau si ele ramase in urma: descriau
un desen weekly de 38 de candele, desenul e lunar, de 31.

Specificatiile .spec.txt: cerute doar cand desenul il face alt agent decat cel
care scrie capitolul. Sunt o predare intre doi agenti, nu arhiva.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
This commit is contained in:
Claude Agent
2026-09-22 20:37:49 +00:00
parent 6d90bcfa69
commit 5b157116d3
5 changed files with 201 additions and 52 deletions

View File

@@ -81,7 +81,11 @@ Pentru fiecare diagramă de care are nevoie capitolul, faci două lucruri:
1. Pui în `.md`, exact acolo unde trebuie să apară desenul, linia: 1. Pui în `.md`, exact acolo unde trebuie să apară desenul, linia:
`<!-- DIAGRAMA: <cheie> -->` `<!-- DIAGRAMA: <cheie> -->`
2. Scrii `gen/strategii/<slug>_<cheie>.spec.txt`, care conține: 2. Scrii `gen/strategii/<slug>_<cheie>.spec.txt` — **doar dacă desenul îl face
alt agent** (un `grafician`, care citește spec-ul ca sursă). Dacă scrii tu
și generatorul, sari peste: specificația e o predare între doi agenți, nu
un document de arhivă, iar generatorul o conține oricum. Când îl scrii, el
conține:
- **tipul**: `PRET` (candele, entry/SL/TP — trece prin `svg_candles`) sau - **tipul**: `PRET` (candele, entry/SL/TP — trece prin `svg_candles`) sau
`CONCEPT` (funnel, matrice, cadrane, tabel vizual); `CONCEPT` (funnel, matrice, cadrane, tabel vizual);
- ce arată diagrama, în cuvinte; - ce arată diagrama, în cuvinte;

View File

@@ -0,0 +1,48 @@
---
description: Repară sau extinde o unealtă Python din rădăcina proiectului, cu selftest
mode: all
model: opencode-go/deepseek-v4.1-flash
temperature: 0.1
permission:
read: allow
glob: allow
grep: allow
list: allow
edit: allow
bash: allow
webfetch: deny
websearch: deny
task: deny
---
Lucrezi în /workspace/atm-curs-dl. Repari **o singură unealtă**, pe care ți-o
numește promptul. Uneltele stau în rădăcina proiectului (`audit_diagrams.py`,
`check_duplicates.py`, `svg_candles.py`, `render_html.py`, …).
**Atingi doar fișierul uneltei** și, dacă e nevoie, un fișier de test lângă
el. **Nu atinge niciun `.md`**, niciun generator din `gen/`, niciun SVG.
**Fără `git add` / `commit` / `push`.**
## Cum lucrezi
1. Citește unealta **întreagă** înainte să schimbi ceva. Sunt fișiere mici.
2. Înțelege defectul pe un caz concret: ia un fișier real pe care unealta se
poartă greșit și arată, cu o rulare, ce raportează acum.
3. **Fă cea mai mică schimbare care rezolvă cauza**, nu simptomul. Fără
clase noi, fără opțiuni de configurare, fără abstracții pentru „mai
târziu". Dacă o funcție există deja în fișier și face jumătate din treabă,
extinde-o; nu scrie a doua, paralelă.
4. **Lasă în urmă o verificare care rulează.** Un `--selftest` în aceeași
unealtă (tiparul din `check_duplicates.py` și `svg_candles.py`), cu
`assert`-uri pe un exemplu minimal construit în cod: cazul care dădea
greșit înainte, plus un caz care trebuie să rămână semnalat. Fără
framework de teste, fără fixture-uri.
5. Verifică pe repo-ul real: rulează unealta pe fișierele pe care promptul ți
le numește și **compară numărul de raportări înainte și după**. O unealtă
care nu mai raportează nimic pe nimic e tot stricată, doar invers.
## RAPORT, scurt
Ce era cauza (o propoziție), ce ai schimbat, ce verifică selftest-ul și cum
se comportă pe fișierele reale: câte raportări înainte, câte după, și
**care dintre cele rămase sunt reale**. Dacă ai găsit un al doilea defect pe
care nu ți l-a cerut nimeni, raportează-l, nu-l repara.

View File

@@ -7,6 +7,7 @@ linii goale care rup blocul HTML. NU înlocuiește verificarea vizuală — nu
vede dacă un text stă peste o linie de preț. vede dacă un text stă peste o linie de preț.
Rulează: .venv/bin/python audit_diagrams.py summaries/<modul>/<fisier>.md Rulează: .venv/bin/python audit_diagrams.py summaries/<modul>/<fisier>.md
Selftest: .venv/bin/python audit_diagrams.py --selftest
""" """
import re import re
@@ -16,60 +17,156 @@ from pathlib import Path
CHAR_W = 5.7 # latime medie per caracter la 12px sans-serif (calibrat pe randare) CHAR_W = 5.7 # latime medie per caracter la 12px sans-serif (calibrat pe randare)
LINE_H = 13 # înălțimea unei linii de text LINE_H = 13 # înălțimea unei linii de text
_TRANSFORM = re.compile(r'translate\(\s*([-\d.]+)\s*[, ]\s*([-\d.]+)\s*\)')
_ELEMENT = re.compile(
r'<g\b([^>]*)>|</g>|<text\b([^>]*)>(.*?)</text>|<line\b([^>]*?)/>|<[^>]+>', re.S)
_ROLE = re.compile(r'SL\b|stop', re.I)
_ROLE_ENTRY = re.compile(r'intrar|entry', re.I)
_ROLE_TP = re.compile(r'\bTP\d*|\bțint|\btarget', re.I)
def _elements(svg: str):
"""Evenimentele SVG în ordine, cu translația `<g>` acumulată aplicată.
Fiecare `<g transform="translate(dx,dy)">` deschide un cadru; elementele
dinăuntru moștenesc deplasarea, oricât de adânc ar fi imbricate.
"""
stack = [(0.0, 0.0)]
for m in _ELEMENT.finditer(svg):
if m.group(1) is not None: # <g ...>
t = _TRANSFORM.search(m.group(1))
dx, dy = (float(t.group(1)), float(t.group(2))) if t else (0.0, 0.0)
px, py = stack[-1]
stack.append((px + dx, py + dy))
elif m.group(0) == "</g>":
if len(stack) > 1:
stack.pop()
else:
tx, ty = stack[-1]
if m.group(3) is not None: # <text ...>...</text>
attrs = m.group(2)
x = float(re.search(r'\bx="([-\d.]+)"', attrs).group(1)) if re.search(r'\bx="', attrs) else 0.0
y = float(re.search(r'\by="([-\d.]+)"', attrs).group(1)) if re.search(r'\by="', attrs) else 0.0
yield "text", x + tx, y + ty, attrs, m.group(3)
else: # <line .../> sau alt tag
y = re.search(r'\by1="([-\d.]+)"', m.group(4) or "")
if y:
yield "line", float(y.group(1)) + ty, m.group(4)
else:
yield "other", 0.0, 0.0
def texts(svg: str): def texts(svg: str):
for m in re.finditer(r'<text\b([^>]*)>(.*?)</text>', svg, re.S): for ev in _elements(svg):
attrs, inner = m.group(1), re.sub(r'<[^>]+>', '', m.group(2)) if ev[0] != "text":
x = float(re.search(r'\bx="([-\d.]+)"', attrs).group(1)) if re.search(r'\bx="', attrs) else 0.0 continue
y = float(re.search(r'\by="([-\d.]+)"', attrs).group(1)) if re.search(r'\by="', attrs) else 0.0 _, x, y, attrs, raw = ev
inner = re.sub(r'<[^>]+>', '', raw)
anchor = (re.search(r'text-anchor="(\w+)"', attrs) or [None, "start"])[1] anchor = (re.search(r'text-anchor="(\w+)"', attrs) or [None, "start"])[1]
w = len(inner) * CHAR_W w = len(inner) * CHAR_W
x0 = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x) x0 = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x)
yield x0, x0 + w, y, inner.strip() yield x0, x0 + w, y, inner.strip()
def levels(svg: str):
"""Perechile line+text care joacă rolurile SL / intrare / TP, cu y absolut."""
out = []
ev = list(_elements(svg))
for a, b in zip(ev, ev[1:]):
if a[0] != "line" or b[0] != "text":
continue
label = re.sub(r'<[^>]+>', '', b[4]).strip()
price = re.search(r'(\d+\.\d\d)', label)
if not price:
continue
role = ("sl" if _ROLE.search(label)
else "entry" if _ROLE_ENTRY.search(label)
else "tp" if _ROLE_TP.search(label) else None)
if role:
out.append((float(price.group(1)), a[1], role))
return out
def _audit_one(i: int, svg: str) -> list[str]:
problems = []
vb = re.search(r'viewBox="0 0 ([\d.]+) ([\d.]+)"', svg)
if not vb:
return [f"svg {i}: fără viewBox"]
vw, vh = float(vb.group(1)), float(vb.group(2))
if re.search(r"\n[ \t]*\n", svg):
problems.append(f"svg {i}: linie goală înăuntru — markdown rupe blocul")
tx = list(texts(svg))
for x0, x1, y, s in tx:
if x1 > vw + 2:
problems.append(f'svg {i}: text depășește marginea dreaptă ({x1:.0f} > {vw:.0f}): "{s[:45]}"')
if y > vh + 2 or y < 0:
problems.append(f'svg {i}: text în afara înălțimii (y={y:.0f}, viewBox {vh:.0f}): "{s[:45]}"')
for a in range(len(tx)):
for b in range(a + 1, len(tx)):
xa0, xa1, ya, sa = tx[a]
xb0, xb1, yb, sb = tx[b]
if abs(ya - yb) < LINE_H and xa0 < xb1 - 4 and xb0 < xa1 - 4:
problems.append(f'svg {i}: texte suprapuse — "{sa[:28]}" / "{sb[:28]}"')
# dacă diagrama are exact un triplet SL/intrare/TP, raportul distanțelor
# în preț trebuie să fie cel din pixeli
lv = levels(svg)
if len(lv) == 3 and {r for _, _, r in lv} == {"sl", "entry", "tp"}:
pts = sorted((p, y) for p, y, _ in lv)
(plo, ylo), (pmid, ymid), (phi, yhi) = pts
if pmid - plo > 0 and ylo - ymid > 0:
rp = (phi - pmid) / (pmid - plo)
rx = (ymid - yhi) / (ylo - ymid)
if abs(rp - rx) > 0.05:
problems.append(f"svg {i}: SCARĂ GREȘITĂ — raport în prețuri {rp:.2f}R, desenat {rx:.2f}R")
return problems
def check(path: Path) -> list[str]: def check(path: Path) -> list[str]:
md = path.read_text(encoding="utf-8") md = path.read_text(encoding="utf-8")
problems = [] problems = []
for i, svg in enumerate(re.findall(r'<svg .*?</svg>', md, re.S)): for i, svg in enumerate(re.findall(r'<svg .*?</svg>', md, re.S)):
vb = re.search(r'viewBox="0 0 ([\d.]+) ([\d.]+)"', svg) problems += _audit_one(i, svg)
if not vb:
problems.append(f"svg {i}: fără viewBox")
continue
vw, vh = float(vb.group(1)), float(vb.group(2))
if re.search(r"\n[ \t]*\n", svg):
problems.append(f"svg {i}: linie goală înăuntru — markdown rupe blocul")
tx = list(texts(svg))
for x0, x1, y, s in tx:
if x1 > vw + 2:
problems.append(f'svg {i}: text depășește marginea dreaptă ({x1:.0f} > {vw:.0f}): "{s[:45]}"')
if y > vh + 2 or y < 0:
problems.append(f'svg {i}: text în afara înălțimii (y={y:.0f}, viewBox {vh:.0f}): "{s[:45]}"')
for a in range(len(tx)):
for b in range(a + 1, len(tx)):
xa0, xa1, ya, sa = tx[a]
xb0, xb1, yb, sb = tx[b]
if abs(ya - yb) < LINE_H and xa0 < xb1 - 4 and xb0 < xa1 - 4:
problems.append(f'svg {i}: texte suprapuse — "{sa[:28]}" / "{sb[:28]}"')
# dacă diagrama are exact 3 niveluri de preț orizontale (SL/intrare/TP),
# raportul distanțelor în preț trebuie să fie cel din pixeli
lv = re.findall(r'<line x1="[\d.]+" y1="([\d.]+)"[^>]*?/>\s*<text[^>]*>([^<]*?(\d+\.\d\d)[^<]*)</text>', svg)
if len(lv) == 3:
pts = sorted((float(p), float(y)) for y, _, p in lv)
(plo, ylo), (pmid, ymid), (phi, yhi) = pts
if pmid - plo > 0 and ylo - ymid > 0:
rp = (phi - pmid) / (pmid - plo)
rx = (ymid - yhi) / (ylo - ymid)
if abs(rp - rx) > 0.05:
problems.append(f"svg {i}: SCARĂ GREȘITĂ — raport în prețuri {rp:.2f}R, desenat {rx:.2f}R")
return problems return problems
def selftest():
def svg(body):
return f'<svg viewBox="0 0 400 400">{body}</svg>'
# două panouri stivuite: fără translație etichetele ar părea suprapuse
stacked = svg('<g transform="translate(0,0)"><text x="10" y="20">DA — panoul de sus</text></g>'
'<g transform="translate(0,200)"><text x="10" y="20">NU — panoul de jos</text></g>')
assert not any("suprapuse" in p for p in _audit_one(0, stacked)), "translația nu a fost aplicată"
# translație imbricată
nested = svg('<g transform="translate(0,100)"><g transform="translate(0,100)">'
'<text x="10" y="20">jos</text></g></g>'
'<g transform="translate(0,0)"><text x="10" y="20">sus</text></g>')
assert not any("suprapuse" in p for p in _audit_one(0, nested)), "translația imbricată nu s-a acumulat"
# suprapunerea reală din același panou rămâne semnalată
real = svg('<g transform="translate(0,0)"><text x="10" y="20">unu</text>'
'<text x="14" y="20">doi</text></g>')
assert any("suprapuse" in p for p in _audit_one(0, real)), "suprapunerea reală a fost pierdută"
# triplet SL/intrare/TP cu scară greșită rămâne semnalat
bad = svg('<line x1="10" y1="300" x2="380" y2="300"/><text x="10" y="295">SL 90.00</text>'
'<line x1="10" y1="250" x2="380" y2="250"/><text x="10" y="245">intrare 100.00</text>'
'<line x1="10" y1="100" x2="380" y2="100"/><text x="10" y="95">TP 110.00</text>')
assert any("SCARĂ" in p for p in _audit_one(0, bad)), "scara greșită nu a fost prinsă"
# trei niveluri care NU sunt SL/intrare/TP nu mai declanșează SCARĂ
notrip = svg('<line x1="10" y1="300" x2="380" y2="300"/><text x="10" y="295">minimul 99.80</text>'
'<line x1="10" y1="250" x2="380" y2="250"/><text x="10" y="245">cel mai jos 99.90</text>'
'<line x1="10" y1="100" x2="380" y2="100"/><text x="10" y="95">close-ul 100.60</text>')
assert not any("SCARĂ" in p for p in _audit_one(0, notrip)), "niveluri care nu sunt triplet declanșează SCARĂ"
print("selftest ok")
def main(): def main():
files = [Path(a) for a in sys.argv[1:]] or sorted(Path("summaries").glob("*/*.md")) if "--selftest" in sys.argv:
selftest()
return 0
files = [Path(a) for a in sys.argv[1:] if a != "--selftest"] or sorted(Path("summaries").glob("*/*.md"))
total = 0 total = 0
for f in files: for f in files:
probs = check(f) probs = check(f)

View File

@@ -1,11 +1,11 @@
"""Diagrama din `gen/strategii/atmi_exemplu.spec.txt` (TIP: PRET). """Diagrama din `gen/strategii/atmi_exemplu.spec.txt` (TIP: PRET).
Exemplul real din curs (ATMI / SPY weekly): corecție de context ≥4 %, Exemplul real din curs (ATMI / SPY monthly): corecție de context ≥4 %,
intrare pe 27.01.2005 la 117.43 după BIM + candelă verde, fără stop-loss intrare pe 27.01.2005 la 117.43 după BIM + candelă verde, fără stop-loss
standard, urcare lentă până la TP1 +10 % = 129.17, atins ~1 an mai târziu standard, urcare lentă până la TP1 +10 % = 129.17, atins ~1 an mai târziu
(data exactă nesigură în ASR). (data exactă nesigură în ASR).
Serie continuă de 38 de candele weekly (from_closes), panou de volum, Serie continuă de 31 de candele lunare (from_closes), panou de volum,
niveluri reale (intrare / TP1) și marcaje ancorate. Toate cifrele vin din niveluri reale (intrare / TP1) și marcaje ancorate. Toate cifrele vin din
`summaries/04_MODUL_3/03_strategia_2_atmi.md` (liniile citate în spec); `summaries/04_MODUL_3/03_strategia_2_atmi.md` (liniile citate în spec);
singura completare proprie e swing high-ul ~123.00, care trebuie să singura completare proprie e swing high-ul ~123.00, care trebuie să

View File

@@ -1,20 +1,20 @@
TIP: PRET TIP: PRET
CHEIE: exemplu CHEIE: exemplu
TITLU: „SPY — weekly: intrare 117.43 (27.01.2005) → TP +10 % = 129.17" TITLU: „SPY — monthly: intrare ianuarie 2005 la 117.43 → TP1 +10 % = 129.17"
MARCAJ .md: <!-- DIAGRAMA: exemplu --> (Nivel 2, §2.5) MARCAJ .md: <!-- DIAGRAMA: exemplu --> (Nivel 2, §2.5)
CE ARATĂ CE ARATĂ
Exemplul real din material, pe weekly ca să încapă ~1 an în 35-40 de candele: Exemplul real din material, pe monthly ca să încapă ~1 an în 31 de candele:
corecție ≥4 % înainte de intrare, intrarea de pe 27.01.2005 la 117.43, fără corecție ≥4 % înainte de intrare, intrarea de pe 27.01.2005 la 117.43, fără
stop-loss, urcarea lentă până la TP1 +10 % = 129.17, atins aproximativ un an stop-loss, urcarea lentă până la TP1 +10 % = 129.17, atins aproximativ un an
mai târziu. Data exactă a atingerii e marcată ca nesigură (ASR). mai târziu. Data exactă a atingerii e marcată ca nesigură (ASR).
STRUCTURA SERIEI (35-40 candele weekly, lipite, gap=15-18, body_w=9-10) STRUCTURA SERIEI (31 candele lunare, lipite, gap=15, body_w=9)
- ~12 candele de urcare spre un swing high ~123.00. - ~12 candele de urcare spre un swing high ~123.00.
- ~6 candele de corecție de la 123.00 până la zona de intrare (117.43). - 5 candele de corecție de la 123.00 până la zona de intrare (117.43).
Verificare de context: corecția de la swing high la intrare e ≥4 % Verificare de context: corecția de la swing high la intrare e ≥4 %
(`(123.00 - 117.43) / 123.00 ≈ 4.53 %`). (`(123.00 - 117.43) / 123.00 ≈ 4.53 %`).
- Candela de intrare: close = 117.43, precedată de BIM + candelă verde - Candela de intrare (verde): close = 117.43, precedată de BIM
(`04_MODUL_3/03_strategia_2_atmi.md:156-162`). (`04_MODUL_3/03_strategia_2_atmi.md:156-162`).
- 13 candele de urcare de la 117.43 spre 129.17, atins la finalul seriei - 13 candele de urcare de la 117.43 spre 129.17, atins la finalul seriei
(~1 an mai târziu — `04_MODUL_3/03_strategia_2_atmi.md:358-363`). (~1 an mai târziu — `04_MODUL_3/03_strategia_2_atmi.md:358-363`).
@@ -23,8 +23,8 @@ STRUCTURA SERIEI (35-40 candele weekly, lipite, gap=15-18, body_w=9-10)
NIVELURI EXACTE ȘI ETICHETE (fiecare cu sursa) NIVELURI EXACTE ȘI ETICHETE (fiecare cu sursa)
- Intrare 117.43, 27.01.2005: `04_MODUL_3/03_strategia_2_atmi.md:358-363`. - Intrare 117.43, 27.01.2005: `04_MODUL_3/03_strategia_2_atmi.md:358-363`.
- TP1 +10 % = 129.17: `04_MODUL_3/03_strategia_2_atmi.md:170-171, 358-363`. - TP1 +10 % = 129.17: `04_MODUL_3/03_strategia_2_atmi.md:170-171, 358-363`.
- Fără linie de stop-loss; etichetă „fără stop standard": - Fără linie de stop-loss și fără nicio etichetă de stop în desen (strategia
`04_MODUL_3/03_strategia_2_atmi.md:164-169`. nu are stop standard): `04_MODUL_3/03_strategia_2_atmi.md:164-169`.
- Etichetă pe final: „TP1: 13 candele lunare în desen; materialul spune - Etichetă pe final: „TP1: 13 candele lunare în desen; materialul spune
«aproximativ un an» (ziua exactă neclară în transcriere)" (`:358-363`). «aproximativ un an» (ziua exactă neclară în transcriere)" (`:358-363`).
Cele 13 candele sunt o cifră a desenului, nu a materialului; eticheta o Cele 13 candele sunt o cifră a desenului, nu a materialului; eticheta o
@@ -40,12 +40,12 @@ CE VERIFICĂ GENERATORUL CU assert
- assert abs(tp / entry - 1.10) < 0.005 (TP1 = +10 %) - assert abs(tp / entry - 1.10) < 0.005 (TP1 = +10 %)
- assert (swing_high - entry) / swing_high >= 0.04 (corecția de context ≥4 %) - assert (swing_high - entry) / swing_high >= 0.04 (corecția de context ≥4 %)
- assert abs(entry - close(candela_de_intrare)) < 0.05 - assert abs(entry - close(candela_de_intrare)) < 0.05
- assert tp == max(high) pe ultimele ~10 candele - assert tp == max(high) pe ultimele 13 candele
- assert nu există nicio linie de stop-loss în desen - assert nu există nicio linie de stop-loss în desen
- assert numărul de candele de la intrare la TP acoperă ~1 an (~50 săptămâni - assert numărul de luni de la intrare la TP e 13 (12 ≤ LUNI ≤ 14), adică
nu încap; folosește weekly și etichetează intervalul ca „~1 an") „aproximativ un an" din material (~50 de săptămâni nu încap pe monthly)
- assert LUNI == 13 și eticheta spune „13 candele lunare în desen; materialul - assert LUNI == 13 și eticheta spune „13 candele lunare în desen; materialul
spune «aproximativ un an»" (13 = cifra desenului, nu a materialului) spune «aproximativ un an»" (13 = cifra desenului, nu a materialului)
STIL: ~37 candele weekly, fără linii goale în SVG. Marcaj pe candela de STIL: 31 candele lunare, fără linii goale în SVG. Marcaj pe candela de
intrare cu data și prețul; marcaj pe TP. intrare cu data și prețul; marcaj pe TP.