"""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ă ")], 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'")
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)}"