"""Diagrama de ieșire MBP (marcajul `iesire`), Pasul 5. Două panouri de 15 minute care pornesc din aceeași intrare (623.95, stop 623.60, deci 1R = 0.35 — cifrele exemplului din curs) și se deosebesc prin ce faci cu stopul după ce prima tranșă a ieșit la 1:1: - DA: prima tranșă iese la TP1 624.30 (1:1), stopul rămas se mută la break even (623.95, prețul de intrare), iar restul merge pe trail **pe ultimul swing low** — regula poziției 3 din lecția principală (Bull Trail, apoi swing low dacă se formează unul mai sus). Fiecare treaptă pornește de la swing low-ul care o declanșează, nu din marginea graficului. - NU: stopul e mutat la jumătatea drumului („50 %", 624.125). Fitilul unei bare lungi coboară sub el și te scoate, deși prețul urcă apoi mai sus decât în panoul din stânga. Toate nivelurile ies din serie prin calcul: TP1 = intrare + (intrare − stop), break even = intrarea, nivelul „50 %" = intrare + 1R/2, treptele de trail sunt swing low-urile găsite în serie (minim local confirmat de o bară cu low mai sus), iar ieșirea e prima închidere sub ultima treaptă. Rulează: python3 gen/strategii/mbp_iesire.py """ import re import sys from pathlib import Path sys.path.insert(0, "/workspace/atm-curs-dl") sys.path.insert(0, "/workspace/atm-curs-dl/gen") from svg_candles import Arrow, Chart, Level, Mark, from_closes # noqa: E402 from eticheta_libera import eticheta_libera # noqa: E402 OUT = Path(__file__).with_suffix(".svg") RED, GREEN, BLUE, GRAY, ORANGE = "#c00", "#188a3e", "#1a5fb4", "#777", "#b06000" ENTRY, SL = 623.95, 623.60 RISC = ENTRY - SL TP1 = ENTRY + RISC assert abs(RISC - 0.35) < 1e-9 and abs(TP1 - 624.30) < 1e-9 E = 3 # indexul candelei de trigger (intrarea = close-ul ei) PRE = [624.15, 623.98, 623.87, ENTRY] # ...M la index 2, trigger la 3 W_PRE = [(0.06, 0.10), (0.05, 0.16), (0.06, 0.27), (0.05, 0.12)] # DA: TP1, apoi două pullback-uri care lasă fiecare un swing low mai sus; # trail-ul urcă pe ele și te scoate abia la a treia coborâre, pe la +4R. DA = PRE + [624.70, 624.78, 624.60, 625.05, 625.45, 625.70, 625.52, 625.92, 625.35] W_DA = W_PRE + [(0.04, 0.04), (0.04, 0.03), (0.03, 0.09), (0.05, 0.04), (0.05, 0.03), (0.08, 0.02), (0.03, 0.14), (0.05, 0.03), (0.02, 0.04)] # NU: aceeași urcare, dar a treia bară are un fitil lung care taie nivelul 50 % NU = PRE + [624.12, 624.24, 624.20, 624.38, 624.52, 624.60, 624.55] W_NU = W_PRE + [(0.05, 0.06), (0.06, 0.05), (0.04, 0.22), (0.05, 0.06), (0.06, 0.05), (0.06, 0.05), (0.04, 0.07)] da = from_closes(DA, wicks=W_DA, start=624.20) nu = from_closes(NU, wicks=W_NU, start=624.20) assert len(da) == 13 and len(nu) == 11 # ── ce afirmă textul, verificat pe serie ─────────────────────────────────── assert abs(da[E].c - ENTRY) < 1e-9 and abs(nu[E].c - ENTRY) < 1e-9 assert da[E].c > da[E].o and nu[E].c > nu[E].o # candela de trigger e verde assert min(c.l for c in da[E - 3:E + 1]) == SL, "stopul: cel mai jos minim din 4 candele" assert min(c.l for c in nu[E - 3:E + 1]) == SL assert all(abs(a.c - b.c) < 1e-9 for a, b in zip(da[:E + 1], nu[:E + 1])) tp_i = next(i for i in range(E + 1, len(da)) if da[i].h >= TP1) assert tp_i == E + 1, tp_i # TP1 atins de fitilul barei # ── trail-ul pe swing low ────────────────────────────────────────────────── # Un swing low = minim local; devine treaptă de trail din momentul în care o # bară de după el închide un low mai sus. Treptele se caută în serie, nu se # scriu de mână; stopul nu coboară niciodată. swings = [i for i in range(1, len(da) - 1) if da[i].l < da[i - 1].l and da[i].l < da[i + 1].l] trepte = [i for i in swings if i > tp_i] assert trepte == [6, 10], trepte assert all(da[j].l > da[i].l for i, j in zip(trepte, trepte[1:])), "treptele urcă" iesire_i = next(i for i in range(trepte[-1] + 1, len(da)) if da[i].c < da[trepte[-1]].l) trail = da[trepte[-1]].l iesire = da[iesire_i].c assert iesire_i == len(da) - 1, iesire_i r_iesire = (iesire - ENTRY) / RISC assert abs(r_iesire - 4.0) < 0.02, r_iesire # ~1:4, cât spune materialul # nicio treaptă de trail nu are voie să treacă peste low-ul unei bare din # dreapta ei — altfel linia ar fi desenată deasupra prețului. Bara care rupe # trail-ul (ieșirea) e singura excepție, prin definiție. for i in trepte: for j in range(i + 1, iesire_i): assert da[j].l > da[i].l, (i, j, da[i].l, da[j].l) assert da[iesire_i].l < trail < da[iesire_i - 1].l + 1 # contrastul cu varianta „set and forget": trail sub minimul fiecărei candele # închise, pe exact aceeași structură — prima corecție te scoate. cc_i = next(i for i in range(tp_i + 1, len(da)) if da[i].c < da[i - 1].l) cc, r_cc = da[cc_i].c, (da[cc_i].c - ENTRY) / RISC assert cc_i < iesire_i and r_cc < r_iesire, (cc_i, r_cc) # panoul NU: nivelul „50 %" și fitilul care îl taie half = ENTRY + RISC / 2 assert abs(half - 624.125) < 1e-9, half sc_i = E + 3 assert nu[sc_i].l < half, (nu[sc_i].l, half) # fitilul coboară sub 50 % assert nu[sc_i].c > half, nu[sc_i].c # dar candela închide peste el assert nu[sc_i].h < TP1, "în panoul NU ieșirea vine înainte de TP1" ratat = max(c.h for c in nu[sc_i + 1:]) r_scos = (half - ENTRY) / RISC r_ratat = (ratat - ENTRY) / RISC SCALE, GAP, BODY, LEFT = 158.0, 28, 16, 46 pts = [p for c in (da + nu) for p in (c.h, c.l)] + [TP1, SL, half] AER = 0.07 * (max(pts) - min(pts)) SPACER = [Level(max(pts) + AER, "", "none", dash=""), Level(min(pts) - AER, "", "none", dash="")] def panel(cds, levels, arrows, marks, pw): ch = Chart(candles=cds, levels=levels + SPACER, arrows=arrows, marks=marks, scale=SCALE, gap=GAP, body_w=BODY, left=LEFT, width=pw) raw = ch.render() m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', raw) assert m return ch, int(m.group(1)), float(m.group(2)), raw[raw.index(">") + 1:raw.rindex("")].strip("\n") ET_DA = {f"swing low {da[i].l:.2f}" for i in trepte} ch_da, w_da, h_da, in_da = panel( da, [Level(TP1, f"TP1 {TP1:.2f} (1:1)", BLUE, dash="4 3", label_at="left"), Level(ENTRY, f"intrare {ENTRY:.2f} = break even", GREEN, dash="2 3", label_at="right"), Level(SL, f"SL inițial {SL:.2f}", RED, dash="4 3", label_at="left")] + [Level(da[i].l, "", ORANGE, dash="1 3", i0=i) for i in trepte], [Arrow(tp_i, f"prima tranșă iese la {TP1:.2f}", where="above", color=BLUE, dy=-4), Arrow(iesire_i, f"închidere sub trail:\nieși cu +{r_iesire:.2f}R", where="below", color=ORANGE)], [Mark(i, f"swing low {da[i].l:.2f}", color=ORANGE, where="below", dy=400) for i in trepte], 404) ch_nu, w_nu, h_nu, in_nu = panel( nu, [Level(TP1, f"TP1 {TP1:.2f} (1:1)", BLUE, dash="4 3", label_at="left"), Level(half, f"stop la 50 %: {half:.3f}", RED, dash="4 3", label_at="right"), Level(ENTRY, f"intrare {ENTRY:.2f}", GREEN, dash="2 3", label_at="right"), Level(SL, f"SL inițial {SL:.2f}", RED, dash="4 3", label_at="left")], [Arrow(sc_i, f"fitilul taie 50 %:\nieși cu +{r_scos:.1f}R", where="below", color=RED), Arrow(len(nu) - 2, f"prețul urcă la {ratat:.2f}\n(+{r_ratat:.1f}R) fără tine", where="above", color=GRAY)], [], 420) PH = max(h_da, h_nu) TOP = 52 X_DA, X_NU = 8, 8 + w_da + 16 RW = max(780, X_NU + w_nu + 8) FOOTER = [ f"Ambele panouri pornesc din aceeași intrare {ENTRY:.2f}, cu stopul la {SL:.2f}, " f"deci 1R = {RISC:.2f} $ și TP1 = {TP1:.2f}.", f"DA: prima tranșă iese la TP1. Stopul rămas se duce la break even {ENTRY:.2f} — de acolo " f"trade-ul nu mai poate ieși pe minus —", "iar a treia tranșă merge pe trail pe ultimul swing low — liniile portocalii punctate.", f"Treapta 1 = {da[trepte[0]].l:.2f}, treapta 2 = {trail:.2f}; fiecare pornește de la swing " f"low-ul care o ridică, nu din marginea graficului.", f"Ieși la prima închidere sub ultima treaptă, {iesire:.2f} = +{r_iesire:.1f}R — de aici vine " f"„realizat ~1:4\" din material; prețul atinsese {max(c.h for c in da):.2f}.", "Trail-ul sub minimul fiecărei candele închise e pasul 4 al variantei „set and forget\", " "nu regula poziției 3:", f"pe aceeași structură te-ar fi scos la prima corecție, {cc:.2f} = +{r_cc:.1f}R.", f"NU: stopul mutat la jumătatea drumului, {half:.3f}. Fitilul unei singure bare coboară sub " f"el și te scoate cu +{r_scos:.1f}R,", f"deși bara închide peste nivel, iar prețul urcă după aceea la {ratat:.2f} " f"(+{r_ratat:.1f}R).", "Break even nu are cum să te scoată în pierdere: stă fix la prețul de intrare.", "Ce nu a ieșit nici la țintă, nici pe trail se închide oricum la finalul sesiunii.", "Cifrele intrării, stopului și ale TP1 sunt din curs; barele de după intrare sunt " "construite de mine.", ] root_h = TOP + PH + 24 + 17 * len(FOOTER) + 8 out = [f'', 'Pasul 5 — unde muți stopul ' 'după ce prima tranșă a ieșit', f'DA — break even, apoi ' f'trail pe ultimul swing low', f'NU — stopul mutat la ' f'„50 %"', f'' f'{in_da}', f'' f'{in_nu}'] fy = TOP + PH + 24 out += [f'{t}' for i, t in enumerate(FOOTER)] out.append("") svg = "\n ".join(out) def fits(body: str, w: float, h: float, unde: str) -> None: box: list[tuple[float, float, float, str]] = [] for mm in re.finditer(r']*)>(.*?)', body): x, y, attrs, txt = float(mm.group(1)), float(mm.group(2)), mm.group(3), mm.group(4) if not txt.strip(): continue tw = len(txt) * (6.4 if "bold" in attrs else 5.9) x0 = x - tw if 'anchor="end"' in attrs else (x - tw / 2 if 'anchor="middle"' in attrs else x) assert x0 >= -2 and x0 + tw <= w + 2, f"{unde}: text tăiat: {txt!r}" assert 0 <= y <= h + 2, f"{unde}: text în afara înălțimii: {txt!r}" box.append((x0, x0 + tw, y, txt)) for i in range(len(box)): for j in range(i + 1, len(box)): (a0, a1, ay, at), (b0, b1, by, bt) = box[i], box[j] assert not (abs(ay - by) < 13 and a0 < b1 - 4 and b0 < a1 - 4), \ f"{unde}: texte suprapuse: {at!r} / {bt!r}" fits(in_da, w_da, PH, "panoul DA") fits(in_nu, w_nu, PH, "panoul NU") fits("\n".join(out[-len(FOOTER) - 1:]), RW, root_h, "subsolul") # etichetele de trail și marcajele de swing low nu au voie să stea pe desen eticheta_libera(in_da, "panoul DA", ET_DA, rects=True) # etichetele de nivel stau lipite de linia lor prin construcție; aici contează # doar să nu cadă peste corpurile candelelor NIV = {f"TP1 {TP1:.2f} (1:1)", f"intrare {ENTRY:.2f} = break even", f"intrare {ENTRY:.2f}", f"SL inițial {SL:.2f}", f"stop la 50 %: {half:.3f}"} eticheta_libera(in_da, "nivelurile DA", NIV, rects=True, segmente=False) eticheta_libera(in_nu, "nivelurile NU", NIV, rects=True, segmente=False) assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown" assert svg.count("") == 3 # aceeași scară în ambele panouri: 1R desenat identic assert abs((ch_da._y(ENTRY) - ch_da._y(TP1)) - (ch_nu._y(ENTRY) - ch_nu._y(TP1))) < 1e-9 # 1:1 și vizual: distanța intrare-TP1 == distanța SL-intrare assert abs((ch_da._y(ENTRY) - ch_da._y(TP1)) - (ch_da._y(SL) - ch_da._y(ENTRY))) < 1e-9 # etichetele goale (nivelurile-distanțier) nu au ce căuta în SVG: auditul le # citește ca text suprapus peste ce e sub ele svg = re.sub(r"\n\s*]*>\s*", "", svg) assert svg.count("\n\n") == 0 OUT.write_text(svg, encoding="utf-8") print(f"scris {OUT.name}: TP1 {TP1:.2f} la bara {tp_i}, trepte de trail " f"{[f'{da[i].l:.2f}@{i}' for i in trepte]}, ieșire {iesire:.2f} (+{r_iesire:.2f}R); " f"NU: 50 % la {half:.3f}, scos cu +{r_scos:.1f}R, ratat {ratat:.2f} (+{r_ratat:.1f}R)")