Files
atm-curs-dl/gen/strategii/selectia_proximitate.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

202 lines
8.7 KiB
Python

"""Pasul 4 — cât de aproape de suport e prețul (TIP: PRET).
Pentru `summaries/17_STRATEGII/04_selectia.md`, pasul 4. Un singur grafic
daily, cu suportul monthly și cu banda de 3 % de deasupra lui, desenată ca
dreptunghi: înăuntru simbolul e „la nivel", în afară e „în aer". Două momente
din aceeași serie, unul în fiecare parte, ca să se vadă diferența pe care
ochiul n-o prinde.
Pragul de 3 % (cu 4 % „cam departe") e al cursului. Seria e construită, la
scară; toate procentele din etichete se calculează din ea, cu assert.
Atenție la diferența față de capitolul Buy Doji: acolo pragul de 1-2 % se
măsoară pentru low-ul doji-ului. Aici măsori închiderea simbolului față de
suport, ca filtru de selecție — două măsurători diferite, praguri diferite.
Rulează:
python3 gen/strategii/selectia_proximitate.py
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Chart, Level, Arrow, Zone, MA, from_closes # noqa: E402
OUT = Path(__file__).resolve().parent / "selectia_proximitate.svg"
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
SUPORT = 48.00
PRAG = 0.03 # 3 % — pragul cursului
BANDA_SUS = SUPORT * (1 + PRAG) # 49.44
DEPARTE, APROAPE = 10, 30 # barele comentate
CLOSES = [
56.00, 55.60, 55.10, 54.70, 54.20, # 0-4
53.80, 53.30, 52.90, 52.40, 52.00, # 5-9
51.60, 51.20, 50.90, 50.50, 50.20, # 10-14
49.90, 50.30, 50.00, 49.70, 49.40, # 15-19
49.10, 49.50, 49.20, 48.90, 49.20, # 20-24
48.90, 48.70, 49.00, 48.70, 48.50, # 25-29
48.80, 49.10, # 30-31
]
assert len(CLOSES) == 32, len(CLOSES)
WICKS = [(0.22, 0.24)] * 32
WICKS[0] = (0.30, 0.30)
WICKS[29] = (0.20, 0.50) # low exact pe suport: 48.50 - 0.50 = 48.00
assert len(WICKS) == 32
VOLS = [46, 44, 48, 45, 43, 47, 44, 42, 45, 41,
40, 42, 39, 41, 38, 44, 36, 38, 35, 37,
34, 40, 33, 36, 32, 35, 31, 34, 33, 48,
44, 46]
assert len(VOLS) == 32
cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=56.30)
# ---------------------------------------------------------------- assert-uri de conținut
for a, b in zip(cds, cds[1:]):
assert abs(a.c - b.o) < 1e-9, "serie cu gol artificial între bare"
assert abs(min(c.l for c in cds) - SUPORT) < 1e-9, "suportul nu e atins de niciun low"
pct_departe = (cds[DEPARTE].c - SUPORT) / SUPORT * 100
pct_aproape = (cds[APROAPE].c - SUPORT) / SUPORT * 100
assert pct_departe > PRAG * 100, "bara «departe» trebuie să fie peste prag"
assert pct_aproape < PRAG * 100, "bara «aproape» trebuie să fie sub prag"
assert abs(pct_departe - 7.50) < 0.01, pct_departe
assert abs(pct_aproape - 1.67) < 0.01, pct_aproape
# banda acoperă exact ce trebuie: bara «aproape» înăuntru, bara «departe» afară
assert SUPORT <= cds[APROAPE].c <= BANDA_SUS, "bara «aproape» nu e în bandă"
assert cds[DEPARTE].c > BANDA_SUS, "bara «departe» nu e în afara benzii"
# numărul din textul zonei == numărul de bare pe care le acoperă
in_banda = [i for i, c in enumerate(cds) if SUPORT <= c.c <= BANDA_SUS]
assert in_banda and in_banda[0] == 19, in_banda[:3]
levels = [
Level(SUPORT, f"suport monthly {SUPORT:.2f}", BLUE, label_at="left"),
Level(BANDA_SUS, f"suport + 3 % = {BANDA_SUS:.2f}", BLUE, dash="5 3"),
]
zones = [
# eticheta benzii o pun manual, după randare: banda ține toată lățimea, iar
# eticheta automată ar urca deasupra primei candele, peste titlu
Zone(0, 31, "", BLUE, p_top=BANDA_SUS, p_bot=SUPORT),
]
arrows = [
Arrow(DEPARTE, f"close {cds[DEPARTE].c:.2f} = {pct_departe:.2f} % peste suport\n"
f"prea departe: până acolo prețul mai are de căzut,\n"
f"deci nivelul nu-l poate opri acum",
where="above", color=RED, dx=-60, dy=-20),
Arrow(APROAPE, f"close {cds[APROAPE].c:.2f} = {pct_aproape:.2f} % peste suport\n"
f"la nivel: dacă mai scade puțin, dă de ordinele\n"
f"de cumpărare care au mai întors prețul de acolo",
where="below", color=GREEN, dx=-320, dy=42),
]
caption = ("Pragul de 3 % (cu 4 % „cam departe”) e al cursului; seria și prețurile sunt construite, "
"la scară, iar procentele sunt calculate din serie. În capitolul Buy Doji, pragul de "
"1-2 % măsoară altceva: low-ul doji-ului față de nivel, nu închiderea simbolului.")
ch = Chart(
candles=cds, levels=levels, zones=zones, arrows=arrows,
mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")],
scale=26.0, gap=15, body_w=9, left=44, width=660, vol_h=60,
vol_label="volum",
title="Cât de aproape de suport e prețul — daily, exemplu construit",
caption=caption,
)
svg = ch.render()
# eticheta benzii, pusă în interiorul ei, în stânga
m_band = re.findall(r'<line x1="24" y1="([\d.]+)"', svg)
y_banda_sus, y_suport = float(m_band[1]), float(m_band[0])
assert y_suport > y_banda_sus, "banda e desenată invers"
svg = svg.replace("</svg>",
f'<text x="230" y="{(y_banda_sus + y_suport) / 2 + 4:.1f}" fill="{BLUE}" '
f'style="font-weight:bold">banda de 3 %: înăuntru simbolul e „la nivel”</text>'
"</svg>")
# „SMA-20" cade peste eticheta nivelului de bandă: o ridic pe rândul liber
svg, n = re.subn(r'(<text x="\d+" y=")([\d.]+)(" fill="#b06000">SMA-20</text>)',
lambda mm: f'{mm.group(1)}{float(mm.group(2)) - 16:.1f}{mm.group(3)}', svg)
assert n == 1, f"eticheta SMA-20: {n} potriviri"
# liniile nu au voie să cadă pe marginea cadrului
m_sup = re.findall(r'<line x1="24" y1="([\d.]+)"', svg)
assert m_sup, "nu găsesc liniile de nivel"
y_sup = float(m_sup[0])
m_vb = re.search(r'viewBox="0 0 (\d+) (\d+)"', svg)
assert m_vb
h_svg = int(m_vb.group(2))
assert y_sup > 0.06 * h_svg, "linia suportului e lipită de marginea de sus"
# nicio etichetă nu numește un preț care nu e desenat
desenate = {f"{SUPORT:.2f}", f"{BANDA_SUS:.2f}", f"{cds[DEPARTE].c:.2f}", f"{cds[APROAPE].c:.2f}",
f"{pct_departe:.2f}", f"{pct_aproape:.2f}"}
for t in re.findall(r"<text[^>]*>([^<]*)</text>", svg):
for nr in re.findall(r"\d+\.\d\d", t):
assert nr in desenate, f"eticheta numește {nr}, care nu e desenat: {t!r}"
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, 96)
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():]
svg = svg.replace(m_vb.group(0),
f'viewBox="0 0 {m_vb.group(1)} {h_svg + 17 * (len(cap_lines) - 1)}"', 1)
def _text_boxes(fill: str) -> list[tuple[float, float, float, float, str]]:
"""Bbox-urile (x0, x1, ytop, ybot) ale textelor cu un anumit fill."""
out = []
for mm in re.finditer(r"<text ([^>]*)>([^<]*)</text>", svg):
attrs, txt = mm.group(1), mm.group(2)
if f'fill="{fill}"' not in attrs:
continue
x = float(re.search(r'\bx="([-\d.]+)"', attrs).group(1))
y = float(re.search(r'\by="([-\d.]+)"', attrs).group(1))
anchor = (re.search(r'text-anchor="(\w+)"', attrs) or [None, "start"])[1]
w = len(txt) * 5.7 # 12px bold, ca în audit_diagrams.py
x0 = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x)
out.append((x0, x0 + w, y - 9, y + 3, txt))
return out
def _se_suprapun(a, b) -> bool:
return a[0] < b[1] and b[0] < a[1] and a[2] < b[3] and b[2] < a[3]
# defectul inițial: ultimul rând al etichetei săgeții de sus (y=75) și titlul
# (y=84) aveau 9 px între baseline-uri și se călcau pe orizontală. Titlul nu
# are voie să se suprapună peste nicio etichetă de săgeată.
titluri = _text_boxes("#333")
assert len(titluri) == 1, f"titluri #333 găsite: {len(titluri)}"
for b in _text_boxes(RED):
assert not _se_suprapun(titluri[0], b), \
f"titlul se suprapune peste eticheta săgeții: {b[4]!r}"
assert "<svg" in svg and "\n\n" not in svg, "linie goală în SVG"
OUT.write_text(svg)
print(f"scris {OUT.name}: 32 candele, suport {SUPORT:.2f}, banda până la {BANDA_SUS:.2f}, "
f"bara {DEPARTE} la {pct_departe:.2f} % (afară), bara {APROAPE} la {pct_aproape:.2f} % (înăuntru)")