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
171 lines
8.2 KiB
Python
171 lines
8.2 KiB
Python
"""Diagrama de trigger MBP (marcajul `trigger`), Pasul 3.
|
|
|
|
Două panouri de 15 minute care pornesc din exact aceeași candelă M (close
|
|
623.87 sub banda 623.90, low 623.60 — cifrele exemplului din curs), și se
|
|
despart după ea:
|
|
|
|
- DA: prima candelă de după M e verde și închide la 623.95 (Buy Momentum
|
|
din curs). Acolo intri, la close-ul ei.
|
|
- NU: cele trei candele de după M rămân roșii, fără nicio candelă de
|
|
moment. Nu intri. Cine ar fi cumpărat „că oricum era oversold" ar fi
|
|
avut stopul la 623.60 și l-ar fi văzut rupt două bare mai târziu.
|
|
|
|
Zona punctată acoperă exact ferestrele în care aștepți trigger-ul, iar
|
|
numărul din eticheta ei e numărul de bare pe care le acoperă, calculat.
|
|
|
|
Rulează: python3 gen/strategii/mbp_trigger.py
|
|
"""
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, "/workspace/atm-curs-dl")
|
|
from svg_candles import Arrow, Chart, Level, Zone, from_closes # noqa: E402
|
|
|
|
OUT = Path(__file__).with_suffix(".svg")
|
|
RED, GREEN, BLUE, GRAY = "#c00", "#188a3e", "#1a5fb4", "#777"
|
|
|
|
BAND = 623.90 # banda inferioară din exemplul cursului
|
|
M = 10 # indexul candelei M, în ambele panouri
|
|
COMUN = [624.95, 624.80, 624.62, 624.70, 624.50, 624.36, 624.20, 624.28,
|
|
624.10, 624.02, 623.87] # 0..10, ultima = M
|
|
W_COMUN = [(0.06, 0.10)] * 10 + [(0.06, 0.27)] # M: low 623.60
|
|
|
|
DA = COMUN + [623.95, 624.12, 624.24] # 11 = trigger verde
|
|
W_DA = W_COMUN + [(0.05, 0.12), (0.06, 0.08), (0.06, 0.08)]
|
|
NU = COMUN + [623.80, 623.62, 623.45] # trei bare roșii, fără trigger
|
|
W_NU = W_COMUN + [(0.04, 0.10), (0.04, 0.10), (0.04, 0.10)]
|
|
|
|
da = from_closes(DA, wicks=W_DA, start=625.10)
|
|
nu = from_closes(NU, wicks=W_NU, start=625.10)
|
|
|
|
# ── ce afirmă textul, verificat pe serie ───────────────────────────────────
|
|
assert len(da) == len(nu) == 14
|
|
assert all(abs(a.c - b.c) < 1e-9 for a, b in zip(da[:M + 1], nu[:M + 1])), \
|
|
"panourile trebuie să pornească din aceeași candelă M"
|
|
assert abs(da[M].c - 623.87) < 1e-9 and da[M].c < BAND # M închide sub bandă
|
|
assert da[M].c < da[M].o # M e roșie
|
|
assert abs(da[M].l - 623.60) < 1e-9, da[M].l # low-ul lui M = viitorul stop
|
|
|
|
TRIG = M + 1
|
|
entry = da[TRIG].c
|
|
assert abs(entry - 623.95) < 1e-9, entry
|
|
assert da[TRIG].c > da[TRIG].o, "candela de trigger trebuie să fie verde"
|
|
assert entry > da[M].c, "trigger-ul închide peste close-ul lui M"
|
|
|
|
sl = min(c.l for c in da[TRIG - 3:TRIG + 1]) # 4 candele, trigger inclus
|
|
assert abs(sl - 623.60) < 1e-9, sl
|
|
assert sl == da[M].l
|
|
|
|
# panoul NU: nicio candelă verde după M, iar prețul rupe nivelul stopului
|
|
fereastra_nu = nu[M + 1:]
|
|
assert all(c.c < c.o for c in fereastra_nu), "în panoul NU nu are voie să apară o candelă verde"
|
|
assert min(c.l for c in fereastra_nu) < sl, "NU: prețul trebuie să rupă 623.60"
|
|
rupere = min(c.l for c in fereastra_nu)
|
|
n_nu = len(fereastra_nu)
|
|
assert n_nu == 3
|
|
pierdere = sl - rupere
|
|
|
|
SCALE, GAP, BODY, LEFT, PW = 190.0, 22, 14, 44, 372
|
|
|
|
pts_all = [p for c in (da + nu) for p in (c.h, c.l)] + [BAND]
|
|
P_LO, P_HI = min(pts_all), max(pts_all)
|
|
AER = 0.07 * (P_HI - P_LO)
|
|
SPACER = [Level(P_HI + AER, "", "none", dash=""), Level(P_LO - AER, "", "none", dash="")]
|
|
|
|
|
|
def panel(cds, levels, zones, arrows):
|
|
ch = Chart(candles=cds, levels=levels + SPACER, zones=zones, 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
|
|
w, h = int(m.group(1)), float(m.group(2))
|
|
return ch, w, h, raw[raw.index(">") + 1:raw.rindex("</svg>")].strip("\n")
|
|
|
|
|
|
ch_da, w_da, h_da, in_da = panel(
|
|
da,
|
|
[Level(BAND, f"bandă inferioară {BAND:.2f}", BLUE, dash="4 3", label_at="left"),
|
|
Level(sl, f"SL {sl:.2f}", RED, dash="4 3", label_at="left")],
|
|
[Zone(TRIG, TRIG, "", GREEN)],
|
|
[Arrow(TRIG, f"trigger: candelă verde,\nintri la close {entry:.2f}", where="above", color=GREEN),
|
|
Arrow(M, f"M: close {da[M].c:.2f},\nsub bandă", where="below", color=RED)])
|
|
ch_nu, w_nu, h_nu, in_nu = panel(
|
|
nu,
|
|
[Level(BAND, f"bandă inferioară {BAND:.2f}", BLUE, dash="4 3", label_at="left"),
|
|
Level(sl, f"stopul ar fi fost {sl:.2f}", RED, dash="4 3", label_at="left")],
|
|
[Zone(M + 1, M + n_nu, f"{n_nu} candele fără trigger", RED)],
|
|
[Arrow(len(nu) - 1, f"aici s-ar fi rupt stopul,\ncu {pierdere:.2f} $ sub {sl:.2f}",
|
|
where="below", color=RED)])
|
|
|
|
PH = max(h_da, h_nu)
|
|
add_da: list[str] = []
|
|
add_nu: list[str] = []
|
|
|
|
FOOTER = [
|
|
f"Aceeași candelă M în ambele panouri: close {da[M].c:.2f} sub banda {BAND:.2f}, "
|
|
f"low {sl:.2f}. Ce urmează după ea decide.",
|
|
f"DA: candela {TRIG - M} de după M e verde și închide la {entry:.2f} — ăsta e trigger-ul, "
|
|
f"iar close-ul ei e prețul de intrare.",
|
|
f"NU: {n_nu} candele roșii la rând, niciun trigger. Nu intri. Prețul coboară până la "
|
|
f"{rupere:.2f}, cu {pierdere:.2f} $ sub {sl:.2f},",
|
|
"adică exact sub stopul pe care l-ai fi avut dacă intrai „pentru că oricum era oversold\".",
|
|
"Condiția M (bandă + TICUS) nu e un ordin de cumpărare: e doar permisiunea de a aștepta trigger-ul.",
|
|
"Cifrele lui M și ale trigger-ului (623.87 / 623.90 / 623.60 / 623.95) sunt din curs; "
|
|
"restul barelor sunt construite de mine.",
|
|
]
|
|
TOP = 52
|
|
X_DA, X_NU = 8, 8 + w_da + 16
|
|
RW = max(780, X_NU + w_nu + 8)
|
|
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 3 — condiția M deschide '
|
|
'fereastra; trigger-ul e cel care te bagă în trade</text>',
|
|
f'<text x="8" y="40" fill="{GREEN}" style="font-weight:bold">DA — candelă de moment '
|
|
f'după M</text>',
|
|
f'<text x="{X_NU}" y="40" fill="{RED}" style="font-weight:bold">NU — M valid, dar fără '
|
|
f'trigger</text>',
|
|
f'<svg x="{X_DA}" y="{TOP}" width="{w_da}" height="{PH:.0f}" viewBox="0 0 {w_da} {PH:.0f}">'
|
|
f'{in_da}\n {chr(10).join(add_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}\n {chr(10).join(add_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(re.sub(r"<[^>]+>", "", 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 + "\n" + "\n".join(add_da), w_da, PH, "panoul DA")
|
|
fits(in_nu + "\n" + "\n".join(add_nu), w_nu, PH, "panoul NU")
|
|
assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
|
|
assert svg.count("<svg") == 3 and svg.count("</svg>") == 3
|
|
assert f"{n_nu} candele fără trigger" in svg and n_nu == len(fereastra_nu)
|
|
# 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}: M {da[M].c:.2f} (low {sl:.2f}), trigger {entry:.2f}; "
|
|
f"NU {n_nu} bare roșii, rupere {rupere:.2f} ({pierdere:.2f} $ sub stop)")
|