"""MDSRT — diagrama de ansamblu (Nivelul 1) și seria canonică a capitolului. Aici trăiește singurul exemplu construit al capitolului: SPY pe 5 minute, suport/pivot daily 590.00, piciorul 1 la 590.10 (ține nivelul), piciorul 2 la 589.50 (îl rupe, cu divergență pe volum), candela-trigger = Buy Hammer, intrare 590.20, SL 589.40, risc 0.80, TP1 591.00 (1:1), ieșire pe trail la 592.40 (2.75R). Celelalte generatoare de preț ale capitolului (picior, trigger, stop, iesire) IMPORTĂ seria de aici. Așa o regulă numerică — marja stopului, distanța până la țintă — nu poate să difere de la o diagramă la alta. Regulile numerice ale capitolului, definite o singură dată, aici: MARJA = 0.10 — stopul stă cu atât sub low-ul piciorului 2 intrarea = close-ul candelei care sparge high-ul candelei-trigger TP1 = intrare + (intrare − SL) Ce e din curs și ce nu: structura M→A→P e a cursului; suportul 590.00 și minimul piciorului 2 vin din exemplul construit al autorului sumarului (`05_MODUL_4/02_strategia_5_mdsrt.md:166-177, 439-445`), nu din transcriere — transcrierea se întrerupe înainte de planul numeric. Rulează: python3 gen/strategii/mdsrt_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, MA, Zone, Arrow, from_closes # noqa: E402 OUT = Path(__file__).with_suffix(".svg") GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000" # ------------------------------------------------- regulile numerice ale capitolului MARJA = 0.10 # stopul, sub low-ul piciorului 2 SUPORT = 590.00 # pivotul daily (nivelul de pe TF-ul mare) LEG1, TRIG, CONF = 10, 18, 19 # indicii: piciorul 1, candela-trigger, candela de intrare STOP_BARS = 4 # low-ul se caută pe ultimele 4 candele, ca la Buy Doji CLOSES = [ 596.00, 595.20, 595.60, 594.80, 594.10, 593.40, 593.70, 592.90, # 0-7 592.20, 591.60, 590.40, # 10 = piciorul 1 590.90, 591.40, 591.10, 591.30, 590.90, 590.50, 590.05, # 11-17 589.80, # 18 = piciorul 2 (Buy Hammer) 590.20, # 19 = intrarea 590.60, 590.90, 591.00, # 20-22 (22 = TP1) 591.30, 591.10, 591.45, 591.70, 591.95, 592.15, # 23-28 592.80, 593.10, 592.90, # 29-31 = ultimul impuls 592.40, # 32 = ieșire pe trail ] WICKS = [(0.25, 0.30)] * 33 WICKS[LEG1] = (0.25, 0.30) # piciorul 1: low 590.10, deasupra suportului WICKS[16] = (0.20, 0.15) WICKS[17] = (0.15, 0.10) # low 589.95 — prețul intră în zonă, fără să facă minim nou WICKS[TRIG] = (0.10, 0.30) # hammer: high 590.15, low 589.50 WICKS[CONF] = (0.10, 0.05) WICKS[20] = (0.25, 0.15) # retestul nivelului ține: low 590.05, peste pivot WICKS[21] = (0.05, 0.15) # high 590.95 < TP1: ținta e atinsă prima dată la bara 22 VOLS = [ 48, 46, 44, 45, 43, 41, 40, 42, 44, 46, # 0-9 52, # 10 = volumul piciorului 1 38, 40, 42, 39, 44, 50, 58, # 11-17 88, 80, # 18-19 absorbție la minim 70, 64, 58, 60, 55, 52, 50, 48, 46, # 20-28 52, 48, 42, 46, # 29-32 ] assert len(CLOSES) == len(WICKS) == len(VOLS) == 33 EXIT_I = 32 # bara pe care se închide poziția, sub linia de trail def bull_trail(cds, start): """Bull Trail, definiția cursului: low-ul fiecărei candele care se închide peste high-ul candelei dinainte; linia urcă, nu coboară niciodată. Întoarce lista [(index, nivel)] de la bara `start` încolo.""" out, cur = [], None for i in range(start, len(cds)): if cds[i].c > cds[i - 1].h: cur = round(cds[i].l, 2) if cur is None else max(cur, round(cds[i].l, 2)) if cur is not None: out.append((i, cur)) return out def serie(): """Seria canonică a capitolului, 33 de candele de 5 minute.""" return from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=596.60) cds = serie() leg1_l = cds[LEG1].l leg2_l = cds[TRIG].l ENTRY = cds[CONF].c SL = round(leg2_l - MARJA, 2) R = round(ENTRY - SL, 2) TP1 = round(ENTRY + R, 2) # ------------------------------------------------------------------ assert-urile for i, c in enumerate(cds): assert c.h >= max(c.o, c.c) and c.l <= min(c.o, c.c), f"bara {i}: h/l incoerent" if i: assert abs(c.o - cds[i - 1].c) < 1e-9, f"open[{i}] != close[{i-1}]" # 1. piciorul 1 ține nivelul, piciorul 2 îl rupe — asta e povestea capitolului assert leg1_l == 590.10 and leg1_l > SUPORT, leg1_l assert leg2_l == 589.50 and leg2_l < SUPORT, leg2_l assert leg2_l < leg1_l, (leg2_l, leg1_l) # 2. cele două picioare sunt minimele locale, nu doar două bare oarecare assert leg1_l == min(c.l for c in cds[:LEG1 + 6]), "piciorul 1 nu e minimul zonei lui" assert leg2_l == min(c.l for c in cds), "piciorul 2 nu e minimul seriei" # 3. distanța dintre picioare depășește minimul de 5 candele cerut de curs assert TRIG - LEG1 == 8, TRIG - LEG1 # 4. hammer: corp mic sus, coadă lungă în jos body = abs(cds[TRIG].o - cds[TRIG].c) rng = cds[TRIG].h - cds[TRIG].l tail = min(cds[TRIG].o, cds[TRIG].c) - cds[TRIG].l assert body <= 0.5 * rng and tail > body, (body, rng, tail) # 5. intrarea sparge high-ul candelei-trigger (regula de intrare a capitolului) assert ENTRY == 590.20 and ENTRY > cds[TRIG].h, (ENTRY, cds[TRIG].h) # 6. stopul: marja sub cel mai jos low din ultimele 4 candele low4 = min(c.l for c in cds[CONF - STOP_BARS + 1:CONF + 1]) assert low4 == leg2_l, (low4, leg2_l) assert SL == 589.40 and R == 0.80, (SL, R) # 7. TP1 e 1:1 exact și e atins prima dată la bara 22 assert abs((ENTRY - SL) - (TP1 - ENTRY)) < 1e-9 assert TP1 == 591.00 assert next(i for i in range(CONF + 1, len(cds)) if cds[i].h >= TP1 - 1e-9) == 22 # 8. downtrend vizibil înainte de piciorul 1 assert cds[LEG1].c < cds[0].c - 5, (cds[LEG1].c, cds[0].c) # 9. divergența: lower low pe preț, higher low pe volum assert cds[TRIG].l < cds[LEG1].l and VOLS[TRIG] > VOLS[LEG1] assert VOLS[16] < VOLS[17] < VOLS[TRIG] == max(VOLS) # 10. după intrare, retestul ține: nicio bară nu mai închide sub pivot assert all(c.c > SUPORT for c in cds[CONF + 1:]), "prețul recade sub pivot după intrare" assert min(c.l for c in cds[CONF + 1:]) > SUPORT, "o bară de după intrare rupe iar pivotul" # 11. ieșirea pe trail: ultimul swing low înainte de întoarcere e linia de trail, # iar bara de ieșire e prima care se închide sub ea _trail = bull_trail(cds, CONF) TRAIL = dict(_trail)[EXIT_I] # nivelul liniei de trail când vine bara de ieșire assert TRAIL == 592.50, TRAIL assert [p for _, p in _trail] == sorted(p for _, p in _trail), "linia de trail coboară" assert all(cds[i].c > p for i, p in _trail if i < EXIT_I), "trail-ul era rupt mai devreme" assert cds[EXIT_I].c == 592.40 < TRAIL, (cds[EXIT_I].c, TRAIL) r_mult = round((cds[EXIT_I].c - ENTRY) / R, 2) assert r_mult == 2.75, r_mult if __name__ == "__main__": levels = [ Level(SUPORT, f"pivot daily {SUPORT:.2f} — zona (M)", BLUE, label_at="left"), Level(SL, f"SL {SL:.2f} = 0.10 sub piciorul 2 (risc {R:.2f})", RED), Level(TP1, f"TP1 {TP1:.2f} = 1:1", GREEN, label_at="left"), ] marks = [ Mark(LEG1, f"piciorul 1: low {leg1_l:.2f}", GRAY, "below"), Mark(TRIG, f"piciorul 2: low {leg2_l:.2f}", RED, "below", dy=16), ] arrows = [ Arrow(TRIG, "Buy Hammer: coadă lungă în jos\nvânzătorii au dus prețul sub nivel\nși nu l-au putut ține acolo", where="below", price=leg2_l, color=RED, dx=210, dy=58), Arrow(CONF, f"intrare {ENTRY:.2f}: intri abia aici,\nla close peste high-ul candelei-trigger", where="above", color=GREEN, dx=40, dy=-44), Arrow(EXIT_I, f"ieșire pe trail {cds[EXIT_I].c:.2f} = {r_mult:.2f}R\nprima închidere sub linia de trail", where="right", color=GREEN, dy=-6), ] ch = Chart( candles=cds, levels=levels, marks=marks, zones=[ Zone(LEG1, CONF, "M (zonă + divergență) → A (trigger) → P (plan)", BLUE)], arrows=arrows, mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")], scale=48.0, gap=15, body_w=9, left=44, width=660, top=74, vol_h=70, vol_label="volum: la piciorul 2, volum mai mare decât la piciorul 1 = divergență", ) svg = ch.render() TITLE = ["SPY — 5 min (exemplu construit pe scara lecției;", "cifrele planului nu vin din curs)"] title_svg = "\n ".join( f'{t}' for i, t in enumerate(TITLE)) i = svg.index(">") + 1 svg = svg[:i] + "\n " + title_svg + svg[i:] m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg) W, H0 = int(m.group(1)), float(m.group(2)) legend = [ "Legendă: structura M → A → P e a cursului; nivelurile (590.00, 589.50) și planul", "(intrare, SL, TP) sunt exemplul construit al autorului sumarului, nu citate din curs.", ] 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 "\n\n" not in svg, "linie goală în SVG" OUT.write_text(svg) print(f"scris {OUT.name}: picior 1 {leg1_l:.2f} / picior 2 {leg2_l:.2f} sub {SUPORT:.2f}, " f"intrare {ENTRY:.2f}, SL {SL:.2f} (risc {R:.2f}), TP1 {TP1:.2f}, ieșire {r_mult:.2f}R")