#!/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'', '', f'', 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 — " "continuare (09_QA/16:35-43).", 13, "400", BLACK)) p.append(txt(W / 2, 408, "Surse formule: 03_MODUL_2/02_zone_cerere_oferta_support_" "resistance.md:42-50", 13, "400", GRAY)) p.append(txt(W / 2, 426, "Calcul Excel 1-la-1, MP² = comparația a doi pivoți: " "09_QA/09_2025-12-09.md:1176-1180.", 13, "400", GRAY)) p.append('') 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ță", "09_QA/16:35-43", "09_QA/09_2025-12-09.md:1176-1180", ): 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")