"""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'\n ' + "\n ".join(out) + "\n") 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}")