#!/usr/bin/env python3 """MP² — crossing vs. buy the dip: două panouri una sub alta. Citește `gen/strategii/mp2_crossing.spec.txt` și scrie `gen/strategii/mp2_crossing.svg`. Aceleași două linii de pivot pe ambele panouri: - pivot weekly (cel MARE) = 62.00, orizontal; - pivot daily (cel MIC), stepped — se recalculează zilnic și „sare" pe nivel nou. Sus, „DA — crossing (+2)": pivotul daily urcă din 61.70 și traversează 62.00 în sus (marcat cu cerc: „crossing: 61.70 → 62.20"). Trigger = prima candelă verde după crossing care închide peste pivotul daily (bara 11, close 62.40). Jos, „NU — continuare/buy the dip (+1)": pivotul daily e deja deasupra lui 62.00 și rămâne acolo; prețul face închideri peste el, dar nimic nu s-a re-încrucișat, deci semnalele ulterioare sunt buy the dip, nu crossing. Cifrele (62.00 / 61.70 / 62.20) sunt din `summaries/09_QA/09_2025-12-09.md:69-83` și distincția crossing vs. buy the dip din `summaries/09_QA/16_2025-10-07.md:35-43`. Seriile de preț sunt construite (open = close-ul barei anterioare) ca nivelurile afirmate să iasă exact din ele. Rulare: python3 gen/strategii/mp2_crossing.py """ import re import sys from pathlib import Path sys.path.insert(0, "/workspace/atm-curs-dl") from svg_candles import Chart, Level, Mark, from_closes # noqa: E402 OUT = Path(__file__).with_suffix(".svg") W = 480 VGAP = 44 SCALE = 95.0 # px / unitate, aceeași pe ambele panouri WEEKLY = 62.00 GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000" # ---------------------------------------------------------------- panoul DA # pivotul daily: 61.70 (sub 62.00) până la bara 9, apoi 62.20 (peste 62.00) PDA = [61.70] * 10 + [62.20] * 12 CLOSES_DA = [61.30, 61.15, 61.35, 61.20, 61.45, 61.30, 61.55, 61.40, 61.60, 61.75, # 0-9: sub pivot 61.70/62.00 62.05, # 10: pivotul sare la 62.20; close inca sub el 62.40, # 11: trigger — verde, close peste 62.20 62.55, 62.50, 62.65, 62.60, 62.75, 62.70, 62.85, 62.80, 62.90, 62.95] # 12-21: urca peste pivot WICKS_DA = [(0.08, 0.10)] * len(CLOSES_DA) WICKS_DA[10] = (0.16, 0.10) # bara crossing-ului WICKS_DA[11] = (0.22, 0.10) # bara de trigger assert len(CLOSES_DA) == len(PDA) == 22 # ---------------------------------------------------------------- panoul NU # pivotul daily e deja deasupra lui 62.00 pe toată durata (62.30 → 62.20) PNU = [62.30] * 9 + [62.20] * 13 CLOSES_NU = [61.55, 61.70, 61.90, 61.80, 62.05, 62.35, # 5: primul close peste pivotul daily 62.25, 62.45, 62.55, 62.40, 62.60, 62.50, 62.70, 62.65, 62.80, 62.75, 62.90, 62.85, 63.00, 62.95, 63.10, 63.05] WICKS_NU = [(0.08, 0.10)] * len(CLOSES_NU) assert len(CLOSES_NU) == len(PNU) == 22 START_DA = CLOSES_DA[0] + 0.12 START_NU = CLOSES_NU[0] + 0.12 cds_da = from_closes(CLOSES_DA, wicks=WICKS_DA, start=START_DA) cds_nu = from_closes(CLOSES_NU, wicks=WICKS_NU, start=START_NU) # ------------------------------------------------------ assert-urile din spec # 1. DA: pivotul daily e < 62.00 înainte de crossing și > 62.00 după cross_da = next(i for i in range(1, len(PDA)) if PDA[i] > WEEKLY) assert cross_da == 10, f"crossing-ul DA e la bara {cross_da}, nu 10" assert all(v < WEEKLY for v in PDA[:cross_da]), "pivot daily DA nu e sub 62.00 înainte de crossing" assert PDA[cross_da - 1] == 61.70 and PDA[cross_da] == 62.20, \ f"crossing DA {PDA[cross_da - 1]} -> {PDA[cross_da]}, nu 61.70 -> 62.20" assert all(v > WEEKLY for v in PDA[cross_da:]), "pivot daily DA nu e peste 62.00 după crossing" # 2. DA: trigger = prima candelă verde după crossing care închide peste pivotul daily trigger = next(i for i in range(cross_da, len(cds_da)) if cds_da[i].c >= cds_da[i].o and cds_da[i].c > PDA[i]) assert trigger == 11, f"trigger-ul DA e la bara {trigger}, nu 11" assert cds_da[trigger].c > PDA[trigger], "candela de trigger nu închide peste pivotul daily" assert cds_da[10].c <= PDA[10], "bara crossing-ului nu trebuie să închidă peste pivot" entry = cds_da[trigger].c # 3. NU: toate valorile pivot_daily > 62.00 — nimic re-încrucișat assert all(v > WEEKLY for v in PNU), "un pivot daily NU e sub 62.00" assert not any(v < WEEKLY for v in PNU), "există pivot daily NU sub 62.00 => ar fi crossing" assert min(PNU) > WEEKLY and max(PNU) > WEEKLY # prețul face închideri peste pivotul daily, dar pivotul nu cade sub 62.00 prim_nu = next(i for i in range(len(cds_nu)) if cds_nu[i].c > PNU[i]) assert prim_nu == 5 and cds_nu[prim_nu].c > PNU[prim_nu], "NU nu face close peste pivotul daily" assert min(PNU) == 62.20 and max(PNU) == 62.30, "pivotii NU nu sunt 62.20 / 62.30" # 4. liniile sunt „stepped" (au cel puțin un salt), nu drepte assert len(set(PDA)) >= 2 and len(set(PNU)) >= 2, "liniile de pivot nu sunt stepped" # --------------------------------------------------- desen: câte un panou def step_points(ch: Chart, vals: list[float]) -> str: """Polilinie în trepte: orizontal pe palier, salt vertical la schimbare.""" pts: list[tuple[float, float]] = [] for i, v in enumerate(vals): x, y = ch._cx(i), ch._y(v) if i == 0: pts.append((x, y)) elif v != vals[i - 1]: pts.append((x, ch._y(vals[i - 1]))) pts.append((x, y)) else: pts.append((x, y)) return " ".join(f"{x:.1f},{y:.1f}" for x, y in pts) def render_pivot_panel(cds, pivots, title) -> tuple[str, float, Chart]: """Randază un panou cu pivot weekly (Level) + pivot daily (polilinie injectată).""" ch = Chart(candles=cds, levels=[Level(WEEKLY, f"pivot weekly (cel MARE) {WEEKLY:.2f}", BLUE, dash="", label_at="right")], marks=[], scale=SCALE, gap=15, body_w=9, left=44, width=W, title=title) svg = ch.render() # fixează _pmax/_y și adaugă +22 pentru titlu inject = [ f'', f'pivot daily (cel MIC)', ] i = svg.index(">") + 1 svg = svg[:i] + "\n " + "\n ".join(inject) + svg[i:] m = re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', svg) body = svg[svg.index(">") + 1:svg.rindex("")] return body, float(m.group(2)), ch body_da, h_da, ch_da = render_pivot_panel( cds_da, PDA, "DA — crossing (+2)") # marcajul crossing-ului: cerc pe saltul pivotului + eticheta din spec xc = ch_da._cx(cross_da) yc = (ch_da._y(PDA[cross_da - 1]) + ch_da._y(PDA[cross_da])) / 2 circle = (f'') cross_txt = (f'crossing: 61.70 → 62.20') mark_txt = (f'' f'intrare: trigger') i = body_da.rindex("") + len("") # după candele/marks, ca să fie vizibile body_da = body_da[:i] + "\n " + circle + "\n " + cross_txt + "\n " + mark_txt + body_da[i:] # panoul NU: eticheta din spec pe semnalele ulterioare body_nu, h_nu, ch_nu = render_pivot_panel( cds_nu, PNU, "NU — continuare/buy the dip (+1)") body_nu = body_nu.rstrip() + ( f'\n ' f'fără crossing nou; nu a mai căzut sub pivotul mare' f'\n ' f'semnale ulterioare = BUY THE DIP, nu crossing') # ------------------------------------------------- lipirea celor două panouri CAPS = [ "Pivot weekly 62.00 = linia orizontală (pivotul MARE).", "Pivot daily (cel MIC) se recalculează zilnic și sare pe nivel nou.", "Trigger = prima candelă verde după crossing,", "care închide peste pivotul daily.", "Crossing = pivotul mic traversează 62.00 în sus; fără re-încrucișare,", "semnalele ulterioare sunt buy the dip, nu crossing.", ] cap_top = h_da + VGAP + h_nu + 16 H = cap_top + 17 * len(CAPS) + 10 parts = [ f'', f'{body_da}', f'{body_nu}', ] parts += [f'{c}' for i, c in enumerate(CAPS)] parts.append("") svg = "\n ".join(parts) assert "\n\n" not in svg, "linie goală în SVG" # ------------------------------------- verificarea citirii: etichete în panză TEXT = re.compile(r']*)>(.*?)', re.S) def _texts(body: str, y_off: float, tag: str): 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 = "start" if 'text-anchor="end"' in attrs: anchor = "end" elif 'text-anchor="middle"' in attrs: anchor = "middle" bold = "bold" in attrs w = len(s) * 12 * (0.62 if bold 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 labels = _texts(body_da, 0.0, "DA") + _texts(body_nu, h_da + VGAP, "NU") for i, c in enumerate(CAPS): # și caption-urile, care nu-s în panouri labels.append((24, 24 + len(c) * 12 * 0.52, cap_top + 17 * i, c, "CAP")) for left, right, y, s, tag in labels: assert left >= -2 and right <= W + 2, f"[{tag}] text iese din panză: {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}") dist = abs(ch_da._y(WEEKLY) - ch_da._y(PDA[cross_da - 1])) # 62.00 - 61.70, în px assert dist > 10, f"distanța 61.70->62.00 = {dist:.1f}px, prea mică" print(f"DA: crossing la bara {cross_da} (61.70 -> 62.20), trigger bara {trigger} " f"close {entry:.2f} > pivot {PDA[trigger]:.2f}; bare {len(cds_da)}") print(f"NU: pivot daily {min(PNU):.2f}..{max(PNU):.2f} (> 62.00), primul close " f"peste pivot bara {prim_nu} ({cds_nu[prim_nu].c:.2f}); bare {len(cds_nu)}") print(f"scris {OUT.name} ({W}x{H:.0f}); {len(labels)} etichete verificate; " f"61.70->62.00 = {dist:.1f}px") OUT.write_text(svg, encoding="utf-8")