Files
atm-curs-dl/gen/strategii/mbp_ansamblu.py
Claude Agent 01599d2e9d strategii: primele 30 de diagrame, cu generatoarele lor
Cate un agent grafician per diagrama, fiecare cu generatorul si SVG-ul lui.
Graficele de pret trec prin svg_candles, cu assert-urile din specificatie
scrise in generator — verifica structura, nu doar ca ruleaza: ca al doilea
picior e mai jos, ca intrarea e close-ul barei de confirmare, ca 1:1 e exact.

Verificare prin sondaj pe buy_doji_ansamblu, cu screenshot citit: serie
continua de 32 de candele, panou de volum, MA(20), doji-ul si ieșirea pe
trail etichetate, cifrele identice cu specificatia.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-18 19:22:13 +00:00

202 lines
10 KiB
Python

"""Diagrama de ansamblu MBP / MBPA++ (strategii, marcajul `ansamblu`).
„SPY — 15 min: bandă + TICUS pe aceeași candelă → trigger → intrare → SL →
TP1 → trail". Graficul întreg al trade-ului long din exemplul cursului, cu
panoul TICUS dedesubt, legat de candela M printr-o linie punctată verticală
(aceeași candelă sus, aceeași valoare jos).
Toate cifrele desenate vin din curs
(`summaries/05_MODUL_4/03_strategia_6_mbp_mbappe.md`):
- M: close 623.87 < banda inferioară 623.90, TICUS -1150 (liniile 336-342);
- bandă superioară 624.70 și linia 624.30 (SMA20 / TP1) — diagrama
principală (liniile 114-148);
- trigger Buy Momentum → entry 623.95 (liniile 336-342);
- SL 623.60 = lowest low din ultimele 4 candele, trigger inclus (93-94);
- TP1 624.30 (+0.35, 1:1) (95-96);
- „realizat ~1:4 datorită pozițiilor pe trail" (428-429). Prețul de ieșire
~625.35 = entry + 4R este ilustrativ, derivat de mine (nu e preț din curs).
Structura barelor dintre niveluri e construită de mine; o construiesc astfel
încât fiecare nivel afirmat în text să iasă exact din serie (assert-uri mai
jos). Nu există nivel de suport major cu cifră în exemplul din curs.
Rulează:
python3 gen/strategii/mbp_ansamblu.py
Scrie lângă el `mbp_ansamblu.svg`.
"""
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__).resolve().parent / "mbp_ansamblu.svg"
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
# ---------------------------------------------------------------- seria (30 bare)
# 14 bare de downtrend 625.60 -> 624.15 (open-ul fiecărei bare = close-ul
# precedentei), apoi M (index 14), trigger (15), 14 bare de urcare spre 625.35.
DOWN = [625.60, 625.42, 625.55, 625.28, 625.05, 625.12, 624.88,
624.65, 624.72, 624.50, 624.35, 624.30, 624.10, 624.15] # 0-13
M_IX, TR_IX = 14, 15
UP = [624.00, 624.08, 624.16, 624.30, 624.40, 624.50, 624.60, 624.70,
624.82, 624.95, 625.08, 625.20, 625.28, 625.35] # 16-29
CLOSES = DOWN + [623.87, 623.95] + UP # M, trigger
assert len(CLOSES) == 30, len(CLOSES)
# fitile (sus, jos) per bară. Ultimele 3 bare înainte de M au minimele
# 623.95 / 623.80 / 623.72 (peste 623.60), ca SL-ul calculat să fie exact
# 623.60 (low-ul lui M).
WICKS = ([(0.05, 0.18)] + [(0.18, 0.18)] * 10
+ [(0.18, 0.35), # 11: low 623.95
(0.18, 0.30), # 12: low 623.80 (open 624.30, close 624.10)
(0.10, 0.38)] # 13: low 623.72 (open 624.10, close 624.15)
+ [(0.05, 0.27), # 14 M: high 624.20, low 623.60
(0.05, 0.12)] # 15 trigger: high 624.00, low 623.75
+ [(0.05, 0.12)] * 3 # 16-18
+ [(0.00, 0.12)] # 19: high atinge exact 624.30 (TP1)
+ [(0.05, 0.12)] * 9
+ [(0.00, 0.12)]) # 29: high = close = 625.35 (ieșirea ilustrativă)
assert len(WICKS) == len(CLOSES), len(WICKS)
# TICUS: un singur bar relevant, pe candela M, la -1150 (sub -1000)
TICUS = {M_IX: -1150.0}
# fără panou de volum: panoul de dedesubt e cel de TICUS
cds = from_closes(CLOSES, wicks=WICKS, start=625.75)
# ---------------------------------------------------------------- assert-urile din spec
entry = cds[TR_IX].c
assert abs(entry - 623.95) < 0.01, entry
assert entry == cds[TR_IX].c # intrarea = close-ul trigger-ului
assert cds[M_IX].c < 623.90 # M închide sub banda inferioară
assert cds[M_IX].c < cds[M_IX].o # M e roșie (close < open)
ticus_m = TICUS[M_IX]
assert ticus_m <= -1000, ticus_m # =-1150, pragul depășit
assert abs(cds[M_IX].c - 623.87) < 0.005
assert abs(cds[M_IX].h - 624.20) < 0.005
assert abs(cds[TR_IX].h - 624.00) < 0.005
assert abs(cds[TR_IX].l - 623.75) < 0.005
# SL = cel mai jos minim din ultimele 4 candele încheiate de trigger, inclusiv
window = cds[TR_IX - 3:TR_IX + 1]
assert len(window) == 4
sl = min(c.l for c in window)
assert abs(sl - 623.60) < 0.01, sl
assert sl == cds[M_IX].l # low-ul lui M e minimul ferestrei
assert cds[TR_IX].l > sl # trigger-ul nu face low nou
# cele trei candele dinaintea lui M au minime peste 623.60 (ex. 623.95/623.80/623.72)
for cd in cds[TR_IX - 4:TR_IX - 1]:
assert cd.l > sl, (cd.o, cd.c, cd.l)
risc = entry - sl
assert abs(risc - 0.35) < 0.005, risc
tp1 = entry + risc
assert abs(tp1 - (entry + (entry - sl))) < 0.005
assert abs(tp1 - 624.30) < 0.005, tp1
# TP1 nu se atinge prea devreme, ci 4 bare după intrare (cum spune spec-ul)
assert max(c.h for c in cds[TR_IX + 1:TR_IX + 4]) < tp1 - 1e-9
assert abs(cds[TR_IX + 4].h - tp1) < 1e-9
exit_ill = cds[-1].c # ~625.35, ilustrativ (derivat)
assert abs(exit_ill - 625.35) < 0.01, exit_ill
assert abs((exit_ill - entry) / risc - 4.0) < 0.15, (exit_ill - entry) / risc
# nu există nicio linie de stop desenată deasupra intrării
assert sl < entry
levels = [
Level(623.90, "623.90 — bandă inferioară", BLUE, dash="4 3", label_at="left"),
Level(624.70, "624.70 — bandă superioară", BLUE, dash="4 3", label_at="right"),
Level(624.30, "624.30 — SMA20 / TP1", GRAY, dash="4 3", label_at="left"),
Level(sl, f"SL {sl:.2f} (lowest low, 4 candele)", RED, label_at="left"),
Level(entry, f"trigger Buy Momentum → intrare {entry:.2f}", GREEN, label_at="right"),
Level(625.35, "ieșire ilustrativă ~625.35 (≈ 1:4)", GREEN, dash="2 3", label_at="right"),
]
assert all("stop" not in lv.text.lower() or lv.price < entry for lv in levels)
assert all(not ("sl" in lv.text.lower() and lv.price > entry) for lv in levels)
marks = [
Mark(M_IX, "M: roșie, close 623.87 sub bandă", RED, "below"),
]
# titlul si eticheta de trend sunt injectate manual (nu prin Chart.title, care
# ar sta lipit de prima candelă, si nici prin Chart.trend, care s-ar bate cu
# titlul) ca sa rămână spațiu liber deasupra seriei.
ch = Chart(candles=cds, levels=levels, marks=marks,
scale=150.0, gap=16, body_w=10, left=48, width=700, top=96)
svg = ch.render()
TITLE = "SPY — 15 min: bandă + TICUS pe aceeași candelă → trigger → intrare → SL → TP1 → trail"
# ---------------------------------------------------------------- panoul TICUS
# `Chart` nu are panou de indicator; îl adaug după randare, cu aceeași scară
# pe orizontală (ch._cx), ca barul TICUS al lui M să cadă exact sub candela M.
m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg)
W, H0 = int(m.group(1)), float(m.group(2))
ticus_top = H0 + 34
y0 = ticus_top + 70 # linia 0
px1000 = 50.0 # 1000 unități TICUS = 50 px
def ty(v: float) -> float:
return y0 - v / 1000.0 * px1000
xm = ch._cx(M_IX)
bar_top, bar_bot = y0, ty(ticus_m) # bar de la 0 în jos la -1150
panel = [
f'<text x="24" y="{ticus_top:.0f}" fill="#333">TICUS — presiunea pieței (↑ = peste 0)</text>',
# linii de prag
f'<line x1="24" y1="{ty(1000):.1f}" x2="{W - 14}" y2="{ty(1000):.1f}" stroke="{RED}" stroke-width="1.5" stroke-dasharray="4 3"/>',
f'<text x="28" y="{ty(1000) - 4:.1f}" fill="{RED}">+1000 — supracumpărare</text>',
f'<line x1="24" y1="{ty(500):.1f}" x2="{W - 14}" y2="{ty(500):.1f}" stroke="#caa000" stroke-width="1" stroke-dasharray="2 2"/>',
f'<text x="28" y="{ty(500) - 4:.1f}" fill="#caa000">+500 (doar referință vizuală)</text>',
f'<line x1="24" y1="{ty(0):.1f}" x2="{W - 14}" y2="{ty(0):.1f}" stroke="#888" stroke-width="1.5"/>',
f'<text x="{W - 12}" y="{ty(0) + 4:.1f}" fill="#666" text-anchor="end">0</text>',
f'<line x1="24" y1="{ty(-500):.1f}" x2="{W - 14}" y2="{ty(-500):.1f}" stroke="#caa000" stroke-width="1" stroke-dasharray="2 2"/>',
f'<text x="28" y="{ty(-500) + 13:.1f}" fill="#caa000">-500 (doar referință vizuală)</text>',
f'<line x1="24" y1="{ty(-1000):.1f}" x2="{W - 14}" y2="{ty(-1000):.1f}" stroke="{GREEN}" stroke-width="1.5" stroke-dasharray="4 3"/>',
f'<text x="28" y="{ty(-1000) + 13:.1f}" fill="{GREEN}">-1000 — oversold</text>',
# barul TICUS al candelei M, sub -1000 (colorat roșu)
f'<rect x="{xm - 5:.1f}" y="{bar_top:.1f}" width="10" height="{bar_bot - bar_top:.1f}" fill="{RED}"/>',
f'<text x="{xm + 10:.1f}" y="{bar_bot + 4:.1f}" fill="{RED}">M: TICUS -1150 (sub -1000)</text>',
]
# linia punctată verticală: aceeași candelă sus și jos (pornește sub mark-ul
# lui M, ca să nu taie textul „M: roșie...")
y_candle_m = ch._y(cds[M_IX].l) + 26
panel.insert(0, f'<line x1="{xm:.1f}" y1="{y_candle_m:.1f}" x2="{xm:.1f}" y2="{bar_bot:.1f}" '
f'stroke="#aaa" stroke-width="1" stroke-dasharray="3 3"/>')
legend_top = ty(-1000) + 44
legend = [
"SPY 15 min. Cifre din curs: M close 623.87 (sub bandă 623.90), TICUS -1150 pe aceeași candelă,",
"trigger Buy Momentum → entry 623.95, SL 623.60 (lowest low din 4 candele, trigger inclus),",
"TP1 624.30 (+0.35, 1:1). Ieșirea ~625.35 = entry + 4R (≈1:4) e ilustrativă — preț derivat din R,",
"nu din curs. Structura candelelor (30 bare continue) e construită de mine.",
]
legend_svg = [f'<text x="24" y="{legend_top + 17 * i:.1f}" fill="#555">{t}</text>'
for i, t in enumerate(legend)]
header = [
f'<text x="24" y="26" fill="#333" style="font-weight:bold">{TITLE}</text>',
'<text x="24" y="64" fill="#999">trend descendent anterior (14 bare)</text>',
]
new_h = legend_top + 17 * (len(legend) - 1) + 14
svg = re.sub(r'(viewBox="0 0 \d+ )[0-9.]+(")', lambda mm: mm.group(1) + f"{new_h:.0f}" + mm.group(2), svg, count=1)
svg = svg.replace("\n</svg>", "\n " + "\n ".join(header + panel + legend_svg) + "\n</svg>")
assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
# barul TICUS cade exact sub candela M (aceeași candelă sus și jos)
bar_svg = f'<rect x="{xm - 5:.1f}" y="{bar_top:.1f}" width="10" height="{bar_bot - bar_top:.1f}" fill="{RED}"/>'
assert bar_svg in svg, "barul TICUS nu a fost desenat"
assert abs((float(re.search(r'x="([\d.]+)"', bar_svg).group(1)) + 5) - ch._cx(M_IX)) < 1e-9
# aceeași scară px/unitate: SL-intrare și intrare-TP1 sunt egale și vizual (1:1)
assert abs((ch._y(sl) - ch._y(entry)) - (ch._y(entry) - ch._y(tp1))) < 1e-9
assert f'x1="{xm:.1f}"' in svg
OUT.write_text(svg)
print(f"scris {OUT.name}: {len(cds)} candele, entry {entry:.2f}, SL {sl:.2f}, "
f"TP1 {tp1:.2f}, ieșire ilustrativă {exit_ill:.2f} ({(exit_ill-entry)/risc:.2f}R), "
f"ticus(M)={ticus_m:.0f}, x(M)={xm:.0f}")