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
175 lines
8.4 KiB
Python
175 lines
8.4 KiB
Python
"""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}")
|