Files
atm-curs-dl/gen/strategii/mp2_perechi_tf.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

303 lines
14 KiB
Python

#!/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'<text x="{x:.1f}" y="{y:.1f}" fill="{fill}"{a} style="{";".join(st)}">{s}</text>')
def hline(x1, x2, y, color, dash, width=1.5):
out.append(f'<line x1="{x1:.1f}" y1="{y:.1f}" x2="{x2:.1f}" y2="{y:.1f}" stroke="{color}" '
f'stroke-width="{width}" stroke-dasharray="{dash}"/>')
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'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
f'stroke="{color}" stroke-width="{width}"{d}{mk}/>')
def rect(x0, y0, x1, y1, color, fill_op=0.10, dash=""):
d = f' stroke-dasharray="{dash}"' if dash else ""
out.append(f'<rect x="{x0:.1f}" y="{y0:.1f}" width="{x1 - x0:.1f}" height="{y1 - y0:.1f}" '
f'fill="{color}" fill-opacity="{fill_op}" stroke="{color}" stroke-width="1.2"{d}/>')
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'<rect x="{cx - BODY_W / 2:.1f}" y="{top_:.1f}" width="{BODY_W}" '
f'height="{max(yf(min(o, c)) - top_, 1.6):.1f}" '
f'fill="{GREEN if c >= 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'<path d="M{tip:.1f},{ya:.1f} H{x:.1f} V{yb_:.1f} H{tip:.1f}" fill="none" '
f'stroke="{color}" stroke-width="1.6"/>')
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'<path d="M{x_line + 2:.1f},{y - 4:.1f} H{x_line + 7:.1f} '
f'L{COL_X - 4:.1f},{y_lab - 4:.1f}" fill="none" stroke="{fill}" '
f'stroke-width="0.8" stroke-opacity="0.8"/>')
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('<defs><marker id="pt" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" '
'markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#c00"/></marker></defs>')
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'<polyline points="{trepte(PPD_ZI, BX0, BGAP, yb)}" fill="none" stroke="{BLUE}" '
f'stroke-width="1.8"/>')
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'<path d="M{lx1:.1f},{ycb:.1f} H{mid:.1f} V{ycs:.1f} H{FRAME_L:.1f}" fill="none" '
f'stroke="{LINK}" stroke-width="1.0" stroke-dasharray="3 3"/>')
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'<polyline points="{trepte(PPD_FIN, SX0, SGAP, ys)}" fill="none" stroke="{BLUE}" '
f'stroke-width="1.8"/>')
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'<svg viewBox="0 0 {W} {H:.0f}" xmlns="http://www.w3.org/2000/svg" '
f'style="width:100%;height:auto;font:12px sans-serif">\n ' + "\n ".join(out) + "\n</svg>")
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}")