#!/usr/bin/env python3
"""MP² — crossing vs. buy the dip: două panouri una sub alta.
Citește `gen/strategii/mp2_crossing.spec.txt` și scrie
`gen/strategii/mp2_crossing.svg`.
Aceleași două linii de pivot pe ambele panouri:
- pivot weekly (cel MARE) = 62.00, orizontal;
- pivot daily (cel MIC), stepped — se recalculează zilnic și „sare" pe nivel nou.
Sus, „DA — crossing (+2)": pivotul daily urcă din 61.70 și traversează 62.00 în
sus (marcat cu cerc: „crossing: 61.70 → 62.20"). Trigger = prima candelă verde
după crossing care închide peste pivotul daily (bara 11, close 62.40).
Jos, „NU — continuare/buy the dip (+1)": pivotul daily e deja deasupra lui 62.00
și rămâne acolo; prețul face închideri peste el, dar nimic nu s-a re-încrucișat,
deci semnalele ulterioare sunt buy the dip, nu crossing.
Cifrele (62.00 / 61.70 / 62.20) sunt din `summaries/09_QA/09_2025-12-09.md:69-83`
și distincția crossing vs. buy the dip din `summaries/09_QA/16_2025-10-07.md:35-43`.
Seriile de preț sunt construite (open = close-ul barei anterioare) ca nivelurile
afirmate să iasă exact din ele.
Rulare: python3 gen/strategii/mp2_crossing.py
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Chart, Level, Mark, from_closes # noqa: E402
OUT = Path(__file__).with_suffix(".svg")
W = 480
VGAP = 44
SCALE = 95.0 # px / unitate, aceeași pe ambele panouri
WEEKLY = 62.00
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
# ---------------------------------------------------------------- panoul DA
# pivotul daily: 61.70 (sub 62.00) până la bara 9, apoi 62.20 (peste 62.00)
PDA = [61.70] * 10 + [62.20] * 12
CLOSES_DA = [61.30, 61.15, 61.35, 61.20, 61.45, 61.30, 61.55, 61.40,
61.60, 61.75, # 0-9: sub pivot 61.70/62.00
62.05, # 10: pivotul sare la 62.20; close inca sub el
62.40, # 11: trigger — verde, close peste 62.20
62.55, 62.50, 62.65, 62.60, 62.75, 62.70,
62.85, 62.80, 62.90, 62.95] # 12-21: urca peste pivot
WICKS_DA = [(0.08, 0.10)] * len(CLOSES_DA)
WICKS_DA[10] = (0.16, 0.10) # bara crossing-ului
WICKS_DA[11] = (0.22, 0.10) # bara de trigger
assert len(CLOSES_DA) == len(PDA) == 22
# ---------------------------------------------------------------- panoul NU
# pivotul daily e deja deasupra lui 62.00 pe toată durata (62.30 → 62.20)
PNU = [62.30] * 9 + [62.20] * 13
CLOSES_NU = [61.55, 61.70, 61.90, 61.80, 62.05,
62.35, # 5: primul close peste pivotul daily
62.25, 62.45, 62.55,
62.40, 62.60, 62.50, 62.70, 62.65, 62.80, 62.75,
62.90, 62.85, 63.00, 62.95, 63.10, 63.05]
WICKS_NU = [(0.08, 0.10)] * len(CLOSES_NU)
assert len(CLOSES_NU) == len(PNU) == 22
START_DA = CLOSES_DA[0] + 0.12
START_NU = CLOSES_NU[0] + 0.12
cds_da = from_closes(CLOSES_DA, wicks=WICKS_DA, start=START_DA)
cds_nu = from_closes(CLOSES_NU, wicks=WICKS_NU, start=START_NU)
# ------------------------------------------------------ assert-urile din spec
# 1. DA: pivotul daily e < 62.00 înainte de crossing și > 62.00 după
cross_da = next(i for i in range(1, len(PDA)) if PDA[i] > WEEKLY)
assert cross_da == 10, f"crossing-ul DA e la bara {cross_da}, nu 10"
assert all(v < WEEKLY for v in PDA[:cross_da]), "pivot daily DA nu e sub 62.00 înainte de crossing"
assert PDA[cross_da - 1] == 61.70 and PDA[cross_da] == 62.20, \
f"crossing DA {PDA[cross_da - 1]} -> {PDA[cross_da]}, nu 61.70 -> 62.20"
assert all(v > WEEKLY for v in PDA[cross_da:]), "pivot daily DA nu e peste 62.00 după crossing"
# 2. DA: trigger = prima candelă verde după crossing care închide peste pivotul daily
trigger = next(i for i in range(cross_da, len(cds_da))
if cds_da[i].c >= cds_da[i].o and cds_da[i].c > PDA[i])
assert trigger == 11, f"trigger-ul DA e la bara {trigger}, nu 11"
assert cds_da[trigger].c > PDA[trigger], "candela de trigger nu închide peste pivotul daily"
assert cds_da[10].c <= PDA[10], "bara crossing-ului nu trebuie să închidă peste pivot"
entry = cds_da[trigger].c
# 3. NU: toate valorile pivot_daily > 62.00 — nimic re-încrucișat
assert all(v > WEEKLY for v in PNU), "un pivot daily NU e sub 62.00"
assert not any(v < WEEKLY for v in PNU), "există pivot daily NU sub 62.00 => ar fi crossing"
assert min(PNU) > WEEKLY and max(PNU) > WEEKLY
# prețul face închideri peste pivotul daily, dar pivotul nu cade sub 62.00
prim_nu = next(i for i in range(len(cds_nu)) if cds_nu[i].c > PNU[i])
assert prim_nu == 5 and cds_nu[prim_nu].c > PNU[prim_nu], "NU nu face close peste pivotul daily"
assert min(PNU) == 62.20 and max(PNU) == 62.30, "pivotii NU nu sunt 62.20 / 62.30"
# 4. liniile sunt „stepped" (au cel puțin un salt), nu drepte
assert len(set(PDA)) >= 2 and len(set(PNU)) >= 2, "liniile de pivot nu sunt stepped"
# --------------------------------------------------- desen: câte un panou
def step_points(ch: Chart, vals: list[float]) -> str:
"""Polilinie în trepte: orizontal pe palier, salt vertical la schimbare."""
pts: list[tuple[float, float]] = []
for i, v in enumerate(vals):
x, y = ch._cx(i), ch._y(v)
if i == 0:
pts.append((x, y))
elif v != vals[i - 1]:
pts.append((x, ch._y(vals[i - 1])))
pts.append((x, y))
else:
pts.append((x, y))
return " ".join(f"{x:.1f},{y:.1f}" for x, y in pts)
def render_pivot_panel(cds, pivots, title) -> tuple[str, float, Chart]:
"""Randază un panou cu pivot weekly (Level) + pivot daily (polilinie injectată)."""
ch = Chart(candles=cds,
levels=[Level(WEEKLY, f"pivot weekly (cel MARE) {WEEKLY:.2f}",
BLUE, dash="", label_at="right")],
marks=[],
scale=SCALE, gap=15, body_w=9, left=44, width=W,
title=title)
svg = ch.render() # fixează _pmax/_y și adaugă +22 pentru titlu
inject = [
f'',
f'pivot daily (cel MIC)',
]
i = svg.index(">") + 1
svg = svg[:i] + "\n " + "\n ".join(inject) + svg[i:]
m = re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', svg)
body = svg[svg.index(">") + 1:svg.rindex("")]
return body, float(m.group(2)), ch
body_da, h_da, ch_da = render_pivot_panel(
cds_da, PDA, "DA — crossing (+2)")
# marcajul crossing-ului: cerc pe saltul pivotului + eticheta din spec
xc = ch_da._cx(cross_da)
yc = (ch_da._y(PDA[cross_da - 1]) + ch_da._y(PDA[cross_da])) / 2
circle = (f'')
cross_txt = (f'crossing: 61.70 → 62.20')
mark_txt = (f''
f'intrare: trigger')
i = body_da.rindex("") + len("") # după candele/marks, ca să fie vizibile
body_da = body_da[:i] + "\n " + circle + "\n " + cross_txt + "\n " + mark_txt + body_da[i:]
# panoul NU: eticheta din spec pe semnalele ulterioare
body_nu, h_nu, ch_nu = render_pivot_panel(
cds_nu, PNU, "NU — continuare/buy the dip (+1)")
body_nu = body_nu.rstrip() + (
f'\n '
f'fără crossing nou; nu a mai căzut sub pivotul mare'
f'\n '
f'semnale ulterioare = BUY THE DIP, nu crossing')
# ------------------------------------------------- lipirea celor două panouri
CAPS = [
"Pivot weekly 62.00 = linia orizontală (pivotul MARE).",
"Pivot daily (cel MIC) se recalculează zilnic și sare pe nivel nou.",
"Trigger = prima candelă verde după crossing,",
"care închide peste pivotul daily.",
"Crossing = pivotul mic traversează 62.00 în sus; fără re-încrucișare,",
"semnalele ulterioare sunt buy the dip, nu crossing.",
]
cap_top = h_da + VGAP + h_nu + 16
H = cap_top + 17 * len(CAPS) + 10
parts = [
f'")
svg = "\n ".join(parts)
assert "\n\n" not in svg, "linie goală în SVG"
# ------------------------------------- verificarea citirii: etichete în panză
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"
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, 0.0, "DA") + _texts(body_nu, h_da + VGAP, "NU")
for i, c in enumerate(CAPS): # și caption-urile, care nu-s în panouri
labels.append((24, 24 + len(c) * 12 * 0.52, cap_top + 17 * i, c, "CAP"))
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}")
dist = abs(ch_da._y(WEEKLY) - ch_da._y(PDA[cross_da - 1])) # 62.00 - 61.70, în px
assert dist > 10, f"distanța 61.70->62.00 = {dist:.1f}px, prea mică"
print(f"DA: crossing la bara {cross_da} (61.70 -> 62.20), trigger bara {trigger} "
f"close {entry:.2f} > pivot {PDA[trigger]:.2f}; bare {len(cds_da)}")
print(f"NU: pivot daily {min(PNU):.2f}..{max(PNU):.2f} (> 62.00), primul close "
f"peste pivot bara {prim_nu} ({cds_nu[prim_nu].c:.2f}); bare {len(cds_nu)}")
print(f"scris {OUT.name} ({W}x{H:.0f}); {len(labels)} etichete verificate; "
f"61.70->62.00 = {dist:.1f}px")
OUT.write_text(svg, encoding="utf-8")