"""ULTA — daily (pivoți monthly): diagrama de ansamblu a selecției (TIP: PRET). Rolul diagramei în capitol: arată **unde se termină selecția**. Filtrul citește contextul (downtrend, suportul monthly 355.48, pivotul monthly 383.74, buy doji la 373.30, RVOL 0.61) și se oprește la „simbolul intră pe listă". Intrarea, stopul și ținta nu apar aici, pentru că nu aparțin selecției — sunt ale capitolului 1 (Buy Doji). Partea de după linia verticală e desenată gri tocmai ca să se vadă că nu mai e treaba filtrului. Cifrele cursului: close 373.30, suport monthly 355.48, pivot monthly 383.74, RVOL 0.61 (`04_MODUL_3/01_filtre_scaner_bull_bear_reversal_pp.md`). Procentele de 5.01 % și 2.72 % se recalculează din ele, cu assert. Structura barelor e construită, la scară, prin `svg_candles` (from_closes). Rulează: python3 gen/strategii/selectia_ansamblu.py Scrie lângă el `selectia_ansamblu.svg`. """ 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_ansamblu.svg" GRAY, RED, GREEN, BLUE, ORANGE, TEAL = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000", "#00a0aa" SUPORT, PIVOT, DOJI_CLOSE = 355.48, 383.74, 373.30 DOJI = 20 # indexul candelei de buy doji # ---------------------------------------------------------------- seria (32 bare) # bare 0-11 : downtrend vizibil de la ~395 la ~362 (~0.8 %/bară) # bare 12-19 : baza, minimul atinge suportul monthly 355.48 pe bara 15 # bara 20 : buy doji, close 373.30 (open = close[19] = 373.00, corp 0.30) # bare 21-31 : ce urmează — nu mai e treaba selecției, desenat gri CLOSES = [ 395.00, 391.80, 388.60, 385.20, 382.00, 378.90, 375.70, 372.80, # 0-7 369.90, 367.00, 364.20, 361.80, # 8-11 363.20, 361.00, 358.50, 356.20, 360.00, 365.00, 369.50, 373.00, # 12-19 373.30, 375.60, # 20-21 377.00, 378.50, 380.40, 382.00, 384.50, 386.50, 389.50, 393.00, # 22-29 397.50, 401.50, # 30-31 ] assert len(CLOSES) == 32, len(CLOSES) WICKS = [(0.35, 0.40)] * 32 WICKS[0] = (0.40, 0.50) # prima bară, deschide dintr-un vârf al downtrend-ului WICKS[15] = (0.30, 0.72) # low exact 355.48 = suportul monthly WICKS[16] = (0.40, 0.30) # rebound, nu sparge suportul WICKS[DOJI] = (1.45, 1.45) # doji: corp 0.30 din 3.20 înălțime = 9 % assert len(WICKS) == 32, len(WICKS) # bara 20 (doji) sub media seriei: exact ce înseamnă RVOL 0.61 VOLS = [ 42, 44, 41, 46, 43, 45, 40, 44, 47, 43, # 0-9 40, 42, 38, 41, 52, # 10-14 56, 50, 45, 42, 39, # 15-19 30, # 20 doji, sub medie 46, # 21 48, 50, 52, 54, 58, 62, 66, 70, 76, 82, # 22-31 ] assert len(VOLS) == 32, len(VOLS) media_vol = sum(VOLS) / len(VOLS) assert abs(VOLS[DOJI] / media_vol - 0.61) < 0.02, VOLS[DOJI] / media_vol cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=396.50) # barele de după doji sunt gri: nu mai sunt în sarcina filtrului for cd in cds[DOJI + 1:]: cd.color = "#bbb" # ---------------------------------------------------------------- assert-uri de conținut assert abs(cds[DOJI].c - DOJI_CLOSE) < 1e-9, "doji-ul trebuie să închidă la 373.30" corp = abs(cds[DOJI].o - cds[DOJI].c) inaltime = cds[DOJI].h - cds[DOJI].l assert corp / inaltime <= 0.10, f"bara {DOJI} nu e doji: corp {corp / inaltime:.0%} din înălțime" assert abs(min(c.l for c in cds[12:DOJI + 1]) - SUPORT) < 0.05, "suportul 355.48 nu e atins" # de la intrarea în bază și până la doji, prețul stă sub pivot: de asta pivotul # e rezistența cea mai apropiată deasupra doji-ului assert max(c.h for c in cds[12:DOJI + 1]) < PIVOT, "pivotul nu are voie atins în bază" for a, b in zip(cds, cds[1:]): assert abs(a.c - b.o) < 1e-9, "serie cu gol artificial între bare" # procentele din etichete se calculează, nu se scriu de mână pct_suport = (DOJI_CLOSE - SUPORT) / SUPORT * 100 # 5.01 % pct_pivot = (PIVOT - DOJI_CLOSE) / PIVOT * 100 # 2.72 % assert abs(pct_suport - 5.01) < 0.01, pct_suport assert abs(pct_pivot - 2.72) < 0.01, pct_pivot rvol = VOLS[DOJI] / media_vol levels = [ Level(SUPORT, f"suport monthly {SUPORT:.2f}", BLUE, label_at="left"), Level(PIVOT, f"pivot monthly {PIVOT:.2f}", BLUE), ] zones = [ Zone(0, 11, "1. downtrend — de asta apare în scaner", RED), Zone(12, DOJI, "", BLUE, p_bot=SUPORT - 0.6), ] arrows = [ Arrow(15, f"2. suportul monthly {SUPORT:.2f} a mai întors prețul o dată:\n" f"acolo stau ordine de cumpărare, deci coborârea are unde să se oprească", where="below", price=SUPORT, color=BLUE, dx=-30, dy=34), Arrow(DOJI, f"3. buy doji, close {DOJI_CLOSE:.2f} — aici se oprește selecția\n" f"{pct_suport:.2f} % peste suport, {pct_pivot:.2f} % sub pivot: trece proximitatea față de pivot\n" f"volumul zilei = {rvol:.2f} din medie (RVOL {rvol:.2f}), sub pragul de 1.00: minus, nu veto", where="above", color=TEAL, dx=-250, dy=-66), Arrow(27, "trigger, stop și țintă sunt\nale capitolului Buy Doji", where="right", color=GRAY, dx=16, dy=-40), ] caption = ("Ale cursului: close 373.30, suport monthly 355.48, pivot monthly 383.74, RVOL 0.61. " "Procentele sunt calculate din ele. Structura barelor e construită, la scară. " "Intrarea, stopul și ținta nu apar pe desen: nu aparțin selecției.") ch = Chart( candles=cds, levels=levels, zones=zones, arrows=arrows, mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")], scale=8.5, gap=15, body_w=9, left=44, width=640, vol_h=70, vol_label="volum", title="ULTA — daily: ce citește selecția și unde se oprește", caption=caption, ) svg = ch.render() # ---------------------------------------------------------------- linia verticală de predare # offsetul intern al randării, citit din prima linie de Level desenată m_off = re.search(r']*fill="' + RED + r'"', svg) assert m_dx, "nu găsesc dreptunghiul primei zone, din care citesc offsetul pe x" DX = float(m_dx.group(1)) - (ch._cx(0) - ch.body_w / 2 - 5) x_sep = ch._cx(DOJI) + DX + ch.gap / 2 y_top = ch._y(max(c.h for c in cds)) + OFF + 4 y_bot = ch._y(min(c.l for c in cds)) + OFF + 16 sep = (f'' f'aici se termină selecția' f'aici începe capitolul 1') svg = svg.replace("", sep + "") # ---------------------------------------------------------------- caption pe mai multe rânduri 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, 92) 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():] vm = re.search(r'viewBox="0 0 (\d+) (\d+)"', svg) assert vm svg = svg.replace(vm.group(0), f'viewBox="0 0 {vm.group(1)} {int(vm.group(2)) + 17 * (len(cap_lines) - 1) + 24}"', 1) # ---------------------------------------------------------------- nicio cifră inventată în etichete desenate = {f"{SUPORT:.2f}", f"{PIVOT:.2f}", f"{DOJI_CLOSE:.2f}"} for txt in re.findall(r"]*>([^<]*)", svg): for nr in re.findall(r"\d+\.\d\d", txt): assert nr in desenate | {f"{pct_suport:.2f}", f"{pct_pivot:.2f}", f"{rvol:.2f}", "1.00"}, \ f"eticheta numește {nr}, care nu e un nivel desenat: {txt!r}" assert "