Files
atm-curs-dl/gen/strategii/m2d_secventa.py
Claude Agent 01599d2e9d strategii: primele 30 de diagrame, cu generatoarele lor
Cate un agent grafician per diagrama, fiecare cu generatorul si SVG-ul lui.
Graficele de pret trec prin svg_candles, cu assert-urile din specificatie
scrise in generator — verifica structura, nu doar ca ruleaza: ca al doilea
picior e mai jos, ca intrarea e close-ul barei de confirmare, ca 1:1 e exact.

Verificare prin sondaj pe buy_doji_ansamblu, cu screenshot citit: serie
continua de 32 de candele, panou de volum, MA(20), doji-ul si ieșirea pe
trail etichetate, cifrele identice cu specificatia.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-18 19:22:13 +00:00

269 lines
13 KiB
Python

#!/usr/bin/env python3
"""Secvența M → A a strategiei M2D — schemă conceptuală pe două panouri.
Pentru `strategii/03_m2d.md`, marcajul `<!-- DIAGRAMA: secventa -->`.
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'<text x="{x:.1f}" y="{y:.1f}" font-size="{size}" '
f'font-weight="{weight}" fill="{fill}" text-anchor="{anchor}">'
f'{escape(s)}</text>')
def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None):
d = f' stroke-dasharray="{dash}"' if dash else ""
return (f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
f'stroke="{stroke}" stroke-width="{width}"{d}/>')
def poly(pts, stroke, width=2.0):
p = " ".join(f"{x:.1f},{y:.1f}" for x, y in pts)
return f'<polyline points="{p}" fill="none" stroke="{stroke}" stroke-width="{width}"/>'
def dot(x, y, fill, r=5.0):
return f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{r:.1f}" fill="{fill}"/>'
def build():
p = [
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" '
f'width="{W}" height="{H}" role="img" aria-label="Secventa M apoi A a '
f'strategiei M2D, pe doua time frame-uri, pentru long si short: time '
f'frame-ul mare ramane in directie cat timp time frame-ul mic face '
f'pullback (M) si apoi da trigger-ul in directia mare (A)">',
'<g font-family="system-ui, sans-serif">',
f'<rect x="0" y="0" width="{W}" height="{H}" fill="{WHITE}"/>',
txt(W / 2, 30, "Secvența M → A: nu e o stare, e o secvență",
19, "700", BLACK),
txt(W / 2, 52, "M2D — același moment, două time frame-uri: "
"TF mare dă direcția, TF mic dă trigger-ul",
13, "400", GRAY),
txt(W / 2, 72, "sus = LONG; jos = SHORT — aceleași momente, în oglindă",
13, "400", GRAY),
]
# ------------------------------------------------------ panouri
for name, (px, py, pw, ph) in PANELS.items():
p.append(f'<rect x="{px}" y="{py}" width="{pw}" height="{ph}" rx="8" '
f'fill="{PANEL_BG}" stroke="{PANEL_EDGE}" stroke-width="1.2"/>')
# ------------------------------------------------------ panoul LONG
p.append(txt(30, 116, "LONG", 15, "700", GREEN, "start"))
p.append(txt(30, 140, "TF mare (ex. 15 min)", 13, "400", GRAY, "start"))
p.append(txt(660, 114, "rămâne în Buy pe tot parcursul", 13, "400",
GREEN, "end"))
for x, y_top, y_bot in ((LONG_TOP[L_M][0], LONG_TOP[L_M][1], LONG_BOT[L_M][1]),
(LONG_TOP[L_A][0], LONG_TOP[L_A][1], LONG_BOT[L_A][1])):
p.append(line(x, y_top, x, y_bot, "#999999", 1.2, "4 4"))
p.append(poly(LONG_TOP, GREEN))
p.append(dot(*LONG_TOP[1], CYAN))
p.append(txt(185, 126, "Buy Momentum", 13, "400", CYAN, "start"))
p.append(txt(30, 202, "TF mic (ex. 3 min)", 13, "400", GRAY, "start"))
p.append(poly(LONG_BOT, "#333333"))
p.append(dot(*LONG_BOT[L_M], RED))
p.append(txt(262, 296, "M: Sell Momentum (pullback)", 13, "400",
RED, "start"))
p.append(dot(*LONG_BOT[L_A], GREEN, r=6))
p.append(txt(660, 240, "A: Buy Momentum — TRIGGER, intri long",
13, "400", GREEN, "end"))
# ------------------------------------------------------ panoul SHORT
p.append(txt(30, 386, "SHORT", 15, "700", RED, "start"))
p.append(txt(30, 410, "TF mare (ex. 15 min)", 13, "400", GRAY, "start"))
p.append(txt(660, 494, "rămâne în Sell pe tot parcursul", 13, "400",
RED, "end"))
for x, y_top, y_bot in ((SHORT_TOP[S_M][0], SHORT_TOP[S_M][1], SHORT_BOT[S_M][1]),
(SHORT_TOP[S_A][0], SHORT_TOP[S_A][1], SHORT_BOT[S_A][1])):
p.append(line(x, y_top, x, y_bot, "#999999", 1.2, "4 4"))
p.append(poly(SHORT_TOP, RED))
p.append(dot(*SHORT_TOP[1], RED))
p.append(txt(168, 432, "Sell Momentum", 13, "400", RED, "start"))
p.append(txt(30, 470, "TF mic (ex. 3 min)", 13, "400", GRAY, "start"))
p.append(poly(SHORT_BOT, "#333333"))
p.append(dot(*SHORT_BOT[S_M], CYAN))
p.append(txt(238, 494, "M: Buy Momentum (pullback)", 13, "400",
CYAN, "end"))
p.append(dot(*SHORT_BOT[S_A], RED, r=6))
p.append(txt(660, 610, "A: Sell Momentum — TRIGGER, intri short",
13, "400", RED, "end"))
# ------------------------------------------------------ subsol
p.append(f'<rect x="40" y="648" width="620" height="116" rx="8" '
f'fill="#ffffff" stroke="{GRAY}" stroke-width="1.2"/>')
p.append(txt(W / 2, 676,
"Secvența contează, nu starea: mare în Buy → mic în Sell → "
"mic în Buy", 13, "400", BLACK))
p.append(txt(W / 2, 696,
"(cu mare încă în Buy). Un Buy direct pe TF mic, fără Sell "
"între ele,", 13, "400", BLACK))
p.append(txt(W / 2, 716, "NU e trigger (vezi 1.3).", 13, "400", BLACK))
p.append(txt(W / 2, 746,
"Sursă: 14_strategia_1_m2d.md:30-38, 172-180; "
"09_QA/08_2026-01-20.md:65-67", 13, "400", GRAY))
p.append('</g></svg>')
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")