#!/usr/bin/env python3 """M2D — tot trade-ul, pe perechea 15 min → 3 min, cu lupă. Pentru `summaries/17_STRATEGII/03_m2d.md`, marcajul ``. Tiparul e cel din `buy_doji_perechi_tf.py`: panoul din dreapta NU e o serie care vine după cea din stânga — e **descompunerea reală** a ultimelor 4 candele de 15 minute. O candelă de 15 min = k = 5 candele de 3 min, cu open, close, max(high) și min(low) care coincid, verificat bară cu bară. Sursa de adevăr e seria fină (`FINE`, 3 min). Candelele de 15 min din zona mărită se CALCULEAZĂ din ea prin agregare; barele de dinainte (`BASE`) sunt date separat și se leagă de lupă prin close == open. Toate cifrele din desen ies din serii. Regula de stop din tot capitolul: stopul stă FIX pe extrema ultimelor 4 candele de pe TF mic, fără marjă. Rulare: python3 gen/strategii/m2d_ansamblu.py """ from pathlib import Path OUT = Path(__file__).with_suffix(".svg") W = 720 K = 5 # 5 candele de 3 min într-una de 15 min NZ = 4 # câte candele de 15 min sunt mărite STOP_BARS = 4 # stopul = extrema ultimelor 4 bare de pe TF mic PULLBACK_N = 9 # barele 0..8 = pullback-ul (M) TRIG = 9 # bara de trigger (A) pe TF mic # --- seria fină (3 min), 20 de bare: (open, high, low, close) -------------- FINE = [ (100.60, 100.70, 100.40, 100.45), (100.45, 100.50, 100.25, 100.30), (100.30, 100.35, 100.15, 100.20), (100.20, 100.25, 100.05, 100.10), (100.10, 100.15, 100.00, 100.05), (100.05, 100.10, 99.95, 100.00), (100.00, 100.05, 99.92, 99.95), (99.95, 100.00, 99.90, 99.98), (99.98, 100.08, 99.94, 100.02), # 8: capătul pullback-ului (100.02, 100.45, 100.00, 100.40), # 9: A = trigger, intri (100.40, 100.60, 100.35, 100.55), (100.55, 100.75, 100.50, 100.70), (100.70, 101.00, 100.65, 100.95), # 12: TP1 atins (100.95, 101.10, 100.88, 101.05), (101.05, 101.15, 100.95, 101.00), (101.00, 101.40, 100.95, 101.35), (101.35, 101.55, 101.25, 101.45), (101.45, 101.50, 101.20, 101.25), (101.25, 101.30, 101.05, 101.10), # 18: close sub trail = ieși (101.10, 101.15, 100.90, 100.95), ] # --- barele de 15 min dinaintea lupei; ultima poartă semnalul Buy Momentum -- BASE = [ (98.00, 98.35, 97.85, 98.25), (98.25, 98.70, 98.15, 98.60), (98.60, 99.10, 98.50, 99.00), (99.00, 99.45, 98.90, 99.30), (99.30, 99.95, 99.20, 99.85), (99.85, 100.75, 99.80, 100.60), # 5: Buy Momentum pe 15 min ] SIG_B = 5 # candela de 15 min cu semnalul 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 # ---------------------------------------------------------------- verificări for name, bars in (("15 min", BIG), ("3 min", FINE)): for i, (o, h, l, c) in enumerate(bars): assert h >= max(o, c) and l <= min(o, c), f"{name}[{i}]: high/low incoerent" if i: assert eq(o, bars[i - 1][3]), f"{name}[{i}]: open != close precedent" assert len(FINE) == K * NZ, len(FINE) # (1) unde cad granițele candelelor de 15 min peste poveste — exact ce spune # textul capitolului, și poate fi fals span = len({i // K for i in range(PULLBACK_N)}) assert span == 2, f"pullback-ul se întinde peste {span} candele de 15 min, nu 2" trig_big = TRIG // K assert trig_big == 1, f"trigger-ul cade în candela mare {trig_big}" assert eq(FINE[0][0], BASE[SIG_B][3]), "open-ul primei bare fine != close-ul semnalului" # (2) pullback-ul (M): coboară, dar NU rupe low-ul candelei de 15 min din care # a pornit mișcarea — definiția ancorată din mini-glosar PULL = FINE[:PULLBACK_N] ref_low = BASE[SIG_B][2] pull_low = min(b[2] for b in PULL) assert PULL[0][3] > PULL[-1][3], "pullback-ul nu coboară" assert pull_low > ref_low, f"pullback-ul rupe low-ul de referință ({pull_low} <= {ref_low})" # (3) trigger-ul (A). Săgeata vine dintr-un indicator proprietar, deci forma # ei pe desen e construită de mine: bara de trigger e PRIMA din pullback # care se închide peste high-urile ultimelor LOOK bare. Verific că e # într-adevăr prima — altfel desenul ar contrazice regula „primul semnal". LOOK = 3 def rupe(i): return i >= LOOK and FINE[i][3] > max(b[1] for b in FINE[i - LOOK:i]) assert rupe(TRIG), "bara de trigger nu rupe high-urile barelor dinainte" assert not any(rupe(i) for i in range(LOOK, TRIG)), \ "o bară dinaintea trigger-ului rupea deja — nu mai e primul semnal" # (4) trade-ul ENTRY = FINE[TRIG][3] SL = min(b[2] for b in FINE[TRIG - STOP_BARS + 1:TRIG + 1]) RISK = round(ENTRY - SL, 2) TP1 = round(ENTRY + RISK, 2) assert eq(SL, pull_low), "stopul nu e minimul pullback-ului" assert eq(SL, min(b[2] for b in FINE)), "stopul nu e minimul zonei mărite" TP_BAR = next(i for i in range(TRIG + 1, len(FINE)) if FINE[i][1] >= TP1 - 1e-9) assert TP_BAR == 12, TP_BAR # (5) bull trail: low-ul ultimei bare care s-a închis peste high-ul precedentei; # ieșirea = prima bară care se ÎNCHIDE sub linie trail, EXIT_BAR = None, None for i in range(1, len(FINE)): if trail is not None and FINE[i][3] < trail: EXIT_BAR = i break if i > TRIG and FINE[i][3] > FINE[i - 1][1]: trail = FINE[i][2] TRAIL = trail EXIT_BAR = EXIT_BAR if EXIT_BAR is not None else len(FINE) - 1 EXIT = FINE[EXIT_BAR][3] assert (TRAIL, EXIT_BAR) == (101.25, 18), (TRAIL, EXIT_BAR) assert EXIT > ENTRY, "ieșirea pe trail e sub intrare" R = (EXIT - ENTRY) / RISK assert round(R, 1) == 1.4, R # wick-uri sub linie nu scot: bara 17 coboară sub trail, dar se închide deasupra assert FINE[17][2] < TRAIL <= FINE[17][3], "bara 17 nu mai ilustrează wick sub trail" LEVEL = BASE[SIG_B][3] # nivelul comun celor două panouri assert eq(LEVEL, FINE[0][0]) # ---------------------------------------------------------------- desen GREEN, RED, INK, BLUE, GRAY = "#188a3e", "#c00", "#333", "#1a5fb4", "#888" LINK, CYAN = "#aaa", "#0891b2" PLOT_H = 168 BX0, BGAP, BW = 104, 15, 9 SX0, SGAP, SW = 372, 11, 7 FRAME_L = SX0 - 8 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, bw): for i, (o, h, l, c) in enumerate(bars): cx = x0 + i * gap color = GREEN if c >= o else RED 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 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 + 16 else y_prev + 16 if y_lab - y > 2: out.append(f'') else: line(x_line + 2, y - 4, COL_X - 4, y_lab - 4, fill, width=0.8) for j, s in enumerate(lines): text(COL_X, y_lab + j * 14, s, fill=fill, size=10) y_prev = y_lab + (len(lines) - 1) * 14 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, 24, "M2D long, tot trade-ul: 15 min dă direcția, 3 min dă intrarea", bold=True, size=14) text(20, 42, "Graficul din dreapta nu e altă bucată de piață: e aceeași, mărită. " "O candelă de 15 min = 5 candele de 3 min.", fill="#555", size=11) top = 80 text(20, top - 12, "15 min: direcția", fill="#555", size=11) text(FRAME_L, top - 12, f"3 min: descompunerea ultimelor {NZ} candele de 15 min", 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 de 15 min ------------------------------------------------------ sx_a, sx_b = BX0 + SIG_B * BGAP - 8, BX0 + SIG_B * BGAP + 8 rect(sx_a, yb(BASE[SIG_B][1]) - 5, sx_b, yb(BASE[SIG_B][2]) + 5, CYAN) line(BX0 + SIG_B * BGAP, yb(BASE[SIG_B][2]) + 6, BX0 + SIG_B * BGAP, top + PLOT_H + 12, CYAN, width=1) text(BX0 + SIG_B * BGAP - 30, top + PLOT_H + 26, "Buy Momentum pe 15 min:", fill=CYAN, anchor="middle", bold=True, size=11) text(BX0 + SIG_B * BGAP - 30, top + PLOT_H + 39, "de aici ai voie doar long", fill=CYAN, anchor="middle", size=11) # caseta de lupă peste exact candelele de 15 min care se sparg în dreapta lx0, lx1 = BX0 + len(BASE) * BGAP - 7, BX0 + (len(BIG) - 1) * BGAP + 8 ly0, ly1 = yb(hi), yb(lo) rect(lx0, ly0, lx1, ly1, BLUE, fill_op=0.06, dash="4 3") # minimul de referință al pullback-ului: trăiește pe TF-ul mare (pe panoul mic # e sub scară), deci se marchează aici și se trimite acolo din etichetă hline(BX0 - 14, lx1, yb(ref_low), "#555", "2 3", width=1.2) text(24, yb(ref_low) + 12, "minimul din care a pornit", fill="#555", size=10) text(24, yb(ref_low) + 26, f"mișcarea: {ref_low:.2f}", fill="#555", size=10) hline(BX0 - 14, lx1, yb(LEVEL), LINK, "3 3", width=1.0) candles(BIG, BX0, BGAP, yb, BW) # ---- 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) # ---- nivelul comun: conector între panouri, numărul scris o singură dată --- ycb, ycs = yb(LEVEL), ys(LEVEL) 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) dmin = clearance(mid) assert dmin > 10, f"conectorul trece la {dmin:.1f} px de un ghidaj" out.append(f'') tx = min(mid + 5, FRAME_L - 60) text(tx, (ycb + ycs) / 2 + 4, f"{LEVEL:.2f}", fill="#777", size=10) text(tx, (ycb + ycs) / 2 + 17, "același preț", fill="#999", size=10) # ---- panoul de 3 min ------------------------------------------------------- y_e, y_sl, y_tp, y_tr = ys(ENTRY), ys(SL), ys(TP1), ys(TRAIL) 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 + 8 rect(FRAME_L, top, frame_r, top + PLOT_H, BLUE, fill_op=0.03) # zona de evidențiere peste pullback + trigger: acolo se decide intrarea zx0 = SX0 - 7 zx1 = SX0 + TRIG * SGAP + 7 rect(zx0, ys(max(b[1] for b in FINE[:TRIG + 1])) - 5, zx1, ys(SL) + 5, GREEN) hline(FRAME_L, frame_r, ycs, LINK, "3 3", width=1.0) hline(FRAME_L, frame_r, y_tp, GREEN, "2 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_tr, "#b06000", "2 2", width=1.2) candles(FINE, SX0, SGAP, ys, SW) stack([(y_tp + 4, [f"TP1 {TP1:.2f} = 1:1", "iese prima tranșă"], GREEN), (y_tr + 4, [f"bull trail {TRAIL:.2f}", "urcă, nu coboară"], "#b06000"), (y_e + 4, [f"intrare {ENTRY:.2f}", "close-ul trigger-ului"], GREEN), (y_sl + 4, [f"stop {SL:.2f}", f"minimul a {STOP_BARS} bare"], RED)], frame_r) # paranteza pullback-ului, sub cadru pxa, pxb = SX0 - 5, SX0 + (PULLBACK_N - 1) * SGAP + 5 out.append(f'') text((pxa + pxb) / 2, top + PLOT_H + 22, f"M: pullback, {PULLBACK_N} bare", fill=RED, anchor="middle", bold=True, size=11) text((pxa + pxb) / 2, top + PLOT_H + 36, "fără să rupă minimul", fill=RED, anchor="middle", size=11) # ieșirea: săgeată în sus spre bara 18, cu explicația sub cadru, la dreapta exx = SX0 + EXIT_BAR * SGAP line(exx, top + PLOT_H + 14, exx, ys(FINE[EXIT_BAR][2]) + 5, "#b06000", dash="3 3", width=1.1, marker="pt-o") text(frame_r, top + PLOT_H + 22, "close sub trail", fill="#b06000", anchor="end", bold=True, size=11) text(frame_r, top + PLOT_H + 36, f"ieșire {EXIT:.2f} = {R:.1f}R", fill="#b06000", anchor="end", size=11) # trigger-ul: săgeată în sus spre bara 9, cu explicația dedesubt tgx = SX0 + TRIG * SGAP line(tgx, top + PLOT_H + 50, tgx, ys(FINE[TRIG][2]) + 5, GREEN, width=1.6, marker="pt") text(pxa, top + PLOT_H + 58, "A: primul Buy după pullback — aici intri,", fill=GREEN, bold=True, size=11) text(pxa, top + PLOT_H + 72, "pentru că 15 min e încă în Buy.", fill=GREEN, size=11) text(pxa, top + PLOT_H + 86, "Wick-urile nu contează: se citește close-ul.", fill="#555", size=11) LAB_N = len(LABELS) # etichetele de pe panouri; ce urmează e subsol H = top + PLOT_H + 108 text(20, H - 6, f"Serie construită, fără ticker. Risc = {ENTRY:.2f} − {SL:.2f} = {RISK:.2f}; " f"TP1 la aceeași distanță în sus; ieșirea pe trail la {R:.1f}R.", fill="#555", size=11) H += 12 # ---- prețul fiecărui panou rămâne pe panoul lui --------------------------- COMUNE = {round(LEVEL, 2)} MARE = {"ref_low": ref_low} MIC = {k_: v for k_, v in (("entry", ENTRY), ("sl", SL), ("tp", TP1), ("trail", TRAIL)) if round(v, 2) not in COMUNE} for x0_, _x1, _y, s_ in LABELS[:LAB_N]: rau = MARE if x0_ >= MIDX else MIC for kk, v in rau.items(): assert f"{v:.2f}" not in s_, \ f"eticheta {s_!r} de pe panoul {'mic' if x0_ >= MIDX else 'mare'} " \ f"conține prețul {kk}={v:.2f} al celuilalt panou" cx_ = [x0_ for x0_, _x1, _y, s_ in LABELS[:LAB_N] if f"{LEVEL:.2f}" in s_] assert len(cx_) == 1 and lx1 < cx_[0] < FRAME_L, \ f"{LEVEL:.2f} nu e scris exact o dată, în cotul conectorului ({cx_})" for nume, v in (("intrarea", ENTRY), ("stopul", SL), ("TP1", TP1), ("trail-ul", TRAIL)): assert any(f"{v:.2f}" in s_ and x0_ >= MIDX for x0_, _x1, _y, s_ in LABELS[:LAB_N]), \ f"{nume} nu apare pe panoul mic" for a in range(len(LABELS)): assert 0 < LABELS[a][2] < H, f"etichetă în afara înălțimii: {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'\n ' + "\n ".join(out) + "\n") assert "\n\n" not in svg assert ".md:" not in svg, "trimiteri la surse în SVG" OUT.write_text(svg, encoding="utf-8") print(f"scris {OUT} ({W}x{H:.0f}) | 15 min: {len(BIG)} candele, lupă pe ultimele {NZ} " f"= {len(FINE)} bare de 3 min | nivel comun {LEVEL:.2f} | " f"intrare {ENTRY:.2f} SL {SL:.2f} risc {RISK:.2f} TP1 {TP1:.2f} la bara {TP_BAR} | " f"trail {TRAIL:.2f}, ieșire bara {EXIT_BAR} la {EXIT:.2f} = {R:.2f}R | " f"pullback peste {span} candele de 15 min, trigger în candela mare {trig_big}")