#!/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''
f'{escape(s)}')
def line(x1, y1, x2, y2, stroke=GRID, width=1.0, dash=None):
d = f' stroke-dasharray="{dash}"' if dash else ""
return (f'')
def rect(x, y, w, h, fill=WHITE, stroke=BORDER, width=1.2, rx=0):
r = f' rx="{rx}"' if rx else ""
return (f'')
def fmt(v):
return f"{v:.2f}"
def build():
p = [
f'')
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 > 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")