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

3
.gitignore vendored
View File

@@ -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

View File

@@ -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, ianmai 2026, doar `hybrid_be` e pozitiv pe ansamblu ~+0.05R):** edge-ul vine din CÂND, nu din management; 18:0019:00 RO = zonă moartă; ora de start optimă = 19:15. Trei configurații recomandate: **A** 19:1520:15 (1h, edge max/timp min), **B** 19:4521:45 prima (cea mai robustă pe toate validările), **W** 19:1522: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
View 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

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