#!/usr/bin/env python3 """ATMI — Pasul 4: trigger-ul. DA (BIM, apoi candela verde) vs. NU (verde fără BIM). Două panouri, unul sub altul, cu exact aceleași patru bare de corecție. Sus: prețul intră în zona PPQ, apare săgeata BIM, iar prima candelă verde daily de după ea închide la 96.00 — acolo intri. Jos: candela verde vine ÎNAINTE de orice BIM, deci nu e trigger; prețul continuă să cadă spre SQ — „cuțitul în cădere" din curs. Numerele sunt cele ale capitolului: PPQ 95.80, SQ 90.80 (calculate în `atmi_pivoti.py`), intrare 96.00 la close-ul candelei verzi. Rulare: python3 gen/strategii/atmi_trigger.py """ import re import sys from pathlib import Path sys.path.insert(0, "/workspace/atm-curs-dl") from svg_candles import Arrow, Chart, from_closes # noqa: E402 OUT = Path(__file__).with_suffix(".svg") W = 660 SCALE = 46.0 LEFT, GAP = 66, 44 RED, GREEN, BLUE, GREY, CYAN, PURPLE = "#c00", "#188a3e", "#1a5fb4", "#555", "#0a7f9e", "#7b2fbf" PPQ, SQ, ENTRY = 95.80, 90.80, 96.00 COMUN = [98.60, 97.80, 97.00, 96.40] # corecția, identică pe ambele panouri CLOSES_DA = COMUN + [95.70, 96.00, 96.60] # 4: BIM 5: prima verde = intrare CLOSES_NU = COMUN + [96.80, 96.10, 95.10, 94.00, 92.90] # 4: verde fără BIM w_da = [(0.15, 0.15)] * len(CLOSES_DA) w_da[4] = (0.10, 0.15) # bara BIM coboară sub PPQ: 95.55 w_da[5] = (0.10, 0.10) w_nu = [(0.15, 0.15)] * len(CLOSES_NU) w_nu[4] = (0.10, 0.10) START = COMUN[0] + 0.30 cds_da = from_closes(CLOSES_DA, wicks=w_da, start=START) cds_nu = from_closes(CLOSES_NU, wicks=w_nu, start=START) BIM_I, VERDE_DA, VERDE_NU = 4, 5, 4 # --- ce trebuie să fie adevărat ca desenul să susțină textul --------------- ser = lambda c: (round(c.o, 2), round(c.h, 2), round(c.l, 2), round(c.c, 2)) # noqa: E731 assert [ser(c) for c in cds_da[:4]] == [ser(c) for c in cds_nu[:4]], \ "panourile nu pleacă din aceeași corecție" # DA: BIM înainte de verde; verdele e chiar verde; close-ul lui = intrarea assert BIM_I < VERDE_DA assert cds_da[BIM_I].c < cds_da[BIM_I].o, "bara de BIM e bara de epuizare, roșie" assert cds_da[VERDE_DA].c > cds_da[VERDE_DA].o, "candela de intrare nu e verde" assert abs(cds_da[VERDE_DA].c - ENTRY) < 1e-9, cds_da[VERDE_DA].c # criteriul A+: candela de intrare închide DEASUPRA pivotului assert cds_da[VERDE_DA].c > PPQ, "candela de intrare nu închide peste PPQ" # prețul a intrat efectiv în zona PPQ (a coborât sub pivot și a fost respins) assert cds_da[BIM_I].l < PPQ < cds_da[VERDE_DA].c, (cds_da[BIM_I].l, PPQ) assert min(c.l for c in cds_da) == cds_da[BIM_I].l, "fundul nu e pe bara de BIM" # NU: verdele vine primul, și după el prețul face minime tot mai joase assert cds_nu[VERDE_NU].c > cds_nu[VERDE_NU].o, "bara 4 de pe panoul NU nu e verde" lows = [c.l for c in cds_nu[VERDE_NU + 1:]] assert all(b < a for a, b in zip(lows, lows[1:])), "cuțitul nu cade continuu" assert cds_nu[-1].c < PPQ - 2.5, "căderea de pe panoul NU e prea mică ca să se vadă" assert min(c.l for c in cds_nu) > SQ, "căderea desenată trece deja de SQ" # cele două panouri au aceeași scară, deci căderile se compară cu ochiul PIERDERE = round((cds_nu[-1].c - cds_nu[VERDE_NU].c) / cds_nu[VERDE_NU].c * 100, 1) assert PIERDERE < -3.0, PIERDERE ch_da = Chart( candles=cds_da, marks=[], arrows=[ Arrow(BIM_I, "BIM: săgeata indicatorului\ncyan = momentul în care\ntrendul se schimbă", where="below", color=CYAN), Arrow(VERDE_DA, f"AICI INTRI, la close {ENTRY:.2f}\nînchide peste pivot, deci\n" f"cumpărătorii au ținut zona", price=ENTRY, where="right", color=GREEN, dx=18, dy=40), ], scale=SCALE, gap=GAP, body_w=13, left=LEFT, width=W, title="DA — BIM, apoi prima candelă verde daily", ) ch_nu = Chart( candles=cds_nu, marks=[], arrows=[ Arrow(VERDE_NU, "aici NU intri: verdele a venit\nînainte de BIM, deci nimic\n" "nu confirmă că vânzarea s-a oprit", where="below", color=RED, dx=40), Arrow(len(cds_nu) - 1, f"căderea continuă: {abs(PIERDERE):.1f} % mai jos\nde la candela verde —\n" f"„cuțitul prins în cădere”", where="below", color=RED, dx=-30), ], scale=SCALE, gap=GAP, body_w=13, left=LEFT, width=W, title="NU — candelă verde înainte de BIM", ) def panel(ch): svg = ch._render() m = re.search(r'viewBox="0 0 ([\d.]+) ([\d.]+)"', svg) assert float(m.group(1)) == W, f"o etichetă iese din {W}px" return svg[svg.index(">") + 1:svg.rindex("")], float(m.group(2)) def level(ch, price, text, color, below=False): 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)) out.append(f'{s}') return out def zone(ch, i0, i1, label, color, lift=0.0): cds = ch.candles[i0:i1 + 1] x0, x1 = ch._cx(i0) - 17, ch._cx(i1) + 17 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, BIM_I, VERDE_DA, "zona de decizie: BIM + verde", BLUE, lift=16) + level(ch_da, PPQ, [f"PPQ {PPQ:.2f} — pivotul trimestrial"], PURPLE, below=True)) extra_nu = (zone(ch_nu, VERDE_NU, len(cds_nu) - 1, "fără BIM, nimic nu confirmă", RED, lift=16) + level(ch_nu, PPQ, [f"PPQ {PPQ:.2f} — pivotul trimestrial"], PURPLE, below=True)) CAP = ["Aceleași patru bare de corecție pe ambele panouri, aceeași scară.", "PPQ 95.80 e calculat în diagrama pasului 1; intrarea 96.00 e close-ul candelei verzi."] assert all(len(s) * 6.3 < W - 40 for s in CAP), "caption prea lung" cap_top = h_da + h_nu + 20 H = cap_top + 24 svg = "\n ".join([ f'', f'{"".join(extra_da)}{body_da}', f'{"".join(extra_nu)}{body_nu}', f'{CAP[0]}', f'{CAP[1]}', ""]) assert "\n\n" not in svg and ".md:" not in svg TEXT = re.compile(r']*)>(.*?)', re.S) def labels(body, y_off, tag): out = [] for m in TEXT.finditer(body): x, y, attrs, s = float(m.group(1)), float(m.group(2)) + y_off, m.group(3), m.group(4) anchor = "end" if 'text-anchor="end"' in attrs else ( "middle" if 'text-anchor="middle"' in attrs else "start") w = len(s) * 12 * (0.62 if "bold" in attrs else 0.52) lf = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x) out.append((lf, lf + w, y, s, tag)) return out LB = (labels(body_da + "".join(extra_da), 0.0, "DA") + labels(body_nu + "".join(extra_nu), h_da, "NU")) for lf, rt, y, s, tag in LB: assert -2 <= lf and rt <= W + 2, f"[{tag}] text iese din panză: {s!r}" assert 0 <= y <= H, f"[{tag}] text în afara înălțimii: {s!r}" for i in range(len(LB)): for j in range(i + 1, len(LB)): li, ri, yi, si, ti = LB[i] lj, rj, yj, sj, tj = LB[j] assert not (ti == tj and abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4), \ f"[{ti}] texte suprapuse: {si!r} / {sj!r}" # niciun preț scris într-o etichetă care nu e un nivel desenat pe panoul lui for lf, rt, y, s, tag in LB: for p in re.findall(r"\d+\.\d\d", s): assert float(p) in (PPQ, ENTRY), f"[{tag}] prețul {p} din {s!r} nu e un nivel desenat" OUT.write_text(svg) print(f"DA: BIM la {BIM_I} (low {cds_da[BIM_I].l:.2f} < PPQ {PPQ}), intrare {ENTRY:.2f}") print(f"NU: verde la {VERDE_NU} fără BIM, apoi {PIERDERE:.1f} % în jos " f"(ultimul close {cds_nu[-1].c:.2f}, peste SQ {SQ})") print(f"scris {OUT.name} ({W}x{H:.0f})")