separa Dashboard de Trades: backtest.xlsx 39MB -> 1MB

backtest.xlsx ramane doar Config + Trades (editat zilnic, rapid la salvat).
Dashboard-ul devine fisier separat data/Dashboard.xlsx, generat la comanda:

- scripts/generate_dashboard.py: citeste backtest.xlsx read-only/data_only,
  reutilizeaza build_dashboard() pe un sheet Trades static, scrie Dashboard.xlsx
- scripts/strip_dashboard.py: migrare unica prin chirurgie pe zip (pastreaza
  dropdown-urile x14 din Trades; openpyxl le-ar fi sters)
- refresh_dashboard.bat: wrapper dublu-click (regenereaza + deschide)
- build_workbook() nu mai include Dashboard; graficele de echitate eliminate
- data/Dashboard.xlsx ignorat (output regenerabil)

Sincronizare la comanda (nu live): ruleaza refresh_dashboard.bat dupa ce
salvezi backtest.xlsx in Excel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marius
2026-06-01 23:52:58 +03:00
parent 8e51b7dc46
commit 45e6505afa
7 changed files with 374 additions and 49 deletions

203
scripts/strip_dashboard.py Normal file
View File

@@ -0,0 +1,203 @@
# -*- coding: utf-8 -*-
"""Migrare UNICĂ: scoate sheet-ul Dashboard din data/backtest.xlsx.
Dashboard-ul (cu ~4.200 coloane-helper) era cauza dimensiunii de ~39 MB.
După separare trăiește în data/Dashboard.xlsx (generat de generate_dashboard.py).
Acest script doar curăță fișierul real → ~0.8 MB.
NU folosește openpyxl pentru rescriere (ar ȘTERGE dropdown-urile x14 din Trades).
În schimb face chirurgie pe zip: scoate DOAR partea XML a sheet-ului Dashboard +
drawings/charts asociate + calcChain.xml, lăsând XML-ul Trades (cu dropdown-urile)
byte-cu-byte intact. Excel regenerează calcChain la prima deschidere.
Rulare (cere confirmare; face backup automat):
python scripts/strip_dashboard.py --yes
Alternativă manuală 100% sigură (dacă preferi): deschide backtest.xlsx în Excel,
click-dreapta pe tab-ul "Dashboard" → Delete → Salvează.
"""
import posixpath
import re
import shutil
import sys
import xml.etree.ElementTree as ET
import zipfile
from datetime import datetime
from pathlib import Path
SRC = Path(__file__).resolve().parent.parent / "data" / "backtest.xlsx"
NS = {
"main": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
"rel": "http://schemas.openxmlformats.org/package/2006/relationships",
"ct": "http://schemas.openxmlformats.org/package/2006/content-types",
}
def _norm(part: str) -> str:
"""xl/worksheets/sheet3.xml -> /xl/worksheets/sheet3.xml (cheie content-types)."""
return "/" + part if not part.startswith("/") else part
def _resolve(owner_part: str, target: str) -> str:
"""Rezolvă un Target de relationship relativ la partea care îl deține.
ex: owner=xl/worksheets/sheet3.xml, target=../drawings/drawing1.xml
-> xl/drawings/drawing1.xml
"""
if target.startswith("/"):
return target.lstrip("/")
base = posixpath.dirname(owner_part)
return posixpath.normpath(posixpath.join(base, target))
def find_dashboard_parts(z: zipfile.ZipFile):
"""Întoarce (set de părți de șters, r:id-ul Dashboard din workbook.xml)."""
names = set(z.namelist())
# 1) workbook.xml: găsește <sheet name="Dashboard" r:id=...>
wb_xml = z.read("xl/workbook.xml").decode("utf-8")
wb_root = ET.fromstring(wb_xml)
rid = None
for sheet in wb_root.iter(f"{{{NS['main']}}}sheet"):
if sheet.get("name") == "Dashboard":
rid = sheet.get(f"{{{NS['r']}}}id")
break
if rid is None:
return None, None
# 2) workbook.xml.rels: r:id -> worksheets/sheetN.xml
rels_xml = z.read("xl/_rels/workbook.xml.rels").decode("utf-8")
rels_root = ET.fromstring(rels_xml)
target = None
for rel in rels_root.iter(f"{{{NS['rel']}}}Relationship"):
if rel.get("Id") == rid:
target = rel.get("Target") # ex: worksheets/sheet3.xml
break
if target is None:
return None, None
sheet_part = _resolve("xl/workbook.xml", target) # xl/worksheets/sheetN.xml
drop = {sheet_part}
# Helper recursiv: adaugă o parte + tot ce referă rels-ul ei (drawings->charts->...)
def add_with_rels(part: str):
if part not in names:
return
drop.add(part)
rels_path = posixpath.join(
posixpath.dirname(part), "_rels", posixpath.basename(part) + ".rels"
)
if rels_path not in names:
return
drop.add(rels_path)
rr = ET.fromstring(z.read(rels_path).decode("utf-8"))
for rel in rr.iter(f"{{{NS['rel']}}}Relationship"):
tgt = rel.get("Target", "")
if rel.get("TargetMode") == "External" or not tgt:
continue
child = _resolve(part, tgt)
# doar părți interne de tip drawing/chart/style/colors (nu media partajată)
if any(k in child for k in ("drawing", "chart")):
add_with_rels(child)
# 3-4) sheet Dashboard -> drawings -> charts (transitiv, căi rezolvate corect)
add_with_rels(sheet_part)
# 5) calcChain — Excel îl regenerează; scoaterea lui elimină bloat-ul
if "xl/calcChain.xml" in names:
drop.add("xl/calcChain.xml")
return drop, rid
def rewrite(z_in: zipfile.ZipFile, drop: set, rid: str, out_path: Path):
drop_norm = {_norm(p) for p in drop}
# workbook.xml — scoate <sheet name="Dashboard"> și definedName-urile orfane
# care referă Dashboard! (ex: _FilterDatabase). Dashboard e ULTIMUL sheet, deci
# localSheetId-urile Config(0)/Trades(1) nu se reindexează.
wb_xml = z_in.read("xl/workbook.xml").decode("utf-8")
wb_xml = re.sub(
r'<sheet[^>]*name="Dashboard"[^>]*/>', "", wb_xml, count=1
)
wb_xml = re.sub(
r"<definedName\b[^>]*>[^<]*Dashboard![^<]*</definedName>", "", wb_xml
)
# dacă blocul <definedNames> a rămas gol, scoate-l
wb_xml = re.sub(r"<definedNames>\s*</definedNames>", "", wb_xml)
# workbook.xml.rels — scoate relationship-ul rid
rels_xml = z_in.read("xl/_rels/workbook.xml.rels").decode("utf-8")
rels_xml = re.sub(
r'<Relationship[^>]*Id="' + re.escape(rid) + r'"[^>]*/>',
"",
rels_xml,
count=1,
)
# [Content_Types].xml — scoate Override-urile părților șterse
ct_xml = z_in.read("[Content_Types].xml").decode("utf-8")
ct_root = ET.fromstring(ct_xml)
ct_ns = NS["ct"]
for ov in list(ct_root):
if ov.tag == f"{{{ct_ns}}}Override" and ov.get("PartName") in drop_norm:
ct_root.remove(ov)
ct_out = ET.tostring(ct_root, encoding="unicode")
if not ct_out.startswith("<?xml"):
ct_out = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' + ct_out
replacements = {
"xl/workbook.xml": wb_xml,
"xl/_rels/workbook.xml.rels": rels_xml,
"[Content_Types].xml": ct_out,
}
with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as z_out:
for item in z_in.infolist():
if item.filename in drop:
continue
if item.filename in replacements:
z_out.writestr(item, replacements[item.filename])
else:
z_out.writestr(item, z_in.read(item.filename))
def main() -> int:
if "--yes" not in sys.argv:
print("Operație pe data/backtest.xlsx: scoate sheet-ul Dashboard (chirurgie zip).")
print("Dropdown-urile x14 din Trades rămân intacte. Backup automat înainte.")
print("Confirmă cu: python scripts/strip_dashboard.py --yes")
return 1
if not SRC.exists():
print(f"EROARE: nu găsesc {SRC}")
return 1
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
backup = SRC.with_name(f"{SRC.stem}.backup-{timestamp}{SRC.suffix}")
shutil.copy2(SRC, backup)
print(f"Backup -> {backup}")
with zipfile.ZipFile(SRC) as z:
drop, rid = find_dashboard_parts(z)
if not drop:
print("Nimic de făcut: sheet-ul 'Dashboard' nu există în backtest.xlsx.")
return 0
print("Părți eliminate:")
for p in sorted(drop):
print(" ", p)
tmp = SRC.with_name(SRC.stem + ".stripping.tmp.xlsx")
rewrite(z, drop, rid, tmp)
tmp.replace(SRC)
print(f"Sheet 'Dashboard' eliminat. Salvat {SRC}")
print("Pas următor: deschide & SALVEAZĂ backtest.xlsx în Excel (recalcul),")
print("apoi rulează refresh_dashboard.bat pentru a genera Dashboard.xlsx.")
return 0
if __name__ == "__main__":
raise SystemExit(main())