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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -13,6 +13,9 @@ venv/
|
||||
*.xlsx.bak
|
||||
data/backtest.backup-*.xlsx
|
||||
|
||||
# Dashboard generat (output read-only, regenerat de refresh_dashboard.bat)
|
||||
data/Dashboard.xlsx
|
||||
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
31
CLAUDE.md
31
CLAUDE.md
@@ -14,9 +14,12 @@ Documentation and UI strings are in **Romanian**; keep them Romanian when editin
|
||||
|
||||
```powershell
|
||||
pip install openpyxl # one-time
|
||||
python scripts/generate_template.py # regenerate data/backtest.xlsx
|
||||
python scripts/generate_template.py # regenerate data/backtest.xlsx (Config + Trades only)
|
||||
python scripts/generate_dashboard.py # regenerate data/Dashboard.xlsx din backtest.xlsx
|
||||
```
|
||||
|
||||
`refresh_dashboard.bat` (rădăcina repo) = wrapper dublu-click: rulează `generate_dashboard.py` și deschide `Dashboard.xlsx`.
|
||||
|
||||
No test suite, linter, or build step exists. The only "build" is regenerating the Excel.
|
||||
|
||||
**Destructive caveat**: `generate_template.py` **overwrites `data/backtest.xlsx` with no prompt**. The user's logged trades live in that file. Before running it (or asking the user to), confirm they have a backup or that the file is empty/sample-only.
|
||||
@@ -27,11 +30,11 @@ Three artifacts work together; understand all three before editing any:
|
||||
|
||||
### 1. `scripts/generate_template.py` — the only code
|
||||
|
||||
Builds a 3-sheet workbook via openpyxl:
|
||||
`build_workbook()` builds `data/backtest.xlsx` = **2 sheets only** (Config + Trades). `build_dashboard()` still lives here but is **no longer added to backtest.xlsx** — it is reused by `scripts/generate_dashboard.py` to build the separate `data/Dashboard.xlsx` (see "Dashboard separat" below).
|
||||
|
||||
- **Config** sheet — editable params (Account Size, Risk %) and dropdown source lists.
|
||||
- **Trades** sheet — `MAX_ROWS=500` pre-populated rows. Yellow cells = user input (date, time, strategy, indicator, TF, direction, SL/TP %, outcome). Blue cells = derived via formula (Zi, Sesiune, then per-strategy `R_*`, `$_*`, `Bal_*`). Grey cells = helper columns (`Win_*`, `Peak_*`, `DD_*`) consumed by Dashboard.
|
||||
- **Dashboard** sheet — reads from Trades ranges via `SUMIF`/`AVERAGEIF`/`COUNTIF`; renders metrics table, glossary, per-Session/Strategy/Indicator/Direction breakdowns, and a 5-line equity-curve chart.
|
||||
- **Dashboard** (`build_dashboard`, emitted into the separate `Dashboard.xlsx`) — reads from Trades ranges via `SUMIF`/`AVERAGEIF`/`COUNTIF`; renders metrics table, glossary, per-Session/Strategy/Indicator/Direction breakdowns, ferestre candidate, prop compliance. **No equity-curve chart** (removed — Dashboard is pure-tabular).
|
||||
|
||||
Column-name → letter mapping is held in the `COL` dict, built from `TRADES_HEADERS = INPUT_HEADERS + DERIVED_HEADERS + HELPER_HEADERS`. **Never hardcode column letters** — adding/reordering a header shifts every letter. Always look up via `COL["..."]`.
|
||||
|
||||
@@ -71,6 +74,28 @@ Totul se recalculează automat din `backtest.xlsx` (R/$ deja calculate de Excel;
|
||||
|
||||
**Findings curente (330 trade-uri, ian–mai 2026, doar `hybrid_be` e pozitiv pe ansamblu ~+0.05R):** edge-ul vine din CÂND, nu din management; 18:00–19:00 RO = zonă moartă; ora de start optimă = 19:15. Trei configurații recomandate: **A** 19:15–20:15 (1h, edge max/timp min), **B** 19:45–21:45 prima (cea mai robustă pe toate validările), **W** 19:15–22:15 prima (volum/bani max raportat la timp; +30 min până la 22:45 aduc doar ~+$61). Filtrele direcționale (buy) par mai bune dar pică out-of-sample. Edge subțire → ipoteze de confirmat live.
|
||||
|
||||
## Dashboard separat (scripts/generate_dashboard.py)
|
||||
|
||||
`backtest.xlsx` ajunsese ~39 MB și se salva greu. Cauza NU erau tranzacțiile, ci **sheet-ul Dashboard** (~4.200 coloane-helper ascunse → ~2,1M celule cu formule în `calcChain.xml`). Soluție: Dashboard-ul a fost scos din fișierul editat zilnic într-un fișier separat, generat la comandă (același tipar ca Ferestre v2).
|
||||
|
||||
**Cele 3 fișiere:**
|
||||
- `data/backtest.xlsx` — **editat zilnic**, doar Config + Trades (~0.8 MB, rapid la salvat).
|
||||
- `data/Dashboard.xlsx` — **generat read-only** de `generate_dashboard.py` din backtest.xlsx. Conține un sheet `Trades` ascuns cu **valori statice** (copiate din cache-ul Excel) + sheet-ul Dashboard cu formulele reutilizate din `build_dashboard()`. Marius nu-l editează niciodată — se regenerează.
|
||||
- `data/Ferestre_v2.xlsx` — analiza edge/fereastră (separată, vezi mai sus).
|
||||
|
||||
**Reluare după tranzacții noi:**
|
||||
```powershell
|
||||
# întâi: deschide & SALVEAZĂ backtest.xlsx în Excel (populează cache-ul de valori R_/$_/Bal_)
|
||||
python scripts/generate_dashboard.py # sau dublu-click refresh_dashboard.bat
|
||||
```
|
||||
`generate_dashboard.py` citește `backtest.xlsx` **read-only, `data_only=True`** — ia valorile DEJA calculate de Excel (nu recalculează formule). **Constrângere:** dacă nu ai salvat în Excel după ultima editare, cache-ul lipsește și Dashboard-ul iese gol (aceeași condiție ca Ferestre v2).
|
||||
|
||||
**Sincronizare:** la comandă (rulezi scriptul/bat-ul), NU live. Marius a ales explicit acest model.
|
||||
|
||||
**Migrare unică — `scripts/strip_dashboard.py`:** scoate sheet-ul Dashboard din `backtest.xlsx` existent (39 MB → ~0.8 MB). NU folosește openpyxl pentru rescriere (ar șterge cele 12 dropdown-uri **x14** din Trades). Face **chirurgie pe zip**: elimină doar partea Dashboard + drawings/charts + `calcChain.xml` + definedName-ul orfan, lăsând XML-ul Config/Trades byte-cu-byte intact. Cere `--yes` și face backup automat. Alternativă 100% sigură: șterge tab-ul Dashboard manual în Excel (click-dreapta → Delete → Save). **Rulează doar cu acordul lui Marius** (e destructiv pe fișierul real).
|
||||
|
||||
**Escape hatch dimensiune Dashboard.xlsx:** `Config!B17` (Activează filtru Prima) = `NU` reduce drastic grid-ul de ferestre → Dashboard.xlsx mult mai mic/rapid.
|
||||
|
||||
## Reference docs
|
||||
|
||||
- `strategie_M2D.md` — M2D setup rules (color-coded dot bands on TF mare/mic, SL/TP placement, session filters).
|
||||
|
||||
Binary file not shown.
13
refresh_dashboard.bat
Normal file
13
refresh_dashboard.bat
Normal file
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
REM Regenereaza data\Dashboard.xlsx din data\backtest.xlsx si il deschide.
|
||||
REM IMPORTANT: deschide & salveaza backtest.xlsx in Excel inainte de refresh,
|
||||
REM ca valorile calculate (R_/$_/Bal_) sa fie in cache.
|
||||
cd /d D:\PROIECTE\atm-backtesting
|
||||
python scripts\generate_dashboard.py
|
||||
if %errorlevel%==0 (
|
||||
start "" "data\Dashboard.xlsx"
|
||||
) else (
|
||||
echo.
|
||||
echo Generarea a esuat ^(vezi mesajul de mai sus^).
|
||||
)
|
||||
pause
|
||||
120
scripts/generate_dashboard.py
Normal file
120
scripts/generate_dashboard.py
Normal 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())
|
||||
@@ -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
203
scripts/strip_dashboard.py
Normal 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())
|
||||
Reference in New Issue
Block a user