Files
atm-curs-dl/gen/strategii/mp2_pivoti_excel.py
Claude Agent 2ea599f265 02_mp2: capitol reconstruit dupa sablon
Nivelurile 1-2-3, cei 6 pasi cu diagrama fiecare, mini-glosar de 34 de
termeni, oglinda pe short cu diagrama ei proprie, checklist. Sursele in
fise/02_mp2_surse.md.

Toate diagramele procedurii ies acum dintr-o singura serie
(mp2_serie.py), deci capitolul nu mai poate avea doua reguli numerice.
Ansamblul folosea NVDA cu stop la swing low absolut (-23 %), adica alta
regula decat procedura celor 4 bare; NVDA a devenit "bottom", la nivelul
3, marcat explicit ca exceptie.

perechi_tf e diagrama cu doua timeframe-uri: panoul mic e descompunerea
reala a ultimelor 2 candele daily (1 zi = 7 bare de 60 min), cu pivotul
weekly pe ambele panouri si conector. Pivotii se calculeaza din serie, nu
sunt scrisi de mana.

Reparate: trail-ul din proza era 600.05, calculul din serie da 600.75;
doua SVG-uri aveau trimiteri fisier:linie desenate pe ele.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-20 12:51:58 +00:00

313 lines
14 KiB
Python

#!/usr/bin/env python3
"""Foaie de Excel stilizată: pivoții clasici calculați din OHLC, pas cu pas,
și comparația PP zilnic vs. PP săptămânal (crossing).
Pentru `strategii/02_mp2.md`, Transfer — Pe BVB, subsecțiunea „Pivoții în
Excel". Spec: `gen/strategii/mp2_pivoti_excel.spec.txt`.
Nu e grafic de preț (TIP: CONCEPT) — SVG scris de mână, doar stdlib; nu se
folosește svg_candles / pchart. Nu sunt candele, e un tabel.
Ce arată:
- titlul „Pivoții clasici în Excel — formula, pas cu pas";
- un tabel cu coloanele Perioada | High | Low | Close | PP | R1 | S1 | R2 | S2,
cu literele de coloană (A..I) și numerele de rând Excel (1..3), ca formulele
=(B2+C2+D2)/3 etc. să aibă sens;
- două rânduri de date: „Săptămâna încheiată (PPW)" și „Ziua încheiată (PPD)",
fiecare celulă de calcul arătând formula ȘI rezultatul;
- sub tabel, rândul de comparație evidențiat:
„PPD 21.57 > PPW 21.37 → CROSSING BULL (long)";
- legenda că cifrele sunt un exemplu construit, pe TLV, nu date reale.
Valorile și formulele vin din spec (CIFRE ȘI FORMULE); toate assert-urile
cerute de spec sunt scrise ca assert reale mai jos.
Rulare: python3 gen/strategii/mp2_pivoti_excel.py -> mp2_pivoti_excel.svg
"""
from pathlib import Path
from xml.sax.saxutils import escape, unescape
OUT = Path(__file__).with_suffix(".svg")
W, H = 700, 444
BLACK, GRAY, BLUE, GREEN, RED, ORANGE, WHITE = (
"#000000", "#777777", "#1a5fb4", "#188a3e", "#c00", "#b06000", "#ffffff")
BORDER = "#333333"
GRID = "#c9d2dc"
HDR_BG = "#eef1f5"
ALT_BG = "#fafbfc"
# ---------------------------------------------------------------- date (spec)
# Săptămâna încheiată: H=22.00, L=20.50, C=21.60 (row 2)
# Ziua încheiată: H=21.90, L=21.00, C=21.80 (row 3)
WH = (22.00, 20.50, 21.60)
DH = (21.90, 21.00, 21.80)
PPW = (WH[0] + WH[1] + WH[2]) / 3
PPD = (DH[0] + DH[1] + DH[2]) / 3
# derivate standard: R1=2*PP-Low, S1=2*PP-High, R2=PP+(High-Low), S2=PP-(High-Low)
W_R1, W_S1 = 2 * PPW - WH[1], 2 * PPW - WH[0]
W_R2, W_S2 = PPW + (WH[0] - WH[1]), PPW - (WH[0] - WH[1])
D_R1, D_S1 = 2 * PPD - DH[1], 2 * PPD - DH[0]
D_R2, D_S2 = PPD + (DH[0] - DH[1]), PPD - (DH[0] - DH[1])
# ---------------------------------------------------------------- asserturi spec
assert abs((22.00 + 20.50 + 21.60) / 3 - 21.3667) < 0.001, "PPW din formulă"
assert abs((21.90 + 21.00 + 21.80) / 3 - 21.5667) < 0.001, "PPD din formulă"
assert abs(PPW - 21.3667) < 0.001 and abs(PPD - 21.5667) < 0.001
assert PPD > PPW, "PPD trebuie deasupra PPW pentru crossing bull"
# R1/S1/R2/S2 ies din formulă, nu din cap (daily: 2*21.5667-21.00=22.1333 etc.)
assert abs(D_R1 - (2 * 21.5667 - 21.00)) < 0.001, "daily R1"
assert abs(D_S1 - (2 * 21.5667 - 21.90)) < 0.001, "daily S1"
assert abs(D_R2 - (21.5667 + (21.90 - 21.00))) < 0.001, "daily R2"
assert abs(D_S2 - (21.5667 - (21.90 - 21.00))) < 0.001, "daily S2"
# ATENȚIE: spec, linia 29, listează daily R2 = 22.5667. Formula dată chiar de
# spec (R2 = PP + (High-Low), linia 21) și datele ei (H=21.90, L=21.00)
# dau 22.4667: 21.5667 + 0.90. S2 din spec (20.6667) confirmă H-L = 0.90.
# Assert-ul din spec („R2 iese din formulă, nu din cap") decide: desenăm 22.47.
assert abs(D_R2 - 22.4667) < 0.001, "daily R2 trebuie derivat din formulă"
assert abs(W_R1 - (2 * 21.3667 - 20.50)) < 0.001, "weekly R1"
assert abs(W_S1 - (2 * 21.3667 - 22.00)) < 0.001, "weekly S1"
assert abs(W_R2 - (21.3667 + (22.00 - 20.50))) < 0.001, "weekly R2"
assert abs(W_S2 - (21.3667 - (22.00 - 20.50))) < 0.001, "weekly S2"
# ---------------------------------------------------------------- geometrie
LEFT, RIGHT = 8.0, 692.0
GUT_X, GUT_W = LEFT, 20.0
COLS = [(28.0, 116.0), (116.0, 164.0), (164.0, 212.0), (212.0, 260.0),
(260.0, 372.0), (372.0, 440.0), (440.0, 508.0),
(508.0, 600.0), (600.0, 692.0)]
HEADERS = ["Perioada", "High", "Low", "Close", "PP", "R1", "S1", "R2", "S2"]
LETTERS = ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
TBL_Y0, HDR_Y1 = 72.0, 116.0
ROW1_Y0, ROW1_Y1 = 116.0, 184.0 # săptămâna (rândul 2)
ROW2_Y0, ROW2_Y1 = 184.0, 252.0 # ziua (rândul 3)
CMP_Y0, CMP_Y1 = 268.0, 312.0
TEXTS = [] # (x, y, s, size, weight, anchor, cell) cell=(x0,x1) sau None
def txt(x, y, s, size=13, weight="400", fill=BLACK, anchor="middle", cell=None):
TEXTS.append((x, y, s, size, weight, anchor, cell))
return (f'<text x="{x:.1f}" y="{y:.1f}" font-size="{size}" '
f'font-weight="{weight}" fill="{fill}" text-anchor="{anchor}">'
f'{escape(s)}</text>')
def line(x1, y1, x2, y2, stroke=GRID, width=1.0, dash=None):
d = f' stroke-dasharray="{dash}"' if dash else ""
return (f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
f'stroke="{stroke}" stroke-width="{width}"{d}/>')
def rect(x, y, w, h, fill=WHITE, stroke=BORDER, width=1.2, rx=0):
r = f' rx="{rx}"' if rx else ""
return (f'<rect x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}"'
f'{r} fill="{fill}" stroke="{stroke}" stroke-width="{width}"/>')
def fmt(v):
return f"{v:.2f}"
def build():
p = [
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" '
f'width="{W}" height="{H}" role="img" aria-label="Tabel Excel: '
f'pivotii clasici calculati din OHLC, PP saptamanal vs PP zilnic, '
f'crossing bull">',
'<g font-family="system-ui, sans-serif">',
f'<rect x="0" y="0" width="{W}" height="{H}" fill="{WHITE}"/>',
txt(W / 2, 38, "Pivoții clasici în Excel — formula, pas cu pas",
20, "700", BLACK),
]
# ------- fundaluri: bandă de antet, rânduri alternate, gutter
p.append(rect(GUT_X, TBL_Y0, GUT_W,
ROW2_Y1 - TBL_Y0, fill=HDR_BG, stroke=BORDER, width=1.0))
p.append(rect(COLS[0][0], TBL_Y0, RIGHT - COLS[0][0], HDR_Y1 - TBL_Y0,
fill=HDR_BG, stroke=BORDER, width=1.0))
p.append(rect(COLS[0][0], ROW1_Y0, RIGHT - COLS[0][0], ROW1_Y1 - ROW1_Y0,
fill=WHITE, stroke=BORDER, width=1.0))
p.append(rect(COLS[0][0], ROW2_Y0, RIGHT - COLS[0][0], ROW2_Y1 - ROW2_Y0,
fill=ALT_BG, stroke=BORDER, width=1.0))
# ------- grilă verticală + orizontală
for x0, _ in COLS[1:]:
p.append(line(x0, TBL_Y0, x0, ROW2_Y1))
p.append(line(GUT_X + GUT_W, TBL_Y0, GUT_X + GUT_W, ROW2_Y1, BORDER, 1.0))
for y in (HDR_Y1, ROW1_Y1):
p.append(line(GUT_X, y, RIGHT, y, BORDER, 1.0))
p.append(rect(GUT_X, TBL_Y0, RIGHT - GUT_X, ROW2_Y1 - TBL_Y0,
fill="none", stroke=BORDER, width=1.4))
# ------- numere de rând Excel și litere de coloană
for num, (y0, y1) in zip("123", ((TBL_Y0, HDR_Y1), (ROW1_Y0, ROW1_Y1),
(ROW2_Y0, ROW2_Y1))):
p.append(txt(GUT_X + GUT_W / 2, (y0 + y1) / 2 + 5, num, 13, "400",
GRAY))
for (x0, x1), let in zip(COLS, LETTERS):
p.append(txt((x0 + x1) / 2, 90, let, 13, "400", GRAY))
for (x0, x1), h in zip(COLS, HEADERS):
p.append(txt((x0 + x1) / 2, 110, h, 13, "700", BLACK,
cell=(x0, x1)))
# ------- rândul 2: săptămâna (PPW) — celulele de calcul = formulă + rezultat
cA = COLS[0]
for k, s in enumerate(("Săptămâna", "încheiată", "(PPW)")):
p.append(txt((cA[0] + cA[1]) / 2, 138 + k * 18, s, 13, "400",
BLACK if k < 2 else GRAY, cell=cA))
for col, val in zip(COLS[1:4], WH):
p.append(txt((col[0] + col[1]) / 2, 158, fmt(val), 14, "400", BLACK,
cell=col))
week_calc = [("=(B2+C2+D2)/3", PPW), ("=2*E2-C2", W_R1), ("=2*E2-B2", W_S1),
("=E2+(B2-C2)", W_R2), ("=E2-(B2-C2)", W_S2)]
for col, (formula, res) in zip(COLS[4:], week_calc):
cx = (col[0] + col[1]) / 2
p.append(txt(cx, ROW1_Y0 + 26, formula, 13, "400", GRAY, cell=col))
p.append(txt(cx, ROW1_Y0 + 53, fmt(res), 15, "700", BLUE, cell=col))
# ------- rândul 3: ziua (PPD)
for k, s in enumerate(("Ziua", "încheiată", "(PPD)")):
p.append(txt((cA[0] + cA[1]) / 2, 200 + k * 18, s, 13, "400",
BLACK if k < 2 else GRAY, cell=cA))
for col, val in zip(COLS[1:4], DH):
p.append(txt((col[0] + col[1]) / 2, 226, fmt(val), 14, "400", BLACK,
cell=col))
day_calc = [("=(B3+C3+D3)/3", PPD), ("=2*E3-C3", D_R1), ("=2*E3-B3", D_S1),
("=E3+(B3-C3)", D_R2), ("=E3-(B3-C3)", D_S2)]
for col, (formula, res) in zip(COLS[4:], day_calc):
cx = (col[0] + col[1]) / 2
p.append(txt(cx, ROW2_Y0 + 26, formula, 13, "400", GRAY, cell=col))
p.append(txt(cx, ROW2_Y0 + 53, fmt(res), 15, "700", BLUE, cell=col))
# ------- rândul de comparație, evidențiat
p.append(rect(LEFT, CMP_Y0, RIGHT - LEFT, CMP_Y1 - CMP_Y0,
fill="#eaf7ee", stroke=GREEN, width=1.6, rx=6))
p.append(txt(W / 2, CMP_Y0 + 28,
f"PPD {fmt(PPD)} > PPW {fmt(PPW)} → CROSSING BULL (long)",
16, "700", BLACK))
# ------- legenda (exemplu construit) + note
p.append(txt(W / 2, 340,
"Cifrele sunt un exemplu construit, cu valori rotunde, pe TLV —",
13, "400", GRAY))
p.append(txt(W / 2, 358, "nu sunt date reale de piață.", 13, "400", GRAY))
p.append(txt(W / 2, 386,
"crossing = PPD trece deasupra PPW; dacă rămâne deasupra, "
"e continuare, nu crossing.", 13, "400", BLACK))
p.append(txt(W / 2, 408,
"Formula pivotului e cea predată în Modul 2; autorul a confirmat "
"că se calculează 1 la 1 în Excel.", 13, "400", GRAY))
p.append('</g></svg>')
return "\n".join(p) + "\n"
# ---------------------------------------------------------------- verificare
# Măsurăm textul cu fontul real folosit la rasterizare (DejaVu Sans), care e
# mai lat decât majoritatea fonturilor „system-ui": dacă încape aici, încape.
try:
from PIL import ImageFont
_REG = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
_FCACHE = {}
def _pil_w(s, size, weight):
path = _BOLD if weight == "700" else _REG
f = _FCACHE.get((path, size))
if f is None:
f = ImageFont.truetype(path, size)
_FCACHE[(path, size)] = f
return f.getlength(s)
_HAVE_PIL = True
except Exception: # pragma: no cover
_HAVE_PIL = False
def est_w(s, size, weight):
if _HAVE_PIL:
return _pil_w(s, size, weight)
# fallback conservator: supraestimează cu ~12%
narrow = " .,:;'|()[]{}"
total = 0.0
for ch in s:
if ch in narrow:
total += 0.30
elif ch.isdigit():
total += 0.60
elif ch.isupper():
total += 0.70
elif ch.islower():
total += 0.56
else:
total += 0.56
return total * size * (1.06 if weight == "700" else 1.0) * 1.12
def spans(x, y, s, size, weight, anchor):
ew = est_w(s, size, weight)
left = x - ew if anchor == "end" else (x - ew / 2 if anchor == "middle"
else x)
return left, left + ew
def verify(svg):
# 1. toate textele în panză
for x, y, s, size, weight, anchor, cell in TEXTS:
lo, hi = spans(x, y, s, size, weight, anchor)
assert -2 <= lo and hi <= W + 2, f"text în afara panzei: {s!r} ({lo:.0f},{hi:.0f})"
assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y})"
# 2. textele din tabel încap în celula lor
for x, y, s, size, weight, anchor, cell in TEXTS:
if cell is not None:
lo, hi = spans(x, y, s, size, weight, anchor)
assert cell[0] + 1 <= lo and hi <= cell[1] - 1, \
f"text iese din celulă: {s!r} ({lo:.0f},{hi:.0f}) vs {cell}"
# 3. texte care nu se suprapun
for i in range(len(TEXTS)):
xi, yi, si, szi, wi, ai, _ = TEXTS[i]
li, ri = spans(xi, yi, si, szi, wi, ai)
for j in range(i + 1, len(TEXTS)):
xj, yj, sj, szj, wj, aj, _ = TEXTS[j]
lj, rj = spans(xj, yj, sj, szj, wj, aj)
if abs(yi - yj) < 13 and li < rj - 3 and lj < ri - 3:
raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}")
# 4. condiția de crossing din desen
assert CMP_Y0 > ROW2_Y1, "rândul de comparație nu e sub tabel"
assert CMP_Y1 < 340, "comparația intră în legendă"
assert PPD > PPW
# 5. conținutul obligatoriu din spec (textul vizibil, cu &gt; decodat)
plain = unescape(svg)
for cheie in (
"Pivoții clasici în Excel — formula, pas cu pas",
"Perioada", "High", "Low", "Close", "PP", "R1", "S1", "R2", "S2",
"Săptămâna", "încheiată", "(PPW)",
"Ziua", "(PPD)",
"=(B2+C2+D2)/3", "=2*E2-C2", "=2*E2-B2", "=E2+(B2-C2)", "=E2-(B2-C2)",
"=(B3+C3+D3)/3", "=2*E3-C3", "=2*E3-B3", "=E3+(B3-C3)", "=E3-(B3-C3)",
"21.37", "21.57", "22.23", "20.73", "22.87", "19.87",
"22.13", "21.23", "22.47", "20.67",
"PPD 21.57 > PPW 21.37", "CROSSING BULL (long)",
"exemplu construit", "nu sunt date reale de piață",
"e continuare, nu crossing", "se calculează 1 la 1 în Excel",
):
assert cheie in plain, f"lipsește din desen: {cheie!r}"
# 6. fără candele / axe de preț / prețuri inventate
for interzis in ("candela", "SL ", "TP1", "TP2", "intrare ", "$", "USD"):
assert interzis not in svg, f"nu ar trebui să apară: {interzis!r}"
return True
SVG = build()
assert verify(SVG), "verificare eșuată"
OUT.write_text(SVG, encoding="utf-8")
print(f"scris {OUT} ({W}x{H}), verificare OK: {len(TEXTS)} etichete")