"""Diagrama din `gen/strategii/mbp_exemplu.spec.txt` (TIP: PRET). Trade-ul short complet din material (SPY, 15 min, 25.11.2025): ~12 candele de urcare violentă spre vârful ~661, candela M (verde, peste bandă + TICUS ≥ +1000), trigger Sell Momentum → intrare short 660.00, SL 662.71 (risc 0.41 %), TP1 657.29 (1:1) atins în prima candelă, apoi trail pe 7 candele de 15 min până la ieșirea la 646.80 = 2 % = ~4.9R. Serie continuă de 32 de candele (`from_closes`, open = close anterior), cu context înainte de urcare, panou TICUS dedesubt (bar ilustrativ la candela M) și legendă. Toate cifrele din desen vin din specificație; singurele valori construite de mine sunt structura barelor dintre niveluri și bara TICUS (marcată explicit „ilustrativ", pentru că valoarea exactă nu e în material). Rulare: python3 gen/strategii/mbp_exemplu.py """ import re import sys from pathlib import Path sys.path.insert(0, "/workspace/atm-curs-dl") from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402 OUT = Path(__file__).with_suffix(".svg") # ── cifrele din material ──────────────────────────────────────────────────── ENTRY = 660.00 # 09_QA/11_2025-11-25.md:565, 632-636 SL = 662.71 # risc 0.41 % → (sl-entry)/entry*100 TP1 = 657.29 # entry - (sl - entry) = 1:1 EXIT = 646.80 # ieșire pe trail = 2 % = ~4.9R BAND = 660.90 # bandă superioară schematică (peste close-ul lui M) W, GAP, BODY_W, LEFT = 660, 15, 9, 40 SCALE = 20.0 M_I, E_I, TP1_I, EXIT_I = 23, 24, 25, 31 # M, trigger, TP1, ieșire N_RISE = M_I - 11 # 12 candele de urcare (idx 12..23) # ── seria: context → urcare violentă → M → trigger → TP1 → trail ──────────── closes = [ # idx 0-11: context în jurul lui 648, înainte de urcarea violentă 649.00, 648.60, 649.20, 648.40, 648.90, 648.10, 648.70, 647.90, 648.50, 648.00, 648.60, 648.30, # idx 12-23: 12 candele de urcare de la ~648 la vârful ~661; idx 23 = M 648.60, 649.40, 650.30, 651.20, 652.10, 653.10, 654.10, 655.20, 656.30, 657.50, 658.80, 660.50, # idx 24: trigger Sell Momentum → close = intrarea short 660.00, # idx 25: prima candelă după intrare atinge TP1 657.29 657.80, # idx 26-31: trail până la ieșirea la 646.80 (6 candele) 656.40, 655.00, 653.40, 651.60, 649.40, 646.80, ] assert len(closes) == 32, len(closes) wicks = [(0.35, 0.35)] * 12 # context wicks += [(0.30, 0.25)] * (M_I - 12) # urcarea (idx 12..22) wicks += [(0.50, 0.20)] # M: high = 661.00 (vârful) wicks += [(0.20, 0.40), # trigger (0.25, 0.51), # TP1: low = 657.80-0.51 = 657.29 (0.30, 0.30), (0.30, 0.30), (0.30, 0.30), (0.30, 0.30), (0.30, 0.30), (0.30, 0.30)] # trail assert len(wicks) == len(closes) cds = from_closes(closes, wicks=wicks, start=648.90) # ── assert-urile din specificație ─────────────────────────────────────────── entry = cds[E_I].c sl = SL tp1 = TP1 exit_ = cds[EXIT_I].c assert abs(entry - 660.00) < 0.01, entry assert entry == cds[E_I].c, "intrarea nu e close-ul candelei trigger" assert abs((sl - entry) / entry * 100 - 0.41) < 0.01, (sl - entry) / entry * 100 assert abs((sl - entry) - 2.71) < 0.01, sl - entry assert abs(tp1 - (entry - (sl - entry))) < 0.01, (entry - (sl - entry), tp1) assert cds[TP1_I].l <= tp1 + 1e-9, "prima candelă după intrare nu atinge TP1" assert abs(cds[TP1_I].l - tp1) < 0.01, cds[TP1_I].l assert abs((entry - exit_) / entry * 100 - 2.0) < 0.05, (entry - exit_) / entry * 100 assert abs((entry - exit_) / (sl - entry) - 4.9) < 0.1, (entry - exit_) / (sl - entry) assert EXIT_I - E_I == 7, EXIT_I - E_I assert sl > entry > tp1, "short: SL deasupra intrării, TP1 sub intrare" assert BAND > cds[M_I].c, "banda schematică nu e deasupra close-ului lui M" # structura care susține afirmațiile din text assert cds[M_I].c > cds[M_I].o, "candela M trebuie să fie verde" assert cds[E_I].c < cds[E_I].o, "candela trigger trebuie să fie roșie" assert M_I == E_I - 1, "trigger-ul nu e imediat după M" assert N_RISE == 12, N_RISE assert 30 <= len(cds) <= 34, len(cds) assert max(cds[i].c for i in range(12, M_I + 1)) == cds[M_I].c, "M nu e vârful urcării" assert max(c.h for c in cds) == cds[M_I].h, "vârful seriei nu e la M" assert all(cds[i].c > cds[i - 1].c for i in range(13, M_I + 1)), "urcarea nu e monotonă" # ── panoul de preț ────────────────────────────────────────────────────────── levels = [ Level(SL, "SL 662.71 (risc 0.41 %)", "#c00", label_at="right"), Level(ENTRY, "intrare short 660.00", "#188a3e", label_at="left"), Level(BAND, "bandă superioară (schematic)", "#777", dash="2 4", label_at="left"), Level(TP1, "TP1 657.29 (1:1)", "#1a5fb4", label_at="left"), Level(EXIT, "ieșire trail 646.80 = 2 % = ~4.9R", "#1a5fb4", label_at="left"), ] marks = [ Mark(M_I, "M: candelă verde peste bandă + TICUS ≥ +1000", "#188a3e", "above"), ] cds[M_I].note = ("M: valori exacte NEACOPERIT în material (bandă + TICUS); " "bara ~+1000 din panou e ilustrativă") cds[E_I].note = "trigger Sell Momentum → intrare short 660.00 (risc 0.41 % = 2.71)" cds[TP1_I].note = "TP1 657.29 (1:1) atins în prima candelă după intrare" cds[EXIT_I].note = ("ieșire pe trail 646.80 = 2 % = ~4.9R, după 7 candele " "de 15 min de la intrare") ch = Chart( candles=cds, levels=levels, marks=marks, mas=[MA(20, "#b06000", dash="4 3", text="SMA-20")], scale=SCALE, width=W, gap=GAP, body_w=BODY_W, left=LEFT, title="SPY — 15 min: MBAP short 25.11.2025 (risc 0.41 % → ieșire 4.9R)", caption="", ) price_svg = ch.render() # corpul graficului de preț și înălțimea lui (pentru lipirea panoului TICUS) m = re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', price_svg) H1 = float(m.group(2)) body1 = price_svg[price_svg.index(">") + 1:price_svg.rindex("")].strip("\n") # ── panoul TICUS dedesubt (bar ilustrativ la candela M) ───────────────────── VGAP = 26 Y0 = H1 + VGAP TICUS_H = 100 zero_y = Y0 + 84 # linia 0 plus_y = Y0 + 32 # linia +1000 (punctată) bar_top = plus_y - 18 # bara depășește +1000 m_x = LEFT + M_I * GAP assert bar_top < plus_y, "bara TICUS nu depășește linia +1000" assert plus_y < zero_y, "linia +1000 nu e deasupra liniei 0" ticus = [ f'' f'Panou TICUS — linia 0 și pragul +1000 (ilustrativ)', f'', f'' f'+1000 (prag)', f'', f'0', f'', f'' f'ilustrativ', f'' f'candela M', ] # ── legenda (cifrele/sursele) ─────────────────────────────────────────────── legend = [ "Legendă: procentele (risc 0.41 %, ieșire 2 %) sunt ale instructorului.", "Prețurile absolute (660.00 / 662.71 / 657.29 / 646.80) sunt construite de sumarizator —", "09_QA/11_2025-11-25.md:716-719.", "Structura de reținut: risc mic → TP1 (1:1) atins imediat → trail lung (7 candele de 15 min).", "Valoarea exactă a benzii și a TICUS la candela M nu e dată în sesiune; bara ~+1000 e ilustrativă.", "„Taurii urcă pe scări, urșii sar de la etaj\" — de ce short-ul se consumă în ore.", "(09_QA/11_2025-11-25.md:643-648).", ] cap_top = Y0 + TICUS_H + 20 H = cap_top + 16 * (len(legend) - 1) + 14 parts = [ f'', '', body1, "", *ticus, *[f'{t}' for i, t in enumerate(legend)], "", ] svg = "\n ".join(parts) assert "\n\n" not in svg, "linie goală în SVG — rupe markdown-ul" assert svg.count("") == 1, "SVG-ul trebuie un singur root" # ── verificare de citire: etichete în panză, fără suprapuneri ─────────────── TEXT = re.compile(r']*)>(.*?)', re.S) def _fonts(): """Lățimea reală a textului (DejaVuSans ~ system-ui); fallback estimativ.""" try: from PIL import ImageFont base = "/usr/share/fonts/truetype/dejavu/" return (ImageFont.truetype(base + "DejaVuSans.ttf", 12), ImageFont.truetype(base + "DejaVuSans-Bold.ttf", 12)) except Exception: return None, None _F_NORM, _F_BOLD = _fonts() def _tw(s: str, bold: bool) -> float: if _F_BOLD is not None: return (_F_BOLD if bold else _F_NORM).getlength(s) return len(s) * 12 * (0.62 if bold else 0.52) labels = [] for mm in TEXT.finditer(svg): x, y = float(mm.group(1)), float(mm.group(2)) attrs, s = mm.group(3), mm.group(4) anchor = "start" if 'text-anchor="end"' in attrs: anchor = "end" elif 'text-anchor="middle"' in attrs: anchor = "middle" w = _tw(s, "bold" in attrs) left_ = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x) labels.append((left_, left_ + w, y, s)) for left_, right_, y, s in labels: assert left_ >= -2 and right_ <= W + 2, f"text iese din panză: {s!r}" assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y:.0f})" for i in range(len(labels)): li, ri, yi, si = labels[i] for j in range(i + 1, len(labels)): lj, rj, yj, sj = labels[j] if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4: raise AssertionError(f"texte suprapuse: {si!r} / {sj!r} (y={yi:.0f}/{yj:.0f})") OUT.write_text(svg, encoding="utf-8") print(f"serie {len(cds)} candele | M@{M_I} {cds[M_I].c:.2f} (h {cds[M_I].h:.2f}) | " f"trigger@{E_I} {entry:.2f} | SL {sl:.2f} ({(sl-entry)/entry*100:.2f} %) | " f"TP1 {tp1:.2f} (low {cds[TP1_I].l:.2f}) | ieșire@{EXIT_I} {exit_:.2f} " f"({(entry-exit_)/entry*100:.2f} %, {(entry-exit_)/(sl-entry):.2f}R)") print(f"scris {OUT} ({W}x{H:.0f}); {len(labels)} etichete verificate, fără suprapuneri")