From 01599d2e9dd18cc7511295d30db15108b97fc208 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Fri, 18 Sep 2026 19:22:13 +0000 Subject: [PATCH] strategii: primele 30 de diagrame, cu generatoarele lor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ --- gen/strategii/atmi_ansamblu.py | 123 +++++++++ gen/strategii/atmi_ansamblu.svg | 119 ++++++++ gen/strategii/atmi_aplus.py | 158 +++++++++++ gen/strategii/atmi_aplus.svg | 119 ++++++++ gen/strategii/atmi_exemplu.py | 125 +++++++++ gen/strategii/atmi_exemplu.svg | 131 +++++++++ gen/strategii/atmi_orizont.py | 230 ++++++++++++++++ gen/strategii/atmi_orizont.svg | 47 ++++ gen/strategii/buy_doji_ansamblu.py | 103 +++++++ gen/strategii/buy_doji_ansamblu.svg | 116 ++++++++ gen/strategii/buy_doji_aplus.py | 247 +++++++++++++++++ gen/strategii/buy_doji_aplus.svg | 162 +++++++++++ gen/strategii/buy_doji_fereastra.py | 188 +++++++++++++ gen/strategii/buy_doji_fereastra.svg | 60 ++++ gen/strategii/buy_doji_labu.py | 190 +++++++++++++ gen/strategii/buy_doji_labu.svg | 170 ++++++++++++ gen/strategii/buy_doji_sell.py | 114 ++++++++ gen/strategii/buy_doji_sell.svg | 111 ++++++++ gen/strategii/hammer_mbt10_bh_ansamblu.py | 118 ++++++++ gen/strategii/hammer_mbt10_bh_ansamblu.svg | 115 ++++++++ gen/strategii/hammer_mbt10_bh_aplus.py | 279 +++++++++++++++++++ gen/strategii/hammer_mbt10_bh_aplus.svg | 139 ++++++++++ gen/strategii/hammer_mbt10_bh_labu.py | 177 ++++++++++++ gen/strategii/hammer_mbt10_bh_labu.svg | 133 +++++++++ gen/strategii/hammer_mbt10_harta.py | 266 ++++++++++++++++++ gen/strategii/hammer_mbt10_harta.svg | 48 ++++ gen/strategii/hammer_mbt10_mbt_ansamblu.py | 196 +++++++++++++ gen/strategii/hammer_mbt10_mbt_ansamblu.svg | 142 ++++++++++ gen/strategii/hammer_mbt10_mbt_gapdown.py | 181 ++++++++++++ gen/strategii/hammer_mbt10_mbt_gapdown.svg | 118 ++++++++ gen/strategii/hammer_mbt10_mbt_tesla.py | 151 ++++++++++ gen/strategii/hammer_mbt10_mbt_tesla.svg | 59 ++++ gen/strategii/m2d_ansamblu.py | 136 +++++++++ gen/strategii/m2d_ansamblu.svg | 117 ++++++++ gen/strategii/m2d_exemplu.py | 289 ++++++++++++++++++++ gen/strategii/m2d_exemplu.svg | 109 ++++++++ gen/strategii/m2d_secventa.py | 268 ++++++++++++++++++ gen/strategii/m2d_secventa.svg | 42 +++ gen/strategii/mbp_ansamblu.py | 201 ++++++++++++++ gen/strategii/mbp_ansamblu.svg | 95 +++++++ gen/strategii/mbp_exemplu.py | 242 ++++++++++++++++ gen/strategii/mbp_exemplu.svg | 101 +++++++ gen/strategii/mbp_samecandle.py | 204 ++++++++++++++ gen/strategii/mbp_samecandle.svg | 78 ++++++ gen/strategii/mdsrt_ansamblu.py | 166 +++++++++++ gen/strategii/mdsrt_ansamblu.svg | 109 ++++++++ gen/strategii/mdsrt_exemplu.py | 222 +++++++++++++++ gen/strategii/mdsrt_exemplu.svg | 113 ++++++++ gen/strategii/mdsrt_masurare.py | 254 +++++++++++++++++ gen/strategii/mdsrt_masurare.svg | 36 +++ gen/strategii/mdsrt_trinity.py | 199 ++++++++++++++ gen/strategii/mdsrt_trinity.svg | 30 ++ gen/strategii/mp2_ansamblu.py | 139 ++++++++++ gen/strategii/mp2_ansamblu.svg | 132 +++++++++ gen/strategii/mp2_aplus.py | 272 ++++++++++++++++++ gen/strategii/mp2_aplus.svg | 221 +++++++++++++++ gen/strategii/mp2_crossing.py | 228 +++++++++++++++ gen/strategii/mp2_crossing.svg | 112 ++++++++ gen/strategii/mp2_exemplu.py | 147 ++++++++++ gen/strategii/mp2_exemplu.svg | 138 ++++++++++ 60 files changed, 9035 insertions(+) create mode 100644 gen/strategii/atmi_ansamblu.py create mode 100644 gen/strategii/atmi_ansamblu.svg create mode 100644 gen/strategii/atmi_aplus.py create mode 100644 gen/strategii/atmi_aplus.svg create mode 100644 gen/strategii/atmi_exemplu.py create mode 100644 gen/strategii/atmi_exemplu.svg create mode 100644 gen/strategii/atmi_orizont.py create mode 100644 gen/strategii/atmi_orizont.svg create mode 100644 gen/strategii/buy_doji_ansamblu.py create mode 100644 gen/strategii/buy_doji_ansamblu.svg create mode 100644 gen/strategii/buy_doji_aplus.py create mode 100644 gen/strategii/buy_doji_aplus.svg create mode 100644 gen/strategii/buy_doji_fereastra.py create mode 100644 gen/strategii/buy_doji_fereastra.svg create mode 100644 gen/strategii/buy_doji_labu.py create mode 100644 gen/strategii/buy_doji_labu.svg create mode 100644 gen/strategii/buy_doji_sell.py create mode 100644 gen/strategii/buy_doji_sell.svg create mode 100644 gen/strategii/hammer_mbt10_bh_ansamblu.py create mode 100644 gen/strategii/hammer_mbt10_bh_ansamblu.svg create mode 100644 gen/strategii/hammer_mbt10_bh_aplus.py create mode 100644 gen/strategii/hammer_mbt10_bh_aplus.svg create mode 100644 gen/strategii/hammer_mbt10_bh_labu.py create mode 100644 gen/strategii/hammer_mbt10_bh_labu.svg create mode 100644 gen/strategii/hammer_mbt10_harta.py create mode 100644 gen/strategii/hammer_mbt10_harta.svg create mode 100644 gen/strategii/hammer_mbt10_mbt_ansamblu.py create mode 100644 gen/strategii/hammer_mbt10_mbt_ansamblu.svg create mode 100644 gen/strategii/hammer_mbt10_mbt_gapdown.py create mode 100644 gen/strategii/hammer_mbt10_mbt_gapdown.svg create mode 100644 gen/strategii/hammer_mbt10_mbt_tesla.py create mode 100644 gen/strategii/hammer_mbt10_mbt_tesla.svg create mode 100644 gen/strategii/m2d_ansamblu.py create mode 100644 gen/strategii/m2d_ansamblu.svg create mode 100644 gen/strategii/m2d_exemplu.py create mode 100644 gen/strategii/m2d_exemplu.svg create mode 100644 gen/strategii/m2d_secventa.py create mode 100644 gen/strategii/m2d_secventa.svg create mode 100644 gen/strategii/mbp_ansamblu.py create mode 100644 gen/strategii/mbp_ansamblu.svg create mode 100644 gen/strategii/mbp_exemplu.py create mode 100644 gen/strategii/mbp_exemplu.svg create mode 100644 gen/strategii/mbp_samecandle.py create mode 100644 gen/strategii/mbp_samecandle.svg create mode 100644 gen/strategii/mdsrt_ansamblu.py create mode 100644 gen/strategii/mdsrt_ansamblu.svg create mode 100644 gen/strategii/mdsrt_exemplu.py create mode 100644 gen/strategii/mdsrt_exemplu.svg create mode 100644 gen/strategii/mdsrt_masurare.py create mode 100644 gen/strategii/mdsrt_masurare.svg create mode 100644 gen/strategii/mdsrt_trinity.py create mode 100644 gen/strategii/mdsrt_trinity.svg create mode 100644 gen/strategii/mp2_ansamblu.py create mode 100644 gen/strategii/mp2_ansamblu.svg create mode 100644 gen/strategii/mp2_aplus.py create mode 100644 gen/strategii/mp2_aplus.svg create mode 100644 gen/strategii/mp2_crossing.py create mode 100644 gen/strategii/mp2_crossing.svg create mode 100644 gen/strategii/mp2_exemplu.py create mode 100644 gen/strategii/mp2_exemplu.svg diff --git a/gen/strategii/atmi_ansamblu.py b/gen/strategii/atmi_ansamblu.py new file mode 100644 index 0000000..f045744 --- /dev/null +++ b/gen/strategii/atmi_ansamblu.py @@ -0,0 +1,123 @@ +"""Diagrama de ansamblu ATMI (strategii/05_atmi.md, marcajul `ansamblu`). + +„SPY — daily: corecție ≥4 % → SQ/PPQ → BIM + verde → intrare → TP +10 %". + +Structura seriei și nivelurile vin din spec-ul +`gen/strategii/atmi_ansamblu.spec.txt`. Toate cifrele desenate (100.00 swing +high, 96.00 intrare, 105.60 = +10 %) sunt din exemplul „DA" al cursului +(`summaries/04_MODUL_3/03_strategia_2_atmi.md:185-247`). Benzile PPQ/SQ sunt +schematice: cursul spune explicit că nu dă cifrele lor +(`...:143`), deci nu poartă prețuri. + +Rulează: + python3 gen/strategii/atmi_ansamblu.py +Scrie lângă el `atmi_ansamblu.svg`. +""" +import sys +from pathlib import Path + +sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402 + +OUT = Path(__file__).resolve().parent / "atmi_ansamblu.svg" + +GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000" +PURPLE = "#7b2fbf" # folosit în materialul de curs pentru PPQ + +# ---------------------------------------------------------------- seria (33 bare) +# 15 bare de urcare ~88 -> 100, 8 de corecție 100 -> 96 (ultima = intrarea), +# 10 de urcare 96 -> 105.60. open-ul fiecărei bare = close-ul precedentei. +UP_A = [88.2, 88.8, 89.5, 90.1, 90.8, 91.6, 92.5, 93.3, 94.2, 95.1, + 96.0, 96.9, 97.8, 98.9, 100.00] # 0-14 +CORR = [99.3, 98.5, 97.8, 97.2, 96.7, 96.3, 96.00, 96.00] # 15-22 +UP_B = [96.6, 97.2, 97.9, 98.6, 99.4, 100.3, 101.4, 102.6, 104.0, 105.60] # 23-32 +CLOSES = UP_A + CORR + UP_B +assert len(CLOSES) == 33, len(CLOSES) +PEAK, BIM, ENTRY, LAST = 14, 21, 22, 32 + +# fitile (sus, jos) per bară; jos pe corecție ținut ≥ 0 ca minimul să fie fix 96.00 +WICKS = ([(0.30, 0.30)] * 14 + + [(0.00, 0.30)] # 14: vârful atinge exact 100.00 + + [(0.00, 0.00), (0.05, 0.10), (0.05, 0.10), (0.05, 0.15), + (0.05, 0.15), (0.05, 0.20), (0.05, 0.00), (0.12, 0.00)] # 15-22 + + [(0.25, 0.25)] * 9 + + [(0.00, 0.20)]) # 32: închide/atinge exact 105.60 +assert len(WICKS) == len(CLOSES) + +VOLS = ([44, 47, 42, 50, 45, 41, 48, 43, 46, 51, 44, 49, 46, 52, 58] # 0-14 urcare + + [64, 70, 66, 61, 58, 74, 96, 88] # 15-22 corecție + BIM/intrare + + [55, 58, 52, 60, 57, 63, 59, 66, 72, 84]) # 23-32 urcare spre TP +assert len(VOLS) == len(CLOSES) + +cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=88.0) + +# ---------------------------------------------------------------- assert-urile din spec +high = 100.00 +entry = cds[ENTRY].c +tp = cds[LAST].h +assert abs((high - 96.00) / high * 100 - 4.0) < 0.05 # corecția e 4.0 % +assert abs(entry - 96.00) < 0.05 # intrarea = close-ul candelei verzi +assert abs(tp / entry - 1.10) < 0.005 # TP = +10 % +assert abs(tp - max(c.h for c in cds[-10:])) < 1e-9 # TP atins la final, e maximul barelor +assert BIM < ENTRY # BIM înainte de candela verde +assert abs(min(c.l for c in cds[15:23]) - 96.00) < 1e-9 # minimul corecției = 96.00 +assert cds[ENTRY].c >= cds[ENTRY].o # candela de intrare e verde +assert abs(cds[PEAK].h - high) < 1e-9 # swing high = 100.00 +assert abs(cds[LAST].c - tp) < 1e-9 # ultima bară închide pe TP + +levels = [ + Level(high, "100.00 — swing high recent", "#333", label_at="left"), + Level(entry, f"intrare {entry:.2f}", GREEN, label_at="right"), + Level(tp, f"TP +10 % = {tp:.2f}", BLUE, label_at="right"), +] +# nicio linie de stop-loss și niciun nivel sub intrare (absența SL e regula) +assert all("stop" not in lv.text.lower() for lv in levels) +assert all(lv.price >= entry - 1e-9 for lv in levels) + +marks = [ + Mark(15, "corecție 4.0 %", ORANGE, "above"), + Mark(BIM, "BIM", ORANGE, "above"), + Mark(ENTRY, "BIM + candelă verde → intrare", GREEN, "below", dy=20), +] + +ch = Chart(candles=cds, levels=levels, marks=marks, + mas=[MA(20, ORANGE, dash="4 3", text="SMA-20 (ilustrativ)")], + scale=18.0, gap=16, body_w=9, left=44, width=720, vol_h=70, top=70, + vol_label="volum", + caption="Niveluri (100.00 / 96.00 / 105.60) din curs; PPQ/SQ schematice, fără cifre în material.") + +svg = ch.render() + + +def band(price: float, half: float, color: str, label: str) -> str: + """Bandă schematică orizontală, desenată în spatele candelelor. + + Se injectează în SVG după randare, pentru că `Chart` nu are benzi; y-ul + se calculează cu aceeași scară (`ch._y`), deci banda e aliniată corect. + """ + y_top, y_bot = ch._y(price + half), ch._y(price - half) + return (f'' + f'{label}') + + +# SQ e zona joasă (intrarea exact pe ea), PPQ zona mai sus, retragere mai mică +assert (entry + 1.60 - 0.35) > (entry + 0.35), "benzile SQ și PPQ nu se suprapun" +sq = band(entry, 0.35, GREEN, "SQ — zonă quarterly") +ppq = band(entry + 1.60, 0.35, PURPLE, "PPQ — zonă quarterly") +# „fără stop-loss standard" stă sub zona de intrare, unde nu are candele +no_sl = (f'fără stop-loss standard' + f'(03_strategia_2_atmi.md:164-169)') +# titlul e injectat deasupra, ca rând separat de eticheta TP (care stă chiar +# sub marginea de sus a panoului); altfel cele două texte se calcă la dreapta +titlu = ('' + 'SPY — daily: corecție ≥4 % → SQ/PPQ → BIM + verde → intrare → TP +10 %') + +i = svg.index(">") + 1 +svg = svg[:i] + "\n " + "\n ".join([sq, ppq, no_sl, titlu]) + svg[i:] + +assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown" +OUT.write_text(svg) +print(f"scris {OUT.name}: {len(cds)} candele, intrare {entry:.2f}, TP {tp:.2f}") diff --git a/gen/strategii/atmi_ansamblu.svg b/gen/strategii/atmi_ansamblu.svg new file mode 100644 index 0000000..fec12fe --- /dev/null +++ b/gen/strategii/atmi_ansamblu.svg @@ -0,0 +1,119 @@ + + SQ — zonă quarterly + PPQ — zonă quarterly + fără stop-loss standard(03_strategia_2_atmi.md:164-169) + SPY — daily: corecție ≥4 % → SQ/PPQ → BIM + verde → intrare → TP +10 % + + 100.00 — swing high recent + + intrare 96.00 + + TP +10 % = 105.60 + + SMA-20 (ilustrativ) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + corecție 4.0 % + BIM + BIM + candelă verde → intrare + Niveluri (100.00 / 96.00 / 105.60) din curs; PPQ/SQ schematice, fără cifre în material. + \ No newline at end of file diff --git a/gen/strategii/atmi_aplus.py b/gen/strategii/atmi_aplus.py new file mode 100644 index 0000000..4ddcd31 --- /dev/null +++ b/gen/strategii/atmi_aplus.py @@ -0,0 +1,158 @@ +"""ATMI — aceeași corecție: 4.0 % (DA) vs. 3.9 % (NU; cursul zice „3.93 %"). + +Citește specificația `gen/strategii/atmi_aplus.spec.txt`. + +O singură serie de 32 de candele daily (open-ul fiecărei bare = close-ul +precedentei, deci seria e continuă), care urmează calea DA: urcare la swing +high 100.00, corecție exact 4.0 % până la 96.00 (BIM + candelă verde = +intrare), apoi urcare la TP +10 % = 105.60. Peste ea, un traseu punctat +(calea NU) care se oprește la 96.10 = 3.9 % și apoi coboară ilustrativ — fără +intrare, pentru că nu atinge pragul de 4 %. + +Traseele punctate / benzile se inserează în SVG-ul scos de `svg_candles` +folosind coordonatele lui (`ch._cx`, `ch._y`), la fel ca în `az_structura.py` +— candelele rămân desenate de `svg_candles`, nu de mână. +""" +import sys + +sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, Mark, from_closes # noqa: E402 + +GRAY, RED, GREEN, BLUE, ORANGE, INK = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000", "#333" + +# --- seria: 32 de close-uri, structura din specificație ---------------------- +# 0-11 urcare 90.00 -> 100.00 (swing high la bara 11) +# 12-17 corecție (cădere) +# 18 BIM (hammer de reversie, codiță lungă în jos) +# 19 prima candelă verde daily => intrare, close 96.00 +# 20-31 urcare 96.00 -> 105.60 (TP +10 %) +closes = [ + 90.00, 90.90, 91.70, 92.40, 93.20, 94.00, 94.70, 95.40, 96.10, 97.00, 98.30, 100.00, # 0-11 + 99.30, 98.50, 97.70, 97.10, 96.60, 96.20, # 12-17 + 95.98, # 18 BIM + 96.00, # 19 intrare (verde) + 97.00, 97.90, 98.80, 99.70, 100.50, 101.30, 102.10, 102.90, 103.70, 104.50, 105.10, 105.60, # 20-31 +] +assert len(closes) == 32 + +# wick-uri (sus, jos): implicite (0.25, 0.30), cu excepțiile care fixează nivelurile +wicks = [(0.25, 0.30)] * 32 +wicks[11] = (0.00, 0.30) # swing high: high = close = 100.00 +wicks[18] = (0.20, 0.45) # BIM: codiță lungă în jos (hammer) +wicks[19] = (0.20, 0.15) # candela de intrare +wicks[31] = (0.00, 0.30) # TP: high = close = 105.60 + +# volum relativ ales de generator (materialul nu dă volum pe exemplu): +# crește pe corecție (vânzătorii), vârf pe candela de intrare, scade pe urcare. +vols = [48, 50, 46, 52, 54, 50, 56, 52, 58, 60, 64, 72, + 70, 74, 78, 82, 86, 80, 84, + 88, + 72, 66, 62, 58, 54, 50, 46, 62, 68, 72, 78, 86] +assert len(vols) == 32 + +cds = from_closes(closes, wicks=wicks, vols=vols, start=89.40) + +# --- indicii-cheie și nivelurile --------------------------------------------- +SWING_I = 11 # swing high +BIM_I = 18 # primul BIM +ENTRY_I = 19 # candela verde de intrare +SWING = 100.00 +PRAG4 = 96.00 # prag corecție 4.0 % => DA +NU_STOP = 96.10 # cazul real 3.9 % => NU +TP = 105.60 # +10 % față de 96.00 +entry = cds[ENTRY_I].c + +# --- cele 8 assert-uri cerute de specificație -------------------------------- +# 1. pragul de 4 % corespunde lui 96.00 +assert abs((SWING - PRAG4) / SWING * 100 - 4.00) < 0.05, "96.00 nu e 4.0 %" +# 2. cazul NU: 96.10 = 3.9 % (graficul cursului scrie 3.9 %; textul „3.93 %") +assert abs((SWING - NU_STOP) / SWING * 100 - 3.90) < 0.02, "96.10 nu e 3.9 %" +# 3. calea DA coboară mai jos decât calea NU +assert PRAG4 < NU_STOP, "pragul DA nu e sub oprirea NU" +# 4. intrarea = close-ul candelei verzi = 96.00 +assert abs(entry - 96.00) < 1e-9, f"intrarea {entry} != 96.00" +assert cds[ENTRY_I].c >= cds[ENTRY_I].o, "candela de intrare nu e verde" +# 5. TP = +10 % +assert abs(TP / entry - 1.10) < 0.005, f"TP/entry = {TP / entry:.4f}, nu 1.10" +# 6. BIM înainte de candela de intrare +assert BIM_I < ENTRY_I, "BIM nu e înainte de candela de intrare" +# 7. traseul NU nu atinge pragul de trigger: se oprește la 96.10 > 96.00, deci +# rămâne fără intrare; pe traseul punctat nu există nicio candelă / BIM +nu_route = [(SWING_I, 100.00), (12, 99.40), (13, 98.65), (14, 97.95), + (15, 97.35), (16, 96.80), (17, NU_STOP)] +assert min(p for _, p in nu_route) == NU_STOP, "oprirea traseului NU nu e 96.10" +assert min(p for _, p in nu_route) > PRAG4, "traseul NU atinge pragul de 4 %" +assert not any(i >= BIM_I for i, _ in nu_route), "traseul NU ajunge la candela BIM" +# 8. ambele trasee pleacă din același swing high 100.00 +assert abs(cds[SWING_I].h - SWING) < 1e-9, f"swing high {cds[SWING_I].h} != 100.00" +assert abs(nu_route[0][1] - cds[SWING_I].h) < 1e-9, "traseul NU nu pleacă din swing high" + +# verificări de context cerute de STYLE.md (structura susține eticheta) +assert abs(TP - max(c.h for c in cds[-10:])) < 1e-9, "TP nu e maximul de la final" +assert min(closes[12:20]) < 96.10 and min(closes[12:20]) >= 95.90, \ + "corecția DA nu coboară în jurul lui 96.00" +assert all(b > a for a, b in zip(closes[20:31], closes[21:32])), "urcarea spre TP nu e monotonă" + +print(f"swing {SWING:.2f} prag DA {PRAG4:.2f} (4.0 %) stop NU {NU_STOP:.2f} (3.9 %) " + f"intrare {entry:.2f} TP {TP:.2f} (+{TP / entry - 1:.1%}) BIM bara {BIM_I}") + +# --- desenul ----------------------------------------------------------------- +cds[ENTRY_I].note = ("Regula e pe procent, nu pe cât de dramatică arată linia: " + "calea NU pare o cădere mare, dar e 3.9 %.") + +# traseul NU: decizia (până la 96.10) + continuarea ilustrativă în jos +nu_cont = [(17, NU_STOP), (18, 95.70), (19, 95.35), (21, 94.95), (23, 94.65), (25, 94.45)] +nu_pts = nu_route + nu_cont[1:] + +ch = Chart( + candles=cds, + levels=[ + Level(SWING, "100.00 — swing high", INK), + Level(PRAG4, "96.00 = 4.0 % — DA, intri", GREEN), + Level(TP, "TP +10 % = 105.60", BLUE), + ], + marks=[ + Mark(BIM_I, "BIM", GREEN, "above", dy=-2), + Mark(ENTRY_I, "candelă verde → trigger", GREEN, "below", dy=20), + ], + scale=18.0, gap=16, body_w=9, left=44, width=680, vol_h=70, + vol_label="volum", title="SPY — daily: aceeași corecție, 4.0 % (DA) vs. 3.9 % (NU)", + caption="Calea DA (candele): 4.0 % la 96.00. Calea NU (punctat): 3.9 % („3.93 %” în curs) — fără intrare.", +) +svg = ch.render() + +# --- elemente suplimentare, pe coordonatele graficului ----------------------- +x0, x1 = 24, ch.width - 14 +band_top, band_bot = ch._y(96.40), ch._y(95.40) +band = (f'') + +nu_line = ('') +# linia pragului NU (96.10) se desenează manual, ca Level-ul gol să nu lase text vid +nu_hline = (f'') +# punctul de oprire al traseului NU (96.10, strict deasupra pragului de 4 %) +stop_dot = (f'') +# eticheta traseului NU, sub linia 96.10 (nu se suprapune cu eticheta verde de 96.00) +nu_lab = (f'' + f'96.10 = 3.9 % — NU, sub prag') +# eticheta zonei quarterly, sub banda (fără cifre: cursul nu le dă) +zone_lab = (f'' + f'zonă quarterly') +# legende scurte ale celor două trasee +da_lab = (f'calea DA (candelele)') +nu_leg = (f'calea NU (continuă în jos)') + +overlay = [nu_line, nu_hline, stop_dot, nu_lab, zone_lab, da_lab, nu_leg] +svg = svg.replace(">", ">\n " + band, 1) # banda, sub candele +svg = svg.replace("\n", "\n " + "\n ".join(overlay) + "\n") # traseele, peste + +assert "\n\n" not in svg, "linie goală în SVG" +assert "" in svg and "stroke-dasharray=\"5 4\"" in svg, "traseul NU nu a fost inserat" + +with open("/workspace/atm-curs-dl/gen/strategii/atmi_aplus.svg", "w") as f: + f.write(svg) +print("scris gen/strategii/atmi_aplus.svg") diff --git a/gen/strategii/atmi_aplus.svg b/gen/strategii/atmi_aplus.svg new file mode 100644 index 0000000..07e53ee --- /dev/null +++ b/gen/strategii/atmi_aplus.svg @@ -0,0 +1,119 @@ + + + SPY — daily: aceeași corecție, 4.0 % (DA) vs. 3.9 % (NU) + + 100.00 — swing high + + 96.00 = 4.0 % — DA, intri + + TP +10 % = 105.60 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + BIM + candelă verde → trigger + Regula e pe procent, nu pe cât de dramatică arată linia: calea NU pare o cădere mare, dar e 3.9 %. + Calea DA (candele): 4.0 % la 96.00. Calea NU (punctat): 3.9 % („3.93 %” în curs) — fără intrare. + + + + 96.10 = 3.9 % — NU, sub prag + zonă quarterly + calea DA (candelele) + calea NU (continuă în jos) + \ No newline at end of file diff --git a/gen/strategii/atmi_exemplu.py b/gen/strategii/atmi_exemplu.py new file mode 100644 index 0000000..806b23b --- /dev/null +++ b/gen/strategii/atmi_exemplu.py @@ -0,0 +1,125 @@ +"""Diagrama din `gen/strategii/atmi_exemplu.spec.txt` (TIP: PRET). + +Exemplul real din curs (ATMI / SPY weekly): corecție de context ≥4 %, +intrare pe 27.01.2005 la 117.43 după BIM + candelă verde, fără stop-loss +standard, urcare lentă până la TP1 +10 % = 129.17, atins ~1 an mai târziu +(data exactă nesigură în ASR). + +Serie continuă de 38 de candele weekly (from_closes), panou de volum, +niveluri reale (intrare / TP1) și marcaje ancorate. Toate cifrele vin din +`summaries/04_MODUL_3/03_strategia_2_atmi.md` (liniile citate în spec); +singurul număr construit de mine e swing high-ul ~123.00, care trebuie să +conțină corecția de context. +""" +import sys +from pathlib import Path + +sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402 + +OUT = Path(__file__).resolve().parent / "atmi_exemplu.svg" + +# ── cifrele din material ──────────────────────────────────────────────────── +ENTRY = 117.43 # 04_MODUL_3/03_strategia_2_atmi.md:358-363 +TP = 129.17 # TP1 = +10 % pe 117.43 → 129.17 (":170-171, 358-363") +SWING_HIGH = 123.00 # construit de mine ca să conțină corecția ≥4 % +ENTRY_I = 17 # candela de intrare (verde, precedată de BIM) +TP_I = 37 # ultima candelă: TP1 atins +BIM_I = ENTRY_I - 1 # BIM (bară de inversare bearish) înaintea candelei verzi + +# ── structura seriei: 12 sus → 6 corecție → intrare → 20 spre TP ─────────── +closes = [ + # ~12 candele de urcare spre swing high ~123.00 + 115.80, 116.50, 117.20, 117.90, 117.60, 118.30, + 119.00, 119.70, 120.40, 121.00, 121.70, 122.70, + # ~6 candele de corecție, de la 123.00 în zona de intrare + 122.00, 121.10, 120.10, 119.00, 117.00, + 117.43, # candela de intrare (verde) + # ~20 candele de urcare de la 117.43 spre 129.17 + 118.00, 118.55, 119.05, 119.50, 119.95, + 120.40, 120.85, 121.25, 121.65, 122.10, + 122.50, 122.95, 123.40, 123.85, 124.30, + 125.00, 126.00, 127.10, 128.40, 129.17, +] +assert len(closes) == 38, len(closes) + +wicks = [(0.25, 0.30)] * len(closes) +wicks[0] = (0.35, 0.35) +wicks[11] = (0.30, 0.25) # high = 122.70 + 0.30 = 123.00 (swing high exact) +wicks[BIM_I] = (0.15, 0.45) +wicks[ENTRY_I] = (0.20, 0.40) +wicks[TP_I] = (0.00, 0.30) # high = 129.17 exact, ca TP să fie max(high) + +vols = [42, 40, 44, 47, 39, 45, 48, 52, 50, 55, 58, 62, + 57, 61, 66, 72, 84, + 70, + 58, 55, 53, 57, 54, 56, 59, 55, 57, 60, 58, 61, + 63, 60, 64, 66, 68, 72, 76, 88] + +cds = from_closes(closes, wicks=wicks, vols=vols, start=115.30) + +# ── assert-urile din specificație ─────────────────────────────────────────── +entry = cds[ENTRY_I].c +tp = cds[TP_I].c +swing_high = max(c.h for c in cds[:ENTRY_I]) + +assert abs(entry - 117.43) < 0.05, entry +assert abs(tp - 129.17) < 0.05, tp +assert abs(tp / entry - 1.10) < 0.005, tp / entry +assert (swing_high - entry) / swing_high >= 0.04, (swing_high, entry) +assert abs(entry - cds[ENTRY_I].c) < 0.05 +assert abs(max(c.h for c in cds[-10:]) - tp) < 0.05, max(c.h for c in cds[-10:]) + +# structura care susține afirmațiile din text +assert abs(swing_high - SWING_HIGH) < 0.05, swing_high +assert cds[ENTRY_I].c > cds[ENTRY_I].o, "candela de intrare trebuie să fie verde" +assert cds[BIM_I].c < cds[BIM_I].o, "BIM-ul dinaintea intrării trebuie să fie bearish" +assert any(c.vol is not None for c in cds), "lipsește panoul de volum" + +n_entry_to_tp = TP_I - ENTRY_I + 1 +assert 18 <= n_entry_to_tp <= 23, n_entry_to_tp + +TP_LABEL = "TP1 atins ~1 an mai târziu (data exactă neclară în transcriere — ASR)" +assert "~1 an" in TP_LABEL + +levels = [ + Level(ENTRY, "intrare 117.43", "#188a3e", label_at="left"), + Level(TP, "TP1 129.17 (+10 %)", "#1a5fb4"), +] + +marks = [ + Mark(11, f"swing high ~{SWING_HIGH:.2f}", "#c00", "above"), + Mark(BIM_I, "BIM", "#777", "above"), + Mark(ENTRY_I, "27.01.2005", "#188a3e", "below"), + Mark(TP_I, "TP1", "#1a5fb4", "below"), +] + +# nu există nicio linie de stop-loss în desen +assert not any("stop" in t.lower() for t in + [lv.text for lv in levels] + [mk.text for mk in marks]) +assert len(levels) == 2, "doar intrare și TP; strategic nu există stop standard" + +# etichetele lungi merg ca linii de text sub grafic, ca să nu se bată cu +# candelele din dreapta-sus (unde urcarea spre TP nu lasă loc liber) +cds[TP_I - 1].note = "după TP: capital + profit → cash până la următorul trigger valid" +cds[TP_I].note = TP_LABEL + +ch = Chart( + candles=cds, + levels=levels, + marks=marks, + mas=[MA(20, "#b06000", dash="4 3")], + scale=9.5, width=660, gap=15, body_w=9, left=40, vol_h=70, + vol_label="volum", title="SPY — weekly: intrare 117.43 (27.01.2005) → TP +10 % = 129.17", + caption="Swing high ~123.00 construit de mine; cifrele de intrare/TP sunt din material.", +) + +svg = ch.render() +assert svg.count("\n\n") == 0, "linie goală în SVG — rupe markdown-ul" +OUT.write_text(svg) + +print(f"serie {len(cds)} candele | entry {entry:.2f} @ {ENTRY_I} | " + f"tp {tp:.2f} @ {TP_I} | swing high {swing_high:.2f} " + f"(corecție {(swing_high - entry) / swing_high * 100:.2f} %) | " + f"entry→TP {n_entry_to_tp} candele (~1 an)") +print(f"scris {OUT}") diff --git a/gen/strategii/atmi_exemplu.svg b/gen/strategii/atmi_exemplu.svg new file mode 100644 index 0000000..56917a0 --- /dev/null +++ b/gen/strategii/atmi_exemplu.svg @@ -0,0 +1,131 @@ + + SPY — weekly: intrare 117.43 (27.01.2005) → TP +10 % = 129.17 + + intrare 117.43 + + TP1 129.17 (+10 %) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + swing high ~123.00 + BIM + 27.01.2005 + TP1 + după TP: capital + profit → cash până la următorul trigger valid + TP1 atins ~1 an mai târziu (data exactă neclară în transcriere — ASR) + Swing high ~123.00 construit de mine; cifrele de intrare/TP sunt din material. + \ No newline at end of file diff --git a/gen/strategii/atmi_orizont.py b/gen/strategii/atmi_orizont.py new file mode 100644 index 0000000..d6d06b2 --- /dev/null +++ b/gen/strategii/atmi_orizont.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +"""Orizontul de ținere al ATMI — două descrieri incompatibile, pe aceeași axă. + +Pentru `strategii/03_atmi.md`, marcajul ``. +Nu e grafic de preț, e o linie de timp comparativă: SVG scris de mână, doar +stdlib — nu se folosește svg_candles / pchart. + +Ce arată (toate cifrele / citatele vin din spec, nu se inventează prețuri): +- bara de sus, albastră: „ATMI clasică — fișierul principal", orizont luni + până la ~1 an, fără stop-loss, TP1 atins „aproximativ un an mai târziu"; +- bara de jos, portocalie: „ATMI ca adaptare pentru prop — Q&A 11.06.2025", + orizont 2 săptămâni; +- cutia de anotare: pe prop, limita zilnică se calculează și pe pozițiile + deschise, iar la atingere firma închide automat; și nota că descrierile nu + se rezolvă aici, de clarificat cu autorul. + +Barele sunt pe o axă comună 0 → 1 an, ca diferența să fie vizibilă; bara de +2 săptămâni e scurtă exact pentru că atât ține, nu din greșeală de scară. + +Rulare: python3 gen/strategii/atmi_orizont.py -> gen/strategii/atmi_orizont.svg +""" +from pathlib import Path +from xml.sax.saxutils import escape + +OUT = Path(__file__).with_suffix(".svg") + +W, H = 700, 670 +BLACK, GRAY, BLUE, ORANGE, WHITE = "#000000", "#777777", "#1a5fb4", "#b06000", "#ffffff" + +AX_X0, AX_X1 = 60.0, 650.0 +AX_W = AX_X1 - AX_X0 # 590 px = 1 an +AX_Y = 112.0 +TICKS = [0, 3, 6, 9, 12] # luni +TICK_LABELS = {0: "0", 3: "3 luni", 6: "6 luni", 9: "9 luni", 12: "~1 an"} + +WEEKS_YEAR = 52.0 +LEN_PROP = AX_W * 2 / WEEKS_YEAR # ~22.7 px = 2 săptămâni + +BAR_TOP_Y = 172.0 +BAR_PROP_Y = 298.0 +BAR_H = 42.0 + +CARD_X, CARD_Y, CARD_W, CARD_H = 160.0, 292.0, 500.0, 112.0 +BOX_X, BOX_Y, BOX_W, BOX_H = 40.0, 440.0, 620.0, 150.0 + +TEXTS = [] # (x, y, text, size, weight, anchor) pentru verificare + + +def txt(x, y, s, size=13, weight="400", fill=BLACK, anchor="middle"): + TEXTS.append((x, y, s, size, weight, anchor)) + return ( + f'{escape(s)}' + ) + + +def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None): + d = f' stroke-dasharray="{dash}"' if dash else "" + return (f'') + + +def tick_x(months): + return AX_X0 + AX_W * months / 12.0 + + +def build(): + p = [ + f'', + '', + f'', + txt(W / 2, 34, "Cât ții poziția? Două descrieri, același nume", 19, "700", BLACK), + txt(W / 2, 58, "fișierul principal vs. sfatul din Q&A pentru calificarea " + "pe prop", 13, "400", GRAY), + txt(W / 2, 84, "Aceeași axă de timp pentru ambele bare (0 → 1 an); " + "fără axe de preț.", 12, "400", GRAY), + ] + + # grid vertical, în spatele barelor + for m in TICKS: + x = tick_x(m) + p.append(line(x, 120, x, 425, "#ddd", 1.0, "3 4")) + + # axa de timp + p.append(line(AX_X0, AX_Y, AX_X1, AX_Y, GRAY, 1.2)) + for m in TICKS: + x = tick_x(m) + p.append(line(x, AX_Y - 6, x, AX_Y + 6, GRAY, 1.2)) + p.append(txt(x, 132, TICK_LABELS[m], 12, "400", GRAY)) + + # ------- bara de sus: ATMI clasică + p.append(txt(AX_X0, 162, "ATMI clasică — fișierul principal", 15, "700", BLUE, + "start")) + p.append(f'') + p.append(txt(W / 2, BAR_TOP_Y + 26, + "„fără stop-loss, poziția ținută luni/ani”", 13, "400", BLACK)) + p.append(line(AX_X1, 164, AX_X1, 226, BLUE, 1.2, "4 3")) + p.append(txt(AX_X1, 244, "TP1 atins — aproximativ un an mai târziu", + 13, "400", BLUE, "end")) + p.append(txt(AX_X0, 262, "Sursă: 04_MODUL_3/03_strategia_2_atmi.md:26-29; " + ":358-363", 11, "400", GRAY, "start")) + + # ------- bara de jos: adaptarea pentru prop + p.append(txt(AX_X0, 282, "ATMI ca adaptare pentru prop — Q&A 11.06.2025", + 15, "700", ORANGE, "start")) + p.append(f'') + p.append(line(AX_X0 + LEN_PROP, 290, AX_X0 + LEN_PROP, 360, ORANGE, 1.2, "4 3")) + p.append(line(AX_X0 + LEN_PROP, BAR_PROP_Y + BAR_H / 2, CARD_X, + BAR_PROP_Y + BAR_H / 2, ORANGE, 1.0, "3 3")) + p.append(txt(AX_X0 + 30, 312, "2 săptămâni", 11, "700", ORANGE, "start")) + + p.append(f'') + p.append(txt(CARD_X + CARD_W / 2, 316, + "„aștepți o corecție și intri pe ATMI la suport quarterly,", + 13, "400", BLACK)) + p.append(txt(CARD_X + CARD_W / 2, 334, "swing, 2 săptămâni în poziție”", + 13, "400", BLACK)) + p.append(txt(CARD_X + CARD_W / 2, 358, + "„Ai nevoie de 3 transacții să califici propul.”", + 13, "400", BLACK)) + p.append(txt(CARD_X + CARD_W / 2, 382, + "Sursă: 09_QA/26_2025-06-11.md:225-227", 11, "400", GRAY)) + + # ------- cutia de anotare + p.append(f'') + p.append(txt(BOX_X + 20, 466, + "Anotare separată — mecanismul care face diferența", + 14, "700", BLACK, "start")) + p.append(txt(BOX_X + 20, 492, + "Pe prop, limita zilnică se calculează și pe pozițiile deschise,", + 13, "400", BLACK, "start")) + p.append(txt(BOX_X + 20, 510, "iar la atingere firma închide automat poziția.", + 13, "400", BLACK, "start")) + p.append(txt(BOX_X + 20, 530, "Sursă: 09_QA/26_2025-06-11.md:216-222", + 11, "400", GRAY, "start")) + p.append(txt(BOX_X + 20, 560, + "Notă: cele două descrieri nu se rezolvă aici — de clarificat " + "cu autorul.", 13, "700", BLACK, "start")) + + p.append(txt(W / 2, 626, "Fără axe de preț și fără să alegem una dintre " + "variante.", 12, "400", GRAY)) + p.append(txt(W / 2, 646, "Bara de 2 săptămâni e scurtă exact pentru că " + "atât ține, pe aceeași scară.", 12, "400", GRAY)) + p.append('') + return "\n".join(p) + "\n" + + +# ---------------------------------------------------------------- verificare +def est_w(s, size, weight): + factor = 0.62 if weight == "700" else 0.56 + return len(s) * size * factor + + +def verify(svg): + # 1. geometria barelor, față de axa comună + assert abs(AX_W - (AX_X1 - AX_X0)) < 1e-9 + assert abs(LEN_PROP - AX_W * 2 / WEEKS_YEAR) < 1e-9 + assert LEN_PROP < AX_W / 20, "bara de 2 săptămâni ar trebui să fie vizibil scurtă" + assert AX_X0 + LEN_PROP < CARD_X, "bara prop intră în card" + assert BAR_TOP_Y + BAR_H < BAR_PROP_Y + assert CARD_X + CARD_W <= AX_X1 + 10 + + # 2. toate etichetele în panză + for x, y, s, size, weight, anchor in TEXTS: + ew = est_w(s, size, weight) + left = x - ew if anchor == "end" else (x - ew / 2 if anchor == "middle" else x) + right = left + ew + assert -2 <= left and right <= W + 2, f"text în afara panzei: {s!r}" + assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y})" + + # 3. etichete care nu se suprapun + for i in range(len(TEXTS)): + xi, yi, si, szi, wi, ai = TEXTS[i] + we = est_w(si, szi, wi) + li = xi - we if ai == "end" else (xi - we / 2 if ai == "middle" else xi) + ri = li + we + for j in range(i + 1, len(TEXTS)): + xj, yj, sj, szj, wj, aj = TEXTS[j] + wej = est_w(sj, szj, wj) + lj = xj - wej if aj == "end" else (xj - wej / 2 if aj == "middle" else xj) + rj = lj + wej + if abs(yi - yj) < 13 and li < rj - 4 and lj < ri - 4: + raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}") + + # 4. textul cardului încape în card + for x, y, s, size, weight, anchor in TEXTS: + if CARD_Y <= y <= CARD_Y + CARD_H and CARD_X <= x <= CARD_X + CARD_W: + ew = est_w(s, size, weight) + assert ew <= CARD_W - 24, f"eticheta nu încape în card: {s!r} ({ew:.0f})" + + # 5. cutia de anotare: textul nu iese + for x, y, s, size, weight, anchor in TEXTS: + if BOX_Y <= y <= BOX_Y + BOX_H and x >= BOX_X: + ew = est_w(s, size, weight) + assert BOX_X + 20 + ew <= BOX_X + BOX_W - 12, f"text iese din cutie: {s!r}" + + # 6. citatele și sursele din spec sunt prezente + for cheie in ("fără stop-loss, poziția ținută luni/ani", + "aproximativ un an mai târziu", + "suport quarterly", + "2 săptămâni în poziție", + "3 transacții să califici propul", + "limita zilnică se calculează", + "închide automat", + "nu se rezolvă aici", + "03_strategia_2_atmi.md:26-29", + "09_QA/26_2025-06-11.md:225-227", + "09_QA/26_2025-06-11.md:216-222"): + assert cheie in svg, f"lipsește din desen: {cheie!r}" + + # 7. fără cifre de preț inventate + for interzis in ("$", "USD", "SL ", "intrare "): + assert interzis not in svg, f"nu ar trebui să apară preț: {interzis!r}" + return True + + +SVG = build() +assert verify(SVG), "verificare eșuată" +OUT.write_text(SVG, encoding="utf-8") +print(f"scris {OUT} ({W}x{H}), verificare OK: {len(TEXTS)} etichete, " + f"bara prop {LEN_PROP:.1f}px din {AX_W:g}px") diff --git a/gen/strategii/atmi_orizont.svg b/gen/strategii/atmi_orizont.svg new file mode 100644 index 0000000..1f6c0af --- /dev/null +++ b/gen/strategii/atmi_orizont.svg @@ -0,0 +1,47 @@ + + + +Cât ții poziția? Două descrieri, același nume +fișierul principal vs. sfatul din Q&A pentru calificarea pe prop +Aceeași axă de timp pentru ambele bare (0 → 1 an); fără axe de preț. + + + + + + + +0 + +3 luni + +6 luni + +9 luni + +~1 an +ATMI clasică — fișierul principal + +„fără stop-loss, poziția ținută luni/ani” + +TP1 atins — aproximativ un an mai târziu +Sursă: 04_MODUL_3/03_strategia_2_atmi.md:26-29; :358-363 +ATMI ca adaptare pentru prop — Q&A 11.06.2025 + + + +2 săptămâni + +„aștepți o corecție și intri pe ATMI la suport quarterly, +swing, 2 săptămâni în poziție” +„Ai nevoie de 3 transacții să califici propul.” +Sursă: 09_QA/26_2025-06-11.md:225-227 + +Anotare separată — mecanismul care face diferența +Pe prop, limita zilnică se calculează și pe pozițiile deschise, +iar la atingere firma închide automat poziția. +Sursă: 09_QA/26_2025-06-11.md:216-222 +Notă: cele două descrieri nu se rezolvă aici — de clarificat cu autorul. +Fără axe de preț și fără să alegem una dintre variante. +Bara de 2 săptămâni e scurtă exact pentru că atât ține, pe aceeași scară. + diff --git a/gen/strategii/buy_doji_ansamblu.py b/gen/strategii/buy_doji_ansamblu.py new file mode 100644 index 0000000..3ab5937 --- /dev/null +++ b/gen/strategii/buy_doji_ansamblu.py @@ -0,0 +1,103 @@ +"""Buy Doji — diagrama de ansamblu (daily, exemplu construit, fără ticker). + +Citește specificația `gen/strategii/buy_doji_ansamblu.spec.txt`. Seria de +32 de candele e construită din lista de close-uri dată, cu open-ul fiecărei +bare = close-ul precedentului; wick-urile sunt fixate ca nivelurile +(doji 50.10/49.20, intrare 50.40, SL 49.20, TP1 51.60, trail 52.60, ieșire +sub trail la bara 28) să iasă exact din serie, nu doar din etichetă. +""" +import sys; sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, Mark, MA, from_closes + +# --- seria: 32 de close-uri, exact ca în specificație ----------------------- +closes = [ + 60.20, 59.20, 59.60, 58.40, 57.30, 57.70, 56.50, 55.40, 55.80, 54.70, # 0-9 + 53.50, 52.30, 51.00, 49.65, # 10-13 downtrend + 49.60, # 14 doji + 49.90, # 15 +1 (nu confirmă) + 50.40, # 16 +2 confirmare => intrare + 50.90, 51.20, 51.50, 51.80, 52.10, 52.40, 52.20, 52.70, 52.95, 53.20, # 17-26 + 52.80, 52.30, 52.00, 51.80, 51.90, # 27-31 +] +assert len(closes) == 32 + +# wick-uri: (up, down) implicite (0.25, 0.30) + excepțiile din specificație +wicks = [(0.25, 0.30)] * 32 +wicks[14] = (0.45, 0.40) # doji: high 50.10, low 49.20 +wicks[15] = (0.15, 0.15) # high 50.05, low 49.45 +wicks[16] = (0.15, 0.15) # high 50.55, low 49.75 +for k in range(24, 28): # barele 24-27 + wicks[k] = (0.25, 0.20) + +# volum relativ ales de generator (materialul nu dă volum pe exemplu); +# peste medie pe doji (14) și pe confirmare (16), apoi normal +vols = [42, 44, 40, 46, 43, 41, 45, 42, 44, 47, + 43, 45, 48, 52, 78, 46, 88, 55, + 50, 48, 52, 47, 45, 49, 46, 44, 48, 51, + 54, 47, 45, 46] +assert len(vols) == 32 +media_vol = sum(vols) / len(vols) +assert vols[14] > media_vol and vols[16] > media_vol, "volum peste medie pe doji/confirmare" + +cds = from_closes(closes, wicks=wicks, vols=vols, start=60.20) + +# adnotările pe bara +1 și pe confirmare, ca linii sub grafic (fără suprapunere) +cds[15].color = "#777" # bara de așteptare: neutră, nu confirmă +cds[15].note = "bara 15: close 49.90 sub high-ul dojiului 50.10 — NU confirmă" +cds[16].note = "bara 16: close 50.40 peste 50.10 — confirmare (intrare)" + +doji, wait, conf = cds[14], cds[15], cds[16] +entry, sl, tp1, trail = 50.40, 49.20, 51.60, 52.60 + +# --- cele 9 assert-uri cerute de specificație ------------------------------ +# 1. SL-ul e exact low-ul dojiului, cel mai jos din fereastra de 4 candele +assert abs(min(cd.l for cd in cds[13:17]) - 49.20) < 1e-9, "min low[13:17] != 49.20" +# 2. intrarea e close-ul barei 16 +assert abs(conf.c - entry) < 1e-9, f"close[16]={conf.c} != 50.40" +# 3. confirmarea închide peste high-ul dojiului +assert abs(doji.h - 50.10) < 1e-9 and conf.c > doji.h, "close[16] nu e peste high[14]" +# 4. bara +1 NU confirmă +assert wait.c <= doji.h, "bara 15 ar confirma, fereastra n-ar avea sens" +# 5. 1:1 exact +assert abs(abs(entry - sl) - abs(tp1 - entry)) < 1e-9 and abs((entry - sl) - 1.20) < 1e-9, \ + "risc != recompensă (nu e 1:1 exact)" +# 6. primul high >= TP1 după intrare e bara 19 +primul_tp = next(i for i in range(17, len(cds)) if cds[i].h >= tp1) +assert primul_tp == 19, f"TP1 51.60 atins întâi la bara {primul_tp}, nu 19" +# 7. trailing 52.60 = low[27]; ieșire la bara 28 sub el; ieșirea e pe profit +assert abs(cds[27].l - trail) < 1e-9, f"low[27]={cds[27].l} != 52.60" +assert abs(cds[28].c - 52.30) < 1e-9 and cds[28].c < trail, "close[28] nu e sub trail" +assert trail > entry, "trail sub intrare, ieșirea n-ar fi pe profit" +# 8. doji: corp <= 10% din amplitudine +assert abs(doji.o - doji.c) <= 0.10 * (doji.h - doji.l), "bara 14 nu e doji" +# 9. downtrend vizibil +assert cds[13].c < cds[0].c - 10, "downtrend prea mic" + +print(f"doji {doji.h:.2f}/{doji.l:.2f} intrare {conf.c:.2f} " + f"SL {sl:.2f} TP1 {tp1:.2f} trail {trail:.2f} ieșire bara 28 @ {cds[28].c:.2f}") + +# --- desenul ---------------------------------------------------------------- +ch = Chart( + candles=cds, + levels=[ + Level(sl, "SL 49.20 (low 4 candele)", "#c00", label_at="left"), + Level(entry, "intrare 50.40", "#188a3e", label_at="left"), + Level(tp1, "TP1 51.60 = 1R", "#1a5fb4", label_at="left"), + Level(trail, "trail 52.60", "#b06000"), + ], + marks=[ + Mark(14, "doji 50.10 / 49.20", "#b06000", "below"), + Mark(28, "ieșire pe trail ~52.60", "#c00", "above", dy=-6), + ], + mas=[MA(20, "#777", dash="4 3", text="MA(20)")], + scale=24.0, gap=15, body_w=9, left=44, width=640, vol_h=70, + vol_label="volum", title="Buy Doji — daily (exemplu construit, fără ticker)", + caption="Intrare la close-ul confirmării, SL la low-ul ultimelor 4 candele, " + "TP1 1:1, apoi trailing stop.", +) + +svg = ch.render() +assert " + Buy Doji — daily (exemplu construit, fără ticker) + + SL 49.20 (low 4 candele) + + intrare 50.40 + + TP1 51.60 = 1R + + trail 52.60 + + MA(20) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + doji 50.10 / 49.20 + ieșire pe trail ~52.60 + bara 15: close 49.90 sub high-ul dojiului 50.10 — NU confirmă + bara 16: close 50.40 peste 50.10 — confirmare (intrare) + Intrare la close-ul confirmării, SL la low-ul ultimelor 4 candele, TP1 1:1, apoi trailing stop. + \ No newline at end of file diff --git a/gen/strategii/buy_doji_aplus.py b/gen/strategii/buy_doji_aplus.py new file mode 100644 index 0000000..4f83c7e --- /dev/null +++ b/gen/strategii/buy_doji_aplus.py @@ -0,0 +1,247 @@ +"""Buy Doji — A+ vs. mediocru (două panouri, exemple construite). + +Citește `gen/strategii/buy_doji_aplus.spec.txt` (marcajul ``). +Același pattern (trend → doji → confirmare), context opus: + +- stânga (A+): downtrend 52 → 42.60, doji cu low 41.95 chiar pe suportul + 42.00 (0.12 % sub nivel), confirmare close 43.60 pe volum peste media + ultimelor 10 bare — acumulare; +- dreapta (NU): uptrend 70 → 76.80, gap-up la open 78.20, doji lipit de + rezistența 78.50, confirmare close 79.30 pe volum sub medie („varză"), apoi + prețul cade înapoi sub 78.00. + +Nivelurile 42.00 / 78.50 și cifrele de preț sunt construite de mine ca să +susțină etichetele și assert-urile din spec (materialul nu dă prețuri pentru +acest contrast). Două `Chart`-uri randate separat, lipite într-un SVG wrapper: +coordonatele fiecărui panou rămân corecte prin construcție. + +Rulează: python3 gen/strategii/buy_doji_aplus.py +Scrie: gen/strategii/buy_doji_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, MA, from_closes # noqa: E402 + +OUT = Path(__file__).resolve().parent / "buy_doji_aplus.svg" + +GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000" + +DOJI_A, CONF_A = 10, 11 # indicii în panoul A+ +DOJI_N, CONF_N = 14, 15 # indicii în panoul NU + +# ============================================================ PANOUL A+ (24 bare) +# 10 bare de downtrend de la 52.00 spre 42.60, apoi doji, apoi confirmare. +CLOSES_A = [ + 51.60, 50.80, 50.00, 49.20, 48.40, 47.60, 46.80, 45.50, 44.20, 42.60, # 0-9 downtrend + 42.60, # 10 doji (open = close) + 43.60, # 11 confirmare + 44.10, 44.50, 44.20, 44.80, 45.20, 45.00, 45.50, 45.90, 46.20, 46.50, 46.80, 47.10, # 12-23 acumulare +] +assert len(CLOSES_A) == 24, len(CLOSES_A) + +WICKS_A = [(0.30, 0.30)] * 24 +WICKS_A[DOJI_A] = (0.30, 0.65) # high 42.90, low 41.95 (pe suportul 42.00) +WICKS_A[CONF_A] = (0.25, 0.30) # high 43.85 + +VOLS_A = [56, 53, 51, 49, 47, 45, 44, 42, 41, 39, # 0-9 vânzare în scădere + 60, # 10 doji: exces de vânzare + 86, # 11 confirmare: volum mare + 72, 75, 70, 78, 82, 80, 85, 88, 92, 95, 98, 100] # 12-23 acumulare + +cds_a = from_closes(CLOSES_A, wicks=WICKS_A, vols=VOLS_A, start=52.00) +doji_a, conf_a = cds_a[DOJI_A], cds_a[CONF_A] + +# ============================================================ PANOUL NU (24 bare) +# uptrend 70 → 76.80 (14 bare), gap-up la open 78.20 + doji, confirmare 79.30 +# pe volum slab, apoi cădere înapoi sub 78.00. Lista e manuală pentru că gap-ul +# (open > high-ul barei precedente) nu iese din `from_closes`. +BARE_NU = [ + # o, h, l, c, vol + (70.00, 70.70, 69.80, 70.50, 48), (70.50, 71.20, 70.30, 71.00, 50), + (71.00, 71.75, 70.85, 71.55, 49), (71.55, 72.30, 71.40, 72.10, 52), + (72.10, 72.80, 71.95, 72.60, 51), (72.60, 73.30, 72.45, 73.10, 54), + (73.10, 73.80, 72.95, 73.60, 53), (73.60, 74.25, 73.45, 74.05, 56), + (74.05, 74.70, 73.90, 74.50, 55), (74.50, 75.15, 74.35, 74.95, 58), + (74.95, 75.60, 74.80, 75.40, 57), (75.40, 76.00, 75.25, 75.80, 60), + (75.80, 76.40, 75.65, 76.20, 59), (76.20, 76.80, 76.05, 76.80, 62), # 13: high 76.80 + (78.20, 78.50, 78.10, 78.20, 58), # 14: GAP-UP + doji (o = c = 78.20) + (78.20, 79.50, 78.10, 79.30, 30), # 15: confirmare, volum slab + (79.30, 79.40, 78.40, 78.60, 55), (78.60, 78.70, 77.80, 78.00, 60), + (78.00, 78.10, 77.20, 77.40, 64), (77.40, 77.55, 76.80, 77.00, 62), + (77.00, 77.15, 76.40, 76.60, 66), (76.60, 76.75, 76.10, 76.30, 63), + (76.30, 76.40, 75.80, 76.00, 65), (76.00, 76.15, 75.50, 75.70, 61), +] +assert len(BARE_NU) == 24, len(BARE_NU) +cds_n = [Candle(o, h, l, c, vol=v) for (o, h, l, c, v) in BARE_NU] +doji_n, conf_n = cds_n[DOJI_N], cds_n[CONF_N] + +# ============================================================ assert-urile din spec +# 1. A+: low-ul dojiului e la ≤ 0,5 % de 42.00 (41.95 e la 0.12 %) +dist_a = abs(doji_a.l - 42.00) / 42.00 +assert dist_a <= 0.005, f"low doji A+ {doji_a.l} la {dist_a * 100:.2f} % de 42.00" +assert abs(doji_a.l - 41.95) < 1e-9, f"low doji A+ = {doji_a.l}, aștept 41.95" +# 2. A+: volumul pe bara de confirmare > media volumelor pe 10 bare +media_a = sum(VOLS_A[CONF_A - 10:CONF_A]) / 10 +assert VOLS_A[CONF_A] > media_a, f"vol confirmare {VOLS_A[CONF_A]} <= media {media_a}" +# 3. NU: doji-ul apare după un gap-up (open doji > high bara precedentă) +assert cds_n[DOJI_N].o > cds_n[DOJI_N - 1].h, "doji NU nu e după gap-up" +assert abs(cds_n[DOJI_N].o - 78.20) < 1e-9 and abs(cds_n[DOJI_N - 1].h - 76.80) < 1e-9 +# 4. NU: |high doji - 78.50| / 78.50 <= 0.005 +dist_n = abs(doji_n.h - 78.50) / 78.50 +assert dist_n <= 0.005, f"high doji NU {doji_n.h} la {dist_n * 100:.2f} % de 78.50" +# 5. NU: volumul pe bara de confirmare < media pe 10 bare +media_n = sum(b[4] for b in BARE_NU[CONF_N - 10:CONF_N]) / 10 +assert conf_n.vol < media_n, f"vol confirmare NU {conf_n.vol} >= media {media_n}" +# 6. ambele: close-ul de confirmare > high-ul dojiului (tehnic valid) +assert conf_a.c > doji_a.h, f"A+ close {conf_a.c} <= high doji {doji_a.h}" +assert conf_n.c > doji_n.h, f"NU close {conf_n.c} <= high doji {doji_n.h}" +assert abs(conf_a.c - 43.60) < 1e-9 and abs(conf_n.c - 79.30) < 1e-9 +# 7. ambele doji: abs(o-c) <= 0.10*(h-l) +for tag, d in (("A+", doji_a), ("NU", doji_n)): + assert abs(d.o - d.c) <= 0.10 * (d.h - d.l), f"bara doji {tag} nu e doji" +# suplimentare: structura promisă de spec +assert min(c.l for c in cds_a[:10]) > doji_a.l, "low-ul dojiului A+ nu e minimul" +assert cds_a[9].c < cds_a[0].c - 8, "downtrend A+ prea mic" +assert cds_n[CONF_N].c > 78.50 and cds_n[-1].c < 78.00, "NU nu cade sub 78.00 la final" +assert cds_n[13].c < 78.00 < cds_n[DOJI_N].o, "NU: uptrendul nu se oprește sub 78.00" + +print(f"A+ doji {doji_a.h:.2f}/{doji_a.l:.2f} conf {conf_a.c:.2f} " + f"vol {VOLS_A[CONF_A]} > medie {media_a:.1f}") +print(f"NU doji {doji_n.h:.2f}/{doji_n.l:.2f} conf {conf_n.c:.2f} " + f"vol {conf_n.vol} < medie {media_n:.1f}") + +# ============================================================ desenele celor două panouri +PW = 560 # lățimea fiecărui panou +ch_a = Chart( + candles=cds_a, + levels=[Level(42.00, "suport 42.00", GREEN, label_at="left")], + marks=[Mark(DOJI_A, "doji la nivel (low 41.95)", ORANGE, "below"), + Mark(CONF_A, "acumulare", GREEN, "above", dy=-6)], + mas=[], scale=22.0, gap=15, body_w=9, left=44, width=PW, vol_h=60, + vol_label="volum", title="A+ — doji la suport, volum în acumulare", + caption="acumulare: volum peste media ultimelor 10 bare.", +) +ch_n = Chart( + candles=cds_n, + levels=[Level(78.50, "rezistență 78.50", RED, label_at="right")], + marks=[Mark(DOJI_N, "doji 78.50 / 78.10", ORANGE, "below", dy=93)], + mas=[], scale=22.0, gap=15, body_w=9, left=44, width=PW, vol_h=60, + vol_label="volum", title="Mediocru — gap-up în rezistență, volum varză", + caption="gap-up + doji în rezistență, confirmare pe volum slab.", +) + +# ============================================================ 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, 22 +svg_a, svg_n = ch_a.render(), ch_n.render() + +# „gap-up, volum slab" stă în colțul liber din stânga-sus, cu linie indicatoare +# spre golul dintre bara 13 (high 76.80) și dojiul barei 14 (open 78.20). +GAP_X = (ch_n._cx(DOJI_N - 1) + ch_n._cx(DOJI_N)) / 2 +GAP_Y = ch_n._y(77.50) +gap_ann = ( + f'gap-up, volum slab' + f'' + f'' +) +svg_n = svg_n.replace("", gap_ann + "") + +HA, HN = vb_height(svg_a), vb_height(svg_n) +assert svg_a.count("\n\n") == 0 and svg_n.count("\n\n") == 0, "linie goală în panou" + +TOTAL_W = PAD + PW + PANEL_GAP + PW + PAD +TOTAL_H = PAD + TITLE_H + max(HA, HN) + FOOTER_H + PAD +OFF_A = (PAD, PAD + TITLE_H) +OFF_N = (PAD + PW + PANEL_GAP, PAD + TITLE_H) + +FOOTER = ("Surse: 13_strategia_3_byh.md:188-190 (doji la nivel); " + "01_buy_doji_si_strategia_mici.md:227-235 (context invalid).") + +wrapper = ( + f'' + f'Setup A+ vs. setup mediocru (două panouri, exemple construite)' + f'{inner(svg_a)}' + f'{inner(svg_n)}' + f'{FOOTER}' + f'' +) + +# ============================================================ verificare 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_n), OFF_N)): + 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} ({gl:.0f}..{gr:.0f})" + 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 + footer), în coordonate globale +for x, y, anchor, t in ((TOTAL_W / 2, 20.0, "middle", + "Setup A+ vs. setup mediocru (două panouri, exemple construite)"), + (TOTAL_W / 2, TOTAL_H - PAD, "middle", FOOTER)): + 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) >= 8, "prea puține etichete" + +# 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 42.00", "doji la nivel", "acumulare", + "rezistență 78.50", "gap-up, volum slab", + "A+ — doji la suport, volum în acumulare", + "Mediocru — gap-up în rezistență, volum varză"): + 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") diff --git a/gen/strategii/buy_doji_aplus.svg b/gen/strategii/buy_doji_aplus.svg new file mode 100644 index 0000000..12b7ffb --- /dev/null +++ b/gen/strategii/buy_doji_aplus.svg @@ -0,0 +1,162 @@ +Setup A+ vs. setup mediocru (două panouri, exemple construite) + A+ — doji la suport, volum în acumulare + + suport 42.00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + doji la nivel (low 41.95) + acumulare + acumulare: volum peste media ultimelor 10 bare. + + Mediocru — gap-up în rezistență, volum varză + + rezistență 78.50 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + doji 78.50 / 78.10 + gap-up + doji în rezistență, confirmare pe volum slab. +gap-up, volum slabSurse: 13_strategia_3_byh.md:188-190 (doji la nivel); 01_buy_doji_si_strategia_mici.md:227-235 (context invalid). \ No newline at end of file diff --git a/gen/strategii/buy_doji_fereastra.py b/gen/strategii/buy_doji_fereastra.py new file mode 100644 index 0000000..135b6bd --- /dev/null +++ b/gen/strategii/buy_doji_fereastra.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Buy Doji — fereastra de 3 candele: DA (valid) vs. NU (expirat). + +Citește `gen/strategii/buy_doji_fereastra.spec.txt` și scrie +`gen/strategii/buy_doji_fereastra.svg`. Două panouri mici, unul sub altul, +cu aceeași serie până la doji: sus confirmarea vine la bara +2 (în fereastră), +jos niciuna din +1..+3 nu confirmă, iar a 4-a închide sub low-ul dojiului. + +Toate cifrele din desen vin din specificație: doji h 50.10 / l 49.20, +closes-urile DA/NU, pragul 50.10, low-ul 49.20. Seriile sunt construite +(open = close-ul barei anterioare, mai puțin open-ul primei bare, start-ul), +ca nivelurile afirmate să iasă exact din serie. + +Rulare: python3 gen/strategii/buy_doji_fereastra.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 = 460 +VGAP = 34 +SCALE = 45.0 # px / unitate, aceeași pe ambele panouri (comparație) + +# --- seriile din specificație ---------------------------------------------- +CLOSES_DA = [52.20, 51.40, 50.60, 49.65, + 49.60, # doji, h 50.10 / l 49.20 + 49.90, # +1 — sub 50.10 + 50.40, # +2 — peste 50.10 => confirmă + 50.80] +CLOSES_NU = [52.20, 51.40, 50.60, 49.65, + 49.60, # doji, h 50.10 / l 49.20 + 49.95, # +1 + 50.05, # +2 + 49.85, # +3 + 49.15] # a 4-a — sub low-ul dojiului +assert len(CLOSES_DA) == 8 and len(CLOSES_NU) == 9 +assert CLOSES_DA[:5] == CLOSES_NU[:5] + +HIGH_DOJI, LOW_DOJI = 50.10, 49.20 + +# wick-uri: (up, down). Doji-ul (index 4) primește mecioni ca să iasă exact +# h=50.10 / l=49.20; restul au mecioni mici, egali. +wicks_da = [(0.15, 0.15)] * len(CLOSES_DA) +wicks_nu = [(0.15, 0.15)] * len(CLOSES_NU) +wicks_da[4] = wicks_nu[4] = (0.45, 0.40) + +START = CLOSES_DA[0] + 0.15 # prima bară roșie, open > close +cds_da = from_closes(CLOSES_DA, wicks=wicks_da, start=START) +cds_nu = from_closes(CLOSES_NU, wicks=wicks_nu, start=START) + +doji_da, doji_nu = cds_da[4], cds_nu[4] +entry = cds_da[6].c # intrare la close-ul confirmării + +# --- assert-urile din specificație ----------------------------------------- +# doji-ul iese exact la nivelurile afirmate +assert abs(doji_da.h - HIGH_DOJI) < 1e-9 and abs(doji_da.l - LOW_DOJI) < 1e-9, \ + f"doji DA {doji_da.h}/{doji_da.l} != {HIGH_DOJI}/{LOW_DOJI}" +assert abs(doji_nu.h - HIGH_DOJI) < 1e-9 and abs(doji_nu.l - LOW_DOJI) < 1e-9, \ + f"doji NU {doji_nu.h}/{doji_nu.l} != {HIGH_DOJI}/{LOW_DOJI}" + +# 1. DA: primul close peste 50.10 DUPĂ doji e bara +2 (index 6), în fereastră; +# bara +1 nu confirmă +primul_da = next(i for i in range(5, len(cds_da)) if cds_da[i].c > HIGH_DOJI) +assert primul_da == 6, f"prima confirmare la index {primul_da}, nu 6 (+2)" +assert primul_da - 4 <= 3, "confirmarea nu e în fereastra de 3 candele" +assert abs(cds_da[5].c - 49.90) < 1e-9 and cds_da[5].c <= HIGH_DOJI, \ + "bara +1 nu trebuie să confirme" +assert abs(cds_da[6].c - entry) < 1e-9 and abs(entry - 50.40) < 1e-9, \ + "intrarea nu e close-ul de 50.40 al barei +2" + +# 2. NU: niciun close din +1..+3 nu depășește 50.10; a 4-a închide sub 49.20 +assert all(cds_nu[i].c <= HIGH_DOJI for i in (5, 6, 7)), \ + "un close din +1..+3 ar confirma, fereastra n-ar fi expirat" +assert cds_nu[8].c < LOW_DOJI, f"close[+4]={cds_nu[8].c} nu e sub {LOW_DOJI}" + +# 3. ambele doji: corp <= 10% din amplitudine +for d in (doji_da, doji_nu): + assert abs(d.o - d.c) <= 0.10 * (d.h - d.l), "bara 4 nu e doji" + +# 4. aceeași serie până la doji (aceleași 5 bare) +ser_da = [(round(c.o, 9), round(c.h, 9), round(c.l, 9), round(c.c, 9)) for c in cds_da[:5]] +ser_nu = [(round(c.o, 9), round(c.h, 9), round(c.l, 9), round(c.c, 9)) for c in cds_nu[:5]] +assert ser_da == ser_nu, "panourile nu au aceeași serie până la doji" + +# --- desen: două Chart-uri, lipite vertical într-un singur SVG ------------- +def _panel(ch: Chart): + svg = ch.render() + m = re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', svg) + body = svg[svg.index(">") + 1:svg.rindex("")] + return body, float(m.group(2)) + + +# DA — confirmarea vine la +2 +cds_da[5].note = "+1: close 49.90 — sub 50.10, nu confirmă" +cds_da[6].note = "+2: close 50.40 — peste 50.10, confirmă (intrare la close)" +ch_da = Chart( + candles=cds_da, + levels=[Level(HIGH_DOJI, "high doji 50.10", "#b06000", label_at="left"), + Level(LOW_DOJI, "low doji 49.20", "#c00", label_at="left")], + marks=[Mark(4, "doji", "#b06000", "above"), + Mark(6, f"confirmare +2 — intrare {entry:.2f}", "#188a3e", "above")], + scale=SCALE, gap=40, body_w=13, left=44, width=W, + title="DA — confirmare la +2, în fereastră", +) + +# NU — nicio confirmare în 3 candele, apoi setup mort +cds_nu[5].note = "+1: 49.95; +2: 50.05; +3: 49.85 — toate sub 50.10" +cds_nu[8].note = "a 4-a: close 49.15 — sub low-ul dojiului 49.20, setup mort" +ch_nu = Chart( + candles=cds_nu, + levels=[Level(HIGH_DOJI, "high doji 50.10", "#b06000", label_at="left"), + Level(LOW_DOJI, "low doji 49.20", "#c00", label_at="left")], + marks=[Mark(4, "doji", "#b06000", "above"), + Mark(8, "a 4-a", "#c00", "above")], + scale=SCALE, gap=40, body_w=13, left=44, width=W, + title="NU — nicio confirmare în 3 candele", +) + +body_da, h_da = _panel(ch_da) +body_nu, h_nu = _panel(ch_nu) + +cap1 = "Serii construite: open = close-ul barei anterioare." +cap2 = "Doji: h 50.10 / l 49.20. Fereastra = maximum 3 candele după doji." +cap_top = h_da + VGAP + h_nu + 20 +H = cap_top + 22 + +parts = [ + f'', + f'{body_da}', + f'{body_nu}', + f'{cap1}', + f'{cap2}', + "", +] +svg = "\n ".join(parts) +assert "\n\n" not in svg, "linie goală în SVG" +assert "<" not in cap1 and "<" not in cap2 + +# --- verificarea citirii: etichete în panză, fără suprapuneri --------------- +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" # implicit în SVG + 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 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}") + +print(f"DA: doji {doji_da.h:.2f}/{doji_da.l:.2f} +1 {cds_da[5].c:.2f} " + f"+2 {entry:.2f} (confirmă) bare {len(cds_da)}") +print(f"NU: +1 {cds_nu[5].c:.2f} +2 {cds_nu[6].c:.2f} +3 {cds_nu[7].c:.2f} " + f"a 4-a {cds_nu[8].c:.2f} (< {LOW_DOJI}) bare {len(cds_nu)}") + +# 5. distanța în pixeli 50.10 -> 50.40 e vizibilă, cu scară comună +dist = ch_da._y(HIGH_DOJI) - ch_da._y(entry) +assert ch_da.scale == ch_nu.scale == SCALE, "scara nu e comună între panouri" +assert dist > 0 and dist > 10, f"distanța 50.10->50.40 = {dist:.1f}px, prea mică" +print(f"scris {OUT} ({W}x{H:.0f}); {len(labels)} etichete verificate; " + f"50.10->50.40 = {dist:.1f}px") +OUT.write_text(svg, encoding="utf-8") diff --git a/gen/strategii/buy_doji_fereastra.svg b/gen/strategii/buy_doji_fereastra.svg new file mode 100644 index 0000000..7ef8659 --- /dev/null +++ b/gen/strategii/buy_doji_fereastra.svg @@ -0,0 +1,60 @@ + + + DA — confirmare la +2, în fereastră + + high doji 50.10 + + low doji 49.20 + + + + + + + + + + + + + + + + + doji + confirmare +2 — intrare 50.40 + +1: close 49.90 — sub 50.10, nu confirmă + +2: close 50.40 — peste 50.10, confirmă (intrare la close) + + + NU — nicio confirmare în 3 candele + + high doji 50.10 + + low doji 49.20 + + + + + + + + + + + + + + + + + + + doji + a 4-a + +1: 49.95; +2: 50.05; +3: 49.85 — toate sub 50.10 + a 4-a: close 49.15 — sub low-ul dojiului 49.20, setup mort + + Serii construite: open = close-ul barei anterioare. + Doji: h 50.10 / l 49.20. Fereastra = maximum 3 candele după doji. + \ No newline at end of file diff --git a/gen/strategii/buy_doji_labu.py b/gen/strategii/buy_doji_labu.py new file mode 100644 index 0000000..c95cd06 --- /dev/null +++ b/gen/strategii/buy_doji_labu.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""LABU — Buy Doji la pivotul monthly (Nivel 2.5). + +Citește `gen/strategii/buy_doji_labu.spec.txt` și scrie +`gen/strategii/buy_doji_labu.svg`: 49 de candele daily, downtrend → triple +bottom → doji la pivotul monthly 86.00 → confirmare (close 89.50 peste +high-ul dojiului) → bull reversal → higher high, cu SL 81.45 (−9 %) și +TP 116.35 (+30 %). + +Toate cifrele din desen vin din specificație: pivot 86.00, intrare 89.50, +SL 81.45, TP 116.35, high/low-ul dojiului (86.30 / 84.20) și low-urile +triple bottom-ului (82.40 / 81.45 / 81.45). Procentele 9 % și 30 % sunt ale +cursului (reale); nivelurile de preț sunt construite de sumarizator — +legenda o spune explicit. + +Rulare: python3 gen/strategii/buy_doji_labu.py +""" +import re +import sys +from pathlib import Path + +sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, MA, Mark, from_closes # noqa: E402 + +OUT = Path(__file__).with_suffix(".svg") + +# --- nivelurile din specificație ------------------------------------------- +PIVOT = 86.00 # pivot monthly (construit) +ENTRY = 89.50 # intrare = close-ul confirmării (construit) +SL = 81.45 # SL = −9 % față de intrare (construit; procentul e real) +TP = 116.35 # TP = +30 % față de intrare (construit; procentul e real) +DOJI_H, DOJI_L = 86.30, 84.20 + +# --- seria de close-uri din specificație ----------------------------------- +closes = [ + # downtrend 0-14 + 108.0, 106.5, 105.0, 103.0, 101.5, 100.0, 98.0, 96.5, 95.0, 93.5, + 92.5, 91.0, 89.5, 87.5, 85.5, + # triple bottom 15 / 23 / 31 + 82.80, 84.5, 86.5, 88.0, 88.50, + 87.0, 85.0, 83.0, 81.50, + 83.5, 85.5, 87.0, 87.80, 87.0, 85.0, + 83.0, 81.60, 85.00, + 84.90, # 33 = doji (o 85.00, c 84.90, h 86.30, l 84.20) + 89.50, # 34 = confirmare, close > 86.30 => intrare + # bull reversal / trend up 35-48 + 91.0, 93.0, 92.0, 95.0, 97.0, 96.0, 99.0, 102.0, 101.0, 105.0, 108.0, + 112.0, 116.35, 118.0, +] +assert len(closes) == 49, f"seria are {len(closes)} bare, spec cere 49" + +# wick-uri: (up, down). Restul (0.4, 0.5); ajustate ca low-urile/high-urile +# cerute de asserts să iasă exact (bar 15/23/31 = triple bottom; bar 16/24 +# nu au voie să coboare sub piciorul din stânga; bar 33 = doji). +wicks = [(0.4, 0.5)] * len(closes) +wicks[15] = (0.4, 0.40) # low 82.40 +wicks[16] = (0.4, 0.20) # low 82.60 > 82.40 (piciorul 1 rămâne minim local) +wicks[23] = (0.4, 0.05) # low 81.45 +wicks[24] = (0.4, 0.00) # low 81.50 > 81.45 (piciorul 2 rămâne minim local) +wicks[31] = (0.4, 0.15) # low 81.45 +wicks[32] = (0.4, 0.10) # low 81.50 (nu sub 81.45) +wicks[33] = (1.30, 0.70) # doji: high 86.30, low 84.20 + +# volume construite: vânzare peste medie la triple bottom și la confirmare. +# Nu sunt numere din curs (nu sunt etichetate), doar panoul de volum cerut. +vols = [ + 55, 52, 48, 50, 45, 47, 44, 46, 42, 44, + 40, 42, 38, 41, 58, + 62, 50, 46, 44, 43, + 45, 44, 47, 66, 55, + 50, 47, 45, 44, 42, + 43, 45, 60, 72, 84, + 78, 70, 64, 60, 58, + 54, 52, 56, 60, 66, + 74, 82, 90, 98, +] +assert len(vols) == len(closes), f"volume {len(vols)} != bare {len(closes)}" + +cds = from_closes(closes, wicks=wicks, vols=vols, start=110.0) +assert abs(cds[0].o - 110.0) < 1e-9 and cds[0].c < cds[0].o, "prima bară nu e roșie" + +# nota explicativă a dojiului (bloc stânga-jos, nu se suprapune cu candelele) +cds[33].note = ("bara 33 = doji: o 85.00, c 84.90, h 86.30, l 84.20 — " + "corp mic exact pe pivotul monthly 86.00 (indecizie la suport)") + +# --- assert-urile din specificație (toate, ca assert reale) ---------------- +# 1. pivot 86.00 e straddled de doji +assert cds[33].l <= PIVOT <= cds[33].h, "doji-ul nu straddlează 86.00" + +# 2. confirmarea: close[34] == 89.50 == intrare și > high[33] +assert abs(cds[34].c - ENTRY) < 1e-9, f"close[34]={cds[34].c} != intrare 89.50" +assert cds[34].c > cds[33].h, "confirmarea nu închide peste high-ul dojiului" + +# 3. SL = min(low[30:35]) (regula: extrema ultimelor 4 candele de la confirmare) +min_30_35 = min(c.l for c in cds[30:35]) +assert abs(min_30_35 - SL) < 1e-9, f"min low[30:35]={min_30_35} != SL {SL}" + +# 4./5. procentele REALE ale cursului +assert round((ENTRY - SL) / ENTRY * 100, 1) == 9.0, "stopul nu iese −9 %" +assert round((TP - ENTRY) / ENTRY * 100, 1) == 30.0, "TP nu iese +30 %" + +# 6. triple bottom: exact trei minime locale sub 86.00, în zona 81-83, +# al doilea și al treilea fără low nou semnificativ față de primul +lows = [c.l for c in cds] +bottom_idx = [i for i in range(1, len(cds) - 1) + if lows[i] < lows[i - 1] and lows[i] < lows[i + 1] + and lows[i] < PIVOT and 81.0 <= lows[i] <= 83.0] +assert bottom_idx == [15, 23, 31], f"triple bottom la {bottom_idx}, nu [15, 23, 31]" +b1, b2, b3 = lows[15], lows[23], lows[31] +tol = 0.02 * b1 # „semnificativ" = > 2 % sub primul low +assert b2 >= b1 - tol and b3 >= b1 - tol, \ + f"al doilea/treilea picior coboară semnificativ sub primul: {b1}/{b2}/{b3}" +assert abs(b1 - 82.40) < 1e-9 and abs(b2 - SL) < 1e-9 and abs(b3 - SL) < 1e-9, \ + f"low-urile triple bottom: {b1}/{b2}/{b3}" + +# 7. TP atins: există i > 34 cu high[i] >= 116.35 +assert any(c.h >= TP for c in cds[35:]), "TP 116.35 nu e atins după intrare" + +# 8. doji: corp <= 10 % din amplitudine +assert abs(cds[33].o - cds[33].c) <= 0.10 * (cds[33].h - cds[33].l), "bara 33 nu e doji" +assert abs(cds[33].h - DOJI_H) < 1e-9 and abs(cds[33].l - DOJI_L) < 1e-9, \ + "doji h/l nu ies 86.30/84.20" + +# în plus: confirmarea vine cu volum peste medie (cerut de spec) +mean_vol = sum(vols) / len(vols) +assert vols[34] > mean_vol, f"volumul confirmării {vols[34]} <= media {mean_vol:.0f}" + +entry = cds[34].c +w = 820 # lățime mărită: 49 bare × gap 15 + etichetele din dreapta + # (spec sugera 760, dar ultima candelă ajunge la x=764) +ch = Chart( + candles=cds, + levels=[Level(PIVOT, "pivot monthly 86.00 (construit)", "#777", + dash="4 3", label_at="left"), + Level(SL, "SL 81.45 (construit) = −9 % (procent real)", "#c00", + label_at="left"), + Level(ENTRY, "intrare 89.50 (construit)", "#188a3e"), + Level(TP, "TP 116.35 (construit) = +30 % (procent real)", "#1a5fb4")], + marks=[Mark(15, f"bottom 1 {lows[15]:.2f}", "#c00", "below"), + Mark(23, f"bottom 2 {lows[23]:.2f}", "#c00", "below"), + Mark(31, f"bottom 3 {lows[31]:.2f}", "#c00", "below"), + Mark(33, "doji", "#b06000", "above"), + Mark(34, "confirmare (intrare)", "#188a3e", "above", dy=-14), + Mark(48, "higher high — TP atins", "#188a3e", "above")], + mas=[MA(20, "#b06000", dash="4 3", text="SMA-20")], + scale=8.0, gap=15, body_w=9, left=44, width=w, vol_h=70, + vol_label="volum", title="LABU — daily", + caption=("Procentele (stop 9 %, +30 %) sunt ale cursului; nivelurile de " + "preț sunt construite de sumarizator (18_2025-09-23.md:713)."), +) +svg = ch.render() +assert "\n\n" not in svg and "]*)>(.*?)', re.S) +H = float(re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', svg).group(2)) +labels = [] +for m in TEXT.finditer(svg): + x, y, attrs, s = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4) + anchor = "end" if 'text-anchor="end"' in attrs else \ + ("middle" if 'text-anchor="middle"' in attrs else "start") + bold = "bold" in attrs + wpx = len(s) * 12 * (0.62 if bold else 0.52) + left = x - wpx if anchor == "end" else (x - wpx / 2 if anchor == "middle" else x) + labels.append((left, left + wpx, y, s)) + +for left, right, y, s in labels: + assert left >= -2 and right <= w + 2, f"text iese din panză: {s!r} [{left:.0f},{right:.0f}]" + assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y:.0f})" + +for i in range(len(labels)): + li, ri, yi, si = labels[i] + for j in range(i + 1, len(labels)): + lj, rj, yj, sj = labels[j] + if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4: + raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}\n {labels[i]}\n {labels[j]}") + +# candelele sunt o serie continuă, lipită +xs = [ch._cx(i) for i in range(len(cds))] +assert xs[-1] + ch.body_w / 2 < w, f"ultima candelă iese din panză (x={xs[-1]})" +assert all(xs[i + 1] - xs[i] == ch.gap for i in range(len(xs) - 1)), "serie cu goluri" +for a, b in zip(cds, cds[1:]): + assert abs(a.c - b.o) < 1e-9, "open != close-ul barei anterioare" + +print(f"LABU: {len(cds)} bare, doji {cds[33].l:.2f}-{cds[33].h:.2f} peste pivot " + f"{PIVOT:.2f}, intrare {entry:.2f}, SL {SL:.2f} (−9 %), TP {TP:.2f} (+30 %)") +print(f"triple bottom: {b1:.2f} / {b2:.2f} / {b3:.2f} " + f"volum confirmare {vols[34]} > medie {mean_vol:.0f}") +print(f"scris {OUT} ({w}x{H:.0f}); {len(labels)} etichete verificate, fără suprapuneri") +OUT.write_text(svg, encoding="utf-8") diff --git a/gen/strategii/buy_doji_labu.svg b/gen/strategii/buy_doji_labu.svg new file mode 100644 index 0000000..a262517 --- /dev/null +++ b/gen/strategii/buy_doji_labu.svg @@ -0,0 +1,170 @@ + + LABU — daily + + pivot monthly 86.00 (construit) + + SL 81.45 (construit) = −9 % (procent real) + + intrare 89.50 (construit) + + TP 116.35 (construit) = +30 % (procent real) + + SMA-20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + bottom 1 82.40 + bottom 2 81.45 + bottom 3 81.45 + doji + confirmare (intrare) + higher high — TP atins + bara 33 = doji: o 85.00, c 84.90, h 86.30, l 84.20 — corp mic exact pe pivotul monthly 86.00 (indecizie la suport) + Procentele (stop 9 %, +30 %) sunt ale cursului; nivelurile de preț sunt construite de sumarizator (18_2025-09-23.md:713). + \ No newline at end of file diff --git a/gen/strategii/buy_doji_sell.py b/gen/strategii/buy_doji_sell.py new file mode 100644 index 0000000..067690a --- /dev/null +++ b/gen/strategii/buy_doji_sell.py @@ -0,0 +1,114 @@ +"""Sell Doji — oglinda short a ansamblului Buy Doji (daily, exemplu construit). + +Citește specificația `gen/strategii/buy_doji_sell.spec.txt`. Seria de 31 de +candele e construită din lista de close-uri dată, cu open-ul fiecărei bare = +close-ul precedentului; wick-urile sunt fixate ca nivelurile (doji 99.20 / +100.10, intrare short 99.00, SL 100.10, TP 97.90, trail 96.45, ieșire la 96.60) +să iasă exact din serie, nu doar din etichetă. +""" +import sys; sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, Mark, from_closes + +# --- seria: 31 de close-uri, exact ca în specificație ----------------------- +closes = [ + 93.00, 94.10, 93.70, 95.00, 96.10, 95.80, 97.00, 98.10, 97.80, 98.90, # 0-9 + 99.80, 100.20, 99.90, 99.65, # 10-13 uptrend + 99.60, # 14 doji + 99.40, # 15 +1 (nu confirmă) + 99.00, # 16 +2 confirmare => intrare short + 98.50, 98.30, 97.80, 97.40, 97.00, 96.70, 96.40, 96.10, 95.80, # 17-25 + 96.20, 96.60, 96.40, 96.10, 96.30, # 26-30 +] +assert len(closes) == 31 + +# wick-uri: (up, down) implicite (0.30, 0.25) + excepțiile din specificație +wicks = [(0.30, 0.25)] * 31 +# ajustat: cu wu=0.30, high[13] = 99.90+0.30 = 100.20 > SL; îl coborâm la +# 100.10 ca max(high[13:17]) == 100.10 (high-ul ultimelor 4 candele = SL) +wicks[13] = (0.20, 0.25) +wicks[14] = (0.45, 0.40) # doji: high 100.10, low 99.20 +wicks[15] = (0.20, 0.15) # high 99.80, low 99.25 (peste 99.20 => nu confirmă) +wicks[16] = (0.20, 0.15) # high 99.60, low 98.85 +wicks[25] = (0.35, 0.20) # high 96.45 (= trail), low 95.60 (minimul corecției) +# ajustat: cu wd=0.25, low[26] = 95.80-0.25 = 95.55 < 95.60, adică bara 26 ar +# fi minimul, nu bara 25; wd=0.10 păstrează bara 25 ca minim (low 95.60) +wicks[26] = (0.30, 0.10) + +# volum relativ ales de generator (materialul nu dă volum pe exemplu); +# peste medie pe doji (14) și pe confirmare (16) — distribuție — apoi normal +vols = [44, 42, 46, 40, 45, 48, 43, 47, 50, 45, + 52, 58, 49, 55, 90, 62, 84, + 56, 52, 50, 48, 46, 44, 42, 40, 45, + 50, 46, 44, 43, 45] +assert len(vols) == 31 +media_vol = sum(vols) / len(vols) +assert vols[14] > media_vol and vols[16] > media_vol, "volum peste medie pe doji/confirmare" + +cds = from_closes(closes, wicks=wicks, vols=vols, start=closes[0]) + +# adnotările pe bara +1 și pe confirmare, ca linii sub grafic (fără suprapunere) +cds[15].color = "#777" # bara de așteptare: neutră, nu confirmă +cds[15].note = "bara 15: close 99.40 peste low-ul dojiului 99.20 — NU confirmă" +cds[16].note = "bara 16: close 99.00 sub 99.20 — confirmare (intrare short)" + +doji, wait, conf = cds[14], cds[15], cds[16] +entry, sl, tp = 99.00, 100.10, 97.90 + +# --- cele 9 assert-uri cerute de specificație ------------------------------ +# 1. SL-ul e exact high-ul ultimelor 4 candele +assert abs(max(cd.h for cd in cds[13:17]) - 100.10) < 1e-9, "max high[13:17] != 100.10" +# 2. intrarea e close-ul barei 16 +assert abs(conf.c - entry) < 1e-9, f"close[16]={conf.c} != 99.00" +# 3. confirmarea închide sub low-ul dojiului +assert abs(doji.l - 99.20) < 1e-9 and conf.c < doji.l, "close[16] nu e sub low[14]" +# 4. bara +1 NU confirmă +assert wait.c >= doji.l, "bara 15 ar confirma, fereastra n-ar avea sens" +# 5. 1:1 exact +assert abs(abs(sl - entry) - abs(entry - tp)) < 1e-9 and abs((entry - tp) - 1.10) < 1e-9, \ + "risc != recompensă (nu e 1:1 exact)" +# 6. primul low <= TP după intrare e bara 19 +primul_tp = next(i for i in range(17, len(cds)) if cds[i].l <= tp) +assert primul_tp == 19, f"TP 97.90 atins întâi la bara {primul_tp}, nu 19" +assert abs(cds[18].l - 98.05) < 1e-9 and abs(cds[19].l - 97.55) < 1e-9, "low-urile 18/19 nu ies" +# 7. trailing = high[25] = 96.45; primul close peste el e bara 27; ieșirea e pe profit +trail = cds[25].h +assert abs(trail - 96.45) < 1e-9, f"high[25]={trail} != 96.45" +primul_exit = next(i for i in range(26, len(cds)) if cds[i].c > trail) +assert primul_exit == 27 and abs(cds[27].c - 96.60) < 1e-9, "ieșirea nu e bara 27 @ 96.60" +assert cds[27].c < entry, "ieșirea n-ar fi pe profit" +# 8. doji: corp <= 10% din amplitudine +assert abs(doji.o - doji.c) <= 0.10 * (doji.h - doji.l), "bara 14 nu e doji" +# 9. uptrend vizibil +assert cds[13].c > cds[0].c + 5, "uptrend prea mic" + +print(f"doji {doji.l:.2f}/{doji.h:.2f} intrare short {conf.c:.2f} " + f"SL {sl:.2f} TP {tp:.2f} trail {trail:.2f} ieșire bara 27 @ {cds[27].c:.2f}") + +# --- desenul ---------------------------------------------------------------- +ch = Chart( + candles=cds, + levels=[ + # toate etichetele în dreapta: în stânga sus se suprapuneau cu mark-ul + # dojiului (SL la y=62, text până în ~x196; doji la y=58, din ~x193) + Level(sl, "SL 100.10 (high 4 candele)", "#c00"), + Level(entry, "intrare short 99.00", "#188a3e"), + Level(tp, "TP 97.90 = 1R", "#1a5fb4"), + Level(trail, "trail 96.45", "#b06000"), + ], + marks=[ + Mark(14, "doji 99.20 / 100.10", "#b06000", "above"), + Mark(27, "ieșire pe trail 96.60", "#c00", "above", dy=-6), + ], + scale=30.0, gap=15, body_w=9, left=44, width=640, vol_h=70, + vol_label="volum", title="Sell Doji — daily (exemplu construit, fără ticker)", + # scurt: o linie de caption mai lungă de ~90 de caractere iese din viewBox + # (640px) și se taie la randare + caption="Intrare short la close sub low-ul dojiului, SL la high 4 candele, " + "TP 1:1, apoi trail.", +) + +svg = ch.render() +assert " + Sell Doji — daily (exemplu construit, fără ticker) + + SL 100.10 (high 4 candele) + + intrare short 99.00 + + TP 97.90 = 1R + + trail 96.45 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + doji 99.20 / 100.10 + ieșire pe trail 96.60 + bara 15: close 99.40 peste low-ul dojiului 99.20 — NU confirmă + bara 16: close 99.00 sub 99.20 — confirmare (intrare short) + Intrare short la close sub low-ul dojiului, SL la high 4 candele, TP 1:1, apoi trail. + \ No newline at end of file diff --git a/gen/strategii/hammer_mbt10_bh_ansamblu.py b/gen/strategii/hammer_mbt10_bh_ansamblu.py new file mode 100644 index 0000000..2971154 --- /dev/null +++ b/gen/strategii/hammer_mbt10_bh_ansamblu.py @@ -0,0 +1,118 @@ +"""Buy hammer — diagrama de ansamblu (daily, ticker ilustrativ XYZ). + +Citește specificația `gen/strategii/hammer_mbt10_bh_ansamblu.spec.txt`. +Seria de 34 de candele e construită din lista de close-uri dată, cu open-ul +fiecărei bare = close-ul precedentului; wick-urile sunt fixate ca nivelurile +(hammer H=C 63.30 / L 62.10, intrare 63.35, SL 62.05, TP 65.95) să iasă +exact din serie, nu doar din etichetă. +""" +import re +import sys; sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, Mark, from_closes + +# --- seria: 34 de close-uri, exact ca în specificație ------------------------ +closes = [ + 72.00, 71.20, 71.50, 70.40, 69.60, 70.00, 68.90, 68.10, 68.40, 67.30, # 0-9 + 66.50, 66.80, 65.70, 64.90, 65.20, 64.10, 63.40, 63.70, 63.05, # 10-18 + 63.30, # 19 = hammer: o 63.05, c/h 63.30, l 62.10 (H = C, fitil sus 0) + 63.35, # 20 = confirmare: close peste 63.30 => intrare + 63.80, 64.20, 64.60, 64.40, 64.90, 65.20, 65.50, 65.80, 66.10, # 21-29 + 66.00, 65.70, 65.40, 65.60, # 30-33 +] +assert len(closes) == 34 + +# wick-uri: (up, down) implicite (0.25, 0.30) + excepțiile din specificație +wicks = [(0.25, 0.30)] * 34 +wicks[19] = (0.00, 0.95) # hammer: high 63.30 (= close), low 62.10 +# bara 20: wu=0.15 => high 63.50. Spec-ul dă wd=0.15 cu „low 63.20", dar +# wd se măsoară de la min(o,c)=63.30, deci 0.15 dă 63.15; folosim tuplul +# literal din specificație (0.15, 0.15), low-ul barei 20 nu e etichetat. +wicks[20] = (0.15, 0.15) + +# volum relativ ales de generator (materialul nu dă volum pe exemplul construit): +# normal pe downtrend, în creștere pe hammer (19) și pe confirmare (20), normal după +vols = [50, 48, 47, 46, 45, 46, 44, 43, 45, 42, + 41, 43, 40, 39, 41, 38, 37, 39, 55, + 90, 95, + 58, 55, 53, 50, 52, 54, 56, 60, 64, + 58, 55, 52, 50] +assert len(vols) == 34 +media_vol = sum(vols) / len(vols) +assert vols[19] > media_vol and vols[20] > media_vol, "volum peste medie pe hammer/confirmare" + +cds = from_closes(closes, wicks=wicks, vols=vols, start=72.00) + +hammer, conf = cds[19], cds[20] +# confirmarea o punem ca notă sub grafic: un Mark deasupra barei 20 s-ar +# suprapune cu fitilul lung al hammer-ului (bara 19) +conf.note = "bara 20: confirmare — close 63.35 peste high-ul hammer-ului 63.30 (intrare)" +intrare, sl, tp = 63.35, 62.05, 65.95 + +# --- cele 8 assert-uri cerute de specificație ------------------------------- +# 1. hammer: H = C = 63.30, L = 62.10 +assert abs(cds[19].c - 63.30) < 1e-9 and abs(cds[19].h - 63.30) < 1e-9, "H != C la hammer" +assert abs(cds[19].l - 62.10) < 1e-9, f"low[19]={cds[19].l} != 62.10" +# 2. open[19] = close[18]; corp 0.25; fitil jos 0.95 >= 2*0.25 +assert abs(cds[19].o - cds[18].c) < 1e-9 and abs(cds[19].o - 63.05) < 1e-9, "open[19] != close[18]" +corp = hammer.c - hammer.o +fitil_jos = hammer.o - hammer.l +assert abs(corp - 0.25) < 1e-9, f"corp={corp} != 0.25" +assert abs(fitil_jos - 0.95) < 1e-9, f"fitil jos={fitil_jos} != 0.95" +assert fitil_jos >= 2 * corp, "fitilul nu e cel puțin dublul corpului" +# 3. confirmarea închide peste high-ul hammer-ului +assert abs(conf.c - intrare) < 1e-9, f"close[20]={conf.c} != 63.35" +assert conf.c > hammer.h and abs(hammer.h - 63.30) < 1e-9, "close[20] nu e peste high[19]" +# 4. SL = low-ul hammer-ului - 0.05 +assert abs(sl - (cds[19].l - 0.05)) < 1e-9, "SL != low[19] - 0.05" +# 5. risc 1.30, TP 2.60 = 2R exact +assert abs(abs(intrare - sl) - 1.30) < 1e-9, "risc != 1.30" +assert abs(abs(tp - intrare) - 2.60) < 1e-9, "TP != 2R exact" +# 6. primul high >= TP e bara 28 (65.80 + wick 0.25 = 66.05) +primul_tp = next(i for i in range(21, len(cds)) if cds[i].h >= tp) +assert primul_tp == 28, f"TP 65.95 atins întâi la bara {primul_tp}, nu 28" +# 7. downtrend vizibil +assert cds[18].c < cds[0].c - 8, "downtrend prea mic" +# 8. ordinea nivelurilor +assert sl < intrare < tp, "SL / intrare / TP nu sunt în ordine" + +print(f"hammer H/C {hammer.h:.2f} L {hammer.l:.2f} intrare {conf.c:.2f} " + f"SL {sl:.2f} TP {tp:.2f} = {abs(tp-intrare)/abs(intrare-sl):.1f}R " + f"TP atins la bara {primul_tp} (high {cds[primul_tp].h:.2f})") + +# --- desenul ---------------------------------------------------------------- +ch = Chart( + candles=cds, + levels=[ + Level(sl, "SL 62.05 — sub low-ul hammer-ului", "#c00", label_at="left"), + Level(intrare, "intrare 63.35", "#188a3e"), + Level(tp, "TP 65.95 = 2R", "#1a5fb4"), + ], + marks=[ + Mark(19, "hammer 63.30 / 62.10", "#b06000", "below"), + ], + scale=32.0, gap=15, body_w=9, left=44, width=720, vol_h=70, + vol_label="volum", title="Buy hammer — daily (ticker ilustrativ XYZ, din lecția de pattern)", + caption="", # legenda e pusă manual, pe două rânduri, ca să nu iasă din pânză +) + +svg = ch.render() +assert "", "".join( + f'{t}' + for i, t in enumerate(legenda)) + "") +assert "\n\n" not in svg, "linie goală în SVG după legendă" + +with open("/workspace/atm-curs-dl/gen/strategii/hammer_mbt10_bh_ansamblu.svg", "w") as f: + f.write(svg) +print("scris gen/strategii/hammer_mbt10_bh_ansamblu.svg") diff --git a/gen/strategii/hammer_mbt10_bh_ansamblu.svg b/gen/strategii/hammer_mbt10_bh_ansamblu.svg new file mode 100644 index 0000000..8f1a7d0 --- /dev/null +++ b/gen/strategii/hammer_mbt10_bh_ansamblu.svg @@ -0,0 +1,115 @@ + + Buy hammer — daily (ticker ilustrativ XYZ, din lecția de pattern) + + SL 62.05 — sub low-ul hammer-ului + + intrare 63.35 + + TP 65.95 = 2R + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + hammer 63.30 / 62.10 + bara 20: confirmare — close 63.35 peste high-ul hammer-ului 63.30 (intrare) +Exemplu ilustrativ (XYZ) din lecția de pattern (01_MODUL_1/05_patternuri_candele.md:210-215);prețurile sunt cele ale lecției, structura barelor dintre ele e construită. \ No newline at end of file diff --git a/gen/strategii/hammer_mbt10_bh_aplus.py b/gen/strategii/hammer_mbt10_bh_aplus.py new file mode 100644 index 0000000..fac805c --- /dev/null +++ b/gen/strategii/hammer_mbt10_bh_aplus.py @@ -0,0 +1,279 @@ +"""Buy hammer — A+ vs. mediocru (două panouri, exemple construite). + +Citește `gen/strategii/hammer_mbt10_bh_aplus.spec.txt` +(marcajul ``, Partea A §2.2). + +Același pattern ca formă (downtrend → hammer → confirmare), context opus: + +- stânga (A+): downtrend de 12 candele de la 50.00 spre 42.80, hammer-ul cu + low 41.90 chiar pe suportul monthly 42.00 (0,24 % sub nivel; corp 0.10, + fitil jos 0.90), confirmarea închide 42.95 peste high-ul 42.90, pe volum + peste media ultimelor 10 bare — „acumulare"; +- dreapta (NU): fără downtrend real (range lateral 44–46), hammer-ul în + mijlocul intervalului (o=45.00, c=h=45.10, l=44.10), departe de orice + nivel (suportul construit 42.50 e la 3,6 % sub low), confirmarea închide + tot 45.15 peste 45.10, dar pe volum slab — context invalid. + +Nivelurile 42.00 / 42.50 și cifrele de preț sunt construite (materialul nu +dă prețuri pentru acest contrast); regulile (locația la nivel, downtrend, +volum) sunt din curs. Două `Chart`-uri randate separat, lipite într-un SVG +wrapper: coordonatele fiecărui panou rămân corecte prin construcție. + +Notă de serie: specificația listează `close[11]=42.40` (A+) și `44.60` (NU), +dar descrie hammer-ul cu `o=42.80 / c=42.90`, respectiv `o=45.00 / c=45.10`, +iar assert 9 cere `close == high`. Cum open-ul fiecărei bare = close-ul +precedentei (STYLE.md), am ridicat close[11] la 42.80 / 45.00 și close[12] +la 42.90 / 45.10, ca seria să susțină exact OHLC-ul din etichetă. Restul +close-urilor sunt cele din specificație. + +Rulează: python3 gen/strategii/hammer_mbt10_bh_aplus.py +Scrie: gen/strategii/hammer_mbt10_bh_aplus.svg +""" +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__).resolve().parent / "hammer_mbt10_bh_aplus.svg" + +GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000" + +HAM_A, CONF_A = 12, 13 # indicii în panoul A+ +HAM_N, CONF_N = 12, 13 # indicii în panoul NU +SUPORT_A = 42.00 # suport monthly (A+) +SUPORT_N = 42.50 # cel mai apropiat nivel construit (NU) + +# ============================================================ PANOUL A+ (20 bare) +# 12 bare de downtrend 50.00 -> 42.80, hammer la 42.00, confirmare, apoi urcare. +CLOSES_A = [ + 50.00, 49.10, 49.30, 48.20, 47.30, 47.60, 46.40, 45.50, 44.60, 43.70, + 43.00, 42.80, + 42.90, # 12 = hammer: o=42.80, c=h=42.90, l=41.90 (H=C, fitil jos 0.90) + 42.95, # 13 = confirmare, close > 42.90 => intrare + 43.40, 43.80, 44.20, 44.00, 44.50, 44.80, +] +assert len(CLOSES_A) == 20, len(CLOSES_A) + +WICKS_A = [(0.25, 0.30)] * 20 +WICKS_A[HAM_A] = (0.00, 0.90) # high 42.90, low 41.90 (pe suportul 42.00) +WICKS_A[CONF_A] = (0.20, 0.20) # confirmarea: o=42.90, c=42.95 + +# Volum ales de generator (materialul nu dă volum): scade pe downtrend, climax +# pe hammer, spike pe confirmare („acumulare"), moderat după. +VOLS_A = [54, 52, 51, 49, 48, 47, 46, 45, 44, 43, + 42, 45, + 64, # 12 = hammer: exces de vânzare + 90, # 13 = confirmare: volum peste medie + 74, 70, 66, 62, 60, 58] +assert len(VOLS_A) == 20 + +cds_a = from_closes(CLOSES_A, wicks=WICKS_A, vols=VOLS_A, start=50.60) +ham_a, conf_a = cds_a[HAM_A], cds_a[CONF_A] + +# ============================================================ PANOUL NU (20 bare) +# range lateral 44-46, fără downtrend; hammer în mijloc, confirmare pe volum slab. +CLOSES_N = [ + 46.00, 45.30, 45.80, 45.10, 45.60, 44.90, 45.40, 44.80, 45.20, 44.70, + 45.10, 45.00, + 45.10, # 12 = hammer: o=45.00, c=h=45.10, l=44.10 (H=C, fitil jos 0.90) + 45.15, # 13 = confirmare, close > 45.10 + 44.90, 44.70, 44.40, 44.20, 44.00, 43.80, +] +assert len(CLOSES_N) == 20, len(CLOSES_N) + +WICKS_N = [(0.25, 0.30)] * 20 +WICKS_N[HAM_N] = (0.00, 0.90) # high 45.10, low 44.10 +WICKS_N[CONF_N] = (0.20, 0.20) + +# Volum ales de generator: lateral, constant, confirmarea pe volum slab. +VOLS_N = [52, 50, 53, 49, 51, 48, 52, 50, 49, 51, + 50, 49, + 56, # 12 + 31, # 13 = confirmare: volum slab + 52, 50, 48, 51, 49, 47] +assert len(VOLS_N) == 20 + +cds_n = from_closes(CLOSES_N, wicks=WICKS_N, vols=VOLS_N, start=45.60) +ham_n, conf_n = cds_n[HAM_N], cds_n[CONF_N] + +# ============================================================ assert-urile din spec +# 1. A+: low[12] la <= 0,5 % de 42.00 (41.90 e la 0,24 %) +dist_a = abs(ham_a.l - SUPORT_A) / SUPORT_A +assert dist_a <= 0.005, f"low hammer A+ {ham_a.l} la {dist_a * 100:.2f} % de 42.00" +# low-ul hammer-ului chiar trece sub nivel (fitilul sparge suportul) +assert ham_a.l < SUPORT_A <= ham_a.h, "hammer-ul A+ nu are fitilul pe/la nivel" +# 2. A+: close[13] > high[12] (tehnic valid) +assert conf_a.c > ham_a.h, f"A+ close {conf_a.c} <= high hammer {ham_a.h}" +assert abs(conf_a.c - 42.95) < 1e-9 +# 3. A+: volumul pe bara 13 > media volumelor pe 10 bare +media_a = sum(VOLS_A[CONF_A - 10:CONF_A]) / 10 +assert VOLS_A[CONF_A] > media_a, f"vol confirmare A+ {VOLS_A[CONF_A]} <= media {media_a}" +# 4. A+: close[12] < close[0] - 5 (downtrend vizibil) +assert cds_a[HAM_A].c < cds_a[0].c - 5, "downtrendul A+ nu e vizibil" +# 5. NU: (low[12] - 42.50) / 42.50 > 0.02 (hammer-ul NU e departe de nivel) +dist_n = (ham_n.l - SUPORT_N) / SUPORT_N +assert dist_n > 0.02, f"low hammer NU {ham_n.l} prea aproape de 42.50 ({dist_n:.1%})" +# eticheta „la -3,6 %" din spec: (44.10 - 42.50) / 44.10 +pct_n = (ham_n.l - SUPORT_N) / ham_n.l * 100 +assert round(pct_n, 1) == 3.6, f"procentul NU e {pct_n:.2f} %, nu 3,6 %" +# 6. NU: close[13] > high[12] (tehnic valid) +assert conf_n.c > ham_n.h, f"NU close {conf_n.c} <= high hammer {ham_n.h}" +assert abs(conf_n.c - 45.15) < 1e-9 +# 7. NU: volumul pe bara 13 < media volumelor pe 10 bare +media_n = sum(VOLS_N[CONF_N - 10:CONF_N]) / 10 +assert VOLS_N[CONF_N] < media_n, f"vol confirmare NU {VOLS_N[CONF_N]} >= media {media_n}" +# 8. NU: |close[12] - close[0]| <= 1.5 (fără downtrend real, range lateral) +assert abs(cds_n[HAM_N].c - cds_n[0].c) <= 1.5, "NU nu e range lateral" +# 9. ambele hammer: fitil jos >= 2x corp si close == high (fitil de sus inexistent) +for tag, o, l, c, h in ( + ("A+", ham_a.o, ham_a.l, ham_a.c, ham_a.h), + ("NU", ham_n.o, ham_n.l, ham_n.c, ham_n.h)): + assert (o - l) >= 2 * (c - o), f"fitilul hammer-ului {tag} < 2x corpul" + assert abs(c - h) < 1e-9, f"hammer-ul {tag} are fitil de sus" +# OHLC-ul descris în spec (eticheta e adevărul) +assert abs(ham_a.o - 42.80) < 1e-9 and abs(ham_a.c - 42.90) < 1e-9 and abs(ham_a.l - 41.90) < 1e-9 +assert abs(ham_n.o - 45.00) < 1e-9 and abs(ham_n.c - 45.10) < 1e-9 and abs(ham_n.l - 44.10) < 1e-9 +# context: hammer-ul A+ e la baza downtrendului, NU nu are downtrend +assert min(c.l for c in cds_a[:HAM_A]) > ham_a.l, "low-ul hammer-ului A+ nu e minimul" +assert abs(cds_a[-1].c - 44.80) < 1e-9 and cds_a[-1].c > conf_a.c, "A+ nu urcă după intrare" + +print(f"A+ hammer {ham_a.o:.2f}/{ham_a.c:.2f}/{ham_a.h:.2f}/{ham_a.l:.2f} " + f"conf {conf_a.c:.2f} > high {ham_a.h:.2f} vol {VOLS_A[CONF_A]} > medie {media_a:.1f}") +print(f"NU hammer {ham_n.o:.2f}/{ham_n.c:.2f}/{ham_n.h:.2f}/{ham_n.l:.2f} " + f"conf {conf_n.c:.2f} > high {ham_n.h:.2f} vol {VOLS_N[CONF_N]} < medie {media_n:.1f}") +print(f"NU low {ham_n.l:.2f} vs suport {SUPORT_N:.2f} = -{pct_n:.1f} %") + +# ============================================================ desenele celor două panouri +PW = 430 # lățimea fiecărui panou +SCALE = 26.0 # aceeași scară în ambele panouri (comparație cinstită) +ch_a = Chart( + candles=cds_a, + levels=[Level(SUPORT_A, "suport monthly 42.00", GREEN, label_at="left")], + marks=[Mark(HAM_A, "hammer la nivel (low 41.90)", ORANGE, "below", dy=0), + Mark(CONF_A, "acumulare", GREEN, "above", dy=-26)], + mas=[], scale=SCALE, gap=15, body_w=9, left=44, width=PW, vol_h=60, + vol_label="volum", + title="A+ — hammer la suport monthly, volum în acumulare", + caption="bara 13: 42.95 > high 42.90, pe volum peste medie.", +) +ch_n = Chart( + candles=cds_n, + levels=[Level(SUPORT_N, "fără nivel apropiat (suport 42.50 la −3,6 %)", GRAY, + label_at="left")], + marks=[Mark(HAM_N, "hammer în mijloc de range", ORANGE, "below", dy=0), + Mark(CONF_N, "volum slab", RED, "above", dy=0)], + mas=[], scale=SCALE, gap=15, body_w=9, left=44, width=PW, vol_h=60, + vol_label="volum", + title="Mediocru — hammer în mijloc de range, fără nivel", + caption="bara 13: 45.15 > 45.10, dar pe volum slab.", +) + +# ============================================================ 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, 38 +svg_a, svg_n = ch_a.render(), ch_n.render() + +HA, HN = vb_height(svg_a), vb_height(svg_n) +assert svg_a.count("\n\n") == 0 and svg_n.count("\n\n") == 0, "linie goală în panou" + +TOTAL_W = PAD + PW + PANEL_GAP + PW + PAD +TOTAL_H = PAD + TITLE_H + max(HA, HN) + FOOTER_H + PAD +OFF_A = (PAD, PAD + TITLE_H) +OFF_N = (PAD + PW + PANEL_GAP, PAD + TITLE_H) + +TOP_TITLE = "Buy hammer A+ vs. mediocru (două panouri, exemple construite)" +FOOTER1 = ("Reguli din curs: 26_2025-06-11.md:701-703 (locația la nivel), " + "05_patternuri_candele.md:77-80, 05_volum.md:2169.") +FOOTER2 = ("Nivelurile 42.00 / 42.50 și cifrele de preț sunt construite " + "(materialul nu dă prețuri pentru acest contrast).") +FOOTER = FOOTER1 + " " + FOOTER2 + +wrapper = ( + f'' + f'{TOP_TITLE}' + f'{inner(svg_a)}' + f'{inner(svg_n)}' + f'{FOOTER1}' + f'{FOOTER2}' + f'' +) + +# ============================================================ verificare 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_n), OFF_N)): + 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} ({gl:.0f}..{gr:.0f})" + 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 + footer), în coordonate globale +for x, y, anchor, t in ((TOTAL_W / 2, 20.0, "middle", TOP_TITLE), + (TOTAL_W / 2, TOTAL_H - PAD - 14, "middle", FOOTER1), + (TOTAL_W / 2, TOTAL_H - PAD, "middle", FOOTER2)): + 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) >= 10, "prea puține etichete" + +# 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 monthly 42.00", "hammer la nivel", "acumulare", + "fără nivel apropiat (suport 42.50 la −3,6 %)", "volum slab", + "A+ — hammer la suport monthly, volum în acumulare", + "Mediocru — hammer în mijloc de range, fără nivel"): + 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") diff --git a/gen/strategii/hammer_mbt10_bh_aplus.svg b/gen/strategii/hammer_mbt10_bh_aplus.svg new file mode 100644 index 0000000..eae82ef --- /dev/null +++ b/gen/strategii/hammer_mbt10_bh_aplus.svg @@ -0,0 +1,139 @@ +Buy hammer A+ vs. mediocru (două panouri, exemple construite) + A+ — hammer la suport monthly, volum în acumulare + + suport monthly 42.00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + hammer la nivel (low 41.90) + acumulare + bara 13: 42.95 > high 42.90, pe volum peste medie. + + Mediocru — hammer în mijloc de range, fără nivel + + fără nivel apropiat (suport 42.50 la −3,6 %) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + hammer în mijloc de range + volum slab + bara 13: 45.15 > 45.10, dar pe volum slab. +Reguli din curs: 26_2025-06-11.md:701-703 (locația la nivel), 05_patternuri_candele.md:77-80, 05_volum.md:2169.Nivelurile 42.00 / 42.50 și cifrele de preț sunt construite (materialul nu dă prețuri pentru acest contrast). \ No newline at end of file diff --git a/gen/strategii/hammer_mbt10_bh_labu.py b/gen/strategii/hammer_mbt10_bh_labu.py new file mode 100644 index 0000000..f2432c8 --- /dev/null +++ b/gen/strategii/hammer_mbt10_bh_labu.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""LABU — buy hammer la suportul monthly, +20 % în jumătate de lună (Nivel 2.5). + +Citește `gen/strategii/hammer_mbt10_bh_labu.spec.txt` și scrie +`gen/strategii/hammer_mbt10_bh_labu.svg`: 38 de candele daily, downtrend + +consolidare până pe suportul monthly 80.00, hammer chiar pe nivel +(o 80.80 / h 80.90 / l 79.60 / c 80.90), confirmarea de la 81.30 (close peste +high-ul hammer-ului) și urcarea în ~11 bare până la 97.56 = +20,0 % față de +intrare. + +REAL din curs (`18_2025-09-23.md:721-726`): că e buy hammer la suportul +monthly și că a dat +20 % în jumătate de lună. CONSTRUITE de sumarizator: +suportul 80.00, cifrele hammer-ului (80.80/80.90/79.60), intrarea 81.30, SL +79.60 și seria de prețuri — materialul nu dă entry, SL sau TP pentru cazul +ăsta. Legenda din desen spune exact asta. + +Rulare: python3 gen/strategii/hammer_mbt10_bh_labu.py +""" +import re +import sys +from pathlib import Path + +sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, MA, Mark, from_closes # noqa: E402 + +OUT = Path(__file__).with_suffix(".svg") + +# --- nivelurile din specificație (toate CONSTRUITE) ------------------------ +SUPPORT = 80.00 # suport monthly (construit) +HAMMER_H = 80.90 # high-ul hammer-ului = close (H=C) +HAMMER_L = 79.60 # low-ul hammer-ului = SL propus +ENTRY = 81.30 # intrare = close-ul confirmării, peste 80.90 (construit) +SL = 79.60 # SL sub low-ul hammer-ului (construit) +PEAK = 97.56 # max close de după intrare: 97.56 / 81.30 = +20,0 % (real) + +# --- seria de close-uri din specificație (38 bare, o = close precedent) ---- +closes = [ + # downtrend + consolidare 0-22 + 92.00, 90.50, 91.20, 89.40, 88.10, 88.60, 87.00, 85.80, 86.30, 84.90, + 83.60, 82.40, 83.00, 81.80, 80.70, 81.20, 80.20, 80.80, 81.50, 80.40, + 80.10, 80.60, 80.80, + 80.90, # 23 = hammer: o 80.80, c 80.90, h 80.90, l 79.60 + 81.30, # 24 = confirmare, close > high[23] = intrare + # 25..37 = 13 bare de urcare; max close 97.56 / 81.30 - 1 = +20,0 % + 81.90, 82.60, 83.40, 84.20, 85.10, 86.00, 87.00, 88.10, 89.30, 90.60, + 92.00, 93.50, 97.56, +] +assert len(closes) == 38, f"seria are {len(closes)} bare, spec cere 38" + +# wick-uri: (up, down). Restul (0.30, 0.40); hammer-ul are fitil jos 1.20 și +# zero fitil sus ca low-ul/high-urile cerute de asserts să iasă exact. +wicks = [(0.30, 0.40)] * len(closes) +wicks[23] = (0.00, 1.20) # hammer: h 80.90, l 79.60 + +# volume construite (nu sunt cifre de curs, doar panoul de volum cerut de +# stil): vânzare în creștere spre suport, explozie la hammer + confirmare. +vols = [ + 52, 50, 47, 49, 45, 46, 43, 45, 41, 43, + 44, 47, 42, 45, 48, 46, 50, 44, 43, 46, + 51, 49, 55, 88, 84, + 70, 62, 58, 54, 52, 50, 48, 46, 45, 47, + 55, 62, 72, +] +assert len(vols) == len(closes), f"volume {len(vols)} != bare {len(closes)}" + +cds = from_closes(closes, wicks=wicks, vols=vols, start=94.00) +assert abs(cds[0].o - 94.00) < 1e-9 and cds[0].c < cds[0].o, "prima bară nu e roșie" +for a, b in zip(cds, cds[1:]): + assert abs(a.c - b.o) < 1e-9, "open != close-ul barei anterioare" + +# --- assert-urile din specificație (toate, ca assert reale) ---------------- +# 1. hammer-ul stă pe suportul monthly 80.00 +assert cds[23].l == 79.60 <= SUPPORT <= cds[23].h == HAMMER_H, \ + f"hammer 79.60 <= 80.00 <= 80.90 nu iese: {cds[23].l}/{cds[23].h}" + +# 2. forma hammer-ului: o = close[22] = 80.80, corp 0.10, fitil jos 1.20, +# 1.20 >= 2 * 0.10, close == high (fără fitil sus) +assert cds[23].o == cds[22].c == 80.80, "open[23] nu e close[22] = 80.80" +body = abs(cds[23].c - cds[23].o) +lower_wick = min(cds[23].o, cds[23].c) - cds[23].l +upper_wick = cds[23].h - max(cds[23].o, cds[23].c) +assert abs(body - 0.10) < 1e-9, f"corpul hammer-ului e {body}, nu 0.10" +assert abs(lower_wick - 1.20) < 1e-9, f"fitilul jos e {lower_wick}, nu 1.20" +assert abs(upper_wick) < 1e-9, f"hammer-ul are fitil sus {upper_wick}" +assert lower_wick >= 2 * body, f"1.20 < 2 * {body} (nu e hammer valid)" +assert cds[23].c == cds[23].h == HAMMER_H, "close[23] != high[23] == 80.90" + +# 3. confirmarea: close[24] == 81.30 == intrare și > high[23] == 80.90 +assert abs(cds[24].c - ENTRY) < 1e-9, f"close[24]={cds[24].c} != intrare 81.30" +assert cds[24].c > cds[23].h == HAMMER_H, "confirmarea nu închide peste 80.90" + +# 4. SL = min(low[22:25]) = low-ul hammer-ului = 79.60 +min_22_25 = min(c.l for c in cds[22:25]) +assert abs(min_22_25 - SL) < 1e-9, f"min low[22:25]={min_22_25} != SL {SL}" +assert abs(SL - cds[23].l) < 1e-9, "SL nu e low-ul hammer-ului" + +# 5. procentul REAL al cursului: +20,0 % de la intrare la vârf +peak = max(c.c for c in cds[25:38]) +pct = round((peak / ENTRY - 1) * 100, 1) +assert abs(peak - PEAK) < 1e-9, f"max close[25:38]={peak} != 97.56" +assert pct == 20.0, f"procentul iese {pct}, nu +20,0 %" +assert abs(peak / ENTRY - 1.20) < 1e-9, "97.56 / 81.30 nu e exact 1.20" + +# 6. downtrend/consolidare înainte de hammer +assert cds[23].c < cds[0].c - 8, f"close[23]={cds[23].c} nu e cu 8 sub close[0]" + +# 7. ordinea nivelurilor +assert SL < ENTRY < peak, f"SL/entry/vârf: {SL}/{ENTRY}/{peak}" + +# --- verificarea serii (stil + geometrie) ---------------------------------- +entry = cds[24].c +w = 760 +ch = Chart( + candles=cds, + levels=[ + Level(SUPPORT, "suport monthly 80.00", "#777", + dash="4 3", label_at="left"), + Level(SL, "SL sub low-ul hammer-ului 79.60 (construit)", "#c00", + label_at="right"), + Level(ENTRY, "intrare 81.30 (construit)", "#188a3e", label_at="right"), + ], + marks=[ + Mark(23, "hammer 80.90 / 79.60 (construit)", "#b06000", "below"), + Mark(37, f"+20 % (cifra cursului): {peak:.2f} / {ENTRY:.2f}", + "#188a3e", "above"), + ], + mas=[MA(20, "#b06000", dash="4 3", text="SMA-20")], + scale=8.0, gap=15, body_w=9, left=44, width=w, vol_h=70, + vol_label="volum", title="LABU — daily", + caption="nivelurile de preț sunt construite de sumarizator.", +) +# note pe bare: explicațiile hammer-ului/confirmării și legenda obligatorie +# (textul lung de nivel ar intra peste candele, deci stă în blocul de jos). +cds[0].note = ("bara 23 = hammer: o 80.80, h 80.90, l 79.60, c 80.90 — " + "corp 0.10, fitil jos 1.20 (H=C), pe suportul 80.00") +cds[1].note = ("bara 24 = confirmare: close 81.30 — close peste high-ul " + "hammer-ului 80.90 = intrare 81.30 (construit)") +cds[2].note = ("Procentul (+20 % în jumătate de lună) și contextul (suport " + "monthly) sunt ale cursului (18_2025-09-23.md:721-726);") + +svg = ch.render() +assert "\n\n" not in svg and "]*)>(.*?)', re.S) +H = float(re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', svg).group(2)) +labels = [] +for m in TEXT.finditer(svg): + x, y, attrs, s = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4) + anchor = "end" if 'text-anchor="end"' in attrs else \ + ("middle" if 'text-anchor="middle"' in attrs else "start") + bold = "bold" in attrs + wpx = len(s) * 12 * (0.62 if bold else 0.52) + left = x - wpx if anchor == "end" else (x - wpx / 2 if anchor == "middle" else x) + labels.append((left, left + wpx, y, s)) + +for left, right, y, s in labels: + assert left >= -2 and right <= w + 2, f"text iese din panză: {s!r} [{left:.0f},{right:.0f}]" + assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y:.0f})" + +for i in range(len(labels)): + li, ri, yi, si = labels[i] + for j in range(i + 1, len(labels)): + lj, rj, yj, sj = labels[j] + if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4: + raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}\n {labels[i]}\n {labels[j]}") + +# candelele sunt o serie continuă, lipită +xs = [ch._cx(i) for i in range(len(cds))] +assert xs[-1] + ch.body_w / 2 < w, f"ultima candelă iese din panză (x={xs[-1]})" +assert all(xs[i + 1] - xs[i] == ch.gap for i in range(len(xs) - 1)), "serie cu goluri" + +print(f"LABU: {len(cds)} bare, hammer {cds[23].l:.2f}/{cds[23].h:.2f} pe suport " + f"{SUPPORT:.2f}, intrare {entry:.2f}, SL {cds[23].l:.2f}, vârf {peak:.2f}") +print(f"procent: {peak:.2f} / {entry:.2f} - 1 = +{pct:.1f} % (cifra cursului)") +print(f"scris {OUT} ({w}x{H:.0f}); {len(labels)} etichete verificate, fără suprapuneri") +OUT.write_text(svg, encoding="utf-8") diff --git a/gen/strategii/hammer_mbt10_bh_labu.svg b/gen/strategii/hammer_mbt10_bh_labu.svg new file mode 100644 index 0000000..e385f25 --- /dev/null +++ b/gen/strategii/hammer_mbt10_bh_labu.svg @@ -0,0 +1,133 @@ + + LABU — daily + + suport monthly 80.00 + + SL sub low-ul hammer-ului 79.60 (construit) + + intrare 81.30 (construit) + + SMA-20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + hammer 80.90 / 79.60 (construit) + +20 % (cifra cursului): 97.56 / 81.30 + bara 23 = hammer: o 80.80, h 80.90, l 79.60, c 80.90 — corp 0.10, fitil jos 1.20 (H=C), pe suportul 80.00 + bara 24 = confirmare: close 81.30 — close peste high-ul hammer-ului 80.90 = intrare 81.30 (construit) + Procentul (+20 % în jumătate de lună) și contextul (suport monthly) sunt ale cursului (18_2025-09-23.md:721-726); + nivelurile de preț sunt construite de sumarizator. + \ No newline at end of file diff --git a/gen/strategii/hammer_mbt10_harta.py b/gen/strategii/hammer_mbt10_harta.py new file mode 100644 index 0000000..5674e22 --- /dev/null +++ b/gen/strategii/hammer_mbt10_harta.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +"""Hartă de orientare: buy hammer (public) vs. MBT-10 (cutie neagră), același +trigger de bottom. + +Pentru `strategii/06_hammer_mbt10.md`, marcajul ``. +Nu e grafic de preț (spec: TIP CONCEPT, CHEIE harta) — SVG scris de mână, +doar stdlib; nu se folosește svg_candles / pchart. + +Ce arată (toate citatele și trimiterile vin din spec, nu se inventează): +- bara comună de sus: setup-urile de fond spun UNDE, trigger-ul de candelă + spune CÂND (`02_mbt10.md:9-11`, `43-49`); +- două cutii alăturate: BUY HAMMER (pattern standard, public) și MBT-10 + (cutie neagră, indicator proprietar), fiecare cu regulile ei și cu + trimiterile de rigoare; +- săgeata de jos spre caseta finală: ce rămâne utilizabil pe BVB. + +Verdictele de BVB din cutii sunt judecata mea (secțiunea Transfer), marcate +ca atare în legendă, exact cum cere specul. + +Rulare: python3 gen/strategii/hammer_mbt10_harta.py -> hammer_mbt10_harta.svg +""" +from pathlib import Path +from xml.sax.saxutils import escape + +OUT = Path(__file__).with_suffix(".svg") + +W, H = 700, 610 +BLACK, GRAY, BLUE, GREEN, RED, ORANGE, WHITE = ( + "#000000", "#777777", "#1a5fb4", "#188a3e", "#c00", "#b06000", "#ffffff") +BORDER = "#333333" + +# ---------------------------------------------------------------- geometrie +BAR_X0, BAR_X1, BAR_Y, BAR_H = 40.0, 660.0, 82.0, 76.0 + +BOX_W = 302.0 +BOX_LX, BOX_RX, BOX_Y = 40.0, 358.0, 190.0 +BOX_H = 256.0 +PAD = 16.0 + +ROW0 = BOX_Y + 56.0 # baseline-ul primei reguli +ROW_H = 52.0 +VERDICT_SEP = BOX_Y + 216.0 +VERDICT_Y = VERDICT_SEP + 26.0 + +BUS_Y = 460.0 # linia orizontală de unire a celor două cutii +TRI_TIP = 500.0 +FINAL_X0, FINAL_Y, FINAL_W, FINAL_H = 110.0, 500.0, 480.0, 56.0 + +TEXTS = [] # (x, y, text, size, weight, anchor) pentru verificare + + +def txt(x, y, s, size=13, weight="400", fill=BLACK, anchor="middle"): + TEXTS.append((x, y, s, size, weight, anchor)) + return ( + f'{escape(s)}' + ) + + +def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None): + d = f' stroke-dasharray="{dash}"' if dash else "" + return (f'') + + +def rect(x, y, w, h, fill=WHITE, stroke=GRAY, width=1.2): + return (f'') + + +def box(bx, title, title_fill, rows, verdict, verdict_fill, fill): + """O cutie cu chenar: titlu, reguli (linii + trimitere), verdict.""" + x = bx + PAD + area = BOX_W - 2 * PAD + out = [rect(bx, BOX_Y, BOX_W, BOX_H, fill=fill, stroke=title_fill, width=1.4)] + out.append(txt(x, BOX_Y + 26, title, 15, "700", title_fill, "start")) + out.append(line(bx + PAD, BOX_Y + 38, bx + BOX_W - PAD, BOX_Y + 38, + title_fill, 1.0)) + for i, (mains, ref) in enumerate(rows): + top = ROW0 + i * ROW_H + for k, m in enumerate(mains): + out.append(txt(x, top + 14 + k * 17, m, 13, "400", BLACK, "start")) + if ref is not None: + out.append(txt(x, top + 48, ref, 13, "400", GRAY, "start")) + out.append(line(bx + PAD, VERDICT_SEP, bx + BOX_W - PAD, VERDICT_SEP, + verdict_fill, 1.2)) + out.append(txt(x, VERDICT_Y, verdict, 13, "700", verdict_fill, "start")) + return out, area + + +def build(): + p = [ + f'', + '', + f'', + txt(W / 2, 34, "Același trigger de bottom, două naturi", 19, "700", BLACK), + txt(W / 2, 58, "buy hammer (public) vs. MBT-10 (cutie neagră)", + 14, "400", GRAY), + ] + + # ------- zona 1: bara comună + p.append(rect(BAR_X0, BAR_Y, BAR_X1 - BAR_X0, BAR_H, fill="#f2f4f7", + stroke=GRAY, width=1.2)) + p.append(txt(BAR_X0 + PAD, BAR_Y + 26, "Trigger de bottom: unde + când", + 15, "700", BLUE, "start")) + p.append(txt(BAR_X0 + PAD, BAR_Y + 48, + "Setup-urile de fond — suport, locul crimei, double bottom, buy " + "doji,", 13, "400", BLACK, "start")) + p.append(txt(BAR_X0 + PAD, BAR_Y + 66, + "divergență de volum — spun UNDE. Trigger-ul de candelă spune " + "CÂND.", 13, "400", BLACK, "start")) + p.append(txt(BAR_X1 - PAD, BAR_Y + 26, "02_mbt10.md:9-11, 43-49", + 13, "400", GRAY, "end")) + + # ------- conector bară -> cutii (un cot, fără săgeți redundante) + lx = BOX_LX + BOX_W / 2 + rx = BOX_RX + BOX_W / 2 + p.append(line(W / 2, BAR_Y + BAR_H, W / 2, 172.0, GRAY, 1.2)) + p.append(line(lx, 172.0, rx, 172.0, GRAY, 1.2)) + p.append(line(lx, 172.0, lx, BOX_Y, GRAY, 1.2)) + p.append(line(rx, 172.0, rx, BOX_Y, GRAY, 1.2)) + + # ------- zona 2: cele două cutii + left_rows = [ + (['„formă de candelă + downtrend”'], "05_patternuri_candele.md:77-88"), + (['„trigger: close peste', 'high-ul hammer-ului”'], + "05_patternuri_candele.md:99-124"), + (['„fără niciun indicator proprietar”'], None), + ] + right_rows = [ + (['„formula nu e publică nicăieri”'], "02_mbt10.md:21-39"), + (['„reconstituibil doar:', 'close peste pivotul daily”'], + "02_mbt10.md:179-188"), + (['„scanner + indicator proprietar”'], "02_mbt10.md:853-863"), + ] + left_svg, left_area = box( + BOX_LX, "BUY HAMMER — pattern standard", BLUE, left_rows, + "„pe BVB: se poate rula long-only”", GREEN, "#eef4fd") + right_svg, right_area = box( + BOX_RX, "MBT-10 — cutie neagră", BORDER, right_rows, + "„pe BVB: nu, ca sistem”", RED, "#f5f5f5") + p += left_svg + right_svg + + # ------- zona 3: ambele cutii -> caseta finală + p.append(line(lx, BOX_Y + BOX_H, lx, BUS_Y, GRAY, 1.2)) + p.append(line(rx, BOX_Y + BOX_H, rx, BUS_Y, GRAY, 1.2)) + p.append(line(lx, BUS_Y, rx, BUS_Y, GRAY, 1.2)) + p.append(line(W / 2, BUS_Y, W / 2, TRI_TIP - 11, GRAY, 1.4)) + p.append(f'') + + p.append(rect(FINAL_X0, FINAL_Y, FINAL_W, FINAL_H, fill="#fdf0e0", + stroke=ORANGE, width=1.6)) + p.append(txt(W / 2, FINAL_Y + 24, + "Ce rămâne pe BVB: hammer la un nivel major", 14, "700", + BLACK)) + p.append(txt(W / 2, FINAL_Y + 44, "+ close peste PPD", 14, "700", BLACK)) + + # ------- legendă + p.append(txt(W / 2, 578, + "Regulile = materialul cursului (cu trimiteri); verdictul de " + "BVB = judecata mea", 13, "400", GRAY)) + p.append(txt(W / 2, 596, "din secțiunea Transfer.", 13, "400", GRAY)) + + p.append('') + return "\n".join(p) + "\n", left_area, right_area + + +# ---------------------------------------------------------------- verificare +def est_w(s, size, weight): + factor = 0.62 if weight == "700" else 0.56 + return len(s) * size * factor + + +def box_of(x, y): + for bx in (BOX_LX, BOX_RX): + if bx <= x <= bx + BOX_W and BOX_Y <= y <= BOX_Y + BOX_H: + return bx + return None + + +def verify(svg, left_area, right_area): + # 1. geometrie: zonele nu se calcă + assert BAR_Y + BAR_H < BOX_Y, "bara comună intră în cutii" + assert BOX_Y + BOX_H < BUS_Y < TRI_TIP <= FINAL_Y, "săgeata nu are loc" + assert FINAL_Y + FINAL_H < 578, "caseta finală intră în legendă" + assert left_area == right_area == BOX_W - 2 * PAD + + # 2. toate textele în panză + for x, y, s, size, weight, anchor in TEXTS: + ew = est_w(s, size, weight) + left = x - ew if anchor == "end" else (x - ew / 2 if anchor == "middle" else x) + right = left + ew + assert -2 <= left and right <= W + 2, f"text în afara panzei: {s!r}" + assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y})" + + # 3. texte care nu se suprapun + for i in range(len(TEXTS)): + xi, yi, si, szi, wi, ai = TEXTS[i] + we = est_w(si, szi, wi) + li = xi - we if ai == "end" else (xi - we / 2 if ai == "middle" else xi) + ri = li + we + for j in range(i + 1, len(TEXTS)): + xj, yj, sj, szj, wj, aj = TEXTS[j] + wej = est_w(sj, szj, wj) + lj = xj - wej if aj == "end" else (xj - wej / 2 if aj == "middle" else xj) + rj = lj + wej + if abs(yi - yj) < 13 and li < rj - 4 and lj < ri - 4: + raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}") + + # 4. textele din cutii încap în cutii + for x, y, s, size, weight, anchor in TEXTS: + b = box_of(x, y) + if b is not None: + ew = est_w(s, size, weight) + assert x + ew <= b + BOX_W - PAD + 1, f"text iese din cutie: {s!r}" + + # 5. verdictul e sub linia lui de separare + assert VERDICT_SEP < VERDICT_Y < BOX_Y + BOX_H + + # 6. conținutul obligatoriu din spec + for cheie in ( + "Trigger de bottom: unde + când", + "Setup-urile de fond — suport, locul crimei, double bottom, buy doji,", + "divergență de volum — spun UNDE. Trigger-ul de candelă spune CÂND.", + "02_mbt10.md:9-11, 43-49", + "BUY HAMMER — pattern standard", + "„formă de candelă + downtrend”", + "05_patternuri_candele.md:77-88", + "„trigger: close peste", + "high-ul hammer-ului”", + "05_patternuri_candele.md:99-124", + "„fără niciun indicator proprietar”", + "„pe BVB: se poate rula long-only”", + "MBT-10 — cutie neagră", + "„formula nu e publică nicăieri”", + "02_mbt10.md:21-39", + "„reconstituibil doar:", + "close peste pivotul daily”", + "02_mbt10.md:179-188", + "„scanner + indicator proprietar”", + "02_mbt10.md:853-863", + "„pe BVB: nu, ca sistem”", + "Ce rămâne pe BVB: hammer la un nivel major", + "+ close peste PPD", + "verdictul de BVB = judecata mea", + "din secțiunea Transfer.", + ): + assert cheie in svg, f"lipsește din desen: {cheie!r}" + + # 7. fără axe de preț / candele / cifre inventate + for interzis in ("candela", "SL ", "intrare ", "TP1", "TP2", "$", "USD"): + assert interzis not in svg, f"nu ar trebui să apară preț: {interzis!r}" + return True + + +SVG, LEFT_AREA, RIGHT_AREA = build() +assert verify(SVG, LEFT_AREA, RIGHT_AREA), "verificare eșuată" +OUT.write_text(SVG, encoding="utf-8") +print(f"scris {OUT} ({W}x{H}), verificare OK: {len(TEXTS)} etichete, " + f"cutii {BOX_W:g}x{BOX_H:g} la {BOX_LX:g}/{BOX_RX:g}") diff --git a/gen/strategii/hammer_mbt10_harta.svg b/gen/strategii/hammer_mbt10_harta.svg new file mode 100644 index 0000000..2a5f5bd --- /dev/null +++ b/gen/strategii/hammer_mbt10_harta.svg @@ -0,0 +1,48 @@ + + + +Același trigger de bottom, două naturi +buy hammer (public) vs. MBT-10 (cutie neagră) + +Trigger de bottom: unde + când +Setup-urile de fond — suport, locul crimei, double bottom, buy doji, +divergență de volum — spun UNDE. Trigger-ul de candelă spune CÂND. +02_mbt10.md:9-11, 43-49 + + + + + +BUY HAMMER — pattern standard + +„formă de candelă + downtrend” +05_patternuri_candele.md:77-88 +„trigger: close peste +high-ul hammer-ului” +05_patternuri_candele.md:99-124 +„fără niciun indicator proprietar” + +„pe BVB: se poate rula long-only” + +MBT-10 — cutie neagră + +„formula nu e publică nicăieri” +02_mbt10.md:21-39 +„reconstituibil doar: +close peste pivotul daily” +02_mbt10.md:179-188 +„scanner + indicator proprietar” +02_mbt10.md:853-863 + +„pe BVB: nu, ca sistem” + + + + + + +Ce rămâne pe BVB: hammer la un nivel major ++ close peste PPD +Regulile = materialul cursului (cu trimiteri); verdictul de BVB = judecata mea +din secțiunea Transfer. + diff --git a/gen/strategii/hammer_mbt10_mbt_ansamblu.py b/gen/strategii/hammer_mbt10_mbt_ansamblu.py new file mode 100644 index 0000000..731a10b --- /dev/null +++ b/gen/strategii/hammer_mbt10_mbt_ansamblu.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""MBT-10 — pullback în locul crimei + hammer, intrare la close 54.60 (ansamblu). + +Citește `gen/strategii/hammer_mbt10_mbt_ansamblu.spec.txt` și scrie +`gen/strategii/hammer_mbt10_mbt_ansamblu.svg`: 41 de candele daily pe exemplul +construit al sumarizatorului (`summaries/08_ANALIZA_ZILNICA/02_mbt10.md:165-173`, +`978-984`) — uptrend până la fostul vârf 52.10, spargere, vârf nou 58.90, +pullback în locul crimei, hammer cu codița exact pe 52.10, iar bara următoare +se închide în MBT-10 la 54.60 (intrare la close). SL 52.00, TP 58.90 = 1.65R. + +Toate prețurile/nivelurile sunt cele din specificație. Seria de close-uri e +cea dată; open-ul fiecărei bare = close-ul precedentului. MBT-10 nu se +desenează (formula nu e publică) — se marchează doar bara la care semnalul +s-a închis. + +Rulare: python3 gen/strategii/hammer_mbt10_mbt_ansamblu.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") + +# --- nivelurile din specificație ------------------------------------------- +CRIMEA = 52.10 # locul crimei = fostul vârf spart, retestat ca suport +HAMMER_H = 53.40 # high = close al hammer-ului +HAMMER_L = 52.10 # codița hammer-ului, exact pe locul crimei +ENTRY = 54.60 # close-ul barei care s-a închis în MBT-10 = intrare +SL = 52.00 # sub codița hammer-ului și sub locul crimei +TP = 58.90 # vârful anterior = 1.65R (risc 2.60) + +# --- seria: 41 de close-uri, exact ca în specificație ----------------------- +closes = [ + # 0-8: uptrend care duce spre fostul vârf + 46.00, 46.80, 47.50, 48.30, 49.00, 49.80, 50.50, 51.20, 52.00, + 52.10, # 9 = fostul vârf 52.10 (high cu wick 52.40) + # 10-20: spargerea nivelului și urcarea spre vârful nou + 51.40, 50.80, 51.50, 52.40, 53.20, 54.00, 54.80, 55.60, 56.40, 57.20, + 58.00, + 58.90, # 21 = vârf nou + # 22-28: pullback înapoi în locul crimei + 58.20, 57.40, 56.50, 55.60, 54.70, 53.60, 52.20, + # 29-30: hammer pe locul crimei + ziua care se închide în MBT-10 + 53.40, # 29 = hammer: o 52.20, h/c 53.40, l 52.10 (H = C) + 54.60, # 30 = MBT-10 închis la close => intrare + # 31-40: urcarea spre ținta de la vârful anterior + 55.20, 55.80, 56.40, 57.00, 57.60, 58.10, 58.60, + 58.90, # 38 = TP (vârful anterior) + 58.70, 58.30, +] +assert len(closes) == 41, f"seria are {len(closes)} bare, spec cere 41" + +# wick-uri (sus, jos): restul (0.30, 0.40); excepțiile din specificație. +# `from_closes` măsoară wick-ul de jos de la min(o, c), nu de la close: pentru +# hammer-ul cu o 52.20 / c 53.40, ca low-ul să iasă exact 52.10, wick-ul de jos +# e 0.10 (nu 1.30, cum dă specificația măsurând de la close). +wicks = [(0.30, 0.40)] * 41 +wicks[9] = (0.30, 0.30) # high 52.40, low 51.70 (fostul vârf) +wicks[28] = (0.30, 0.10) # low 52.10: pullback-ul atinge locul crimei +wicks[29] = (0.00, 0.10) # hammer: high = close = 53.40, low = 52.10 +# bara 30: specificația dă (0.20, 0.20) cu „low 54.40"; 54.40 ar fi peste +# open-ul 53.40, deci imposibil. Cu (0.20, 0.20) low-ul iese 53.20; nu e +# etichetat nicăieri și nu intră în niciun assert. +wicks[30] = (0.20, 0.20) + +# volum „normal", fără argument de volum (specificația o spune explicit): +# doar panoul de volum cerut de stil, fără spike-uri pe hammer/confirmare. +vols = [ + 50, 49, 51, 50, 52, 51, 50, 52, 51, + 52, + 50, 49, 51, 50, 52, 51, 50, 52, 51, 52, + 51, + 52, + 50, 49, 50, 51, 50, 49, 50, + 51, 52, + 50, 51, 52, 51, 50, 52, 51, + 52, + 51, 50, +] +assert len(vols) == 41 + +cds = from_closes(closes, wicks=wicks, vols=vols, start=45.60) + +# seria continuă: open = close-ul barei anterioare +for a, b in zip(cds, cds[1:]): + assert abs(a.c - b.o) < 1e-9, "open != close-ul barei anterioare" + +intrare = cds[30].c +sl, tp = SL, TP + +# --- cele 8 assert-uri cerute de specificație ------------------------------ +# 1. hammer 29: low exact pe locul crimei, close == high (fără fitil de sus) +assert abs(cds[29].l - CRIMEA) < 1e-9, f"low[29]={cds[29].l} != 52.10" +assert abs(cds[29].c - HAMMER_H) < 1e-9 and abs(cds[29].h - HAMMER_H) < 1e-9, \ + f"close[29]/high[29] != 53.40 ({cds[29].c}/{cds[29].h})" +assert cds[29].c == cds[29].h, "hammer-ul are fitil de sus" +# 2. bara 30 se închide la 54.60 = intrare și peste high-ul hammer-ului +assert abs(intrare - ENTRY) < 1e-9, f"close[30]={intrare} != intrare 54.60" +assert intrare > cds[29].h == HAMMER_H, "close[30] nu e peste high[29] == 53.40" +# 3. SL = low-ul hammer-ului - 0.10 +assert abs(sl - (cds[29].l - 0.10)) < 1e-9, "SL != low[29] - 0.10" +# 4. risc 2.60; TP 58.90 = 1.65R +risc = intrare - sl +assert abs(risc - 2.60) < 1e-9, f"risc={risc} != 2.60" +assert abs(tp - 58.90) < 1e-9, "TP != 58.90" +assert round((tp - intrare) / (intrare - sl), 2) == 1.65, "TP != 1.65R" +# 5. fostul vârf 52.10 există înainte de breakout (bara 9, high 52.40) +assert any(abs(cds[i].h - 52.10) <= 0.30 for i in range(15)), \ + "niciun high din primele 15 bare nu e în 0.30 de 52.10" +assert abs(cds[9].h - 52.40) < 1e-9, f"high[9]={cds[9].h} != 52.40" +# 6. TP atins după intrare +assert max(c.c for c in cds[30:41]) >= 58.90, "TP 58.90 nu e atins după intrare" +# 7. vârful nou 58.90 e la bara 21, înainte de pullback; pullback-ul coboară +# exact la locul crimei (minimul secțiunii 21-29 e codița hammer-ului) +assert abs(cds[21].c - 58.90) < 1e-9, f"close[21]={cds[21].c} != 58.90" +assert abs(min(c.l for c in cds[21:30]) - CRIMEA) < 1e-9, \ + f"min low[21:30]={min(c.l for c in cds[21:30])} != 52.10" +# 8. prima etapă e urcare +assert cds[9].c > cds[0].c, "prima etapă nu e uptrend" + +print(f"hammer H/C {cds[29].h:.2f} L {cds[29].l:.2f} intrare {intrare:.2f} " + f"SL {sl:.2f} TP {tp:.2f} = {round((tp-intrare)/(intrare-sl),2)}R " + f"vârf nou {cds[21].c:.2f} (bara 21)") + +# --- desenul ---------------------------------------------------------------- +ch = Chart( + candles=cds, + levels=[ + Level(CRIMEA, "locul crimei 52.10 (fost vârf spart)", "#777", + label_at="left"), + Level(SL, "SL 52.00 — sub codița hammer-ului", "#c00"), + Level(ENTRY, "intrare 54.60 (close în MBT-10)", "#188a3e"), + Level(TP, "TP 58.90 = 1.65R", "#1a5fb4"), + ], + marks=[ + Mark(29, "hammer — codița pe 52.10", "#b06000", "below"), + # dy negativ: bara 30 e urmată imediat de bare mai înalte (31-32), iar + # textul lipit de high-ul barei 30 ar cădea peste corpurile lor. + Mark(30, "MBT-10 închis", "#188a3e", "above", dy=-24), + ], + scale=20.0, gap=15, body_w=9, left=44, width=780, vol_h=70, + vol_label="volum", + title="MBT-10 — pullback în locul crimei + hammer, închidere la 54.60 (exemplu construit)", +) + +# explicațiile și legenda obligatorie: bloc de text sub grafic (nu se +# suprapun cu candelele, iar textele lungi nu încap pe verticală lângă bare). +cds[0].note = ("Legendă: MBT-10 nu se desenează — formula nu e publică " + "(02_mbt10.md:21-39); se marchează doar bara la care") +cds[1].note = ("semnalul s-a închis. Nivelurile sunt exemplul construit al " + "sumarizatorului (02_mbt10.md:165-173, 978-984).") +cds[2].note = ("bara 9 = fostul vârf 52.10 (locul crimei); bara 21 = vârful " + "nou 58.90.") +cds[3].note = ("bara 29 = hammer: o 52.20, h/c 53.40, l 52.10 (codița exact " + "pe locul crimei); bara 30 = ziua închisă în MBT-10.") + +svg = ch.render() +assert "]*)>(.*?)', re.S) +W = float(re.search(r'viewBox="0 0 (\d+) ', svg).group(1)) +H = float(re.search(r'viewBox="0 0 \d+ ([\d.]+)"', svg).group(1)) +labels = [] +for m in TEXT.finditer(svg): + x, y, attrs, s = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4) + anchor = "end" if 'text-anchor="end"' in attrs else \ + ("middle" if 'text-anchor="middle"' in attrs else "start") + bold = "bold" in attrs + wpx = len(s) * 12 * (0.62 if bold else 0.52) + left = x - wpx if anchor == "end" else (x - wpx / 2 if anchor == "middle" else x) + labels.append((left, left + wpx, y, s)) + +for left, right, y, s in labels: + assert left >= -2 and right <= W + 2, \ + f"text iese din pânză: {s!r} [{left:.0f},{right:.0f}]" + assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y:.0f})" + +for i in range(len(labels)): + li, ri, yi, si = labels[i] + for j in range(i + 1, len(labels)): + lj, rj, yj, sj = labels[j] + if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4: + raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}\n" + f" {labels[i]}\n {labels[j]}") + +# candelele sunt o serie continuă, lipită, în pânză +xs = [ch._cx(i) for i in range(len(cds))] +assert all(xs[i + 1] - xs[i] == ch.gap for i in range(len(xs) - 1)), "serie cu goluri" +assert xs[-1] + ch.body_w / 2 < W, f"ultima candelă iese din pânză (x={xs[-1]})" + +print(f"scris {OUT} ({W:.0f}x{H:.0f}); {len(labels)} etichete verificate") +OUT.write_text(svg, encoding="utf-8") diff --git a/gen/strategii/hammer_mbt10_mbt_ansamblu.svg b/gen/strategii/hammer_mbt10_mbt_ansamblu.svg new file mode 100644 index 0000000..7d74efc --- /dev/null +++ b/gen/strategii/hammer_mbt10_mbt_ansamblu.svg @@ -0,0 +1,142 @@ + + MBT-10 — pullback în locul crimei + hammer, închidere la 54.60 (exemplu construit) + + locul crimei 52.10 (fost vârf spart) + + SL 52.00 — sub codița hammer-ului + + intrare 54.60 (close în MBT-10) + + TP 58.90 = 1.65R + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + hammer — codița pe 52.10 + MBT-10 închis + Legendă: MBT-10 nu se desenează — formula nu e publică (02_mbt10.md:21-39); se marchează doar bara la care + semnalul s-a închis. Nivelurile sunt exemplul construit al sumarizatorului (02_mbt10.md:165-173, 978-984). + bara 9 = fostul vârf 52.10 (locul crimei); bara 21 = vârful nou 58.90. + bara 29 = hammer: o 52.20, h/c 53.40, l 52.10 (codița exact pe locul crimei); bara 30 = ziua închisă în MBT-10. + \ No newline at end of file diff --git a/gen/strategii/hammer_mbt10_mbt_gapdown.py b/gen/strategii/hammer_mbt10_mbt_gapdown.py new file mode 100644 index 0000000..bb3407b --- /dev/null +++ b/gen/strategii/hammer_mbt10_mbt_gapdown.py @@ -0,0 +1,181 @@ +"""După gap down — primul MBT-10 te scoate, al doilea merge (exemplu construit). + +Citește `gen/strategii/hammer_mbt10_mbt_gapdown.spec.txt` +(marcajul ``, Partea B §2.3 Gotchas). + +Serie continuă de 33 candele daily, construită din close-urile date în +specificație, cu open-ul fiecărei bare = close-ul precedentei (STYLE.md), +cu două excepții impuse de exemplu: + +- bara 13 (ultima dinainte de gap): ca să iasă ȘI ``low[13] = 70.60`` + (nivelul de închidere a gap-ului, cerut de assert 1 și 6) ȘI + ``high[13] = 71.00`` (structura seriei, „de la ~71"), bara trebuie să + fie un doji ``o = c = 70.60``. De aceea am ridicat ``close[12]`` de la + 69.80 (cât dă lista) la 70.60: altfel ``min(open,close)=69.80`` și + low-ul ar ieși 69.80, nu 70.60. Wu=0.40 => high 71.00, wd=0.00 => low + 70.60. (Linia „Mecioni" cere wu=0.20 => high 70.80; am ales 71.00, + cifra din structura seriei și din textul „~71".) +- bara 14 (gap down): ``open[14] = 66.40``, NU close[13]=70.60 — singura + bară cu open diferit de close-ul precedent. O construim manual: + o=c=66.40, wu=0.30 => high 66.70, wd=0.30 => low 66.10. + +Wick-urile speciale: bara 16 wd=0.20 (low 64.60), bara 18 wd=0.20 (low +63.20), bara 21 wd=0.20 (low 62.00); restul (0.30, 0.40). + +Rulează: python3 gen/strategii/hammer_mbt10_mbt_gapdown.py +Scrie: gen/strategii/hammer_mbt10_mbt_gapdown.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 / "hammer_mbt10_mbt_gapdown.svg" + +GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000" + +# --- seria: 33 de close-uri, exact ca în specificație ------------------------ +# Singura abatere: closes[12] 69.80 -> 70.60, ca bara 13 să poată avea +# low 70.60 (vezi docstring-ul). +closes = [ + 60.00, 60.80, 61.60, 62.50, 63.30, 64.20, 65.00, 65.80, 66.60, 67.40, # 0-9 + 68.20, 69.00, 70.60, 70.60, # 10-13 + 66.40, # 14 = bara de GAP DOWN (open impus 66.40, NU close[13]) + 65.50, # 15 + 64.80, # 16 = MBT-10 #1, închidere => intrare #1 + 64.20, # 17 + 63.40, # 18 = low 63.20 => stop #1 + 62.90, # 19 + 62.50, # 20 + 62.20, # 21 = MBT-10 #2, închidere => intrare #2 + 62.80, 63.60, 64.50, 65.50, 66.50, 67.50, 68.50, 69.50, # 22-29 + 70.60, # 30 = gap închis (TP) + 70.90, # 31 + 70.50, # 32 +] +assert len(closes) == 33, len(closes) + +WICKS = [(0.30, 0.40)] * 33 # restul barelor +WICKS[13] = (0.40, 0.00) # high 71.00, low 70.60 +WICKS[14] = (0.30, 0.30) # high 66.70, low 66.10 (bara de gap) +WICKS[16] = (0.30, 0.20) # low 64.60 +WICKS[18] = (0.30, 0.20) # low 63.20 +WICKS[21] = (0.30, 0.20) # low 62.00 + +# Volume relative, alese de generator (materialul nu dă volum aici): +# în scădere pe piciorul coborârii după gap (14 -> 21), apoi recuperare. +vols = [ + 48, 46, 44, 45, 43, 42, 44, 41, 40, 42, # 0-9 + 43, 41, 46, 58, # 10-13 + 90, # 14 = gap down + 76, 66, 57, 50, 45, 42, # 15-20 + 40, # 21 = MBT-10 #2 (minim) + 46, 52, 58, 63, 68, 73, 78, 82, 86, 88, 80, # 22-32 +] +assert len(vols) == 33, len(vols) +coborare = vols[14:22] +assert all(coborare[i] >= coborare[i + 1] for i in range(len(coborare) - 1)), \ + f"volumul nu scade pe piciorul coborârii: {coborare}" +assert vols[-1] > vols[21], "volumul nu recuperează după bara 21" + +cds = from_closes(closes, wicks=WICKS, vols=vols, start=60.00) +# bara de gap: open = 66.40, nu close[13] = 70.60 +cds[14] = Candle(66.40, 66.70, 66.10, 66.40, vol=vols[14], color=RED) + +# --- assert-urile din specificație ----------------------------------------- +# 1. gap down real +assert abs(cds[14].o - 66.40) < 1e-9, cds[14].o +assert abs(cds[13].l - 70.60) < 1e-9, f"low[13]={cds[13].l} != 70.60" +assert abs(cds[13].c - 70.60) < 1e-9, cds[13].c +assert cds[14].o < cds[13].l and cds[14].h < cds[13].l, "gapul nu e real" +assert abs(cds[14].o - cds[13].c) > 1e-9, "open[14] nu e excepția de la serie" + +# 2. #1 +intrare1, sl1 = 64.80, 63.20 +assert abs(cds[16].c - intrare1) < 1e-9, f"close[16]={cds[16].c} != 64.80" +assert abs(sl1 - 63.20) < 1e-9 +assert min(c.l for c in cds[16:19]) <= sl1, "stopul #1 nu e atins" +assert cds[16].c > sl1, "intrarea #1 e sub stopul ei" + +# 3. #2 +intrare2, sl2 = 62.20, 60.40 +assert abs(cds[21].c - intrare2) < 1e-9, f"close[21]={cds[21].c} != 62.20" +assert abs(sl2 - 60.40) < 1e-9 +assert not any(c.l <= sl2 for c in cds[22:]), "un low atinge SL2 după bara 21" + +# 4. riscuri +risc1 = intrare1 - sl1 # 1.60 +risc2 = intrare2 - sl2 # 1.80 +assert abs(risc1 - 1.60) < 1e-9 and abs(risc2 - 1.80) < 1e-9, (risc1, risc2) + +# 5. TP = închiderea gap-ului +TP = 70.60 +reward2 = TP - intrare2 # 8.40 +assert abs(reward2 - 8.40) < 1e-9, reward2 +assert round(reward2 / risc2, 1) == 4.7, round(reward2 / risc2, 1) + +# 6. low[13] e nivelul de închidere a gap-ului și TP-ul e atins în urcare +assert abs(cds[13].l - TP) < 1e-9, "low[13] != 70.60 (nivelul gap-ului)" +assert max(c.h for c in cds[22:33]) >= TP, "TP 70.60 nu e atins după #2" +primul_tp = next(i for i in range(22, 33) if cds[i].h >= TP) + +# 7. același risc în bani: mărimea #2 = mărimea #1 × (risc1 / risc2) +marime1 = 1.0 +marime2 = marime1 * (risc1 / risc2) +assert abs(marime2 - 1.60 / 1.80) < 1e-9, marime2 + +print(f"gap {cds[13].l:.2f} -> {cds[14].o:.2f} " + f"#1 intrare {intrare1:.2f} SL {sl1:.2f} (risc {risc1:.2f}) " + f"#2 intrare {intrare2:.2f} SL {sl2:.2f} (risc {risc2:.2f}) " + f"TP {TP:.2f} = {reward2 / risc2:.1f}R " + f"atins la bara {primul_tp} (high {cds[primul_tp].h:.2f}) " + f"mărime #2 = {marime2:.3f} × mărime #1") + +# --- desenul ---------------------------------------------------------------- +ch = Chart( + candles=cds, + levels=[ + # toate etichetele stau în dreapta (x > ultima candelă), fără suprapuneri + Level(intrare1, "#1 intrare 64.80", GRAY, label_at="right"), + Level(sl1, "#1 SL 63.20", GRAY, label_at="right"), + Level(intrare2, "#2 intrare 62.20", GRAY, label_at="right"), + Level(sl2, "#2 SL 60.40", GRAY, label_at="right"), + Level(TP, "TP 70.60 = 4.7R (gap închis)", BLUE, label_at="right"), + ], + marks=[ + # decalate pe verticală cât să nu atingă candelele din coborâre + Mark(14, "gap down", RED, "above", dy=-22), + Mark(16, "MBT-10 #1", GRAY, "above", dy=-25), + Mark(18, "stop #1 atins", RED, "below", dy=30), + Mark(21, "MBT-10 #2", GREEN, "below", dy=25), + ], + scale=28.0, gap=15, body_w=9, left=44, width=820, vol_h=70, + vol_label="volum", + title="După gap down — primul MBT-10 te scoate, al doilea merge (exemplu construit)", + caption="", +) + +svg = ch.render() +assert "", "".join( + f'{t}' + for i, t in enumerate(linii)) + "") +assert "\n\n" not in svg, "linie goală în SVG după legendă" + +OUT.write_text(svg) +print(f"scris {OUT.relative_to('/workspace/atm-curs-dl')}") diff --git a/gen/strategii/hammer_mbt10_mbt_gapdown.svg b/gen/strategii/hammer_mbt10_mbt_gapdown.svg new file mode 100644 index 0000000..61ac1b4 --- /dev/null +++ b/gen/strategii/hammer_mbt10_mbt_gapdown.svg @@ -0,0 +1,118 @@ + + După gap down — primul MBT-10 te scoate, al doilea merge (exemplu construit) + + #1 intrare 64.80 + + #1 SL 63.20 + + #2 intrare 62.20 + + #2 SL 60.40 + + TP 70.60 = 4.7R (gap închis) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + gap down + MBT-10 #1 + stop #1 atins + MBT-10 #2 +MBT-10 #1: intrare 64.80 / SL 63.20 — risc 1R în baniMBT-10 #2: intrare 62.20 / SL 60.40 — risc 1R în bani (mărime ajustată)Exemplu construit (02_mbt10.md:728-734): #1 și #2 au același risc în bani; #2 singur dă 4.7R.De aceea mărimea se împarte pe 2-3 încercări. \ No newline at end of file diff --git a/gen/strategii/hammer_mbt10_mbt_tesla.py b/gen/strategii/hammer_mbt10_mbt_tesla.py new file mode 100644 index 0000000..39c7893 --- /dev/null +++ b/gen/strategii/hammer_mbt10_mbt_tesla.py @@ -0,0 +1,151 @@ +"""Diagrama din `gen/strategii/hammer_mbt10_mbt_tesla.spec.txt` (TIP: PRET). + +Exemplul real Tesla din Q&A (`03_2026-08-11.md:118-120`): trigger MBT-10 după +o cădere anterioară, intrare la închiderea zilei la 311.21, ieșire la 333.00 +șase zile mai târziu = +7.0 % pe acțiune. + +Reale: 311.21 / 333.00 / +7.0 % și contextul calitativ. Construită de mine: +structura barelor dintre cele două niveluri. SL-ul lipsește din material, deci +NU se trasează nicio linie de stop (spec, „LIPSĂ"). + +Serie continuă de 25 de candele daily din lista de close-uri din specificație, +open = close-ul precedentului, wick-uri implicite (0.50, 0.60), fără panou de +volum (materialul nu discută volumul exemplului). Legenda e spartă pe trei +rânduri și lipește sub grafic, cu viewBox extins. +""" +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__).resolve().parent / "hammer_mbt10_mbt_tesla.svg" + +# ── cifrele reale din material (`03_2026-08-11.md:118-120`) ───────────────── +INTRARE = 311.21 # intrarea la închiderea zilei +REZULTAT = 333.00 # six zile mai târziu +ZILE = 6 # (21 - 15) +PROCENT = 7.0 # +7.0 % pe acțiune +I_INTRARE = 15 # candela de intrare (trigger MBT-10) +I_REZULTAT = 21 + +# ── seria: 25 de close-uri, exact ca în specificație ─────────────────────── +closes = [ + 360.00, 356.00, 358.00, 352.00, 348.00, 349.00, 343.00, 338.00, 340.00, + 334.00, 329.00, 331.00, 325.00, 320.00, 315.00, + 311.21, # 15 = trigger MBT-10, intrare la închiderea zilei + 314.00, 318.00, 322.00, 326.00, 330.00, + 333.00, # 21 = 6 bare după intrare + 331.00, 328.00, 330.00, +] +assert len(closes) == 25, len(closes) + +# wick-uri implicite pentru toate barele +wicks = [(0.50, 0.60)] * len(closes) +cds = from_closes(closes, wicks=wicks, start=closes[0]) + +# ── assert-urile cerute de specificație ──────────────────────────────────── +# 1. close[15] == 311.21 == intrare (la închiderea zilei) +assert abs(cds[I_INTRARE].c - 311.21) < 1e-9, cds[I_INTRARE].c +assert abs(cds[I_INTRARE].c - INTRARE) < 1e-9, (cds[I_INTRARE].c, INTRARE) + +# 2. close[21] == 333.00 și (21 - 15) == 6 +assert abs(cds[I_REZULTAT].c - 333.00) < 1e-9, cds[I_REZULTAT].c +assert (I_REZULTAT - I_INTRARE) == 6, I_REZULTAT - I_INTRARE + +# 3. procentul REAL +pct = round((cds[I_REZULTAT].c / cds[I_INTRARE].c - 1) * 100, 1) +assert pct == 7.0, pct + +# 4. cădere anterioară: close[15] < close[0] - 40 +assert cds[I_INTRARE].c < cds[0].c - 40, (cds[I_INTRARE].c, cds[0].c) + +# 5. nu se trasează linie de SL; în structură nu apare niciun nivel de stop +# (verificat mai jos, după construirea listelor de niveluri/marcaje) + +# 6. entry < exit, iar exit e atins după exact 6 bare (nu mai devreme) +assert cds[I_INTRARE].c < cds[I_REZULTAT].c +for i in range(I_INTRARE + 1, I_REZULTAT): + assert cds[i].c < REZULTAT, f"rezultatul 333.00 e atins deja la bara {i}" +assert cds[I_REZULTAT].c == REZULTAT + +# serie continuă: open-ul fiecărei bare = close-ul precedentului +for a, b in zip(cds, cds[1:]): + assert abs(a.c - b.o) < 1e-9, "serie cu gol artificial între bare" + +# ── desenul ──────────────────────────────────────────────────────────────── +levels = [ + Level(INTRARE, "intrare 311.21 (close)", "#188a3e", label_at="left"), + Level(REZULTAT, "rezultat 333.00 = +7.0 % în 6 zile", "#1a5fb4"), +] +marks = [ + Mark(I_INTRARE, "trigger MBT-10", "#188a3e", "below"), + Mark(I_REZULTAT, "exact 6 bare", "#1a5fb4", "above", dy=-16), +] + +# 5. niciun nivel/marcaj de stop în structură +assert not any("stop" in t.lower() for t in + [lv.text for lv in levels] + [mk.text for mk in marks]) +assert not any(t.strip().upper().startswith("SL") for t in + [lv.text for lv in levels] + [mk.text for mk in marks]) + +ch = Chart( + candles=cds, + levels=levels, + marks=marks, + scale=7.5, gap=15, body_w=9, left=44, width=620, + title="Tesla — daily: trigger MBT-10, intrare 311.21 → 333.00 = +7.0 % în 6 zile", + caption="", # legenda obligatorie e pusă manual, pe rânduri, cu viewBox extins +) +svg = ch.render() +assert "", "".join( + f'{t}' + for i, t in enumerate(legenda)) + "") +assert "\n\n" not in svg, "linie goală în SVG după legendă" + +# legenda completă există, rând cu rând +for rand in legenda: + assert rand in svg, f"lipsește din legendă: {rand!r}" + +# ── verificare: fiecare etichetă e în pânză și nu se suprapune alta ──────── +texte = re.findall(r']*)>([^<]*)', svg) +cutii = [] +for xs, ys, attrs, continut in texte: + x, y = float(xs), float(ys) + w = len(continut) * (7.44 if "bold" in attrs else 6.72) + if 'text-anchor="end"' in attrs: + x0 = x - w + elif 'text-anchor="middle"' in attrs: + x0 = x - w / 2 + else: + x0 = x + assert -2 <= x0 and x0 + w <= W + 2, f"etichetă în afara pânzei: {continut!r}" + assert 0 <= y <= H + extra, f"etichetă în afara înălțimii: {continut!r}" + cutii.append((x0, x0 + w, y, continut)) +for i in range(len(cutii)): + for j in range(i + 1, len(cutii)): + a, b = cutii[i], cutii[j] + if abs(a[2] - b[2]) < 13 and a[0] < b[1] - 2 and b[0] < a[1] - 2: + raise AssertionError(f"texte suprapuse: {a[3]!r} / {b[3]!r}") + +OUT.write_text(svg, encoding="utf-8") +print(f"serie {len(cds)} candele | intrare {INTRARE:.2f} @ bara {I_INTRARE} | " + f"rezultat {REZULTAT:.2f} @ bara {I_REZULTAT} (+{pct:.1f} % în " + f"{I_REZULTAT - I_INTRARE} zile, atins nu mai devreme)") +print(f"scris {OUT}") diff --git a/gen/strategii/hammer_mbt10_mbt_tesla.svg b/gen/strategii/hammer_mbt10_mbt_tesla.svg new file mode 100644 index 0000000..6f0a98c --- /dev/null +++ b/gen/strategii/hammer_mbt10_mbt_tesla.svg @@ -0,0 +1,59 @@ + + Tesla — daily: trigger MBT-10, intrare 311.21 → 333.00 = +7.0 % în 6 zile + + intrare 311.21 (close) + + rezultat 333.00 = +7.0 % în 6 zile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + trigger MBT-10 + exact 6 bare +Cifrele 311.21 / 333.00 / +7.0 % sunt reale (03_2026-08-11.md:118-120);structura barelor dintre ele e construită.SL: materialul nu îl dă pentru acest exemplu. \ No newline at end of file diff --git a/gen/strategii/m2d_ansamblu.py b/gen/strategii/m2d_ansamblu.py new file mode 100644 index 0000000..1bf4f40 --- /dev/null +++ b/gen/strategii/m2d_ansamblu.py @@ -0,0 +1,136 @@ +"""Diagrama de ansamblu M2D (strategii/03_m2d.md, marcajul `ansamblu`). + +„Setup M2D LONG complet pe TF mic (3 min), cu TF mare (15 min) în Buy": +urcare -> pullback (M = Sell Momentum) -> trigger (A = primul Buy) -> intrare +-> SL de 4 candele -> TP1 la 1:1 -> continuare -> vârf -> ieșire pe bull trail. + +Toate close-urile, wick-urile, volumele și nivelurile vin din +`gen/strategii/m2d_ansamblu.spec.txt`. Seria e construită (exemplu generic, +fără ticker), la scara 1.00 $ risc / 2.2R randament — nu e un trade real. + +Rulează: + python3 gen/strategii/m2d_ansamblu.py +Scrie lângă el `m2d_ansamblu.svg`. +""" +import sys +from pathlib import Path + +sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402 + +OUT = Path(__file__).resolve().parent / "m2d_ansamblu.svg" + +GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000" + +# ---------------------------------------------------------------- seria (32 bare) +CLOSES = [ + 96.8, 97.2, 97.7, 98.1, 98.6, 99.1, 99.5, 100.0, # 0-7 urcare + 99.8, 99.4, 99.0, 99.2, 98.9, 99.15, 99.3, # 8-14 pullback (M) + 99.8, 100.2, # 15-16 trigger (A) + 100.7, 101.2, 101.6, 102.0, 102.4, 102.7, 103.0, # 17-23 continuare + 103.3, 103.1, 102.8, 102.7, 102.9, 102.5, 102.2, 102.0, # 24-31 vârf + violare +] +assert len(CLOSES) == 32, len(CLOSES) + +# fitile (sus, jos); jos mai mari pe 10-12, ca minimul de 4 candele (11:15) să +# fie clar sub intrare, la exact 98.80 (candela 12). +WICKS = ( + [(0.15, 0.10)] * 8 # 0-7 + + [(0.10, 0.15), (0.10, 0.20), (0.10, 0.15), # 8-10 + (0.10, 0.15), (0.10, 0.10), (0.10, 0.05), (0.10, 0.10)] # 11-14 + + [(0.15, 0.10), (0.15, 0.10)] # 15-16 + + [(0.15, 0.10)] * 7 # 17-23 + + [(0.20, 0.10), (0.10, 0.15), (0.10, 0.15), (0.12, 0.15), # 24-27 + (0.10, 0.15), (0.10, 0.15), (0.10, 0.15), (0.10, 0.20)] # 28-31 +) +assert len(WICKS) == 32, len(WICKS) + +VOLS = ( + [62, 66, 60, 68, 64, 70, 66, 74] # 0-7 trend + + [44, 40, 38, 36, 34, 33, 35] # 8-14 pullback mic + + [82, 88] # 15-16 trigger + + [72, 70, 74, 68, 72, 66, 70] # 17-23 trend + + [76, 64, 60, 58, 62, 56, 54, 66] # 24-31 vârf + ieșire +) +assert len(VOLS) == 32, len(VOLS) + +cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=96.4) + +# ---------------------------------------------------------------- assert-urile din spec +entry = cds[15].c # 1. intrarea = close-ul trigger-ului +SL = min(c.l for c in cds[11:15]) # 2. minimul ultimelor 4 dinainte +TP1 = entry + (entry - SL) # 4. TP1 exact 1:1 + +assert entry == round(cds[15].c, 2), (entry, cds[15].c) +assert SL == min(c.l for c in cds[11:15]), SL +assert SL < entry, (SL, entry) +assert abs(TP1 - (entry + (entry - SL))) < 1e-6 +for a, b in zip(cds, cds[1:]): # 5. serie continuă, fără goluri + assert a.c == b.o, (a.c, b.o) +assert cds[15].c > cds[14].c # 6. trigger în sus după pullback +assert any(c.c < c.o for c in cds[8:15]) # 7. pullback = Sell +assert any(c.c > c.o for c in cds[0:8]) # context în sus +exit_price = cds[31].c +assert exit_price > entry # 8. ieșirea e peste intrare + +# bull trail = low-ul ultimei candele care a închis peste high-ul precedentei +qual = [i for i in range(1, 31) if cds[i].c > cds[i - 1].h] +assert qual and qual[-1] == 24, qual +bull = cds[24].l +assert cds[31].c < bull, (cds[31].c, bull) # finala închide sub linie = ieșire aici + +r_mult = (exit_price - entry) / (entry - SL) # 9. R-ul afișat +assert round(r_mult, 1) == 2.2, r_mult + +# ---------------------------------------------------------------- niveluri, etichete, marks +levels = [ + Level(entry, f"intrare {entry:.2f}", GREEN, label_at="left"), + Level(SL, f"SL {SL:.2f} (4 candele)", RED, label_at="right"), + Level(TP1, f"TP1 {TP1:.2f} = 1:1", BLUE, label_at="right"), +] +assert [round(lv.price, 2) for lv in levels] == [99.80, 98.80, 100.80] + +exit_text = f"ieșire {exit_price:.2f} pe bull trail ({r_mult:.1f}R)" +assert f"{r_mult:.1f}R" in exit_text + +marks = [ + Mark(0, "TF mare (15 min) în Buy", BLUE, "above", dy=-227), + Mark(11, "M: Sell Momentum (pullback)", RED, "below"), + Mark(15, "A: primul Buy = trigger", GREEN, "above", dy=-122), + # „vârf" stă sub candela 24: deasupra ei, orice text de la y-ul vârfului se + # calcă pe titlul din stânga-sus (ambele pornesc din `top`). + Mark(24, "vârf 103.30", GRAY, "below", dy=25), + Mark(31, "close sub bull trail = ieși", RED, "below", dy=55), + Mark(31, exit_text, RED, "below", dy=73), +] + +caption = (f"Serie construită (32 bare, 3 min). R = ({exit_price:.2f} − {entry:.2f}) / " + f"{entry - SL:.2f} = {r_mult:.1f}R.") +assert f"{r_mult:.1f}R" in caption + +ch = Chart( + candles=cds, levels=levels, marks=marks, + mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")], + scale=52.0, gap=16, body_w=9, left=44, width=720, vol_h=70, + vol_label="volum", + title="Simbol construit — 3 min (TF mare 15 min în Buy)", + caption=caption, +) +svg = ch.render() + +# bull trail: nu e un `Level` (spec-ul cere exact trei niveluri), deci se +# injectează ca linie separată, la aceeași scară (`ch._y`). +yb = ch._y(bull) +bull_layer = ( + f'' + f'bull trail {bull:.2f}' +) +i = svg.index(">") + 1 +svg = svg[:i] + "\n " + bull_layer + svg[i:] + +assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown" +OUT.write_text(svg) +print(f"scris {OUT.name}: {len(cds)} candele, intrare {entry:.2f}, SL {SL:.2f}, " + f"TP1 {TP1:.2f}, ieșire {exit_price:.2f} = {r_mult:.1f}R, bull trail {bull:.2f}") diff --git a/gen/strategii/m2d_ansamblu.svg b/gen/strategii/m2d_ansamblu.svg new file mode 100644 index 0000000..47c536d --- /dev/null +++ b/gen/strategii/m2d_ansamblu.svg @@ -0,0 +1,117 @@ + + bull trail 102.90 + Simbol construit — 3 min (TF mare 15 min în Buy) + + intrare 99.80 + + SL 98.80 (4 candele) + + TP1 100.80 = 1:1 + + SMA-20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + TF mare (15 min) în Buy + M: Sell Momentum (pullback) + A: primul Buy = trigger + vârf 103.30 + close sub bull trail = ieși + ieșire 102.00 pe bull trail (2.2R) + Serie construită (32 bare, 3 min). R = (102.00 − 99.80) / 1.00 = 2.2R. + \ No newline at end of file diff --git a/gen/strategii/m2d_exemplu.py b/gen/strategii/m2d_exemplu.py new file mode 100644 index 0000000..e1df47b --- /dev/null +++ b/gen/strategii/m2d_exemplu.py @@ -0,0 +1,289 @@ +"""Diagrama din `gen/strategii/m2d_exemplu.spec.txt` (TIP: PRET). + +Exemplul real Riot din curs (`14_strategia_1_m2d.md:227-236`): SHORT pe TF mic +3 min, cu TF mare 15 min în Sell. Pullback-ul Buy de pe TF mic (M) e urmat de +primul Sell (A = trigger); SL = cel mai sus high din 4 candele = +0.85%, +TP1 = 1:1 = -0.85%, iar ieșirea se face pe violarea bear trail-ului la +4.55% +(risc/recompensă efectiv ≈ 1:5.3). + +Reale din curs: procentele 0.85% / 1.42% / 4.55% și contextul calitativ. +Construită de mine: axa de preț absolută, intrarea fixată la 100.00 ca reper. +Legenda spune explicit lucrul ăsta. + +Serie continuă de 30 de candele din lista de close-uri din specificație, +open = close-ul precedentului, wick-uri alese ca să scoată exact nivelurile +(candela 13: high 100.85 = SL), panou de volum dedesubt. +""" +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__).resolve().parent / "m2d_exemplu.svg" + +# ── nivelurile cerute de specificație ────────────────────────────────────── +ENTRY = 100.00 # close-ul candelei de trigger 14 +SL = 100.85 # max high pe candelele 11:15 = +0.85% +TP1 = 99.15 # entry - 0.85% (1:1) +EXIT = 95.45 # close-ul candelei 29 = +4.55% + +# ── seria: 30 de close-uri, exact ca în specificație ─────────────────────── +closes = [ + 97.6, 97.1, 96.7, 96.3, 95.9, 95.6, 95.9, # 0-6 coborâre + 96.4, 96.9, 97.5, 98.2, 99.0, 99.7, 100.3, # 7-13 pullback Buy (M) + 100.00, # 14 trigger Sell (A) + 99.5, 99.0, 98.5, 98.0, 97.6, 97.2, 96.9, 96.6, 96.3, 96.1, + 95.8, 95.5, 95.2, 95.35, 95.45, # 15-29 bear trail +] +assert len(closes) == 30, len(closes) + +# wick-uri: candela 13 are nevoie de un wick sus până la 100.85 (SL-ul de 4 +# candele); restul sunt mici, ca seria să arate ca un grafic real. +wicks = [ + (0.15, 0.20), (0.12, 0.18), (0.15, 0.15), (0.10, 0.20), (0.12, 0.25), + (0.10, 0.30), (0.25, 0.15), + (0.20, 0.15), (0.15, 0.20), (0.20, 0.15), (0.20, 0.20), (0.20, 0.18), + (0.25, 0.15), (SL - 100.3, 0.15), # 13: high = 100.85 + (0.15, 0.25), # 14: trigger Sell + (0.15, 0.20), (0.15, 0.18), (0.12, 0.20), (0.15, 0.18), (0.15, 0.20), + (0.12, 0.18), (0.15, 0.20), (0.12, 0.18), (0.15, 0.20), (0.12, 0.18), + (0.15, 0.20), (0.12, 0.18), (0.15, 0.20), (0.20, 0.15), (0.30, 0.10), +] +assert len(wicks) == 30, len(wicks) + +vols = [ + 48, 45, 42, 40, 44, 50, 46, + 42, 44, 40, 43, 47, 52, 68, + 85, + 60, 55, 52, 48, 46, 44, 42, 40, 43, 41, 44, 42, 40, 45, 72, +] +assert len(vols) == 30, len(vols) + +cds = from_closes(closes, wicks=wicks, vols=vols, start=98.0) + +# ── assert-urile cerute de specificație ──────────────────────────────────── +# 1. entry == close-ul candelei de trigger 14 +assert round(cds[14].c, 2) == ENTRY == 100.00, cds[14].c + +# 2. SL == max high pe candelele 11:15 +max_h = max(c.h for c in cds[11:15]) +assert round(max_h, 2) == SL == 100.85, max_h + +# 3. SL = +0.85% față de entry +assert abs((SL - ENTRY) / ENTRY - 0.0085) < 1e-4 + +# 4. TP1 = entry − (SL − entry) = 1:1 +assert TP1 == round(ENTRY - (SL - ENTRY), 2) == 99.15, TP1 +assert abs((ENTRY - TP1) / ENTRY - 0.0085) < 1e-4 + +# 5. exit == close-ul candelei 29 +assert round(cds[29].c, 2) == EXIT == 95.45, cds[29].c + +# 6. R efectiv ≈ 1:5.3 +assert abs((ENTRY - EXIT) / (SL - ENTRY) - 5.35) < 0.05 + +# 7. niciun close după trigger nu trece peste SL +assert max(c.c for c in cds[15:]) < SL, max(c.c for c in cds[15:]) + +# 8. fiecare open == close-ul barei anterioare +for a, b in zip(cds, cds[1:]): + assert abs(a.c - b.o) < 1e-9, "serie cu gol artificial între bare" + +# 9. candela 14 închide în jos după pullback-ul Buy (7-13) +assert cds[14].c < cds[13].c, (cds[13].c, cds[14].c) +assert any(cds[i].c > cds[i].o for i in range(7, 14)), "pullback Buy absent" + +# ── geometria, ca să așez marks-urile „below" fără suprapunere ───────────── +SCALE = 42 +levels = [ + Level(SL, "SL 100.85 = +0.85%", "#c00"), + Level(ENTRY, "intrare 100.00 (trigger Sell)", "#333"), + Level(TP1, "TP1 99.15 = 1:1", "#1a5fb4"), + Level(EXIT, "ieșire 95.45 = +4.55% ≈ 1:5.3", "#188a3e"), +] +prices = [p for c in cds for p in (c.o, c.h, c.l, c.c)] + [lv.price for lv in levels] +pmax, pmin = max(prices), min(prices) +top = 34 + 22 # implicit + titlu, fără segment de trend +body_bottom = top + (pmax - pmin) * SCALE + + +def y(price: float) -> float: + return top + (pmax - price) * SCALE + + +# ── așezarea etichetelor: caut un y unde textul nu atinge vreun corp de +# candelă și nu se suprapune cu altă etichetă. Sub grafic sunt doar ~14 px +# liberi (body_bottom .. body_bottom+14), deci etichetele lungi trebuie +# coborâte/diferite pe rânduri. Fără asta, „A" și „M" trec peste candele. +GAP, LEFT, WIDTH, BODY_W = 15, 44, 700, 10 + + +def cx(i: int) -> float: + return LEFT + i * GAP + + +def text_x(i: int, text: str) -> tuple[float, float]: + """(x0, x1) al textului bold ancorat pe candela i, ca în svg_candles.""" + x = cx(i) + w = len(text) * 7.44 + if x < 90: + return x - 12, x - 12 + w + if x > WIDTH - 90: + return x + 12 - w, x + 12 + return x - w / 2, x + w / 2 + + +corpuri = [] +for i, c in enumerate(cds): + y0, y1 = y(max(c.o, c.c)), y(min(c.o, c.c)) + y1 = max(y1, y0 + 1.6) + corpuri.append((cx(i) - BODY_W / 2, cx(i) + BODY_W / 2, y0, y1)) + +linii = [y(lv.price) for lv in levels] + + +def liber_fara_puse(i: int, text: str, yv: float) -> bool: + x0, x1 = text_x(i, text) + ty0, ty1 = yv - 10, yv + 3 + if x0 < -2 or x1 > WIDTH + 2 or yv < top + 12 or yv > body_bottom + 14: + return False + for bx0, bx1, by0, by1 in corpuri: + if x0 < bx1 - 2 and bx0 < x1 - 2 and ty0 < by1 - 2 and by0 < ty1 - 2: + return False + # nicio linie de nivel (SL/entry/TP1/exit) prin mijlocul textului + for ly in linii: + if ty0 - 1 <= ly <= ty1 + 1: + return False + return True + + +def conflict(a: tuple, b: tuple) -> bool: + """Două etichete se bat dacă au aceeași bandă de y și se suprapun pe x.""" + ax0, ax1, ay = a + bx0, bx1, by = b + return abs(ay - by) < 13 and ax0 < bx1 - 2 and bx0 < ax1 - 2 + + +puse: list[tuple] = [] +definiții = [ + (0, "TF mare (15 min) în Sell", "#c00", "above"), + (7, "M: Buy Momentum (pullback)", "#0891b2", "above"), + (14, "A: primul Sell = trigger, intri short", "#c00", "below"), + (17, "deja peste TP1 (1.42% din curs)", "#188a3e", "below"), + (29, "close peste bear trail = ieși", "#188a3e", "below"), +] + + +def natural_y(i: int, where: str) -> float: + return (y(cds[i].h) - 10 if where == "above" + else min(y(cds[i].l) + 20, body_bottom + 14)) + + +# candidați: toate y-urile care nu ating niciun corp de candelă, ordonate +# după cât de aproape sunt de poziția naturală (cu preferință pentru partea +# cerută de specificație: „above" în sus, „below" în jos) +candidati = [] +for i, text, _color, where in definiții: + nat = natural_y(i, where) + ys = [yy for yy in range(int(top + 12), int(body_bottom + 14) + 1) + if liber_fara_puse(i, text, yy)] + if where == "below": + # o etichetă „below" rămâne sub candelă: nu urcă peste preț + ys = [yy for yy in ys if yy >= nat] + ys.sort(key=lambda yv: yv - nat) + else: + # „above" preferă în sus, dar dacă nu încape coboară sub serie + ys.sort(key=lambda yv: (abs(yv - nat), 0 if yv <= nat else 1)) + candidati.append(ys) + assert ys, f"nicio poziție liberă pentru {text!r}" + +# backtracking: plasez întâi etichetele cu cele mai puține opțiuni +ordine = sorted(range(len(definiții)), key=lambda k: len(candidati[k])) +y_final = [None] * len(definiții) + + +def rezolva(k: int) -> bool: + if k == len(ordine): + return True + idx = ordine[k] + ci = definiții[idx][0] + for yv in candidati[idx]: + box = (*text_x(ci, definiții[idx][1]), yv) + if all(not conflict(box, p) for p in puse): + puse.append(box) + y_final[idx] = yv + if rezolva(k + 1): + return True + puse.pop() + return False + + +assert rezolva(0), "nu găsesc o așezare fără suprapuneri pentru etichete" + +marks = [Mark(i, text, color, where, dy=y_final[k] - natural_y(i, where)) + for k, (i, text, color, where) in enumerate(definiții)] +assert all(y_final), y_final + + +ch = Chart( + candles=cds, + levels=levels, + marks=marks, + scale=SCALE, gap=GAP, body_w=BODY_W, left=LEFT, width=WIDTH, vol_h=70, + vol_label="volum", + title="RIOT — 3 min (TF mare 15 min în Sell)", + caption="Axa de preț e construită (intrarea = 100.00); procentele sunt reale din curs.", +) +svg = ch.render() +assert "]*)>([^<]*)', svg) +cutii, cutii_text = [], [] +for xs, ys, attrs, continut in texte: + x, yv = float(xs), float(ys) + w = len(continut) * (7.44 if "bold" in attrs else 6.72) + if 'text-anchor="end"' in attrs: + x0 = x - w + elif 'text-anchor="middle"' in attrs: + x0 = x - w / 2 + else: + x0 = x + assert -2 <= x0 and x0 + w <= W + 2, f"etichetă în afara pânzei: {continut!r}" + assert 0 <= yv <= H, f"etichetă în afara înălțimii: {continut!r}" + cutii.append((x0, x0 + w, yv, continut)) + cutii_text.append((x0, x0 + w, yv - 10, yv + 3, continut)) +for i in range(len(cutii)): + for j in range(i + 1, len(cutii)): + a, b = cutii[i], cutii[j] + if abs(a[2] - b[2]) < 13 and a[0] < b[1] - 2 and b[0] < a[1] - 2: + raise AssertionError(f"texte suprapuse: {a[3]!r} / {b[3]!r}") + +# ── verificare: nicio etichetă peste un corp de candelă ──────────────────── +corpuri = [] +for mm in re.finditer( + r'', svg): + x0, y0, hh = map(float, mm.groups()) + corpuri.append((x0, x0 + 10, y0, y0 + hh)) +for tx0, tx1, ty0, ty1, continut in cutii_text: + for cx0, cx1, cy0, cy1 in corpuri: + if tx0 < cx1 - 2 and cx0 < tx1 - 2 and ty0 < cy1 - 2 and cy0 < ty1 - 2: + raise AssertionError( + f"etichetă peste candelă: {continut!r} la y~{ty0:.0f} " + f"({tx0:.0f}-{tx1:.0f}) vs rect {cx0:.0f}-{cx1:.0f} " + f"y {cy0:.0f}-{cy1:.0f}") + +# legenda mai spune explicit că axa e construită (cerință de fidelitate) +assert "construită" in svg and "procentele sunt reale" in svg, \ + "lipsește nota despre axa construită" + +OUT.write_text(svg, encoding="utf-8") +print(f"serie {len(cds)} candele | intrare {ENTRY:.2f} @ bara 14 | " + f"SL {SL:.2f} (+0.85%) | TP1 {TP1:.2f} (1:1) | " + f"ieșire {EXIT:.2f} (+4.55%, R {(ENTRY - EXIT) / (SL - ENTRY):.2f})") +print(f"scris {OUT}") diff --git a/gen/strategii/m2d_exemplu.svg b/gen/strategii/m2d_exemplu.svg new file mode 100644 index 0000000..a01467b --- /dev/null +++ b/gen/strategii/m2d_exemplu.svg @@ -0,0 +1,109 @@ + + RIOT — 3 min (TF mare 15 min în Sell) + + SL 100.85 = +0.85% + + intrare 100.00 (trigger Sell) + + TP1 99.15 = 1:1 + + ieșire 95.45 = +4.55% ≈ 1:5.3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + TF mare (15 min) în Sell + M: Buy Momentum (pullback) + A: primul Sell = trigger, intri short + deja peste TP1 (1.42% din curs) + close peste bear trail = ieși + Axa de preț e construită (intrarea = 100.00); procentele sunt reale din curs. + \ No newline at end of file diff --git a/gen/strategii/m2d_secventa.py b/gen/strategii/m2d_secventa.py new file mode 100644 index 0000000..db1c725 --- /dev/null +++ b/gen/strategii/m2d_secventa.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +"""Secvența M → A a strategiei M2D — schemă conceptuală pe două panouri. + +Pentru `strategii/03_m2d.md`, marcajul ``. +Nu e grafic de preț (spec: „schemă pe două panouri, nu candele de trade”): +SVG scris de mână, doar stdlib — nu se folosește svg_candles / pchart. + +Ce arată (toate etichetele vin din spec; nicio cifră de preț nu se inventează): +- panoul de sus, LONG: pe TF mare (15 min) polilinia urcă și „rămâne în Buy pe + tot parcursul”; pe TF mic (3 min) urcă → pullback (M: Sell Momentum) → urcă + peste vârful anterior (A: Buy Momentum — TRIGGER, intri long); +- panoul de jos, SHORT: oglindit — TF mare coboară („rămâne în Sell”), TF mic + coboară → pullback în sus (M: Buy Momentum) → coboară sub minimul anterior + (A: Sell Momentum — TRIGGER, intri short); +- două linii verticale punctate leagă momentul M și momentul A de rândul de sus + (verificarea că TF mare rămâne în aceeași direcție); +- textul de subsol: „Secvența contează, nu starea...”. + +Rulare: python3 gen/strategii/m2d_secventa.py -> gen/strategii/m2d_secventa.svg +""" +from pathlib import Path +from xml.sax.saxutils import escape + +OUT = Path(__file__).with_suffix(".svg") + +W, H = 700, 780 +BLACK, GRAY, RED, GREEN, BLUE, CYAN, WHITE = ( + "#000000", "#777777", "#c00", "#188a3e", "#1a5fb4", "#00b8c4", "#ffffff") +PANEL_BG, PANEL_EDGE = "#fafafa", "#ddd" + +# ---------------------------------------------------------------- geometrie +# Fiecare polilinie: listă de (x, y). y mic = sus. +# Index: 0 = start, 1 = vârf/trough anterior, 2 = M, 3 = A, 4 = continuare. +LONG_TOP = [(40, 162), (150, 148), (250, 138), (430, 128), (660, 118)] +LONG_BOT = [(40, 255), (180, 225), (250, 282), (430, 210), (660, 198)] +SHORT_TOP = [(40, 430), (150, 444), (250, 454), (430, 464), (660, 474)] +SHORT_BOT = [(40, 527), (180, 562), (250, 500), (430, 574), (660, 588)] + +L_M, L_A, S_M, S_A = 2, 3, 2, 3 # indicii momentelor M și A + +PANELS = { + "LONG": (14, 92, 672, 250), + "SHORT": (14, 362, 672, 270), +} + +TEXTS = [] # (x, y, text, size, weight, anchor, color) pentru verificare + + +def txt(x, y, s, size=13, weight="400", fill=BLACK, anchor="middle"): + TEXTS.append((x, y, s, size, weight, anchor, fill)) + return (f'' + f'{escape(s)}') + + +def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None): + d = f' stroke-dasharray="{dash}"' if dash else "" + return (f'') + + +def poly(pts, stroke, width=2.0): + p = " ".join(f"{x:.1f},{y:.1f}" for x, y in pts) + return f'' + + +def dot(x, y, fill, r=5.0): + return f'' + + +def build(): + p = [ + f'', + '', + f'', + txt(W / 2, 30, "Secvența M → A: nu e o stare, e o secvență", + 19, "700", BLACK), + txt(W / 2, 52, "M2D — același moment, două time frame-uri: " + "TF mare dă direcția, TF mic dă trigger-ul", + 13, "400", GRAY), + txt(W / 2, 72, "sus = LONG; jos = SHORT — aceleași momente, în oglindă", + 13, "400", GRAY), + ] + + # ------------------------------------------------------ panouri + for name, (px, py, pw, ph) in PANELS.items(): + p.append(f'') + + # ------------------------------------------------------ panoul LONG + p.append(txt(30, 116, "LONG", 15, "700", GREEN, "start")) + p.append(txt(30, 140, "TF mare (ex. 15 min)", 13, "400", GRAY, "start")) + p.append(txt(660, 114, "rămâne în Buy pe tot parcursul", 13, "400", + GREEN, "end")) + for x, y_top, y_bot in ((LONG_TOP[L_M][0], LONG_TOP[L_M][1], LONG_BOT[L_M][1]), + (LONG_TOP[L_A][0], LONG_TOP[L_A][1], LONG_BOT[L_A][1])): + p.append(line(x, y_top, x, y_bot, "#999999", 1.2, "4 4")) + p.append(poly(LONG_TOP, GREEN)) + p.append(dot(*LONG_TOP[1], CYAN)) + p.append(txt(185, 126, "Buy Momentum", 13, "400", CYAN, "start")) + + p.append(txt(30, 202, "TF mic (ex. 3 min)", 13, "400", GRAY, "start")) + p.append(poly(LONG_BOT, "#333333")) + p.append(dot(*LONG_BOT[L_M], RED)) + p.append(txt(262, 296, "M: Sell Momentum (pullback)", 13, "400", + RED, "start")) + p.append(dot(*LONG_BOT[L_A], GREEN, r=6)) + p.append(txt(660, 240, "A: Buy Momentum — TRIGGER, intri long", + 13, "400", GREEN, "end")) + + # ------------------------------------------------------ panoul SHORT + p.append(txt(30, 386, "SHORT", 15, "700", RED, "start")) + p.append(txt(30, 410, "TF mare (ex. 15 min)", 13, "400", GRAY, "start")) + p.append(txt(660, 494, "rămâne în Sell pe tot parcursul", 13, "400", + RED, "end")) + for x, y_top, y_bot in ((SHORT_TOP[S_M][0], SHORT_TOP[S_M][1], SHORT_BOT[S_M][1]), + (SHORT_TOP[S_A][0], SHORT_TOP[S_A][1], SHORT_BOT[S_A][1])): + p.append(line(x, y_top, x, y_bot, "#999999", 1.2, "4 4")) + p.append(poly(SHORT_TOP, RED)) + p.append(dot(*SHORT_TOP[1], RED)) + p.append(txt(168, 432, "Sell Momentum", 13, "400", RED, "start")) + + p.append(txt(30, 470, "TF mic (ex. 3 min)", 13, "400", GRAY, "start")) + p.append(poly(SHORT_BOT, "#333333")) + p.append(dot(*SHORT_BOT[S_M], CYAN)) + p.append(txt(238, 494, "M: Buy Momentum (pullback)", 13, "400", + CYAN, "end")) + p.append(dot(*SHORT_BOT[S_A], RED, r=6)) + p.append(txt(660, 610, "A: Sell Momentum — TRIGGER, intri short", + 13, "400", RED, "end")) + + # ------------------------------------------------------ subsol + p.append(f'') + p.append(txt(W / 2, 676, + "Secvența contează, nu starea: mare în Buy → mic în Sell → " + "mic în Buy", 13, "400", BLACK)) + p.append(txt(W / 2, 696, + "(cu mare încă în Buy). Un Buy direct pe TF mic, fără Sell " + "între ele,", 13, "400", BLACK)) + p.append(txt(W / 2, 716, "NU e trigger (vezi 1.3).", 13, "400", BLACK)) + p.append(txt(W / 2, 746, + "Sursă: 14_strategia_1_m2d.md:30-38, 172-180; " + "09_QA/08_2026-01-20.md:65-67", 13, "400", GRAY)) + + p.append('') + return "\n".join(p) + "\n" + + +# ---------------------------------------------------------------- verificare +def est_w(s, size, weight): + factor = 0.62 if weight == "700" else 0.56 + return len(s) * size * factor + + +def verify(svg): + # 1. geometria: panourile nu se suprapun, sunt în panză + long_ = PANELS["LONG"] + short_ = PANELS["SHORT"] + assert long_[1] + long_[3] < short_[1], "panourile LONG/SHORT se suprapun" + assert short_[1] + short_[3] < 648, "panoul SHORT intră în subsol" + for _, (px, py, pw, ph) in PANELS.items(): + assert px >= 0 and py >= 0 and px + pw <= W and py + ph <= H + + # 2. secvența LONG: TF mare rămâne în Buy (urcă monoton: y strict descrescător) + ys = [y for _, y in LONG_TOP] + assert all(ys[i] > ys[i + 1] for i in range(len(ys) - 1)), \ + "LONG: TF mare nu urcă monoton (nu rămâne în Buy)" + # M = pullback (minim local), A = reluare peste vârful anterior + assert LONG_BOT[L_M][1] > LONG_BOT[L_M - 1][1] and \ + LONG_BOT[L_M][1] > LONG_BOT[L_M + 1][1], "LONG: M nu e un minim local" + assert LONG_BOT[L_A][1] < LONG_BOT[1][1], \ + "LONG: A nu urcă peste vârful anterior (pullback-ul)" + assert LONG_TOP[L_M][0] < LONG_TOP[L_A][0], "LONG: M trebuie înainte de A" + assert LONG_TOP[L_M][0] == LONG_BOT[L_M][0] and \ + LONG_TOP[L_A][0] == LONG_BOT[L_A][0], \ + "LONG: liniile verticale nu leagă exact M și A de rândul de sus" + + # 3. secvența SHORT: oglindit + ys = [y for _, y in SHORT_TOP] + assert all(ys[i] < ys[i + 1] for i in range(len(ys) - 1)), \ + "SHORT: TF mare nu coboară monoton (nu rămâne în Sell)" + assert SHORT_BOT[S_M][1] < SHORT_BOT[S_M - 1][1] and \ + SHORT_BOT[S_M][1] < SHORT_BOT[S_M + 1][1], "SHORT: M nu e un maxim local" + assert SHORT_BOT[S_A][1] > SHORT_BOT[1][1], \ + "SHORT: A nu coboară sub minimul anterior" + assert SHORT_TOP[S_M][0] < SHORT_TOP[S_A][0], "SHORT: M trebuie înainte de A" + assert SHORT_TOP[S_M][0] == SHORT_BOT[S_M][0] and \ + SHORT_TOP[S_A][0] == SHORT_BOT[S_A][0], \ + "SHORT: liniile verticale nu leagă exact M și A de rândul de sus" + + # 4. verticalele chiar traversează de la rândul de sus până la moment + assert LONG_TOP[2][1] < LONG_BOT[L_M][1] + assert LONG_TOP[3][1] < LONG_BOT[L_A][1] + assert SHORT_TOP[2][1] < SHORT_BOT[S_M][1] + assert SHORT_TOP[3][1] < SHORT_BOT[S_A][1] + + # 5. toate etichetele în panză + for x, y, s, size, weight, anchor, _ in TEXTS: + ew = est_w(s, size, weight) + left = x - ew if anchor == "end" else (x - ew / 2 if anchor == "middle" else x) + right = left + ew + assert -2 <= left and right <= W + 2, f"text în afara panzei: {s!r}" + assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y})" + + # 6. etichete care nu se suprapun + for i in range(len(TEXTS)): + xi, yi, si, szi, wi, ai, _ = TEXTS[i] + we = est_w(si, szi, wi) + li = xi - we if ai == "end" else (xi - we / 2 if ai == "middle" else xi) + ri = li + we + for j in range(i + 1, len(TEXTS)): + xj, yj, sj, szj, wj, aj, _ = TEXTS[j] + wej = est_w(sj, szj, wj) + lj = xj - wej if aj == "end" else (xj - wej / 2 if aj == "middle" else xj) + rj = lj + wej + if abs(yi - yj) < 13 and li < rj - 4 and lj < ri - 4: + raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}") + + # 7. niciun text nu se tastează peste o polilinie (coliziune text-linie) + segments = [(LONG_TOP, PANELS["LONG"]), (LONG_BOT, PANELS["LONG"]), + (SHORT_TOP, PANELS["SHORT"]), (SHORT_BOT, PANELS["SHORT"])] + for x, y, s, size, weight, anchor, _ in TEXTS: + ew = est_w(s, size, weight) + left = x - ew if anchor == "end" else (x - ew / 2 if anchor == "middle" else x) + right = left + ew + top, bot = y - size + 1, y + for pts, _ in segments: + for (x1, y1), (x2, y2) in zip(pts, pts[1:]): + xa, xb = min(x1, x2), max(x1, x2) + if xb < left - 2 or xa > right + 2: + continue + for xc in (max(xa, left), min(xb, right)): + t = 0 if x2 == x1 else (xc - x1) / (x2 - x1) + yc = y1 + t * (y2 - y1) + if top - 1 <= yc <= bot + 1: + raise AssertionError( + f"text peste polilinie: {s!r} (y={y}, linia y={yc:.0f})") + + # 8. etichetele obligatorii din spec sunt prezente + for cheie in ("LONG", "SHORT", + "TF mare (ex. 15 min)", "TF mic (ex. 3 min)", + "rămâne în Buy pe tot parcursul", + "rămâne în Sell pe tot parcursul", + "Buy Momentum", "Sell Momentum", + "M: Sell Momentum (pullback)", + "A: Buy Momentum — TRIGGER, intri long", + "M: Buy Momentum (pullback)", + "A: Sell Momentum — TRIGGER, intri short", + "Secvența contează, nu starea", + "NU e trigger (vezi 1.3)"): + assert cheie in svg, f"lipsește din desen: {cheie!r}" + + # 9. fără cifre de preț inventate (spec: diagramă fără niveluri) + for interzis in ("$", "%", "€", "intrare ", "SL ", "TP"): + assert interzis not in svg, f"nu ar trebui să apară preț: {interzis!r}" + return True + + +SVG = build() +assert verify(SVG), "verificare eșuată" +OUT.write_text(SVG, encoding="utf-8") +print(f"scris {OUT} ({W}x{H}), verificare OK: {len(TEXTS)} etichete, " + f"{len(LONG_TOP) + len(LONG_BOT) + len(SHORT_TOP) + len(SHORT_BOT)} puncte") diff --git a/gen/strategii/m2d_secventa.svg b/gen/strategii/m2d_secventa.svg new file mode 100644 index 0000000..f835cf0 --- /dev/null +++ b/gen/strategii/m2d_secventa.svg @@ -0,0 +1,42 @@ + + + +Secvența M → A: nu e o stare, e o secvență +M2D — același moment, două time frame-uri: TF mare dă direcția, TF mic dă trigger-ul +sus = LONG; jos = SHORT — aceleași momente, în oglindă + + +LONG +TF mare (ex. 15 min) +rămâne în Buy pe tot parcursul + + + + +Buy Momentum +TF mic (ex. 3 min) + + +M: Sell Momentum (pullback) + +A: Buy Momentum — TRIGGER, intri long +SHORT +TF mare (ex. 15 min) +rămâne în Sell pe tot parcursul + + + + +Sell Momentum +TF mic (ex. 3 min) + + +M: Buy Momentum (pullback) + +A: Sell Momentum — TRIGGER, intri short + +Secvența contează, nu starea: mare în Buy → mic în Sell → mic în Buy +(cu mare încă în Buy). Un Buy direct pe TF mic, fără Sell între ele, +NU e trigger (vezi 1.3). +Sursă: 14_strategia_1_m2d.md:30-38, 172-180; 09_QA/08_2026-01-20.md:65-67 + diff --git a/gen/strategii/mbp_ansamblu.py b/gen/strategii/mbp_ansamblu.py new file mode 100644 index 0000000..ea5a8e5 --- /dev/null +++ b/gen/strategii/mbp_ansamblu.py @@ -0,0 +1,201 @@ +"""Diagrama de ansamblu MBP / MBPA++ (strategii, marcajul `ansamblu`). + +„SPY — 15 min: bandă + TICUS pe aceeași candelă → trigger → intrare → SL → +TP1 → trail". Graficul întreg al trade-ului long din exemplul cursului, cu +panoul TICUS dedesubt, legat de candela M printr-o linie punctată verticală +(aceeași candelă sus, aceeași valoare jos). + +Toate cifrele desenate vin din curs +(`summaries/05_MODUL_4/03_strategia_6_mbp_mbappe.md`): + - M: close 623.87 < banda inferioară 623.90, TICUS -1150 (liniile 336-342); + - bandă superioară 624.70 și linia 624.30 (SMA20 / TP1) — diagrama + principală (liniile 114-148); + - trigger Buy Momentum → entry 623.95 (liniile 336-342); + - SL 623.60 = lowest low din ultimele 4 candele, trigger inclus (93-94); + - TP1 624.30 (+0.35, 1:1) (95-96); + - „realizat ~1:4 datorită pozițiilor pe trail" (428-429). Prețul de ieșire + ~625.35 = entry + 4R este ilustrativ, derivat de mine (nu e preț din curs). + +Structura barelor dintre niveluri e construită de mine; o construiesc astfel +încât fiecare nivel afirmat în text să iasă exact din serie (assert-uri mai +jos). Nu există nivel de suport major cu cifră în exemplul din curs. + +Rulează: + python3 gen/strategii/mbp_ansamblu.py +Scrie lângă el `mbp_ansamblu.svg`. +""" +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__).resolve().parent / "mbp_ansamblu.svg" + +GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000" + +# ---------------------------------------------------------------- seria (30 bare) +# 14 bare de downtrend 625.60 -> 624.15 (open-ul fiecărei bare = close-ul +# precedentei), apoi M (index 14), trigger (15), 14 bare de urcare spre 625.35. +DOWN = [625.60, 625.42, 625.55, 625.28, 625.05, 625.12, 624.88, + 624.65, 624.72, 624.50, 624.35, 624.30, 624.10, 624.15] # 0-13 +M_IX, TR_IX = 14, 15 +UP = [624.00, 624.08, 624.16, 624.30, 624.40, 624.50, 624.60, 624.70, + 624.82, 624.95, 625.08, 625.20, 625.28, 625.35] # 16-29 +CLOSES = DOWN + [623.87, 623.95] + UP # M, trigger +assert len(CLOSES) == 30, len(CLOSES) + +# fitile (sus, jos) per bară. Ultimele 3 bare înainte de M au minimele +# 623.95 / 623.80 / 623.72 (peste 623.60), ca SL-ul calculat să fie exact +# 623.60 (low-ul lui M). +WICKS = ([(0.05, 0.18)] + [(0.18, 0.18)] * 10 + + [(0.18, 0.35), # 11: low 623.95 + (0.18, 0.30), # 12: low 623.80 (open 624.30, close 624.10) + (0.10, 0.38)] # 13: low 623.72 (open 624.10, close 624.15) + + [(0.05, 0.27), # 14 M: high 624.20, low 623.60 + (0.05, 0.12)] # 15 trigger: high 624.00, low 623.75 + + [(0.05, 0.12)] * 3 # 16-18 + + [(0.00, 0.12)] # 19: high atinge exact 624.30 (TP1) + + [(0.05, 0.12)] * 9 + + [(0.00, 0.12)]) # 29: high = close = 625.35 (ieșirea ilustrativă) +assert len(WICKS) == len(CLOSES), len(WICKS) + +# TICUS: un singur bar relevant, pe candela M, la -1150 (sub -1000) +TICUS = {M_IX: -1150.0} + +# fără panou de volum: panoul de dedesubt e cel de TICUS +cds = from_closes(CLOSES, wicks=WICKS, start=625.75) + +# ---------------------------------------------------------------- assert-urile din spec +entry = cds[TR_IX].c +assert abs(entry - 623.95) < 0.01, entry +assert entry == cds[TR_IX].c # intrarea = close-ul trigger-ului +assert cds[M_IX].c < 623.90 # M închide sub banda inferioară +assert cds[M_IX].c < cds[M_IX].o # M e roșie (close < open) +ticus_m = TICUS[M_IX] +assert ticus_m <= -1000, ticus_m # =-1150, pragul depășit +assert abs(cds[M_IX].c - 623.87) < 0.005 +assert abs(cds[M_IX].h - 624.20) < 0.005 +assert abs(cds[TR_IX].h - 624.00) < 0.005 +assert abs(cds[TR_IX].l - 623.75) < 0.005 + +# SL = cel mai jos minim din ultimele 4 candele încheiate de trigger, inclusiv +window = cds[TR_IX - 3:TR_IX + 1] +assert len(window) == 4 +sl = min(c.l for c in window) +assert abs(sl - 623.60) < 0.01, sl +assert sl == cds[M_IX].l # low-ul lui M e minimul ferestrei +assert cds[TR_IX].l > sl # trigger-ul nu face low nou +# cele trei candele dinaintea lui M au minime peste 623.60 (ex. 623.95/623.80/623.72) +for cd in cds[TR_IX - 4:TR_IX - 1]: + assert cd.l > sl, (cd.o, cd.c, cd.l) + +risc = entry - sl +assert abs(risc - 0.35) < 0.005, risc +tp1 = entry + risc +assert abs(tp1 - (entry + (entry - sl))) < 0.005 +assert abs(tp1 - 624.30) < 0.005, tp1 +# TP1 nu se atinge prea devreme, ci 4 bare după intrare (cum spune spec-ul) +assert max(c.h for c in cds[TR_IX + 1:TR_IX + 4]) < tp1 - 1e-9 +assert abs(cds[TR_IX + 4].h - tp1) < 1e-9 + +exit_ill = cds[-1].c # ~625.35, ilustrativ (derivat) +assert abs(exit_ill - 625.35) < 0.01, exit_ill +assert abs((exit_ill - entry) / risc - 4.0) < 0.15, (exit_ill - entry) / risc + +# nu există nicio linie de stop desenată deasupra intrării +assert sl < entry +levels = [ + Level(623.90, "623.90 — bandă inferioară", BLUE, dash="4 3", label_at="left"), + Level(624.70, "624.70 — bandă superioară", BLUE, dash="4 3", label_at="right"), + Level(624.30, "624.30 — SMA20 / TP1", GRAY, dash="4 3", label_at="left"), + Level(sl, f"SL {sl:.2f} (lowest low, 4 candele)", RED, label_at="left"), + Level(entry, f"trigger Buy Momentum → intrare {entry:.2f}", GREEN, label_at="right"), + Level(625.35, "ieșire ilustrativă ~625.35 (≈ 1:4)", GREEN, dash="2 3", label_at="right"), +] +assert all("stop" not in lv.text.lower() or lv.price < entry for lv in levels) +assert all(not ("sl" in lv.text.lower() and lv.price > entry) for lv in levels) + +marks = [ + Mark(M_IX, "M: roșie, close 623.87 sub bandă", RED, "below"), +] + +# titlul si eticheta de trend sunt injectate manual (nu prin Chart.title, care +# ar sta lipit de prima candelă, si nici prin Chart.trend, care s-ar bate cu +# titlul) ca sa rămână spațiu liber deasupra seriei. +ch = Chart(candles=cds, levels=levels, marks=marks, + scale=150.0, gap=16, body_w=10, left=48, width=700, top=96) +svg = ch.render() + +TITLE = "SPY — 15 min: bandă + TICUS pe aceeași candelă → trigger → intrare → SL → TP1 → trail" + +# ---------------------------------------------------------------- panoul TICUS +# `Chart` nu are panou de indicator; îl adaug după randare, cu aceeași scară +# pe orizontală (ch._cx), ca barul TICUS al lui M să cadă exact sub candela M. +m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg) +W, H0 = int(m.group(1)), float(m.group(2)) + +ticus_top = H0 + 34 +y0 = ticus_top + 70 # linia 0 +px1000 = 50.0 # 1000 unități TICUS = 50 px +def ty(v: float) -> float: + return y0 - v / 1000.0 * px1000 + +xm = ch._cx(M_IX) +bar_top, bar_bot = y0, ty(ticus_m) # bar de la 0 în jos la -1150 + +panel = [ + f'TICUS — presiunea pieței (↑ = peste 0)', + # linii de prag + f'', + f'+1000 — supracumpărare', + f'', + f'+500 (doar referință vizuală)', + f'', + f'0', + f'', + f'-500 (doar referință vizuală)', + f'', + f'-1000 — oversold', + # barul TICUS al candelei M, sub -1000 (colorat roșu) + f'', + f'M: TICUS -1150 (sub -1000)', +] +# linia punctată verticală: aceeași candelă sus și jos (pornește sub mark-ul +# lui M, ca să nu taie textul „M: roșie...") +y_candle_m = ch._y(cds[M_IX].l) + 26 +panel.insert(0, f'') + +legend_top = ty(-1000) + 44 +legend = [ + "SPY 15 min. Cifre din curs: M close 623.87 (sub bandă 623.90), TICUS -1150 pe aceeași candelă,", + "trigger Buy Momentum → entry 623.95, SL 623.60 (lowest low din 4 candele, trigger inclus),", + "TP1 624.30 (+0.35, 1:1). Ieșirea ~625.35 = entry + 4R (≈1:4) e ilustrativă — preț derivat din R,", + "nu din curs. Structura candelelor (30 bare continue) e construită de mine.", +] +legend_svg = [f'{t}' + for i, t in enumerate(legend)] + +header = [ + f'{TITLE}', + 'trend descendent anterior (14 bare)', +] + +new_h = legend_top + 17 * (len(legend) - 1) + 14 +svg = re.sub(r'(viewBox="0 0 \d+ )[0-9.]+(")', lambda mm: mm.group(1) + f"{new_h:.0f}" + mm.group(2), svg, count=1) +svg = svg.replace("\n", "\n " + "\n ".join(header + panel + legend_svg) + "\n") + +assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown" +# barul TICUS cade exact sub candela M (aceeași candelă sus și jos) +bar_svg = f'' +assert bar_svg in svg, "barul TICUS nu a fost desenat" +assert abs((float(re.search(r'x="([\d.]+)"', bar_svg).group(1)) + 5) - ch._cx(M_IX)) < 1e-9 +# aceeași scară px/unitate: SL-intrare și intrare-TP1 sunt egale și vizual (1:1) +assert abs((ch._y(sl) - ch._y(entry)) - (ch._y(entry) - ch._y(tp1))) < 1e-9 +assert f'x1="{xm:.1f}"' in svg +OUT.write_text(svg) +print(f"scris {OUT.name}: {len(cds)} candele, entry {entry:.2f}, SL {sl:.2f}, " + f"TP1 {tp1:.2f}, ieșire ilustrativă {exit_ill:.2f} ({(exit_ill-entry)/risc:.2f}R), " + f"ticus(M)={ticus_m:.0f}, x(M)={xm:.0f}") diff --git a/gen/strategii/mbp_ansamblu.svg b/gen/strategii/mbp_ansamblu.svg new file mode 100644 index 0000000..5822b75 --- /dev/null +++ b/gen/strategii/mbp_ansamblu.svg @@ -0,0 +1,95 @@ + + + 623.90 — bandă inferioară + + 624.70 — bandă superioară + + 624.30 — SMA20 / TP1 + + SL 623.60 (lowest low, 4 candele) + + trigger Buy Momentum → intrare 623.95 + + ieșire ilustrativă ~625.35 (≈ 1:4) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + M: roșie, close 623.87 sub bandă + SPY — 15 min: bandă + TICUS pe aceeași candelă → trigger → intrare → SL → TP1 → trail + trend descendent anterior (14 bare) + + TICUS — presiunea pieței (↑ = peste 0) + + +1000 — supracumpărare + + +500 (doar referință vizuală) + + 0 + + -500 (doar referință vizuală) + + -1000 — oversold + + M: TICUS -1150 (sub -1000) + SPY 15 min. Cifre din curs: M close 623.87 (sub bandă 623.90), TICUS -1150 pe aceeași candelă, + trigger Buy Momentum → entry 623.95, SL 623.60 (lowest low din 4 candele, trigger inclus), + TP1 624.30 (+0.35, 1:1). Ieșirea ~625.35 = entry + 4R (≈1:4) e ilustrativă — preț derivat din R, + nu din curs. Structura candelelor (30 bare continue) e construită de mine. + \ No newline at end of file diff --git a/gen/strategii/mbp_exemplu.py b/gen/strategii/mbp_exemplu.py new file mode 100644 index 0000000..0af6719 --- /dev/null +++ b/gen/strategii/mbp_exemplu.py @@ -0,0 +1,242 @@ +"""Diagrama din `gen/strategii/mbp_exemplu.spec.txt` (TIP: PRET). + +Trade-ul short complet din material (SPY, 15 min, 25.11.2025): ~12 candele +de urcare violentă spre vârful ~661, candela M (verde, peste bandă + TICUS +≥ +1000), trigger Sell Momentum → intrare short 660.00, SL 662.71 +(risc 0.41 %), TP1 657.29 (1:1) atins în prima candelă, apoi trail pe 7 +candele de 15 min până la ieșirea la 646.80 = 2 % = ~4.9R. + +Serie continuă de 32 de candele (`from_closes`, open = close anterior), cu +context înainte de urcare, panou TICUS dedesubt (bar ilustrativ la candela M) +și legendă. Toate cifrele din desen vin din specificație; singurele valori +construite de mine sunt structura barelor dintre niveluri și bara TICUS +(marcată explicit „ilustrativ", pentru că valoarea exactă nu e în material). + +Rulare: python3 gen/strategii/mbp_exemplu.py +""" +import re +import sys +from pathlib import Path + +sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402 + +OUT = Path(__file__).with_suffix(".svg") + +# ── cifrele din material ──────────────────────────────────────────────────── +ENTRY = 660.00 # 09_QA/11_2025-11-25.md:565, 632-636 +SL = 662.71 # risc 0.41 % → (sl-entry)/entry*100 +TP1 = 657.29 # entry - (sl - entry) = 1:1 +EXIT = 646.80 # ieșire pe trail = 2 % = ~4.9R +BAND = 660.90 # bandă superioară schematică (peste close-ul lui M) +W, GAP, BODY_W, LEFT = 660, 15, 9, 40 +SCALE = 20.0 + +M_I, E_I, TP1_I, EXIT_I = 23, 24, 25, 31 # M, trigger, TP1, ieșire +N_RISE = M_I - 11 # 12 candele de urcare (idx 12..23) + +# ── seria: context → urcare violentă → M → trigger → TP1 → trail ──────────── +closes = [ + # idx 0-11: context în jurul lui 648, înainte de urcarea violentă + 649.00, 648.60, 649.20, 648.40, 648.90, 648.10, + 648.70, 647.90, 648.50, 648.00, 648.60, 648.30, + # idx 12-23: 12 candele de urcare de la ~648 la vârful ~661; idx 23 = M + 648.60, 649.40, 650.30, 651.20, 652.10, 653.10, + 654.10, 655.20, 656.30, 657.50, 658.80, 660.50, + # idx 24: trigger Sell Momentum → close = intrarea short + 660.00, + # idx 25: prima candelă după intrare atinge TP1 657.29 + 657.80, + # idx 26-31: trail până la ieșirea la 646.80 (6 candele) + 656.40, 655.00, 653.40, 651.60, 649.40, 646.80, +] +assert len(closes) == 32, len(closes) + +wicks = [(0.35, 0.35)] * 12 # context +wicks += [(0.30, 0.25)] * (M_I - 12) # urcarea (idx 12..22) +wicks += [(0.50, 0.20)] # M: high = 661.00 (vârful) +wicks += [(0.20, 0.40), # trigger + (0.25, 0.51), # TP1: low = 657.80-0.51 = 657.29 + (0.30, 0.30), (0.30, 0.30), (0.30, 0.30), + (0.30, 0.30), (0.30, 0.30), (0.30, 0.30)] # trail +assert len(wicks) == len(closes) + +cds = from_closes(closes, wicks=wicks, start=648.90) + +# ── assert-urile din specificație ─────────────────────────────────────────── +entry = cds[E_I].c +sl = SL +tp1 = TP1 +exit_ = cds[EXIT_I].c + +assert abs(entry - 660.00) < 0.01, entry +assert entry == cds[E_I].c, "intrarea nu e close-ul candelei trigger" +assert abs((sl - entry) / entry * 100 - 0.41) < 0.01, (sl - entry) / entry * 100 +assert abs((sl - entry) - 2.71) < 0.01, sl - entry +assert abs(tp1 - (entry - (sl - entry))) < 0.01, (entry - (sl - entry), tp1) +assert cds[TP1_I].l <= tp1 + 1e-9, "prima candelă după intrare nu atinge TP1" +assert abs(cds[TP1_I].l - tp1) < 0.01, cds[TP1_I].l +assert abs((entry - exit_) / entry * 100 - 2.0) < 0.05, (entry - exit_) / entry * 100 +assert abs((entry - exit_) / (sl - entry) - 4.9) < 0.1, (entry - exit_) / (sl - entry) +assert EXIT_I - E_I == 7, EXIT_I - E_I +assert sl > entry > tp1, "short: SL deasupra intrării, TP1 sub intrare" +assert BAND > cds[M_I].c, "banda schematică nu e deasupra close-ului lui M" + +# structura care susține afirmațiile din text +assert cds[M_I].c > cds[M_I].o, "candela M trebuie să fie verde" +assert cds[E_I].c < cds[E_I].o, "candela trigger trebuie să fie roșie" +assert M_I == E_I - 1, "trigger-ul nu e imediat după M" +assert N_RISE == 12, N_RISE +assert 30 <= len(cds) <= 34, len(cds) +assert max(cds[i].c for i in range(12, M_I + 1)) == cds[M_I].c, "M nu e vârful urcării" +assert max(c.h for c in cds) == cds[M_I].h, "vârful seriei nu e la M" +assert all(cds[i].c > cds[i - 1].c for i in range(13, M_I + 1)), "urcarea nu e monotonă" + +# ── panoul de preț ────────────────────────────────────────────────────────── +levels = [ + Level(SL, "SL 662.71 (risc 0.41 %)", "#c00", label_at="right"), + Level(ENTRY, "intrare short 660.00", "#188a3e", label_at="left"), + Level(BAND, "bandă superioară (schematic)", "#777", dash="2 4", label_at="left"), + Level(TP1, "TP1 657.29 (1:1)", "#1a5fb4", label_at="left"), + Level(EXIT, "ieșire trail 646.80 = 2 % = ~4.9R", "#1a5fb4", label_at="left"), +] + +marks = [ + Mark(M_I, "M: candelă verde peste bandă + TICUS ≥ +1000", "#188a3e", "above"), +] + +cds[M_I].note = ("M: valori exacte NEACOPERIT în material (bandă + TICUS); " + "bara ~+1000 din panou e ilustrativă") +cds[E_I].note = "trigger Sell Momentum → intrare short 660.00 (risc 0.41 % = 2.71)" +cds[TP1_I].note = "TP1 657.29 (1:1) atins în prima candelă după intrare" +cds[EXIT_I].note = ("ieșire pe trail 646.80 = 2 % = ~4.9R, după 7 candele " + "de 15 min de la intrare") + +ch = Chart( + candles=cds, + levels=levels, + marks=marks, + mas=[MA(20, "#b06000", dash="4 3", text="SMA-20")], + scale=SCALE, width=W, gap=GAP, body_w=BODY_W, left=LEFT, + title="SPY — 15 min: MBAP short 25.11.2025 (risc 0.41 % → ieșire 4.9R)", + caption="", +) +price_svg = ch.render() + +# corpul graficului de preț și înălțimea lui (pentru lipirea panoului TICUS) +m = re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', price_svg) +H1 = float(m.group(2)) +body1 = price_svg[price_svg.index(">") + 1:price_svg.rindex("")].strip("\n") + +# ── panoul TICUS dedesubt (bar ilustrativ la candela M) ───────────────────── +VGAP = 26 +Y0 = H1 + VGAP +TICUS_H = 100 +zero_y = Y0 + 84 # linia 0 +plus_y = Y0 + 32 # linia +1000 (punctată) +bar_top = plus_y - 18 # bara depășește +1000 +m_x = LEFT + M_I * GAP + +assert bar_top < plus_y, "bara TICUS nu depășește linia +1000" +assert plus_y < zero_y, "linia +1000 nu e deasupra liniei 0" + +ticus = [ + f'' + f'Panou TICUS — linia 0 și pragul +1000 (ilustrativ)', + f'', + f'' + f'+1000 (prag)', + f'', + f'0', + f'', + f'' + f'ilustrativ', + f'' + f'candela M', +] + +# ── legenda (cifrele/sursele) ─────────────────────────────────────────────── +legend = [ + "Legendă: procentele (risc 0.41 %, ieșire 2 %) sunt ale instructorului.", + "Prețurile absolute (660.00 / 662.71 / 657.29 / 646.80) sunt construite de sumarizator —", + "09_QA/11_2025-11-25.md:716-719.", + "Structura de reținut: risc mic → TP1 (1:1) atins imediat → trail lung (7 candele de 15 min).", + "Valoarea exactă a benzii și a TICUS la candela M nu e dată în sesiune; bara ~+1000 e ilustrativă.", + "„Taurii urcă pe scări, urșii sar de la etaj\" — de ce short-ul se consumă în ore.", + "(09_QA/11_2025-11-25.md:643-648).", +] +cap_top = Y0 + TICUS_H + 20 +H = cap_top + 16 * (len(legend) - 1) + 14 + +parts = [ + f'', + '', + body1, + "", + *ticus, + *[f'{t}' + for i, t in enumerate(legend)], + "", +] +svg = "\n ".join(parts) +assert "\n\n" not in svg, "linie goală în SVG — rupe markdown-ul" +assert svg.count("") == 1, "SVG-ul trebuie un singur root" + +# ── verificare de citire: etichete în panză, fără suprapuneri ─────────────── +TEXT = re.compile(r']*)>(.*?)', re.S) + + +def _fonts(): + """Lățimea reală a textului (DejaVuSans ~ system-ui); fallback estimativ.""" + try: + from PIL import ImageFont + base = "/usr/share/fonts/truetype/dejavu/" + return (ImageFont.truetype(base + "DejaVuSans.ttf", 12), + ImageFont.truetype(base + "DejaVuSans-Bold.ttf", 12)) + except Exception: + return None, None + + +_F_NORM, _F_BOLD = _fonts() + + +def _tw(s: str, bold: bool) -> float: + if _F_BOLD is not None: + return (_F_BOLD if bold else _F_NORM).getlength(s) + return len(s) * 12 * (0.62 if bold else 0.52) + + +labels = [] +for mm in TEXT.finditer(svg): + x, y = float(mm.group(1)), float(mm.group(2)) + attrs, s = mm.group(3), mm.group(4) + anchor = "start" + if 'text-anchor="end"' in attrs: + anchor = "end" + elif 'text-anchor="middle"' in attrs: + anchor = "middle" + w = _tw(s, "bold" in attrs) + left_ = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x) + labels.append((left_, left_ + w, y, s)) + +for left_, right_, y, s in labels: + assert left_ >= -2 and right_ <= W + 2, f"text iese din panză: {s!r}" + assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y:.0f})" + +for i in range(len(labels)): + li, ri, yi, si = labels[i] + for j in range(i + 1, len(labels)): + lj, rj, yj, sj = labels[j] + if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4: + raise AssertionError(f"texte suprapuse: {si!r} / {sj!r} (y={yi:.0f}/{yj:.0f})") + +OUT.write_text(svg, encoding="utf-8") + +print(f"serie {len(cds)} candele | M@{M_I} {cds[M_I].c:.2f} (h {cds[M_I].h:.2f}) | " + f"trigger@{E_I} {entry:.2f} | SL {sl:.2f} ({(sl-entry)/entry*100:.2f} %) | " + f"TP1 {tp1:.2f} (low {cds[TP1_I].l:.2f}) | ieșire@{EXIT_I} {exit_:.2f} " + f"({(entry-exit_)/entry*100:.2f} %, {(entry-exit_)/(sl-entry):.2f}R)") +print(f"scris {OUT} ({W}x{H:.0f}); {len(labels)} etichete verificate, fără suprapuneri") diff --git a/gen/strategii/mbp_exemplu.svg b/gen/strategii/mbp_exemplu.svg new file mode 100644 index 0000000..525a83a --- /dev/null +++ b/gen/strategii/mbp_exemplu.svg @@ -0,0 +1,101 @@ + + + SPY — 15 min: MBAP short 25.11.2025 (risc 0.41 % → ieșire 4.9R) + + SL 662.71 (risc 0.41 %) + + intrare short 660.00 + + bandă superioară (schematic) + + TP1 657.29 (1:1) + + ieșire trail 646.80 = 2 % = ~4.9R + + SMA-20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + M: candelă verde peste bandă + TICUS ≥ +1000 + M: valori exacte NEACOPERIT în material (bandă + TICUS); bara ~+1000 din panou e ilustrativă + trigger Sell Momentum → intrare short 660.00 (risc 0.41 % = 2.71) + TP1 657.29 (1:1) atins în prima candelă după intrare + ieșire pe trail 646.80 = 2 % = ~4.9R, după 7 candele de 15 min de la intrare + + Panou TICUS — linia 0 și pragul +1000 (ilustrativ) + + +1000 (prag) + + 0 + + ilustrativ + candela M + Legendă: procentele (risc 0.41 %, ieșire 2 %) sunt ale instructorului. + Prețurile absolute (660.00 / 662.71 / 657.29 / 646.80) sunt construite de sumarizator — + 09_QA/11_2025-11-25.md:716-719. + Structura de reținut: risc mic → TP1 (1:1) atins imediat → trail lung (7 candele de 15 min). + Valoarea exactă a benzii și a TICUS la candela M nu e dată în sesiune; bara ~+1000 e ilustrativă. + „Taurii urcă pe scări, urșii sar de la etaj" — de ce short-ul se consumă în ore. + (09_QA/11_2025-11-25.md:643-648). + \ No newline at end of file diff --git a/gen/strategii/mbp_samecandle.py b/gen/strategii/mbp_samecandle.py new file mode 100644 index 0000000..712d5c3 --- /dev/null +++ b/gen/strategii/mbp_samecandle.py @@ -0,0 +1,204 @@ +"""Diagrama „aceeași candelă" MBP / MBPA++ (marcajul `samecandle`). + +Două panouri alături, fiecare cu un grafic de 10 candele de 15 min și un panou +TICUS dedesubt, legate printr-o linie punctată verticală de la candela M la +valoarea ei TICUS. Singura diferență dintre panouri: pe care candelă e TICUS +extrem. + + - Panoul DA: M (roșie, index 5) închide 623.87 sub banda inferioară 623.90 + ȘI TICUS(M) = -1150 (≤ -1000) — pe EXACT aceeași candelă. M îndeplinit. + - Panoul NU: aceeași structură de preț, dar TICUS(M) = -600 (peste prag), + iar extremul de -1200 e pe candela m-1, a cărei închidere e peste bandă. + M neîndeplinit, deși vizual „pare" același setup. + +Regula și cifrele vin din `summaries/05_MODUL_4/03_strategia_6_mbp_mbappe.md`: + - regula (bandă și TICUS pe ACEEAȘI candelă) — liniile 179-221, 439-442; + - verificarea prin click pe candelă, nu din ochi — liniile 217-221, 436-438; + - pragurile ±1000, ±500 doar referință vizuală — liniile 56-64; + - M close 623.87 sub banda 623.90 și TICUS(M) -1150 (exemplul real) — + liniile 336-342. +Cifrele din panoul NU (TICUS(M) = -600, TICUS(m-1) = -1200) sunt construite de +mine ca ilustrare a cazului „aproape, dar nu" (spec, rândurile 27-31). +Structura candelelor e construită de mine, ca fiecare nivel afirmat să iasă +exact din serie (assert-urile de mai jos). + +Rulează: + python3 gen/strategii/mbp_samecandle.py +Scrie lângă el `mbp_samecandle.svg`. +""" +import re +import sys +from pathlib import Path + +sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, from_closes # noqa: E402 + +OUT = Path(__file__).resolve().parent / "mbp_samecandle.svg" + +GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000" + +# ---------------------------------------------------------------- seria (10 bare) +# 5 bare de coborâre până la M (index 5), apoi 4 bare care își revin. Open-ul +# fiecărei bare = close-ul precedentei. Aceeași serie în ambele panouri. +CLOSES = [625.35, 625.10, 624.90, 624.65, 624.30, # 0-4 downtrend + 623.87, # 5 = M, roșie, sub bandă + 623.98, 624.12, 624.28, 624.45] # 6-9 revenire +WICKS = [(0.08, 0.08), (0.10, 0.10), (0.08, 0.10), (0.10, 0.10), (0.08, 0.12), + (0.06, 0.10), # M: high 624.36, low 623.77 + (0.06, 0.06), (0.08, 0.06), (0.06, 0.06), (0.08, 0.05)] +assert len(CLOSES) == 10 and len(WICKS) == 10, (len(CLOSES), len(WICKS)) + +BAND = 623.90 +M = 5 + +cds = from_closes(CLOSES, wicks=WICKS, start=625.55) + +# ---------------------------------------------------------------- assert-urile din spec +assert abs(cds[M].c - 623.87) < 0.005, cds[M].c # exemplul real din curs +assert cds[M].c < BAND, cds[M].c # M închide sub banda inferioară +assert cds[M].c < cds[M].o, (cds[M].o, cds[M].c) # M e roșie +assert abs(cds[M].l - 623.77) < 0.005, cds[M].l +assert cds[M].l == min(c.l for c in cds) # M are minimul seriei +assert cds[M - 1].c > BAND, cds[M - 1].c # candela m-1 închide PESTE bandă +assert [round(c.c, 2) for c in cds] == CLOSES # close-urile sunt respectate + +# TICUS: valoarea pe candelă. DA: doar M, sub prag. NU: extremul pe m-1. +TICUS_DA = {M: -1150.0} +TICUS_NU = {M - 1: -1200.0, M: -600.0} + +assert TICUS_DA[M] <= -1000, TICUS_DA[M] # DA: ticus(M) under -1000 +assert TICUS_NU[M] > -1000, TICUS_NU[M] # NU: ticus(M) peste prag +assert TICUS_NU[M - 1] <= -1000, TICUS_NU[M - 1] # NU: extremul e pe m-1 +assert not any(i == M and v <= -1000 for i, v in TICUS_NU.items()), \ + "NU: nu are voie să existe bar TICUS sub -1000 aliniat cu candela M" + +# ---------------------------------------------------------------- construcția panourilor +SCALE, GAP, BODY, LEFT, W = 85.0, 16, 10, 44, 380 + + +def _ref_line(Wv, y, color, dash, text, lab_dy): + dash_attr = f' stroke-dasharray="{dash}"' if dash else "" + return (f'' + f'{text}') + + +def build_panel(ticus, labels): + """Un panou = grafic de preț (svg_candles) + panou TICUS dedesubt.""" + ch = Chart(candles=cds, + levels=[Level(BAND, "bandă 623.90", BLUE, dash="4 3", label_at="right")], + scale=SCALE, gap=GAP, body_w=BODY, left=LEFT, width=W) + raw = ch.render() + mm = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', raw) + Wv, H0 = int(mm.group(1)), float(mm.group(2)) + inner = raw[raw.index(">") + 1: raw.rindex("")].strip("\n") + + ticus_top = H0 + 30 + y0 = H0 + 100 # linia 0 a TICUS + PX = 44.0 # 1000 unități TICUS = 44 px + bar_label_x = ch._cx(M) + 16 # etichetele barelor stau în coloană, la dreapta + + def ty(v): + return y0 - v / 1000.0 * PX + + total_h = y0 + 96 + parts = [] + + # linia punctată verticală: aceeași candelă sus și jos (desenată prima, ca + # etichetele să vină peste ea) + for i, v in sorted(ticus.items()): + xb = ch._cx(i) + assert -1 <= xb <= Wv + 1, xb + parts.append(f'') + + parts.append(f'TICUS') + # praguri: 0 (linie plină), ±1000 (punctate), ±500 (fine, doar referință) + parts.append(_ref_line(Wv, ty(1000), RED, "4 3", "+1000 (prag)", -5)) + parts.append(_ref_line(Wv, ty(500), "#caa000", "2 2", "+500", -5)) + parts.append(_ref_line(Wv, ty(0), "#888", "", "0", 4)) + parts.append(_ref_line(Wv, ty(-500), "#caa000", "2 2", "-500", 13)) + parts.append(_ref_line(Wv, ty(-1000), GREEN, "4 3", "-1000 (prag)", 13)) + + # barele TICUS (centrate pe x-ul candelei) + for i, v in sorted(ticus.items()): + xb = ch._cx(i) + yv = ty(v) + col = RED if v <= -1000 else ORANGE + parts.append(f'') + + # eticheta de valoare lângă fiecare bar (aceeași coloană x, rânduri diferite) + for i, (txt, dy) in sorted(labels.items()): + col = RED if ticus[i] <= -1000 else ORANGE + parts.append(f'{txt}') + + # adnotarea candelei M, în zona de preț, sub bară (zona e liberă) + parts.append(f'' + f'M: close 623.87 < bandă 623.90') + + # alinierea TICUS(M) cu candela M: barul se centrează exact pe x-ul candelei + xm = ch._cx(M) + x_bar_m = ch._cx(M) # centrul barei TICUS a lui M + assert abs(x_bar_m - xm) < 1e-9 + assert ticus[M] <= -1000 if ticus is TICUS_DA else ticus[M] > -1000 + + body = inner + "\n " + "\n ".join(parts) + # bara TICUS a lui M chiar a fost scrisă cu centrul pe x-ul candelei M + assert f' aceleași x-uri ale candelelor în ambele panouri +assert all(abs(ch_da._cx(i) - ch_nu._cx(i)) < 1e-9 for i in range(len(cds))) + +# ---------------------------------------------------------------- compunerea SVG-ului +TOP = 48 +FOOTER = [ + "Regula: banda (close sub 623.90) și TICUS ≤ -1000 trebuie pe ACEEAȘI candelă.", + "Pragurile TICUS sunt ±1000 (overbought / oversold); ±500 sunt doar referință vizuală.", + "Verificarea se face prin click pe candelă, nu din ochi — TICUS(M) se citește exact pe candela M.", + "DA: TICUS(M) = -1150 (exemplul real din curs).", + "NU: TICUS(M) = -600, TICUS(m-1) = -1200 — cifre construite de mine ca ilustrare.", + "Structura candelelor (10 bare de 15 min, aceeași în ambele panouri) e construită de mine.", +] +root_h = TOP + PH + 22 + 17 * len(FOOTER) + 8 + +out = [f'', + f'{inner_da}', + f'{inner_nu}', + f'DA — aceeași candelă, M îndeplinit', + f'NU — TICUS extrem pe altă candelă,', + f'M neîndeplinit'] +fy = TOP + PH + 22 +for i, line in enumerate(FOOTER): + out.append(f'{line}') +out.append("") +svg = "\n ".join(out) + +# ---------------------------------------------------------------- verificări pe SVG-ul final +assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown" +assert "DA — aceeași candelă, M îndeplinit" in svg +assert "NU — TICUS extrem pe altă candelă," in svg +assert "M neîndeplinit" in svg +assert "click pe candelă, nu din ochi" in svg +assert svg.count("") == 3, "cele două panouri + rădăcina" + +OUT.write_text(svg) +print(f"scris {OUT.name}: {len(cds)} candele/panou, M close {cds[M].c:.2f} " + f"(< {BAND}), TICUS DA {TICUS_DA[M]:.0f} / NU {TICUS_NU[M]:.0f} " + f"(extrem {TICUS_NU[M - 1]:.0f} pe m-1), x(M)={ch_da._cx(M):.0f}, " + f"înălțime {root_h:.0f}px") diff --git a/gen/strategii/mbp_samecandle.svg b/gen/strategii/mbp_samecandle.svg new file mode 100644 index 0000000..fa27641 --- /dev/null +++ b/gen/strategii/mbp_samecandle.svg @@ -0,0 +1,78 @@ + + + bandă 623.90 + + + + + + + + + + + + + + + + + + + + + + TICUS + +1000 (prag) + +500 + 0 + -500 + -1000 (prag) + + M: TICUS -1150 (sub -1000) + M: close 623.87 < bandă 623.90 + + bandă 623.90 + + + + + + + + + + + + + + + + + + + + + + + TICUS + +1000 (prag) + +500 + 0 + -500 + -1000 (prag) + + + TICUS(m-1) = -1200 (extrem) + TICUS(M) = -600 (nu atinge -1000) + M: close 623.87 < bandă 623.90 + DA — aceeași candelă, M îndeplinit + NU — TICUS extrem pe altă candelă, + M neîndeplinit + Regula: banda (close sub 623.90) și TICUS ≤ -1000 trebuie pe ACEEAȘI candelă. + Pragurile TICUS sunt ±1000 (overbought / oversold); ±500 sunt doar referință vizuală. + Verificarea se face prin click pe candelă, nu din ochi — TICUS(M) se citește exact pe candela M. + DA: TICUS(M) = -1150 (exemplul real din curs). + NU: TICUS(M) = -600, TICUS(m-1) = -1200 — cifre construite de mine ca ilustrare. + Structura candelelor (10 bare de 15 min, aceeași în ambele panouri) e construită de mine. + \ No newline at end of file diff --git a/gen/strategii/mdsrt_ansamblu.py b/gen/strategii/mdsrt_ansamblu.py new file mode 100644 index 0000000..4114e65 --- /dev/null +++ b/gen/strategii/mdsrt_ansamblu.py @@ -0,0 +1,166 @@ +"""Diagrama de ansamblu MDSRT long (strategii, marcajul `ansamblu`). + +„SPY — 5 min: al doilea picior mai jos la 589.80, sub suportul 590.00 → +divergență bull (M) preț-vs-volum → candela-trigger Buy Hammer/Doji → +intrare 590.20 → SL 589.40 (risc 0.80) → TP1 591.00 (1:1) → poziția rămasă +pe trail, ieșire la close 592.40 (≈ 2.75R)." + +Toate close-urile, wick-urile speciale și nivelurile vin din +`gen/strategii/mdsrt_ansamblu.spec.txt`. Structura M→A→P, suportul 590.00, +minimul 589.80 și riscul 0.80 sunt din exemplul cursului +(`05_MODUL_4/02_strategia_5_mdsrt.md:159-177, 246, 308-311`). Intrare/SL/TP +sunt exemplul construit al autorului sumarului, marcat explicit ca NU citat +din curs (`...:166-177, 439-445`); transcrierea se întrerupe înainte de +planul numeric. Volumele sunt relative, alese de generator (materialul nu dă +volum pentru acest exemplu). + +Rulează: + python3 gen/strategii/mdsrt_ansamblu.py +Scrie lângă el `mdsrt_ansamblu.svg`. +""" +import re +import sys +from pathlib import Path + +sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402 + +OUT = Path(__file__).resolve().parent / "mdsrt_ansamblu.svg" + +GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000" + +# ---------------------------------------------------------------- seria (30 bare) +CLOSES = [ + 596.00, 595.20, 595.60, 594.80, 594.10, 593.40, 593.70, 592.90, # 0-7 + 592.20, 591.60, 591.90, 591.30, 590.80, 590.30, 590.55, 590.10, # 8-15 + 589.85, 590.05, # 16-17 + 589.80, # 18 = al doilea picior mai jos + candela-trigger (hammer) + 590.20, # 19 = intrare (close = entry; sparge high-ul trigger-ului) + 590.60, 590.90, 591.00, # 20-22 (22 = TP1 1:1) + 591.30, 591.10, 591.45, 591.70, 591.95, 592.15, # 23-28 + 592.40, # 29 = ieșire pe trail (close) +] +assert len(CLOSES) == 30, len(CLOSES) + +# fitile (sus, jos): (0.25, 0.30) peste tot, cu excepțiile din spec. +WICKS = [(0.25, 0.30)] * 30 +WICKS[18] = (0.10, 0.40) # hammer: high 590.15, low 589.40 (= SL) +WICKS[19] = (0.10, 0.05) # intrare: high 590.30, low 589.75 +# bara 21 coborâtă sub TP1 (high 590.95 < 591.00), ca TP1 să fie atins exact +# prima dată la bara 22 (altfel assert-ul 5 ar pica pe „primul high"). +WICKS[21] = (0.05, 0.30) + +# volum relativ (ales de generator): 16-19 crescător, 18-19 peste medie, +# vol[18] > vol[10] (higher low pe volum la lower low pe preț). +VOLS = [ + 48, 46, 44, 45, 43, 41, 40, 42, 44, 46, # 0-9 + 52, 38, 40, 42, 39, 44, # 10-15 + 50, 58, 72, 85, # 16-19 absorbție la minim + 70, 64, 58, 60, 55, 52, 50, 48, 46, 44, # 20-29 +] +assert len(VOLS) == 30, len(VOLS) + +cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=596.60) + +# ---------------------------------------------------------------- assert-urile din spec +entry = cds[19].c +sl = cds[18].l +tp1 = cds[22].c + +# 1. minimul și SL-ul pe bara 18 +assert cds[18].c == 589.80, cds[18].c +assert cds[18].l == 589.40, cds[18].l +# 2. al doilea picior rupe suportul/pivotul 590.00 +assert cds[18].l < 590.00, cds[18].l +# 3. intrarea = close-ul barei 19 și sparge high-ul trigger-ului +assert cds[19].c == 590.20, cds[19].c +assert cds[19].c == entry +assert cds[19].c > cds[18].h, (cds[19].c, cds[18].h) +# 4. TP1 exact 1:1 +assert abs((entry - sl) - (tp1 - entry)) < 1e-9, (entry - sl, tp1 - entry) +assert abs((entry - sl) - 0.80) < 1e-9 +# 5. TP1 atins la bara 22 (și nu mai devreme) +assert cds[22].c == 591.00, cds[22].c +assert cds[22].c == tp1 +assert max(c.h for c in cds[20:22]) < tp1, [c.h for c in cds[20:22]] +# 6. ieșirea pe trail e pe profit +assert cds[29].c == 592.40, cds[29].c +assert cds[29].c > entry, (cds[29].c, entry) +# 7. hammer: corp mic, coadă lungă în jos +body = abs(cds[18].o - cds[18].c) +rng = cds[18].h - cds[18].l +tail = min(cds[18].o, cds[18].c) - cds[18].l +assert body == 0.25, body +assert body <= 0.5 * rng, (body, rng) +assert tail > body, (tail, body) +# 8. downtrend vizibil +assert cds[13].c < cds[0].c - 5, (cds[13].c, cds[0].c) +# 9. divergență pe volum: lower low pe preț, higher low pe volum +assert cds[18].l < cds[10].l, (cds[18].l, cds[10].l) +assert VOLS[18] > VOLS[10], (VOLS[18], VOLS[10]) +# barele 16-19 au volum crescător; 18-19 peste media seriei +assert VOLS[16] < VOLS[17] < VOLS[18] < VOLS[19], VOLS[16:20] +avg_vol = sum(VOLS) / len(VOLS) +assert VOLS[18] > avg_vol and VOLS[19] > avg_vol, (VOLS[18], VOLS[19], avg_vol) +# 10. fiecare open == close-ul barei anterioare +for a, b in zip(cds, cds[1:]): + assert a.c == b.o, (a.c, b.o) + +r_mult = (cds[29].c - entry) / (entry - sl) +assert abs(r_mult - 2.75) < 1e-9, r_mult + +# ---------------------------------------------------------------- niveluri, marks +levels = [ + Level(590.00, "suport / pivot 590.00", BLUE, label_at="left"), + Level(sl, f"SL {sl:.2f} — risc 0.80", RED, label_at="left"), + # stânga: altfel eticheta din dreapta se calcă pe textul „SMA-20" al mediei + Level(tp1, f"TP1 {tp1:.2f} = 1:1", GREEN, label_at="left"), +] + +marks = [ + Mark(18, f"minim 589.80 — divergență (M)", RED, "below"), + # ridicat deasupra vârfurilor candelelor de urcare (altfel textul cade peste + # fitilele barelor 20-22) — bara 19 e totuși exact sub el + Mark(19, f"intrare {entry:.2f}", GREEN, "above", dy=-46), + Mark(29, f"ieșire trail {cds[29].c:.2f}", GREEN, "above"), +] + +# titlul e injectat pe două linii: textul din spec (82 caractere) nu încape +# pe un singur rând în 640 px fără să fie tăiat la margine +TITLE = ["SPY — 5 min (exemplu construit pe scara lecției;", + "cifrele planului nu vin din curs)"] + +ch = Chart( + candles=cds, levels=levels, marks=marks, + mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")], + scale=48.0, gap=15, body_w=9, left=44, width=640, top=64, vol_h=70, + vol_label="volum", + title="", caption="", +) +svg = ch.render() +title_svg = "\n ".join( + f'{t}' + for i, t in enumerate(TITLE)) +i = svg.index(">") + 1 +svg = svg[:i] + "\n " + title_svg + svg[i:] + +# ---------------------------------------------------------------- legenda (onestitate) +m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg) +W, H0 = int(m.group(1)), float(m.group(2)) +legend = [ + "Legendă: entry/SL/TP/trail sunt exemplul construit al autorului sumarului,", + "nu citate din curs (transcrierea se întrerupe înainte de planul numeric).", +] +legend_svg = [f'{t}' + for i, t in enumerate(legend)] +new_h = H0 + 16 + 17 * (len(legend) - 1) + 14 +svg = re.sub(r'(viewBox="0 0 \d+ )[0-9.]+(")', + lambda mm: mm.group(1) + f"{new_h:.0f}" + mm.group(2), svg, count=1) +svg = svg.replace("\n", "\n " + "\n ".join(legend_svg) + "\n") + +assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown" +assert f"TP1 {tp1:.2f}" in svg and f"intrare {entry:.2f}" in svg +OUT.write_text(svg) +print(f"scris {OUT.name}: {len(cds)} candele, suport 590.00, minim {cds[18].l:.2f}, " + f"intrare {entry:.2f}, SL {sl:.2f} (risc 0.80), TP1 {tp1:.2f} (1:1), " + f"ieșire {cds[29].c:.2f} = {r_mult:.2f}R, vol[18]={VOLS[18]} > vol[10]={VOLS[10]}") diff --git a/gen/strategii/mdsrt_ansamblu.svg b/gen/strategii/mdsrt_ansamblu.svg new file mode 100644 index 0000000..0718c56 --- /dev/null +++ b/gen/strategii/mdsrt_ansamblu.svg @@ -0,0 +1,109 @@ + + SPY — 5 min (exemplu construit pe scara lecției; + cifrele planului nu vin din curs) + + suport / pivot 590.00 + + SL 589.40 — risc 0.80 + + TP1 591.00 = 1:1 + + SMA-20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + minim 589.80 — divergență (M) + intrare 590.20 + ieșire trail 592.40 + Legendă: entry/SL/TP/trail sunt exemplul construit al autorului sumarului, + nu citate din curs (transcrierea se întrerupe înainte de planul numeric). + \ No newline at end of file diff --git a/gen/strategii/mdsrt_exemplu.py b/gen/strategii/mdsrt_exemplu.py new file mode 100644 index 0000000..8423bf2 --- /dev/null +++ b/gen/strategii/mdsrt_exemplu.py @@ -0,0 +1,222 @@ +"""Diagrama din `gen/strategii/mdsrt_exemplu.spec.txt` (TIP: PRET). + +Exemplul real din Q&A 15.07.2025 (`09_QA/24_2025-07-15.md`): SPY pe 5 min, +double bottom cu al doilea picior mai jos (MDSRT pe 5 min, suportul = pivotul +daily). Downtrend → picior 1 la 622.40 → revenire → picior 2 la 621.40 (rupe +pivotul 622.00) → Buy Hammer care închide la 622.60, înapoi peste pivot. După +trigger, un plan construit de mine (marcat) duce trade-ul până la TP1. + +Reale din curs: nivelurile 622.40 / 622.00 / 621.40 / 622.60 și cele 8 bare +dintre picioare. Construcția mea: entry 622.60, SL 621.30, TP1 623.90 (1:1) — +materialul nu le dă (transcrierea planului se întrerupe). Volumele sunt +relative, alese de generator. + +Rulează: + python3 gen/strategii/mdsrt_exemplu.py +Scrie lângă el `mdsrt_exemplu.svg`. +""" +import re +import sys +from pathlib import Path + +sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402 + +OUT = Path(__file__).resolve().parent / "mdsrt_exemplu.svg" + +GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000" + +# ── nivelurile reale din curs + planul construit (marcat) ────────────────── +PIVOT = 622.00 # pivot daily, suportul +LEG1 = 622.40 # swing low picior 1 +LEG2 = 621.40 # picior 2 (mai jos, rupe pivotul) +ENTRY = 622.60 # close-ul Buy Hammer (construit) +SL = 621.30 # 0.10 sub low-ul piciorului 2 (construit) +TP1 = 623.90 # entry + risc = 1:1 (construit) + +# ── seria: 30 de close-uri, exact ca în specificație ─────────────────────── +CLOSES = [ + 627.00, 626.40, 626.70, 626.00, 625.40, 624.80, 624.40, 623.80, # 0-7 + 623.20, 622.80, # 8-9 + 622.40, # 10 = picior 1 (swing low 622.40) + 622.70, 623.10, 623.50, 623.20, 622.90, 622.60, 622.30, # 11-17 + 622.60, # 18 = picior 2: low 621.40, Buy Hammer care închide 622.60 + 623.00, 623.40, 623.90, # 19-21, 21 = TP1 1:1 (construit) + 624.20, 624.00, 624.40, 624.80, 625.20, 625.60, # 22-27 + 625.30, 625.70, # 28-29 +] +assert len(CLOSES) == 30, len(CLOSES) + +# wick-uri (sus, jos): alternanță naturală; excepțiile sunt piciorul 1 și +# hammer-ul de la piciorul 2 (cerute exact de specificație). +WICKS = [ + (0.10, 0.10), (0.12, 0.14), (0.15, 0.12), (0.18, 0.15), (0.20, 0.16), + (0.18, 0.14), (0.16, 0.13), (0.15, 0.12), (0.14, 0.12), (0.12, 0.10), + (0.15, 0.05), # 10 = picior 1: low 622.40 - 0.05 = 622.35 + (0.12, 0.12), (0.15, 0.14), (0.18, 0.15), (0.16, 0.13), (0.14, 0.12), + (0.12, 0.10), (0.10, 0.12), + (0.10, 0.90), # 18 = hammer: high 622.70, low 621.40 + (0.12, 0.12), (0.14, 0.12), (0.15, 0.13), + (0.16, 0.15), (0.14, 0.12), (0.15, 0.13), (0.16, 0.14), (0.17, 0.14), + (0.18, 0.15), (0.14, 0.12), (0.15, 0.13), +] +assert len(WICKS) == 30, len(WICKS) + +# volum relativ (ales de generator; materialul nu dă volumul pentru exemplu): +# scade pe downtrend, spike de absorbție pe hammer, higher low vs piciorul 1. +VOLS = [ + 58, 56, 54, 52, 50, 49, 48, 47, 46, 45, # 0-9 + 44, # 10 = picior 1 + 43, 42, 41, 40, 39, 38, 39, # 11-17 + 100, # 18 = hammer, maximul seriei + 62, 58, 54, # 19-21 + 52, 50, 49, 48, 47, 46, 45, 44, # 22-29 +] +assert len(VOLS) == 30, len(VOLS) + +cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=627.00) + +# ── assert-urile din specificație ────────────────────────────────────────── +# 1. picior 1: low 622.35, close 622.40 +assert abs(cds[10].l - 622.35) < 1e-9, cds[10].l +assert cds[10].c == 622.40, cds[10].c + +# 2. picior 2 mai jos (621.40 < 622.35) și rupe pivotul daily 622.00 +assert abs(cds[18].l - LEG2) < 1e-9, cds[18].l +assert cds[18].l < cds[10].l, (cds[18].l, cds[10].l) +assert cds[18].l < PIVOT, cds[18].l + +# 3. trigger-ul închide înapoi peste pivot +assert cds[18].c == 622.60, cds[18].c +assert cds[18].c > PIVOT, cds[18].c + +# 4. 8 candele între picioare (peste minimul de 5) +assert 18 - 10 == 8 + +# 5. hammer: coadă lungă în jos (0.90) > corp (0.30) +corp = abs(cds[18].o - cds[18].c) +coada = min(cds[18].o, cds[18].c) - cds[18].l +assert abs(corp - 0.30) < 1e-9, corp +assert abs(coada - 0.90) < 1e-9, coada +assert coada > corp, (coada, corp) + +# 6. entry = close-ul trigger-ului (construit), SL 0.10 sub low-ul piciorului 2 +assert cds[18].c == ENTRY +assert abs(cds[18].l - SL - 0.10) < 1e-9, (cds[18].l, SL) +risk = ENTRY - SL +assert abs(risk - 1.30) < 1e-9, risk + +# 7. TP1 simetric 1:1 +assert abs((ENTRY - SL) - (TP1 - ENTRY)) < 1e-9 + +# 8. după trigger, primul close care atinge 623.90 e bara 21 (nu mai devreme). +# Seria pornește la 627.00, deci „atinge" se citește de jos în sus, din zona +# picioarelor: barele 10-20 rămân sub TP1, bara 21 îl atinge. +assert all(c.c < TP1 for c in cds[10:21]), [c.c for c in cds[10:21]] +assert max(c.h for c in cds[19:21]) < TP1, [c.h for c in cds[19:21]] +assert cds[21].c == TP1, cds[21].c + +# 9. divergență: lower low pe preț, higher low pe volum +assert cds[18].l < cds[10].l, (cds[18].l, cds[10].l) +assert VOLS[18] > VOLS[10], (VOLS[18], VOLS[10]) +assert VOLS[18] == max(VOLS) + +# 10. fiecare open == close-ul barei anterioare +for a, b in zip(cds, cds[1:]): + assert a.c == b.o, (a.c, b.o) + +# ── geometria, ca să așez marks-urile fără suprapunere de text ───────────── +SCALE, GAP, LEFT, WIDTH, BODY_W, VOL_H = 62, 15, 44, 640, 9, 70 + + +def cx(i: int) -> float: + return LEFT + i * GAP + + +levels = [ + Level(PIVOT, "pivot daily 622.00", ORANGE, label_at="left"), + Level(SL, "SL 621.30 (construit)", RED, label_at="left"), + Level(ENTRY, "intrare 622.60 (construit)", GREEN, label_at="left"), + Level(TP1, "TP1 623.90 = 1:1 (construit)", GREEN, label_at="left"), + Level(LEG1, "picior 1 = 622.40", BLUE, label_at="right"), +] + +# dy-uri calculate manual ca etichetele „below" să nu calce linia pivotului +# (bara 10) și ca „hammer" să nu calce eticheta intrării (bara 18). +marks = [ + Mark(10, "picior 1 = 622.40", BLUE, "below", dy=15), + Mark(18, "picior 2 = 621.40 (mai jos) · Buy Hammer", RED, "below"), + Mark(18, "hammer închide 622.60, înapoi peste pivot", GREEN, "above", dy=-20), +] + +ch = Chart( + candles=cds, levels=levels, marks=marks, + mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")], + scale=SCALE, gap=GAP, body_w=BODY_W, left=LEFT, width=WIDTH, vol_h=VOL_H, + vol_label="volum", + title="SPY — 5 min (Q&A 15.07.2025; intrare/SL/TP construite)", + caption="", +) +svg = ch.render() + +# ── legenda de onestitate (assert 11): ce e real, ce e construit ─────────── +legend = [ + "Legendă: nivelurile 622.40 / 622.00 / 621.40 / 622.60 și cele 8 candele dintre", + "picioare sunt reale, din curs. Intrarea 622.60, SL-ul 621.30 și TP-ul 623.90", + "sunt construcția mea — materialul nu le dă.", +] +m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg) +W, H0 = int(m.group(1)), float(m.group(2)) +legend_svg = [f'{t}' + for i, t in enumerate(legend)] +new_h = H0 + 16 + 17 * (len(legend) - 1) + 14 +svg = re.sub(r'(viewBox="0 0 \d+ )[0-9.]+(")', + lambda mm: mm.group(1) + f"{new_h:.0f}" + mm.group(2), svg, count=1) +svg = svg.replace("\n", "\n " + "\n ".join(legend_svg) + "\n") + +assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown" +assert "sunt reale, din curs" in svg and "construcția mea" in svg, \ + "legenda nu spune explicit ce e real și ce e construit" + +# toate etichetele cerute de specificație sunt prezente +for eticheta in ("pivot daily 622.00", "picior 1 = 622.40", + "picior 2 = 621.40 (mai jos) · Buy Hammer", + "hammer închide 622.60, înapoi peste pivot", + "intrare 622.60 (construit)", "SL 621.30 (construit)", + "TP1 623.90 = 1:1 (construit)"): + assert eticheta in svg, f"lipsește eticheta: {eticheta!r}" +assert "SPY — 5 min" in svg, "lipsește titlul" + +# 1:1 desenat la 1:1 (STYLE.md: raportul din preț = raportul din pixeli) +ys = {p: ch._y(p) for p in (SL, ENTRY, TP1)} +assert abs((ys[SL] - ys[ENTRY]) - (ys[ENTRY] - ys[TP1])) < 1e-6, ys + +# ── verificare: fiecare etichetă e în pânză și nu se suprapune alta ──────── +m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg) +W, H = int(m.group(1)), float(m.group(2)) +texte = re.findall(r']*)>([^<]*)', svg) +cutii = [] +for xs, ys, attrs, continut in texte: + x, yv = float(xs), float(ys) + w = len(continut) * (7.44 if "bold" in attrs else 6.72) + if 'text-anchor="end"' in attrs: + x0 = x - w + elif 'text-anchor="middle"' in attrs: + x0 = x - w / 2 + else: + x0 = x + assert -2 <= x0 and x0 + w <= W + 2, f"etichetă în afara pânzei: {continut!r}" + assert 0 <= yv <= H, f"etichetă în afara înălțimii: {continut!r}" + cutii.append((x0, x0 + w, yv, continut)) +for i in range(len(cutii)): + for j in range(i + 1, len(cutii)): + a, b = cutii[i], cutii[j] + if abs(a[2] - b[2]) < 13 and a[0] < b[1] - 2 and b[0] < a[1] - 2: + raise AssertionError(f"texte suprapuse: {a[3]!r} / {b[3]!r}") + +OUT.write_text(svg, encoding="utf-8") +print(f"scris {OUT.name}: {len(cds)} candele | picior 1 {cds[10].l:.2f} (close " + f"{cds[10].c:.2f}) | picior 2 {cds[18].l:.2f} (mai jos) | hammer închide " + f"{cds[18].c:.2f} | intrare {ENTRY:.2f} | SL {SL:.2f} (risc {risk:.2f}) | " + f"TP1 {TP1:.2f} = {(TP1 - ENTRY) / risk:.2f}:1 la bara 21 | " + f"vol[18]={VOLS[18]} > vol[10]={VOLS[10]}") diff --git a/gen/strategii/mdsrt_exemplu.svg b/gen/strategii/mdsrt_exemplu.svg new file mode 100644 index 0000000..5e14419 --- /dev/null +++ b/gen/strategii/mdsrt_exemplu.svg @@ -0,0 +1,113 @@ + + SPY — 5 min (Q&A 15.07.2025; intrare/SL/TP construite) + + pivot daily 622.00 + + SL 621.30 (construit) + + intrare 622.60 (construit) + + TP1 623.90 = 1:1 (construit) + + picior 1 = 622.40 + + SMA-20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + picior 1 = 622.40 + picior 2 = 621.40 (mai jos) · Buy Hammer + hammer închide 622.60, înapoi peste pivot + Legendă: nivelurile 622.40 / 622.00 / 621.40 / 622.60 și cele 8 candele dintre + picioare sunt reale, din curs. Intrarea 622.60, SL-ul 621.30 și TP-ul 623.90 + sunt construcția mea — materialul nu le dă. + \ No newline at end of file diff --git a/gen/strategii/mdsrt_masurare.py b/gen/strategii/mdsrt_masurare.py new file mode 100644 index 0000000..5e856c3 --- /dev/null +++ b/gen/strategii/mdsrt_masurare.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +"""Cum se masoara divergenta MDSRT — schema pe doua panouri (pret + indicator). + +Pentru `strategii/07_mdsrt.md`, marcajul `` (Nivel 1, +§1.2 „Setup", dupa pasul 5). NU e grafic de pret cu candele: spec-ul cere o +schema de regula, cu linii, fara axe de pret reale, fara ticker, fara cifre de +pret. De aceea se scrie SVG de mana, doar stdlib — nu se folosesc +svg_candles / pchart. + +Ce arata (toate etichetele si pragurile vin din spec / din Q&A): +- panoul de sus: linia pretului intr-un downtrend; L1 primul swing low + important, L2 al doilea, mai jos → lower low; +- panoul de jos: linia indicatorului (Power / volum / RS) cu I1 si I2, al + doilea mai sus → higher low = divergenta bull; +- bracket orizontal intre L1 si L2: „minim 5 candele intre puncte"; +- bracket orizontal in stanga lui L2, pana la 60 de pasi in spate: + „maxim 60 de candele in spate"; +- al treilea swing low mic, la 2-3 pasi de L2, taiat cu cruce rosie si + etichetat „2-3 candele → NU e divergenta". + +Pragurile, cu sursa: +- „ultimele 2 swing low-uri importante" + „60 de candele in spate si minim 5 + candele intre divergente": `09_QA/24_2025-07-15.md:151-155`; +- „Daca aveti pe 3 candele divergenta, nu e": `09_QA/23_2025-07-29.md:873-874`; +- mecanismul pret-vs-volum (lower low pe pret, higher low pe indicator): + `09_QA/19_2025-09-08.md:168-177`; +- perechea de low-uri se muta pe masura ce apare un low nou, mai jos: + `09_QA/24_2025-07-15.md:159-162`. + +Ruleaza: .venv/bin/python gen/strategii/mdsrt_masurare.py + -> gen/strategii/mdsrt_masurare.svg +""" +from pathlib import Path +from xml.sax.saxutils import escape + +OUT = Path(__file__).with_suffix(".svg") + +W, H = 700, 620 +BLACK, GRAY, RED, GREEN, BLUE, ORANGE, WHITE = ( + "#000000", "#777777", "#c00", "#188a3e", "#1a5fb4", "#b06000", "#ffffff") + +# ---- grila de timp: un pas = o candela. L2 e la 60 de pasi de la marginea +# stanga a panoului, ca bracket-ul „maxim 60 de candele" sa aiba exact 60 de +# pasi spre stanga si sa acopere si L1. +X0, STEPW = 60.0, 7.0 +L1_STEP, L2_STEP, L3_STEP = 54.0, 60.0, 62.5 +X_L1, X_L2, X_L3 = X0 + L1_STEP * STEPW, X0 + L2_STEP * STEPW, X0 + L3_STEP * STEPW + +# ---- coordonatele verticale ale punctelor +Y_L1, Y_L2, Y_L3 = 252.0, 278.0, 272.0 # pret: L2 mai jos (lower low) +Y_I1, Y_I2 = 486.0, 468.0 # indicator: I2 mai sus (higher low) + +PRICE_BOX = (40.0, 76.0, 620.0, 250.0) # y 76 .. 326 +IND_BOX = (40.0, 344.0, 620.0, 200.0) # y 344 .. 544 + +BR60_Y = 134.0 # bracket-ul de 60 de candele, sus +BR5_Y = 300.0 # bracket-ul de 5 candele, sub cele doua low-uri + +TEXTS = [] # (x, y, text, size, weight, anchor) pentru verificarea finala + + +def txt(x, y, s, size=13, weight="400", fill=BLACK, anchor="middle"): + TEXTS.append((x, y, s, size, weight, anchor)) + return (f'' + f'{escape(s)}') + + +def line(x1, y1, x2, y2, stroke=BLACK, width=1.4, dash=None): + d = f' stroke-dasharray="{dash}"' if dash else "" + return (f'') + + +def poly(points, stroke=BLACK, width=1.8): + pts = " ".join(f"{x:.1f},{y:.1f}" for x, y in points) + return (f'') + + +def dot(x, y, color=BLACK, r=3.6): + return f'' + + +def hbracket(x0, x1, y, color, tick=7.0): + """Bracket orizontal de dimensiune: linie + capete verticale.""" + return [ + line(x0, y, x1, y, color, 1.4), + line(x0, y - tick, x0, y + tick, color, 1.4), + line(x1, y - tick, x1, y + tick, color, 1.4), + ] + + +def cross(cx, cy, color=RED, arm=10.0): + """Crucea rosie care marcheaza al treilea low (NU e divergenta).""" + return [ + line(cx - arm, cy - arm, cx + arm, cy + arm, color, 2.0), + line(cx - arm, cy + arm, cx + arm, cy - arm, color, 2.0), + ] + + +def build(): + p = [ + f'', + '', + f'', + txt(W / 2, 34, "Divergența: cum o măsori corect", 19, "700", BLACK), + txt(W / 2, 58, "Aceeași structură pe preț și pe indicator: două swing " + "low-uri, două minime", 13, "400", GRAY), + ] + + # ---------------- panoul de sus: pretul + bx, by, bw, bh = PRICE_BOX + p.append(f'') + p.append(txt(bx + 16, by + 20, "PREȚ (schematic) — downtrend, lower low", + 14, "700", BLACK, "start")) + + # bracket „maxim 60 de candele in spate": de la L2 spre stanga, 60 de pasi + x60_left, x60_right = X0, X_L2 + p.extend(hbracket(x60_left, x60_right, BR60_Y, BLACK)) + p.append(txt((x60_left + x60_right) / 2, BR60_Y - 12, + "maxim 60 de candele în spate", 13, "400", BLACK)) + + # linia pretului: downtrend cu L1, apoi L2 mai jos, apoi un low mic L3 + price = [(0, 158), (6, 178), (12, 166), (18, 192), (24, 180), (30, 206), + (36, 194), (42, 218), (48, 232), (L1_STEP, Y_L1), (57, 226), + (L2_STEP, Y_L2), (61.3, 248), (L3_STEP, Y_L3), (65, 250)] + p.append(poly([(X0 + s * STEPW, y) for s, y in price], BLACK, 1.8)) + p.extend([dot(X_L1, Y_L1, BLACK), dot(X_L2, Y_L2, BLACK), dot(X_L3, Y_L3, BLACK)]) + p.append(txt(X_L1 - 10, Y_L1 + 6, "L1", 13, "700", BLACK, "end")) + p.append(txt(X_L2 - 12, Y_L2 + 8, "L2", 13, "700", BLACK, "end")) + + # bracket „minim 5 candele intre puncte": intre L1 si L2 + p.extend(hbracket(X_L1, X_L2, BR5_Y, BLACK)) + p.append(txt((X_L1 + X_L2) / 2, BR5_Y + 18, + "minim 5 candele între puncte", 13, "400", BLACK)) + + # al treilea low: cruce rosie + „NU" + eticheta + p.extend(cross(X_L3, Y_L3, RED, arm=10.0)) + p.append(txt(X_L3, 234, "NU", 14, "700", RED)) + p.append(txt(525, 246, "2-3 candele", 13, "700", RED, "start")) + p.append(txt(525, 266, "NU e divergență", 13, "700", RED, "start")) + + # ---------------- panoul de jos: indicatorul + bx, by, bw, bh = IND_BOX + p.append(f'') + p.append(txt(bx + 16, by + 20, + "INDICATOR (Power / volum / RS) — higher low", + 14, "700", BLUE, "start")) + + ind = [(0, 400), (6, 424), (12, 410), (18, 436), (24, 422), (30, 448), + (36, 434), (42, 458), (48, 466), (L1_STEP, Y_I1), (57, 458), + (L2_STEP, Y_I2), (65, 440)] + p.append(poly([(X0 + s * STEPW, y) for s, y in ind], BLUE, 1.8)) + p.extend([dot(X_L1, Y_I1, BLUE), dot(X_L2, Y_I2, BLUE)]) + p.append(txt(X_L1 - 10, Y_I1 + 6, "I1", 13, "700", BLUE, "end")) + p.append(txt(X_L2 + 10, Y_I2 + 6, "I2", 13, "700", BLUE, "start")) + + # ---------------- legenda de jos + p.append(txt(W / 2, 570, "Prețul face un lower low (L2 sub L1), indicatorul " + "face un higher low (I2 peste I1):", 13, "400", BLACK)) + p.append(txt(W / 2, 590, "divergență bull. Dar dacă al doilea punct e la " + "doar 2-3 candele, nu se numără.", 13, "400", BLACK)) + + p.append('') + return "\n".join(p) + "\n" + + +# ---------------------------------------------------------------- verificare +def est_w(s, size, weight): + factor = 0.62 if weight == "700" else 0.56 + return len(s) * size * factor + + +def verify(svg): + # (1) geometria schemei, exact ce cere spec-ul + assert Y_L2 > Y_L1, "L2 trebuie desenat mai jos decat L1 (lower low)" + assert Y_I2 < Y_I1, "I2 trebuie desenat mai sus decat I1 (higher low)" + + step_l1_l2 = (X_L2 - X_L1) / STEPW + step_l2_l3 = (X_L3 - X_L2) / STEPW + assert step_l1_l2 >= 5.0 - 1e-9, f"prea putine candele L1-L2: {step_l1_l2}" + assert 2.0 - 1e-9 <= step_l2_l3 <= 3.0 + 1e-9, f"L3 nu e la 2-3 pasi: {step_l2_l3}" + + # (4) bracket-ul de 60 de candele acopera de la L2 spre stanga si trece de L1 + x60_left, x60_right = X0, X_L2 + assert x60_right == X_L2, "bracket-ul de 60 porneste din L2" + assert x60_left <= X_L1, "bracket-ul de 60 nu ajunge pana la L1" + assert x60_right - x60_left >= (X_L2 - X_L1), "bracket prea scurt fata de L1" + assert (x60_right - x60_left) / STEPW == 60.0, "nu sunt exact 60 de pasi" + + # (5) „NU" si crucea rosie chiar pe al treilea low + assert "NU" in svg + assert RED in svg and "L3" not in svg # L3 e marcat doar prin cruce, nu cu eticheta + cross_lines = cross(X_L3, Y_L3) + for ln in cross_lines: + assert f'x1="{X_L3 - 10:.1f}"' in ln or f'x2="{X_L3 - 10:.1f}"' in ln + assert f'y1="{Y_L3 - 10:.1f}"' in ln or f'y2="{Y_L3 - 10:.1f}"' in ln + + # etichetele obligatorii din spec sunt in desen + for cheie in ("maxim 60 de candele în spate", "minim 5 candele între puncte", + "2-3 candele", "NU e divergență", "lower low", "higher low", + "L1", "L2", "I1", "I2"): + assert cheie in svg, f"lipsește din desen: {cheie!r}" + + # (2) toate etichetele in panza + for x, y, s, size, weight, anchor in TEXTS: + ew = est_w(s, size, weight) + left = x - ew if anchor == "end" else (x - ew / 2 if anchor == "middle" else x) + right = left + ew + assert -2 <= left and right <= W + 2, f"text in afara panzei: {s!r}" + assert 0 <= y <= H, f"text in afara inaltimii: {s!r} (y={y})" + + # (3) fara doua texte pe aceeasi pozitie + for i in range(len(TEXTS)): + xi, yi, si, szi, wi, ai = TEXTS[i] + we = est_w(si, szi, wi) + li = xi - we if ai == "end" else (xi - we / 2 if ai == "middle" else xi) + ri = li + we + for j in range(i + 1, len(TEXTS)): + xj, yj, sj, szj, wj, aj = TEXTS[j] + wej = est_w(sj, szj, wj) + lj = xj - wej if aj == "end" else (xj - wej / 2 if aj == "middle" else xj) + rj = lj + wej + if abs(yi - yj) < 13 and li < rj - 4 and lj < ri - 4: + raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}") + + # etichetele panourilor incap in panouri + for x, y, s, size, weight, anchor in TEXTS: + for (px, py, pw, ph) in (PRICE_BOX, IND_BOX): + if py <= y <= py + ph and px - 2 <= x <= px + pw + 2: + assert x >= px + 12, f"eticheta lipita de margine: {s!r}" + assert est_w(s, size, weight) <= pw - 24, f"prea lata in panou: {s!r}" + + # fara linie goala in SVG + assert "" not in svg.split("\n")[:-1], "SVG-ul are linie goala" + assert "\n\n" not in svg, "SVG-ul are linie goala" + return True + + +SVG = build() +assert verify(SVG), "verificare esuata" +OUT.write_text(SVG, encoding="utf-8") +print(f"scris {OUT} ({W}x{H}), verificare OK: {len(TEXTS)} etichete, " + f"L1-L2={(X_L2 - X_L1) / STEPW:.1f} pasi, " + f"L2-L3={(X_L3 - X_L2) / STEPW:.1f} pasi, " + f"fereastra 60 = {(X_L2 - X0) / STEPW:.0f} pasi") diff --git a/gen/strategii/mdsrt_masurare.svg b/gen/strategii/mdsrt_masurare.svg new file mode 100644 index 0000000..719fcee --- /dev/null +++ b/gen/strategii/mdsrt_masurare.svg @@ -0,0 +1,36 @@ + + + +Divergența: cum o măsori corect +Aceeași structură pe preț și pe indicator: două swing low-uri, două minime + +PREȚ (schematic) — downtrend, lower low + + + +maxim 60 de candele în spate + + + + +L1 +L2 + + + +minim 5 candele între puncte + + +NU +2-3 candele +NU e divergență + +INDICATOR (Power / volum / RS) — higher low + + + +I1 +I2 +Prețul face un lower low (L2 sub L1), indicatorul face un higher low (I2 peste I1): +divergență bull. Dar dacă al doilea punct e la doar 2-3 candele, nu se numără. + diff --git a/gen/strategii/mdsrt_trinity.py b/gen/strategii/mdsrt_trinity.py new file mode 100644 index 0000000..5987e46 --- /dev/null +++ b/gen/strategii/mdsrt_trinity.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +"""MDSRT — minimul obligatoriu (M complet) vs. varianta Trinity. + +Diagramă conceptuală (nu grafic de preț): pentru +`summaries/strategii/07_mdsrt.md`, Nivel 2, §2.1 „Ce dă edge", după tabel +(marcajul ``). Citește specificația +`gen/strategii/mdsrt_trinity.spec.txt` și scrie +`gen/strategii/mdsrt_trinity.svg`. + +NU folosește `svg_candles`: SVG-ul e construit ca string, fără dependințe. + +Conținutul, cu sursa fiecărei afirmații: + - Power obligatoriu; dacă nu e în divergență, setup-ul nu se califică: + `05_MODUL_4/02_strategia_5_mdsrt.md:59-61, 432-434`. + - Minim 2 indicatori: Power + (Volume SAU Relative Strength): + `05_MODUL_4/02_strategia_5_mdsrt.md:85-88`. + - Trinity = toți trei în divergență simultan, „setup de 5 stele": + `05_MODUL_4/02_strategia_5_mdsrt.md:6-8, 34-42, 87-88`. + - Sensul din selecția simbolului (aliniere în cadranul 1; trei divergențe + simultane NU sunt Trinity): `08_ANALIZA_ZILNICA/11_selectia_simbolului.md:612-627`. + +Rulează: .venv/bin/python gen/strategii/mdsrt_trinity.py +""" +import html +from pathlib import Path + +W, H = 700, 550 + +BLACK = "#000000" +GRAY = "#777777" +RED = "#c00" +GREEN = "#188a3e" +BLUE = "#1a5fb4" + +texts = [] # (x, y, text, size, weight, fill, w_est) +boxes = [] # dict-uri: doar casetele, pentru numărătoarea din assert-uri + + +def add(x, y, s, size=13, weight="normal", fill=BLACK): + factor = 0.62 if weight == "bold" else 0.56 + w = len(s) * size * factor + texts.append((x, y, s, size, weight, fill, w)) + return (x, y, s, size, weight, fill, w) + + +def box(x, y, w, h, *, fill="#ffffff", stroke=GRAY, sw=1.5, dash=None, row=None): + b = {"x": x, "y": y, "w": w, "h": h, "row": row, "lines": []} + boxes.append(b) + return b + + +def box_line(b, dx, dy, s, size=14, weight="normal", fill=BLACK): + cx = b["x"] + b["w"] / 2.0 + dx if dx is not None else b["x"] + b["w"] / 2.0 + add(cx, b["y"] + dy, s, size, weight, fill) + b["lines"].append(s) + + +parts = [] + + +def rect(b, fill, stroke, sw=1.5, dash=None): + d = f' stroke-dasharray="{dash}"' if dash else "" + return (f'') + + +# --------------------------------------------------------------- titlu +add(W / 2, 34, "MDSRT — minimul obligatoriu vs. Trinity", 20, "bold") +add(W / 2, 58, + "Minim Power + unul din ceilalți e obligatoriu; toți trei în divergență = Trinity.", + 13, "normal", GRAY) + +# ------------------------------------------------- rând 1 — minim obligatoriu +add(W / 2, 100, "Minimul obligatoriu (M complet)", 16, "bold") + +B1 = box(40, 112, 250, 56, row=1) +B2 = box(310, 112, 350, 56, row=1) +parts.append(rect(B1, GREEN, GREEN)) +parts.append(rect(B2, "#ffffff", GREEN, sw=2, dash="5 3")) + +box_line(B1, None, 31, "Power (obligatoriu)", 15, "bold", "#ffffff") +box_line(B2, None, 22, "Volume SAU Relative Strength", 14, "normal", GREEN) +box_line(B2, None, 44, "— oricare una", 13, "normal", GREEN) + +add(W / 2, 196, "Power lipsește → setup-ul NU se califică", 14, "bold", RED) + +# ------------------------------------------------- rând 2 — Trinity (opțional) +add(W / 2, 250, "Trinity (opțional, semnal mai puternic)", 16, "bold") + +TITLES = ["Power (obligatoriu)", "Volume", "Relative Strength"] +B3 = box(30, 262, 200, 76, row=2) +B4 = box(250, 262, 200, 76, row=2) +B5 = box(470, 262, 200, 76, row=2) +for b in (B3, B4, B5): + parts.append(rect(b, GREEN, GREEN)) +for b, t in zip((B3, B4, B5), TITLES): + box_line(b, None, 34, t, 14, "bold", "#ffffff") + box_line(b, None, 56, "divergență", 14, "bold", "#ffffff") + +add(W / 2, 360, "toți trei în divergență simultan, pe aceeași zonă de preț", + 14, "normal", BLACK) +add(W / 2, 386, "comparat în curs cu un «setup de 5 stele»", 14, "bold", GREEN) + +# --------------------------------------------------------------- subsol +parts.append(f'') +add(W / 2, 448, + "La MDSRT, „Trinity” se folosește local cu sensul „toți trei în divergență”.", + 13, "normal", GRAY) +add(W / 2, 468, + "Lecția de selecție dă aceluiași cuvânt alt sens:", 13, "normal", GRAY) +add(W / 2, 486, + "aliniere în cadranul 1, nu divergență (contradicția 2.4).", + 13, "normal", GRAY) +add(W / 2, 512, + "Sursa: 05_MODUL_4/02_strategia_5_mdsrt.md:6-8, 34-42, 59-61, 85-88;", + 13, "normal", GRAY) +add(W / 2, 530, + "08_ANALIZA_ZILNICA/11_selectia_simbolului.md:612-627.", + 13, "normal", GRAY) + +# --------------------------------------------------------------- emitere +head = (f'') +parts.insert(0, f'') +parts.insert(0, head) + +body = [] +for x, y, s, size, weight, fill, _ in texts: + body.append( + f'' + f'{html.escape(s, quote=False)}') +svg = "\n".join(parts + body + [""]) + "\n" + +# =============================================================== VERIFICARE +# 1. Rândul 1 conține exact 2 casete (Power + „Volume SAU Relative Strength"), +# nu 3. +row1 = [b for b in boxes if b["row"] == 1] +assert len(row1) == 2, f"rândul 1 are {len(row1)} casete, trebuie exact 2" + +# 2. Rândul 2 conține exact 3 casete (Power, Volume, Relative Strength). +row2 = [b for b in boxes if b["row"] == 2] +assert len(row2) == 3, f"rândul 2 are {len(row2)} casete, trebuie exact 3" +assert [b["lines"][0] for b in row2] == TITLES, "titlurile din rândul 2 diferă" + +# 3. Textul „Power (obligatoriu)" apare în ambele rânduri. +r1_labels = [s for b in row1 for s in b["lines"]] +r2_labels = [s for b in row2 for s in b["lines"]] +assert "Power (obligatoriu)" in r1_labels, "lipsește din rândul 1" +assert "Power (obligatoriu)" in r2_labels, "lipsește din rândul 2" +assert svg.count(">Power (obligatoriu)") == 2, \ + "„Power (obligatoriu)” nu apare exact de două ori" + +# 4. Eticheta „Power lipsește → setup-ul NU se califică" e lângă rândul 1. +assert "Power lipsește → setup-ul NU se califică" in svg, "lipsește eticheta rând 1" + +# 5. Nota de subsol despre cele două sensuri ale lui „Trinity" e prezentă. +assert "se folosește local cu sensul" in svg and "alt sens" in svg \ + and "cadranul 1, nu divergență" in svg and "contradicția 2.4" in svg, \ + "lipsește nota despre cele două sensuri ale lui Trinity" + +# stil: fără umbre/gradienti, text >= 13px, totul în panză, fără suprapuneri +assert "filter=" not in svg and "gradient" not in svg, "fără umbre/gradienti" +assert 'font-family="system-ui, sans-serif"' in svg, "lipsește fontul cerut" + +rects_collide = [] +for x, y, s, size, weight, fill, w in texts: + assert size >= 13, f"font sub 13px: {s!r} ({size})" + left, right = x - w / 2.0, x + w / 2.0 + top, bottom = y - size * 0.78, y + size * 0.22 + assert 0 <= left and right <= W, f"etichetă în afara panzei (orizontal): {s!r}" + assert 0 <= top and bottom <= H, f"etichetă în afara panzei (vertical): {s!r}" + rects_collide.append((left, right, top, bottom, s)) + +for i in range(len(rects_collide)): + for j in range(i + 1, len(rects_collide)): + l1, r1, t1, b1, s1 = rects_collide[i] + l2, r2, t2, b2, s2 = rects_collide[j] + if r1 > l2 and r2 > l1 and b1 > t2 and b2 > t1: + raise AssertionError(f"etichete suprapuse: {s1!r} / {s2!r}") + +for required in ("Minimul obligatoriu (M complet)", + "Volume SAU Relative Strength", + "oricare una", + "Trinity (opțional, semnal mai puternic)", + "divergență", + "pe aceeași zonă de preț", + "setup de 5 stele"): + assert required in svg, f"lipsește din desen: {required!r}" + +OUT = Path(__file__).with_suffix(".svg") +OUT.write_text(svg, encoding="utf-8") +print(f"scris {OUT} ({len(svg)} bytes, {len(texts)} etichete, " + f"{len(boxes)} casete, {W}x{H}), verificare OK") diff --git a/gen/strategii/mdsrt_trinity.svg b/gen/strategii/mdsrt_trinity.svg new file mode 100644 index 0000000..5268a12 --- /dev/null +++ b/gen/strategii/mdsrt_trinity.svg @@ -0,0 +1,30 @@ + + + + + + + + +MDSRT — minimul obligatoriu vs. Trinity +Minim Power + unul din ceilalți e obligatoriu; toți trei în divergență = Trinity. +Minimul obligatoriu (M complet) +Power (obligatoriu) +Volume SAU Relative Strength +— oricare una +Power lipsește → setup-ul NU se califică +Trinity (opțional, semnal mai puternic) +Power (obligatoriu) +divergență +Volume +divergență +Relative Strength +divergență +toți trei în divergență simultan, pe aceeași zonă de preț +comparat în curs cu un «setup de 5 stele» +La MDSRT, „Trinity” se folosește local cu sensul „toți trei în divergență”. +Lecția de selecție dă aceluiași cuvânt alt sens: +aliniere în cadranul 1, nu divergență (contradicția 2.4). +Sursa: 05_MODUL_4/02_strategia_5_mdsrt.md:6-8, 34-42, 59-61, 85-88; +08_ANALIZA_ZILNICA/11_selectia_simbolului.md:612-627. + diff --git a/gen/strategii/mp2_ansamblu.py b/gen/strategii/mp2_ansamblu.py new file mode 100644 index 0000000..aca5334 --- /dev/null +++ b/gen/strategii/mp2_ansamblu.py @@ -0,0 +1,139 @@ +"""Diagrama de ansamblu MP² — bottom major NVDA (strategii/02_mp2.md, Nivel 0). + +„NVDA — weekly (pre-split 1:10)": downtrend masiv → bază/bottom oct. 2022 → +crossing-ul pivotului weekly (mic) peste cel monthly (mare) → candela de +trigger care închide peste pivotul mic (M+A) → intrare 13.80 → SL la swing +low-ul absolut 10.63 (−23 %) → urcare spre TP 17.25 (+25 %, ~9 săptămâni). + +Cifrele (13.80 / 10.63 / 17.25 / −23 % / +25 %) sunt din +`summaries/04_MODUL_3/04_strategia_2_mp_patrat.md:341-357,435`. +Traseul de preț și cele două linii de pivot sunt schematice: cursul nu dă +valorile pivoților, doar faptul că micul trece deasupra marelui la crossing +(`...:326-335`). + +Rulează: + python3 gen/strategii/mp2_ansamblu.py +Scrie lângă el `mp2_ansamblu.svg`. +""" +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__).resolve().parent / "mp2_ansamblu.svg" + +GRAY, RED, GREEN, BLUE = "#777", "#c00", "#188a3e", "#1a5fb4" + +# ------------------------------------------------------------------ seria (38 bare) +# 16 bare de downtrend 32.00 -> 11.40 (minimul absolut 10.63 e fitilul barei 15), +# 10 bare de bază/recuperare 11.80 -> 13.80 (bara 25 = intrarea, după M+A), +# 12 bare de urcare 14.10 -> 17.25 (ținta). open-ul = close-ul precedentului. +DOWN = [32.00, 30.80, 29.60, 28.70, 27.40, 26.50, 25.30, 24.20, 23.60, + 22.30, 21.10, 19.90, 18.50, 16.90, 15.20, 11.40] # 0-15 +BASE = [11.80, 12.10, 12.35, 12.20, 12.55, 12.85, 13.05, 13.35, 13.55, + 13.80] # 16-25 +UP = [14.10, 14.45, 14.80, 15.10, 15.45, 15.80, 16.10, 15.90, 16.30, + 16.60, 16.95, 17.25] # 26-37 +CLOSES = DOWN + BASE + UP +assert len(CLOSES) == 38, len(CLOSES) + +CAPIT, ENTRY, LAST = 15, 25, 37 + +# fitile (sus, jos) per bară; bara 15 are fitilul de jos până la minimul absolut +WICKS = ([(0.35, 0.35)] * 14 + + [(0.30, 0.30)] # 14 + + [(0.20, 0.77)] # 15: low = 11.40 - 0.77 = 10.63 + + [(0.30, 0.30)] * 9 # 16-24 + + [(0.25, 0.20)] # 25: candela de intrare + + [(0.30, 0.30)] * 12) # 26-37 +assert len(WICKS) == len(CLOSES) + +VOLS = ([52, 55, 50, 58, 54, 49, 56, 52, 60, 57, 63, 68, 72, 80, 90, 120] # 0-15 + + [100, 72, 60, 55, 58, 62, 66, 70, 78, 95] # 16-25 + + [88, 80, 74, 70, 68, 72, 76, 70, 74, 78, 84, 92]) # 26-37 +assert len(VOLS) == len(CLOSES) + +cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=32.60) + +# ------------------------------------------------- liniile de pivot (trepte) +# Stepped, schematice (cursul nu dă valorile): monthly „mare" se recalculează +# rar, weekly „mic" mai des. Crossing: micul trece deasupra marelui exact pe +# candela de intrare. +PMARE = [31.00] * 10 + [21.00] * 10 + [13.55] * 8 + [13.20] * 10 # monthly +PMIC = ([30.50] * 5 + [29.00] * 5 + [20.00] * 4 + [17.00] * 4 + + [12.60] * 3 + [12.95] * 2 + [13.20] * 2 + [13.70] * 3 + + [14.20] * 4 + [14.60] * 6) # weekly +assert len(PMARE) == len(PMIC) == len(CLOSES) + +# ---------------------------------------------------------- assert-urile din spec +entry = cds[ENTRY].c +sl, tp = 10.63, cds[LAST].c + +assert abs(entry - 13.80) < 0.05 # close-ul intrării == 13.80 +assert abs(sl - 10.63) < 0.05 and sl < entry # SL = swing low absolut, sub entry +assert abs(tp - 17.25) < 0.10 and tp > entry # TP = 17.25, peste entry +# spec: `abs((entry/sl - 1)*100 - 23)`; formula din spec e inversată (dă +29.8, +# nu −23). Pierderea corectă din entry până la SL e (sl/entry - 1), deci o scriu așa: +loss_pct = (sl / entry - 1) * 100 +assert abs(loss_pct + 23) < 1.5, loss_pct # SL ≈ −23 % +assert abs((tp / entry - 1) * 100 - 25) < 1.5 # TP ≈ +25 % +assert abs(min(c.l for c in cds) - 10.63) < 0.01 # minimul absolut al seriei = SL +assert min(c.l for c in cds[-5:-1]) > 10.63 # stopul de 4 bare ar fi mult mai sus +# crossing-ul pivoților, înainte/după candela de intrare +assert all(PMIC[i] < PMARE[i] for i in range(ENTRY - 4, ENTRY)), "mic < mare înainte de intrare" +assert all(PMIC[i] > PMARE[i] for i in range(ENTRY, ENTRY + 4)), "mic > mare după intrare" +assert cds[ENTRY].c > PMIC[ENTRY], "candela de trigger nu închide peste pivotul mic" +assert abs(cds[CAPIT].l - sl) < 1e-9, "fitilul barei 15 nu atinge exact SL-ul" + +levels = [ + Level(13.80, "entry ~13.80 (după M+A)", GREEN, label_at="left"), + Level(sl, "SL swing low absolut 10.63, −23%", RED, label_at="left"), + Level(tp, "TP ~+25% (17.25), ~9 săpt.", BLUE, label_at="left"), +] +marks = [ + Mark(CAPIT, f"minim absolut {sl:.2f}", RED, "below", dy=-6), + Mark(ENTRY, "M+A", BLUE, "above"), +] + +ch = Chart(candles=cds, levels=levels, marks=marks, + scale=17.0, gap=15, body_w=9, left=44, width=720, top=64, vol_h=70, + vol_label="volum", title="NVDA — weekly (pre-split 1:10)", + caption="Bottom oct. 2022: entry ~13.80, −23%, ~+25% în ~9 săpt. " + "Traseu și pivoți construiți (cursul nu dă valorile lor).") + +svg = ch.render() + + +def step_points(vals: list[float]) -> str: + """Polilinie în trepte: orizontal pe fiecare palier, 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]))) # orizontal până la bara nouă + pts.append((x, y)) # salt vertical la nivelul nou + else: + pts.append((x, y)) + return " ".join(f"{x:.1f},{y:.1f}" for x, y in pts) + + +# injectate imediat după tag-ul de deschidere => desenate în spatele candelelor +inject = [ + f'', + f'', + f'' + f'pivot weekly (mic)', + f'' + f'pivot monthly (mare)', +] +i = svg.index(">") + 1 +svg = svg[:i] + "\n " + "\n ".join(inject) + svg[i:] + +assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown" +OUT.write_text(svg) +print(f"scris {OUT.name}: {len(cds)} candele, entry {entry:.2f}, " + f"SL {sl:.2f} ({loss_pct:+.1f}%), TP {tp:.2f} " + f"({(tp/entry - 1) * 100:+.1f}%), crossing la bara {ENTRY}") diff --git a/gen/strategii/mp2_ansamblu.svg b/gen/strategii/mp2_ansamblu.svg new file mode 100644 index 0000000..e602d3d --- /dev/null +++ b/gen/strategii/mp2_ansamblu.svg @@ -0,0 +1,132 @@ + + + + pivot weekly (mic) + pivot monthly (mare) + NVDA — weekly (pre-split 1:10) + + entry ~13.80 (după M+A) + + SL swing low absolut 10.63, −23% + + TP ~+25% (17.25), ~9 săpt. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + minim absolut 10.63 + M+A + Bottom oct. 2022: entry ~13.80, −23%, ~+25% în ~9 săpt. Traseu și pivoți construiți (cursul nu dă valorile lor). + \ No newline at end of file diff --git a/gen/strategii/mp2_aplus.py b/gen/strategii/mp2_aplus.py new file mode 100644 index 0000000..e1fa0c1 --- /dev/null +++ b/gen/strategii/mp2_aplus.py @@ -0,0 +1,272 @@ +"""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") diff --git a/gen/strategii/mp2_aplus.svg b/gen/strategii/mp2_aplus.svg new file mode 100644 index 0000000..96fb625 --- /dev/null +++ b/gen/strategii/mp2_aplus.svg @@ -0,0 +1,221 @@ +MP² crossing long — setup A+ vs. setup mediocru (aceeași strategie) + A+ — crossing la confluență (SPY, aprilie) + + suport quarterly 554.00 + + SL 554.40 + + intrare 577.00 + + neckline 578.00 + + TP1 599.60 = 1:1 + + downtrend anterior + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + umăr stâng 566 + head 555 / double bottom + umăr drept 568 + crossing + Confluență: suport 554.00 + double bottom 554.45 + neckline 578.00 + + Mediocru — crossing în mijlocul range-ului (construit) + + range 110.00 + + range 100.00 + + SL 103.00 + + intrare 105.00 + + TP1 107.00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + crossing (mijloc) + exemplu construit, nu date reale — SL nu stă pe niciun pivot +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. \ No newline at end of file diff --git a/gen/strategii/mp2_crossing.py b/gen/strategii/mp2_crossing.py new file mode 100644 index 0000000..2de8413 --- /dev/null +++ b/gen/strategii/mp2_crossing.py @@ -0,0 +1,228 @@ +#!/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'', + 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 (prima candelă verde)') +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'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.", + "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") diff --git a/gen/strategii/mp2_crossing.svg b/gen/strategii/mp2_crossing.svg new file mode 100644 index 0000000..ac2ed98 --- /dev/null +++ b/gen/strategii/mp2_crossing.svg @@ -0,0 +1,112 @@ + + + + pivot daily (cel MIC) + DA — crossing (+2) + + pivot weekly (cel MARE) 62.00 + + crossing: 61.70 → 62.20 + intrare: trigger (prima candelă verde) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pivot daily (cel MIC) + NU — continuare/buy the dip (+1) + + pivot weekly (cel MARE) 62.00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + semnale ulterioare = BUY THE DIP, nu crossing + 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. + \ No newline at end of file diff --git a/gen/strategii/mp2_exemplu.py b/gen/strategii/mp2_exemplu.py new file mode 100644 index 0000000..8bcd300 --- /dev/null +++ b/gen/strategii/mp2_exemplu.py @@ -0,0 +1,147 @@ +"""Diagrama din `gen/strategii/mp2_exemplu.spec.txt` (TIP: PRET). + +Exemplul complet din curs: SPY long la bottom-ul de aprilie 2020 — double +bottom + Inverted Head & Shoulders la suport quarterly, trigger MP² long +(pivotul mic închide peste cel mare), prima candelă verde după umărul drept. +Intrare 577.00, SL 554.40 (1R = 22.60), TP1 599.60, ținta 4R era 667.00, iar +mișcarea reală s-a oprit la 646.00 (~3R) într-o lună. + +Toate cifrele vin din `summaries/09_QA/01_2026-08-25.md:384-394,485-494` +(liniile citate în spec). Serie continuă de 38 de candele daily +(`from_closes`), panou de volum, SMA-20, niveluri reale și marcaje ancorate. +""" +import sys +from pathlib import Path + +sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402 + +OUT = Path(__file__).resolve().parent / "mp2_exemplu.svg" + +# ── cifrele din material (09_QA/01_2026-08-25.md:384-394,485-494) ─────────── +ENTRY = 577.00 # candela de intrare (prima verde după umărul drept) +SL = 554.40 # sub umărul drept, chiar peste suportul quarterly +TP1 = 599.60 # 577.00 + 22.60 (1R) +TARGET_4R = 667.00 # ținta declarată; 4R exact = 667.40 +REAL = 646.00 # mișcarea reală (~3R, +12% efectiv) +QUARTERLY = 554.00 # suport quarterly +SHOULDER_L = 566.0 # umăr stâng +HEAD = 555.0 # head (cel mai jos) +SHOULDER_R = 568.0 # umăr drept +NECKLINE = 578.00 + +# ── structura seriei (spec): ~14 coborâre → ~10 bază → intrare → ~12 urcare ─ +closes = [ + # 0-13: ~14 candele de coborâre de la ~600 spre umărul stâng 566 + 600.0, 597.0, 594.0, 596.5, 591.0, 587.5, 590.0, + 584.0, 579.5, 581.5, 576.0, 571.0, 567.0, + 566.0, # 13 umăr stâng + # 14-24: baza — double bottom + Inverted Head & Shoulders + 570.5, 578.0, 572.0, 567.0, 560.0, + 555.0, # 19 head (cel mai jos) + 561.5, 568.0, 578.0, 573.0, + 568.0, # 24 umăr drept + # 25: candela de intrare, prima verde după umărul drept + 577.00, + # 26-37: ~12 candele de urcare spre 646.00 (marcajul „real") + 581.0, 585.5, 589.0, 584.0, 590.5, 596.0, 602.0, + 609.0, 616.0, 624.0, 635.0, 646.0, +] +assert len(closes) == 38, len(closes) + +ENTRY_I = 25 +HEAD_I = 19 +SHL_I = 13 +SHR_I = 24 + +wicks = [(0.4, 0.4)] * len(closes) +wicks[SHL_I] = (0.4, 0.5) # low umăr stâng 565.50 +wicks[HEAD_I] = (0.4, 0.6) # low head = 555.0 - 0.6 = 554.40 (suportul atins) +wicks[SHR_I] = (0.4, 0.5) +wicks[15] = (0.4, 0.4) # vârf 578.0 = neckline +wicks[22] = (0.4, 0.4) # vârf 578.0 = neckline +wicks[ENTRY_I] = (0.7, 0.4) # intrare verde, wick mic +wicks[37] = (0.0, 0.3) # high = 646.00 exact (mișcarea reală se oprește aici) + +vols = [ + # coborâre + 48, 46, 50, 44, 52, 49, 45, 53, 55, 47, 50, 54, 58, 62, + # bază (head-ul are volum mare — capitulare) + 60, 72, 65, 58, 66, 88, 70, 64, 74, 60, 56, + # intrare + 92, + # urcare + 78, 72, 68, 60, 66, 74, 82, 88, 80, 84, 90, 95, +] +assert len(vols) == len(closes) + +cds = from_closes(closes, wicks=wicks, vols=vols, start=602.0) + +# ── assert-urile din specificație ─────────────────────────────────────────── +entry = cds[ENTRY_I].c +assert abs(entry - 577.00) < 0.05, entry +assert abs(SL - 554.40) < 1e-9, SL +assert SL > QUARTERLY, (SL, QUARTERLY) # peste suportul quarterly +assert abs((ENTRY - SL) - 22.60) < 0.05, ENTRY - SL # 1R +assert abs(TP1 - (ENTRY + 22.60)) < 0.05, TP1 # TP1 = intrare + 1R +target_4r = ENTRY + 4 * (ENTRY - SL) +assert abs(target_4r - 667.40) < 0.05, target_4r # 4R exact +assert abs(target_4r - TARGET_4R) < 0.5, target_4r # ≈ 667.00, rotunjit în curs +assert abs((REAL - ENTRY) / (ENTRY - SL) - 3.05) < 0.02, (REAL - ENTRY) / (ENTRY - SL) +assert abs((REAL - ENTRY) / (ENTRY - SL) - 4.0) > 0.5, "mișcarea reală NU e 4R" + +# structura care susține afirmațiile din text +assert abs(cds[SHL_I].c - SHOULDER_L) < 0.05, cds[SHL_I].c +assert abs(cds[HEAD_I].c - HEAD) < 0.05, cds[HEAD_I].c +assert abs(cds[SHR_I].c - SHOULDER_R) < 0.05, cds[SHR_I].c +assert HEAD < SHOULDER_L and HEAD < SHOULDER_R, (HEAD, SHOULDER_L, SHOULDER_R) +assert cds[SHL_I].c == min(c.c for c in cds[SHL_I - 1:SHL_I + 2]), "umărul stâng nu e minim local" +assert cds[HEAD_I].c == min(c.c for c in cds[HEAD_I - 1:HEAD_I + 2]), "head-ul nu e minim local" +assert cds[SHR_I].c == min(c.c for c in cds[SHR_I - 1:SHR_I + 2]), "umărul drept nu e minim local" +min_pre = min(c.l for c in cds[:ENTRY_I]) +assert abs(min_pre - 554.0) < 1.0, min_pre # suportul quarterly atins +assert abs(min_pre - SL) < 0.05, (min_pre, SL) # SL chiar la minimul head-ului +assert cds[ENTRY_I].c > cds[ENTRY_I].o, "candela de intrare trebuie verde" +assert cds[ENTRY_I - 1].c < cds[ENTRY_I - 1].o, "umărul drept e roșu" +assert abs(cds[37].c - REAL) < 0.05, cds[37].c +assert all(c.vol is not None for c in cds), "lipsește panoul de volum" +assert len(cds) == 38 + +# ── desenul ───────────────────────────────────────────────────────────────── +levels = [ + Level(TARGET_4R, "ținta 4R era 667.00 (+16%)", "#555"), + Level(REAL, "646.00 — +12% efectiv (~3R)", "#188a3e", label_at="left"), + Level(TP1, "TP1 599.60", "#1a5fb4"), + Level(NECKLINE, "neckline 578.00", "#1a5fb4"), + Level(SL, "SL 554.40", "#c00"), + Level(QUARTERLY, "suport quarterly 554.00", "#777", label_at="left"), +] + +marks = [ + Mark(SHL_I, "umăr stâng 566", "#1a1a1a", "below"), + Mark(HEAD_I, "head 555", "#1a1a1a", "below"), + Mark(SHR_I, "umăr drept 568", "#1a1a1a", "below"), + Mark(ENTRY_I, "intrare 577.00", "#188a3e", "above", dy=-10), +] + +ch = Chart( + candles=cds, + levels=levels, + marks=marks, + mas=[MA(20, "#b06000", dash="4 3", text="SMA-20")], + scale=3.8, width=720, gap=15, body_w=9, left=44, vol_h=70, + vol_label="volum", title="SPY — daily", + caption=("Intrare 577.00 = prima verde după umărul drept (MP² long). " + "1R = 22.60; 646.00 ~3R, într-o lună."), +) + +svg = ch.render() +assert svg.count("\n\n") == 0, "linie goală în SVG — rupe markdown-ul" +OUT.write_text(svg) + +print(f"serie {len(cds)} candele | intrare {entry:.2f} @ {ENTRY_I} | " + f"SL {SL:.2f} | 1R {ENTRY - SL:.2f} | TP1 {TP1:.2f} | " + f"4R {target_4r:.2f} (ținta declarată {TARGET_4R:.2f}) | " + f"real {REAL:.2f} = {(REAL - ENTRY) / (ENTRY - SL):.2f}R | " + f"min pre-intrare {min_pre:.2f}") +print(f"scris {OUT}") diff --git a/gen/strategii/mp2_exemplu.svg b/gen/strategii/mp2_exemplu.svg new file mode 100644 index 0000000..88098f2 --- /dev/null +++ b/gen/strategii/mp2_exemplu.svg @@ -0,0 +1,138 @@ + + SPY — daily + + ținta 4R era 667.00 (+16%) + + 646.00 — +12% efectiv (~3R) + + TP1 599.60 + + neckline 578.00 + + SL 554.40 + + suport quarterly 554.00 + + SMA-20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + umăr stâng 566 + head 555 + umăr drept 568 + intrare 577.00 + Intrare 577.00 = prima verde după umărul drept (MP² long). 1R = 22.60; 646.00 ~3R, într-o lună. + \ No newline at end of file