"""MDSRT, Pasul 1 — nivelul trăiește pe TF-ul mare, trigger-ul pe cel mic.
Un singur rând: daily (stânga) → 60 min (dreapta). Graficul din dreapta NU e
o serie care vine după cel din stânga: e **descompunerea reală** a ultimelor
două candele daily. O zi de bursă are 7 bare de 60 de minute (ședința
9:30-16:00 are 6 ore și jumătate — ultima bară a zilei e de 30 de minute),
deci 2 candele daily = 14 bare de 60 de minute, cu open, close, max(high) și
min(low) care coincid — verificat bară cu bară prin assert.
Sursa de adevăr e seria fină; candelele daily se calculează din ea prin
agregare. Pivotul monthly (76.00) e desenat pe amândouă panourile, cu același
număr de ambele părți, și legat printr-un conector — scările au pante
diferite, deci același preț cade la înălțimi diferite: e o lupă, nu o eroare.
Prețurile sunt ale altui simbol decât exemplul din ceilalți pași (care e pe
SPY, 5 min); regulile sunt aceleași. Toate cifrele din desen ies din serii.
Rulează: python3 gen/strategii/mdsrt_zona.py
"""
from pathlib import Path
OUT = Path(__file__).with_suffix(".svg")
W = 720
MARJA = 0.10 # aceeași marjă de stop ca în tot capitolul
PIVOT = 76.00 # pivot monthly — nivelul de pe TF-ul mare
K = 7 # 7 bare de 60 min pe zi
NZ = 2 # 2 candele daily mărite
# --- seria fină (60 min): (open, high, low, close) --------------------------
# 0-6 = ziua 1: coborârea spre pivot; bara 4 = piciorul 1 (low 76.05, ține nivelul)
# 7-13 = ziua 2: piciorul 2 rupe pivotul (bara 10, hammer), apoi intrarea
FINE = [
(77.40, 77.55, 77.05, 77.15), (77.15, 77.30, 76.80, 76.90),
(76.90, 77.00, 76.50, 76.60), (76.60, 76.75, 76.30, 76.45),
(76.45, 76.55, 76.05, 76.20), # 4: piciorul 1, low 76.05
(76.20, 76.60, 76.15, 76.50), (76.50, 76.65, 76.25, 76.35),
(76.35, 76.45, 76.05, 76.15), (76.15, 76.25, 75.90, 75.95),
(75.95, 76.05, 75.85, 75.95),
(75.95, 76.20, 75.70, 76.10), # 10: piciorul 2, hammer (low 75.70)
(76.10, 76.40, 76.05, 76.30), # 11: intrarea (close peste high-ul 76.20)
(76.30, 76.75, 76.20, 76.60), (76.60, 77.15, 76.55, 77.05),
]
# --- candelele daily dinaintea casetei de lupă ------------------------------
BASE = [
(86.00, 86.60, 84.20, 84.60), (84.60, 85.20, 83.00, 83.40),
(83.40, 84.60, 82.80, 84.00), (84.00, 84.40, 81.50, 81.90),
(81.90, 82.40, 80.20, 80.60), (80.60, 81.20, 79.00, 79.40),
(79.40, 80.40, 78.20, 79.60), (79.60, 80.00, 77.20, 77.40),
]
LEG1, LEG2, ENTRY_I = 4, 10, 11
def aggregate(fine, k, n):
out_ = []
for j in range(n):
part = fine[j * k:(j + 1) * k]
out_.append((part[0][0], max(b[1] for b in part),
min(b[2] for b in part), part[-1][3]))
return out_
def eq(a, b):
return abs(a - b) < 0.005
zoom = aggregate(FINE, K, NZ)
big = BASE + zoom
# ------------------------------------------------------------- assert-urile
for name, 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"{name}[{i}] h/l incoerent"
if i:
assert eq(o, bars[i - 1][3]), f"{name}[{i}] open != close precedent"
assert len(FINE) == K * NZ
assert eq(FINE[0][0], BASE[-1][3]), "lupa nu se leagă de ultima candelă daily"
# 1. proprietăți pe care textul le pretinde (nu recalculul agregării)
assert LEG1 // K == 0 and LEG2 // K == 1, "cele două picioare nu cad în zile diferite"
assert LEG2 - LEG1 >= 5, "sub minimul de 5 bare între punctele divergenței"
leg1_l, leg2_l = FINE[LEG1][2], FINE[LEG2][2]
assert leg1_l == 76.05 > PIVOT, leg1_l
assert leg2_l == 75.70 < PIVOT, leg2_l
assert leg2_l == min(b[2] for b in FINE), "piciorul 2 nu e minimul zonei mărite"
assert leg1_l == min(b[2] for b in FINE[:K]), "piciorul 1 nu e minimul zilei 1"
# 2. ziua a doua, ca o singură candelă daily, NU arată piciorul 2 ca minim propriu
assert eq(zoom[1][2], leg2_l), "low-ul zilei 2 nu e low-ul piciorului 2"
assert zoom[1][3] > PIVOT, "ziua 2 nu se închide peste pivot"
# 3. hammer-ul: corp mic sus, coadă lungă în jos, close înapoi peste pivot
o, h, l, c = FINE[LEG2]
assert abs(o - c) <= 0.5 * (h - l) and min(o, c) - l > abs(o - c), (o, h, l, c)
assert c > PIVOT, c
# 4. trade-ul, cu regulile capitolului
low4 = min(b[2] for b in FINE[ENTRY_I - 3:ENTRY_I + 1])
assert eq(low4, leg2_l), low4
sl = round(low4 - MARJA, 2)
entry = FINE[ENTRY_I][3]
assert entry > FINE[LEG2][1], "intrarea nu sparge high-ul candelei-trigger"
risk = round(entry - sl, 2)
tp = round(entry + risk, 2)
first = next(i for i in range(ENTRY_I + 1, len(FINE)) if FINE[i][1] >= tp - 1e-9)
assert (sl, entry, risk, tp, first) == (75.60, 76.30, 0.70, 77.00, 13), \
(sl, entry, risk, tp, first)
# 5. stopul măsurat pe daily (sub low-ul zilei 2) e de câteva ori mai larg
b_sl = round(zoom[1][2] - MARJA, 2)
b_entry = zoom[1][3]
b_risk = round(b_entry - b_sl, 2)
ratio = (b_risk / b_entry) / (risk / entry)
assert 1.8 <= ratio <= 2.5, ratio
# --- desen ------------------------------------------------------------------
GREEN, RED, GRAY, INK, BLUE = "#188a3e", "#c00", "#888", "#333", "#1a5fb4"
LINK = "#aaa"
PLOT_H = 190
BX0, BGAP = 105, 15
SX0, SGAP = 372, 15
BODY_W = 9
FRAME_L = SX0 - 9
COL_X = 600
MIDX = FRAME_L - 20
PAD = 0.06
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 viewBox: {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 ""
m = 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_))
hh = max(yf(min(o_, c_)) - top_, 1.6)
out.append(f'')
def seg_dist(p, a, b):
(px, py), (ax, ay), (bx, by) = p, a, b
dx, dy = bx - ax, by - ay
t = 0.0 if dx == dy == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy)
/ (dx * dx + dy * dy)))
return ((px - ax - t * dx) ** 2 + (py - ay - t * dy) ** 2) ** 0.5
out.append(''
'')
text(20, 22, "Pasul 1 — nivelul se citește pe daily, trigger-ul se ia pe 60 min",
bold=True, size=13)
text(20, 40, "Graficul din dreapta e aceeași bucată de piață, mărită: 2 candele daily = 14 bare "
"de 60 min", fill="#555", size=11)
top = 76
text(20, top - 8, "daily: unde e nivelul", fill="#555", size=11)
text(FRAME_L, top - 8, "60 min: descompunerea ultimelor 2 candele daily", fill="#555", size=11)
zlo_, zhi_ = sl, max(b[1] for b in FINE)
pad = PAD * (zhi_ - zlo_)
lo, hi = zlo_ - pad, zhi_ + pad
bmax = max(b[1] for b in big)
bmin = min(min(b[2] for b in big), lo)
bs, ss = PLOT_H / (bmax - bmin), PLOT_H / (hi - lo)
yb = lambda p: top + (bmax - p) * bs # noqa: E731
ys = lambda p: top + (hi - p) * ss # noqa: E731
# ---- panoul daily ----------------------------------------------------------
lx0, lx1 = BX0 + len(BASE) * BGAP - 6, BX0 + (len(big) - 1) * BGAP + 9
ly0, ly1 = yb(hi), yb(lo)
rect(lx0, ly0, lx1, ly1, BLUE, fill_op=0.06, dash="4 3")
hline(BX0 - 15, lx1, yb(PIVOT), BLUE, "4 3")
text(30, yb(PIVOT) + 12, "pivot monthly", fill=BLUE, size=11)
text(30, yb(PIVOT) + 26, f"{PIVOT:.2f}", fill=BLUE, size=11, bold=True)
candles(big, BX0, BGAP, yb)
# zona de evidențiere: ultimele candele daily, acolo unde prețul ajunge la nivel
zx0, zx1 = BX0 + (len(BASE) - 2) * BGAP - 8, lx0 - 3
rect(zx0, yb(big[-3][1]) - 4, zx1, yb(PIVOT) + 10, GREEN, fill_op=0.10)
text(BX0 - 15, top + PLOT_H + 20, "downtrend care se oprește la un nivel:", fill=GREEN,
size=11, bold=True)
text(BX0 - 15, top + PLOT_H + 34, "fără nivelul de dedesubt e doar downtrend", fill=GREEN,
size=11, bold=True)
text(BX0 - 15, top + PLOT_H + 48, "care continuă — nu ai unde pune stopul", fill=GREEN,
size=11, bold=True)
# pe daily nu ai unde să intri mai devreme de close-ul zilei: stopul e același
# (low-ul zilei = low-ul piciorului 2), dar intrarea e cu o zi întreagă mai sus
line(BX0 - 19, yb(b_entry), lx1, yb(b_entry), GREEN, dash="6 3", width=1.3)
text(30, yb(b_entry) - 20, f"intrare pe daily {b_entry:.2f} = close-ul zilei", fill=GREEN, size=10)
text(30, yb(b_entry) - 6, "(mai devreme n-ai ce citi pe daily)", fill=GREEN, size=10)
# ---- 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 -------------------------------------------
ycb, ycs = yb(PIVOT), ys(PIVOT)
def clearance(mx):
conn = [((lx1, ycb), (mx, ycb)), ((mx, ycb), (mx, ycs)), ((mx, ycs), (FRAME_L, ycs))]
pts = [(sx + (ex - sx) * t / 24, sy + (ey - sy) * t / 24)
for (sx, sy), (ex, ey) in conn for t in range(25)]
return min(seg_dist(q, ga, gb) for q in pts if q[0] >= lx1 + 10 for ga, gb in guides)
mid = max(range(int(lx1) + 14, int(FRAME_L) - 13), key=clearance)
# pivotul e aproape de marginea de jos a lupei, deci conectorul are de mers
# printr-un coridor îngust între cele două ghidaje; 8 px e minimul la care
# linia lui punctată se citește încă separat de ghidaj.
assert clearance(mid) > 8, f"conectorul trece la {clearance(mid):.1f} px de un ghidaj"
out.append(f'')
text(min(mid + 5, FRAME_L - 58), (ycb + ycs) / 2 + 4, "același nivel", fill="#888", size=10)
# ---- panoul 60 min ---------------------------------------------------------
y_e, y_sl, y_tp, y_p = ys(entry), ys(sl), ys(tp), ys(PIVOT)
assert abs((y_sl - y_e) / (y_e - y_tp) - 1) < 1e-6, "SL-intrare != intrare-TP1 în px"
assert top + PLOT_H - y_sl > 6, "linia de stop e lipită de marginea cadrului"
frame_r = SX0 + (len(FINE) - 1) * SGAP + 9
rect(FRAME_L, top, frame_r, top + PLOT_H, BLUE, fill_op=0.03)
# zona verde peste piciorul 2 + intrare
sx0, sx1 = SX0 + LEG2 * SGAP - 9, SX0 + ENTRY_I * SGAP + 9
rect(sx0, y_e - 6, sx1, ys(leg2_l) + 4, GREEN)
hline(FRAME_L, frame_r, y_p, BLUE, "4 3")
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)
# linia care desparte cele două zile
xd = SX0 + (K - 0.5) * SGAP
line(xd, top + 2, xd, top + PLOT_H - 2, "#bbb", dash="2 4", width=1)
text(SX0 + 3 * SGAP, top + 12, "ziua 1", fill="#999", anchor="middle", size=10)
text(SX0 + 10 * SGAP, top + 12, "ziua 2", fill="#999", anchor="middle", size=10)
# etichetele coloanei din dreapta, fiecare legată de linia ei
for y, lines_, fill in [(y_tp + 4, [f"TP1 {tp:.2f} = 1:1"], GREEN),
(y_e + 4, [f"intrare {entry:.2f}"], GREEN),
(y_p + 4, [f"pivot monthly {PIVOT:.2f}"], BLUE),
(y_sl + 4, [f"stop {sl:.2f}", f"= {risk / entry * 100:.2f} %".replace(".", ","),
"0.10 sub piciorul 2"], RED)]:
line(frame_r + 2, y - 4, COL_X - 4, y - 4, fill, width=0.8)
for j, s in enumerate(lines_):
text(COL_X, y + j * 14, s, fill=fill, size=10)
# marcaje pe cele două picioare
line(SX0 + LEG1 * SGAP, ys(leg1_l) + 4, SX0 + LEG1 * SGAP, top + PLOT_H + 8, GRAY, width=1)
text(SX0 + LEG1 * SGAP, top + PLOT_H + 20, f"piciorul 1: {leg1_l:.2f}", fill="#666",
anchor="middle", size=10)
text(SX0 + LEG1 * SGAP, top + PLOT_H + 34, "ține pivotul", fill="#666", anchor="middle", size=10)
line(SX0 + LEG2 * SGAP, ys(leg2_l) + 4, SX0 + LEG2 * SGAP, top + PLOT_H + 46, RED, width=1)
text(frame_r, top + PLOT_H + 58, f"piciorul 2: {leg2_l:.2f} — rupe pivotul și se închide", fill=RED,
anchor="end", size=10)
text(frame_r, top + PLOT_H + 72, "înapoi peste el: acolo sunt măturate stopurile", fill=RED,
anchor="end", size=10)
# săgeata de comparație a stopurilor
ya = top + PLOT_H + 92
line(BX0 - 19, ya, SX0 + ENTRY_I * SGAP, ya, RED, width=1.6, marker="pt-red")
text(20, ya + 18, f"același stop, două intrări: pe daily riști {b_risk / b_entry * 100:.2f} %, "
f"pe 60 min {risk / entry * 100:.2f} % — de {ratio:.1f} ori mai puțin,".replace(".", ","),
fill=RED, size=11, bold=True)
text(20, ya + 32, "pentru că pe TF-ul mic intri lângă minim, nu la close-ul zilei", fill=RED,
size=11, bold=True)
H = ya + 44
for a in range(len(LABELS)):
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.5 and xa0 < xb1 - 4 and xb0 < xa1 - 4), \
f"etichete suprapuse: {sa!r} / {sb!r}"
assert 0 < LABELS[a][2] < H, f"etichetă în afara înălțimii: {LABELS[a][3]!r}"
# nivelul comun apare cu același număr pe ambele panouri
v = f"{PIVOT:.2f}"
assert any(v in s and x0 < MIDX for x0, _x1, _y, s in LABELS), "pivotul lipsește de pe panoul mare"
assert any(v in s and x0 >= MIDX for x0, _x1, _y, s in LABELS), "pivotul lipsește de pe panoul mic"
# niciun preț al panoului mic pe panoul mare și invers (în afara nivelului comun)
for x0, _x1, _y, s in LABELS:
rau = [b_entry] if x0 >= MIDX else [entry, sl, tp, leg1_l, leg2_l]
for val in rau:
assert f"{val:.2f}" not in s, f"eticheta {s!r} poartă prețul {val:.2f} al celuilalt panou"
svg = (f'")
assert "\n\n" not in svg
OUT.write_text(svg, encoding="utf-8")
print(f"scris {OUT.name}: pivot {PIVOT:.2f}, picior 1 {leg1_l:.2f} / picior 2 {leg2_l:.2f}, "
f"intrare {entry:.2f}, SL {sl:.2f} (risc {risk:.2f}), TP1 {tp:.2f} la bara {first}; "
f"stop daily {b_sl:.2f} = x{ratio:.2f}")