mbp: patru diagrame noi, pentru pasii care nu aveau
Context (DA/NU: cadere in suport major vs. consolidare stransa, cu benzile Bollinger calculate din close-uri), trigger (aceeasi candela M in ambele panouri), stop (fereastra de 4 candele, 1R si sizing derivate) si iesire (TP1 -> break even -> trail, fata de capcana iesirii la 50 %, unde prețul urca apoi la +2R). Aceeasi regula de stop in toate cinci diagramele ale capitolului, si banda inferioara calibrata explicit ca sa nu existe doua valori pentru acelasi nivel. Textul capitolului e neatins — agentul a atins pragul de context. SVG-urile sunt deocamdata fisiere in plus, fara marcaje care sa le ceara. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
This commit is contained in:
248
gen/strategii/mbp_context.py
Normal file
248
gen/strategii/mbp_context.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""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
|
||||
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}")
|
||||
Reference in New Issue
Block a user