Files
atm-curs-dl/gen/strategii/mp2_serie.py
Claude Agent 2ea599f265 02_mp2: capitol reconstruit dupa sablon
Nivelurile 1-2-3, cei 6 pasi cu diagrama fiecare, mini-glosar de 34 de
termeni, oglinda pe short cu diagrama ei proprie, checklist. Sursele in
fise/02_mp2_surse.md.

Toate diagramele procedurii ies acum dintr-o singura serie
(mp2_serie.py), deci capitolul nu mai poate avea doua reguli numerice.
Ansamblul folosea NVDA cu stop la swing low absolut (-23 %), adica alta
regula decat procedura celor 4 bare; NVDA a devenit "bottom", la nivelul
3, marcat explicit ca exceptie.

perechi_tf e diagrama cu doua timeframe-uri: panoul mic e descompunerea
reala a ultimelor 2 candele daily (1 zi = 7 bare de 60 min), cu pivotul
weekly pe ambele panouri si conector. Pivotii se calculeaza din serie, nu
sunt scrisi de mana.

Reparate: trail-ul din proza era 600.05, calculul din serie da 600.75;
doua SVG-uri aveau trimiteri fisier:linie desenate pe ele.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-20 12:51:58 +00:00

269 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Seria comună a capitolului `17_STRATEGII/02_mp2.md` — un singur trade.
Toate diagramele din Nivelul 1 și Nivelul 2 ale capitolului desenează ACELAȘI
trade, din aceeași serie. Așa nu se poate întâmpla ca două diagrame să
folosească două reguli de stop diferite, fiecare etichetată corect local.
Ce e din material și ce e construit
-----------------------------------
Din material (rezumatul Q&A, exemplul ilustrativ al mecanicii celor două
poziții): intrare 592.00, SL 586.00, 1R = 6.00, TP1 598.00, mutarea SL-ului
la break-even la 60 % din drum, mărimea totală multiplu de 2.
Construit de mine: traseul barelor de 60 de minute care produce exact
cifrele alea, și pivoții — care NU sunt scriși de mână, ci calculați din
serie cu formula cursului, PP = (High + Low + Close) / 3 al perioadei
încheiate. Fără ticker: e un exemplu construit, nu date reale de piață.
Structura: 5 zile × 7 bare de 60 de minute (ședința 9:30-16:00 are 6 ore și
jumătate, deci ultima bară a zilei e de 30 de minute).
ziua 1 bare 0-6 coborâre
ziua 2 bare 7-13 minimul și întoarcerea
ziua 3 bare 14-20 ziua verde care ridică pivotul zilnic peste cel weekly
ziua 4 bare 21-27 crossing-ul e pe tablă: dip, trigger la 592.00, TP1
ziua 5 bare 28-34 poziția 2 pe trail, ieșire la 599.80
Rulat direct, verifică toate afirmațiile capitolului:
python3 gen/strategii/mp2_serie.py
"""
import re
import sys
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Candle, from_closes # noqa: E402
BARE_ZI = 7 # bare de 60 min într-o ședință
ZILE = 5
# pivotul săptămânal (MARE): al săptămânii încheiate, H 588.00 / L 580.00 /
# C 584.00. E un dat din afara ferestrei desenate — săptămâna trecută nu
# încape pe grafic — deci îl declar aici, cu OHLC-ul din care iese.
PPW_OHLC = (588.00, 580.00, 584.00)
# pivotul zilnic al PRIMEI zile: al ședinței de vineri, dinaintea ferestrei.
PPD0_OHLC = (586.00, 581.00, 582.60)
START = 585.00 # open-ul primei bare
CLOSES = [
# ziua 1 — coborâre
584.20, 583.40, 583.90, 582.60, 581.80, 582.20, 581.20,
# ziua 2 — minimul (580.00) și întoarcerea
580.60, 581.40, 580.90, 582.30, 583.10, 583.80, 584.60,
# ziua 3 — ziua verde; închiderile ei sunt peste pivotul zilnic, dar
# pivotul zilnic e încă sub cel weekly, deci NU e trigger
585.10, 584.70, 585.60, 586.40, 587.20, 587.80, 588.40,
# ziua 4 — dip sub pivotul zilnic nou, apoi trigger-ul
586.90, 586.30, 586.80, 592.00, 593.40, 595.90, 598.20,
# ziua 5 — poziția 2 pe trail
599.60, 601.20, 600.40, 602.30, 601.10, 599.80, 598.90,
]
W = [(0.25, 0.25)] * 35 # fitile (sus, jos); excepțiile fixează extremele zilei
W[0] = (0.40, 0.25) # H zi 1 = 585.40
W[6] = (0.25, 0.60) # L zi 1 = 580.60
W[7] = (0.25, 0.60) # L zi 2 = 580.00
W[13] = (0.40, 0.25) # H zi 2 = 585.00
W[14] = (0.25, 0.40) # L zi 3 = 584.20
W[20] = (0.40, 0.25) # H zi 3 = 588.80
W[21] = (0.30, 0.30)
W[22] = (0.30, 0.30) # L = 586.00 — minimul celor 4 bare de la trigger
W[23] = (0.30, 0.20)
W[24] = (0.40, 0.30) # bara de trigger
W[25] = (0.35, 0.40)
W[26] = (0.35, 0.40)
W[27] = (0.35, 0.40)
for i in range(28, 35):
W[i] = (0.35, 0.35)
VOLS = [46, 40, 38, 42, 39, 36, 44,
58, 52, 44, 41, 38, 36, 40,
44, 38, 41, 45, 48, 44, 52,
50, 46, 42, 118, 86, 78, 82,
64, 58, 46, 62, 44, 56, 48]
ZI3, ZI4 = 2, 3 # indexul zilei verzi și al zilei de trigger
TRIGGER = 24 # bara de trigger (ziua 4, a 4-a oră)
PREA_DEVREME = 16 # închidere peste pivotul mic cu M neîndeplinit
NU_INCHIDE = 22 # trece peste pivot în timpul barei, dar nu închide
BARE_STOP = 4 # stopul: extrema ultimelor 4 bare, de la trigger
PRAG_BE = 0.60 # la 60 % din drumul spre TP1, SL-ul merge la BE
PRAG_90 = 0.90
def pp(h, l, c):
"""Pivotul clasic al unei perioade încheiate (formula cursului)."""
return round((h + l + c) / 3, 2)
def serie(closes=None, wicks=None, start=None):
return from_closes(closes or CLOSES, wicks=wicks or W, vols=VOLS,
start=START if start is None else start)
def zilnice(cds):
"""Agregă barele de 60 min în candele zilnice: 7 bare = o ședință."""
out = []
for d in range(ZILE):
p = cds[d * BARE_ZI:(d + 1) * BARE_ZI]
out.append(Candle(p[0].o, max(c.h for c in p), min(c.l for c in p), p[-1].c))
return out
def pivoti(cds, ppw_ohlc=PPW_OHLC, ppd0_ohlc=PPD0_OHLC):
"""(PPW, PPD pe zi, PPD pe bară). PPD al zilei N = pivotul zilei N-1."""
zi = zilnice(cds)
ppw = pp(*ppw_ohlc)
ppd = [pp(*ppd0_ohlc)] + [pp(z.h, z.l, z.c) for z in zi[:-1]]
return ppw, ppd, [ppd[i // BARE_ZI] for i in range(len(cds))]
def trail(cds, de_la):
"""Bull trail: low-ul fiecărei bare care face un maxim nou, de la `de_la`.
Întoarce (linia pe bară, indexul barei de ieșire) — ieșirea e prima
închidere sub linie, nu primul fitil sub ea.
"""
linie, nivel, hmax, iesire = [], None, cds[de_la].h, None
for i, c in enumerate(cds):
if i >= de_la:
if c.h >= hmax:
hmax, nivel = c.h, c.l
if iesire is None and nivel is not None and i > de_la and c.c < nivel:
iesire = i
linie.append(nivel)
return linie, iesire
def oglinda(a=246.40, f=0.2):
"""Aceeași structură, întoarsă: p' = a - f*p. Transformarea e afină, deci
agregarea zilnică și pivoții se păstrează; short-ul nu e „e invers", are
desenul lui complet. Cu f = 0.2 toate prețurile rămân exacte la 2 zecimale."""
m = lambda p: round(a - f * p, 2) # noqa: E731
cds = [Candle(m(c.o), m(c.l), m(c.h), m(c.c), vol=c.vol) for c in serie()]
ppw = m(pp(*PPW_OHLC))
zi = zilnice(cds)
ppd = [m(pp(*PPD0_OHLC))] + [pp(z.l, z.h, z.c) for z in zi[:-1]]
return cds, ppw, ppd, [ppd[i // BARE_ZI] for i in range(len(cds))]
# ---------------------------------------------------------------- cifrele
CDS = serie()
PPW, PPD, PPD_BARA = pivoti(CDS)
ENTRY = CDS[TRIGGER].c
SL = min(c.l for c in CDS[TRIGGER - BARE_STOP + 1:TRIGGER + 1])
R = round(ENTRY - SL, 2)
TP1 = round(ENTRY + R, 2)
BE = round(ENTRY + PRAG_BE * R, 2)
P90 = round(ENTRY + PRAG_90 * R, 2)
SWING_LOW = min(c.l for c in CDS) # varianta de bottom major
TRAIL, IESIRE = trail(CDS, TRIGGER)
IES_PRET = CDS[IESIRE].c
I_TP1 = next(i for i in range(TRIGGER + 1, len(CDS)) if CDS[i].h >= TP1)
I_BE = next(i for i in range(TRIGGER + 1, len(CDS)) if CDS[i].h >= BE)
# mărimea poziției: regula de risc a cursului (0,5 % pe prop), contul e al meu
CONT, RISC_PCT = 50_000, 0.005
BUCATI = int(CONT * RISC_PCT / R) // 2 * 2 # multiplu de 2: două ordine egale
RISC_REAL = round(BUCATI * R, 2)
def _verifica():
for i, c in enumerate(CDS):
assert c.l <= min(c.o, c.c) and c.h >= max(c.o, c.c), f"bara {i}: h/l nu cuprind o/c"
if i:
assert abs(c.o - CDS[i - 1].c) < 1e-9, f"bara {i}: open != close precedent"
assert len(CDS) == BARE_ZI * ZILE == 35
# 1. cifrele din material ies din serie, nu sunt scrise de mână
assert (ENTRY, SL, R, TP1) == (592.00, 586.00, 6.00, 598.00), (ENTRY, SL, R, TP1)
assert BE == 595.60 and P90 == 597.40
# 2. M: pivotul mic e sub cel mare până în ziua 4, și peste din ziua 4
assert all(PPD[d] < PPW for d in range(ZI4)), PPD
assert PPD[ZI4] > PPW, (PPD[ZI4], PPW)
assert PPD[ZI4] - PPW > 2.5, "crossing-ul e prea strâns ca să se vadă pe desen"
# 3. crossing PROASPĂT, nu continuare: în ziua dinainte era de partea cealaltă
assert PPD[ZI4 - 1] < PPW < PPD[ZI4]
assert PPD[ZI4 + 1] > PPW, "ziua 5 e continuare (+1), nu crossing"
# 4. trigger-ul: PRIMA închidere peste pivotul mic din ziua în care M e îndeplinit
zi4 = range(ZI4 * BARE_ZI, (ZI4 + 1) * BARE_ZI)
assert TRIGGER == next(i for i in zi4 if CDS[i].c > PPD_BARA[i]), "nu e prima închidere"
assert CDS[TRIGGER].c > CDS[TRIGGER].o, "bara de trigger nu e verde"
# 5. NU — prea devreme: închidere peste pivotul mic cu M neîndeplinit
assert CDS[PREA_DEVREME].c > PPD_BARA[PREA_DEVREME] > 0
assert PPD_BARA[PREA_DEVREME] < PPW, "în ziua 3 M ar fi îndeplinit"
assert CDS[PREA_DEVREME].c > CDS[PREA_DEVREME].o, "bara „prea devreme” nu e verde"
# 6. NU — trece peste pivot în timpul barei, dar nu închide peste
assert CDS[NU_INCHIDE].h > PPD_BARA[NU_INCHIDE] > CDS[NU_INCHIDE].c
# 7. stopul: minimul celor 4 bare, și e minimul lor strict
assert SL == CDS[22].l and SL < min(CDS[i].l for i in (21, 23, 24))
assert SWING_LOW < SL, "varianta de bottom major ar coincide cu stopul de 4 bare"
assert round(SL - SWING_LOW, 2) == 6.00, round(SL - SWING_LOW, 2)
# 8. ieșirile, în ordine: BE, TP1, apoi trail-ul
assert I_BE == 26 and I_TP1 == 27 and IESIRE == 33, (I_BE, I_TP1, IESIRE)
assert CDS[IESIRE].c < TRAIL[IESIRE] and all(
CDS[i].c >= TRAIL[i] for i in range(TRIGGER + 1, IESIRE)), "ieșire greșită pe trail"
assert round((IES_PRET - ENTRY) / R, 2) == 1.30, (IES_PRET - ENTRY) / R
# 9. mărimea poziției
assert BUCATI == 40 and RISC_REAL == 240.00 and RISC_REAL <= CONT * RISC_PCT
# 10. oglinda păstrează structura
o_cds, o_ppw, o_ppd, _ = oglinda()
assert all(o_ppd[d] > o_ppw for d in range(ZI4)) and o_ppd[ZI4] < o_ppw
o_entry = o_cds[TRIGGER].c
o_sl = max(c.h for c in o_cds[TRIGGER - BARE_STOP + 1:TRIGGER + 1])
assert (o_entry, o_sl) == (128.00, 129.20), (o_entry, o_sl)
assert round(o_sl - o_entry, 2) == 1.20
_verifica()
# -------------------------------------------------- ajutoare pentru desen
def offset(svg, ch):
"""Câți px a coborât `render()` graficul față de ce întoarce acum ch._y().
`render()` mută `top` ca să facă loc titlului/săgeților și îl restaurează
la final, deci ch._y() de după randare e deplasat. Citim deplasarea din
prima linie de Level desenată, ca ce injectăm pe urmă să cadă exact peste
candele (bug real: liniile de pivot cu 22 px mai sus decât candelele).
"""
y = float(re.search(r'<line x1="24" y1="([\d.]+)"', svg).group(1))
return y - ch._y(ch.levels[0].price)
def trepte(ch, valori, off):
"""Polilinie în trepte pentru un pivot care se recalculează periodic."""
pts = []
for i, v in enumerate(valori):
if v is None:
continue
x, y = ch._cx(i), ch._y(v) + off
if pts and v != valori[i - 1]:
pts.append((x, ch._y(valori[i - 1]) + off))
pts.append((x, y))
return " ".join(f"{x:.1f},{y:.1f}" for x, y in pts)
def injecteaza(svg, bucati):
"""Pune `bucati` imediat după tag-ul de deschidere: desenate în spate."""
i = svg.index(">") + 1
return svg[:i] + "\n " + "\n ".join(bucati) + svg[i:]
def scrie(out, svg, w, h):
assert "\n\n" not in svg, "linie goală în SVG rupe blocul din markdown"
assert ".md:" not in svg, "trimitere fișier:linie în SVG"
for x, y in re.findall(r'<text x="([-\d.]+)" y="([-\d.]+)"', svg):
assert -1 <= float(x) <= w + 1 and 0 <= float(y) <= h + 1, f"text în afara pânzei ({x}, {y})"
out.write_text(svg, encoding="utf-8")
print(f"scris {out.name} ({w}x{h:.0f})")
if __name__ == "__main__":
print(f"PPW (mare) = {PPW:.2f} PPD (mic), pe zile = {PPD}")
print(f"crossing în ziua {ZI4 + 1}: {PPD[ZI4 - 1]:.2f} -> {PPD[ZI4]:.2f}, peste {PPW:.2f}")
print(f"trigger bara {TRIGGER}: close {ENTRY:.2f} peste pivotul mic {PPD[ZI4]:.2f}")
print(f"SL {SL:.2f} (minimul barelor {TRIGGER - 3}-{TRIGGER}), 1R = {R:.2f} "
f"({R / ENTRY * 100:.2f} %), TP1 {TP1:.2f} la bara {I_TP1}")
print(f"BE {BE:.2f} la bara {I_BE}; trail: ieșire la bara {IESIRE}, "
f"{IES_PRET:.2f} = {(IES_PRET - ENTRY) / R:.2f}R")
print(f"swing low absolut {SWING_LOW:.2f} (varianta de bottom major)")
print(f"mărime: {BUCATI} bucăți (2 x {BUCATI // 2}), risc real {RISC_REAL:.2f} $")
print("toate verificările trec")