"""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'', f'{inner_da}', f'{inner_nu}', f'DA — aceeași candelă, M îndeplinit', f'NU — TICUS extrem pe altă candelă,', f'M neîndeplinit'] fy = TOP + PH + 22 for i, line in enumerate(FOOTER): out.append(f'{line}') out.append("") 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")