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
313 lines
14 KiB
Python
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 > 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")
|