#!/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'= o else RED}"/>')
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'")
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}")