"""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'') 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("")].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'suport major {nivel:.2f}', f'candela M: close {da[M].c:.2f},', f'sub banda inferioară {lo_da:.2f}', f'cădere de {cadere:.2f} $ în {n_fall} bare', f'', ] add_nu = [ f'' f'benzile se strâng la {lat_nu:.2f} $ lățime', f'niciun close în afara lor — semnalul nu apare', ] 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'', f'Pasul 1 — unde are voie ' f'strategia să caute: mișcare rapidă într-un nivel major', f'DA — cădere rapidă în ' f'suport, banda atinsă', f'NU — consolidare strânsă, ' f'banda nu se atinge', f'' f'{in_da}\n {chr(10).join(add_da)}', f'' f'{in_nu}\n {chr(10).join(add_nu)}'] fy = TOP + PH + 24 out += [f'{t}' for i, t in enumerate(FOOTER)] out.append("") 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']*)>(.*?)', 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("") == 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*]*>\s*", "", 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}")