#!/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'"])
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})")