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:
Claude Agent
2026-09-20 12:43:53 +00:00
parent f1033a916c
commit 3143e35ed9
8 changed files with 1164 additions and 0 deletions

View 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}")