"""ULTA — daily (pivoți monthly): diagrama de ansamblu a selecției (TIP: PRET). Citește `gen/strategii/selectia_ansamblu.spec.txt`. Un simbol care vine dintr-un downtrend weekly, atinge suportul monthly 355.48 și face un buy doji la 373.30 cu RVOL 0.61; apoi confirmarea (intrare 374.80) și drumul spre pivot/TP1 383.74, respectiv TP2 402.00. Grafic daily, 32 de candele, serie continuă, cu panou de volum și SMA-20 — construit prin `svg_candles` (from_closes), la scară. 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, Mark, 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" # ---------------------------------------------------------------- seria (32 bare) # bare 0-11 : downtrend vizibil de la ~395 la ~362 (~0.8%/bara) # bare 12-19 : baza/churn 356-373, minimul atinge suportul 355.48 pe bara 15 # bara 20 : buy doji, close 373.30 (open = close[19] = 373.00, corp 0.30) # bara 21 : confirmare, inchide peste intrarea 374.80 # bare 22-31 : urcare spre pivot/TP1 383.74 (bara 26) si apoi spre TP2 402.00 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 bara, deschide dintr-un varf 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[20] = (0.80, 0.30) # doji: high 374.10, low 372.70 WICKS[31] = (0.55, 0.40) # high 402.05, atinge TP2 402.00 assert len(WICKS) == 32, len(WICKS) # barele 25-31 in crestere; bara 20 (doji) sub media seriei (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 confirmare 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 VOLS[20] < media_vol, "bara 20 (doji) trebuie sub media seriei" assert all(a < b for a, b in zip(VOLS[25:32], VOLS[26:32])), "barele 25-31 nu cresc" # RVOL afisat (0.61) ~ raportul barei de doji fata de media seriei assert abs(VOLS[20] / media_vol - 0.61) < 0.02, VOLS[20] / media_vol cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=396.50) # ---------------------------------------------------------------- assert-urile din spec assert abs(cds[20].c - 373.30) < 1e-9, "doji-ul trebuie sa inchida la 373.30" assert abs(max(cds[20].o, cds[20].c) - min(cds[20].o, cds[20].c)) <= 0.40, "bara 20 nu e doji" assert abs(min(c.l for c in cds[12:21]) - 355.48) < 0.05, "suportul 355.48 nu e atins" assert 352.00 < 355.48 < 373.30 < 374.80 < 383.74 < 402.00, "ordinea nivelurilor" risc = 374.80 - 352.00 # 22.80 assert abs((383.74 - 374.80) / risc - 0.39) < 0.02, "TP1 nu e ~0.39R" assert abs((402.00 - 374.80) / risc - 1.19) < 0.02, "TP2 nu e ~1.19R" assert cds[-1].c >= 383.74, "TP1 nu e atins in serie" assert min(c.l for c in cds) >= 351.5, "SL-ul de 352.00 nu poate fi sub minimul seriei" for a, b in zip(cds, cds[1:]): assert abs(a.c - b.o) < 1e-9, "serie cu gol artificial intre bare" # structura ceruta explicit: doji mic cu fitilele date, confirmare peste intrare assert abs(cds[20].h - 374.10) < 0.05 and abs(cds[20].l - 372.70) < 0.05, (cds[20].h, cds[20].l) assert cds[21].c > 374.80, "bara 21 nu confirma intrarea" assert max(cds[i].h for i in range(22, 27)) >= 383.74, "TP1 nu e atins in jurul barei 26" assert cds[31].h >= 402.00, "TP2 nu e atins spre final" # ---------------------------------------------------------------- niveluri, etichete levels = [ Level(352.00, "SL 352.00", RED, label_at="left"), Level(355.48, "suport monthly 355.48", BLUE, label_at="left"), Level(374.80, "intrare 374.80", GREEN), Level(383.74, "pivot / TP1 383.74", BLUE), Level(402.00, "TP2 402.00", BLUE), ] marks = [ # „sub bara 0" ar insemna peste candelele care coboara (staircase); dy-ul # o ridica deasupra varfului de start, unde textul se citeste curat Mark(0, "downtrend weekly", GRAY, "below", dy=-62), Mark(20, "buy doji close 373.30 (RVOL 0.61)", TEAL, "below"), Mark(21, "intrare 374.80", GREEN, "above"), Mark(26, "TP1 383.74 = pivot", BLUE, "above", dy=-8), Mark(31, "TP2 402.00", BLUE, "above", dy=-22), ] caption = ("Ale cursului: close 373.30, suport 355.48, pivot 383.74, RVOL 0.61. " "Intrarea 374.80, SL 352.00 si TP2 402.00 sunt estimari din 04_MODUL_3/01; " "structura barelor dintre niveluri e construita, la scara.") ch = Chart( candles=cds, levels=levels, marks=marks, 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 (pivoți monthly)", caption=caption, ) svg = ch.render() # ---------------------------------------------------------------- verificarea de scara ys = {p: ch._y(p) for p in (352.00, 355.48, 374.80, 383.74, 402.00)} px_risc = ys[352.00] - ys[374.80] # y creste in jos, deci SL-ul are y mai mare assert abs((ys[374.80] - ys[383.74]) / px_risc - (383.74 - 374.80) / risc) < 1e-9, "TP1 la scara gresita" assert abs((ys[374.80] - ys[402.00]) / px_risc - (402.00 - 374.80) / risc) < 1e-9, "TP2 la scara gresita" # ---------------------------------------------------------------- finisaj text # caption-ul pe un singur rand depaseste latimea panzei (se taie la viewBox): # il rupem in linii si crestem inaltimea desenului. 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"\s*", svg) assert m, "caption-ul nu a fost gasit in SVG" y0 = int(m.group(1)) wrapped = "\n ".join(f'{ln}' for i, ln in enumerate(cap_lines)) svg = svg[:m.start()] + wrapped + "\n" 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)}"', 1) # „SMA-20" (x=515, y=269) sta la 12.6px de eticheta „intrare 374.80" (y=281.6), # adica lipite: o ridicam 12px pe linia libera de deasupra. svg, n = re.subn(r'(SMA-20)', lambda mm: f'{mm.group(1)}{int(mm.group(2)) - 12}{mm.group(3)}', svg) assert n == 1, f"eticheta SMA-20: {n} potriviri" assert "