Files
atm-curs-dl/gen/strategii/m2d_duo.py
Claude Agent 0ed607134d 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
2026-09-20 12:45:14 +00:00

125 lines
5.5 KiB
Python

"""Ajutor comun pentru diagramele capitolului M2D cu două panouri (DA / NU).
Tiparul e cel din `buy_doji_fereastra.py`: două `Chart`-uri desenate cu
`_render()` și puse unul sub altul într-un singur SVG, plus verificarea că
nicio etichetă nu iese din pânză și că nicio pereche de etichete de pe
ACELAȘI panou nu se suprapune. E scos aici ca să nu fie copiat în patru
generatoare; nu are alt rol.
Nu se rulează singur: îl importă `m2d_directie.py`, `m2d_secventa.py`,
`m2d_trigger.py`, `m2d_stop.py`, `m2d_iesire.py`.
"""
import re
TEXT = re.compile(r'<text x="([-\d.]+)" y="([-\d.]+)"([^>]*)>(.*?)</text>', re.S)
GREY = "#555"
def panel(ch, width):
"""Corpul SVG al unui Chart, fără <svg>, plus înălțimea lui.
`_render()`, nu `render()`: al doilea restaurează top/left după desen,
iar liniile desenate de mână ar cădea deplasate față de corp.
"""
svg = ch._render()
m = re.search(r'viewBox="0 0 ([\d.]+) ([\d.]+)"', svg)
assert m, "viewBox lipsă"
assert float(m.group(1)) == width, f"textul unei săgeți iese din {width}px"
return svg[svg.index(">") + 1:svg.rindex("</svg>")], float(m.group(2))
def level(ch, price, lines, color, width, below=False, at_left=True, dash="4 3"):
"""Linie punctată pe toată lățimea + etichetă, desenată de mână.
Eticheta din `Level` stă lipită de linie și cade peste candelele care o
traversează; aici o pun în marginea liberă a panoului.
"""
y = ch._y(price)
out = [f'<line x1="24" y1="{y:.1f}" x2="{width - 14}" y2="{y:.1f}" stroke="{color}" '
f'stroke-width="1.5" stroke-dasharray="{dash}"/>']
for k, s in enumerate(lines):
ty = (y + 21 + 14 * k) if below else (y - 6 - 14 * (len(lines) - 1 - k))
tx, anchor = (30, "start") if at_left else (width - 24, "end")
out.append(f'<text x="{tx}" y="{ty:.1f}" fill="{color}" text-anchor="{anchor}" '
f'style="font-weight:bold">{s}</text>')
return out
def zone(ch, i0, i1, label, color, lift=0.0, pad=16):
"""Dreptunghi de evidențiere peste candelele i0..i1, cu eticheta deasupra."""
cds = ch.candles[i0:i1 + 1]
x0, x1 = ch._cx(i0) - pad, ch._cx(i1) + pad
y0 = ch._y(max(c.h for c in cds)) - 8 - lift
y1 = ch._y(min(c.l for c in cds)) + 8
out = [f'<rect x="{x0:.1f}" y="{y0:.1f}" width="{x1 - x0:.1f}" height="{y1 - y0:.1f}" '
f'fill="{color}" fill-opacity="0.12" stroke="{color}" stroke-width="1"/>']
if label:
out.append(f'<text x="{x0 + 6:.1f}" y="{y0 - 7:.1f}" fill="{color}" '
f'style="font-weight:bold">{label}</text>')
return out
def titlu(s, y=22, color="#333", x=24):
"""Titlul panoului, desenat de mână.
`Chart(title=...)` îl pune la `top - 8`, adică la 7 px de eticheta unei
zone desenate la `y(pmax) - 15` — se calcă mereu, indiferent de `top`.
"""
return [f'<text x="{x}" y="{y}" fill="{color}" style="font-weight:bold">{s}</text>']
def _texts(body, y_off, tag):
out = []
for m in TEXT.finditer(body):
x, y = float(m.group(1)), float(m.group(2)) + y_off
attrs, s = m.group(3), m.group(4)
anchor = ("end" if 'text-anchor="end"' in attrs
else "middle" if 'text-anchor="middle"' in attrs else "start")
w = len(s) * 12 * (0.62 if "bold" in attrs else 0.52)
left = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x)
out.append((left, left + w, y, s, tag))
return out
def build(panels, captions, width, vgap=0):
"""`panels` = listă de (eticheta panoului, corp SVG, înălțime, extra).
Întoarce SVG-ul întreg, după ce verifică etichetele.
"""
parts = [f'<svg viewBox="0 0 {width} {{H}}" xmlns="http://www.w3.org/2000/svg" '
f'style="width:100%;height:auto;font:12px sans-serif">']
labels, off = [], 0.0
for tag, body, h, extra in panels:
parts.append(f'<g transform="translate(0,{off:.0f})">{"".join(extra)}{body}</g>')
labels += _texts(body + "".join(extra), off, tag)
off += h + vgap
cap_top = off + 6
for i, c in enumerate(captions):
assert "<" not in c
parts.append(f'<text x="24" y="{cap_top + 17 * i:.0f}" fill="{GREY}">{c}</text>')
labels.append((24, 24 + len(c) * 6.3, cap_top + 17 * i, c, "cap"))
H = cap_top + 17 * len(captions) + 6
parts.append("</svg>")
svg = "\n ".join(parts).replace("{H}", f"{H:.0f}")
assert "\n\n" not in svg, "linie goală în SVG"
assert ".md:" not in svg, "trimitere fișier:linie în SVG"
for left, right, y, s, tag in labels:
assert left >= -2 and right <= width + 2, f"[{tag}] text iese din pânză: {s!r}"
assert 0 <= y <= H, f"[{tag}] text în afara înălțimii: {s!r} (y={y:.0f})"
for i in range(len(labels)):
li, ri, yi, si, ti = labels[i]
for j in range(i + 1, len(labels)):
lj, rj, yj, sj, tj = labels[j]
if ti == tj and abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
raise AssertionError(f"[{ti}] texte suprapuse: {si!r} / {sj!r}")
return svg, H, len(labels)
def fara_preturi_straine(svg, permise):
"""Nicio etichetă nu numește un `\\d+\\.\\d\\d` care nu e un nivel desenat."""
gasite = {float(x) for x in re.findall(r"\d+\.\d\d", " ".join(
m.group(4) for m in TEXT.finditer(svg)))}
strain = {g for g in gasite if not any(abs(g - p) < 0.005 for p in permise)}
assert not strain, f"prețuri în etichete care nu sunt niveluri desenate: {sorted(strain)}"