Files
atm-curs-dl/gen/az_pivoti.py
Claude Agent 646205e60f 04 pivoti: reparate 6 defecte, plus o eroare de sens preluata din harvest
Verificare independenta fata de harvest/ si transcrieri. Toate punctele
confirmate in surse inainte de reparatie, niciunul contestat.

Conținut:
- citat compus cu sensul rasturnat, scos: documentul ii punea in gura
  "rejectat sub pivotul de an, asta nu-mi place". In transcriere, la 25.08
  el spune despre exact acea situatie "asta imi place"; "nu-mi place" e din
  29.07 si e despre volumul in distributie negativa. Coloana NU are acum un
  citat real, din 03.09, iar 29.07 e adaugata la Surse.
- "timeframe-urile instituționale" sunt, la el, monthly si weekly (04.09,
  citat), nu quarterly si anual. Documentul 05 le definea deja corect, deci
  cele doua se contraziceau.
- pasajul despre wedge nu mai e prezentat ca regula explicita: el vrea volum
  mai mare, iar breakout-ul masiv il leaga de reintrarea in MBT-10.
- "proiectie neutra": imperativul "trateaza-le la fel" a devenit deducție
  marcata, si indicatia e declarata in completari, nu doar legatura.

EROARE IN SURSA, gasita pe drum: harvest/2026-08-25.md l.15 da ca Trigger NU
"pretul ramane sub pivotul de an (reject)", dar in transcriere (l.9) el zice
"Reject-a si sub pivotul de an, stochasticul s-a dus aproape de oversold,
asta imi place". Rescris dupa transcriere, cu nota in Surse ca recolta
greseste acolo. Distinctia reala e stochasticul: sub pivotul de an cu
oversold ii place, cu overbought e semnal contra (02.09).

Desen:
- svg9: toate cele patru etichete de swing stateau SUB linia pivotului desi
  candelele erau deasupra - exact pe dos fata de regula pe care o ilustrau.
- svg10: etichetele din dreapta erau decalate cu o poziție fata de liniile
  lor. Cauza era in spread() din PChart: SMA-20 se termina exact pe linia
  pivot Q3 si impingerea mergea doar in jos, deci fiecare eticheta de sub ea
  aluneca. Acum etichetele care se ating se asaza centrat in jurul poziției
  lor naturale, iar peste 3px de linia lor PChart trage o linie de legatura.
  Semnatura si interfata clasei sunt neschimbate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtSTyTmt6AbCL9j5ajEDm1
2026-09-14 06:31:29 +00:00

588 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Diagramele pentru summaries/08_ANALIZA_ZILNICA/04_pivoti_si_proiectii.md.
Toate prețurile sunt construite (fără ticker). Rulează:
.venv/bin/python gen/az_pivoti.py
Înlocuiește în .md fiecare bloc <!--SVG:nume--> ... <!--/SVG--> cu output-ul
generatorului. Ca să schimbi o cifră, o schimbi aici și rulezi din nou.
Pivoții sunt „în trepte": fiecare perioadă (lună / trimestru) are nivelurile
ei, calculate din H/L/C ale perioadei anterioare. `PChart` desenează aceste
segmente cu aceeași scară ca a candelelor (`_y`, `_cx` din svg_candles) —
nu există coordonate scrise de mână.
"""
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402
MD = Path("/workspace/atm-curs-dl/summaries/08_ANALIZA_ZILNICA/04_pivoti_si_proiectii.md")
GRAY, RED, GREEN, BLUE, ORANGE, PURPLE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000", "#6a3d9a"
STYLE = dict(gap=15, body_w=9, left=44, width=830)
svgs = {}
@dataclass
class Seg:
"""nivel valabil doar pe barele i0..i1 (o lună, un trimestru)"""
i0: int
i1: int
price: float
text: str
color: str = ORANGE
label_at: str = "out" # "out" marginea dreaptă | "lm" marginea stângă | "left"/"right" pe linie
dy: float = -5.0 # pentru "left"/"right": eticheta deasupra liniei (negativ) sau dedesubt
dash: str = ""
def spread(items, gap=14):
"""[(y, ...)] -> aceleași, cu y-urile împinse ca două etichete să nu se atingă"""
# grupurile care se ating se centrează pe media pozițiilor naturale (nu doar împinse în jos),
# altfel o etichetă care coincide cu linia de deasupra decalează toate etichetele de sub ea
groups = [] # [start_y, [items]]
for it in sorted(items, key=lambda t: t[0]):
groups.append([it[0], [it]])
while len(groups) > 1 and groups[-2][0] + gap * len(groups[-2][1]) > groups[-1][0]:
last = groups.pop()
its = groups[-1][1] + last[1]
groups[-1] = [sum(t[0] - gap * k for k, t in enumerate(its)) / len(its), its]
return [(g[0] + gap * k,) + tuple(t[1:]) for g in groups for k, t in enumerate(g[1])]
@dataclass
class PChart(Chart):
"""Chart cu niveluri pe perioade. Nivelurile (`levels`) devin segmente pe toată seria;
etichetele stau în margini (dreapta / stânga), niciodată peste candele."""
segs: list = field(default_factory=list)
seps: list = field(default_factory=list) # indecși unde începe o perioadă nouă
pad_px: float = 24.0 # spațiu între cel mai sus preț și titlu
lm: int = 104 # marginea stângă pentru etichetele "lm"
pad_bot_px: float = 0.0 # spațiu sub cel mai jos preț, pentru etichetele de sub candele
def render(self) -> str:
n = len(self.candles)
self.left = self.lm if any(s.label_at == "lm" for s in self.segs) else self.left
segs = list(self.segs) + [Seg(0, n - 1, lv.price, lv.text, lv.color, "out", dash="4 3")
for lv in self.levels]
saved_lv, saved_ma = list(self.levels), list(self.mas)
# lățimea: cât să încapă cea mai lungă etichetă din marginea dreaptă
longest = max([len(s.text) for s in segs if s.label_at == "out"] + [len(m.text) for m in saved_ma] + [0])
self.width = max(self.width, int(self._cx(n - 1) + self.gap / 2 + 6 + longest * 6.2 + 8))
top_price = max([c.h for c in self.candles] + [s.price for s in segs])
# prețurile segmentelor intră în scară ca niveluri invizibile
self.levels = [Level(s.price, "", "none") for s in segs]
self.levels.append(Level(top_price + self.pad_px / self.scale, "", "none"))
if self.pad_bot_px:
low_price = min([c.l for c in self.candles] + [s.price for s in segs])
self.levels.append(Level(low_price - self.pad_bot_px / self.scale, "", "none"))
self.mas = [MA(m.period, m.color, m.dash) for m in saved_ma] # eticheta MA o punem noi, în margine
svg = self.render_base()
self.levels, self.mas = saved_lv, saved_ma
parts = svg.split("\n ")
extra = []
for k in self.seps:
x = self._cx(k) - self.gap / 2
extra.append(f'<line x1="{x:.1f}" y1="{self.top - 4:.1f}" x2="{x:.1f}" '
f'y2="{self._body_bottom:.1f}" stroke="#bbb" stroke-dasharray="2 3"/>')
closes = [c.c for c in self.candles]
xr = self._cx(n - 1) + self.gap / 2 + 6
out_lab = [(self._y(s.price) + 4, s) for s in segs if s.label_at == "out"]
out_lab += [(self._y(sum(closes[-m.period:]) / m.period) + 4, Seg(0, 0, 0, m.text, m.color))
for m in saved_ma if m.text]
placed = {id(s): y for y, s in spread(out_lab)}
placed.update({id(s): y for y, s in spread([(self._y(s.price) + 4, s) for s in segs if s.label_at == "lm"])})
for y, s in out_lab: # etichetă mutată de pe linia ei: legătură scurtă până la ea
if abs(placed[id(s)] - y) > 3:
extra.append(f'<line x1="{xr - 6:.1f}" y1="{y - 4:.1f}" x2="{xr - 1:.1f}" '
f'y2="{placed[id(s)] - 4:.1f}" stroke="{s.color}" stroke-width="1"/>')
for s in segs:
x0, x1, y = self._cx(s.i0) - self.gap / 2, self._cx(s.i1) + self.gap / 2, self._y(s.price)
dash = f' stroke-dasharray="{s.dash}"' if s.dash else ""
extra.append(f'<line x1="{x0:.1f}" y1="{y:.1f}" x2="{x1:.1f}" y2="{y:.1f}" '
f'stroke="{s.color}" stroke-width="{1.5 if s.dash else 2}"{dash}/>')
if s.label_at == "out":
extra.append(f'<text x="{xr:.1f}" y="{placed[id(s)]:.1f}" fill="{s.color}">{s.text}</text>')
elif s.label_at == "lm":
extra.append(f'<text x="{x0 - 6:.1f}" y="{placed[id(s)]:.1f}" fill="{s.color}" '
f'text-anchor="end">{s.text}</text>')
elif s.label_at == "left":
extra.append(f'<text x="{x0 + 3:.1f}" y="{y + s.dy:.1f}" fill="{s.color}">{s.text}</text>')
else:
extra.append(f'<text x="{x1 - 3:.1f}" y="{y + s.dy:.1f}" fill="{s.color}" '
f'text-anchor="end">{s.text}</text>')
for y, s in out_lab:
if s.i1 == 0 and s.price == 0: # eticheta de MA
extra.append(f'<text x="{xr:.1f}" y="{placed[id(s)]:.1f}" fill="{s.color}">{s.text}</text>')
k = 2 if self.title else 1
return "\n ".join(parts[:k] + extra + parts[k:])
def render_base(self):
svg = Chart.render(self)
# nivelurile invizibile lasă linii/etichete goale: le scoatem
svg = re.sub(r'\n <line [^>]*stroke="none"[^>]*/>\n <text [^>]*fill="none"[^>]*></text>', "", svg)
prices = [p for c in self.candles for p in (c.o, c.h, c.l, c.c)] + [lv.price for lv in self.levels]
self._body_bottom = self.top + (max(prices) - min(prices)) * self.scale
return svg
def pivots(bars):
"""floor pivots din H/L/C ale perioadei încheiate"""
H, L, C = max(c.h for c in bars), min(c.l for c in bars), bars[-1].c
P = (H + L + C) / 3
return dict(H=H, L=L, C=C, P=P, R1=2 * P - L, S1=2 * P - H, R2=P + (H - L), S2=P - (H - L))
def px_ratio(ch, a, b, c):
"""raportul (c-b)/(b-a) măsurat în pixeli, după render"""
return (ch._y(b) - ch._y(c)) / (ch._y(a) - ch._y(b))
def f2(x):
return f"{x:.2f}"
# ================================================================ 1. calcul + proiecție bull (daily, pivot monthly)
A0, B0, N = 0, 21, 42
PA = 48.00 # pivotul lunii A (calculat din luna dinaintea ei, nu e pe desen)
a_cl = [47.0, 46.8, 47.4, 47.1, 48.0, 48.5, 48.2, 49.1, 49.7, 49.3, 50.2, 50.8, 50.4, 51.3, 51.8, 51.5, 51.9, 51.3,
51.7, 51.1, 51.4]
b_cl = [51.0, 50.6, 50.4, 51.1, 51.8, 52.5, 52.2, 53.0, 53.5, 53.0, 52.7, 53.3, 54.0, 54.6, 54.3, 55.0, 55.5, 54.9,
54.5, 54.9, 55.1]
w = [(0.3, 0.3)] * 21 + [(0.3, 0.3), (0.2, 0.3), (0.3, 0.35), (0.3, 0.3)] + [(0.3, 0.3)] * 17
cds = from_closes(a_cl + b_cl, wicks=w, start=47.3)
pv = pivots(cds[A0:B0])
b = cds[B0:]
assert pv["P"] > PA, "proiecția trebuie să fie bull: pivotul lunii B peste pivotul lunii A"
assert max(c.h for c in b) >= pv["R2"], "în proiecția bull exemplul trebuie să atingă R2"
hit_r1 = B0 + next(i for i, c in enumerate(b) if c.h >= pv["R1"])
hit_r2 = B0 + next(i for i, c in enumerate(b) if c.h >= pv["R2"])
low_b = min(c.l for c in b)
i_low_b = B0 + min(range(len(b)), key=lambda i: b[i].l)
assert abs(low_b - pv["P"]) <= 0.15 and low_b > pv["S1"], "pullback-ul lunii B trebuie să se oprească la pivot, peste S1"
P1 = pv
assert hit_r1 - B0 + 1 == 9 and hit_r2 - B0 + 1 == 17, "textul: R1 în ziua 9, R2 în ziua 17"
print(f"D1 luna A H {f2(pv['H'])} L {f2(pv['L'])} C {f2(pv['C'])} -> P {f2(pv['P'])} R1 {f2(pv['R1'])} "
f"R2 {f2(pv['R2'])} S1 {f2(pv['S1'])}; low B {f2(low_b)} la {i_low_b}; R1 la {hit_r1}, R2 la {hit_r2}")
ch = PChart(candles=cds,
segs=[Seg(A0, B0 - 1, PA, f"pivot A {PA:.2f}", ORANGE, "lm"),
Seg(B0, N - 1, pv["R2"], f"R2 {f2(pv['R2'])}", BLUE),
Seg(B0, N - 1, pv["R1"], f"R1 {f2(pv['R1'])}", BLUE),
Seg(B0, N - 1, pv["P"], f"pivot B {f2(pv['P'])}", ORANGE),
Seg(B0, N - 1, pv["S1"], f"S1 {f2(pv['S1'])}", RED)],
seps=[B0],
marks=[Mark(i_low_b, "ține pivotul", ORANGE, "below", dy=4),
Mark(hit_r1, "R1", BLUE, "above"),
Mark(hit_r2, "R2", BLUE, "above")],
scale=20, title="Exemplu construit (fără ticker) — daily, pivot monthly",
caption=f"Pivotul B = (H {f2(pv['H'])} + L {f2(pv['L'])} + C {f2(pv['C'])}) / 3 = {f2(pv['P'])} > {PA:.2f}: proiecție bull.",
**STYLE)
svgs["bull"] = ch.render()
# ================================================================ 2. proiecție bear + reject sub pivot (short)
PA2 = 52.00
a_cl = [53.0, 53.2, 52.6, 52.9, 52.0, 51.5, 51.8, 50.9, 50.3, 50.7, 49.8, 49.2, 49.6, 48.7, 48.2, 48.5, 48.1, 48.7,
48.3, 48.9, 48.6]
b_cl = [49.0, 49.4, 49.7, 49.0, 48.8, 48.3, 48.6, 47.9, 47.3, 47.6, 46.9, 46.3, 46.7, 46.0, 45.5, 45.8, 45.0, 44.6,
44.9, 44.5, 44.8]
w = [(0.3, 0.3)] * 21 + [(0.3, 0.3), (0.3, 0.3), (0.6, 0.2), (0.2, 0.3)] + [(0.3, 0.3)] * 17
cds = from_closes(a_cl + b_cl, wicks=w, start=52.7)
pv = pivots(cds[A0:B0])
b = cds[B0:]
RJ, TR = B0 + 2, B0 + 3
assert pv["P"] < PA2, "proiecția trebuie să fie bear"
assert cds[RJ].h > pv["P"] and cds[RJ].c < pv["P"], "bara de reject: codiță peste pivot, închidere sub"
assert all(c.h < pv["P"] for c in cds[B0:RJ]), "înainte de reject prețul e sub pivot"
assert cds[TR].c < cds[RJ].l, "trigger-ul short: închidere sub low-ul barei de reject"
entry = cds[TR].c
sl = round(cds[RJ].h + 0.10, 2)
risk = sl - entry
tp = round(entry - 2 * risk, 2)
hit = next(i for i in range(TR + 1, N) if cds[i].l <= tp)
assert min(c.l for c in b) <= pv["S2"], "în proiecția bear exemplul atinge S2"
assert max(c.h for c in cds[TR + 1:]) < pv["P"], "după reject prețul nu mai revine peste pivot"
hit_s1 = next(i for i in range(B0, N) if cds[i].l <= pv["S1"])
hit_s2 = next(i for i in range(B0, N) if cds[i].l <= pv["S2"])
assert hit - B0 + 1 == 12, "textul: TP în a 12-a zi"
print(f"D2 P {f2(pv['P'])} R1 {f2(pv['R1'])} S1 {f2(pv['S1'])} S2 {f2(pv['S2'])}; reject high {f2(cds[RJ].h)} "
f"close {f2(cds[RJ].c)}; entry {f2(entry)} SL {f2(sl)} risc {f2(risk)} TP {f2(tp)} atins la {hit}; S1 {hit_s1} S2 {hit_s2}")
ch = PChart(candles=cds,
segs=[Seg(A0, B0 - 1, PA2, f"pivot A {PA2:.2f}", ORANGE, "lm"),
Seg(B0, N - 1, pv["R1"], f"R1 {f2(pv['R1'])}", BLUE),
Seg(B0, N - 1, pv["P"], f"pivot B {f2(pv['P'])}", ORANGE),
Seg(B0, N - 1, pv["S1"], f"S1 {f2(pv['S1'])}", RED),
Seg(B0, N - 1, pv["S2"], f"S2 {f2(pv['S2'])}", RED)],
seps=[B0],
levels=[Level(sl, f"SL {f2(sl)}", RED, label_at="left"),
Level(entry, f"intrare short {f2(entry)}", GREEN, label_at="left"),
Level(tp, f"TP {f2(tp)} = 2R", BLUE, label_at="left")],
marks=[Mark(RJ, "reject", RED, "above", dy=-4)],
scale=20, title="Exemplu construit (fără ticker) — daily, pivot monthly",
caption=f"Pivotul B {f2(pv['P'])} < pivotul A {PA2:.2f}: proiecție bear. Rejectul sub pivot dă short-ul.",
**STYLE)
svg_ = ch.render()
assert abs(1 / px_ratio(ch, tp, entry, sl) - 2.0) < 0.01, "2R nu e desenat la 2x risc"
svgs["bear"] = svg_
BEAR = dict(pv=pv, entry=entry, sl=sl, tp=tp, risk=risk, rj_h=cds[RJ].h, rj_c=cds[RJ].c, PA=PA2)
# ================================================================ 3-4. pivoți divergenți = trading range
PAd, R1Ad, S1Ad = 50.00, 54.00, 46.00 # nivelurile lunii A (din luna dinaintea ei)
a_cl = [51.6, 52.3, 52.9, 53.2, 52.6, 52.0, 51.3, 50.6, 49.9, 49.2, 48.7, 48.4, 48.9, 49.6, 50.3, 50.9, 51.4, 50.8,
50.2, 50.6, 50.2]
aw = [(0.3, 0.3)] * 21
# 3. NU: luna B rămâne între S1 și R1
b_nu = [50.8, 51.5, 52.2, 52.6, 52.1, 51.4, 50.6, 49.8, 49.0, 48.5, 48.9, 49.7, 50.5, 51.3, 52.0, 52.5, 52.0, 51.2,
50.5, 49.8, 50.3]
cds = from_closes(a_cl + b_nu, wicks=aw + [(0.3, 0.3)] * 21, start=51.0)
pv = pivots(cds[A0:B0])
b = cds[B0:]
assert pv["P"] > PAd and pv["S1"] > S1Ad and pv["R1"] < R1Ad, "pivoții nu sunt divergenți"
assert max(c.h for c in b) < pv["R1"] and min(c.l for c in b) > pv["S1"], "luna B trebuie să rămână în range"
top_b = B0 + max(range(21), key=lambda i: b[i].h)
bot_b = B0 + min(range(21), key=lambda i: b[i].l)
DIV = pv
print(f"D3 A-levels P {PAd} R1 {R1Ad} S1 {S1Ad}; B: P {f2(pv['P'])} R1 {f2(pv['R1'])} S1 {f2(pv['S1'])} R2 {f2(pv['R2'])}; "
f"B high {f2(max(c.h for c in b))} low {f2(min(c.l for c in b))}")
ch = PChart(candles=cds,
segs=[Seg(A0, B0 - 1, R1Ad, f"R1 A {R1Ad:.2f}", BLUE, "lm"),
Seg(A0, B0 - 1, PAd, f"pivot A {PAd:.2f}", ORANGE, "lm"),
Seg(A0, B0 - 1, S1Ad, f"S1 A {S1Ad:.2f}", RED, "lm"),
Seg(B0, N - 1, pv["R1"], f"R1 B {f2(pv['R1'])} (mai jos)", BLUE),
Seg(B0, N - 1, pv["P"], f"pivot B {f2(pv['P'])} (mai sus)", ORANGE),
Seg(B0, N - 1, pv["S1"], f"S1 B {f2(pv['S1'])} (mai sus)", RED)],
seps=[B0],
marks=[Mark(top_b, "nu atinge R1", GRAY, "above", dy=-12),
Mark(bot_b, "nu atinge S1", GRAY, "below", dy=14)],
scale=24, title="Exemplu construit (fără ticker) — daily, pivot monthly",
caption="Suport și pivot mai sus, rezistență mai jos: luna B e un trading range.", **STYLE)
svgs["divergenti_nu"] = ch.render()
# 4. DA: luna B atinge R1 și continuă
b_da = [50.8, 51.4, 52.0, 52.6, 53.4, 53.9, 53.6, 53.4, 53.9, 54.3, 54.9, 54.6, 55.2, 55.7, 55.4, 56.0, 56.5, 56.2,
56.6, 56.3, 56.8]
bw = [(0.3, 0.3)] * 7 + [(0.2, 0.3), (0.3, 0.2)] + [(0.3, 0.3)] * 12
cds = from_closes(a_cl + b_da, wicks=aw + bw, start=51.0)
pv = pivots(cds[A0:B0])
BO, RT, CF = B0 + 4, B0 + 7, B0 + 8
assert abs(pv["P"] - DIV["P"]) < 1e-9, "luna A trebuie să fie aceeași ca la diagrama NU"
assert all(c.c <= pv["R1"] for c in cds[B0:BO]) and cds[BO].c > pv["R1"], "bara 25 e prima închidere peste R1 B"
assert abs(cds[RT].l - pv["R1"]) <= 0.12 and cds[RT].c > pv["R1"], "retestul trebuie să atingă R1 B și să închidă peste"
assert cds[CF].c > cds[RT].h, "candela din dreapta confirmă retestul"
entry = cds[CF].c
sl = round(cds[RT].l - 0.20, 2)
risk = entry - sl
tp = round(pv["R2"], 2)
R = (tp - entry) / risk
hit = next(i for i in range(CF + 1, N) if cds[i].h >= tp)
hit_r1a = next(i for i in range(CF, N) if cds[i].h >= R1Ad)
assert R >= 2, R
print(f"D4 breakout {f2(cds[BO].c)} retest low {f2(cds[RT].l)} entry {f2(entry)} SL {f2(sl)} risc {f2(risk)} "
f"TP R2 {f2(tp)} = {R:.2f}R atins la {hit}; R1 A atins la {hit_r1a}")
DIVT = dict(entry=entry, sl=sl, tp=tp, R=R, bo=cds[BO].c, rt=cds[RT].l, risk=risk)
ch = PChart(candles=cds,
segs=[Seg(A0, B0 - 1, R1Ad, f"R1 A {R1Ad:.2f}", BLUE, "lm"),
Seg(A0, B0 - 1, PAd, f"pivot A {PAd:.2f}", ORANGE, "lm"),
Seg(A0, B0 - 1, S1Ad, f"S1 A {S1Ad:.2f}", RED, "lm"),
Seg(B0, N - 1, pv["R1"], f"R1 B {f2(pv['R1'])}", BLUE),
Seg(B0, N - 1, pv["P"], f"pivot B {f2(pv['P'])}", ORANGE),
Seg(B0, N - 1, pv["S1"], f"S1 B {f2(pv['S1'])}", RED)],
seps=[B0],
levels=[Level(sl, f"SL {f2(sl)}", RED, label_at="left"),
Level(entry, f"intrare {f2(entry)}", GREEN, label_at="left"),
Level(tp, f"TP = R2 B {f2(tp)} = {R:.1f}R", BLUE, label_at="left")],
marks=[Mark(BO, "close peste R1 B", GREEN, "above", dy=-18),
Mark(RT, "retest", ORANGE, "below", dy=4)],
scale=24, title="Exemplu construit (fără ticker) — daily, pivot monthly",
caption=f"Aceeași lună A. De data asta luna B atinge R1 ({f2(pv['R1'])}) și merge spre R2.", **STYLE)
svg_ = ch.render()
assert abs(px_ratio(ch, sl, entry, tp) - R) < 0.01
svgs["divergenti_da"] = svg_
# ================================================================ 5-6. proiecție „inside" = compresie
PAi, R1Ai, S1Ai = 50.00, 55.00, 45.00
a_cl = [50.6, 51.1, 51.5, 51.0, 50.4, 49.9, 49.6, 50.1, 50.7, 51.2, 51.4, 50.9, 50.3, 49.8, 50.2, 50.8, 51.2, 50.7,
50.3, 50.6, 50.9]
aw = [(0.25, 0.25)] * 21
av = [52, 48, 50, 46, 44, 47, 45, 42, 44, 43, 40, 42, 41, 38, 40, 39, 37, 38, 36, 37, 35]
# 5. NU: luna B stă în compresie
b_nu = [50.5, 51.0, 51.5, 51.2, 50.6, 50.2, 50.5, 50.9, 51.3, 51.0, 50.6, 50.4, 50.7, 51.1, 50.9, 50.6, 50.5, 50.8,
51.0, 50.8, 50.7]
bv = [34, 33, 35, 31, 30, 32, 29, 28, 30, 27, 26, 27, 25, 26, 24, 25, 23, 24, 22, 23, 22]
cds = from_closes(a_cl + b_nu, wicks=aw + [(0.2, 0.2)] * 21, vols=av + bv, start=50.2)
pv = pivots(cds[A0:B0])
b = cds[B0:]
wa, wb = R1Ai - S1Ai, pv["R1"] - pv["S1"]
assert abs(wb - (pv["H"] - pv["L"])) < 1e-9, "R1 - S1 = H - L al lunii anterioare"
assert wb / wa < 0.35, "proiecția B nu e inside față de A"
assert max(c.h for c in b) <= pv["R1"] and min(c.l for c in b) >= pv["S1"], "luna B trebuie să rămână între S1 și R1"
INS = pv
print(f"D5 A: H {f2(pv['H'])} L {f2(pv['L'])} C {f2(pv['C'])} -> P {f2(pv['P'])} R1 {f2(pv['R1'])} S1 {f2(pv['S1'])}; "
f"lățime A {wa:.2f} B {wb:.2f} ({wb / wa:.0%})")
ch = PChart(candles=cds,
segs=[Seg(A0, B0 - 1, R1Ai, f"R1 A {R1Ai:.2f}", BLUE, "lm"),
Seg(A0, B0 - 1, PAi, f"pivot A {PAi:.2f}", ORANGE, "lm"),
Seg(A0, B0 - 1, S1Ai, f"S1 A {S1Ai:.2f}", RED, "lm"),
Seg(B0, N - 1, pv["R1"], f"R1 B {f2(pv['R1'])}", BLUE),
Seg(B0, N - 1, pv["S1"], f"S1 B {f2(pv['S1'])}", RED)],
seps=[B0],
marks=[Mark(10, "luna A: range îngust", GRAY, "above", dy=-24),
Mark(B0 + 10, "luna B: compresie", GRAY, "below", dy=30)],
scale=22, vol_h=70, title="Exemplu construit (fără ticker) — daily, pivot monthly",
caption=f"R1 − S1 = H − L al lunii trecute: {wb:.2f} față de {wa:.2f}. Proiecție inside, volum în scădere.",
**STYLE)
svgs["inside_nu"] = ch.render()
# 6. DA: din compresie, breakout cu volum până la rezistența lunii trecute
b_da = [50.5, 51.0, 51.5, 51.1, 50.6, 50.2, 50.7, 51.3, 51.4, 51.3, 51.6, 52.3, 53.0, 53.5, 53.2, 53.9, 54.4, 54.1,
54.7, 55.2, 55.0]
bw = [(0.2, 0.2)] * 7 + [(0.15, 0.15)] * 4 + [(0.1, 0.2)] + [(0.3, 0.3)] * 9
bv = [34, 33, 35, 31, 30, 32, 29, 28, 27, 26, 30, 92, 70, 64, 48, 60, 58, 44, 55, 57, 45]
cds = from_closes(a_cl + b_da, wicks=aw + bw, vols=av + bv, start=50.2)
pv = pivots(cds[A0:B0])
BK = B0 + 11
assert abs(pv["P"] - INS["P"]) < 1e-9
assert all(c.c <= pv["R1"] for c in cds[B0:BK]) and cds[BK].c > pv["R1"], "bara 32 e prima închidere peste R1 B"
assert bv[11] >= 2.5 * sum(bv[1:11]) / 10, "breakout-ul trebuie să aibă volum clar peste media compresiei"
entry = cds[BK].c
hl = min(c.l for c in cds[BK - 3:BK])
sl = round(hl - 0.10, 2)
risk = entry - sl
tp = R1Ai
R = (tp - entry) / risk
hit = next(i for i in range(BK + 1, N) if cds[i].h >= tp)
assert R >= 2, R
assert min(c.l for c in cds[BK + 1:]) > sl
assert hit - B0 + 1 == 19 and BK - B0 + 1 == 12, "textul: breakout ziua 12, TP ziua 19"
assert all(abs(cds[i].l - 51.15) < 0.01 for i in range(BK - 3, BK)), "textul: ultimele trei low-uri la 51.15"
print(f"D6 breakout close {f2(entry)} vol {bv[11]} vs medie {sum(bv[1:11]) / 10:.0f}; HL {f2(hl)} SL {f2(sl)} "
f"risc {f2(risk)} TP {f2(tp)} = {R:.2f}R atins la {hit}")
INST = dict(entry=entry, sl=sl, tp=tp, R=R, risk=risk, vol=bv[11], vavg=sum(bv[1:11]) / 10)
ch = PChart(candles=cds,
segs=[Seg(A0, B0 - 1, PAi, f"pivot A {PAi:.2f}", ORANGE, "lm"),
Seg(A0, B0 - 1, S1Ai, f"S1 A {S1Ai:.2f}", RED, "lm"),
Seg(B0, N - 1, pv["R1"], f"R1 B {f2(pv['R1'])}", PURPLE),
Seg(B0, N - 1, pv["S1"], f"S1 B {f2(pv['S1'])}", RED)],
seps=[B0],
levels=[Level(sl, f"SL {f2(sl)}", RED, label_at="left"),
Level(entry, f"intrare {f2(entry)}", GREEN, label_at="left"),
Level(tp, f"TP = R1 lunii trecute {f2(tp)} = {R:.1f}R", BLUE, label_at="left")],
marks=[Mark(BK, "breakout cu volum", GREEN, "above", dy=-30)],
scale=22, vol_h=70, title="Exemplu construit (fără ticker) — daily, pivot monthly",
caption="Aceeași compresie; volumul de 3x pe breakout o scoate până la rezistența lunii trecute.",
**STYLE)
svg_ = ch.render()
assert abs(px_ratio(ch, sl, entry, tp) - R) < 0.01
svgs["inside_da"] = svg_
# ================================================================ 7. reject deasupra pivotului (suport) — long
PIV = 50.00
cl7 = [55.8, 55.2, 55.5, 54.7, 54.1, 54.4, 53.6, 53.0, 53.2, 52.4, 51.8, 51.1, # 0-11 pullback spre pivot
50.5, 50.3, 50.7, 50.4, 50.6, # 12-16 rejecturi pe pivot
50.65, # 17 doji
51.6, # 18 buy doji
52.3, 52.0, 52.8, 53.5, 53.2, 54.0, 54.6, 54.3, 55.0, 55.6, 55.3, 56.0] # 19-30
w7 = [(0.3, 0.3)] * 12 + [(0.2, 0.7), (0.2, 0.6), (0.2, 0.8), (0.2, 0.5), (0.2, 0.6), (0.5, 0.5), (0.2, 0.2)] \
+ [(0.3, 0.3)] * 12
cds = from_closes(cl7, wicks=w7, start=56.2)
DJ, BD = 17, 18
rej = range(12, 17)
assert all(cds[i].l < PIV and cds[i].c > PIV for i in rej), "fiecare reject: codiță sub pivot, închidere peste"
assert abs(cds[DJ].o - cds[DJ].c) <= 0.1 and cds[BD].c > cds[DJ].h, "buy doji după doji"
entry = cds[BD].c
sl = round(min(cds[i].l for i in range(12, BD + 1)) - 0.10, 2)
risk = entry - sl
tp = round(entry + 2 * risk, 2)
hit = next(i for i in range(BD + 1, len(cds)) if cds[i].h >= tp)
assert min(c.l for c in cds[BD + 1:]) > PIV
assert hit - BD == 12, "textul: TP după 12 săptămâni"
print(f"D7 rejecturi lows {[f2(cds[i].l) for i in rej]}; entry {f2(entry)} SL {f2(sl)} risc {f2(risk)} TP {f2(tp)} atins la {hit}")
REJL = dict(entry=entry, sl=sl, tp=tp, risk=risk, low=min(cds[i].l for i in rej))
ch = PChart(candles=cds,
levels=[Level(PIV, f"pivot quarterly {PIV:.2f}", ORANGE, label_at="right"),
Level(sl, f"SL {f2(sl)}", RED, label_at="left"),
Level(entry, f"intrare {f2(entry)}", GREEN, label_at="left"),
Level(tp, f"TP {f2(tp)} = 2R", BLUE, label_at="left")],
marks=[Mark(4, "pullback", GRAY, "below", dy=8),
Mark(14, "5 rejecturi", ORANGE, "above", dy=-16),
Mark(BD, "buy doji", GREEN, "above", dy=-4),
Mark(hit, "TP", BLUE, "above")],
scale=22, pad_bot_px=14, title="Exemplu construit (fără ticker) — weekly, pivot quarterly",
caption="Codițele coboară sub pivot, close-urile rămân deasupra: pivotul e apărat.", **STYLE)
svg_ = ch.render()
assert abs(px_ratio(ch, sl, entry, tp) - 2.0) < 0.01
svgs["reject_deasupra"] = svg_
# ================================================================ 8. reject sub pivot -> cumperi mai jos, în suportul quarterly
PM, PQ = 52.00, 48.00 # pivot monthly, pivot quarterly
cl8 = [49.4, 49.9, 50.6, 51.2, 51.7, # 0-4 urcare spre pivotul monthly
51.5, 51.8, 51.4, 51.6, # 5-8 rejecturi sub pivot
50.9, 50.3, 50.6, 49.8, 49.2, 49.5, 48.4, # 9-15 cădere
48.5, 48.55, # 16 hammer, 17 doji în pivotul quarterly
49.1, # 18 buy doji
49.6, 49.4, 50.2, 50.8, 50.5, 51.3, 51.9, 51.6, 52.3, 52.6, 52.4, 53.0] # 19-30
w8 = [(0.3, 0.3)] * 5 + [(0.6, 0.2), (0.5, 0.2), (0.7, 0.2), (0.5, 0.2)] + [(0.3, 0.3)] * 7 \
+ [(0.1, 0.4), (0.35, 0.3), (0.2, 0.2)] + [(0.3, 0.3)] * 12
v8 = [40, 42, 45, 44, 41, 38, 36, 40, 35, 50, 55, 42, 58, 60, 44, 62, 70, 48, 66, 52, 40, 55, 58, 42, 54, 57, 40, 52, 50, 41, 49]
cds = from_closes(cl8, wicks=w8, vols=v8, start=49.0)
HM, DJ, BD = 16, 17, 18
assert all(cds[i].h > PM and cds[i].c < PM for i in range(5, 9)), "rejecturi: codiță peste pivotul monthly, close sub"
assert abs(cds[HM].l - PQ) <= 0.1 and cds[HM].c > PQ, "hammer-ul atinge pivotul quarterly și închide peste"
assert abs(cds[DJ].o - cds[DJ].c) <= 0.1 and cds[BD].c > cds[DJ].h
entry = cds[BD].c
sl = round(min(c.l for c in cds[HM:BD + 1]) - 0.20, 2)
risk = entry - sl
tp1 = PM
R1m = (tp1 - entry) / risk
tp2 = round(entry + 3 * risk, 2)
hit1 = next(i for i in range(BD + 1, len(cds)) if cds[i].h >= tp1)
hit2 = next(i for i in range(BD + 1, len(cds)) if cds[i].h >= tp2)
assert R1m >= 2, R1m
assert min(c.l for c in cds[BD + 1:]) > sl
assert v8[HM] > max(v8[9:HM]), "textul: hammer cu cel mai mare volum de la cădere încoace"
print(f"D8 rejecturi highs {[f2(cds[i].h) for i in range(5, 9)]}; hammer low {f2(cds[HM].l)}; entry {f2(entry)} SL {f2(sl)} "
f"risc {f2(risk)} TP1 {f2(tp1)} = {R1m:.2f}R la {hit1}; TP2 {f2(tp2)} = 3R la {hit2}")
REJS = dict(entry=entry, sl=sl, tp1=tp1, R1=R1m, tp2=tp2, risk=risk, hm=cds[HM].l)
ch = PChart(candles=cds,
levels=[Level(tp2, f"TP2 {f2(tp2)} = 3R", BLUE, label_at="left"),
Level(PM, f"pivot monthly {PM:.2f} = TP1 ({R1m:.1f}R)", ORANGE, label_at="left"),
Level(entry, f"intrare {f2(entry)}", GREEN, label_at="right"),
Level(PQ, f"pivot quarterly {PQ:.2f}", PURPLE, label_at="right"),
Level(sl, f"SL {f2(sl)}", RED, label_at="right")],
marks=[Mark(6, "reject sub pivot: NU", RED, "above", dy=-16),
Mark(HM, "hammer", PURPLE, "below", dy=0),
Mark(BD, "buy doji", GREEN, "above", dy=-6)],
scale=26, pad_bot_px=12, vol_h=70, title="Exemplu construit (fără ticker) — daily",
caption="Nu cumperi rejectul de sub pivotul monthly. Cumperi mai jos, în pivotul quarterly.", **STYLE)
svg_ = ch.render()
assert abs(px_ratio(ch, sl, entry, tp2) - 3.0) < 0.01
svgs["reject_sub"] = svg_
# ================================================================ 9. respins de 5 ori sub pivot, apoi închidere deasupra cu doji
PQ9 = 50.00
cl9 = [47.2, 46.6, 46.9, 46.2, 45.6, 45.9, 45.3, # 0-6 downtrend
45.8, 46.6, 47.4, 48.3, 49.1, # 7-11 revenire spre pivot
49.6, 49.3, 49.7, 49.4, 49.8, 49.5, # 12-17 rejecturi sub pivot (5 cu codiță peste)
50.35, 50.40, # 18 close peste, 19 doji peste
51.2, # 20 buy doji
51.0, 51.7, 52.3, 52.0, 52.8, 53.4, 53.1, 53.9, 54.4, 54.1, 54.8] # 21-31
w9 = [(0.3, 0.3)] * 12 + [(0.6, 0.2), (0.3, 0.2), (0.6, 0.2), (0.45, 0.2), (0.6, 0.2), (0.3, 0.2)] \
+ [(0.2, 0.3), (0.35, 0.3), (0.2, 0.2)] + [(0.3, 0.3)] * 11
cds = from_closes(cl9, wicks=w9, start=47.6)
rj = [i for i in range(12, 18) if cds[i].h > PQ9 and cds[i].c < PQ9]
assert len(rj) == 5, rj
assert all(c.c < PQ9 for c in cds[:18]), "până la bara 18 niciun close peste pivot"
X, DJ, BD = 18, 19, 20
assert cds[X].c > PQ9 and cds[DJ].c > PQ9 and abs(cds[DJ].o - cds[DJ].c) <= 0.1 and cds[BD].c > cds[DJ].h
entry = cds[BD].c
sl = round(min(cds[DJ].l, PQ9) - 0.25, 2) # sub doji și sub pivot
risk = entry - sl
tp = round(entry + 2 * risk, 2)
hit = next(i for i in range(BD + 1, len(cds)) if cds[i].h >= tp)
assert min(c.l for c in cds[BD + 1:]) > PQ9
assert hit - BD == 8, "textul: TP după 8 săptămâni"
print(f"D9 rejecturi la {rj}; close peste {f2(cds[X].c)}, doji {f2(cds[DJ].c)}; entry {f2(entry)} SL {f2(sl)} "
f"risc {f2(risk)} TP {f2(tp)} atins la {hit}")
BRK = dict(entry=entry, sl=sl, tp=tp, risk=risk)
ch = PChart(candles=cds,
levels=[Level(PQ9, f"pivot quarterly {PQ9:.2f}", ORANGE, label_at="left"),
Level(sl, f"SL {f2(sl)}", RED, label_at="right"),
Level(entry, f"intrare {f2(entry)}", GREEN, label_at="right"),
Level(tp, f"TP {f2(tp)} = 2R", BLUE, label_at="left")],
marks=[Mark(3, "downtrend", GRAY, "above", dy=-10),
Mark(12, "5 rejecturi sub pivot", RED, "above", dy=-40),
Mark(BD, "doji + buy doji", GREEN, "above", dy=-4)],
scale=24, title="Exemplu construit (fără ticker) — weekly, pivot quarterly",
caption="Cinci respingeri, apoi două close-uri peste pivot și buy doji: acum pivotul e suport.", **STYLE)
svg_ = ch.render()
assert abs(px_ratio(ch, sl, entry, tp) - 2.0) < 0.01
svgs["reject_apoi_peste"] = svg_
# ================================================================ 10-11. scărița pe pivoți (weekly, pivot quarterly)
Q = 13
PQ1 = 44.00
q1 = [45.6, 45.2, 45.8, 46.5, 47.2, 48.0, 48.6, 48.2, 47.6, 47.9, 48.8, 49.4, 49.0]
q2 = [48.7, 48.4, 48.9, 49.6, 50.3, 51.0, 51.5, 51.0, 50.4, 50.1, 50.6, 51.3, 51.1]
q3_da = [50.8, 50.7, 51.1, 51.7, 52.3, 51.9, 52.6, 53.3, 53.0, 53.8, 54.4, 54.1, 54.9]
q3_nu = [50.8, 50.7, 51.1, 51.4, 51.2, 50.6, 50.1, 49.6, 49.9, 49.2, 48.7, 49.0, 48.4]
wq = [(0.3, 0.3)] * 39
wq[14] = (0.3, 0.3) # low-ul trimestrului 2
wq[22] = (0.3, 0.4) # higher low în trimestrul 2
wq[27] = (0.3, 0.3) # higher low în trimestrul 3
def stairs(q3):
cds = from_closes(q1 + q2 + q3, wicks=wq, start=46.0)
p2 = pivots(cds[0:Q])
p3 = pivots(cds[Q:2 * Q])
return cds, p2, p3
cds, p2, p3 = stairs(q3_da)
lows = [min(range(0, Q), key=lambda i: cds[i].l),
min(range(Q, 18), key=lambda i: cds[i].l),
min(range(20, 2 * Q), key=lambda i: cds[i].l),
min(range(2 * Q, 30), key=lambda i: cds[i].l)]
hh = [max(range(0, Q), key=lambda i: cds[i].h), max(range(Q, 2 * Q), key=lambda i: cds[i].h)]
assert PQ1 < p2["P"] < p3["P"], "pivoții quarterly nu urcă în trepte"
assert all(cds[lows[k]].l < cds[lows[k + 1]].l for k in range(3)), "swing low-urile nu urcă"
assert cds[lows[1]].l > p2["P"] and cds[lows[2]].l > p2["P"] and cds[lows[3]].l > p3["P"] - 0.05, "HL sub pivotul trimestrului"
assert cds[lows[3]].l < p3["P"] + 0.5
brk = next(i for i in range(2 * Q, 39) if cds[i].c > cds[hh[1]].h)
assert cds[hh[1]].h > cds[hh[0]].h
print(f"D10 pivoti Q1 {PQ1} Q2 {f2(p2['P'])} Q3 {f2(p3['P'])}; lows {[f2(cds[i].l) for i in lows]} la {lows}; "
f"high Q1 {f2(cds[hh[0]].h)} Q2 {f2(cds[hh[1]].h)}; breakout la {brk} close {f2(cds[brk].c)}")
STAIR = dict(p2=p2, p3=p3, lows=[cds[i].l for i in lows], hq1=cds[hh[0]].h, hq2=cds[hh[1]].h, brk=cds[brk].c)
ch = PChart(candles=cds,
segs=[Seg(0, Q - 1, PQ1, f"pivot Q1 {PQ1:.2f}", ORANGE, "lm"),
Seg(Q, 2 * Q - 1, p2["P"], f"pivot Q2 {f2(p2['P'])}", ORANGE, "left", 14),
Seg(2 * Q, 38, p3["P"], f"pivot Q3 {f2(p3['P'])}", ORANGE)],
seps=[Q, 2 * Q],
marks=[Mark(lows[0], "low", RED, "above", dy=-2),
Mark(lows[1], "HL", GREEN, "above", dy=-2),
Mark(lows[2], "HL", GREEN, "above", dy=-2),
Mark(lows[3], "HL", GREEN, "above", dy=-2),
Mark(hh[1], "vârf Q2", BLUE, "above"),
Mark(brk, "breakout", GREEN, "above", dy=-4)],
mas=[MA(20, GRAY, dash="4 3", text="SMA-20")],
scale=18, title="Exemplu construit (fără ticker) — weekly, pivot quarterly",
caption="Treaptă cu treaptă: pivotul fiecărui trimestru mai sus, fiecare higher low deasupra lui.", **STYLE)
svgs["scarita_da"] = ch.render()
cds, p2n, p3n = stairs(q3_nu)
assert abs(p3n["P"] - p3["P"]) < 1e-9, "Q1 și Q2 identice cu diagrama DA"
assert max(c.h for c in cds[2 * Q:]) < STAIR["hq2"], "breakout-ul peste vârful Q2 nu vine"
hl2 = STAIR["lows"][2]
ll = next(i for i in range(2 * Q, 39) if cds[i].c < hl2)
top3 = max(range(2 * Q, 39), key=lambda i: cds[i].h)
assert cds[ll].c < p3n["P"]
under = next(i for i in range(2 * Q, 39) if cds[i].c < p3n["P"])
print(f"D11 high Q3 {f2(max(c.h for c in cds[2 * Q:]))} < vârf Q2 {f2(STAIR['hq2'])}; close sub pivot Q3 la {under} "
f"({f2(cds[under].c)}); close sub HL {f2(hl2)} la {ll} ({f2(cds[ll].c)})")
STAIRN = dict(h3=max(c.h for c in cds[2 * Q:]), under=cds[under].c, ll=cds[ll].c, hl2=hl2)
ch = PChart(candles=cds,
segs=[Seg(0, Q - 1, PQ1, f"pivot Q1 {PQ1:.2f}", ORANGE, "lm"),
Seg(Q, 2 * Q - 1, p2n["P"], f"pivot Q2 {f2(p2n['P'])}", ORANGE, "left", 14),
Seg(2 * Q, 38, p3n["P"], f"pivot Q3 {f2(p3n['P'])}", ORANGE)],
seps=[Q, 2 * Q],
levels=[Level(STAIR["hq2"], f"vârf Q2 {f2(STAIR['hq2'])}", BLUE, label_at="left"),
Level(hl2, f"HL din Q2 {f2(hl2)}", RED, label_at="left")],
marks=[Mark(top3, "fără breakout", GRAY, "above", dy=-4),
Mark(ll, "lower low", RED, "below", dy=4)],
mas=[MA(20, GRAY, dash="4 3", text="SMA-20")],
scale=18, title="Exemplu construit (fără ticker) — weekly, pivot quarterly",
caption="Pivotul Q3 a urcat, dar prețul nu face vârf nou și închide sub HL-ul din Q2: scărița s-a rupt.",
**STYLE)
svgs["scarita_nu"] = ch.render()
# ---------------------------------------------------------------- substituție în .md
if MD.exists():
md = MD.read_text(encoding="utf-8")
for name, svg in svgs.items():
pat = re.compile(rf"<!--SVG:{name}-->.*?<!--/SVG-->", re.S)
assert pat.search(md), f"marcatorul {name} lipsește din .md"
md = pat.sub(lambda _m: f"<!--SVG:{name}-->\n{svg}\n<!--/SVG-->", md)
MD.write_text(md, encoding="utf-8")
print("scris", MD)