m2d_exemplu (Riot): eticheta zicea 'close peste bear trail = iesi' pe o bara
care se inchidea SUB linie - doar fitilul trecea, adica exact cazul pe care
capitolul il interzice de trei ori. Seria refacuta, cu assert care recalculeaza
trail-ul din serie si cere ca bara de iesire sa inchida peste el.
mbp_exemplu: candela M se inchidea la 660.50 sub banda de 660.90, iar
generatorul chiar afirma relatia inversa ('assert BAND > c'); desenul arata un
setup invalid ca valid. Plus stopul 662.71, care plutea 1.71 deasupra extremei
celor 4 bare - exact marja pe care capitolul o declara absenta la MBP.
Ambele reparate, cu assert pe fiecare.
atmi_orizont: trei etichete purtau 'fisier:linie' in desen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
251 lines
12 KiB
Python
251 lines
12 KiB
Python
"""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 662.71, candela M (verde, close 660.50 peste
|
|
banda superioară + TICUS ≥ +1000), trigger Sell Momentum → intrare short
|
|
660.00, SL 662.71 = chiar maximul ultimelor 4 candele (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.20 # bandă superioară schematică (sub 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 += [(SL - closes[M_I], 0.20)] # M: high = 662.71 = SL (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 cds[M_I].c > BAND, "candela M nu se închide peste banda superioară"
|
|
|
|
# stopul stă FIX pe cel mai înalt high al ultimelor 4 candele (trigger inclus)
|
|
FEREASTRA = cds[E_I - 3:E_I + 1]
|
|
assert len(FEREASTRA) == 4 and FEREASTRA[-1] is cds[E_I], "fereastra de stop nu are 4 bare"
|
|
assert abs(max(c.h for c in FEREASTRA) - sl) < 1e-9, \
|
|
f"stopul {sl:.2f} nu e fix pe maximul celor 4 bare " \
|
|
f"({max(c.h for c in FEREASTRA):.2f})"
|
|
|
|
# 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="right"),
|
|
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: close peste bandă", "#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",
|
|
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("</svg>")].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'<text x="24" y="{Y0 + 16:.0f}" fill="#333" style="font-weight:bold">'
|
|
f'Panou TICUS — linia 0 și pragul +1000 (ilustrativ)</text>',
|
|
f'<line x1="24" y1="{plus_y:.1f}" x2="{W - 14}" y2="{plus_y:.1f}" '
|
|
f'stroke="#b06000" stroke-width="1.2" stroke-dasharray="2 4"/>',
|
|
f'<text x="{W - 28}" y="{plus_y - 5:.1f}" fill="#b06000" text-anchor="end">'
|
|
f'+1000 (prag)</text>',
|
|
f'<line x1="24" y1="{zero_y:.1f}" x2="{W - 14}" y2="{zero_y:.1f}" stroke="#888"/>',
|
|
f'<text x="24" y="{zero_y - 5:.1f}" fill="#555">0</text>',
|
|
f'<rect x="{m_x - BODY_W / 2:.1f}" y="{bar_top:.1f}" width="{BODY_W}" '
|
|
f'height="{zero_y - bar_top:.1f}" fill="#188a3e" opacity="0.85"/>',
|
|
f'<text x="{m_x:.1f}" y="{bar_top - 5:.1f}" fill="#777" text-anchor="middle">'
|
|
f'ilustrativ</text>',
|
|
f'<text x="{m_x:.1f}" y="{zero_y + 16:.1f}" fill="#777" text-anchor="middle">'
|
|
f'candela M</text>',
|
|
]
|
|
|
|
# ── legenda (cifrele/sursele) ───────────────────────────────────────────────
|
|
# Fără trimiteri „fișier:linie" în etichetele SVG: sursele trăiesc doar în
|
|
# fisa capitolului (STYLE.md, secțiunea Referințe).
|
|
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.",
|
|
"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.",
|
|
]
|
|
cap_top = Y0 + TICUS_H + 20
|
|
H = cap_top + 16 * (len(legend) - 1) + 14
|
|
|
|
parts = [
|
|
f'<svg viewBox="0 0 {W} {H:.0f}" xmlns="http://www.w3.org/2000/svg" '
|
|
f'style="width:100%;height:auto;font:12px sans-serif">',
|
|
'<g transform="translate(0,0)">',
|
|
body1,
|
|
"</g>",
|
|
*ticus,
|
|
*[f'<text x="24" y="{cap_top + 16 * i:.1f}" fill="#555">{t}</text>'
|
|
for i, t in enumerate(legend)],
|
|
"</svg>",
|
|
]
|
|
svg = "\n ".join(parts)
|
|
assert "\n\n" not in svg, "linie goală în SVG — rupe markdown-ul"
|
|
assert svg.count("<svg") == 1 and svg.count("</svg>") == 1, "SVG-ul trebuie un singur root"
|
|
|
|
# ── verificare de citire: etichete în panză, fără suprapuneri ───────────────
|
|
TEXT = re.compile(r'<text x="([-\d.]+)" y="([-\d.]+)"([^>]*)>(.*?)</text>', 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")
|