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
269 lines
12 KiB
Python
269 lines
12 KiB
Python
"""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")
|