"""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']*)>(.*?)', re.S) GREY = "#555" def panel(ch, width): """Corpul SVG al unui Chart, fără , 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("")], 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''] 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'{s}') 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''] if label: out.append(f'{label}') 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'{s}'] 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''] labels, off = [], 0.0 for tag, body, h, extra in panels: parts.append(f'{"".join(extra)}{body}') 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'{c}') labels.append((24, 24 + len(c) * 6.3, cap_top + 17 * i, c, "cap")) H = cap_top + 17 * len(captions) + 6 parts.append("") 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)}"