Primul capitol terminat cap-coada dupa 01_buy_doji: nivelurile 1-2-3, cei 5 pasi cu propozitie de actiune si "Treci mai departe doar daca", mini-glosar de 38 de termeni, oglinda pe short cu diagrama ei, checklist. Sursele au iesit in fise/08_mbp_surse.md. Diagrame noi pentru pasii 1, 3, 4 si 5, care nu aveau: context, trigger, stop, iesire. Benzile Bollinger din context sunt calculate din close-uri, nu desenate, cu 20 de bare de incalzire nedesenate. MBP e monotimeframe, deci cerinta de corespondenta intre timeframe-uri nu se aplica aici — nu s-a inventat o diagrama pentru ea. Stopul e fix pe cel mai jos minim din 4 candele, fara marja: asa spune cursul pentru strategia asta, spre deosebire de 01_buy_doji. Diferenta e spusa explicit in text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
196 lines
9.8 KiB
Python
196 lines
9.8 KiB
Python
"""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), restul merge pe trail — stopul urcă sub
|
||
minimul fiecărei candele închise — și iese la prima închidere sub el.
|
||
- 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, iar ieșirea pe trail e
|
||
prima închidere sub minimul candelei precedente, căutată în serie.
|
||
|
||
Rulează: python3 gen/strategii/mbp_iesire.py
|
||
"""
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, "/workspace/atm-curs-dl")
|
||
from svg_candles import Arrow, Chart, Level, from_closes # 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: urcare la TP1, încă două bare, apoi întoarcere care rupe trail-ul
|
||
DA = PRE + [624.12, 624.24, 624.34, 624.46, 624.52, 624.40, 624.28]
|
||
W_DA = W_PRE + [(0.05, 0.06), (0.06, 0.05), (0.04, 0.06), (0.05, 0.06),
|
||
(0.07, 0.05), (0.04, 0.08), (0.03, 0.06)]
|
||
# 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.30)
|
||
nu = from_closes(NU, wicks=W_NU, start=624.30)
|
||
assert len(da) == 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 + 2], nu[:E + 2]))
|
||
|
||
tp_i = next(i for i in range(E + 1, len(da)) if da[i].h >= TP1)
|
||
assert abs(da[tp_i].h - 624.34) < 0.05, da[tp_i].h # TP1 atins de fitilul barei
|
||
assert tp_i == E + 3, tp_i
|
||
|
||
# trail: stopul urcă sub minimul fiecărei candele închise; ieși la prima
|
||
# închidere sub el. Nivelul și bara de ieșire se caută în serie, nu se scriu.
|
||
iesire_i = next(i for i in range(tp_i + 1, len(da)) if da[i].c < da[i - 1].l)
|
||
trail = da[iesire_i - 1].l
|
||
iesire = da[iesire_i].c
|
||
assert iesire > ENTRY, iesire
|
||
r_iesire = (iesire - ENTRY) / RISC
|
||
assert 0.5 < r_iesire < 2.5, r_iesire
|
||
assert tp_i < iesire_i < len(da), iesire_i # ieșirea vine după TP1, în serie
|
||
|
||
# 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"
|
||
assert max(c.h for c in nu[sc_i + 1:]) > max(c.h for c in da), \
|
||
"NU: prețul urcă după aceea mai sus decât în panoul DA"
|
||
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, PW = 300.0, 30, 18, 52, 372
|
||
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):
|
||
ch = Chart(candles=cds, levels=levels + SPACER, arrows=arrows, 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("</svg>")].strip("\n")
|
||
|
||
|
||
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="left"),
|
||
Level(SL, f"SL inițial {SL:.2f}", RED, dash="4 3", label_at="left"),
|
||
Level(trail, f"trail {trail:.2f}", ORANGE, dash="1 3", label_at="right")],
|
||
[Arrow(tp_i, f"prima tranșă iese la {TP1:.2f}", where="above", color=BLUE, dy=-6),
|
||
Arrow(iesire_i, f"închidere sub trail:\nieși cu +{r_iesire:.2f}R", where="below", color=ORANGE)])
|
||
ch_nu, w_nu, h_nu, in_nu = panel(
|
||
nu,
|
||
[Level(TP1, f"TP1 {TP1:.2f} (1:1) — nu mai ajungi la el", BLUE, dash="4 3", label_at="left"),
|
||
Level(half, f"stop mutat la 50 %: {half:.3f}", RED, dash="4 3", label_at="right"),
|
||
Level(ENTRY, f"intrare {ENTRY:.2f}", GREEN, dash="2 3", label_at="left"),
|
||
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)])
|
||
|
||
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 —",
|
||
f"iar restul merge pe trail: stopul urcă sub minimul fiecărei candele închise "
|
||
f"({trail:.2f} aici)",
|
||
f"și ieși la prima închidere sub el, {iesire:.2f} = +{r_iesire:.2f}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'<svg viewBox="0 0 {RW:.0f} {root_h:.0f}" xmlns="http://www.w3.org/2000/svg" '
|
||
f'style="width:100%;height:auto;font:12px sans-serif">',
|
||
'<text x="8" y="20" fill="#333" style="font-weight:bold">Pasul 5 — unde muți stopul '
|
||
'după ce prima tranșă a ieșit</text>',
|
||
f'<text x="8" y="40" fill="{GREEN}" style="font-weight:bold">DA — break even, apoi '
|
||
f'trail</text>',
|
||
f'<text x="{X_NU}" y="40" fill="{RED}" style="font-weight:bold">NU — stopul mutat la '
|
||
f'„50 %"</text>',
|
||
f'<svg x="{X_DA}" y="{TOP}" width="{w_da}" height="{PH:.0f}" viewBox="0 0 {w_da} {PH:.0f}">'
|
||
f'{in_da}</svg>',
|
||
f'<svg x="{X_NU}" y="{TOP}" width="{w_nu}" height="{PH:.0f}" viewBox="0 0 {w_nu} {PH:.0f}">'
|
||
f'{in_nu}</svg>']
|
||
fy = TOP + PH + 24
|
||
out += [f'<text x="8" y="{fy + 17 * i:.0f}" fill="#555">{t}</text>' for i, t in enumerate(FOOTER)]
|
||
out.append("</svg>")
|
||
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'<text x="([-\d.]+)" y="([-\d.]+)"([^>]*)>(.*?)</text>', 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")
|
||
assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
|
||
assert svg.count("<svg") == 3 and svg.count("</svg>") == 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*<text[^>]*>\s*</text>", "", 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}, trail {trail:.2f}, ieșire {iesire:.2f} "
|
||
f"(+{r_iesire:.2f}R); NU: 50 % la {half:.3f}, scos cu +{r_scos:.1f}R, ratat {ratat:.2f} "
|
||
f"(+{r_ratat:.1f}R)")
|