#!/usr/bin/env python3 """Buy Doji — NU: Sell Doji daily contra unui Buy Doji weekly. Citește `gen/strategii/buy_doji_directie.spec.txt` și scrie `gen/strategii/buy_doji_directie.svg`. Două panouri stivuite: sus weekly (12 bare, Buy Doji confirmat), jos daily (15 bare = ultimele 3 bare weekly desfăcute zi cu zi, 5 zile pe săptămână). Seria weekly 9..11 NU e scrisă de mână: e agregarea reală a celor 15 zile (open = open-ul primei zile, close = close-ul ultimei, high/low = extremele). Panoul weekly poartă un dreptunghi peste exact acele 3 bare, plus două ghidaje către colțurile cadrului daily, iar high-ul și low-ul doji-ului daily sunt desenate ca aceeași linie punctată, cu același număr, pe ambele panouri. Coordonatele sunt absolute (fără ), ca audit_diagrams.py să vadă pozițiile reale ale textelor. Rulare: python3 gen/strategii/buy_doji_directie.py """ import re from pathlib import Path OUT = Path(__file__).with_suffix(".svg") W = 900 GROUP = 5 # zile pe săptămână ZOOM = 3 # câte bare weekly sunt desfăcute jos def trend(start, closes, wick=0.15): bars, o = [], start for c in closes: bars.append((o, round(max(o, c) + wick, 2), round(min(o, c) - wick, 2), c)) o = c return bars def agg(days): """Agregă GROUP zile într-o bară weekly.""" return (days[0][0], max(b[1] for b in days), min(b[2] for b in days), days[-1][3]) # --- seria daily: 15 bare = 3 săptămâni -------------------------------------- DY = trend(81.30, [81.70, 82.20, 82.00, 82.60, 83.10, 83.50]) + [ (83.50, 84.30, 83.40, 83.60), # 6: zi cu respingere sus (dă high-ul de referință) (83.60, 83.90, 83.05, 83.55), # 7: doji daily (83.55, 83.65, 82.60, 82.70), # 8: confirmare => Sell Doji, intrare short (82.70, 82.80, 82.00, 82.25), # 9: singura zi de scădere (82.25, 83.05, 82.15, 82.95), (82.95, 83.70, 82.85, 83.60), (83.60, 84.45, 83.50, 84.30), # 12: SL lovit (84.30, 84.95, 84.20, 84.80), (84.80, 85.45, 84.70, 85.30)] # --- seria weekly: 9 bare de context + cele 3 agregate din daily ------------- WK = trend(89.0, [88.0, 86.2, 84.9, 83.1, 81.6, 80.2, 78.9]) + [ (78.90, 79.80, 77.60, 78.80), # 7: doji weekly (78.80, 81.60, 78.60, 81.30)] # 8: confirmare => Buy Doji weekly WK += [agg(DY[k:k + GROUP]) for k in range(0, len(DY), GROUP)] Z0 = len(WK) - ZOOM # prima bară weekly mărită jos def eq(a, b): return abs(a - b) < 0.005 # --- assert-uri: coerență, agregare, poveste --------------------------------- assert len(DY) == ZOOM * GROUP and len(WK) == 9 + ZOOM for name, bars in (("weekly", WK), ("daily", DY)): for i, (o, h, l, c) in enumerate(bars): assert h >= max(o, c) and l <= min(o, c), f"{name}[{i}] h/l incoerent" if i: assert eq(o, bars[i - 1][3]), f"{name}[{i}] open != close precedent" assert eq(DY[0][0], WK[8][3]), "daily nu începe la close-ul confirmării weekly" # agregarea, verificată din nou pe valorile finale (nu pe cele care au construit-o) for k in range(ZOOM): days = DY[k * GROUP:(k + 1) * GROUP] o, h, l, c = WK[Z0 + k] assert eq(o, days[0][0]) and eq(c, days[-1][3]), f"weekly[{Z0+k}] o/c != zile" assert eq(h, max(b[1] for b in days)), f"weekly[{Z0+k}] high != max(high zile)" assert eq(l, min(b[2] for b in days)), f"weekly[{Z0+k}] low != min(low zile)" for name, (o, h, l, c) in (("doji weekly", WK[7]), ("doji daily", DY[7])): assert abs(o - c) <= 0.10 * (h - l) + 1e-9, f"{name} nu e doji" assert WK[8][3] > WK[7][1], "weekly: fără confirmare" assert DY[8][3] < DY[7][2], "daily: fără confirmare Sell Doji" DH, DL = DY[7][1], DY[7][2] # nivelurile comune: high/low doji daily ENTRY = DY[8][3] SL = round(max(b[1] for b in DY[5:9]) + 0.05, 2) # 5 cenți peste high-ul de 4 candele RISK = round(SL - ENTRY, 2) TP1 = round(ENTRY - RISK, 2) assert eq(ENTRY, 82.70) and eq(SL, 84.35) and eq(RISK, 1.65) and eq(TP1, 81.05) assert SL - DH > 0.30, "stopul e prea lipit de high-ul doji-ului (etichete suprapuse)" assert DL - ENTRY > 0.30, "intrarea e prea lipită de low-ul doji-ului" LOW_AFTER = min(b[2] for b in DY[9:]) assert eq(LOW_AFTER, 82.00) and LOW_AFTER > TP1, "TP1 atins" HIT = next(i for i in range(9, len(DY)) if DY[i][1] >= SL - 1e-9) assert HIT == 12, f"SL lovit la {HIT}" assert min(b[2] for b in DY) < DH < max(b[1] for b in WK), "nivel comun în afara scării" # --- desen ------------------------------------------------------------------- GREEN, RED, ORANGE, GRAY, INK, BLUE = "#188a3e", "#c00", "#d06a00", "#777", "#333", "#1a5fb4" BODY_W = 9 out, texts = [], [] PANEL = "none" def text(x, y, s, fill=INK, anchor="start", bold=False, size=None, panel=None): st = (["font-weight:bold"] if bold else []) + ([f"font-size:{size}px"] if size else []) style = f' style="{";".join(st)}"' if st else "" a = f' text-anchor="{anchor}"' if anchor != "start" else "" out.append(f'{s}') texts.append((panel or PANEL, s)) def line(x1, y1, x2, y2, color, width=1.5, dash=None): d = f' stroke-dasharray="{dash}"' if dash else "" out.append(f'') def rect(x, y, w, h, fill, opacity, stroke="none", dash=None): d = f' stroke-dasharray="{dash}"' if dash else "" out.append(f'') def arrow(x1, y1, x2, y2, color): d = max(((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5, 1e-9) ux, uy = (x2 - x1) / d, (y2 - y1) / d bx, by = x2 - 9 * ux, y2 - 9 * uy line(x1, y1, bx, by, color, 1.8) out.append(f'') def candles(bars, x0, gap, yf): for i, (o, h, l, c) in enumerate(bars): cx = x0 + i * gap line(cx, yf(h), cx, yf(l), "#1a1a1a", 1.2) top = yf(max(o, c)) hh = max(yf(min(o, c)) - top, 1.6) out.append(f'') text(20, 24, "NU: Sell Doji daily contra unui Buy Doji weekly — exemplu construit", bold=True, size=14) # ---- panoul de sus: weekly ---------------------------------------------------- PANEL = "weekly" WX0, WGAP, WS, WT = 60, 16, 16, 70 wmax, wmin = max(b[1] for b in WK), min(b[2] for b in WK) yw = lambda p: WT + (wmax - p) * WS # noqa: E731 xw = lambda i: WX0 + i * WGAP # noqa: E731 w_bottom = yw(wmin) text(20, 50, "weekly: direcția", fill="#555", bold=True) rect(xw(7) - 8, yw(WK[8][1]) - 4, WGAP + 16, yw(WK[7][2]) - yw(WK[8][1]) + 8, GREEN, 0.12, GREEN) # dreptunghiul de corespondență: exact barele weekly desfăcute în panoul daily ZX1, ZX2, ZY1, ZY2 = xw(Z0) - 8, xw(len(WK) - 1) + 8, WT - 10, w_bottom + 8 rect(ZX1, ZY1, ZX2 - ZX1, ZY2 - ZY1, BLUE, 0.07, BLUE, "5 4") text(ZX2 + 12, ZY1 + 14, f"ultimele {ZOOM} săptămâni,", fill=BLUE, size=11) text(ZX2 + 12, ZY1 + 30, "mărite jos", fill=BLUE, size=11) # nivelurile comune, desenate identic pe ambele panouri COMMON = [(DH, f"{DH:.2f}", "high doji"), (DL, f"{DL:.2f}", "low doji")] candles(WK, WX0, WGAP, yw) for k, (lvl, lab, _) in enumerate(COMMON): line(44, yw(lvl), ZX2 + 8, yw(lvl), BLUE, 1.2, "3 3") text(40, yw(lvl) + (-2 if k == 0 else 11), lab, fill=BLUE, anchor="end", size=10, panel="common") for k in range(ZOOM): text(xw(Z0 + k), ZY2 - 6, f"S{k + 1}", fill=BLUE, anchor="middle", size=10, bold=True, panel="common") arrow(470, yw(80.50), xw(8) + 11, yw(80.50), GREEN) text(476, yw(80.50) - 4, f"weekly: Buy Doji confirmat (close {WK[8][3]:.2f}, peste " f"high-ul doji-ului {WK[7][1]:.2f})", fill=GREEN, bold=True, size=11) text(476, yw(80.50) + 14, "deci direcția permisă e LONG: pe daily cauți doar Buy Doji", fill=GREEN, size=11) text(xw(7) + 5, ZY2 + 8, "doji weekly", fill=ORANGE, anchor="end", bold=True, size=10) # ---- panoul de jos: daily ----------------------------------------------------- PANEL = "daily" DX0, DGAP, DS = 60, 18, 40.0 FT = w_bottom + 92 # marginea de sus a cadrului daily DT = FT + 14 dmax, dmin = max(b[1] for b in DY), min(b[2] for b in DY) yd = lambda p: DT + (dmax - p) * DS # noqa: E731 xd = lambda i: DX0 + i * DGAP # noqa: E731 FX1, FX2 = xd(0) - 14, xd(len(DY) - 1) + 14 FB = max(yd(dmin), yd(TP1)) + 14 # ghidajele: colțurile de jos ale dreptunghiului -> colțurile de sus ale cadrului line(ZX1, ZY2, FX1, FT, BLUE, 1.2, "5 4") line(ZX2, ZY2, FX2, FT, BLUE, 1.2, "5 4") out.append(f'') text(FX2 + 34, ZY2 + 26, f"daily: aceleași {ZOOM} săptămâni, zi cu zi", fill=BLUE, bold=True) text(FX2 + 34, ZY2 + 44, f"({GROUP} zile = o bară weekly; open, close și extremele coincid)", fill="#555", size=11) rect(xd(7) - 8, yd(DY[7][1]) - 3, DGAP + 16, yd(DY[8][2]) - yd(DY[7][1]) + 6, ORANGE, 0.15, ORANGE) for k, (lvl, lab, _) in enumerate(COMMON): line(44, yd(lvl), FX2 + 14, yd(lvl), BLUE, 1.2, "3 3") text(40, yd(lvl) + 4, lab, fill=BLUE, anchor="end", size=10, panel="common") LX2, COL = 340, 348 y_sl, y_e, y_tp = yd(SL), yd(ENTRY), yd(TP1) assert abs((y_sl - y_e) - (y_e - y_tp)) < 1e-6, "SL-intrare != intrare-TP1 în px" line(44, y_sl, LX2, y_sl, RED, 1.5, "6 3") line(xd(8) - 8, y_e, LX2, y_e, ORANGE, 1.5, "6 3") line(xd(8) - 8, y_tp, LX2, y_tp, GRAY, 1.5, "2 3") text(COL, y_sl + 4, f"stop {SL:.2f}", fill=RED, size=11, bold=True) text(COL, y_e + 4, f"intrare short {ENTRY:.2f}", fill=ORANGE, size=11, bold=True) text(COL, y_tp + 4, f"ținta {TP1:.2f} = 1R — neatinsă", fill=GRAY, size=11) candles(DY, DX0, DGAP, yd) for k in range(ZOOM): text((xd(k * GROUP) + xd(k * GROUP + GROUP - 1)) / 2, FB + 16, f"S{k + 1}", fill=BLUE, anchor="middle", size=10, bold=True, panel="common") text((xd(7) + xd(8)) / 2, yd(DY[7][1]) - 6, "doji daily", fill=ORANGE, anchor="middle", bold=True, size=10) cx_hit = xd(HIT) out.append(f'') arrow(512, y_sl - 26, cx_hit + 9, y_sl - 9, RED) text(520, y_sl - 30, f"aici te-a scos: stopul {SL:.2f} (5 cenți peste high-ul de 4 candele),", fill=RED, bold=True, size=11) text(520, y_sl - 12, f"lovit a {HIT - 8}-a zi — banii mari de pe weekly cumpără mai departe", fill=RED, size=11) arrow(512, y_e + 52, xd(8) + 8, y_e + 4, ORANGE) text(520, y_e + 48, f"aici ai intrat SHORT, la {ENTRY:.2f}: Sell Doji corect ca formă", fill=ORANGE, bold=True, size=11) text(520, y_e + 66, f"(close sub low-ul doji-ului {DL:.2f}) — dar contra weekly-ului", fill=ORANGE, size=11) text(520, y_e + 84, f"a scăzut o singură zi, până la {LOW_AFTER:.2f}", fill=ORANGE, size=11) PANEL = "none" y0 = FB + 42 text(20, y0, "Liniile albastre punctate sunt aceleași niveluri pe ambele panouri: " "high-ul și low-ul doji-ului daily.", fill="#555", size=11) text(20, y0 + 18, "Seria e construită: un singur caz ilustrează regula, nu o dovedește.", fill="#555", size=11) H = y0 + 40 # --- niciun număr scris de mână: fiecare preț din etichete vine din serii ------ WNUM = {f"{v:.2f}" for b in WK for v in b} DNUM = {f"{v:.2f}" for b in DY for v in b} | {f"{v:.2f}" for v in (SL, ENTRY, TP1, RISK)} CNUM = {f"{DH:.2f}", f"{DL:.2f}"} ALLOWED = {"weekly": WNUM, "daily": DNUM, "common": CNUM, "none": set()} for panel, s in texts: for n in re.findall(r"\d+\.\d\d", s): assert n in ALLOWED[panel], f"eticheta {panel} conține prețul străin {n}: {s!r}" # nivelurile comune apar cu exact același număr pe ambele panouri com = [s for p, s in texts if p == "common" and re.fullmatch(r"\d+\.\d\d", s)] for lab in (f"{DH:.2f}", f"{DL:.2f}"): assert com.count(lab) == 2, f"nivelul comun {lab} nu apare pe ambele panouri: {com}" svg = (f'\n ' + "\n ".join(out) + "\n") assert "\n\n" not in svg OUT.write_text(svg, encoding="utf-8") for k in range(ZOOM): print(f"S{k+1}: weekly {WK[Z0+k]} = zilele {k*GROUP}..{k*GROUP+GROUP-1}") print(f"intrare {ENTRY:.2f} SL {SL:.2f} risc {RISK:.2f} TP1 {TP1:.2f} | " f"doji high {DH:.2f} low {DL:.2f} | min după {LOW_AFTER:.2f} | SL lovit la bara {HIT}") print(f"scris {OUT} ({W}x{H:.0f})")