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
125 lines
5.5 KiB
Python
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)}"
|