Raportata era una singura. eticheta_libera le-a gasit pe toate noua, inclusiv cinci praguri din panoul TICUS scrise de mana in generator, la trei inaltimi diferite fata de linia lor. Doua lucruri pe care nu le vedeam fara unealta: marcajul candelei M era plafonat de Chart exact pe linia SL-ului, iar o mutare de-a corectorului a creat o regresie noua (text peste text), prinsa de audit_diagrams. Verificarea ruleaza acum in generator fara exceptii, deci regresiile se prind singure data viitoare. Cifrele n-au fost atinse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
227 lines
12 KiB
Python
227 lines
12 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")
|
||
sys.path.insert(0, "/workspace/atm-curs-dl/gen")
|
||
from svg_candles import Chart, Level, Mark, from_closes # noqa: E402
|
||
from eticheta_libera import eticheta_libera # 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
|
||
# `label_dy` ridică eticheta cu 8 px peste linia ei: implicit stă la y − 6, cu
|
||
# coada literelor peste linie. Banda inferioară merge în jos (dy = +22), nu în
|
||
# sus: la 0.05 $ deasupra ei trece linia de intrare, iar la scara asta (150
|
||
# px/$) între cele două linii nu încape un rând de text.
|
||
levels = [
|
||
Level(623.90, "623.90 — bandă inferioară", BLUE, dash="4 3", label_at="left", label_dy=22),
|
||
Level(624.70, "624.70 — bandă superioară", BLUE, dash="4 3", label_at="right", label_dy=-8),
|
||
Level(624.30, "624.30 — SMA20 / TP1", GRAY, dash="4 3", label_at="left", label_dy=-8),
|
||
Level(sl, f"SL {sl:.2f} (lowest low, 4 candele)", RED, label_at="left", label_dy=-8),
|
||
Level(entry, f"trigger Buy Momentum → intrare {entry:.2f}", GREEN, label_at="right",
|
||
label_dy=-8),
|
||
Level(625.35, "ieșire ilustrativă ~625.35 (≈ 1:4)", GREEN, dash="2 3", label_at="right",
|
||
label_dy=-8),
|
||
]
|
||
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.
|
||
# nivel-distanțier fără etichetă: coboară baza graficului cu 15 px, ca marcajul
|
||
# de sub candela M să nu mai fie plafonat exact pe linia SL-ului
|
||
SPACER = [Level(sl - 0.10, "", "none", dash="")]
|
||
ch = Chart(candles=cds, levels=levels + SPACER, marks=marks,
|
||
scale=150.0, gap=16, body_w=10, left=48, width=700, top=96)
|
||
svg = ch.render()
|
||
# eticheta goală a distanțierului nu are ce căuta în SVG: auditul o citește ca
|
||
# text suprapus peste ce e sub ea
|
||
svg = re.sub(r"\n\s*<text[^>]*>\s*</text>", "", svg)
|
||
|
||
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 + 84 # 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) - 9:.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) - 9:.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) - 9:.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) - 9:.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) - 9:.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 + 14:.1f}" y="{bar_bot + 18:.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) + 46
|
||
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
|
||
# nicio etichetă de nivel sau de candelă nu are voie să stea pe desen: aici
|
||
# contează mai ales liniile vecine (banda inferioară și intrarea sunt la 0.05 $
|
||
# una de alta, deci etichetele lor se bat cap în cap cu linia celuilalt)
|
||
eticheta_libera(svg, "ansamblu", {lv.text for lv in levels} | {mk.text for mk in marks},
|
||
rects=True)
|
||
# același tratament pentru etichetele panoului de TICUS, scrise de mână aici
|
||
eticheta_libera(svg, "panoul TICUS",
|
||
{"+1000 — supracumpărare", "+500 (doar referință vizuală)", "0",
|
||
"-500 (doar referință vizuală)", "-1000 — oversold",
|
||
"M: TICUS -1150 (sub -1000)", "TICUS — presiunea pieței (↑ = peste 0)"},
|
||
rects=True)
|
||
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}")
|