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
202 lines
8.7 KiB
Python
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)")
|