Files
atm-curs-dl/gen/strategii/mp2_aplus.py
Claude Agent 01599d2e9d strategii: primele 30 de diagrame, cu generatoarele lor
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
2026-09-18 19:22:13 +00:00

273 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""MP2 crossing long — setup A+ vs. setup mediocru (două panouri).
Citește `gen/strategii/mp2_aplus.spec.txt` (TIP: PRET).
Panoul stânga (A+): SPY, aprilie — downtrend spre suportul quarterly 554.00,
bază cu Inverted Head & Shoulders (umăr stâng 566, head 555, umăr drept 568,
neckline 578.00, adică și un double bottom la ~554.5), intrare 577.00 pe prima
candelă verde după umărul drept, SL 554.40 chiar peste suport, 1R = 22.60,
TP1 599.60 (1:1).
Panoul dreapta (Mediocru): integral construit (cifre rotunde), NU din
material — range 100.00–110.00, crossing în mijlocul range-ului la 105.00
fără niciun pivot/suport dedesubte, intrare 105.00, SL 103.00, TP1 107.00.
Eticheta „exemplu construit, nu date reale" e cerută explicit de spec.
Două `Chart`-uri randate separat, lipite într-un SVG wrapper: fiecare panou
are scara lui, deci coordonatele rămân corecte prin construcție.
Rulează: python3 gen/strategii/mp2_aplus.py
Scrie: gen/strategii/mp2_aplus.svg
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Candle, Chart, Level, Mark, from_closes # noqa: E402
OUT = Path(__file__).resolve().parent / "mp2_aplus.svg"
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
# ============================================================ PANOUL A+ (33 bare)
# 12 bare de cădere, 10 bare de bază (IHS), candela de intrare, 10 bare de urcare.
ENTRY_A = 22 # prima candelă verde după umărul drept
LS_A, HEAD_A, RS_A = 14, 17, 20 # umăr stâng / head / umăr drept
SUP_A, SL_A, NECK_A, TP1_A = 554.00, 554.40, 578.00, 599.60
CLOSES_A = [
596.0, 592.5, 589.0, 585.5, 582.5, 579.5, 576.5, 573.5, 571.0, 568.5, 566.5, 564.0, # 0-11 cădere
561.0, 559.5, # 12-13
566.0, # 14 umăr stâng (566)
561.5, # 15
555.8, 555.0, # 16-17 double bottom + head (555)
558.5, 563.0, # 18-19
568.0, # 20 umăr drept (568)
566.5, # 21 pullback
577.0, # 22 INTRARE 577.00
580.5, 583.5, 585.0, 583.5, 587.0, 590.0, 592.0, 594.0, 596.0, 597.5, # 23-32 urcare
]
assert len(CLOSES_A) == 33, len(CLOSES_A)
WICKS_A = [(0.30, 0.30)] * 33
WICKS_A[LS_A] = (0.00, 0.30) # high exact 566.00
WICKS_A[16] = (0.30, 1.20) # low 554.60 (primul picior al double bottom)
WICKS_A[HEAD_A] = (0.30, 0.55) # low 554.45, deasupra suportului 554.00
WICKS_A[RS_A] = (0.00, 0.30) # high exact 568.00
WICKS_A[ENTRY_A] = (0.25, 0.25)
VOLS_A = [58, 56, 54, 52, 50, 48, 47, 45, 44, 42, 41, 40,
38, 44, 52, 46, 61, 66, 58, 54, 62, 50,
88,
74, 70, 66, 61, 68, 63, 59, 57, 55, 60]
cds_a = from_closes(CLOSES_A, wicks=WICKS_A, vols=VOLS_A, start=598.0)
# ============================================================ PANOUL MEDIOCRU (28 bare)
# Integral construit. Range 100.00–110.00, crossing în mijloc la 105.00.
ENTRY_M = 7
SL_M, TP1_M = 103.00, 107.00
RANGE_LO, RANGE_HI = 100.00, 110.00
CLOSES_M = [
100.5, 101.1, 101.8, 102.5, 103.2, 103.9, 104.6, # 0-6 urcare spre 105
105.0, # 7 crossing / intrare 105.00
105.8, 106.5, 107.0, 106.2, 104.8, 103.4, 102.0, # 8-14 oscilație în range
103.0, 104.5, 106.0, 107.5, 108.8, 109.5, 108.6, # 15-21
107.2, 105.8, 104.4, 103.2, 104.5, 106.0, # 22-27
]
assert len(CLOSES_M) == 28, len(CLOSES_M)
WICKS_M = [(0.30, 0.30)] * 28
WICKS_M[ENTRY_M] = (0.30, 0.20)
WICKS_M[21] = (0.30, 0.30) # vârf 109.80
VOLS_M = [42, 44, 46, 48, 50, 52, 55, 72,
64, 60, 58, 56, 60, 54, 52,
50, 53, 56, 58, 62, 60, 58,
55, 52, 50, 48, 51, 54]
cds_m = from_closes(CLOSES_M, wicks=WICKS_M, vols=VOLS_M, start=100.5)
# ============================================================ assert-urile din spec
entry_a = cds_a[ENTRY_A].c
# A+: intrare 577.00
assert abs(entry_a - 577.00) < 0.05, f"intrare A+ = {entry_a}, aștept 577.00"
# A+: SL 554.40, peste suportul quarterly 554.00 (nu sub el)
assert abs(SL_A - 554.40) < 0.05 and SL_A > SUP_A, f"SL A+ {SL_A} vs suport {SUP_A}"
# A+: 1R = 22.60
r_a = entry_a - SL_A
assert abs(r_a - 22.60) < 0.05, f"1R A+ = {r_a}, aștept 22.60"
# A+: head 555 < umărul stâng 566 și < umărul drept 568
head_a, ls_a, rs_a = cds_a[HEAD_A].c, cds_a[LS_A].h, cds_a[RS_A].h
assert head_a < ls_a and head_a < rs_a, f"head {head_a} vs umeri {ls_a}/{rs_a}"
# A+: TP1 = entry + 1R = 599.60
assert abs(TP1_A - (entry_a + r_a)) < 0.05, f"TP1 A+ {TP1_A}, entry+1R {entry_a + r_a:.2f}"
# suplimentar: structura promisă de spec
assert cds_a[HEAD_A].l > SUP_A, f"low head {cds_a[HEAD_A].l} a spart suportul {SUP_A}"
assert cds_a[HEAD_A].l == min(c.l for c in cds_a), "head-ul nu e minimul seriei"
assert CLOSES_A[0] - CLOSES_A[HEAD_A] > 35, "downtrendul A+ e prea mic"
assert entry_a > cds_a[RS_A].c, "intrarea nu e deasupra umărului drept"
# Mediocru: intrare 105.00, SL 103.00, TP1 107.00
entry_m = cds_m[ENTRY_M].c
assert abs(entry_m - 105.00) < 0.05, f"intrare mediocru = {entry_m}, aștept 105.00"
assert abs(SL_M - 103.00) < 0.05 and abs(TP1_M - 107.00) < 0.05, "SL/TP mediocru greșite"
# Mediocru: niciun nivel de suport între SL 103.00 și intrarea 105.00
levels_m = [RANGE_LO, RANGE_HI, entry_m, SL_M, TP1_M]
assert not any(SL_M < p < entry_m for p in levels_m), \
f"există suport între {SL_M} și {entry_m}: {levels_m}"
# Mediocru: oscilația stă în range
assert all(RANGE_LO <= c.c <= RANGE_HI for c in cds_m), "bare în afara range-ului"
assert min(c.l for c in cds_m) >= RANGE_LO, "low sub range"
print(f"A+ : intrare {entry_a:.2f} SL {SL_A:.2f} 1R {r_a:.2f} "
f"head {head_a:.0f} < umeri {ls_a:.0f}/{rs_a:.0f} TP1 {TP1_A:.2f}")
print(f"MED: intrare {entry_m:.2f} SL {SL_M:.2f} TP1 {TP1_M:.2f} "
f"niveluri între SL și intrare: {[p for p in levels_m if SL_M < p < entry_m]}")
# ============================================================ desenele celor două panouri
PW = 560
ch_a = Chart(
candles=cds_a,
levels=[Level(SUP_A, f"suport quarterly {SUP_A:.2f}", GRAY, label_at="left"),
Level(SL_A, f"SL {SL_A:.2f}", RED, label_at="right"),
Level(entry_a, f"intrare {entry_a:.2f}", GREEN, label_at="left"),
Level(NECK_A, f"neckline {NECK_A:.2f}", ORANGE, label_at="right"),
Level(TP1_A, f"TP1 {TP1_A:.2f} = 1:1", BLUE, label_at="right")],
marks=[Mark(LS_A, f"umăr stâng {ls_a:.0f}", GRAY, "above", dy=-4),
Mark(HEAD_A, f"head {head_a:.0f} / double bottom", RED, "below", dy=8),
Mark(RS_A, f"umăr drept {rs_a:.0f}", GRAY, "above", dy=-4),
Mark(ENTRY_A, "crossing", GREEN, "above", dy=-6)],
scale=7.5, gap=15, body_w=9, left=44, width=PW, vol_h=64,
vol_label="volum", title="A+ — crossing la confluență (SPY, aprilie)",
trend="down",
caption="Confluență: suport 554.00 + double bottom 554.45 + neckline 578.00",
)
ch_m = Chart(
candles=cds_m,
levels=[Level(RANGE_HI, f"range {RANGE_HI:.2f}", GRAY, label_at="right"),
Level(RANGE_LO, f"range {RANGE_LO:.2f}", GRAY, label_at="left"),
Level(SL_M, f"SL {SL_M:.2f}", RED, label_at="right"),
Level(entry_m, f"intrare {entry_m:.2f}", GREEN, label_at="right"),
Level(TP1_M, f"TP1 {TP1_M:.2f}", BLUE, label_at="right")],
marks=[Mark(ENTRY_M, "crossing (mijloc)", GREEN, "above", dy=-4)],
scale=34.0, gap=15, body_w=9, left=44, width=PW, vol_h=64,
vol_label="volum", title="Mediocru — crossing în mijlocul range-ului (construit)",
caption="exemplu construit, nu date reale — SL nu stă pe niciun pivot",
)
# ============================================================ wrapper: două panouri alăturate
def inner(svg: str) -> str:
"""Conținutul dintre tag-urile <svg> ale unui panou, fără elementul rădăcină."""
return svg[svg.index(">") + 1:svg.rindex("</svg>")]
def vb_height(svg: str) -> float:
m = re.search(r'viewBox="0 0 [\d.]+ ([\d.]+)"', svg)
assert m, "nu găsesc viewBox în panoul randat"
return float(m.group(1))
PANEL_GAP, PAD = 24, 8
TITLE_H, FOOTER_H = 30, 62
svg_a, svg_m = ch_a.render(), ch_m.render()
HA, HM = vb_height(svg_a), vb_height(svg_m)
assert svg_a.count("\n\n") == 0 and svg_m.count("\n\n") == 0, "linie goală în panou"
TOTAL_W = PAD + PW + PANEL_GAP + PW + PAD
TOTAL_H = PAD + TITLE_H + max(HA, HM) + FOOTER_H + PAD
OFF_A = (PAD, PAD + TITLE_H)
OFF_M = (PAD + PW + PANEL_GAP, PAD + TITLE_H)
TITLU = "MP² crossing long — setup A+ vs. setup mediocru (aceeași strategie)"
LEGENDA = [
"Legendă: ce separă cele două sunt nivelurile independente dedesubte "
"(suport quarterly + double bottom + neckline),",
"nu un filtru pus în scaner. Panoul din dreapta e exemplu construit, nu date reale.",
"Surse A+: 09_QA/01_2026-08-25.md:389-394, 485-490, 501-507.",
]
wrapper = (
f'<svg viewBox="0 0 {TOTAL_W} {TOTAL_H:.0f}" width="{TOTAL_W}" height="{TOTAL_H:.0f}" '
f'xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;font:12px sans-serif">'
f'<text x="{TOTAL_W / 2:.0f}" y="20" fill="#333" text-anchor="middle" '
f'style="font-weight:bold">{TITLU}</text>'
f'<g transform="translate({OFF_A[0]},{OFF_A[1]})">{inner(svg_a)}</g>'
f'<g transform="translate({OFF_M[0]},{OFF_M[1]})">{inner(svg_m)}</g>'
+ "".join(
f'<text x="{TOTAL_W / 2:.0f}" y="{TOTAL_H - FOOTER_H + 18 + 18 * i:.0f}" '
f'fill="#555" text-anchor="middle">{ln}</text>'
for i, ln in enumerate(LEGENDA)
)
+ '</svg>'
)
# ============================================================ verificarea pe SVG-ul final
def texts(svg: str):
"""Toate (x, y, text-anchor, text) din elementele <text> ale unui fragment."""
out = []
for m in re.finditer(r'<text x="([-\d.]+)" y="([-\d.]+)"([^>]*)>(.*?)</text>', svg):
x, y, attrs, t = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4)
if t.startswith("<"):
continue
am = re.search(r'text-anchor="(\w+)"', attrs)
out.append((x, y, am.group(1) if am else "start", t))
return out
def span(x: float, anchor: str, t: str) -> tuple[float, float]:
"""Intervalul orizontal aproximativ al unui text, față de ancora lui."""
w = len(t) * 12 * 0.56
left = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x)
return left, left + w
assert wrapper.count("\n\n") == 0, "linie goală în SVG"
# fiecare text (după translatarea panoului) e în întregime în interiorul pânzei
all_txt = []
for frag, (ox, oy) in ((inner(svg_a), OFF_A), (inner(svg_m), OFF_M)):
for x, y, anchor, t in texts(frag):
gx, gy = x + ox, y + oy
gl, gr = gx + span(0, anchor, t)[0], gx + span(0, anchor, t)[1]
assert ox <= gl and gr <= ox + PW, f"text iese din panou: {t!r} ({gl:.0f}..{gr:.0f})"
assert -1 <= gl and gr <= TOTAL_W + 1, f"text ieșit pe orizontală: {t!r}"
assert 0 <= gy <= TOTAL_H, f"text ieșit pe verticală: {t!r} ({gy:.0f})"
all_txt.append((gx, gy, anchor, t))
# textele din wrapper (titlu + legenda), în coordonate globale
wtxt = [(TOTAL_W / 2, 20.0, "middle", TITLU)]
wtxt += [(TOTAL_W / 2, TOTAL_H - FOOTER_H + 18 + 18 * i, "middle", ln)
for i, ln in enumerate(LEGENDA)]
for x, y, anchor, t in wtxt:
gl, gr = span(x, anchor, t)
assert -1 <= gl and gr <= TOTAL_W + 1, f"text din wrapper iese din pânză: {t!r}"
assert 0 <= y <= TOTAL_H, f"text din wrapper în afara înălțimii: {t!r}"
all_txt.append((x, y, anchor, t))
assert len(all_txt) >= 12, f"prea puține etichete: {len(all_txt)}"
# nu se suprapun două texte pe aceeași poziție sau pe rândul apropiat
for i in range(len(all_txt)):
xi, yi, ai, si = all_txt[i]
li, ri = (xi + span(0, ai, si)[0], xi + span(0, ai, si)[1])
for j in range(i + 1, len(all_txt)):
xj, yj, aj, sj = all_txt[j]
lj, rj = xj + span(0, aj, sj)[0], xj + span(0, aj, sj)[1]
if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}")
# etichetele cerute de spec există în desen
for cheie in ("suport quarterly 554.00", f"SL {SL_A:.2f}", "intrare 577.00",
"neckline 578.00", "TP1 599.60 = 1:1",
f"umăr stâng {ls_a:.0f}", f"head {head_a:.0f}", f"umăr drept {rs_a:.0f}",
"A+ — crossing la confluență (SPY, aprilie)",
"Mediocru — crossing în mijlocul range-ului (construit)",
"range 110.00", "range 100.00", "SL 103.00", "intrare 105.00", "TP1 107.00",
"crossing (mijloc)", "exemplu construit, nu date reale"):
assert cheie in wrapper, f"lipsește din desen: {cheie!r}"
OUT.write_text(wrapper, encoding="utf-8")
print(f"scris {OUT.name}: {TOTAL_W}x{TOTAL_H:.0f}, "
f"{len(all_txt)} etichete, verificare OK")