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

View File

@@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
"""Generează data/Dashboard.xlsx dintr-un snapshot al data/backtest.xlsx.
CITEȘTE backtest.xlsx (read-only, data_only=True) și SCRIE un fișier SEPARAT
data/Dashboard.xlsx. NU atinge backtest.xlsx. Refolosește build_config() și
build_dashboard() din generate_template.py — aceeași logică de Dashboard, dar
pe un sheet Trades static (valori, fără formule), ca să țină backtest.xlsx mic.
Reruleaza prin refresh_dashboard.bat (sau direct):
python scripts/generate_dashboard.py
IMPORTANT: deschide și SALVEAZĂ backtest.xlsx în Excel cel puțin o dată după
ultima editare înainte de refresh. Scriptul citește valorile DEJA calculate de
Excel (R_/$_/Bal_/helpere). Dacă nu ai salvat în Excel, cache-ul de valori
lipsește și Dashboard-ul iese gol. (Aceeași constrângere ca Ferestre v2.)
"""
from pathlib import Path
import openpyxl
from openpyxl import Workbook
from generate_template import (
build_config,
build_dashboard,
TRADES_HEADERS,
MAX_ROWS,
)
SRC = Path(__file__).resolve().parent.parent / "data" / "backtest.xlsx"
OUT = Path(__file__).resolve().parent.parent / "data" / "Dashboard.xlsx"
# Rândurile de input (galbene) din sheet-ul Config — singurele pe care le purtăm
# din workbook-ul real (Account, Lot, limite prop, calibrare $/punct). Restul
# celulelor Config sunt formule recreate de build_config().
CONFIG_INPUT_ROWS = [4, 5, 9, 10, 12, 14, 17, 19, 20, 21, 22, 25]
def read_config_inputs(ws_cfg) -> dict[int, object]:
"""Citește valorile din coloana B a sheet-ului Config (read-only)."""
vals: dict[int, object] = {}
for r, row in enumerate(
ws_cfg.iter_rows(min_row=1, max_row=40, min_col=2, max_col=2), start=1
):
# read_only poate întoarce EmptyCell (fără .value) pentru celule goale
vals[r] = getattr(row[0], "value", None)
return vals
def apply_config_inputs(wb: Workbook, cfg_inputs: dict[int, object]) -> None:
"""Suprascrie inputurile Config cu valorile reale ale lui Marius."""
ws = wb["Config"]
for r in CONFIG_INPUT_ROWS:
v = cfg_inputs.get(r)
if v is not None:
ws.cell(row=r, column=2, value=v)
def copy_trades_values(wb: Workbook, ws_src) -> None:
"""Creează un sheet Trades static (valori) în ordinea exactă TRADES_HEADERS.
Mapează după NUMELE coloanei din sursă, ca literele din COL să corespundă cu
ce așteaptă formulele/charturile din build_dashboard, indiferent de ordinea
fizică din backtest.xlsx.
"""
ws = wb.create_sheet("Trades", 1)
src_rows = ws_src.iter_rows(min_row=1, values_only=True)
src_hdr = next(src_rows)
src_idx = {name: i for i, name in enumerate(src_hdr) if name is not None}
# Header (necesar pentru titles_from_data al charturilor Bal_*/BalProp_*)
for col_idx, name in enumerate(TRADES_HEADERS, start=1):
ws.cell(row=1, column=col_idx, value=name)
# Date — rândurile 2..MAX_ROWS+1, ca rangurile Trades!$X$2:$X$501 să se alinieze
r_out = 2
for src_row in src_rows:
if r_out > MAX_ROWS + 1:
break
for col_idx, name in enumerate(TRADES_HEADERS, start=1):
si = src_idx.get(name)
if si is None or si >= len(src_row):
continue
val = src_row[si]
if val is not None:
ws.cell(row=r_out, column=col_idx, value=val)
r_out += 1
ws.sheet_state = "hidden" # snapshot intern; Dashboard e singurul vizibil util
def main() -> int:
if not SRC.exists():
print(f"EROARE: nu găsesc {SRC}")
return 1
wb_src = openpyxl.load_workbook(SRC, read_only=True, data_only=True)
if "Trades" not in wb_src.sheetnames or "Config" not in wb_src.sheetnames:
print("EROARE: backtest.xlsx nu are sheet-urile Trades + Config.")
return 1
cfg_inputs = read_config_inputs(wb_src["Config"])
wb = Workbook()
wb.remove(wb.active)
build_config(wb) # Config la index 0 (cu formule)
apply_config_inputs(wb, cfg_inputs)
copy_trades_values(wb, wb_src["Trades"]) # Trades static la index 1 (ascuns)
build_dashboard(wb) # Dashboard la index 2 — formule + charturi
wb.active = wb.sheetnames.index("Dashboard")
wb_src.close()
wb.save(OUT)
print(f"Scris {OUT}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -19,7 +19,6 @@ from datetime import date, datetime, time, timedelta
from pathlib import Path
from openpyxl import Workbook
from openpyxl.chart import LineChart, Reference
from openpyxl.formatting.rule import CellIsRule
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter
@@ -1746,49 +1745,8 @@ def build_dashboard(wb: Workbook) -> None:
for r in range(5, 5 + len(metrics)):
ws.row_dimensions[r].height = 75
# Equity curve chart — 5 linii
chart = LineChart()
chart.title = "Equity Curve — 5 strategii"
chart.style = 12
chart.y_axis.title = "Balance ($)"
chart.x_axis.title = "Trade #"
chart.height = 12
chart.width = 24
data = Reference(
wb["Trades"],
min_col=_col_to_int(COL[f"Bal_{STRAT_KEYS[0]}"]),
max_col=_col_to_int(COL[f"Bal_{STRAT_KEYS[-1]}"]),
min_row=1,
max_row=MAX_ROWS + 1,
)
chart.add_data(data, titles_from_data=True)
cats = Reference(
wb["Trades"], min_col=1, max_col=1,
min_row=2, max_row=MAX_ROWS + 1,
)
chart.set_categories(cats)
ws.add_chart(chart, "V4")
# Equity curve prop — al doilea chart, separat de modelul abstract
chart_prop = LineChart()
chart_prop.title = "Equity Curve — Prop (cont real)"
chart_prop.style = 12
chart_prop.y_axis.title = "Balance Prop ($)"
chart_prop.x_axis.title = "Trade #"
chart_prop.height = 12
chart_prop.width = 24
data_prop = Reference(
wb["Trades"],
min_col=_col_to_int(COL[f"BalProp_{STRAT_KEYS[0]}"]),
max_col=_col_to_int(COL[f"BalProp_{STRAT_KEYS[-1]}"]),
min_row=1,
max_row=MAX_ROWS + 1,
)
chart_prop.add_data(data_prop, titles_from_data=True)
chart_prop.set_categories(cats)
ws.add_chart(chart_prop, "V30")
# Notă: graficele de echitate au fost eliminate (nu sunt folosite). Dashboard-ul
# rămâne pur tabelar — metrici + breakdown-uri + ferestre + compliance prop.
# ---------------------------------------------------------------------------
@@ -1797,13 +1755,16 @@ def build_dashboard(wb: Workbook) -> None:
def build_workbook() -> Workbook:
# backtest.xlsx = doar Config + Trades (fișierul editat zilnic, ușor/rapid).
# Dashboard-ul trăiește separat în data/Dashboard.xlsx, generat la comandă de
# scripts/generate_dashboard.py (vezi refresh_dashboard.bat). build_dashboard()
# rămâne aici și e refolosit de acel script.
wb = Workbook()
default = wb.active
wb.remove(default)
build_config(wb)
build_trades(wb)
build_dashboard(wb)
wb.active = wb.sheetnames.index("Dashboard")
wb.active = wb.sheetnames.index("Trades")
return wb

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())