From 5b157116d3ab92bba5ac813c37b2ce81d3d52de1 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Tue, 22 Sep 2026 20:37:49 +0000 Subject: [PATCH] audit_diagrams: intelege , 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 -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 Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ --- .opencode/agents/capitol.md | 6 +- .opencode/agents/unealta.md | 48 ++++++++ audit_diagrams.py | 173 ++++++++++++++++++++++------ gen/strategii/atmi_exemplu.py | 4 +- gen/strategii/atmi_exemplu.spec.txt | 22 ++-- 5 files changed, 201 insertions(+), 52 deletions(-) create mode 100644 .opencode/agents/unealta.md diff --git a/.opencode/agents/capitol.md b/.opencode/agents/capitol.md index 2408458..fdc0a34 100644 --- a/.opencode/agents/capitol.md +++ b/.opencode/agents/capitol.md @@ -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: `` -2. Scrii `gen/strategii/_.spec.txt`, care conține: +2. Scrii `gen/strategii/_.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 `CONCEPT` (funnel, matrice, cadrane, tabel vizual); - ce arată diagrama, în cuvinte; diff --git a/.opencode/agents/unealta.md b/.opencode/agents/unealta.md new file mode 100644 index 0000000..74ed448 --- /dev/null +++ b/.opencode/agents/unealta.md @@ -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. diff --git a/audit_diagrams.py b/audit_diagrams.py index 268361a..712b0ac 100644 --- a/audit_diagrams.py +++ b/audit_diagrams.py @@ -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ț. Rulează: .venv/bin/python audit_diagrams.py summaries//.md +Selftest: .venv/bin/python audit_diagrams.py --selftest """ 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) 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']*)>||]*)>(.*?)|]*?)/>|<[^>]+>', 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 `` acumulată aplicată. + + Fiecare `` 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: # + 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) == "": + if len(stack) > 1: + stack.pop() + else: + tx, ty = stack[-1] + if m.group(3) is not None: # ... + 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: # 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): - for m in re.finditer(r']*)>(.*?)', svg, re.S): - attrs, inner = m.group(1), re.sub(r'<[^>]+>', '', 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 + for ev in _elements(svg): + if ev[0] != "text": + continue + _, x, y, attrs, raw = ev + inner = re.sub(r'<[^>]+>', '', raw) anchor = (re.search(r'text-anchor="(\w+)"', attrs) or [None, "start"])[1] w = len(inner) * CHAR_W x0 = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x) 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]: md = path.read_text(encoding="utf-8") problems = [] for i, svg in enumerate(re.findall(r'', md, re.S)): - vb = re.search(r'viewBox="0 0 ([\d.]+) ([\d.]+)"', 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']*?/>\s*]*>([^<]*?(\d+\.\d\d)[^<]*)', 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") + problems += _audit_one(i, svg) return problems +def selftest(): + def svg(body): + return f'{body}' + + # două panouri stivuite: fără translație etichetele ar părea suprapuse + stacked = svg('DA — panoul de sus' + 'NU — panoul de jos') + assert not any("suprapuse" in p for p in _audit_one(0, stacked)), "translația nu a fost aplicată" + # translație imbricată + nested = svg('' + 'jos' + 'sus') + 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('unu' + 'doi') + 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('SL 90.00' + 'intrare 100.00' + 'TP 110.00') + 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('minimul 99.80' + 'cel mai jos 99.90' + 'close-ul 100.60') + 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(): - 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 for f in files: probs = check(f) diff --git a/gen/strategii/atmi_exemplu.py b/gen/strategii/atmi_exemplu.py index c60ac85..ee0a505 100644 --- a/gen/strategii/atmi_exemplu.py +++ b/gen/strategii/atmi_exemplu.py @@ -1,11 +1,11 @@ """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 standard, urcare lentă până la TP1 +10 % = 129.17, atins ~1 an mai târziu (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 `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ă diff --git a/gen/strategii/atmi_exemplu.spec.txt b/gen/strategii/atmi_exemplu.spec.txt index 8fda3e9..3beb87e 100644 --- a/gen/strategii/atmi_exemplu.spec.txt +++ b/gen/strategii/atmi_exemplu.spec.txt @@ -1,20 +1,20 @@ TIP: PRET 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: (Nivel 2, §2.5) 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ă 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). -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. -- ~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 % (`(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`). - 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`). @@ -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) - 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`. -- Fără linie de stop-loss; etichetă „fără stop standard": - `04_MODUL_3/03_strategia_2_atmi.md:164-169`. +- Fără linie de stop-loss și fără nicio etichetă de stop în desen (strategia + 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 «aproximativ un an» (ziua exactă neclară în transcriere)" (`:358-363`). 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 (swing_high - entry) / swing_high >= 0.04 (corecția de context ≥4 %) - 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 numărul de candele de la intrare la TP acoperă ~1 an (~50 săptămâni - nu încap; folosește weekly și etichetează intervalul ca „~1 an") +- assert numărul de luni de la intrare la TP e 13 (12 ≤ LUNI ≤ 14), adică + „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 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.