"""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 ale unui panou, fără elementul rădăcină.""" return svg[svg.index(">") + 1:svg.rindex("")] 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'' f'{TITLU}' f'{inner(svg_a)}' f'{inner(svg_m)}' + "".join( f'{ln}' for i, ln in enumerate(LEGENDA) ) + '' ) # ============================================================ verificarea pe SVG-ul final def texts(svg: str): """Toate (x, y, text-anchor, text) din elementele ale unui fragment.""" out = [] for m in re.finditer(r']*)>(.*?)', 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")