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
303 lines
14 KiB
Python
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}")
|