"""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' y_banda_sus, "banda e desenată invers" svg = svg.replace("", f'banda de 3 %: înăuntru simbolul e „la nivel”' "") # „SMA-20" cade peste eticheta nivelului de bandă: o ridic pe rândul liber svg, n = re.subn(r'(SMA-20)', 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' 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"]*>([^<]*)", 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'' + re.escape(caption) + r"", svg) assert m, "caption-ul nu a fost găsit în SVG" y0 = int(m.group(1)) wrapped = "\n ".join(f'{ln}' 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"]*)>([^<]*)", 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 "