#!/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'")
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}")