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:
124
gen/strategii/m2d_duo.py
Normal file
124
gen/strategii/m2d_duo.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""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)}"
|
||||
Reference in New Issue
Block a user