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
254 lines
12 KiB
Python
254 lines
12 KiB
Python
"""Diagrama de context MBP (marcajul `context`), Pasul 1.
|
|
|
|
Două panouri de 15 minute, cu benzile Bollinger calculate din close-uri
|
|
(SMA20 ± 2 deviații standard), nu desenate de mână:
|
|
|
|
- DA: mișcare rapidă în jos, opt bare, care ajunge într-un nivel major
|
|
(minimul zilei precedente) și împinge o candelă să închidă SUB banda
|
|
inferioară. Ăsta e terenul strategiei.
|
|
- NU: consolidare strânsă, aceeași durată; benzile se strâng odată cu
|
|
prețul, niciun close nu iese din ele, deci condiția de bandă nu se poate
|
|
îndeplini niciodată.
|
|
|
|
Cifrele desenate ies din serie prin calcul (assert-uri mai jos). Nivelul
|
|
„suport major" e construit de mine: exemplul din curs nu dă un preț pentru
|
|
el, spune doar că semnalul are sens la capătul unei mișcări rapide care
|
|
ajunge într-un nivel major.
|
|
|
|
Rulează: python3 gen/strategii/mbp_context.py
|
|
"""
|
|
import re
|
|
import statistics
|
|
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__).with_suffix(".svg")
|
|
RED, GREEN, BLUE, GRAY = "#c00", "#188a3e", "#1a5fb4", "#777"
|
|
N, K = 20, 2.0
|
|
|
|
|
|
def bb(closes: list[float]) -> list[tuple[float, float, float] | None]:
|
|
"""SMA20 ± 2 deviații standard, per bară; None până se adună 20 de close-uri."""
|
|
out: list[tuple[float, float, float] | None] = []
|
|
for i in range(len(closes)):
|
|
if i + 1 < N:
|
|
out.append(None)
|
|
continue
|
|
w = closes[i + 1 - N:i + 1]
|
|
mid = sum(w) / N
|
|
sd = statistics.pstdev(w)
|
|
out.append((mid - K * sd, mid, mid + K * sd))
|
|
return out
|
|
|
|
|
|
# Benzile au nevoie de 20 de close-uri înainte să existe. Ca să se vadă pe tot
|
|
# graficul, seriile au un prefix de 20 de bare care se calculează, dar nu se
|
|
# desenează; se afișează doar cele 28 de bare de după el.
|
|
PRE_DA = [624.55, 624.40, 624.52, 624.35, 624.20, 624.32, 624.15, 624.28,
|
|
624.10, 624.22, 624.05, 624.18, 624.00, 624.12, 623.95, 624.08,
|
|
623.90, 624.02, 623.85, 623.80]
|
|
# ── panoul DA: dip în suport, urcare, apoi 8 bare de cădere rapidă înapoi în el
|
|
CTX_DA = [623.72, 623.65, 623.88, 624.10, 624.35, 624.60, 624.80, 625.00,
|
|
625.15, 625.30, 625.20, 625.35, 625.25, 625.42, 625.30, 625.48,
|
|
625.36, 625.52, 625.44, 625.58]
|
|
FALL = [625.20, 624.85, 624.60, 624.40, 624.25, 624.10, 624.02, 623.87]
|
|
SHOW_DA = CTX_DA + FALL
|
|
# Calibrare: banda inferioară se calculează din close-uri, iar în exemplul din
|
|
# curs ea e 623.90. Ridic cu 1,2 cenți close-urile din fereastra de 20 de bare
|
|
# (fără candela M, al cărei close 623.87 e cifra cursului), ca banda calculată
|
|
# să iasă exact 623.90 și capitolul să nu aibă două valori pentru același nivel.
|
|
CAL = 0.012
|
|
CLOSES_DA = [c + (CAL if 28 <= i <= 46 else 0.0)
|
|
for i, c in enumerate(PRE_DA + SHOW_DA)]
|
|
W_DA = ([(0.08, 0.08)] * 20
|
|
+ [(0.06, 0.06), (0.06, 0.05), (0.10, 0.05)] + [(0.10, 0.10)] * 17
|
|
+ [(0.06, 0.14)] * 7 + [(0.06, 0.27)])
|
|
assert len(CLOSES_DA) == len(W_DA) == 48
|
|
|
|
# ── panoul NU: aceeași lungime afișată, dar consolidare strânsă până la capăt
|
|
PRE_NU = [625.30, 625.52, 625.60, 625.44, 625.20, 625.05, 625.12, 625.31,
|
|
625.53, 625.62, 625.45, 625.22, 625.07, 625.14, 625.34, 625.55,
|
|
625.59, 625.47, 625.26, 625.08]
|
|
SHOW_NU = [625.35, 625.57, 625.62, 625.45, 625.23, 625.08, 625.15, 625.33,
|
|
625.55, 625.64, 625.47, 625.21, 625.10, 625.13, 625.37, 625.56,
|
|
625.60, 625.49, 625.25, 625.06, 625.14, 625.36, 625.58, 625.61,
|
|
625.46, 625.24, 625.09, 625.16]
|
|
CLOSES_NU = PRE_NU + SHOW_NU
|
|
W_NU = [(0.07, 0.07)] * 48
|
|
assert len(CLOSES_NU) == len(W_NU) == 48
|
|
assert len(SHOW_DA) == len(SHOW_NU) == 28
|
|
|
|
SKIP = 20 # bare de încălzire, nedesenate
|
|
|
|
da_full = from_closes(CLOSES_DA, wicks=W_DA, start=624.70)
|
|
nu_full = from_closes(CLOSES_NU, wicks=W_NU, start=625.20)
|
|
da, nu = da_full[SKIP:], nu_full[SKIP:]
|
|
|
|
bb_da, bb_nu = bb(CLOSES_DA)[SKIP:], bb(CLOSES_NU)[SKIP:]
|
|
M = len(SHOW_DA) - 1 # ultima bară a panoului DA = candela M
|
|
|
|
# ── ce afirmă textul, verificat pe serie ───────────────────────────────────
|
|
b_m = bb_da[M]
|
|
assert b_m is not None
|
|
lo_da, mid_da, up_da = b_m
|
|
assert abs(lo_da - 623.90) < 0.005, lo_da # aceeași bandă ca în exemplul din curs
|
|
assert da[M].c < lo_da, (da[M].c, lo_da) # DA: close sub banda inferioară
|
|
assert da[M].c < da[M].o # candela M e roșie
|
|
# într-o cădere rapidă ies din bandă mai multe bare; le numărăm, ca eticheta
|
|
# să spună o cifră care chiar iese din serie
|
|
sub = [i for i, v in enumerate(bb_da) if v and da[i].c < v[0]]
|
|
assert M in sub and len(sub) >= 2, sub
|
|
n_sub = len(sub)
|
|
|
|
nivel = min(c.l for c in da[:6]) # suport major = minimul din stânga graficului
|
|
assert abs(nivel - 623.60) < 1e-9, nivel
|
|
assert abs(da[M].l - nivel) < 1e-9, da[M].l # căderea se oprește exact în el
|
|
assert min(c.l for c in da[6:M]) > nivel + 0.2 # între timp prețul n-a mai fost acolo
|
|
|
|
cadere = da[len(CTX_DA) - 1].c - da[M].c
|
|
assert cadere > 1.50, cadere # mișcare rapidă, nu derivă
|
|
n_fall = len(FALL)
|
|
assert n_fall == 8
|
|
|
|
lat_da = up_da - lo_da
|
|
b_nu = bb_nu[-1]
|
|
assert b_nu is not None
|
|
lat_nu = b_nu[2] - b_nu[0]
|
|
assert lat_nu < lat_da / 2.5, (lat_nu, lat_da) # benzile se strâng în consolidare
|
|
for i in range(len(SHOW_NU)):
|
|
v = bb_nu[i]
|
|
assert v is not None and v[0] < nu[i].c < v[2], (i, nu[i].c, v)
|
|
|
|
SCALE, GAP, BODY, LEFT, PW = 78.0, 12, 8, 40, 372
|
|
|
|
|
|
def bands_svg(ch: Chart, vals, color=BLUE) -> str:
|
|
"""Cele trei linii Bollinger, desenate din valorile calculate mai sus."""
|
|
parts = []
|
|
for j, dash in ((0, "4 3"), (1, ""), (2, "4 3")):
|
|
pts = [f"{ch._cx(i):.1f},{ch._y(v[j]):.1f}" for i, v in enumerate(vals) if v]
|
|
d = f' stroke-dasharray="{dash}"' if dash else ""
|
|
parts.append(f'<polyline points="{" ".join(pts)}" fill="none" stroke="{color}" '
|
|
f'stroke-width="1.2" opacity="0.85"{d}/>')
|
|
return "\n ".join(parts)
|
|
|
|
|
|
# aceeași fereastră de preț în ambele panouri (altfel lățimile de bandă nu se
|
|
# pot compara cu ochiul) și 6 % aer sus și jos, ca nicio linie să nu cadă pe
|
|
# marginea cadrului
|
|
pts_all = [p for c in (da + nu) for p in (c.h, c.l)]
|
|
pts_all += [v[j] for vv in (bb_da, bb_nu) for v in vv if v for j in (0, 2)]
|
|
P_LO, P_HI = min(pts_all), max(pts_all)
|
|
AER = 0.06 * (P_HI - P_LO)
|
|
SPACER = [Level(P_HI + AER, "", "none", dash=""), Level(P_LO - AER, "", "none", dash="")]
|
|
|
|
|
|
def panel(cds, vals, levels):
|
|
ch = Chart(candles=cds, levels=levels + SPACER, 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)
|
|
w, h = int(m.group(1)), float(m.group(2))
|
|
inner = raw[raw.index(">") + 1:raw.rindex("</svg>")].strip("\n")
|
|
return ch, w, h, bands_svg(ch, vals) + "\n " + inner
|
|
|
|
|
|
ch_da, w_da, h_da, in_da = panel(
|
|
da, bb_da,
|
|
[Level(nivel, "", GRAY, dash="6 3", label_at="left")])
|
|
ch_nu, w_nu, h_nu, in_nu = panel(nu, bb_nu, [])
|
|
|
|
# liniile nu au voie să cadă pe marginea cadrului (6 % aer sus și jos)
|
|
for ch, cds, vals in ((ch_da, da, bb_da), (ch_nu, nu, bb_nu)):
|
|
ys = [ch._y(p) for c in cds for p in (c.h, c.l)]
|
|
ys += [ch._y(v[j]) for v in vals if v for j in (0, 2)]
|
|
assert min(ys) > 8.0, min(ys) # aer față de marginea de sus
|
|
assert max(ys) < min(h_da, h_nu) - 8, (max(ys), h_da)
|
|
assert max(ys) - min(ys) > 40
|
|
|
|
PH = max(h_da, h_nu)
|
|
TOP = 52
|
|
add_da = [
|
|
# eticheta nivelului, legată de linia lui, pusă în zona liberă din mijloc
|
|
f'<text x="{ch_da._cx(13):.1f}" y="{ch_da._y(nivel) - 6:.1f}" fill="{GRAY}" '
|
|
f'text-anchor="middle">suport major {nivel:.2f}</text>',
|
|
f'<text x="{ch_da._cx(M) - 4:.1f}" y="{ch_da._y(da[M].l) + 20:.1f}" fill="{RED}" '
|
|
f'text-anchor="end" style="font-weight:bold">candela M: close {da[M].c:.2f},</text>',
|
|
f'<text x="{ch_da._cx(M) - 4:.1f}" y="{ch_da._y(da[M].l) + 34:.1f}" fill="{RED}" '
|
|
f'text-anchor="end" style="font-weight:bold">sub banda inferioară {lo_da:.2f}</text>',
|
|
f'<text x="{ch_da._cx(len(CTX_DA) - 1) - 6:.1f}" '
|
|
f'y="{ch_da._y(da[len(CTX_DA) - 1].h) - 14:.1f}" fill="{RED}" text-anchor="end" '
|
|
f'style="font-weight:bold">cădere de {cadere:.2f} $ în {n_fall} bare</text>',
|
|
f'<line x1="{ch_da._cx(len(CTX_DA) - 1):.1f}" y1="{ch_da._y(da[len(CTX_DA) - 1].h) - 6:.1f}" '
|
|
f'x2="{ch_da._cx(M):.1f}" y2="{ch_da._y(da[M].h) - 6:.1f}" stroke="{RED}" '
|
|
f'stroke-width="1.5" stroke-dasharray="3 3"/>',
|
|
]
|
|
add_nu = [
|
|
f'<text x="{ch_nu._cx(14):.1f}" y="{ch_nu._y(max(c.h for c in nu)) - 16:.1f}" '
|
|
f'fill="{GRAY}" text-anchor="middle" style="font-weight:bold">'
|
|
f'benzile se strâng la {lat_nu:.2f} $ lățime</text>',
|
|
f'<text x="{ch_nu._cx(14):.1f}" y="{ch_nu._y(min(c.l for c in nu)) + 22:.1f}" '
|
|
f'fill="{GRAY}" text-anchor="middle">niciun close în afara lor — semnalul nu apare</text>',
|
|
]
|
|
|
|
FOOTER = [
|
|
f"DA: opt bare duc prețul cu {cadere:.2f} $ în jos, în suportul major {nivel:.2f} "
|
|
f"(minimul din stânga graficului),",
|
|
f"iar {n_sub} bare închid sub banda inferioară; M e ultima dintre ele, la {da[M].c:.2f} "
|
|
f"față de bandă {lo_da:.2f}.",
|
|
"Care dintre ele e candela M o alege TICUS, la pasul 2 — banda singură nu decide.",
|
|
f"NU: aceeași durată, dar prețul stă pe loc; banda are {lat_nu:.2f} $ lățime față de "
|
|
f"{lat_da:.2f} $ în stânga,",
|
|
"iar niciun close nu iese din ea. Fără atingerea benzii nu există condiția M, deci nu există trade.",
|
|
"Benzile sunt calculate din close-uri (SMA20 ± 2 deviații standard), nu desenate de mână.",
|
|
"Nivelul de suport major e construit de mine: exemplul din curs nu dă un preț pentru el.",
|
|
]
|
|
root_h = TOP + PH + 24 + 17 * len(FOOTER) + 8
|
|
out = [f'<svg viewBox="0 0 780 {root_h:.0f}" xmlns="http://www.w3.org/2000/svg" '
|
|
f'style="width:100%;height:auto;font:12px sans-serif">',
|
|
f'<text x="8" y="20" fill="#333" style="font-weight:bold">Pasul 1 — unde are voie '
|
|
f'strategia să caute: mișcare rapidă într-un nivel major</text>',
|
|
f'<text x="8" y="40" fill="{GREEN}" style="font-weight:bold">DA — cădere rapidă în '
|
|
f'suport, banda atinsă</text>',
|
|
f'<text x="398" y="40" fill="{RED}" style="font-weight:bold">NU — consolidare strânsă, '
|
|
f'banda nu se atinge</text>',
|
|
f'<svg x="8" 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="398" 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)
|
|
|
|
assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
|
|
|
|
|
|
def _fits(body: str, w: float, h: float, unde: str) -> None:
|
|
"""Nicio etichetă nu are voie să iasă din cadrul panoului (text tăiat)."""
|
|
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} ({x0:.0f}..{x0 + tw:.0f} / {w})"
|
|
assert 0 <= y <= h + 2, f"{unde}: text în afara înălțimii: {txt!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("<svg") == 3 and svg.count("</svg>") == 3
|
|
assert f"{n_fall} bare" in svg and len(FALL) == n_fall
|
|
# 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}: DA close {da[M].c:.2f} < bandă {lo_da:.2f}, cădere {cadere:.2f} $ "
|
|
f"în {n_fall} bare, suport {nivel:.2f}; NU lățime bandă {lat_nu:.2f} vs {lat_da:.2f}")
|