Nivelurile 1-2-3, cei 6 pasi cu diagrama fiecare, mini-glosar de 33 de termeni, oglinda pe short cu diagrama ei, checklist. Sursele in fise/07_mdsrt_surse.md. Capitolul avea TREI reguli numerice diferite in acelasi loc: stop fix pe low in ansamblu, 0.10 sub low in aplus si exemplu, plus doua reguli de intrare. Acum mdsrt_ansamblu.py tine seria si regulile, iar picior, trigger, stop, iesire si short le importa — nu mai pot diverge. Noi: zona (daily -> 60 min ca descompunere reala, 2 candele daily = 14 bare, cu caseta, ghidaje si pivotul pe ambele panouri), picior, trigger, stop, iesire, short (seria long rasturnata, deci fiecare cifra are perechea ei). Verificare numerica text-diagrama: trei nepotriviri reparate, inclusiv o cifra din proza care nu exista pe niciun desen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
314 lines
15 KiB
Python
314 lines
15 KiB
Python
"""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'<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}" '
|
|
f'stroke="{color}" 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 ""
|
|
m = 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}{m}/>')
|
|
|
|
|
|
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}" '
|
|
f'height="{y1 - y0_:.1f}" fill="{color}" fill-opacity="{fill_op}" '
|
|
f'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_))
|
|
hh = max(yf(min(o_, c_)) - top_, 1.6)
|
|
out.append(f'<rect x="{cx - BODY_W / 2:.1f}" y="{top_:.1f}" width="{BODY_W}" '
|
|
f'height="{hh:.1f}" fill="{GREEN if c_ >= o_ else RED}"/>')
|
|
|
|
|
|
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('<defs><marker id="pt-red" 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 — 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'<path d="M{lx1:.1f},{ycb:.1f} H{mid:.1f} V{ycs:.1f} H{FRAME_L:.1f}" '
|
|
f'fill="none" stroke={LINK!r} stroke-width="1.0" stroke-dasharray="3 3"/>')
|
|
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'<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>")
|
|
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}")
|