#!/usr/bin/env python3
"""Secvența M → A a strategiei M2D — schemă conceptuală pe două panouri.
Pentru `strategii/03_m2d.md`, marcajul ``.
Nu e grafic de preț (spec: „schemă pe două panouri, nu candele de trade”):
SVG scris de mână, doar stdlib — nu se folosește svg_candles / pchart.
Ce arată (toate etichetele vin din spec; nicio cifră de preț nu se inventează):
- panoul de sus, LONG: pe TF mare (15 min) polilinia urcă și „rămâne în Buy pe
tot parcursul”; pe TF mic (3 min) urcă → pullback (M: Sell Momentum) → urcă
peste vârful anterior (A: Buy Momentum — TRIGGER, intri long);
- panoul de jos, SHORT: oglindit — TF mare coboară („rămâne în Sell”), TF mic
coboară → pullback în sus (M: Buy Momentum) → coboară sub minimul anterior
(A: Sell Momentum — TRIGGER, intri short);
- două linii verticale punctate leagă momentul M și momentul A de rândul de sus
(verificarea că TF mare rămâne în aceeași direcție);
- textul de subsol: „Secvența contează, nu starea...”.
Rulare: python3 gen/strategii/m2d_secventa.py -> gen/strategii/m2d_secventa.svg
"""
from pathlib import Path
from xml.sax.saxutils import escape
OUT = Path(__file__).with_suffix(".svg")
W, H = 700, 780
BLACK, GRAY, RED, GREEN, BLUE, CYAN, WHITE = (
"#000000", "#777777", "#c00", "#188a3e", "#1a5fb4", "#00b8c4", "#ffffff")
PANEL_BG, PANEL_EDGE = "#fafafa", "#ddd"
# ---------------------------------------------------------------- geometrie
# Fiecare polilinie: listă de (x, y). y mic = sus.
# Index: 0 = start, 1 = vârf/trough anterior, 2 = M, 3 = A, 4 = continuare.
LONG_TOP = [(40, 162), (150, 148), (250, 138), (430, 128), (660, 118)]
LONG_BOT = [(40, 255), (180, 225), (250, 282), (430, 210), (660, 198)]
SHORT_TOP = [(40, 430), (150, 444), (250, 454), (430, 464), (660, 474)]
SHORT_BOT = [(40, 527), (180, 562), (250, 500), (430, 574), (660, 588)]
L_M, L_A, S_M, S_A = 2, 3, 2, 3 # indicii momentelor M și A
PANELS = {
"LONG": (14, 92, 672, 250),
"SHORT": (14, 362, 672, 270),
}
TEXTS = [] # (x, y, text, size, weight, anchor, color) pentru verificare
def txt(x, y, s, size=13, weight="400", fill=BLACK, anchor="middle"):
TEXTS.append((x, y, s, size, weight, anchor, fill))
return (f''
f'{escape(s)}')
def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None):
d = f' stroke-dasharray="{dash}"' if dash else ""
return (f'')
def poly(pts, stroke, width=2.0):
p = " ".join(f"{x:.1f},{y:.1f}" for x, y in pts)
return f''
def dot(x, y, fill, r=5.0):
return f''
def build():
p = [
f'')
return "\n".join(p) + "\n"
# ---------------------------------------------------------------- verificare
def est_w(s, size, weight):
factor = 0.62 if weight == "700" else 0.56
return len(s) * size * factor
def verify(svg):
# 1. geometria: panourile nu se suprapun, sunt în panză
long_ = PANELS["LONG"]
short_ = PANELS["SHORT"]
assert long_[1] + long_[3] < short_[1], "panourile LONG/SHORT se suprapun"
assert short_[1] + short_[3] < 648, "panoul SHORT intră în subsol"
for _, (px, py, pw, ph) in PANELS.items():
assert px >= 0 and py >= 0 and px + pw <= W and py + ph <= H
# 2. secvența LONG: TF mare rămâne în Buy (urcă monoton: y strict descrescător)
ys = [y for _, y in LONG_TOP]
assert all(ys[i] > ys[i + 1] for i in range(len(ys) - 1)), \
"LONG: TF mare nu urcă monoton (nu rămâne în Buy)"
# M = pullback (minim local), A = reluare peste vârful anterior
assert LONG_BOT[L_M][1] > LONG_BOT[L_M - 1][1] and \
LONG_BOT[L_M][1] > LONG_BOT[L_M + 1][1], "LONG: M nu e un minim local"
assert LONG_BOT[L_A][1] < LONG_BOT[1][1], \
"LONG: A nu urcă peste vârful anterior (pullback-ul)"
assert LONG_TOP[L_M][0] < LONG_TOP[L_A][0], "LONG: M trebuie înainte de A"
assert LONG_TOP[L_M][0] == LONG_BOT[L_M][0] and \
LONG_TOP[L_A][0] == LONG_BOT[L_A][0], \
"LONG: liniile verticale nu leagă exact M și A de rândul de sus"
# 3. secvența SHORT: oglindit
ys = [y for _, y in SHORT_TOP]
assert all(ys[i] < ys[i + 1] for i in range(len(ys) - 1)), \
"SHORT: TF mare nu coboară monoton (nu rămâne în Sell)"
assert SHORT_BOT[S_M][1] < SHORT_BOT[S_M - 1][1] and \
SHORT_BOT[S_M][1] < SHORT_BOT[S_M + 1][1], "SHORT: M nu e un maxim local"
assert SHORT_BOT[S_A][1] > SHORT_BOT[1][1], \
"SHORT: A nu coboară sub minimul anterior"
assert SHORT_TOP[S_M][0] < SHORT_TOP[S_A][0], "SHORT: M trebuie înainte de A"
assert SHORT_TOP[S_M][0] == SHORT_BOT[S_M][0] and \
SHORT_TOP[S_A][0] == SHORT_BOT[S_A][0], \
"SHORT: liniile verticale nu leagă exact M și A de rândul de sus"
# 4. verticalele chiar traversează de la rândul de sus până la moment
assert LONG_TOP[2][1] < LONG_BOT[L_M][1]
assert LONG_TOP[3][1] < LONG_BOT[L_A][1]
assert SHORT_TOP[2][1] < SHORT_BOT[S_M][1]
assert SHORT_TOP[3][1] < SHORT_BOT[S_A][1]
# 5. toate etichetele în panză
for x, y, s, size, weight, anchor, _ in TEXTS:
ew = est_w(s, size, weight)
left = x - ew if anchor == "end" else (x - ew / 2 if anchor == "middle" else x)
right = left + ew
assert -2 <= left and right <= W + 2, f"text în afara panzei: {s!r}"
assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y})"
# 6. etichete care nu se suprapun
for i in range(len(TEXTS)):
xi, yi, si, szi, wi, ai, _ = TEXTS[i]
we = est_w(si, szi, wi)
li = xi - we if ai == "end" else (xi - we / 2 if ai == "middle" else xi)
ri = li + we
for j in range(i + 1, len(TEXTS)):
xj, yj, sj, szj, wj, aj, _ = TEXTS[j]
wej = est_w(sj, szj, wj)
lj = xj - wej if aj == "end" else (xj - wej / 2 if aj == "middle" else xj)
rj = lj + wej
if abs(yi - yj) < 13 and li < rj - 4 and lj < ri - 4:
raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}")
# 7. niciun text nu se tastează peste o polilinie (coliziune text-linie)
segments = [(LONG_TOP, PANELS["LONG"]), (LONG_BOT, PANELS["LONG"]),
(SHORT_TOP, PANELS["SHORT"]), (SHORT_BOT, PANELS["SHORT"])]
for x, y, s, size, weight, anchor, _ in TEXTS:
ew = est_w(s, size, weight)
left = x - ew if anchor == "end" else (x - ew / 2 if anchor == "middle" else x)
right = left + ew
top, bot = y - size + 1, y
for pts, _ in segments:
for (x1, y1), (x2, y2) in zip(pts, pts[1:]):
xa, xb = min(x1, x2), max(x1, x2)
if xb < left - 2 or xa > right + 2:
continue
for xc in (max(xa, left), min(xb, right)):
t = 0 if x2 == x1 else (xc - x1) / (x2 - x1)
yc = y1 + t * (y2 - y1)
if top - 1 <= yc <= bot + 1:
raise AssertionError(
f"text peste polilinie: {s!r} (y={y}, linia y={yc:.0f})")
# 8. etichetele obligatorii din spec sunt prezente
for cheie in ("LONG", "SHORT",
"TF mare (ex. 15 min)", "TF mic (ex. 3 min)",
"rămâne în Buy pe tot parcursul",
"rămâne în Sell pe tot parcursul",
"Buy Momentum", "Sell Momentum",
"M: Sell Momentum (pullback)",
"A: Buy Momentum — TRIGGER, intri long",
"M: Buy Momentum (pullback)",
"A: Sell Momentum — TRIGGER, intri short",
"Secvența contează, nu starea",
"NU e trigger (vezi 1.3)"):
assert cheie in svg, f"lipsește din desen: {cheie!r}"
# 9. fără cifre de preț inventate (spec: diagramă fără niveluri)
for interzis in ("$", "%", "€", "intrare ", "SL ", "TP"):
assert interzis not in svg, f"nu ar trebui să apară preț: {interzis!r}"
return True
SVG = build()
assert verify(SVG), "verificare eșuată"
OUT.write_text(SVG, encoding="utf-8")
print(f"scris {OUT} ({W}x{H}), verificare OK: {len(TEXTS)} etichete, "
f"{len(LONG_TOP) + len(LONG_BOT) + len(SHORT_TOP) + len(SHORT_BOT)} puncte")