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
269 lines
13 KiB
Python
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")
|