m2d: diagramele refacute; ansamblul primeste corespondenta de TF

Ansamblul arata acum 15 min si 3 min ca descompunere reala (k=5, cu
assert), cu caseta punctata, ghidaje si nivelul comun legat prin
conector — sablonul din buy_doji_perechi_tf.

Noi, pentru pasii care nu aveau diagrama: directie (15 min ramane in Buy
vs. se inchide Sell), trigger (bara inchisa peste capat vs. atinsa doar
cu fitilul, plus al doilea semnal marcat "nu mai intri"), stop (cele 4
bare si alternativa 2xATR, cu cifrele SPY din curs), iesire (TP1 plus
bull trail, long si short, oglinda calculata nu desenata din ochi).
Secventa era din polilinii desenate de mana — rescrisa pe serie.

Stopul e fix pe extrema celor 4 bare, fara marja: asa spune cursul aici,
spre deosebire de 01_buy_doji. Diferenta trebuie spusa in text.

Textul capitolului e neatins — agentul a atins pragul de context.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
This commit is contained in:
Claude Agent
2026-09-20 12:45:14 +00:00
parent 3143e35ed9
commit 0ed607134d
13 changed files with 1610 additions and 527 deletions

View File

@@ -1,268 +1,125 @@
#!/usr/bin/env python3
"""Secvența M → A a strategiei M2D — schemă conceptuală pe două panouri.
"""M2D, pasul 2 — pullback-ul (M) pe TF mic: DA (există) vs. NU (lipsește).
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.
Pentru `summaries/17_STRATEGII/03_m2d.md`, marcajul `<!-- DIAGRAMA: secventa -->`.
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...”.
Panoul de sus e chiar graficul de 3 minute din diagrama de ansamblu, primele
13 bare — aceleași cifre în tot capitolul. Panoul de jos pornește din același
preț, dar urcă fără să se oprească: nu există pullback, deci nu există setup
M2D, oricât de frumoasă ar fi mișcarea.
Rulare: python3 gen/strategii/m2d_secventa.py -> gen/strategii/m2d_secventa.svg
Definiția pe care o susține desenul: pullback = revenire scurtă contra
direcției mari care NU rupe minimul din care a pornit mișcarea. Minimul acela
e low-ul candelei de 15 min care a dat semnalul (99.80) și e desenat pe
ambele panouri.
Rulare: python3 gen/strategii/m2d_secventa.py
"""
import sys
from pathlib import Path
from xml.sax.saxutils import escape
sys.path.insert(0, "/workspace/atm-curs-dl")
sys.path.insert(0, str(Path(__file__).resolve().parent))
from svg_candles import Arrow, Candle, Chart # noqa: E402
import m2d_duo as duo # noqa: E402
OUT = Path(__file__).with_suffix(".svg")
W, SCALE, GAP, LEFT = 680, 108.0, 32, 66
GREEN, RED, BLUE, CYAN, GREY = "#188a3e", "#c00", "#1a5fb4", "#0891b2", "#555"
W, H = 700, 780
BLACK, GRAY, RED, GREEN, BLUE, CYAN, WHITE = (
"#000000", "#777777", "#c00", "#188a3e", "#1a5fb4", "#00b8c4", "#ffffff")
PANEL_BG, PANEL_EDGE = "#fafafa", "#ddd"
REF_LOW = 99.80 # low-ul candelei de 15 min de semnal (vezi diagrama „directie")
SEMNAL = 100.60 # close-ul ei = open-ul primei bare de 3 min
# ---------------------------------------------------------------- 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)]
# --- DA: primele 13 bare din seria de 3 min a diagramei de ansamblu --------
DA = [(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: primul Buy = trigger
(100.40, 100.60, 100.35, 100.55), (100.55, 100.75, 100.50, 100.70),
(100.70, 101.00, 100.65, 100.95)]
PULL_END = 8
L_M, L_A, S_M, S_A = 2, 3, 2, 3 # indicii momentelor M și A
# --- NU: aceeași plecare, dar urcare continuă, fără pullback --------------
NU = [(100.60, 100.80, 100.55, 100.75), (100.75, 100.95, 100.70, 100.90),
(100.90, 101.10, 100.85, 101.05), (101.05, 101.20, 101.00, 101.15),
(101.15, 101.35, 101.10, 101.30), (101.30, 101.45, 101.25, 101.40),
(101.40, 101.60, 101.35, 101.55), (101.55, 101.75, 101.50, 101.70),
(101.70, 101.90, 101.65, 101.85), (101.85, 102.00, 101.80, 101.95),
(101.95, 102.15, 101.90, 102.10), (102.10, 102.25, 102.05, 102.20),
(102.20, 102.40, 102.15, 102.35)]
PANELS = {
"LONG": (14, 92, 672, 250),
"SHORT": (14, 362, 672, 270),
}
TEXTS = [] # (x, y, text, size, weight, anchor, color) pentru verificare
# ---------------------------------------------------------------- verificări
for nume, bars in (("DA", DA), ("NU", NU)):
for i, (o, h, l, c) in enumerate(bars):
assert h >= max(o, c) and l <= min(o, c), f"{nume}[{i}]: high/low incoerent"
if i:
assert abs(o - bars[i - 1][3]) < 1e-9, f"{nume}[{i}]: open != close precedent"
assert abs(bars[0][0] - SEMNAL) < 1e-9, f"{nume}: nu pornește din close-ul semnalului"
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 run_in_jos(bars):
"""Cel mai lung șir de close-uri strict descrescătoare."""
best = cur = 1
for a, b in zip(bars, bars[1:]):
cur = cur + 1 if b[3] < a[3] else 1
best = max(best, cur)
return best
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}/>')
assert run_in_jos(DA) >= 3, "DA nu are pullback (niciun șir de 3 close-uri în jos)"
assert run_in_jos(NU) < 3, "NU are totuși un pullback"
PULL_LOW = min(b[2] for b in DA[:PULL_END + 1])
assert abs(PULL_LOW - 99.90) < 1e-9, PULL_LOW
assert PULL_LOW > REF_LOW, "pullback-ul rupe minimul din care a pornit mișcarea"
assert DA[0][3] > DA[PULL_END - 1][3], "pullback-ul nu coboară"
assert DA[9][3] > max(b[1] for b in DA[6:9]), "bara 9 nu rupe în sus capătul pullback-ului"
assert min(b[3] for b in NU) > SEMNAL, "NU: o bară se închide sub prețul de plecare"
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 chart(bars, arrows):
return Chart(candles=[Candle(*b) for b in bars], arrows=arrows,
scale=SCALE, gap=GAP, body_w=11, left=LEFT, width=W, top=64)
def dot(x, y, fill, r=5.0):
return f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{r:.1f}" fill="{fill}"/>'
ch_da = chart(DA, [
Arrow(PULL_END - 1, "M: pullback-ul obligatoriu — prețul merge\n"
"contra direcției mari, dar se oprește la 99.90",
where="below", color=RED, dx=-24, dy=46),
Arrow(9, "prima bară care urcă peste capătul pullback-ului:\n"
"aici începe pasul 3 (trigger-ul)",
where="above", color=GREEN, dx=-40, dy=6)])
ch_nu = chart(NU, [
Arrow(6, "urcare continuă, fără nicio pauză în jos:\n"
"asta e breakout, nu M2D — nu ai unde pune stopul",
where="below", color=RED, dx=-70, dy=40)])
body_da, h_da = duo.panel(ch_da, W)
body_nu, h_nu = duo.panel(ch_nu, W)
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),
]
extra_da = (duo.titlu("DA — pullback pe 3 min, care nu rupe minimul de referință", color=GREEN)
+ duo.zone(ch_da, 0, PULL_END, "M: pullback", RED, pad=12, lift=26)
+ duo.level(ch_da, REF_LOW,
["minimul din care a pornit mișcarea 99.80 — sub el nu mai e pullback"],
GREY, W, below=True, dash="2 3")
+ duo.level(ch_da, PULL_LOW, ["cel mai jos punct al pullback-ului 99.90"],
RED, W, below=False, at_left=False))
extra_nu = (duo.titlu("NU — niciun pullback: nu ai setup, oricât de frumoasă e urcarea",
color=RED)
+ duo.level(ch_nu, SEMNAL,
["close-ul candelei de 15 min de semnal 100.60"], CYAN, W,
below=True, at_left=False))
# ------------------------------------------------------ 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")
svg, H, n = duo.build(
[("DA", body_da, h_da, extra_da), ("NU", body_nu, h_nu, extra_nu)],
["Ambele panouri: 3 minute, serie construită, aceleași cifre ca în diagrama de ansamblu.",
'Pullback nu înseamnă „nu mai face minim nou": înseamnă că nu rupe minimul '
'din care a pornit mișcarea.'],
W)
duo.fara_preturi_straine(svg, [REF_LOW, PULL_LOW, SEMNAL])
OUT.write_text(svg, encoding="utf-8")
print(f"scris {OUT} ({W}x{H:.0f}), {n} etichete | DA: șir în jos de {run_in_jos(DA)} bare, "
f"minim {PULL_LOW:.2f} peste referința {REF_LOW:.2f} | NU: șir în jos de "
f"{run_in_jos(NU)} bare")