diff --git a/.gitignore b/.gitignore index e0512f6..420a29b 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 8297030..2688b7f 100644 --- a/CLAUDE.md +++ b/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). diff --git a/data/backtest.xlsx b/data/backtest.xlsx index f07819f..1068943 100644 Binary files a/data/backtest.xlsx and b/data/backtest.xlsx differ diff --git a/refresh_dashboard.bat b/refresh_dashboard.bat new file mode 100644 index 0000000..31368af --- /dev/null +++ b/refresh_dashboard.bat @@ -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 diff --git a/scripts/generate_dashboard.py b/scripts/generate_dashboard.py new file mode 100644 index 0000000..cfc1616 --- /dev/null +++ b/scripts/generate_dashboard.py @@ -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()) diff --git a/scripts/generate_template.py b/scripts/generate_template.py index a3d7151..edf630f 100644 --- a/scripts/generate_template.py +++ b/scripts/generate_template.py @@ -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 diff --git a/scripts/strip_dashboard.py b/scripts/strip_dashboard.py new file mode 100644 index 0000000..d810364 --- /dev/null +++ b/scripts/strip_dashboard.py @@ -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 + 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 ș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']*name="Dashboard"[^>]*/>', "", wb_xml, count=1 + ) + wb_xml = re.sub( + r"]*>[^<]*Dashboard![^<]*", "", wb_xml + ) + # dacă blocul a rămas gol, scoate-l + wb_xml = re.sub(r"\s*", "", 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']*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("\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())