"""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'") + 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' {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")