#!/usr/bin/env python3 """Buy Doji — fereastra de 3 candele: DA (valid) vs. NU (expirat). Citește `gen/strategii/buy_doji_fereastra.spec.txt` și scrie `gen/strategii/buy_doji_fereastra.svg`. Două panouri mici, unul sub altul, cu aceeași serie până la doji: sus confirmarea vine la bara +2 (în fereastră), jos niciuna din +1..+3 nu confirmă, iar a 4-a închide sub low-ul dojiului. Adnotat „ca la proști": zonă dreptunghiulară peste cele 3 candele ale ferestrei pe ambele panouri, plus săgeți cu text care spun CE și DE CE („aici NU intri — s-a închis sub high-ul dojiului", „AICI INTRI, la close"). Toate cifrele din desen vin din specificație: doji h 50.10 / l 49.25, closes-urile DA/NU, pragul 50.10. Seriile sunt construite (open = close-ul barei anterioare, mai puțin open-ul primei bare, start-ul), ca nivelurile afirmate să iasă exact din serie. Rulare: python3 gen/strategii/buy_doji_fereastra.py """ import re import sys from pathlib import Path sys.path.insert(0, "/workspace/atm-curs-dl") from svg_candles import Arrow, Chart, Mark, from_closes # noqa: E402 OUT = Path(__file__).with_suffix(".svg") W = 640 VGAP = 0 SCALE = 60.0 # px / unitate, aceeași pe ambele panouri (comparație) LEFT, GAP = 64, 40 ORANGE, RED, GREEN, BLUE, GREY = "#b06000", "#c00", "#188a3e", "#1a5fb4", "#555" # --- seriile din specificație ---------------------------------------------- CLOSES_DA = [52.20, 51.40, 50.60, 49.65, 49.60, # doji, h 50.10 / l 49.25 49.90, # +1 — sub 50.10 50.40, # +2 — peste 50.10 => confirmă 50.80] CLOSES_NU = [52.20, 51.40, 50.60, 49.65, 49.60, # doji, h 50.10 / l 49.25 49.95, # +1 50.05, # +2 49.85, # +3 49.15] # a 4-a — sub low-ul dojiului assert len(CLOSES_DA) == 8 and len(CLOSES_NU) == 9 assert CLOSES_DA[:5] == CLOSES_NU[:5] HIGH_DOJI, LOW_DOJI = 50.10, 49.25 # wick-uri: (up, down). Doji-ul (index 4) primește fitile ca să iasă exact # h=50.10 / l=49.25; restul au fitile mici, egale. wicks_da = [(0.15, 0.15)] * len(CLOSES_DA) wicks_nu = [(0.15, 0.15)] * len(CLOSES_NU) wicks_da[4] = wicks_nu[4] = (0.45, 0.35) START = CLOSES_DA[0] + 0.15 # prima bară roșie, open > close cds_da = from_closes(CLOSES_DA, wicks=wicks_da, start=START) cds_nu = from_closes(CLOSES_NU, wicks=wicks_nu, start=START) doji_da, doji_nu = cds_da[4], cds_nu[4] entry = cds_da[6].c # intrare la close-ul confirmării # --- assert-urile din specificație ----------------------------------------- # doji-ul iese exact la nivelurile afirmate assert abs(doji_da.h - HIGH_DOJI) < 1e-9 and abs(doji_da.l - LOW_DOJI) < 1e-9, \ f"doji DA {doji_da.h}/{doji_da.l} != {HIGH_DOJI}/{LOW_DOJI}" assert abs(doji_nu.h - HIGH_DOJI) < 1e-9 and abs(doji_nu.l - LOW_DOJI) < 1e-9, \ f"doji NU {doji_nu.h}/{doji_nu.l} != {HIGH_DOJI}/{LOW_DOJI}" # 1. DA: primul close peste 50.10 DUPĂ doji e bara +2 (index 6), în fereastră; # bara +1 nu confirmă primul_da = next(i for i in range(5, len(cds_da)) if cds_da[i].c > HIGH_DOJI) assert primul_da == 6, f"prima confirmare la index {primul_da}, nu 6 (+2)" assert primul_da - 4 <= 3, "confirmarea nu e în fereastra de 3 candele" assert abs(cds_da[5].c - 49.90) < 1e-9 and cds_da[5].c <= HIGH_DOJI, \ "bara +1 nu trebuie să confirme" assert abs(cds_da[6].c - entry) < 1e-9 and abs(entry - 50.40) < 1e-9, \ "intrarea nu e close-ul de 50.40 al barei +2" # 2. NU: niciun close din +1..+3 nu depășește 50.10; a 4-a închide sub 49.25 assert all(cds_nu[i].c <= HIGH_DOJI for i in (5, 6, 7)), \ "un close din +1..+3 ar confirma, fereastra n-ar fi expirat" assert cds_nu[8].c < LOW_DOJI, f"close[+4]={cds_nu[8].c} nu e sub {LOW_DOJI}" # 3. ambele doji: corp <= 10% din amplitudine for d in (doji_da, doji_nu): assert abs(d.o - d.c) <= 0.10 * (d.h - d.l), "bara 4 nu e doji" # 4. aceeași serie până la doji (aceleași 5 bare) ser_da = [(round(c.o, 9), round(c.h, 9), round(c.l, 9), round(c.c, 9)) for c in cds_da[:5]] ser_nu = [(round(c.o, 9), round(c.h, 9), round(c.l, 9), round(c.c, 9)) for c in cds_nu[:5]] assert ser_da == ser_nu, "panourile nu au aceeași serie până la doji" # --- desen: două Chart-uri, lipite vertical într-un singur SVG ------------- cds_nu[7].note = "+1: 49.95 · +2: 50.05 · +3: 49.85 — toate sub 50.10" ch_da = Chart( candles=cds_da, marks=[Mark(4, "doji", ORANGE, "above")], arrows=[ Arrow(5, "aici NU intri: s-a închis sub 50.10\nsemnalul cere o ÎNCHIDERE peste", where="below", color=RED), Arrow(6, "AICI INTRI, la close 50.40\nprima închidere peste\nhigh-ul dojiului", price=entry, where="right", color=GREEN, dx=20, dy=46), ], scale=SCALE, gap=GAP, body_w=13, left=LEFT, width=W, title="DA — confirmare la +2, în fereastră", ) ch_nu = Chart( candles=cds_nu, marks=[Mark(4, "doji", ORANGE, "above"), Mark(8, "a 4-a", RED, "above")], arrows=[ Arrow(7, "a treia s-a închis tot sub 50.10\nfereastra s-a terminat: ștergi setup-ul", where="above", color=RED, dx=135), Arrow(8, "a închis sub low-ul dojiului\nsetup mort — nu mai intri", where="below", color=RED), ], scale=SCALE, gap=GAP, body_w=13, left=LEFT, width=W, title="NU — nicio confirmare în 3 candele", ) def _panel(ch: Chart): """`_render()`, nu `render()`: al doilea restaurează top/left după desen, iar liniile și zonele desenate de mână ar pica deplasate față de body.""" svg = ch._render() m = re.search(r'viewBox="0 0 ([\d.]+) ([\d.]+)"', svg) assert m, "viewBox lipsă" assert float(m.group(1)) == W, f"textul unei săgeți iese din {W}px" body = svg[svg.index(">") + 1:svg.rindex("")] return body, float(m.group(2)) def level(ch, price, text, color, below=False, at_left=True): """Linie punctată pe toată lățimea + etichetă, desenată de mână: eticheta din `Level` stă lipită de linie și cade peste candelele care o traversează.""" y = ch._y(price) out = [f''] for k, s in enumerate(text): ty = (y + 15 + 14 * k) if below else (y - 6 - 14 * (len(text) - 1 - k)) tx, anchor = (30, "start") if at_left else (W - 24, "end") out.append(f'{s}') return out def zone(ch, i0, i1, label, color, lift=0.0): """Dreptunghi de evidențiere peste candelele i0..i1, cu eticheta deasupra. `lift` ridică marginea de sus (și deci eticheta), ca să nu cadă peste săgețile care intră în zonă de sus.""" cds = ch.candles[i0:i1 + 1] x0, x1 = ch._cx(i0) - 16, ch._cx(i1) + 16 y0 = ch._y(max(c.h for c in cds)) - 8 - lift y1 = ch._y(min(c.l for c in cds)) + 8 return [f'', f'{label}'] body_da, h_da = _panel(ch_da) body_nu, h_nu = _panel(ch_nu) extra_da = (zone(ch_da, 5, 7, "fereastra: 3 candele", BLUE) + level(ch_da, HIGH_DOJI, ["high-ul dojiului 50.10", "trebuie ÎNCHIS peste,", "nu doar atins"], ORANGE, below=True) + level(ch_da, LOW_DOJI, ["low-ul dojiului 49.25"], RED, below=True)) extra_nu = (zone(ch_nu, 5, 7, "fereastra: 3 candele", BLUE) + level(ch_nu, HIGH_DOJI, ["high-ul dojiului 50.10", "trebuie ÎNCHIS peste,", "nu doar atins"], ORANGE, below=True) + level(ch_nu, LOW_DOJI, ["low-ul dojiului 49.25"], RED, below=True)) cap1 = "Serii construite: open = close-ul barei anterioare." cap2 = "Doji: h 50.10 / l 49.25. Fereastra = maximum 3 candele după doji." cap_top = h_da + VGAP + h_nu + 20 H = cap_top + 22 parts = [ f'', # zonele și liniile întâi, ca să stea în spatele candelelor f'{"".join(extra_da)}{body_da}', f'{"".join(extra_nu)}{body_nu}', f'{cap1}', f'{cap2}', "", ] svg = "\n ".join(parts) assert "\n\n" not in svg, "linie goală în SVG" assert "<" not in cap1 and "<" not in cap2 # --- verificarea citirii: etichete în panză, fără suprapuneri --------------- TEXT = re.compile(r']*)>(.*?)', re.S) def _texts(body: str, y_off: float, tag: str): out = [] for m in TEXT.finditer(body): x, y = float(m.group(1)), float(m.group(2)) + y_off attrs, s = m.group(3), m.group(4) anchor = "start" # implicit în SVG if 'text-anchor="end"' in attrs: anchor = "end" elif 'text-anchor="middle"' in attrs: anchor = "middle" bold = "bold" in attrs w = len(s) * 12 * (0.62 if bold else 0.52) left = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x) out.append((left, left + w, y, s, tag)) return out labels = (_texts(body_da + "".join(extra_da), 0.0, "DA") + _texts(body_nu + "".join(extra_nu), h_da + VGAP, "NU")) for left, right, y, s, tag in labels: assert left >= -2 and right <= W + 2, f"[{tag}] text iese din panză: {s!r}" assert 0 <= y <= H, f"[{tag}] text în afara înălțimii: {s!r} (y={y:.0f})" for i in range(len(labels)): li, ri, yi, si, ti = labels[i] for j in range(i + 1, len(labels)): lj, rj, yj, sj, tj = labels[j] if ti == tj and abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4: raise AssertionError(f"[{ti}] texte suprapuse: {si!r} / {sj!r}") print(f"DA: doji {doji_da.h:.2f}/{doji_da.l:.2f} +1 {cds_da[5].c:.2f} " f"+2 {entry:.2f} (confirmă) bare {len(cds_da)}") print(f"NU: +1 {cds_nu[5].c:.2f} +2 {cds_nu[6].c:.2f} +3 {cds_nu[7].c:.2f} " f"a 4-a {cds_nu[8].c:.2f} (< {LOW_DOJI}) bare {len(cds_nu)}") # 5. distanța în pixeli 50.10 -> 50.40 e vizibilă, cu scară comună dist = ch_da._y(HIGH_DOJI) - ch_da._y(entry) assert ch_da.scale == ch_nu.scale == SCALE, "scara nu e comună între panouri" assert dist > 0 and dist > 10, f"distanța 50.10->50.40 = {dist:.1f}px, prea mică" print(f"scris {OUT} ({W}x{H:.0f}); {len(labels)} etichete verificate; " f"50.10->50.40 = {dist:.1f}px") OUT.write_text(svg, encoding="utf-8")