"""Diagrama „aceeași candelă" MBP / MBPA++ (marcajul `samecandle`).
Două panouri alături, fiecare cu un grafic de 10 candele de 15 min și un panou
TICUS dedesubt, legate printr-o linie punctată verticală de la candela M la
valoarea ei TICUS. Singura diferență dintre panouri: pe care candelă e TICUS
extrem.
- Panoul DA: M (roșie, index 5) închide 623.87 sub banda inferioară 623.90
ȘI TICUS(M) = -1150 (≤ -1000) — pe EXACT aceeași candelă. M îndeplinit.
- Panoul NU: aceeași structură de preț, dar TICUS(M) = -600 (peste prag),
iar extremul de -1200 e pe candela m-1, a cărei închidere e peste bandă.
M neîndeplinit, deși vizual „pare" același setup.
Regula și cifrele vin din `summaries/05_MODUL_4/03_strategia_6_mbp_mbappe.md`:
- regula (bandă și TICUS pe ACEEAȘI candelă) — liniile 179-221, 439-442;
- verificarea prin click pe candelă, nu din ochi — liniile 217-221, 436-438;
- pragurile ±1000, ±500 doar referință vizuală — liniile 56-64;
- M close 623.87 sub banda 623.90 și TICUS(M) -1150 (exemplul real) —
liniile 336-342.
Cifrele din panoul NU (TICUS(M) = -600, TICUS(m-1) = -1200) sunt construite de
mine ca ilustrare a cazului „aproape, dar nu" (spec, rândurile 27-31).
Structura candelelor e construită de mine, ca fiecare nivel afirmat să iasă
exact din serie (assert-urile de mai jos).
Rulează:
python3 gen/strategii/mbp_samecandle.py
Scrie lângă el `mbp_samecandle.svg`.
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Chart, Level, from_closes # noqa: E402
OUT = Path(__file__).resolve().parent / "mbp_samecandle.svg"
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
# ---------------------------------------------------------------- seria (10 bare)
# 5 bare de coborâre până la M (index 5), apoi 4 bare care își revin. Open-ul
# fiecărei bare = close-ul precedentei. Aceeași serie în ambele panouri.
CLOSES = [625.35, 625.10, 624.90, 624.65, 624.30, # 0-4 downtrend
623.87, # 5 = M, roșie, sub bandă
623.98, 624.12, 624.28, 624.45] # 6-9 revenire
WICKS = [(0.08, 0.08), (0.10, 0.10), (0.08, 0.10), (0.10, 0.10), (0.08, 0.12),
(0.06, 0.10), # M: high 624.36, low 623.77
(0.06, 0.06), (0.08, 0.06), (0.06, 0.06), (0.08, 0.05)]
assert len(CLOSES) == 10 and len(WICKS) == 10, (len(CLOSES), len(WICKS))
BAND = 623.90
M = 5
cds = from_closes(CLOSES, wicks=WICKS, start=625.55)
# ---------------------------------------------------------------- assert-urile din spec
assert abs(cds[M].c - 623.87) < 0.005, cds[M].c # exemplul real din curs
assert cds[M].c < BAND, cds[M].c # M închide sub banda inferioară
assert cds[M].c < cds[M].o, (cds[M].o, cds[M].c) # M e roșie
assert abs(cds[M].l - 623.77) < 0.005, cds[M].l
assert cds[M].l == min(c.l for c in cds) # M are minimul seriei
assert cds[M - 1].c > BAND, cds[M - 1].c # candela m-1 închide PESTE bandă
assert [round(c.c, 2) for c in cds] == CLOSES # close-urile sunt respectate
# TICUS: valoarea pe candelă. DA: doar M, sub prag. NU: extremul pe m-1.
TICUS_DA = {M: -1150.0}
TICUS_NU = {M - 1: -1200.0, M: -600.0}
assert TICUS_DA[M] <= -1000, TICUS_DA[M] # DA: ticus(M) under -1000
assert TICUS_NU[M] > -1000, TICUS_NU[M] # NU: ticus(M) peste prag
assert TICUS_NU[M - 1] <= -1000, TICUS_NU[M - 1] # NU: extremul e pe m-1
assert not any(i == M and v <= -1000 for i, v in TICUS_NU.items()), \
"NU: nu are voie să existe bar TICUS sub -1000 aliniat cu candela M"
# ---------------------------------------------------------------- construcția panourilor
SCALE, GAP, BODY, LEFT, W = 85.0, 16, 10, 44, 380
def _ref_line(Wv, y, color, dash, text, lab_dy):
dash_attr = f' stroke-dasharray="{dash}"' if dash else ""
return (f''
f'{text}')
def build_panel(ticus, labels):
"""Un panou = grafic de preț (svg_candles) + panou TICUS dedesubt."""
ch = Chart(candles=cds,
levels=[Level(BAND, "bandă 623.90", BLUE, dash="4 3", label_at="right")],
scale=SCALE, gap=GAP, body_w=BODY, left=LEFT, width=W)
raw = ch.render()
mm = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', raw)
Wv, H0 = int(mm.group(1)), float(mm.group(2))
inner = raw[raw.index(">") + 1: raw.rindex("")].strip("\n")
ticus_top = H0 + 30
y0 = H0 + 100 # linia 0 a TICUS
PX = 44.0 # 1000 unități TICUS = 44 px
bar_label_x = ch._cx(M) + 16 # etichetele barelor stau în coloană, la dreapta
def ty(v):
return y0 - v / 1000.0 * PX
total_h = y0 + 96
parts = []
# linia punctată verticală: aceeași candelă sus și jos (desenată prima, ca
# etichetele să vină peste ea)
for i, v in sorted(ticus.items()):
xb = ch._cx(i)
assert -1 <= xb <= Wv + 1, xb
parts.append(f'')
parts.append(f'TICUS')
# praguri: 0 (linie plină), ±1000 (punctate), ±500 (fine, doar referință)
parts.append(_ref_line(Wv, ty(1000), RED, "4 3", "+1000 (prag)", -5))
parts.append(_ref_line(Wv, ty(500), "#caa000", "2 2", "+500", -5))
parts.append(_ref_line(Wv, ty(0), "#888", "", "0", 4))
parts.append(_ref_line(Wv, ty(-500), "#caa000", "2 2", "-500", 13))
parts.append(_ref_line(Wv, ty(-1000), GREEN, "4 3", "-1000 (prag)", 13))
# barele TICUS (centrate pe x-ul candelei)
for i, v in sorted(ticus.items()):
xb = ch._cx(i)
yv = ty(v)
col = RED if v <= -1000 else ORANGE
parts.append(f'')
# eticheta de valoare lângă fiecare bar (aceeași coloană x, rânduri diferite)
for i, (txt, dy) in sorted(labels.items()):
col = RED if ticus[i] <= -1000 else ORANGE
parts.append(f'{txt}')
# adnotarea candelei M, în zona de preț, sub bară (zona e liberă)
parts.append(f''
f'M: close 623.87 < bandă 623.90')
# alinierea TICUS(M) cu candela M: barul se centrează exact pe x-ul candelei
xm = ch._cx(M)
x_bar_m = ch._cx(M) # centrul barei TICUS a lui M
assert abs(x_bar_m - xm) < 1e-9
assert ticus[M] <= -1000 if ticus is TICUS_DA else ticus[M] > -1000
body = inner + "\n " + "\n ".join(parts)
# bara TICUS a lui M chiar a fost scrisă cu centrul pe x-ul candelei M
assert f' aceleași x-uri ale candelelor în ambele panouri
assert all(abs(ch_da._cx(i) - ch_nu._cx(i)) < 1e-9 for i in range(len(cds)))
# ---------------------------------------------------------------- compunerea SVG-ului
TOP = 48
FOOTER = [
"Regula: banda (close sub 623.90) și TICUS ≤ -1000 trebuie pe ACEEAȘI candelă.",
"Pragurile TICUS sunt ±1000 (overbought / oversold); ±500 sunt doar referință vizuală.",
"Verificarea se face prin click pe candelă, nu din ochi — TICUS(M) se citește exact pe candela M.",
"DA: TICUS(M) = -1150 (exemplul real din curs).",
"NU: TICUS(M) = -600, TICUS(m-1) = -1200 — cifre construite de mine ca ilustrare.",
"Structura candelelor (10 bare de 15 min, aceeași în ambele panouri) e construită de mine.",
]
root_h = TOP + PH + 22 + 17 * len(FOOTER) + 8
out = [f'")
svg = "\n ".join(out)
# ---------------------------------------------------------------- verificări pe SVG-ul final
assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
assert "DA — aceeași candelă, M îndeplinit" in svg
assert "NU — TICUS extrem pe altă candelă," in svg
assert "M neîndeplinit" in svg
assert "click pe candelă, nu din ochi" in svg
assert svg.count("") == 3, "cele două panouri + rădăcina"
OUT.write_text(svg)
print(f"scris {OUT.name}: {len(cds)} candele/panou, M close {cds[M].c:.2f} "
f"(< {BAND}), TICUS DA {TICUS_DA[M]:.0f} / NU {TICUS_NU[M]:.0f} "
f"(extrem {TICUS_NU[M - 1]:.0f} pe m-1), x(M)={ch_da._cx(M):.0f}, "
f"înălțime {root_h:.0f}px")