Cate un agent grafician per diagrama, fiecare cu generatorul si SVG-ul lui. Graficele de pret trec prin svg_candles, cu assert-urile din specificatie scrise in generator — verifica structura, nu doar ca ruleaza: ca al doilea picior e mai jos, ca intrarea e close-ul barei de confirmare, ca 1:1 e exact. Verificare prin sondaj pe buy_doji_ansamblu, cu screenshot citit: serie continua de 32 de candele, panou de volum, MA(20), doji-ul si ieșirea pe trail etichetate, cifrele identice cu specificatia. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
229 lines
11 KiB
Python
229 lines
11 KiB
Python
#!/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="left")],
|
|
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'<polyline points="{step_points(ch, pivots)}" fill="none" '
|
|
f'stroke="{ORANGE}" stroke-width="1.8"/>',
|
|
f'<text x="{W - 10}" y="{ch._y(pivots[-1]) - 6:.1f}" fill="{ORANGE}" '
|
|
f'text-anchor="end" style="font-weight:bold">pivot daily (cel MIC)</text>',
|
|
]
|
|
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("</svg>")]
|
|
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'<circle cx="{xc:.1f}" cy="{yc:.1f}" r="13" fill="none" '
|
|
f'stroke="{GREEN}" stroke-width="2"/>')
|
|
cross_txt = (f'<text x="{xc + 16:.1f}" y="{ch_da._y(PDA[cross_da - 1]) + 18:.1f}" '
|
|
f'fill="{GREEN}" style="font-weight:bold">crossing: 61.70 → 62.20</text>')
|
|
mark_txt = (f'<text x="{ch_da._cx(trigger):.1f}" y="{ch_da._y(cds_da[trigger].h) - 10:.1f}" '
|
|
f'fill="{GREEN}" text-anchor="middle" style="font-weight:bold">'
|
|
f'intrare: trigger (prima candelă verde)</text>')
|
|
i = body_da.rindex("</text>") + len("</text>") # 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 <text x="{ch_nu._cx(13):.1f}" y="{h_nu - 12:.1f}" '
|
|
f'fill="{GRAY}" text-anchor="middle" style="font-weight:bold">'
|
|
f'semnale ulterioare = BUY THE DIP, nu crossing</text>')
|
|
|
|
# ------------------------------------------------- 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.",
|
|
"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'<svg viewBox="0 0 {W} {H:.0f}" xmlns="http://www.w3.org/2000/svg" '
|
|
f'style="width:100%;height:auto;font:12px sans-serif">',
|
|
f'<g transform="translate(0,0)">{body_da}</g>',
|
|
f'<g transform="translate(0,{h_da + VGAP:.0f})">{body_nu}</g>',
|
|
]
|
|
parts += [f'<text x="24" y="{cap_top + 17 * i:.0f}" fill="#555">{c}</text>'
|
|
for i, c in enumerate(CAPS)]
|
|
parts.append("</svg>")
|
|
svg = "\n ".join(parts)
|
|
assert "\n\n" not in svg, "linie goală în SVG"
|
|
|
|
# ------------------------------------- verificarea citirii: etichete în panză
|
|
TEXT = re.compile(r'<text x="([-\d.]+)" y="([-\d.]+)"([^>]*)>(.*?)</text>', 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")
|