strategii: diagramele puse in capitole
36 din 37 de diagrame substituite in .md. substituie.py accepta acum si cheia cu slug inclus (unii agenti au scris mp2_exemplu, altii exemplu) si marcajul indentat. A ramas mdsrt_aplus: agentul a raportat OK fara sa scrie niciun fisier — exact capcana din CLAUDE.md, un raport de agent nu e dovada. Relansat separat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
This commit is contained in:
315
gen/strategii/mp2_pivoti_excel.py
Normal file
315
gen/strategii/mp2_pivoti_excel.py
Normal file
@@ -0,0 +1,315 @@
|
||||
#!/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 — "
|
||||
"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('</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ță",
|
||||
"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")
|
||||
Reference in New Issue
Block a user