#!/usr/bin/env python3 """MP² — perechea de timeframe-uri, cu lupă (Pasul 1 din `02_mp2.md`). Stânga: graficul daily, unde trăiesc cei doi pivoți și unde se vede crossing-ul. Dreapta: graficul de 60 de minute, unde intri. Graficul din dreapta NU e o serie care vine după cel din stânga: e **descompunerea reală** a ultimelor două candele daily. O ședință = 7 bare de 60 de minute (9:30-16:00 înseamnă 6 ore și jumătate, deci ultima bară a zilei e de 30 de minute), cu același open, același close, același maxim și același minim — verificat bară cu bară prin assert. Sursa de adevăr e seria fină din `mp2_serie.py`; candelele daily se calculează din ea prin agregare. Caseta punctată de pe panoul mare arată exact ce bucată e mărită; ghidajele îi leagă colțurile de cadrul mic; cele două linii de pivot sunt trasate pe ambele panouri, cu același număr de ambele părți, iar conectorul subțire leagă cele două înălțimi ale aceluiași preț. Nu se pot alinia: scările au pante diferite, e o lupă, nu o eroare. Rulare: python3 gen/strategii/mp2_perechi_tf.py """ import sys from pathlib import Path sys.path.insert(0, "/workspace/atm-curs-dl") sys.path.insert(0, str(Path(__file__).parent)) import mp2_serie as S # noqa: E402 OUT = Path(__file__).with_suffix(".svg") W, PLOT_H = 720, 190 GREEN, RED, GRAY, INK, BLUE, ORANGE = "#188a3e", "#c00", "#888", "#333", "#1a5fb4", "#b06000" LINK = "#aaa" BX0, BGAP = 96, 22 # panoul daily SX0, SGAP = 374, 15 # panoul de 60 min BODY_W = 9 FRAME_L = SX0 - 9 COL_X = 586 MIDX = FRAME_L - 20 # granița stânga/dreapta pentru verificări PAD = 0.06 # aer sus/jos pe panoul mic TOP = 86 # --- zilele dinaintea ferestrei fine, ca daily (open = close precedent) ------- DOWN_C = [634.00, 626.00, 629.00, 618.00, 610.00, 612.00, 598.00, 585.00] DOWN_W = 2.5 ZI_FINA = 2 # câte ședințe sunt desfăcute în panoul mic def daily_din_closes(closes, start, w): out, o = [], start for c in closes: out.append((o, max(o, c) + w, min(o, c) - w, c)) o = c return out A_FIN, B_FIN = (S.ZI4 + 1 - ZI_FINA) * S.BARE_ZI, (S.ZI4 + 1) * S.BARE_ZI zile = S.zilnice(S.CDS) fine = [(c.o, c.h, c.l, c.c) for c in S.CDS[A_FIN:B_FIN]] zoom = [(z.o, z.h, z.l, z.c) for z in zile[S.ZI4 + 1 - ZI_FINA:S.ZI4 + 1]] base = (daily_din_closes(DOWN_C, 640.00, DOWN_W) + [(z.o, z.h, z.l, z.c) for z in zile[:S.ZI4 + 1 - ZI_FINA]]) big = base + zoom PPD_ZI = [None] * len(DOWN_C) + S.PPD[:S.ZI4 + 1] # pivotul mic, pe candele daily PPD_FIN = S.PPD_BARA[A_FIN:B_FIN] N_BASE = len(base) # --- ce afirmă desenul, verificat pe serii ------------------------------------ for nume, bars in (("daily", big), ("60 min", fine)): for i, (o, h, l, c) in enumerate(bars): assert h >= max(o, c) and l <= min(o, c), f"{nume}[{i}] h/l incoerent" if i: assert abs(o - bars[i - 1][3]) < 1e-9, f"{nume}[{i}] open != close precedent" assert len(fine) == S.BARE_ZI * ZI_FINA # 1. descompunerea e reală: fiecare candelă daily mărită = 7 bare de 60 min for j, (o, h, l, c) in enumerate(zoom): part = fine[j * S.BARE_ZI:(j + 1) * S.BARE_ZI] assert (o, c) == (part[0][0], part[-1][3]) assert h == max(b[1] for b in part) and l == min(b[2] for b in part) # 2. lupa se leagă de restul graficului mare assert abs(fine[0][0] - base[-1][3]) < 1e-9, "open-ul primei bare fine != close-ul ultimei daily" # 3. trigger-ul cade în a doua candelă daily mărită, nu în prima I_TRIG = S.TRIGGER - A_FIN assert I_TRIG // S.BARE_ZI == 1, "trigger-ul nu cade în ultima candelă daily" assert PPD_FIN[I_TRIG] == S.PPD[S.ZI4] > S.PPW > PPD_FIN[0], "crossing-ul nu e în fereastră" # 4. trade-ul pe TF-ul mic ENTRY, SL, R, TP1 = S.ENTRY, S.SL, S.R, S.TP1 assert SL == min(b[2] for b in fine[I_TRIG - S.BARE_STOP + 1:I_TRIG + 1]) PCT = R / ENTRY * 100 # 5. același setup, dacă ai fi intrat pe daily: alt trigger, alt stop B_ENTRY = big[-1][3] # close-ul candelei daily de trigger B_SL = min(b[2] for b in big[-S.BARE_STOP:]) # low-ul ultimelor 4 candele daily B_R = round(B_ENTRY - B_SL, 2) B_PCT = B_R / B_ENTRY * 100 assert B_SL == S.SWING_LOW == 580.00 and B_R == 18.20, (B_SL, B_R) RAPORT = B_PCT / PCT assert 2.9 <= RAPORT <= 3.1, f"raportul stopurilor e {RAPORT:.2f}" # 6. cadrul mic cuprinde și pivotul mare, ca nivelul comun să aibă unde sta ZLO, ZHI = min(b[2] for b in fine), max(b[1] for b in fine) assert ZLO == min(b[2] for b in zoom) and ZHI == max(b[1] for b in zoom) assert S.PPW < ZLO, "pivotul mare e în interiorul barelor mici; verifică scara" lo_, hi_ = min(ZLO, S.PPW), ZHI pad = PAD * (hi_ - lo_) LO, HI = lo_ - pad, hi_ + pad bmax = max(b[1] for b in big) bmin = min(min(b[2] for b in big), B_SL, LO) bs, ss = PLOT_H / (bmax - bmin), PLOT_H / (HI - LO) assert ss / bs > 2.5, f"lupa mărește de doar {ss / bs:.1f} ori" def yb(p): return TOP + (bmax - p) * bs def ys(p): return TOP + (HI - p) * ss # --- desen -------------------------------------------------------------------- out, LABELS = [], [] def text(x, y, s, fill=INK, anchor="start", bold=False, size=12): st = [f"font-size:{size}px"] + (["font-weight:bold"] if bold else []) a = f' text-anchor="{anchor}"' if anchor != "start" else "" w = len(s) * (5.7 if size >= 12 else size * 0.5) x0 = x if anchor == "start" else (x - w / 2 if anchor == "middle" else x - w) assert 4 <= x0 and x0 + w <= W - 4, f"text iese din pânză: {s!r} ({x0:.0f}..{x0 + w:.0f})" LABELS.append((x0, x0 + w, y, s)) out.append(f'{s}') def hline(x1, x2, y, color, dash, width=1.5): out.append(f'') def line(x1, y1, x2, y2, color, dash="", width=1.2, marker=""): d = f' stroke-dasharray="{dash}"' if dash else "" mk = f' marker-end="url(#{marker})"' if marker else "" out.append(f'') def rect(x0, y0, x1, y1, color, fill_op=0.10, dash=""): d = f' stroke-dasharray="{dash}"' if dash else "" out.append(f'') def candles(bars, x0, gap, yf): for i, (o, h, l, c) in enumerate(bars): cx = x0 + i * gap line(cx, yf(h), cx, yf(l), "#1a1a1a", width=1.2) top_ = yf(max(o, c)) out.append(f'') def trepte(vals, x0, gap, yf): pts = [] for i, v in enumerate(vals): if v is None: continue x, y = x0 + i * gap, yf(v) if pts and vals[i - 1] is not None and v != vals[i - 1]: pts.append((x, yf(vals[i - 1]))) pts.append((x, y)) return " ".join(f"{x:.1f},{y:.1f}" for x, y in pts) def bracket(x, ya, yb_, color, label, side="right", below=False): tip = x + 5 if side == "right" else x - 5 out.append(f'') if below: text((x + tip) / 2, max(ya, yb_) + 14, label, fill=color, anchor="middle", bold=True, size=11) else: text(tip + 4, (ya + yb_) / 2 + 4, label, fill=color, bold=True, size=11) def stack(items, x_line): y_prev = None for y, lines, fill in sorted(items, key=lambda t: t[0]): y_lab = y if y_prev is None or y >= y_prev + 15 else y_prev + 15 out.append(f'') for j, s in enumerate(lines): text(COL_X, y_lab + j * 13, s, fill=fill, size=10) y_prev = y_lab + (len(lines) - 1) * 13 out.append('') text(20, 22, "Pasul 1 — perechea de timeframe-uri: pivoții stau pe daily, intrarea se face pe 60 min", bold=True, size=13) text(20, 40, "Graficul din dreapta e aceeași bucată de piață, mărită: fiecare candelă daily din " "caseta punctată", fill="#555", size=11) text(20, 54, f"e desfăcută în {S.BARE_ZI} bare de 60 de minute (ședința are 6 ore și jumătate, " f"deci ultima bară a zilei e de 30 min).", fill="#555", size=11) text(20, TOP - 8, "daily: direcția", fill="#555", size=11) text(FRAME_L, TOP - 8, f"60 min: ultimele {ZI_FINA} candele daily, desfăcute", fill="#555", size=11) # ---- panoul daily ------------------------------------------------------------ lx0, lx1 = BX0 + N_BASE * BGAP - 9, BX0 + (len(big) - 1) * BGAP + 10 ly0, ly1 = yb(HI), yb(LO) rect(lx0, ly0, lx1, ly1, BLUE, fill_op=0.06, dash="4 3") hline(BX0 - 18, lx1, yb(S.PPW), GRAY, "8 4") out.append(f'') candles(big, BX0, BGAP, yb) text(BX0 - 8, yb(S.PPW) + 14, f"pivot weekly (mare) {S.PPW:.2f}", fill="#666", size=11) text(BX0 + 2 * BGAP, yb(bmin) + 16, "pivot daily (mic), în trepte", fill=BLUE, size=11) # stopul, dacă ai fi măsurat pe daily bracket(34, yb(B_ENTRY), yb(B_SL), RED, f"stop {B_PCT:.2f} %".replace(".", ","), below=True) # ---- ghidajele de lupă ------------------------------------------------------- guides = [((lx1, ly0), (FRAME_L, TOP)), ((lx1, ly1), (FRAME_L, TOP + PLOT_H))] for a_, b_ in guides: line(a_[0], a_[1], b_[0], b_[1], BLUE, dash="5 3", width=1.1) # ---- conectorul nivelului comun ---------------------------------------------- # Aceeași linie de pivot weekly cade la înălțimi diferite: scările au pante # diferite. Conectorul le leagă; „același preț" e scris o singură dată, în cot. ycb, ycs = yb(S.PPW), ys(S.PPW) mid = (lx1 + FRAME_L) / 2 out.append(f'') text(mid + 4, (ycb + ycs) / 2 + 4, "același preț", fill="#999", size=10) # ---- panoul de 60 min -------------------------------------------------------- frame_r = SX0 + (len(fine) - 1) * SGAP + 9 rect(FRAME_L, TOP, frame_r, TOP + PLOT_H, BLUE, fill_op=0.03) y_e, y_sl, y_tp = ys(ENTRY), ys(SL), ys(TP1) assert abs((y_sl - y_e) - (y_e - y_tp)) < 1e-6, "1R în sus nu e desenat cât 1R în jos" assert TOP + PLOT_H - ys(S.PPW) > 6, "linia pivotului mare e lipită de marginea cadrului" # stopul e minimul ULTIMELOR 4 bare, nu al întregii ferestre: ziua dinainte # coboară mai jos, iar acele bare nu intră la socoteală assert y_sl < ys(ZLO), "stopul de 4 bare ar trebui să stea peste minimul ferestrei" # zona verde peste bara de trigger sx0, sx1 = SX0 + I_TRIG * SGAP - 9, SX0 + I_TRIG * SGAP + 9 rect(sx0, y_e - 6, sx1, ys(ZLO) + 4, GREEN) line((sx0 + sx1) / 2, TOP + PLOT_H, (sx0 + sx1) / 2, TOP + PLOT_H + 26, GREEN, width=1) text((sx0 + sx1) / 2, TOP + PLOT_H + 38, "închide peste pivotul mic: aici intri", fill=GREEN, anchor="middle", bold=True, size=11) hline(FRAME_L, frame_r, ys(S.PPW), GRAY, "8 4") out.append(f'') hline(FRAME_L, frame_r, y_e, GREEN, "6 3") hline(FRAME_L, frame_r, y_sl, RED, "6 3") hline(FRAME_L, frame_r, y_tp, GREEN, "2 3") candles(fine, SX0, SGAP, ys) stack([(y_tp + 4, [f"TP1 {TP1:.2f}"], GREEN), (y_e + 4, [f"intrare {ENTRY:.2f}"], GREEN), (y_sl + 4, [f"stop {SL:.2f}", f"minimul ultimelor", f"{S.BARE_STOP} bare de 60 min"], RED), (ys(S.PPW) + 4, [f"pivot weekly", f"(mare) {S.PPW:.2f}"], "#666")], frame_r) kx = SX0 + (I_TRIG + 1) * SGAP + BODY_W / 2 + 1 bracket(kx, y_e, y_sl, RED, f"stop {PCT:.2f} %".replace(".", ",")) # ---- de ce merită lupa ------------------------------------------------------- ya = TOP + PLOT_H + 62 line(34, ya, kx, ya, RED, width=1.6, marker="pt") text((34 + kx) / 2, ya + 17, f"același setup, stop de {RAPORT:.1f} ori mai mic".replace(".", ","), fill=RED, anchor="middle", bold=True, size=11) text((34 + kx) / 2, ya + 31, f"→ de {RAPORT:.1f} ori mai multe acțiuni la același risc în bani" .replace(".", ","), fill=RED, anchor="middle", bold=True, size=11) text(20, ya + 52, f"Pe daily ai fi intrat la close-ul zilei ({B_ENTRY:.2f}) cu stopul sub " f"minimul ultimelor {S.BARE_STOP} zile ({B_SL:.2f}).", fill="#555", size=11) text(20, ya + 66, "Pe 60 de minute vezi unde s-a oprit coborârea, deci ai de unde măsura un stop " "mult mai strâns.", fill="#555", size=11) # --- fiecare preț stă pe panoul lui, cu excepția nivelului comun -------------- COMUN = f"{S.PPW:.2f}" MARE = {"B_ENTRY": B_ENTRY, "B_SL": B_SL} MIC = {"ENTRY": ENTRY, "SL": SL, "TP1": TP1} for x0, _x1, _y, s in LABELS: pe_mic = x0 >= MIDX for k, v in (MARE if pe_mic else MIC).items(): assert f"{v:.2f}" not in s, f"eticheta {s!r} poartă prețul {k} al celuilalt panou" st = [s for x0, _1, _2, s in LABELS if COMUN in s and x0 < MIDX] dr = [s for x0, _1, _2, s in LABELS if COMUN in s and x0 >= MIDX] assert st and dr, f"nivelul comun {COMUN} nu apare pe ambele panouri ({st} / {dr})" for k, v in MIC.items(): assert any(f"{v:.2f}" in s and x0 >= MIDX for x0, _1, _2, s in LABELS), f"{k} lipsește de pe mic" H = ya + 76 for a in range(len(LABELS)): assert 0 < LABELS[a][2] < H, f"etichetă în afara pânzei: {LABELS[a][3]!r}" for b in range(a + 1, len(LABELS)): xa0, xa1, ya_, sa = LABELS[a] xb0, xb1, yb_, sb = LABELS[b] assert not (abs(ya_ - yb_) < 13 and xa0 < xb1 - 4 and xb0 < xa1 - 4), \ f"etichete suprapuse: {sa!r} / {sb!r}" svg = (f'\n ' + "\n ".join(out) + "\n") S.scrie(OUT, svg, W, H) print(f"lupă x{ss / bs:.1f} | daily: entry {B_ENTRY:.2f} SL {B_SL:.2f} = {B_PCT:.2f} % | " f"60 min: entry {ENTRY:.2f} SL {SL:.2f} = {PCT:.2f} % | raport {RAPORT:.2f}")