Files
atm-curs-dl/gen/strategii/mp2_aplus.py
Claude Agent 2ea599f265 02_mp2: capitol reconstruit dupa sablon
Nivelurile 1-2-3, cei 6 pasi cu diagrama fiecare, mini-glosar de 34 de
termeni, oglinda pe short cu diagrama ei proprie, checklist. Sursele in
fise/02_mp2_surse.md.

Toate diagramele procedurii ies acum dintr-o singura serie
(mp2_serie.py), deci capitolul nu mai poate avea doua reguli numerice.
Ansamblul folosea NVDA cu stop la swing low absolut (-23 %), adica alta
regula decat procedura celor 4 bare; NVDA a devenit "bottom", la nivelul
3, marcat explicit ca exceptie.

perechi_tf e diagrama cu doua timeframe-uri: panoul mic e descompunerea
reala a ultimelor 2 candele daily (1 zi = 7 bare de 60 min), cu pivotul
weekly pe ambele panouri si conector. Pivotii se calculeaza din serie, nu
sunt scrisi de mana.

Reparate: trail-ul din proza era 600.05, calculul din serie da 600.75;
doua SVG-uri aveau trimiteri fisier:linie desenate pe ele.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-20 12:51:58 +00:00

274 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} (peste suport)", 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ță: 554.00 + double bottom + neckline 578.00 · 1R 22.60",
)
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.",
"Cifrele A+ sunt din material; trimiterile stau în fișa de surse a capitolului.",
]
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} (peste suport)", "intrare 577.00",
"crossing", "1R 22.60",
"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")