From 1f45d77e4e170d659d4a8a0cded7e018a2519592 Mon Sep 17 00:00:00 2001 From: Marius Date: Wed, 3 Jun 2026 01:43:02 +0300 Subject: [PATCH] combina Ferestre_v2 in Dashboard.xlsx: un singur script, auto-scan ferestre - generate_dashboard.py: adauga build_ferestre() (auto-scan edge x durata x fiabilitate, nimic hardcodat) + sheet date_grafic; scoate grila de ferestre pe formule din build_dashboard() - sterge scripts/generate_ferestre_v2.py si data/Ferestre_v2.xlsx (inlocuite) - generate_template.py: Dashboard pur-tabular (fara grila ferestre pe formule) - CLAUDE.md: documenteaza modelul combinat (un fisier Dashboard.xlsx) Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 54 +- data/Ferestre_v2.xlsx | Bin 17803 -> 0 bytes scripts/generate_dashboard.py | 666 ++++++- scripts/generate_ferestre_v2.py | 460 ----- scripts/generate_template.py | 3105 +++++++++++++------------------ 5 files changed, 2004 insertions(+), 2281 deletions(-) delete mode 100644 data/Ferestre_v2.xlsx delete mode 100644 scripts/generate_ferestre_v2.py diff --git a/CLAUDE.md b/CLAUDE.md index d1f2170..20723d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,11 +30,11 @@ Three artifacts work together; understand all three before editing any: ### 1. `scripts/generate_template.py` — the only code -`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). +`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 "Analiză combinată" 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** (`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). +- **Dashboard** (`build_dashboard`, emitted into the separate `Dashboard.xlsx`) — reads from Trades ranges via `SUMIF`/`AVERAGEIF`/`COUNTIF`; renders metrics table, glossary, per-Strategy/Indicator/Direction breakdowns, prop compliance. **No formula-based ferestre grid** (removed — that window scan is now the python auto-scan in the `Ferestre` sheet; see "Analiză combinată" below) and **no equity-curve chart** in the Dashboard sheet itself. 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["..."]`. @@ -60,42 +60,42 @@ The `Sesiune` column is computed by `_f_session` from `Data` + `Ora RO` (Romania `STOPPING_RULE.md` is a **signed document** (the user committed it as a commitment). It defines GO LIVE / EXTEND / ABANDON thresholds: `N≥40`, `WR≥55%`, `Expectancy≥+0.20R`. Treat these numbers as fixed unless the user explicitly asks to renegotiate them — do not "improve" them in passing. -## Ferestre v2 — analiză edge/fereastră (scripts/generate_ferestre_v2.py) +## Analiză combinată — `Dashboard.xlsx` (scripts/generate_dashboard.py) -Analiză separată care găsește **fereastra de timp (ora RO) cu cel mai bun raport edge / nr. tranzacții / durată**, fără să breach-uiască contul prop. Citește `data/backtest.xlsx` **read-only** și scrie un fișier nou `data/Ferestre_v2.xlsx` (NU atinge workbook-ul cu tranzacții; date_grafic rămâne sheet vizibil ca să se randeze chart-ul). +`backtest.xlsx` (editat zilnic) = doar Config + Trades (~0.8 MB, rapid la salvat). TOATĂ analiza trăiește într-un **singur** fișier read-only `data/Dashboard.xlsx`, generat la comandă din backtest.xlsx de **un singur script** `generate_dashboard.py`. (Istoric: erau două fișiere cu două scannere de ferestre — `Dashboard.xlsx` pe formule + `Ferestre_v2.xlsx` python cu ferestre A/B/W hardcodate; au fost combinate. `generate_ferestre_v2.py` a fost șters.) **Reluare după ce Marius adaugă tranzacții noi:** ```powershell -python scripts/generate_ferestre_v2.py -``` -Totul se recalculează automat din `backtest.xlsx` (R/$ deja calculate de Excel; scriptul nu recalculează formule). Conține: Concluzii, Tabel unic cu toate variantele, validări Forward 1 (lunar) / Forward 2 (train-test 70/30) / Walk-forward (3 felii) pe toate ferestrele, bootstrap CI, calendar, grafic echitate. - -**ÎNAINTE de analiză — verifică typo-uri de tastare în Trades** (TP%/SL% cu zecimală lipsă umflă fals edge-ul). Cele găsite și corectate manual: #314 (TP2 17→0.17), #298 (TP0 0.5→0.05), #240 (TP1 0.8→0.08), #182 (ordine TP0/TP1 inversată → 0.06/0.10), #338 (TP1 0.011→0.11; era SL deci R neschimbat). La date noi, caută valori TP/SL ≥1 sau TP0>TP1>TP2 inversate și confirmă cu Marius înainte de a corecta. - -**Findings curente (407 trade-uri, dec 2025–iun 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; ExpR +0.187), **B** 19:45–21:45 prima (cea mai robustă — ExpR +0.200, atinge pragul 0.20R, 100% bootstrap pozitiv, pozitivă în toate cele 7 luni, se întărește OOS), **W** 19:15–22:15 prima (volum/bani max raportat la timp; ExpR +0.135). Adăugarea lui decembrie a întărit B și a slăbit ușor A/W (dec a fost lună slabă pe start 19:15 și ferestre lungi). 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_) +# întâi: deschide & SALVEAZĂ backtest.xlsx în Excel (populează cache-ul 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). +`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 analiza iese goală. + +**Sheet-urile din `Dashboard.xlsx`:** +- `Config` — inputurile reale (lot, limite prop) copiate din backtest + formule (`build_config()`). +- `Trades` — snapshot static ascuns (valori, nu formule), ca să țină backtest.xlsx mic. +- `Dashboard` — metrici 5 manageri + breakdown-uri (strategie/indicator/direcție) + prop compliance. **Formule** Excel (recalculează live la schimbarea Config). Grila veche de ferestre pe formule (`FERESTRE CANDIDATE` + `TOP 20`, ~mii de coloane-helper = cauza dimensiunii) a fost **scoasă** din `build_dashboard()`. +- `Ferestre` — **auto-scan python** edge × durată × fiabilitate (valori statice). Vezi mai jos. +- `date_grafic` — sursa curbei de echitate din sheet-ul Ferestre. + +### Sheet-ul Ferestre — auto-scan (NIMIC hardcodat) + +Construit de `build_ferestre()` în `generate_dashboard.py`. Găsește **fereastra (ora RO) cu cel mai bun edge** fără să spargă contul prop: +- **Grilă generativă** la 15 min (start 16:30–22:00, durată 45min–capăt 23:00, filtru {toate, prima}), scanată pe **toate cele 5 manageri**; pe fiecare fereastră raportează managerul cu cel mai bun ExpR dintre cei non-breach. Grila la 15 min e **suprasetul** vechilor două grile (cea Dashboard la 30 min nici nu vedea ferestrele pe :15/:45). +- **Recomandări derivate din date** (etichete = fereastra calculată, nu nume fixe), în familii cu gradații — funcția `recommend()`, praguri în constantele `EDGE_*`/`ROBUST_*`/`VOLUM_*`: **EDGE 45min/1h/1h30** (cea mai profitabilă fereastră la fix acea durată → cea mai mică = cea mai scurtă perioadă profitabilă), **ROBUST 1/2/3** (cel mai bun ExpR pozitiv în toate / ≥80% / ≥60% din luni, N≥40), **VOLUM (max N)** + **VOLUM compact** (cel mai mare volum profitabil, resp. cea mai scurtă fereastră tot cu volum relevant). Dedup pe (start,end) → un geam apare o singură dată. Câte se validează în adâncime = `FERESTRE_MAX_VARIANTS` (BRUT + recomandări + top-N profitabile). +- **Validări** (recalculate de fiecare dată): Forward 1 (lunar), Forward 2 (train/test 70/30), walk-forward (3 felii), bootstrap CI 95%, calendar NFP, grafic echitate pe primele 2 recomandări. +- Limitele prop (cont/daily/max) se citesc din Config (B9/B12/B14), fallback $50k/4%/7%. + +**Fără concluzii hardcodate — NU le re-introduce.** Versiunea veche avea ferestrele A/B/W și concluziile lipite în script dintr-o analiză LLM anterioară; se învecheau la fiecare lot nou. Acum totul se re-derivă din date. Ferestrele bune reapar singure dacă rămân cele mai bune (ex. ROBUST 19:45–21:45 hybrid_be = fostul „B", re-găsit automat). + +**Avertisment overfit:** scanul pe toate cele 5 manageri × mii de ferestre poate scoate în top combinații fragile (ex. `TP2 only` pe N mic = ExpR umflat de varianță). De-aia validările (bootstrap CI, lunar, train/test, walk-forward) sunt în același tabel — citește-le împreună cu ExpR-ul, nu doar vârful. + +**ÎNAINTE de a interpreta — verifică typo-uri în Trades** (TP%/SL% cu zecimală lipsă umflă fals edge-ul). Cele găsite și corectate manual: #314 (TP2 17→0.17), #298 (TP0 0.5→0.05), #240 (TP1 0.8→0.08), #182 (ordine TP0/TP1 inversată → 0.06/0.10), #338 (TP1 0.011→0.11; era SL deci R neschimbat). La date noi, caută valori TP/SL ≥1 sau TP0>TP1>TP2 inversate și confirmă cu Marius înainte de a corecta. **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/Ferestre_v2.xlsx b/data/Ferestre_v2.xlsx deleted file mode 100644 index cab36131d5202849b575a43d0e302a4e894f3f95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17803 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 diff --git a/scripts/generate_dashboard.py b/scripts/generate_dashboard.py index cfc1616..bcf4735 100644 --- a/scripts/generate_dashboard.py +++ b/scripts/generate_dashboard.py @@ -1,10 +1,18 @@ # -*- 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. +UN SINGUR fișier de analiză. Citește backtest.xlsx (read-only, data_only=True) +și scrie data/Dashboard.xlsx cu TOATE analizele, în sheet-uri separate: + + • Config — inputurile reale (lot, limite prop) + formule (din build_config). + • Trades — snapshot static ascuns (valori R_/$_ deja calculate de Excel). + • Dashboard — metrici 5 manageri + breakdown-uri + prop compliance (formule). + • Ferestre — AUTO-SCAN edge × durată × fiabilitate (python, valori statice). + Nicio fereastră hardcodată: grila se scanează la 15 min pe toate + cele 5 manageri și recomandările se DERIVĂ din datele curente. + • Toate ferestrele — grila completă scanată (toate ferestrele × 5 manageri) ca + tabel plat cu AutoFilter, ca să filtrezi/sortezi singur. + • date_grafic — sursa pentru curba de echitate din sheet-ul Ferestre. Reruleaza prin refresh_dashboard.bat (sau direct): python scripts/generate_dashboard.py @@ -12,19 +20,35 @@ Reruleaza prin refresh_dashboard.bat (sau direct): 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.) +lipsește și analiza iese goală. + +Istoric: înainte erau două fișiere (Dashboard.xlsx + Ferestre_v2.xlsx) cu două +scannere de ferestre — unul pe formule (grila fixă din build_dashboard), altul pe +python cu ferestre A/B/W hardcodate de o analiză LLM anterioară. Ambele au fost +înlocuite cu un singur auto-scan generativ (grila la 15 min e suprasetul ambelor). """ +import statistics +import random +from collections import defaultdict +from datetime import datetime, time, date, timedelta from pathlib import Path import openpyxl from openpyxl import Workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter +from openpyxl.chart import LineChart, Reference +from openpyxl.drawing.line import LineProperties +from openpyxl.chart.shapes import GraphicalProperties from generate_template import ( build_config, build_dashboard, TRADES_HEADERS, MAX_ROWS, + STRAT_KEYS, + STRAT_LABELS, ) SRC = Path(__file__).resolve().parent.parent / "data" / "backtest.xlsx" @@ -90,6 +114,612 @@ def copy_trades_values(wb: Workbook, ws_src) -> None: ws.sheet_state = "hidden" # snapshot intern; Dashboard e singurul vizibil util +# =========================================================================== +# FERESTRE — auto-scan edge × durată × fiabilitate (python, valori statice) +# =========================================================================== +# +# Înlocuiește vechiul Ferestre_v2.xlsx (ferestre A/B/W hardcodate). Aici NIMIC +# nu e fixat: grila de ferestre candidate se generează parametric la 15 min, +# fiecare e evaluată pe toate cele 5 manageri pe DATELE CURENTE, iar cele 3 +# recomandări (edge/durată, robust, volum) se aleg după criterii obiective. + +ROLUNI = {1: "Ian", 2: "Feb", 3: "Mar", 4: "Apr", 5: "Mai", 6: "Iun", + 7: "Iul", 8: "Aug", 9: "Sep", 10: "Oct", 11: "Noi", 12: "Dec"} + +# Parametrii grilei de scan (ora RO, în minute de la miezul nopții). +SCAN_START_MIN = 16 * 60 + 30 # 16:30 — cel mai devreme start testat +SCAN_LAST_START = 22 * 60 # 22:00 — cel mai târziu start testat +SCAN_HARD_END = 23 * 60 # 23:00 — capăt maxim al oricărei ferestre +SCAN_MIN_DUR = 45 # durată minimă fereastră (min) +SCAN_STEP = 15 # rezoluție (min) — suprasetul vechilor grile +SCAN_MIN_N = 30 # nr. minim de tranzacții ca o fereastră să conteze +NBOOT = 5000 # re-eșantioane bootstrap pentru CI + +# Câte ferestre intră în validarea detaliată din sheet-ul Ferestre (BRUT + cele 3 +# recomandări + restul = top-N profitabile auto-incluse). Pragurile decid ce e +# "profitabilă" pentru auto-includere — ridică-le ca să vezi mai puține/mai multe. +FERESTRE_MAX_VARIANTS = 20 # total maxim de rânduri validate (BRUT + recs + top scan) +FERESTRE_TOP_MIN_N = 40 # N minim pentru o fereastră suplimentară auto-inclusă +FERESTRE_TOP_MIN_EXPR = 0.10 # ExpR minim pentru "profitabilă" la auto-includere + + +def load_trades(path: Path) -> list[dict]: + """Citește tranzacțiile cu outcome din backtest.xlsx (valori cache Excel).""" + wb = openpyxl.load_workbook(path, read_only=True, data_only=True) + ws = wb["Trades"] + it = ws.iter_rows(min_row=1, values_only=True) + hdr = next(it) + h = {x: i for i, x in enumerate(hdr) if x is not None} + rows: list[dict] = [] + for row in it: + o = row[h["Outcome"]] + if o is None or str(o).strip() == "": + continue + t = row[h["Ora RO"]] + if isinstance(t, (int, float)): + hh = int(t); mm = round((t - hh) * 100); tt = time(hh, mm) + else: + p = str(t).split(":"); tt = time(int(p[0]), int(p[1])) + d = datetime.fromisoformat(str(row[h["Data"]])).date() + rec = {"d": d, "min": tt.hour * 60 + tt.minute, + "t": tt.strftime("%H:%M"), "dir": row[h["Direcție"]]} + for s in STRAT_KEYS: + rec["R_" + s] = row[h["R_" + s]] + rec["$_" + s] = row[h["$_" + s]] + rows.append(rec) + wb.close() + rows.sort(key=lambda r: (r["d"], r["min"])) + return rows + + +# ---------- engine ---------- +def in_window(rows, s, e): + return [r for r in rows if s <= r["min"] < e] + + +def apply_filter(rows, f): + """'toate' = tot; 'prima' = prima tranzacție cronologic din fiecare zi.""" + if f == "toate": + return rows + byd = defaultdict(list) + for r in rows: + byd[r["d"]].append(r) + out = [] + if f == "prima": + for d, rs in byd.items(): + out.append(rs[0]) + out.sort(key=lambda r: (r["d"], r["min"])) + return out + + +def metrics(rows, strat, acct, daily, maxl): + """Metrici + flag breach prop pe un subset, pentru un manager.""" + n = len(rows) + if n == 0: + return None + R = [r["R_" + strat] for r in rows] + D = [r["$_" + strat] for r in rows] + if any(x is None for x in R) or any(x is None for x in D): + return None + eq = acct; peak = acct; maxdd = 0.0 + cur = None; daycum = 0.0; daymin = 0.0; dbreach = False; mbreach = False + for r in rows: + if r["d"] != cur: + cur = r["d"]; daycum = 0.0; daymin = 0.0 + daycum += r["$_" + strat]; daymin = min(daymin, daycum) + if -daymin >= daily: + dbreach = True + eq += r["$_" + strat]; peak = max(peak, eq); maxdd = max(maxdd, peak - eq) + if peak - eq >= maxl: + mbreach = True + return dict(n=n, wr=sum(1 for x in R if x > 0) / n * 100, + exp=statistics.mean(R), totR=sum(R), totD=sum(D), + maxdd=maxdd, breach=dbreach or mbreach) + + +def monthly_expr(rows, strat) -> dict[str, float]: + bym = defaultdict(list) + for r in rows: + bym[f"{r['d']:%Y-%m}"].append(r["R_" + strat]) + return {m: statistics.mean(v) for m, v in bym.items()} + + +def bootstrap(rows, strat, nboot=NBOOT, seed=12345): + """Re-eșantionare cu înlocuire: CI 95% pentru ExpR.""" + rnd = random.Random(seed) + R = [r["R_" + strat] for r in rows] + n = len(R) + if n == 0: + return dict(expR_lo=0.0, expR_hi=0.0, p_pos=0.0) + means = [] + for _ in range(nboot): + means.append(sum(R[rnd.randrange(n)] for _ in range(n)) / n) + means.sort() + + def pct(p): + return means[min(len(means) - 1, int(p * len(means)))] + + return dict(expR_lo=pct(0.025), expR_hi=pct(0.975), + p_pos=sum(1 for x in means if x > 0) / nboot * 100) + + +# ---------- scan + recomandări ---------- +def scan(T, acct, daily, maxl): + """Scanează grila la 15 min × {toate, prima} × 5 manageri. + + Pentru fiecare fereastră alege managerul cu cel mai bun ExpR dintre cei + care NU sparg contul prop pe acea fereastră. Întoarce lista de candidați. + """ + cands = [] + for s in range(SCAN_START_MIN, SCAN_LAST_START + 1, SCAN_STEP): + for e in range(s + SCAN_MIN_DUR, SCAN_HARD_END + 1, SCAN_STEP): + for filt in ("toate", "prima"): + sel = apply_filter(in_window(T, s, e), filt) + if len(sel) < SCAN_MIN_N: + continue + best = None + for st in STRAT_KEYS: + m = metrics(sel, st, acct, daily, maxl) + if m is None or m["breach"]: + continue + if best is None or m["exp"] > best[1]["exp"]: + best = (st, m) + if best is None: + continue + st, m = best + mo = monthly_expr(sel, st) + cands.append(dict( + s=s, e=e, filt=filt, dur=e - s, strat=st, + n=m["n"], wr=m["wr"], exp=m["exp"], totD=m["totD"], + maxdd=m["maxdd"], + mpos=sum(1 for v in mo.values() if v > 0), mtot=len(mo), + )) + return cands + + +def _pick(cands, predicate, key, taken): + """Cel mai bun candidat (după key, desc) care trece predicate și nu e deja luat. + + Dedup pe (start, end) — un geam apare o singură dată în recomandări, indiferent + de filtru/manager (ca ROBUST 2 să nu repete fereastra deja luată de EDGE etc.). + """ + pool = [c for c in cands + if predicate(c) and (c["s"], c["e"]) not in taken] + if not pool: + return None + best = max(pool, key=key) + taken.add((best["s"], best["e"])) + return best + + +# Constante de reglare a recomandărilor (modifică-le ca să schimbi criteriile). +EDGE_DURATIONS = [45, 60, 90] # paliere de durată (min): cea mai mică fereastră + 1h + 1h30 +EDGE_MIN_EXPR = 0.10 # ExpR minim ca o fereastră "edge" să fie profitabilă +ROBUST_MIN_N = 40 # N minim pentru recomandările robust +ROBUST_GRADES = [ # grade de consistență (fracțiune minimă de luni pozitive) + ("ROBUST 1 (toate lunile)", 1.00), + ("ROBUST 2 (≥80% luni)", 0.80), + ("ROBUST 3 (≥60% luni)", 0.60), +] +VOLUM_MIN_EXPR = 0.10 # ExpR minim ca o fereastră de volum să conteze +VOLUM_MIN_N = 60 # N minim ca "VOLUM compact" să fie relevant statistic + + +def recommend(cands): + """Derivă recomandările din scan (etichete = ferestrele calculate, nimic fix). + + Familii: EDGE (cea mai mică fereastră profitabilă, pe paliere de durată), + ROBUST 1/2/3 (grade de consistență lunară de la strict la lax) și VOLUM (cele + mai multe tranzacții profitabile + cea mai densă fereastră). Pragurile sunt în + constantele de mai sus. Dedup: fiecare fereastră apare o singură dată. + """ + taken: set = set() + out = [] + + def add(role, predicate, key): + c = _pick(cands, predicate, key, taken) + if c: + out.append((role, c)) + + # EDGE — cea mai mică perioadă profitabilă, pe paliere de durată (45min, 1h, 1h30). + for dur in EDGE_DURATIONS: + h, m = divmod(dur, 60) + lab = f"EDGE {h}h{m:02d}" if h else f"EDGE {m}min" + add(lab, lambda c, d=dur: c["dur"] == d and c["exp"] >= EDGE_MIN_EXPR, + key=lambda c: (c["exp"], c["n"])) + + # ROBUST — grade de consistență lunară (strict → lax). + for role, frac in ROBUST_GRADES: + add(role, + lambda c, f=frac: c["n"] >= ROBUST_MIN_N and c["mpos"] / max(c["mtot"], 1) >= f, + key=lambda c: (c["exp"], c["n"])) + + # VOLUM — cel mai mare volum (relevant statistic) + cea mai SCURTĂ fereastră + # care încă are volum relevant (N≥VOLUM_MIN_N). + add("VOLUM (max N)", lambda c: c["exp"] >= VOLUM_MIN_EXPR, + key=lambda c: (c["n"], -c["dur"])) + add(f"VOLUM compact (N≥{VOLUM_MIN_N})", + lambda c: c["exp"] >= VOLUM_MIN_EXPR and c["n"] >= VOLUM_MIN_N, + key=lambda c: (-c["dur"], c["n"])) + return out + + +# ---------- styles (sheet Ferestre) ---------- +F_TITLE = Font(bold=True, size=14, color="1F4E78") +F_SUB = Font(bold=True, size=11, color="1F4E78") +F_HF = Font(bold=True, color="FFFFFF") +F_HFILL = PatternFill("solid", fgColor="1F4E78") +F_GOOD = PatternFill("solid", fgColor="C6EFCE") +F_WARN = PatternFill("solid", fgColor="FFEB9C") +F_BAD = PatternFill("solid", fgColor="FFC7CE") +F_GREY = PatternFill("solid", fgColor="F2F2F2") +F_CTR = Alignment(horizontal="center") +F_LEFT = Alignment(horizontal="left", wrap_text=True) +F_THIN = Border(left=Side(style="thin", color="BFBFBF"), + right=Side(style="thin", color="BFBFBF"), + top=Side(style="thin", color="BFBFBF"), + bottom=Side(style="thin", color="BFBFBF")) + + +def _fmt(m): + return f"{m // 60:02d}:{m % 60:02d}" + + +def _wlabel(cfg): + return "(fără fer.)" if cfg["e"] - cfg["s"] >= 1440 else f"{_fmt(cfg['s'])}-{_fmt(cfg['e'])}" + + +def _expcolor(e): + return F_GOOD if e >= 0.10 else (F_WARN if e >= 0 else F_BAD) + + +def build_ferestre(wb: Workbook, T: list[dict], acct, daily, maxl) -> None: + """Construiește sheet-urile Ferestre + date_grafic din auto-scan.""" + ws = wb.create_sheet("Ferestre") + ws.sheet_view.showGridLines = False + for i, w in enumerate([22, 14, 8, 9, 13, 7, 8, 9, 16, 9, 11, 10], 1): + ws.column_dimensions[get_column_letter(i)].width = w + + d0 = min(r["d"] for r in T); d1 = max(r["d"] for r in T) + alld = sorted(set(r["d"] for r in T)) + months = sorted({f"{r['d']:%Y-%m}" for r in T}) + cut = alld[int(len(alld) * 0.70)] + tr = [r for r in T if r["d"] < cut] + te = [r for r in T if r["d"] >= cut] + + cands = scan(T, acct, daily, maxl) + recs = recommend(cands) + + # BRUT (fără fereastră) — referință, managerul cel mai bun pe tot setul. + brut_strat = max( + STRAT_KEYS, + key=lambda st: metrics(T, st, acct, daily, maxl)["exp"], + ) + brut = dict(s=0, e=1440, filt="toate", strat=brut_strat) + + # VARIANTE pentru tabel + validări: BRUT + 3 recomandări + top scan suplimentar. + variants = [("BRUT (ref.)", brut, F_GREY)] + taken = {(0, 1440)} + for role, c in recs: + variants.append((role, c, F_GOOD)) + taken.add((c["s"], c["e"])) + for c in sorted(cands, key=lambda c: c["exp"], reverse=True): + if len(variants) >= FERESTRE_MAX_VARIANTS: + break + if (c["s"], c["e"]) in taken: + continue + if c["n"] < FERESTRE_TOP_MIN_N or c["exp"] < FERESTRE_TOP_MIN_EXPR: + continue + taken.add((c["s"], c["e"])) + variants.append(("top scan", c, None)) + + state = {"R": 1} + + def put(r, c, v, font=None, fill=None, fmtn=None, align=None, border=False): + cell = ws.cell(row=r, column=c, value=v) + if font: cell.font = font + if fill: cell.fill = fill + if fmtn: cell.number_format = fmtn + if align: cell.alignment = align + if border: cell.border = F_THIN + return cell + + def title(txt): + put(state["R"], 1, txt, F_SUB); state["R"] += 1 + + def headers(hs): + for j, hh in enumerate(hs, 1): + put(state["R"], j, hh, F_HF, F_HFILL, align=F_CTR, border=True) + state["R"] += 1 + + def row(vals, fills=None, fmts=None): + for j, v in enumerate(vals, 1): + f = fills[j - 1] if fills else None + nf = fmts[j - 1] if fmts else None + put(state["R"], j, v, fill=f, fmtn=nf, border=True, + align=F_CTR if j > 1 else F_LEFT) + state["R"] += 1 + + def note(txt): + put(state["R"], 1, txt, align=F_LEFT); state["R"] += 1 + + def blank(): + state["R"] += 1 + + def pad(vals, fills, fmts, n=12): + while len(vals) < n: + vals.append(""); fills.append(None); fmts.append(None) + return vals, fills, fmts + + def msel(rows, cfg): + return metrics(apply_filter(in_window(rows, cfg["s"], cfg["e"]), cfg["filt"]), + cfg["strat"], acct, daily, maxl) + + # ---- titlu (fără note de intro — tabelul de mai jos vorbește) ---- + put(state["R"], 1, "FERESTRE — auto-scan edge × durată × fiabilitate", F_TITLE) + state["R"] += 1 + blank() + + # ---- TABEL UNIC ---- + title("TABEL — ferestrele de top din scan (rol + manager cel mai bun)") + note(f"Roluri auto-derivate (nimic hardcodat): BRUT = fără fereastră (referință) · " + f"EDGE 45min/1h/1h30 = cea mai profitabilă fereastră la EXACT acea durată (45min = cea mai scurtă perioadă profitabilă) · " + f"ROBUST 1/2/3 = cel mai bun ExpR pozitiv în toate / ≥80% / ≥60% din luni (N≥{ROBUST_MIN_N}) · " + f"VOLUM (max N) = cel mai mare volum profitabil (relevant statistic) · VOLUM compact = cea mai scurtă fereastră cu N≥{VOLUM_MIN_N} · " + f"top scan = restul ferestrelor profitabile (N≥{FERESTRE_TOP_MIN_N}, ExpR≥{FERESTRE_TOP_MIN_EXPR:.2f}, non-breach) după ExpR, până la {FERESTRE_MAX_VARIANTS} total. " + f"CI 95% = interval bootstrap · OOS = ExpR pe ultimele ~30% zile · Manager = cel mai bun dintre cei 5. " + "Vezi 'Toate ferestrele' pentru grila completă filtrabilă.") + headers(["Rol", "Fereastră", "Durată", "Filtru", "Manager", "N", "WR%", + "ExpR", "CI 95% ExpR", "OOS", "$ total", "maxDD$"]) + for role, cfg, fill in variants: + sel = apply_filter(in_window(T, cfg["s"], cfg["e"]), cfg["filt"]) + m = metrics(sel, cfg["strat"], acct, daily, maxl) + b = bootstrap(sel, cfg["strat"]) + oosm = msel(te, cfg); oosx = oosm["exp"] if oosm else 0.0 + dd = cfg["e"] - cfg["s"] + durs = "—" if dd >= 1440 else f"{dd // 60}h{dd % 60:02d}" + row([role, _wlabel(cfg), durs, cfg["filt"], STRAT_LABELS[cfg["strat"]], + m["n"], m["wr"], round(m["exp"], 3), + f"[{b['expR_lo']:+.2f};{b['expR_hi']:+.2f}]", round(oosx, 3), + round(m["totD"]), round(m["maxdd"])], + fills=[fill, None, None, None, None, None, None, None, None, + _expcolor(oosx), None, None], + fmts=[None, None, None, None, None, "0", '0.0"%"', "0.000", None, + "0.000", "$#,##0", "$#,##0"]) + blank() + + # ---- explicații validări (definiții, NU concluzii) ---- + title("CE ÎNSEAMNĂ VALIDĂRILE") + note("• Forward 1 (LUNAR): ExpR în fiecare lună separat. Pozitiv în toate = edge constant, nu noroc concentrat. N mic/lună → o tranzacție mișcă mult media.") + note("• Forward 2 (TRAIN/TEST): primele 70% din zile = train, ultimele 30% = test (nevăzut la alegere). ExpR test ≈ train → robust; mult mai mic/negativ → overfit.") + note("• Walk-forward (3 FELII): perioada în 3 bucăți cronologice. O regulă bună rămâne pozitivă în toate trei, nu doar la început.") + note("• Culori: VERDE ≥0.10R · GALBEN 0–0.10R · ROȘU negativ · gol = nicio tranzacție în acea felie/lună.") + blank() + + # ---- FORWARD 1 — LUNAR ---- + mlabels = [ROLUNI[int(m[5:7])] for m in months] + title("FORWARD 1 — consistență LUNARĂ (ExpR pe fiecare lună)") + headers(["Variantă", "Fereastră"] + mlabels) + for role, cfg, fill in variants: + sel = apply_filter(in_window(T, cfg["s"], cfg["e"]), cfg["filt"]) + bym = defaultdict(list) + for r in sel: + bym[f"{r['d']:%Y-%m}"].append(r["R_" + cfg["strat"]]) + vals = [role, _wlabel(cfg)]; fills = [fill, None]; fmts = [None, None] + for m in months: + rr = bym.get(m, []) + if rr: + e = statistics.mean(rr) + vals.append(round(e, 3)); fills.append(_expcolor(e)); fmts.append("0.000") + else: + vals.append(""); fills.append(F_GREY); fmts.append(None) + row(*pad(vals, fills, fmts, n=2 + len(months))) + blank() + + # ---- FORWARD 2 — TRAIN/TEST ---- + title("FORWARD 2 — TRAIN/TEST 70/30") + note(f"Train: {tr[0]['d']:%d.%m}–{cut:%d.%m} · Test/OOS: {cut:%d.%m}–{d1:%d.%m}. " + "Verde la ExpR test = edge-ul a ținut pe date nevăzute. Δ≈0 sau pozitiv = stabil; foarte negativ = overfit.") + headers(["Variantă", "Fereastră", "N train", "ExpR train", "N test", + "ExpR test (OOS)", "Δ (test−train)"]) + for role, cfg, fill in variants: + mtr = msel(tr, cfg); mte = msel(te, cfg) + etr = mtr["exp"] if mtr else 0.0; ete = mte["exp"] if mte else 0.0 + ntr = mtr["n"] if mtr else 0; nte = mte["n"] if mte else 0 + row([role, _wlabel(cfg), ntr, round(etr, 3), nte, round(ete, 3), + round(ete - etr, 3)], + fills=[fill, None, None, None, None, _expcolor(ete), None], + fmts=[None, None, "0", "0.000", "0", "0.000", "0.000"]) + blank() + + # ---- WALK-FORWARD — 3 FELII ---- + n3 = len(alld) // 3 + P = [set(alld[:n3]), set(alld[n3:2 * n3]), set(alld[2 * n3:])] + pr = [(alld[0], alld[n3 - 1]), (alld[n3], alld[2 * n3 - 1]), (alld[2 * n3], alld[-1])] + title("WALK-FORWARD — edge pe 3 FELII cronologice") + note("P1=%s–%s · P2=%s–%s · P3=%s–%s. Pozitiv (verde) în toate trei = edge stabil în timp." % ( + pr[0][0].strftime("%d.%m"), pr[0][1].strftime("%d.%m"), + pr[1][0].strftime("%d.%m"), pr[1][1].strftime("%d.%m"), + pr[2][0].strftime("%d.%m"), pr[2][1].strftime("%d.%m"))) + headers(["Variantă", "Fereastră", "P1 ExpR", "P2 ExpR", "P3 ExpR", "N total"]) + for role, cfg, fill in variants: + sel = apply_filter(in_window(T, cfg["s"], cfg["e"]), cfg["filt"]) + vals = [role, _wlabel(cfg)]; fills = [fill, None]; fmts = [None, None] + for ps in P: + rr = [r for r in sel if r["d"] in ps] + if rr: + e = statistics.mean([x["R_" + cfg["strat"]] for x in rr]) + vals.append(round(e, 3)); fills.append(_expcolor(e)); fmts.append("0.000") + else: + vals.append(""); fills.append(F_GREY); fmts.append(None) + vals.append(len(sel)); fills.append(None); fmts.append("0") + row(vals, fills, fmts) + blank() + + # ---- CALENDAR (FOMC/NFP) ---- + title("CALENDAR EVENIMENTE — influență?") + yrs = sorted({d.year for d in alld}) + + def first_fri(y, mo): + d = date(y, mo, 1) + while d.weekday() != 4: + d += timedelta(days=1) + return d + + NFP = {first_fri(y, mo) for y in yrs for mo in range(1, 13) + if date(y, mo, 1) <= d1} + headers(["Grup", "N", "WR%", "ExpR (best mgr)"]) + + def grp(rows): + if not rows: + return 0, 0, 0 + best = max(STRAT_KEYS, key=lambda st: statistics.mean([r["R_" + st] for r in rows])) + Rm = [r["R_" + best] for r in rows] + return len(Rm), sum(1 for x in Rm if x > 0) / len(Rm) * 100, statistics.mean(Rm) + + for label, rows in (("Zile NFP (prima vineri)", [r for r in T if r["d"] in NFP]), + ("Restul zilelor", [r for r in T if r["d"] not in NFP])): + n, wr, ex = grp(rows) + row([label, n, wr, round(ex, 3)], + fmts=[None, "0", '0.0"%"', "0.000"]) + note("Prea puține zile de eveniment pentru o regulă de news-filter; aici doar ca verificare că nu strică edge-ul.") + blank() + + # ---- GRAFIC — curbă de echitate pe primele 2 recomandări ---- + def _find(role_name): + for role, cfg, _ in variants: + if role == role_name: + return (role, cfg) + return None + + chart_variants = [v for v in (_find(ROBUST_GRADES[0][0]), _find("VOLUM (max N)")) if v] + if len(chart_variants) < 2: # fallback: primele 2 recomandări non-BRUT + for role, cfg, _ in variants: + if role != "BRUT (ref.)" and (role, cfg) not in chart_variants: + chart_variants.append((role, cfg)) + if len(chart_variants) == 2: + break + chart_variants = chart_variants[:2] + if len(chart_variants) == 2: + (r1, c1), (r2, c2) = chart_variants + l1 = f"{r1} {_fmt(c1['s'])}-{_fmt(c1['e'])}" + l2 = f"{r2} {_fmt(c2['s'])}-{_fmt(c2['e'])}" + title(f"GRAFIC — curbă de echitate ($ cumulativ): {l1} vs {l2}") + note("Aliniate pe dată. Compară câștigul și 'netezimea' celor mai bune două recomandări.") + chart_anchor = f"A{state['R'] + 1}" + + def daily_sum(cfg): + sel = apply_filter(in_window(T, cfg["s"], cfg["e"]), cfg["filt"]) + byd = defaultdict(float) + for r in sel: + byd[r["d"]] += r["$_" + cfg["strat"]] + return byd + + cumA = daily_sum(c1); cumB = daily_sum(c2) + ds = wb.create_sheet("date_grafic") + ds.append(["Data", l1, l2]) + accA = 0.0; accB = 0.0 + for d in alld: + accA += cumA.get(d, 0.0); accB += cumB.get(d, 0.0) + ds.append([d, round(accA), round(accB)]) + nrows = len(alld) + for rr in range(2, nrows + 2): + ds.cell(row=rr, column=1).number_format = "dd.mm" + + chart = LineChart() + chart.title = f"Curbă de echitate ($ cumulativ) — {l1} vs {l2}" + chart.style = 2 + chart.height = 9.5; chart.width = 24 + chart.y_axis.title = "$ cumulativ (cont prop)" + chart.x_axis.title = "Data" + chart.x_axis.number_format = "dd.mm" + chart.x_axis.majorTimeUnit = "days" + chart.x_axis.delete = False + chart.y_axis.delete = False + data = Reference(ds, min_col=2, max_col=3, min_row=1, max_row=nrows + 1) + cats = Reference(ds, min_col=1, min_row=2, max_row=nrows + 1) + chart.add_data(data, titles_from_data=True) + chart.set_categories(cats) + for s, color in zip(chart.series, ("2E7D32", "1F4E78")): + s.graphicalProperties = GraphicalProperties() + s.graphicalProperties.line = LineProperties(solidFill=color, w=20000) + s.smooth = False + ws.add_chart(chart, chart_anchor) + ds.column_dimensions["A"].width = 11 + for col in ("B", "C"): + ds.column_dimensions[col].width = 18 + + +def build_scan_table(wb: Workbook, T: list[dict], acct, daily, maxl) -> int: + """Sheet cu TOATE ferestrele scanate × toți 5 managerii — tabel plat filtrabil. + + Spre deosebire de sheet-ul Ferestre (care arată doar top-ul + validările), aici + e grila completă cu AutoFilter, ca Marius să filtreze singur după N, ExpR, + breach, manager, durată etc. Sortat descrescător după ExpR. + """ + ws = wb.create_sheet("Toate ferestrele") + ws.sheet_view.showGridLines = False + + alld = sorted(set(r["d"] for r in T)) + cut = alld[int(len(alld) * 0.70)] + te = [r for r in T if r["d"] >= cut] + + head = ["Start", "End", "Durată min", "Filtru", "Manager", "N", "WR%", + "ExpR", "OOS ExpR", "$ total", "maxDD$", "Breach", "Luni+", "Luni tot"] + for j, h in enumerate(head, 1): + c = ws.cell(1, j, h) + c.font = F_HF; c.fill = F_HFILL; c.alignment = F_CTR; c.border = F_THIN + + rows = [] + for s in range(SCAN_START_MIN, SCAN_LAST_START + 1, SCAN_STEP): + for e in range(s + SCAN_MIN_DUR, SCAN_HARD_END + 1, SCAN_STEP): + for filt in ("toate", "prima"): + sel = apply_filter(in_window(T, s, e), filt) + if not sel: + continue + seloos = apply_filter(in_window(te, s, e), filt) + for st in STRAT_KEYS: + m = metrics(sel, st, acct, daily, maxl) + if m is None: + continue + mo = monthly_expr(sel, st) + moos = metrics(seloos, st, acct, daily, maxl) + rows.append((s, e, filt, st, m, mo, moos)) + + rows.sort(key=lambda x: x[4]["exp"], reverse=True) + + r = 2 + for s, e, filt, st, m, mo, moos in rows: + oos = round(moos["exp"], 3) if moos else "" + vals = [_fmt(s), _fmt(e), e - s, filt, STRAT_LABELS[st], m["n"], + m["wr"], round(m["exp"], 3), oos, round(m["totD"]), + round(m["maxdd"]), "DA" if m["breach"] else "NU", + sum(1 for v in mo.values() if v > 0), len(mo)] + for j, v in enumerate(vals, 1): + cell = ws.cell(r, j, v) + cell.border = F_THIN + cell.alignment = F_LEFT if j in (4, 5) else F_CTR + ws.cell(r, 6).number_format = "0" + ws.cell(r, 7).number_format = '0.0"%"' + ws.cell(r, 8).number_format = "+0.000;-0.000;0.000" + if moos: + ws.cell(r, 9).number_format = "+0.000;-0.000;0.000" + ws.cell(r, 10).number_format = "$#,##0" + ws.cell(r, 11).number_format = "$#,##0" + ws.cell(r, 8).fill = _expcolor(m["exp"]) + if m["breach"]: + ws.cell(r, 12).fill = F_BAD + r += 1 + + last = r - 1 + if last >= 2: + ws.auto_filter.ref = f"A1:N{last}" + ws.freeze_panes = "A2" + for i, w in enumerate([7, 7, 11, 8, 14, 6, 8, 9, 10, 11, 11, 8, 7, 8], 1): + ws.column_dimensions[get_column_letter(i)].width = w + return len(rows) + + def main() -> int: if not SRC.exists(): print(f"EROARE: nu găsesc {SRC}") @@ -107,12 +737,30 @@ def main() -> int: 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") - + build_dashboard(wb) # Dashboard la index 2 — formule wb_src.close() + + # Ferestre — auto-scan (limitele prop din Config, cu fallback la default). + acct = cfg_inputs.get(9) or 50000.0 + daily = acct * (cfg_inputs.get(12) or 4.0) / 100.0 + maxl = acct * (cfg_inputs.get(14) or 7.0) / 100.0 + T = load_trades(SRC) + nscan = 0 + if T: + build_ferestre(wb, T, acct, daily, maxl) + nscan = build_scan_table(wb, T, acct, daily, maxl) + else: + print("ATENȚIE: niciun trade cu outcome în cache — sar peste sheet-urile de analiză.") + + # Ordine logică: rezumat (Ferestre) → grila completă → datele chart-ului. + order = ["Config", "Trades", "Dashboard", "Ferestre", + "Toate ferestrele", "date_grafic"] + wb._sheets.sort(key=lambda s: order.index(s.title) if s.title in order else 99) + + wb.active = wb.sheetnames.index("Dashboard") wb.save(OUT) - print(f"Scris {OUT}") + print(f"Scris {OUT} ({len(T)} tranzacții, {nscan} rânduri scan, " + f"sheet-uri: {wb.sheetnames})") return 0 diff --git a/scripts/generate_ferestre_v2.py b/scripts/generate_ferestre_v2.py deleted file mode 100644 index e80a3c5..0000000 --- a/scripts/generate_ferestre_v2.py +++ /dev/null @@ -1,460 +0,0 @@ -# -*- coding: utf-8 -*- -"""Generează data/Ferestre_v2.xlsx — analiză edge × durată × fiabilitate. - -CITEȘTE backtest.xlsx (read-only) și SCRIE un fișier nou separat. -NU atinge backtest.xlsx (păstrează dropdown-urile, chart-ul, tranzacțiile). -Reruleaza oricând după ce adaugi tranzacții noi: - python scripts/generate_ferestre_v2.py -""" -import statistics -import random -from collections import defaultdict -from datetime import datetime, time, date, timedelta -import openpyxl -from openpyxl.styles import Font, PatternFill, Alignment, Border, Side -from openpyxl.utils import get_column_letter -from openpyxl.chart import LineChart, Reference -from openpyxl.drawing.line import LineProperties -from openpyxl.chart.shapes import GraphicalProperties - -import os -_DATA = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data") -SRC = os.path.join(_DATA, "backtest.xlsx") -OUT = os.path.join(_DATA, "Ferestre_v2.xlsx") -STRATS = ['tp0only', 'tp1only', 'tp2only', 'hybrid_be', 'hybrid_nobe'] -ACCT, DAILY, MAXL = 50000.0, 2000.0, 3500.0 -ROLUNI = {1: 'Ian', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'Mai', 6: 'Iun', - 7: 'Iul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Noi', 12: 'Dec'} - - -# ---------- load ---------- -def load(): - wb = openpyxl.load_workbook(SRC, read_only=True, data_only=True) - ws = wb['Trades'] - it = ws.iter_rows(min_row=1, values_only=True) - hdr = next(it) - h = {x: i for i, x in enumerate(hdr) if x is not None} - rows = [] - for row in it: - o = row[h['Outcome']] - if o is None or str(o).strip() == '': - continue - t = row[h['Ora RO']] - if isinstance(t, (int, float)): - hh = int(t); mm = round((t - hh) * 100); tt = time(hh, mm) - else: - p = str(t).split(':'); tt = time(int(p[0]), int(p[1])) - d = datetime.fromisoformat(str(row[h['Data']])).date() - rec = {'num': row[h['#']], 'd': d, 'min': tt.hour * 60 + tt.minute, - 't': tt.strftime('%H:%M'), 'dir': row[h['Direcție']]} - for s in STRATS: - rec['R_' + s] = row[h['R_' + s]] - rec['$_' + s] = row[h['$_' + s]] - rows.append(rec) - rows.sort(key=lambda r: (r['d'], r['min'])) - return rows - - -# ---------- engine ---------- -def in_window(rows, s, e): - return [r for r in rows if s <= r['min'] < e] - - -def apply_filter(rows, f): - if f == 'toate': - return rows - byd = defaultdict(list) - for r in rows: - byd[r['d']].append(r) - out = [] - if f == 'prima': - for d, rs in byd.items(): - out.append(rs[0]) - elif f == 'prima2': - for d, rs in byd.items(): - out.extend(rs[:2]) - elif f == 'buy': - out = [r for r in rows if r['dir'] == 'Buy'] - elif f == 'sell': - out = [r for r in rows if r['dir'] == 'Sell'] - elif f == 'prima_buy': - for d, rs in byd.items(): - b = [r for r in rs if r['dir'] == 'Buy'] - if b: - out.append(b[0]) - elif f == 'prima_sell': - for d, rs in byd.items(): - x = [r for r in rs if r['dir'] == 'Sell'] - if x: - out.append(x[0]) - out.sort(key=lambda r: (r['d'], r['min'])) - return out - - -def metrics(rows, strat): - n = len(rows) - if n == 0: - return None - R = [r['R_' + strat] for r in rows] - D = [r['$_' + strat] for r in rows] - eq = ACCT; peak = ACCT; maxdd = 0.0 - cur = None; daycum = 0.0; daymin = 0.0; dbreach = False; mbreach = False - for r in rows: - if r['d'] != cur: - cur = r['d']; daycum = 0.0; daymin = 0.0 - daycum += r['$_' + strat]; daymin = min(daymin, daycum) - if -daymin >= DAILY: - dbreach = True - eq += r['$_' + strat]; peak = max(peak, eq); maxdd = max(maxdd, peak - eq) - if peak - eq >= MAXL: - mbreach = True - return dict(n=n, wr=sum(1 for x in R if x > 0) / n * 100, exp=statistics.mean(R), - totR=sum(R), totD=sum(D), maxdd=maxdd, breach=dbreach or mbreach) - - -def expr_on(rows, cfg): - """ExpR (R mediu) pentru cfg pe un subset de rânduri; None dacă nu sunt tranzacții.""" - sel = apply_filter(in_window(rows, cfg['s'], cfg['e']), cfg['filt']) - if not sel: - return None - return statistics.mean([r['R_' + cfg['strat']] for r in sel]) - - -def bootstrap(rows, strat, nboot=10000, seed=12345): - """Re-eșantionare cu înlocuire: distribuția lui ExpR și a $ total pe N tranzacții.""" - rnd = random.Random(seed) - R = [r['R_' + strat] for r in rows] - D = [r['$_' + strat] for r in rows] - n = len(R) - if n == 0: - return dict(n=0, expR_med=0, expR_lo=0, expR_hi=0, p_pos=0, p_02=0, totD_med=0, totD_lo=0, totD_hi=0) - means_R = []; tot_D = [] - for _ in range(nboot): - sample = [rnd.randrange(n) for _ in range(n)] - means_R.append(sum(R[i] for i in sample) / n) - tot_D.append(sum(D[i] for i in sample)) - means_R.sort(); tot_D.sort() - - def pct(arr, p): - return arr[min(len(arr) - 1, int(p * len(arr)))] - - return dict( - n=n, - expR_med=means_R[len(means_R) // 2], - expR_lo=pct(means_R, 0.025), expR_hi=pct(means_R, 0.975), - p_pos=sum(1 for x in means_R if x > 0) / nboot * 100, - p_02=sum(1 for x in means_R if x >= 0.20) / nboot * 100, - totD_med=tot_D[len(tot_D) // 2], - totD_lo=pct(tot_D, 0.025), totD_hi=pct(tot_D, 0.975), - ) - - -def fmt(m): - return f"{m // 60:02d}:{m % 60:02d}" - - -# ---------- styles ---------- -TITLE = Font(bold=True, size=14, color="1F4E78") -SUB = Font(bold=True, size=11, color="1F4E78") -HF = Font(bold=True, color="FFFFFF") -HFILL = PatternFill("solid", fgColor="1F4E78") -GOOD = PatternFill("solid", fgColor="C6EFCE") -WARNF = PatternFill("solid", fgColor="FFEB9C") -BAD = PatternFill("solid", fgColor="FFC7CE") -GREY = PatternFill("solid", fgColor="F2F2F2") -CTR = Alignment(horizontal="center") -LEFT = Alignment(horizontal="left", wrap_text=True) -THIN = Border(left=Side(style="thin", color="BFBFBF"), right=Side(style="thin", color="BFBFBF"), - top=Side(style="thin", color="BFBFBF"), bottom=Side(style="thin", color="BFBFBF")) - - -def build(): - T = load() - d0 = min(r['d'] for r in T); d1 = max(r['d'] for r in T) - alld = sorted(set(r['d'] for r in T)) - - A = dict(s=19 * 60 + 15, e=20 * 60 + 15, filt='toate', strat='hybrid_be') - B = dict(s=19 * 60 + 45, e=21 * 60 + 45, filt='prima', strat='hybrid_be') - W = dict(s=19 * 60 + 15, e=22 * 60 + 15, filt='prima', strat='hybrid_be') - cut = alld[int(len(alld) * 0.70)] - tr = [r for r in T if r['d'] < cut] - te = [r for r in T if r['d'] >= cut] - - # toate variantele analizate (folosite în tabelul unic ȘI în toate validările) - VARIANTS = [ - ("BRUT (referință)", dict(s=0, e=1440, filt='toate', strat='hybrid_be'), GREY), - ("A ⭐ edge/timp", A, GOOD), - ("familia 19:15", dict(s=19 * 60 + 15, e=20 * 60 + 45, filt='prima', strat='hybrid_be'), None), - ("familia 19:15", dict(s=19 * 60 + 15, e=21 * 60 + 15, filt='prima', strat='hybrid_be'), None), - ("B ⭐ echilibru", B, GOOD), - ("C direcție (fragil)", dict(s=19 * 60 + 15, e=21 * 60 + 15, filt='prima_buy', strat='tp1only'), WARNF), - ("familia 19:15", dict(s=19 * 60 + 15, e=21 * 60 + 45, filt='prima', strat='hybrid_be'), None), - ("W ⭐ volum/bani", W, GOOD), - ("fereastra ta", dict(s=19 * 60 + 30, e=22 * 60 + 45, filt='prima', strat='hybrid_be'), WARNF), - ("alt. mai lungă", dict(s=19 * 60 + 15, e=22 * 60 + 45, filt='prima', strat='hybrid_be'), None), - ("familia 19:15", dict(s=19 * 60 + 15, e=23 * 60, filt='prima', strat='hybrid_be'), None), - ] - - def msel(rows, cfg): - return metrics(apply_filter(in_window(rows, cfg['s'], cfg['e']), cfg['filt']), cfg['strat']) - - def wlabel(cfg): - dd = cfg['e'] - cfg['s'] - return "(fără fer.)" if dd >= 1440 else f"{fmt(cfg['s'])}-{fmt(cfg['e'])}" - - def expcolor(e): - return GOOD if e >= 0.10 else (WARNF if e >= 0 else BAD) - - wb = openpyxl.Workbook(); ws = wb.active; ws.title = "Ferestre v2" - ws.sheet_view.showGridLines = False - for i, w in enumerate([21, 15, 8, 13, 9, 9, 9, 16, 9, 10, 10, 9], 1): - ws.column_dimensions[get_column_letter(i)].width = w - state = {'R': 1} - - def put(r, c, v, font=None, fill=None, fmtn=None, align=None, border=False): - cell = ws.cell(row=r, column=c, value=v) - if font: cell.font = font - if fill: cell.fill = fill - if fmtn: cell.number_format = fmtn - if align: cell.alignment = align - if border: cell.border = THIN - return cell - - def title(txt): - put(state['R'], 1, txt, SUB); state['R'] += 1 - - def headers(hs): - for j, hh in enumerate(hs, 1): - put(state['R'], j, hh, HF, HFILL, align=CTR, border=True) - state['R'] += 1 - - def row(vals, fills=None, fmts=None): - for j, v in enumerate(vals, 1): - f = fills[j - 1] if fills else None - nf = fmts[j - 1] if fmts else None - put(state['R'], j, v, fill=f, fmtn=nf, border=True, align=CTR if j > 1 else LEFT) - state['R'] += 1 - - def note(txt): - put(state['R'], 1, txt, align=LEFT); state['R'] += 1 - - def blank(): - state['R'] += 1 - - def pad(vals, fills, fmts, n=12): - while len(vals) < n: - vals.append(""); fills.append(None); fmts.append(None) - return vals, fills, fmts - - put(state['R'], 1, "FERESTRE v2 — edge × durată × fiabilitate", TITLE); state['R'] += 1 - note(f"Sursă: backtest.xlsx · {len(T)} tranzacții M2D/DIA · {d0:%d.%m.%Y}–{d1:%d.%m.%Y} · ora RO · cont prop $50k (daily $2k / max $3.5k)") - note("Date corectate (typo #314/#298/#240). ExpR = R mediu/tranzacție · maxDD = drawdown maxim pe traseu · 'breach' = ar fi omorât contul prop.") - blank() - - title("CONCLUZII (citește întâi astea)") - for c in [ - f"1. Edge real dar MODEST. Pe toate cele {len(T)} de tranzacții, doar managementul hybrid_be e pozitiv (~+0.05R). Edge-ul vine din CÂND tranzacționezi, nu din ce management alegi.", - "2. Fereastra de aur = ~19:00–21:00 RO. Ora 18:00–19:00 e zonă moartă (−0.10R); orice fereastră care o include își diluează edge-ul. Ora de START optimă = 19:15.", - "3. Trei opțiuni recomandate: A = 19:15–20:15 (1h, edge maxim/tranzacție, timp minim) · B = 19:45–21:45 (2h, cel mai bun edge robust, trece pragul 0.20R) · W = 19:15–22:15 (3h, cei mai mulți bani raportat la timp: +$1.3k vs B, N=89, edge 0.17R sub prag). A prelungi până la 22:45 aduce doar ~+$61 marginal.", - "4. Pt durate SCURTE (≤2h) plasarea B (19:45-21:45) bate start-ul 19:15; 19:15 câștigă DOAR pe ferestre lungi (3h+). B rămâne cea mai de încredere (pozitiv în fiecare lună, cel mai puternic out-of-sample, cel mai bun interval bootstrap).", - "5. Bootstrap (10.000 scenarii): edge-ul e pozitiv în 98–99% din cazuri → e REAL, nu noroc. DAR mărimea lui e incertă: ~50% șansă să fie efectiv peste 0.20R. Adevărul probabil e 0.10–0.21R.", - "6. Filtrele direcționale (doar Buy — rândul C) dau ExpR mai mare, dar interval bootstrap mai larg cu limita de jos lângă 0 și depind de regimul bull → fragile. Vezi validările: edge-ul direcției se clatină pe felii, A/B/W nu. Opțiunile A/B/W nu depind de direcție.", - "7. Calendarul de evenimente (FOMC/NFP) NU influențează negativ; prea puține zile pt o regulă de news-filter.", - "8. Avertisment: ~5000 configurații scanate pe eșantion mic → tratează totul ca IPOTEZE de confirmat live, nu certitudini.", - ]: - note(c) - blank() - - # ===================== TABEL UNIC ===================== - title("TABEL UNIC — toate variantele (management hybrid_be, dacă nu scrie altfel în Filtru)") - note("Sortate după durată. Rol: ⭐ = recomandate (A edge/timp · B echilibru · W volum-bani) · BRUT = referință fără fereastră (sparge contul!) · " - "'fereastra ta' = 19:30-22:45 · C = variantă pe direcție (mai fragilă). CI 95% ExpR = interval bootstrap (dacă e tot peste 0 → edge robust). " - "OOS = edge pe ultimele ~6 săpt. (verde ≥0.10). Δ$ vs B = bani față de B. Toate sunt non-breach (maxDD ~$1.1–1.9k) EXCEPTÂND BRUT.") - bD = metrics(apply_filter(in_window(T, B['s'], B['e']), B['filt']), B['strat'])['totD'] - - def mrow(rol, cfg, fill): - sel = apply_filter(in_window(T, cfg['s'], cfg['e']), cfg['filt']) - m = metrics(sel, cfg['strat']); b = bootstrap(sel, cfg['strat']) - oosm = msel(te, cfg); oosx = oosm['exp'] if oosm else 0.0 - foos = expcolor(oosx) - dd = cfg['e'] - cfg['s'] - durs = "—" if dd >= 1440 else f"{dd // 60}h{dd % 60:02d}" - flt = cfg['filt'] + ("·tp1only" if cfg['strat'] == 'tp1only' else "") - row([rol, wlabel(cfg), durs, flt, m['n'], m['wr'], round(m['exp'], 3), - f"[{b['expR_lo']:+.2f};{b['expR_hi']:+.2f}]", round(oosx, 3), - round(m['totD']), round(m['totD'] - bD), round(m['maxdd'])], - fills=[fill, None, None, None, None, None, None, None, foos, None, None, None], - fmts=[None, None, None, None, '0', '0.0"%"', '0.000', None, '0.000', '$#,##0', '$#,##0', '$#,##0']) - - headers(["Rol", "Fereastră RO", "Durată", "Filtru", "N", "WR%", "ExpR", "CI 95% ExpR", "OOS", "$ total", "Δ$ vs B", "maxDD$"]) - for rol, cfg, fill in VARIANTS: - mrow(rol, cfg, fill) - blank() - note("Cum citești: B face $%d în 2h. W (19:15-22:15) face cu ~$%d mai mult dar în 3h și cu edge/tranzacție mai mic. " - "Fereastra ta (19:30-22:45) face MAI PUȚIN decât B — problema e start-ul la 19:30 (pierzi slotul tare 19:15-19:30). " - "BRUT (fără fereastră) sparge contul prop. C (direcție) are edge mai mare dar fragil." - % (round(bD), round(metrics(apply_filter(in_window(T, W['s'], W['e']), 'prima'), 'hybrid_be')['totD'] - bD))) - blank() - - # ===================== EXPLICAȚII VALIDĂRI ===================== - title("CE ÎNSEAMNĂ VALIDĂRILE (citește înainte de tabelele de mai jos)") - note("• ExpR = R mediu pe tranzacție = EDGE-ul. 1R = riscul tău pe o tranzacție (SL). +0.20R înseamnă că, în medie, câștigi 0.2× riscul pe fiecare tranzacție. Negativ = pierzi în medie.") - note("• Forward 1 (LUNAR): edge-ul calculat în FIECARE lună separat. Vrei pozitiv în toate lunile = edge constant, nu noroc concentrat într-o lună. Atenție: N mic/lună (6–17) → o singură tranzacție mișcă mult media.") - note("• Forward 2 (TRAIN/TEST): 'antrenez' pe primele 70% din zile, apoi verific pe ultimele 30% pe care nu le-am folosit la alegere (out-of-sample). ExpR test ≈ ExpR train → edge robust. ExpR test mult mai mic sau negativ → era 'potrivit pe trecut' (overfit).") - note("• Walk-forward (3 FELII): împart perioada în 3 bucăți cronologice egale. P1 = început, P2 și P3 = 'viitorul' față de P1. O regulă bună rămâne pozitivă în toate trei feliile — nu doar la început.") - note("• Culori în toate validările: VERDE = bun (≥0.10R) · GALBEN = slab (0–0.10R) · ROȘU = negativ. Gol = nicio tranzacție în acea felie/lună.") - blank() - - # ===================== FORWARD 1 — LUNAR (toate variantele) ===================== - months = sorted({f"{r['d']:%Y-%m}" for r in T}) - mlabels = [ROLUNI[int(m[5:7])] for m in months] - title("VALIDARE FORWARD 1 — consistență LUNARĂ (ExpR pe fiecare lună), TOATE variantele") - headers(pad(["Variantă", "Fereastră"] + mlabels, [None] * (2 + len(mlabels)), [None] * (2 + len(mlabels)))[0]) - for rol, cfg, fill in VARIANTS: - sel = apply_filter(in_window(T, cfg['s'], cfg['e']), cfg['filt']) - bym = defaultdict(list) - for r in sel: - bym[f"{r['d']:%Y-%m}"].append(r) - vals = [rol, wlabel(cfg)]; fills = [fill, None]; fmts = [None, None] - for m in months: - rr = bym.get(m, []) - if rr: - e = statistics.mean([x['R_' + cfg['strat']] for x in rr]) - vals.append(round(e, 3)); fills.append(expcolor(e)); fmts.append('0.000') - else: - vals.append(""); fills.append(GREY); fmts.append(None) - row(*pad(vals, fills, fmts)) - blank() - - # ===================== FORWARD 2 — TRAIN/TEST (toate variantele) ===================== - title("VALIDARE FORWARD 2 — TRAIN/TEST 70/30, TOATE variantele") - note(f"Train: {tr[0]['d']:%d.%m}–{cut:%d.%m} · Test/OOS: {cut:%d.%m}–{d1:%d.%m}. Verde la 'ExpR test' = edge-ul a ținut pe datele nevăzute. " - "Δ (test−train) aproape de 0 sau pozitiv = stabil; foarte negativ = overfit.") - headers(["Variantă", "Fereastră", "N train", "ExpR train", "N test", "ExpR test (OOS)", "Δ (test−train)", "", "", "", "", ""]) - for rol, cfg, fill in VARIANTS: - mtr = msel(tr, cfg); mte = msel(te, cfg) - etr = mtr['exp'] if mtr else 0.0; ete = mte['exp'] if mte else 0.0 - ntr = mtr['n'] if mtr else 0; nte = mte['n'] if mte else 0 - row([rol, wlabel(cfg), ntr, round(etr, 3), nte, round(ete, 3), round(ete - etr, 3), "", "", "", "", ""], - fills=[fill, None, None, None, None, expcolor(ete), None, None, None, None, None, None], - fmts=[None, None, '0', '0.000', '0', '0.000', '0.000', None, None, None, None, None]) - blank() - - # ===================== WALK-FORWARD — 3 FELII (toate variantele) ===================== - n3 = len(alld) // 3 - P = [set(alld[:n3]), set(alld[n3:2 * n3]), set(alld[2 * n3:])] - pr = [(alld[0], alld[n3 - 1]), (alld[n3], alld[2 * n3 - 1]), (alld[2 * n3], alld[-1])] - title("VALIDARE WALK-FORWARD — edge pe 3 FELII cronologice, TOATE variantele") - note("P1=%s–%s · P2=%s–%s · P3=%s–%s. Vrei pozitiv (verde) în toate trei = edge stabil în timp, nu doar la început." % ( - pr[0][0].strftime('%d.%m'), pr[0][1].strftime('%d.%m'), - pr[1][0].strftime('%d.%m'), pr[1][1].strftime('%d.%m'), - pr[2][0].strftime('%d.%m'), pr[2][1].strftime('%d.%m'))) - headers(["Variantă", "Fereastră", "P1 ExpR", "P2 ExpR", "P3 ExpR", "N total", "", "", "", "", "", ""]) - for rol, cfg, fill in VARIANTS: - sel = apply_filter(in_window(T, cfg['s'], cfg['e']), cfg['filt']) - vals = [rol, wlabel(cfg)]; fills = [fill, None]; fmts = [None, None] - for ps in P: - rr = [r for r in sel if r['d'] in ps] - if rr: - e = statistics.mean([x['R_' + cfg['strat']] for x in rr]) - vals.append(round(e, 3)); fills.append(expcolor(e)); fmts.append('0.000') - else: - vals.append(""); fills.append(GREY); fmts.append(None) - vals.append(len(sel)); fills.append(None); fmts.append('0') - row(*pad(vals, fills, fmts)) - note("Observă: A/B/W rămân verzi (pozitive) pe toate feliile = edge stabil. C (direcție) și fereastra ta se clatină mai mult de la o felie la alta. " - "Capcana overfit: dacă ai alege ORBEȘTE fereastra cu edge maxim pe P1, ea tinde să se prăbușească pe P2/P3 — de-aia preferăm stabilitatea, nu vârful.") - blank() - - # ===================== CALENDAR ===================== - title("CALENDAR EVENIMENTE — influență?") - FOMC = {date(2026, 1, 28), date(2026, 3, 18), date(2026, 4, 29)} - - def first_fri(y, mo): - d = date(y, mo, 1) - while d.weekday() != 4: - d += timedelta(days=1) - return d - - NFP = {first_fri(2026, mo) for mo in range(1, 6)} - headers(["Grup", "N", "WR%", "ExpR (hybrid_be)", "", "", "", "", "", "", "", ""]) - - def grp(rows): - Rm = [r['R_hybrid_be'] for r in rows] - return len(Rm), (sum(1 for x in Rm if x > 0) / len(Rm) * 100 if Rm else 0), (statistics.mean(Rm) if Rm else 0) - - A_all = apply_filter(in_window(T, A['s'], A['e']), 'toate') - for label, rows in (("Zile FOMC", [r for r in T if r['d'] in FOMC]), - ("Restul zilelor", [r for r in T if r['d'] not in FOMC]), - ("Zile NFP (prima vineri)", [r for r in T if r['d'] in NFP]), - ("Config A — toate zilele", A_all), - ("Config A — fără FOMC+NFP", [r for r in A_all if r['d'] not in FOMC | NFP])): - n, wr, ex = grp(rows) - row([label, n, wr, round(ex, 3), "", "", "", "", "", "", "", ""], - fmts=[None, '0', '0.0"%"', '0.000', None, None, None, None, None, None, None, None]) - note("Verdict: fără efect negativ măsurabil. FOMC/NFP au fost ușor POZITIVE; prea puține zile (3 FOMC, 5 NFP) pt o regulă de news-filter.") - blank() - - title("NOTE") - for n in [ - "• Edge real subțire: pe toate tranzacțiile, doar hybrid_be e pozitiv (~+0.05R). Edge-ul vine din CÂND, nu din management.", - "• 18:00–19:00 RO = zonă moartă (−0.10R). Ora de start optimă = 19:15.", - "• ~5000 configurații scanate → top-by-ExpR supraestimează. De-aia validăm cu lunar + train/test + walk-forward + bootstrap. ExpR ~0.2R pe N~50-94 = interval de încredere larg.", - "• Filtrele direcționale (buy/sell) dau edge nominal mai mare dar pică out-of-sample (regim). A/B/W nu depind de direcție.", - "• Reruleaza după ce adaugi tranzacții: python scripts/generate_ferestre_v2.py", - ]: - note(n) - blank() - - # ===================== GRAFIC ===================== - title("GRAFIC — curbă de echitate ($ cumulativ): B vs W (19:15-22:15)") - note("Ambele cu filtru Prima + management hybrid_be, pe contul prop $50k. Aliniate pe dată, ca să compari câștigul și 'netezimea'.") - chart_anchor = f"A{state['R'] + 1}" - - def daily_sum(cfg): - sel = apply_filter(in_window(T, cfg['s'], cfg['e']), cfg['filt']) - byd = defaultdict(float) - for r in sel: - byd[r['d']] += r['$_' + cfg['strat']] - return byd - - cumB = daily_sum(B); cumW = daily_sum(W) - ds = wb.create_sheet("date_grafic") - ds.append(["Data", "B 19:45-21:45", "W 19:15-22:15"]) - accB = 0.0; accW = 0.0 - for d in alld: - accB += cumB.get(d, 0.0); accW += cumW.get(d, 0.0) - ds.append([d, round(accB), round(accW)]) - nrows = len(alld) - for r in range(2, nrows + 2): - ds.cell(row=r, column=1).number_format = 'dd.mm' - - chart = LineChart() - chart.title = "Curbă de echitate ($ cumulativ) — B vs W (19:15-22:15)" - chart.style = 2 - chart.height = 9.5; chart.width = 24 - chart.y_axis.title = "$ cumulativ (cont prop)" - chart.x_axis.title = "Data" - chart.x_axis.number_format = 'dd.mm' - chart.x_axis.majorTimeUnit = "days" - chart.x_axis.delete = False - chart.y_axis.delete = False - data = Reference(ds, min_col=2, max_col=3, min_row=1, max_row=nrows + 1) - cats = Reference(ds, min_col=1, min_row=2, max_row=nrows + 1) - chart.add_data(data, titles_from_data=True) - chart.set_categories(cats) - for s, color in zip(chart.series, ("2E7D32", "1F4E78")): - s.graphicalProperties = GraphicalProperties() - s.graphicalProperties.line = LineProperties(solidFill=color, w=20000) - s.smooth = False - ws.add_chart(chart, chart_anchor) - ds.column_dimensions['A'].width = 11 - for col in ('B', 'C'): - ds.column_dimensions[col].width = 15 - - wb.save(OUT) - print(f"Scris {OUT} ({state['R']} rânduri, grafic la {chart_anchor}).") - - -if __name__ == "__main__": - build() diff --git a/scripts/generate_template.py b/scripts/generate_template.py index edf630f..18c644d 100644 --- a/scripts/generate_template.py +++ b/scripts/generate_template.py @@ -1,1785 +1,1320 @@ -"""Generator pentru data/backtest.xlsx. - -5 strategii de management comparate side-by-side pe semnale blackbox: - - TP0 only : 100% close la TP0 - - TP1 only : 100% OCO la SL/TP1 - - TP2 only : 100% OCO la SL/TP2 - - Hybrid + BE : 50% TP0 + mut SL la BE + 50% TP1 (recomandat de trader) - - Hybrid no BE : 50% TP0 + 50% TP1, fără BE (control pentru a izola valoarea BE-ului) - -Rulare: - pip install openpyxl - python scripts/generate_template.py -""" - -from __future__ import annotations - -import shutil -from datetime import date, datetime, time, timedelta -from pathlib import Path - -from openpyxl import Workbook -from openpyxl.formatting.rule import CellIsRule -from openpyxl.styles import Alignment, Border, Font, PatternFill, Side -from openpyxl.utils import get_column_letter -from openpyxl.worksheet.datavalidation import DataValidation - - -OUTPUT = Path(__file__).resolve().parent.parent / "data" / "backtest.xlsx" -MAX_ROWS = 500 # rânduri pre-completate cu formule în sheet-ul Trades - -# --------------------------------------------------------------------------- -# Styles -# --------------------------------------------------------------------------- - -HEADER_FILL = PatternFill("solid", fgColor="1F3864") -HEADER_FONT = Font(name="Calibri", size=11, bold=True, color="FFFFFF") -INPUT_FILL = PatternFill("solid", fgColor="FFF8E1") -DERIVED_FILL = PatternFill("solid", fgColor="E8F1FA") -HIDDEN_FILL = PatternFill("solid", fgColor="F0F0F0") -TITLE_FONT = Font(name="Calibri", size=16, bold=True, color="1F3864") -SUBTITLE_FONT = Font(name="Calibri", size=12, bold=True, color="1F3864") -THIN = Side(border_style="thin", color="BFBFBF") -BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN) -CENTER = Alignment(horizontal="center", vertical="center") -LEFT = Alignment(horizontal="left", vertical="center") -RIGHT = Alignment(horizontal="right", vertical="center") - - -# --------------------------------------------------------------------------- -# Lists -# --------------------------------------------------------------------------- - -STRATEGIES = ["M2D", "EMA cross", "Order block", "Liquidity sweep", "Custom"] -SESSIONS = ["A1", "A2", "A3", "B", "C", "D", "Other"] -INDICATORS = ["DIA", "US30", "SPY", "QQQ", "ES", "NQ"] -TIMEFRAMES = ["1min", "3min", "15min"] -DIRECTIONS = ["Buy", "Sell"] -OUTCOMES = ["SL", "TP0", "TP1", "TP2"] - -# Cele 5 strategii de management (sufix folosit în numele coloanelor) + label friendly -STRAT_KEYS = ["tp0only", "tp1only", "tp2only", "hybrid_be", "hybrid_nobe"] -STRAT_LABELS = { - "tp0only": "TP0 only", - "tp1only": "TP1 only", - "tp2only": "TP2 only", - "hybrid_be": "Hybrid + BE", - "hybrid_nobe": "Hybrid no BE", -} - -# --------------------------------------------------------------------------- -# Trades sheet — schema -# --------------------------------------------------------------------------- - -def _candidate_windows() -> list[tuple[str, time, time]]: - """Ferestre suprapuse intre 16:30 si 23:00, evaluate pe ora Romaniei.""" - base = datetime(2000, 1, 1, 16, 30) - last_start = datetime(2000, 1, 1, 22, 0) - hard_ends = [ - datetime(2000, 1, 1, 22, 45), - datetime(2000, 1, 1, 23, 0), - ] - durations = [timedelta(minutes=m) for m in (60, 90, 120, 180)] - seen: set[tuple[time, time]] = set() - windows: list[tuple[str, time, time]] = [] - - start = base - while start <= last_start: - ends = [start + d for d in durations] - ends += [end for end in hard_ends if end - start >= timedelta(minutes=60)] - for end in ends: - if end > hard_ends[-1]: - continue - key = (start.time(), end.time()) - if key in seen: - continue - seen.add(key) - windows.append((f"{start:%H:%M}-{end:%H:%M}", start.time(), end.time())) - start += timedelta(minutes=30) - return windows - - -TRADABLE_WINDOWS = _candidate_windows() - - -INPUT_HEADERS = [ - "#", "Data", "Ora RO", "Zi", "Sesiune", - "Strategie", "Indicator", "TF", - "Direcție", "SL %", "TP0 %", "TP1 %", "TP2 %", - "Outcome", "Notes", -] -DERIVED_HEADERS = ( - ["SL $", "SL $ Prop"] - + - [f"R_{s}" for s in STRAT_KEYS] - + [f"$_{s}" for s in STRAT_KEYS] - + [f"Bal_{s}" for s in STRAT_KEYS] - + [f"$Prop_{s}" for s in STRAT_KEYS] - + [f"BalProp_{s}" for s in STRAT_KEYS] -) -PRIMA_HELPERS = [f"PrimaWin_{idx}" for idx in range(len(TRADABLE_WINDOWS))] - -HELPER_HEADERS = ( - [f"Win_{s}" for s in STRAT_KEYS] - + [f"Peak_{s}" for s in STRAT_KEYS] - + [f"DD_{s}" for s in STRAT_KEYS] - + [f"DailyPL_{s}" for s in STRAT_KEYS] - + [f"PeakProp_{s}" for s in STRAT_KEYS] - + [f"DDProp_{s}" for s in STRAT_KEYS] - + PRIMA_HELPERS -) -TRADES_HEADERS = INPUT_HEADERS + DERIVED_HEADERS + HELPER_HEADERS - -# Mapă nume → literă coloană Excel -COL = {name: get_column_letter(i + 1) for i, name in enumerate(TRADES_HEADERS)} - - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - - -def _col_to_int(letter: str) -> int: - n = 0 - for ch in letter: - n = n * 26 + (ord(ch) - ord("A") + 1) - return n - - -# --------------------------------------------------------------------------- -# Config sheet -# --------------------------------------------------------------------------- - - -def build_config(wb: Workbook) -> None: - ws = wb.create_sheet("Config", 0) - ws.sheet_view.showGridLines = False - - ws["A1"] = "Config — editează doar celulele galbene" - ws["A1"].font = TITLE_FONT - ws.merge_cells("A1:C1") - - ws["A3"] = "Setting" - ws["B3"] = "Value" - ws["C3"] = "Note" - for c in ("A3", "B3", "C3"): - ws[c].font = HEADER_FONT - ws[c].fill = HEADER_FILL - ws[c].alignment = CENTER - - ws["A4"] = "Account Size Start ($)" - ws["B4"] = 10000 - ws["C4"] = "Balanța inițială pentru calcule $ și HWM (model abstract)" - - ws["A5"] = "Risk reper (%)" - ws["B5"] = 1.0 - ws["C5"] = "Reper opțional; $_* se calculează din SL% × Account Size Start" - - ws["A6"] = "Risk reper ($)" - ws["B6"] = "=B4*B5/100" - ws["C6"] = "Auto — informativ; nu este folosit în formulele $_*" - - for r in (4, 5): - ws.cell(row=r, column=2).fill = INPUT_FILL - ws.cell(row=r, column=2).border = BORDER - ws["B6"].fill = DERIVED_FILL - ws["B6"].border = BORDER - ws["B4"].number_format = "$#,##0" - ws["B5"].number_format = '0.0"%"' - ws["B6"].number_format = "$#,##0.00" - - # ---- Bloc Cont real TradeLocker / Prop Firm ---- - # Lanț de calcul transparent: Lot size -> $/punct -> $ risc la 1% mișcare preț. - # Toate coloanele $ din Trades folosesc B10*B11 (= $ risc la 1% preț pe poziția ta). - # Scalare la 100k: schimbi B9 (Account) și B10 (Lot size) — restul se recalculează. - ws["A8"] = "Cont real TradeLocker (prop firm)" - ws["A8"].font = SUBTITLE_FONT - ws.merge_cells("A8:C8") - - ws["A9"] = "Account Start ($)" - ws["B9"] = 50000 - ws["C9"] = "Balanța contului. Schimbă în 100000 pentru un cont de 100k." - - ws["A10"] = "Lot size per semnal (US30)" - ws["B10"] = 0.08 - ws["C10"] = ( - "Câte loturi US30 intri pe fiecare semnal (TradeLocker). " - "Pe 100k cu același risc %: 0.16." - ) - - ws["A11"] = "$ per 1% preț, la 1.0 lot" - ws["B11"] = "=B24*B25/100" - ws["C11"] = ( - "Auto = ($/punct la 1 lot, B24) × (Preț reper, B25 / 100). " - "Înmulțit cu Lot size (B10) dă riscul real per 1% — vezi B27." - ) - - ws["A12"] = "Daily Loss Limit (%)" - ws["B12"] = 4.0 - ws["C12"] = "Limită zilnică prop firm; depășire = cont mort" - - ws["A13"] = "Daily Loss Limit ($)" - ws["B13"] = "=B9*B12/100" - ws["C13"] = "Auto — derivat din Account Start (B9) și %" - - ws["A14"] = "Max Loss Limit (%)" - ws["B14"] = 7.0 - ws["C14"] = "Limită totală pe cont; depășire = cont mort" - - ws["A15"] = "Max Loss Limit ($)" - ws["B15"] = "=B9*B14/100" - ws["C15"] = "Auto — derivat din Account Start (B9) și %" - - # Escape hatch performanță: activează/dezactivează filtrul Prima per Indicator - ws["A17"] = "Activează filtru Prima" - ws["B17"] = "DA" - ws["C17"] = ( - "DA = adaugi rândurile Prima în window grid. " - "NU = doar Toate (workbook mai rapid)." - ) - ws["B17"].fill = INPUT_FILL - ws["B17"].border = BORDER - ws["B17"].alignment = CENTER - dv_prima = DataValidation( - type="list", formula1='"DA,NU"', allow_blank=False, - ) - dv_prima.add("B17") - ws.add_data_validation(dv_prima) - - # ---- Calibrare $/punct dintr-un ordin reper + derivate informative ---- - # Helper universal: dintr-un singur ordin TradeLocker (orice indicator) derivi - # $/punct, apoi $/1% preț. Pentru alt indicator schimbi cele 4 inputuri reper - # (B19-B22) + Preț reper curent (B25). Restul se recalculează singur. - ws["A18"] = "— Calibrare $/punct (dintr-un ordin reper TradeLocker) —" - ws["A18"].font = Font(name="Calibri", size=10, italic=True, bold=True, color="1F3864") - ws.merge_cells("A18:C18") - - ws["A19"] = "Preț entry (ordin reper)" - ws["B19"] = 50680.15 - ws["C19"] = "Prețul de intrare afișat în ticketul TradeLocker (orice ordin reper)." - - ws["A20"] = "Preț SL (ordin reper)" - ws["B20"] = 50618.24 - ws["C20"] = "Prețul SL din același ticket." - - ws["A21"] = "$ risc afișat (ordin reper)" - ws["B21"] = 495.24 - ws["C21"] = "Cifra $ pe care TradeLocker o arată la SL pentru ordinul reper." - - ws["A22"] = "Lot size (ordin reper)" - ws["B22"] = 0.08 - ws["C22"] = "Câte loturi avea ordinul reper (poate diferi de lotul tău curent, B10)." - - ws["A23"] = "↳ Distanță reper (puncte)" - ws["B23"] = "=ABS(B19-B20)" - ws["C23"] = "Auto = |entry − SL|. Aici ⇒ 61.91 puncte." - - ws["A24"] = "↳ $ per punct la 1.0 lot" - ws["B24"] = "=IF(OR(B23=0,B22=0),0,B21/B23/B22)" - ws["C24"] = ( - "Auto = $risc ÷ distanță ÷ lot reper. Pe US30 ⇒ $100/punct la 1 lot. " - "Asta intră în B11. Pe alt indicator se recalibrează singur." - ) - - ws["A25"] = "Preț reper (curent)" - ws["B25"] = 50700 - ws["C25"] = ( - "Nivelul curent al indicatorului; convertește % mișcare preț în puncte/$. " - "Pentru alt indicator pui prețul lui." - ) - - ws["A26"] = "↳ $ per punct (la lotul tău)" - ws["B26"] = "=B10*B24" - ws["C26"] = "Auto = Lot size (B10) × $/punct la 1 lot (B24). La 0.08 loturi ⇒ $8/punct." - - ws["A27"] = "↳ $ risc la 1% mișcare preț" - ws["B27"] = "=B10*B11" - ws["C27"] = ( - "Auto — INIMA calculului. $ pe trade = R × SL% × această valoare. " - "La 0.08 loturi US30 ⇒ ~$4,056." - ) - - for r in (9, 10, 12, 14, 19, 20, 21, 22, 25): # inputuri galbene - ws.cell(row=r, column=2).fill = INPUT_FILL - ws.cell(row=r, column=2).border = BORDER - for r in (11, 13, 15, 23, 24, 26, 27): # derived blue - ws.cell(row=r, column=2).fill = DERIVED_FILL - ws.cell(row=r, column=2).border = BORDER - - ws["B9"].number_format = "$#,##0" - ws["B10"].number_format = "0.00" - ws["B11"].number_format = "$#,##0" - ws["B12"].number_format = '0.0"%"' - ws["B13"].number_format = "$#,##0" - ws["B14"].number_format = '0.0"%"' - ws["B15"].number_format = "$#,##0" - ws["B19"].number_format = "#,##0.00" - ws["B20"].number_format = "#,##0.00" - ws["B21"].number_format = '"$"#,##0.00' - ws["B22"].number_format = "0.00" - ws["B23"].number_format = "#,##0.00" - ws["B24"].number_format = '"$"#,##0.00' - ws["B25"].number_format = "#,##0" - ws["B26"].number_format = '"$"#,##0.00' - ws["B27"].number_format = '"$"#,##0.00' - - # ---- Utilitar: calcul preț SL pentru TradeLocker (pas cu pas) ---- - # Forward helper: din SL% (TradeStation) + preț intrare (TradeLocker) + direcție - # → prețul exact la care pui SL în TradeLocker. Celule intermediare vizibile. - ws["A29"] = "— Utilitar: ce prețuri SL/TP pun în TradeLocker —" - ws["A29"].font = Font(name="Calibri", size=10, italic=True, bold=True, color="1F3864") - ws.merge_cells("A29:C29") - - ws["A30"] = "SL % (din TradeStation)" - ws["B30"] = 0.08 - ws["C30"] = "Procentul SL al semnalului (% mișcare de preț), ex: 0.08." - - ws["A31"] = "Preț intrare (TradeLocker)" - ws["B31"] = 50649.50 - ws["C31"] = "Prețul tău de intrare US30 (nu prețul reper de la B25)." - - ws["A32"] = "Direcție" - ws["B32"] = "Sell" - ws["C32"] = "Sell ⇒ SL DEASUPRA intrării. Buy ⇒ SL DEDESUBT." - - ws["A33"] = "↳ Distanță SL (puncte)" - ws["B33"] = "=B31*B30/100" - ws["C33"] = "Auto = Preț intrare × SL% / 100. Ex: 50649.50 × 0.08/100 ⇒ ~40.5 puncte." - - ws["A34"] = "↳ PREȚ SL de setat în TradeLocker" - ws["B34"] = '=IF(B32="Sell",B31+B33,B31-B33)' - ws["C34"] = "Sell: intrare + distanță. Buy: intrare − distanță. ASTA pui în TradeLocker." - - ws["A35"] = "↳ $ risc la acest SL" - ws["B35"] = "=B33*B26" - ws["C35"] = ( - "Auto = Distanță (puncte) × $/punct la lotul tău (B26). " - "Verificare: ar trebui să se potrivească cu cifra $ din ticketul TradeLocker." - ) - - for r in (30, 31, 32): # inputuri galbene - ws.cell(row=r, column=2).fill = INPUT_FILL - ws.cell(row=r, column=2).border = BORDER - for r in (33, 34, 35): # derived blue - ws.cell(row=r, column=2).fill = DERIVED_FILL - ws.cell(row=r, column=2).border = BORDER - # Output principal evidențiat - ws["A34"].font = Font(name="Calibri", size=11, bold=True, color="1F3864") - ws["B34"].font = Font(name="Calibri", size=11, bold=True) - ws["B32"].alignment = CENTER - - ws["B30"].number_format = '0.000"%"' - ws["B31"].number_format = "#,##0.00" - ws["B33"].number_format = "#,##0.00" - ws["B34"].number_format = "#,##0.00" - ws["B35"].number_format = '"$"#,##0.00' - - # --- TP0/TP1/TP2: aceeași logică, dar în direcția profitului (opus SL) --- - ws["A36"] = "TP0 % (din TradeStation)" - ws["B36"] = 0.03 - ws["C36"] = "Procentul TP0 al semnalului (% mișcare de preț)." - - ws["A37"] = "TP1 % (din TradeStation)" - ws["B37"] = 0.06 - ws["C37"] = "Procentul TP1 al semnalului." - - ws["A38"] = "TP2 % (din TradeStation)" - ws["B38"] = 0.08 - ws["C38"] = "Procentul TP2 al semnalului." - - ws["A39"] = "↳ PREȚ TP0 de setat" - ws["B39"] = '=IF(B32="Sell",B31-B31*B36/100,B31+B31*B36/100)' - ws["C39"] = ( - "Distanță = intrare × TP0% / 100. Sell: intrare − distanță. " - "Buy: intrare + distanță (TP-ul e opus SL-ului)." - ) - - ws["A40"] = "↳ PREȚ TP1 de setat" - ws["B40"] = '=IF(B32="Sell",B31-B31*B37/100,B31+B31*B37/100)' - ws["C40"] = "La fel, cu TP1%." - - ws["A41"] = "↳ PREȚ TP2 de setat" - ws["B41"] = '=IF(B32="Sell",B31-B31*B38/100,B31+B31*B38/100)' - ws["C41"] = "La fel, cu TP2%." - - for r in (36, 37, 38): # inputuri galbene - ws.cell(row=r, column=2).fill = INPUT_FILL - ws.cell(row=r, column=2).border = BORDER - ws.cell(row=r, column=2).number_format = '0.000"%"' - for r in (39, 40, 41): # output evidențiat (albastru, bold) - ws.cell(row=r, column=2).fill = DERIVED_FILL - ws.cell(row=r, column=2).border = BORDER - ws.cell(row=r, column=2).number_format = "#,##0.00" - ws.cell(row=r, column=2).font = Font(name="Calibri", size=11, bold=True) - ws.cell(row=r, column=1).font = Font( - name="Calibri", size=11, bold=True, color="1F3864" - ) - - dv_dir = DataValidation(type="list", formula1='"Buy,Sell"', allow_blank=False) - dv_dir.add("B32") - ws.add_data_validation(dv_dir) - - # Liste dropdown — coloanele E–J (6 coloane) - list_columns = [ - ("Strategii", STRATEGIES), - ("Sesiuni (auto)", SESSIONS), - ("Indicatori", INDICATORS), - ("TF", TIMEFRAMES), - ("Direcție", DIRECTIONS), - ("Outcome", OUTCOMES), - ] - for col_idx, (label, values) in enumerate(list_columns, start=5): - cell = ws.cell(row=3, column=col_idx, value=label) - cell.font = HEADER_FONT - cell.fill = HEADER_FILL - cell.alignment = CENTER - for row_idx, v in enumerate(values, start=4): - c = ws.cell(row=row_idx, column=col_idx, value=v) - c.alignment = CENTER - - widths = { - "A": 30, "B": 14, "C": 40, "D": 2, - "E": 14, "F": 14, "G": 13, "H": 10, "I": 10, "J": 12, - } - for col, w in widths.items(): - ws.column_dimensions[col].width = w - - -# --------------------------------------------------------------------------- -# Formula builders pentru Trades sheet -# --------------------------------------------------------------------------- - - -def _f_day(r: int) -> str: - d = f'{COL["Data"]}{r}' - return ( - f'=IF({d}="","",' - f'CHOOSE(WEEKDAY({d},2),"Lu","Ma","Mi","Jo","Vi","Sa","Du"))' - ) - - -def _f_session(r: int) -> str: - """Derivă Sesiunea M2D din Data + Ora RO.""" - d = f'{COL["Data"]}{r}' - t = f'{COL["Ora RO"]}{r}' - wd = f"WEEKDAY({d},2)" - mid_week = f"AND({wd}>=2,{wd}<=4)" - return ( - f'=IF(OR({d}="",{t}=""),"",' - f"IF(OR({wd}=1,{wd}=5),\"D\"," - f'IF(AND({t}>=TIME(15,30,0),{t}=TIME(16,35,0),{t}=TIME(17,0,0),{t}=TIME(18,0,0),{t}=TIME(22,0,0),{t} str: - """1 dacă trade-ul este prima cronologic pe (Data, Indicator) ÎN fereastra cu indexul win_idx. - - Guard suplimentar pe Config!B17 (escape hatch performanță): dacă utilizatorul - setează "NU", toate PrimaWin_* devin 0 instant, fără recalcularea COUNTIFS. - Outcome inclus în COUNTIFS ca să nu blocheze rândurile parțial completate. - """ - _, start_t, end_t = TRADABLE_WINDOWS[win_idx] - start_s = f"TIME({start_t.hour},{start_t.minute},0)" - end_s = f"TIME({end_t.hour},{end_t.minute},0)" - d = f'{COL["Data"]}{r}' - ind = f'{COL["Indicator"]}{r}' - t = f'{COL["Ora RO"]}{r}' - o = f'{COL["Outcome"]}{r}' - data_rng = _range("Data") - ind_rng = _range("Indicator") - ora_rng = _range("Ora RO") - outcome_rng = _range("Outcome") - return ( - f'=IF(Config!$B$17<>"DA",0,' - f'IF(OR({d}="",{t}="",{ind}="",{o}=""),0,' - f'IF(AND({t}>={start_s},{t}<{end_s}),' - f'IF(COUNTIFS({data_rng},{d},{ind_rng},{ind},' - f'{ora_rng},">="&{start_s},{ora_rng},"<"&{end_s},' - f'{ora_rng},"<"&{t},' - f'{outcome_rng},"<>"' - f')=0,1,0),0)))' - ) - - -def _f_r_tp0only(r: int) -> str: - o = f'{COL["Outcome"]}{r}' - sl = f'{COL["SL %"]}{r}' - tp0 = f'{COL["TP0 %"]}{r}' - return f'=IF({o}="","",IF({o}="SL",-1,{tp0}/{sl}))' - - -def _f_r_tp1only(r: int) -> str: - o = f'{COL["Outcome"]}{r}' - sl = f'{COL["SL %"]}{r}' - tp1 = f'{COL["TP1 %"]}{r}' - return ( - f'=IF({o}="","",' - f'IF(OR({o}="SL",{o}="TP0"),-1,{tp1}/{sl}))' - ) - - -def _f_r_tp2only(r: int) -> str: - o = f'{COL["Outcome"]}{r}' - sl = f'{COL["SL %"]}{r}' - tp2 = f'{COL["TP2 %"]}{r}' - return f'=IF({o}="","",IF({o}="TP2",{tp2}/{sl},-1))' - - -def _f_r_hybrid_be(r: int) -> str: - o = f'{COL["Outcome"]}{r}' - sl = f'{COL["SL %"]}{r}' - tp0 = f'{COL["TP0 %"]}{r}' - tp1 = f'{COL["TP1 %"]}{r}' - return ( - f'=IF({o}="","",' - f'IF({o}="SL",-1,' - f'IF({o}="TP0",0.5*{tp0}/{sl},' - f'0.5*({tp0}+{tp1})/{sl})))' - ) - - -def _f_r_hybrid_nobe(r: int) -> str: - o = f'{COL["Outcome"]}{r}' - sl = f'{COL["SL %"]}{r}' - tp0 = f'{COL["TP0 %"]}{r}' - tp1 = f'{COL["TP1 %"]}{r}' - return ( - f'=IF({o}="","",' - f'IF({o}="SL",-1,' - f'IF({o}="TP0",0.5*{tp0}/{sl}-0.5,' - f'0.5*({tp0}+{tp1})/{sl})))' - ) - - -R_FN: dict[str, callable] = { - "tp0only": _f_r_tp0only, - "tp1only": _f_r_tp1only, - "tp2only": _f_r_tp2only, - "hybrid_be": _f_r_hybrid_be, - "hybrid_nobe": _f_r_hybrid_nobe, -} - - -def _f_dollar(r: int, r_col: str) -> str: - """$ P&L per trade = R × SL% × Contracte × $/1% per contract (TradeLocker real).""" - rc = f"{COL[r_col]}{r}" - sl = f"{COL['SL %']}{r}" - return f'=IF({rc}="","",{rc}*{sl}*Config!$B$10*Config!$B$11)' - - -def _f_sl_dollar(r: int) -> str: - """SL $ = SL% × Contracte × $/1% per contract.""" - sl = f"{COL['SL %']}{r}" - return f'=IF({sl}="","",{sl}*Config!$B$10*Config!$B$11)' - - -def _f_sl_dollar_prop(r: int) -> str: - """SL $ pe contul de prop — același cont real, formula identică cu SL $.""" - sl = f"{COL['SL %']}{r}" - return f'=IF({sl}="","",{sl}*Config!$B$10*Config!$B$11)' - - -def _f_balance(r: int, dollar_col: str) -> str: - dc = COL[dollar_col] - return f'=IF({dc}{r}="","",Config!$B$4 + SUM(${dc}$2:{dc}{r}))' - - -def _f_win(r: int, r_col: str) -> str: - rc = f"{COL[r_col]}{r}" - return f'=IF({rc}="","",IF({rc}>0,1,0))' - - -def _f_peak(r: int, balance_col: str, peak_col: str) -> str: - bc = COL[balance_col] - pc = COL[peak_col] - if r == 2: - return f'=IF({bc}{r}="","",{bc}{r})' - return ( - f'=IF({bc}{r}="","",' - f'IF({pc}{r-1}="",{bc}{r},MAX({pc}{r-1},{bc}{r})))' - ) - - -def _f_drawdown(r: int, peak_col: str, balance_col: str) -> str: - pc = f"{COL[peak_col]}{r}" - bc = f"{COL[balance_col]}{r}" - return f'=IF({bc}="","",{pc}-{bc})' - - -def _f_dollar_prop(r: int, r_col: str) -> str: - """$ P&L pe contul de prop — același calcul ca _f_dollar (cont real TradeLocker). - - Diferența între cont abstract și prop e doar balanța de start; $-ul per trade - e identic pentru că reflectă realitatea contractelor tranzacționate. - """ - rc = f"{COL[r_col]}{r}" - sl = f"{COL['SL %']}{r}" - return f'=IF({rc}="","",{rc}*{sl}*Config!$B$10*Config!$B$11)' - - -def _f_balance_prop(r: int, dollar_col: str) -> str: - dc = COL[dollar_col] - return f'=IF({dc}{r}="","",Config!$B$9 + SUM(${dc}$2:{dc}{r}))' - - -def _f_daily_pl(r: int, dollar_col: str) -> str: - """Cumul P&L pe ziua curentă (până la rândul r inclusiv).""" - dc = COL[dollar_col] - d_col = COL["Data"] - d = f"{d_col}{r}" - return ( - f'=IF(OR({dc}{r}="",{d}=""),"",' - f'SUMIFS(${dc}$2:{dc}{r},${d_col}$2:{d_col}{r},{d}))' - ) - - -# --------------------------------------------------------------------------- -# Trades sheet -# --------------------------------------------------------------------------- - - -def build_trades(wb: Workbook) -> None: - ws = wb.create_sheet("Trades", 1) - ws.sheet_view.showGridLines = False - ws.freeze_panes = "B2" - - # Headers - for col_idx, header in enumerate(TRADES_HEADERS, start=1): - cell = ws.cell(row=1, column=col_idx, value=header) - cell.font = HEADER_FONT - cell.fill = HEADER_FILL - cell.alignment = CENTER - cell.border = BORDER - - # Formule pe toate rândurile pre-pregătite - for r in range(2, MAX_ROWS + 2): - ws.cell(row=r, column=1, value="=ROW()-1") - ws[f'{COL["Zi"]}{r}'] = _f_day(r) - ws[f'{COL["Sesiune"]}{r}'] = _f_session(r) - ws[f'{COL["SL $"]}{r}'] = _f_sl_dollar(r) - ws[f'{COL["SL $ Prop"]}{r}'] = _f_sl_dollar_prop(r) - - for strat in STRAT_KEYS: - ws[f'{COL[f"R_{strat}"]}{r}'] = R_FN[strat](r) - ws[f'{COL[f"$_{strat}"]}{r}'] = _f_dollar(r, f"R_{strat}") - ws[f'{COL[f"Bal_{strat}"]}{r}'] = _f_balance(r, f"$_{strat}") - ws[f'{COL[f"Win_{strat}"]}{r}'] = _f_win(r, f"R_{strat}") - ws[f'{COL[f"Peak_{strat}"]}{r}'] = _f_peak( - r, f"Bal_{strat}", f"Peak_{strat}" - ) - ws[f'{COL[f"DD_{strat}"]}{r}'] = _f_drawdown( - r, f"Peak_{strat}", f"Bal_{strat}" - ) - # Prop firm tracking — paralel cu modelul abstract - ws[f'{COL[f"$Prop_{strat}"]}{r}'] = _f_dollar_prop(r, f"R_{strat}") - ws[f'{COL[f"BalProp_{strat}"]}{r}'] = _f_balance_prop(r, f"$Prop_{strat}") - ws[f'{COL[f"DailyPL_{strat}"]}{r}'] = _f_daily_pl(r, f"$Prop_{strat}") - ws[f'{COL[f"PeakProp_{strat}"]}{r}'] = _f_peak( - r, f"BalProp_{strat}", f"PeakProp_{strat}" - ) - ws[f'{COL[f"DDProp_{strat}"]}{r}'] = _f_drawdown( - r, f"PeakProp_{strat}", f"BalProp_{strat}" - ) - - # Coloanele PrimaWin_ — 1 dacă trade-ul e prima per (Data, Indicator) în fereastră - for win_idx in range(len(TRADABLE_WINDOWS)): - ws[f'{COL[f"PrimaWin_{win_idx}"]}{r}'] = _f_prima_in_window(r, win_idx) - - # Sample rows - SAMPLE_ROWS = [ - # (data, ora, strat, ind, tf, dir, sl, tp0, tp1, tp2, outcome, notes) - (date(2026, 5, 13), time(17, 33), "M2D", "DIA", "1min", "Sell", 0.30, 0.10, 0.15, 0.30, "TP1", "Prima DIA în 16:30-18:00"), - (date(2026, 5, 13), time(17, 50), "M2D", "DIA", "1min", "Buy", 0.25, 0.10, 0.15, 0.25, "SL", "DIA a doua oară — NU Prima în 16:30-18:00, dar Prima în 17:30-19:00"), - (date(2026, 5, 13), time(17, 34), "M2D", "SPY", "1min", "Sell", 0.20, 0.08, 0.12, 0.20, "TP1", "SPY — indicator diferit, Prima independent"), - (date(2026, 5, 13), time(17, 40), "M2D", "DIA", "1min", "Sell", 0.20, 0.08, 0.12, 0.20, "", "Outcome gol — test D1: NU blochează Prima pentru row 2/3"), - (date(2026, 5, 14), time(22, 15), "M2D", "DIA", "1min", "Sell", 0.30, 0.10, 0.15, 0.30, "TP0", "Zi diferită — Prima reset per (Data, Indicator, Fereastră)"), - ] - for offset, sample in enumerate(SAMPLE_ROWS): - r = 2 + offset - data_v, ora, strat_v, ind, tf, dirn, sl, tp0, tp1, tp2, outcome, notes = sample - ws[f"B{r}"] = data_v - ws[f"C{r}"] = ora - ws[f'{COL["Strategie"]}{r}'] = strat_v - ws[f'{COL["Indicator"]}{r}'] = ind - ws[f'{COL["TF"]}{r}'] = tf - ws[f'{COL["Direcție"]}{r}'] = dirn - ws[f'{COL["SL %"]}{r}'] = sl - ws[f'{COL["TP0 %"]}{r}'] = tp0 - ws[f'{COL["TP1 %"]}{r}'] = tp1 - ws[f'{COL["TP2 %"]}{r}'] = tp2 - ws[f'{COL["Outcome"]}{r}'] = outcome - ws[f'{COL["Notes"]}{r}'] = notes - - # Number formats - for col_name in ("SL %", "TP0 %", "TP1 %", "TP2 %"): - for r in range(2, MAX_ROWS + 2): - ws[f"{COL[col_name]}{r}"].number_format = '0.000"%"' - - for col_name in ("SL $", "SL $ Prop"): - for r in range(2, MAX_ROWS + 2): - ws[f"{COL[col_name]}{r}"].number_format = '"$"#,##0.00' - - for strat in STRAT_KEYS: - for r in range(2, MAX_ROWS + 2): - ws[f"{COL[f'R_{strat}']}{r}"].number_format = "+0.000;-0.000;0.000" - for prefix in ( - "$_", "Bal_", "Peak_", "DD_", - "$Prop_", "BalProp_", "DailyPL_", "PeakProp_", "DDProp_", - ): - ws[f"{COL[f'{prefix}{strat}']}{r}"].number_format = '"$"#,##0.00' - - for r in range(2, MAX_ROWS + 2): - ws[f"B{r}"].number_format = "yyyy-mm-dd" - - # Coloring - input_letters = { - COL[n] - for n in ( - "Data", "Ora RO", "Strategie", "Indicator", "TF", - "Direcție", "SL %", "TP0 %", "TP1 %", "TP2 %", - "Outcome", "Notes", - ) - } - derived_letters = {COL["Zi"], COL["Sesiune"], COL["SL $"], COL["SL $ Prop"]} - for strat in STRAT_KEYS: - for prefix in ("R_", "$_", "Bal_", "$Prop_", "BalProp_"): - derived_letters.add(COL[f"{prefix}{strat}"]) - helper_letters = set() - for strat in STRAT_KEYS: - for prefix in ("Win_", "Peak_", "DD_", "DailyPL_", "PeakProp_", "DDProp_"): - helper_letters.add(COL[f"{prefix}{strat}"]) - for win_idx in range(len(TRADABLE_WINDOWS)): - helper_letters.add(COL[f"PrimaWin_{win_idx}"]) - - for r in range(2, MAX_ROWS + 2): - for cl in input_letters: - ws[f"{cl}{r}"].fill = INPUT_FILL - for cl in derived_letters: - ws[f"{cl}{r}"].fill = DERIVED_FILL - for cl in helper_letters: - ws[f"{cl}{r}"].fill = HIDDEN_FILL - - # Column widths - widths = { - "A": 5, "B": 12, "C": 9, "D": 5, "E": 9, - "F": 12, "G": 11, "H": 8, "I": 9, - "J": 9, "K": 9, "L": 9, "M": 9, - "N": 11, "O": 28, - } - for col, w in widths.items(): - ws.column_dimensions[col].width = w - for col_name in ("SL $", "SL $ Prop"): - ws.column_dimensions[COL[col_name]].width = 12 - # Derived + helper: width 11 - for strat in STRAT_KEYS: - for prefix in ( - "R_", "$_", "Bal_", "Win_", "Peak_", "DD_", - "$Prop_", "BalProp_", "DailyPL_", "PeakProp_", "DDProp_", - ): - ws.column_dimensions[COL[f"{prefix}{strat}"]].width = 11 - - # Ascund helper-ele prop firm într-un outline collapsible - for strat in STRAT_KEYS: - for prefix in ("DailyPL_", "PeakProp_", "DDProp_"): - cl = COL[f"{prefix}{strat}"] - ws.column_dimensions[cl].outlineLevel = 1 - ws.column_dimensions[cl].hidden = True - - # Helper-ele PrimaWin_ — ~40 coloane la sfârșit, ascunse în outline - for win_idx in range(len(TRADABLE_WINDOWS)): - cl = COL[f"PrimaWin_{win_idx}"] - ws.column_dimensions[cl].width = 3 - ws.column_dimensions[cl].outlineLevel = 1 - ws.column_dimensions[cl].hidden = True - - # Data validation dropdowns - def _add_dv(col_name: str, source: str) -> None: - cl = COL[col_name] - dv = DataValidation( - type="list", formula1=source, - allow_blank=True, showErrorMessage=True, - ) - dv.error = "Valoare invalidă — folosește dropdown-ul." - dv.errorTitle = "Input invalid" - dv.add(f"{cl}2:{cl}{MAX_ROWS + 1}") - ws.add_data_validation(dv) - - # Config columns: E=Strategii, F=Sesiuni, G=Indicatori, H=TF, I=Direcție, J=Outcome - _add_dv("Strategie", "=Config!$E$4:$E$8") - _add_dv("Indicator", "=Config!$G$4:$G$9") - _add_dv("TF", "=Config!$H$4:$H$6") - _add_dv("Direcție", "=Config!$I$4:$I$5") - _add_dv("Outcome", "=Config!$J$4:$J$7") - - # Conditional formatting pe coloanele R (5 strategii) - green_fill = PatternFill("solid", fgColor="C6EFCE") - red_fill = PatternFill("solid", fgColor="FFC7CE") - grey_fill = PatternFill("solid", fgColor="D9D9D9") - for strat in STRAT_KEYS: - cl = COL[f"R_{strat}"] - rng = f"{cl}2:{cl}{MAX_ROWS + 1}" - ws.conditional_formatting.add( - rng, CellIsRule(operator="greaterThan", formula=["0"], fill=green_fill) - ) - ws.conditional_formatting.add( - rng, CellIsRule(operator="lessThan", formula=["0"], fill=red_fill) - ) - ws.conditional_formatting.add( - rng, CellIsRule(operator="equal", formula=["0"], fill=grey_fill) - ) - - -# --------------------------------------------------------------------------- -# Dashboard sheet -# --------------------------------------------------------------------------- - - -def _range(col_name: str) -> str: - cl = COL[col_name] - return f"Trades!${cl}$2:${cl}${MAX_ROWS + 1}" - - -METRIC_HINTS: dict[str, str] = { - "Trades Placed": ( - "Câte trade-uri ai logat în total.\n" - "Cu cât N e mai mare, cu atât celelalte metrici sunt mai de încredere.\n" - "Exemplu: la N=10 Win Ratio e zgomot pur, la N=40 începe să aibă semnal, la N=100 e solid." - ), - "Wins": ( - "Câte trade-uri s-au închis pe plus (R > 0).\n" - "Singur nu spune nimic — privește-l raportat la total (vezi Win Ratio mai jos)." - ), - "Win Ratio": ( - "Procentul de trade-uri câștigătoare. WR = 60% înseamnă 6 wins din 10 trade-uri.\n" - "Singur NU spune dacă strategia e profitabilă — citește-l împreună cu R:R de pe rândul următor." - ), - "Average Win ($)": ( - "Câștigul mediu pe trade-urile pozitive.\n" - "Comparat cu Average Loss îți spune cât de mari sunt câștigurile vs pierderile.\n" - "Exemplu: 4 wins de $50 și 2 wins de $80 — Average Win = $60." - ), - "Average Loss ($)": ( - "Pierderea medie pe trade-urile negative (cifra apare cu minus).\n" - "În dolari reali, −1R depinde de SL%: pierdere ≈ SL% × $ risc la 1% preț (Config B27).\n" - "Dacă e mult mai mare decât riscul calculat din SL, ai SL-uri sărite (slippage, gap-uri)." - ), - "Best Trade ($)": ( - "Cel mai mare câștig individual.\n" - "Dacă majoritatea profitului total vine dintr-un singur trade outlier, edge-ul e fragil — " - "elimini acel trade și strategia devine pierzătoare." - ), - "Worst Trade ($)": ( - "Cea mai mare pierdere individuală.\n" - "Ar trebui să fie aproximativ egală cu −1R calculat din SL% × $ risc la 1% preț (Config B27).\n" - "La 0.08 loturi US30: SL=0.30% × ~$4,056 ≈ −$1,217. Dacă e mult mai mare, ai slippage/gap." - ), - "Profit Factor": ( - "Total bani câștigați împărțit la total bani pierduți (în valoare absolută).\n" - "Sub 1.0 = pierzi pe ansamblu. Peste 1.5 = solid. Peste 2.0 = câștigi de 2× cât pierzi.\n" - "Exemplu: 4 wins de $50 (= $200) + 6 losses de $30 (= $180) — PF = 200÷180 = 1.11, profitabil marginal." - ), - "Risk:Reward": ( - "De câte ori e mai mare câștigul mediu decât pierderea medie.\n" - "R:R = 2 înseamnă: când câștigi, câștigi $2; când pierzi, pierzi $1.\n" - "Cu R:R mare poți avea Win Ratio mic și tot să faci bani." - ), - "Expectancy (R)": ( - "Cât câștigi în medie pe UN trade, exprimat în R.\n" - "+0.30R = câștigi 0.30 × riscul mediu al trade-urilor.\n" - "−0.10R = pierzi 0.10 × riscul mediu al trade-urilor.\n" - "Pragul de GO LIVE: +0.20R sau mai mult." - ), - "Expectancy ($)": ( - "Aceeași expectancy convertită în dolari, folosind SL% × $ risc la 1% preț (Config B27).\n" - "Util ca să vezi cât câștigi în medie pe trade în bani reali (TradeLocker), nu doar în R." - ), - "Cumulative P&L ($)": ( - "Suma profitului și pierderii pe toate trade-urile logate.\n" - "E ce-ai avea în plus (sau minus) față de balanța de start din Config." - ), - "HWM Balance ($)": ( - "Highest Watermark — cea mai mare balanță atinsă vreodată în jurnal.\n" - "Punct de referință pentru calculul drawdown-ului." - ), - "Max Drawdown ($)": ( - "Cea mai mare cădere ($) din vârf la fundul ulterior al balanței. Măsoară durerea psihologică maximă.\n" - "Exemplu: ai urcat la $11,500, ai coborât la $9,800 — DD = $1,700, adică 17% din peak.\n" - "Un drawdown mare la backtest e foarte greu de tolerat în live cu bani reali — așteaptă-te să renunți." - ), - # ---- Prop firm metrics ---- - "Account Prop Start ($)": ( - "Capitalul de start al contului de prop firm (default $50,000).\n" - "Editabil în Config B9." - ), - "$ risc la 1% preț ($)": ( - "Câți $ riști pe poziția ta reală la 1% mișcare de preț (Config B27).\n" - "Lanț: Lot size (B10) × $/punct la 1 lot (B24) = $/punct; × Preț reper (B25)/100 = $ per 1%.\n" - "$/punct la 1 lot (B24) e calibrat dintr-un ordin reper TradeLocker — merge pe orice indicator.\n" - "La 0.08 loturi US30 ⇒ ~$4,056. Pe 100k cu lot 0.16 se dublează (~$8,112)." - ), - "Cumulative P&L Prop ($)": ( - "Profitul total al contului de prop pe traseul logat.\n" - "Reflectă $ real (SL% × Contracte × $/1% per contract), nu un procent abstract din cont.\n" - "Adunat peste $50,000 dă balanța finală reală." - ), - "Final Balance Prop ($)": ( - "Balanța finală a contului de prop = Account Start (Config B9) + Cumulative P&L Prop.\n" - "Compar-o cu pragul de stop-out: Account Start − Max Loss Limit $ (Config B15)." - ), - "Worst Daily Loss ($)": ( - "Cea mai proastă pierdere cumulativă într-o zi calendaristică.\n" - "Dacă e mai mică decât −Daily Loss Limit $ (Config B13), ai depășit limita zilnică — cont mort.\n" - "Atenție: un singur breach = pierdere cont, indiferent dacă ai recuperat ulterior." - ), - "Daily Limit Status": ( - "PASS dacă nicio zi nu a depășit Daily Loss Limit $ (Config B13, auto din Account × %).\n" - "FAIL = strategia ar fi pierdut contul prin daily breach pe traseul logat." - ), - "Max Account Drawdown ($)": ( - "Cea mai mare cădere de la peak pe contul de prop.\n" - "Dacă > Max Loss Limit $ (Config B15, auto din Account × %), ai depășit limita — cont mort." - ), - "Max Loss Status": ( - "PASS dacă Max Account Drawdown ≤ Max Loss Limit $ (Config B15).\n" - "FAIL = strategia ar fi pierdut contul prin drawdown cumulativ." - ), - "Overall Prop Status": ( - "CONFORM = strategia ar fi supraviețuit pe contul de prop pe traseul logat.\n" - "CONT PIERDUT = cel puțin o breach (daily sau max) — strategia nu e viabilă pe acest cont prop." - ), -} - - -def build_dashboard(wb: Workbook) -> None: - ws = wb.create_sheet("Dashboard", 2) - ws.sheet_view.showGridLines = False - - ws["A1"] = "Backtest Dashboard" - ws["A1"].font = TITLE_FONT - ws.merge_cells("A1:G1") - - ws["A2"] = ( - "Comparație 5 strategii management — pe aceleași semnale blackbox" - ) - ws["A2"].font = Font(name="Calibri", size=10, italic=True, color="595959") - ws.merge_cells("A2:G2") - - # Row 4: headers (5 columns B-F pentru strategii + G pentru "Cum citesc") - ws["A4"] = "Metric" - strat_cols = {} # strat_key → column letter (B/C/D/E/F) - for i, strat in enumerate(STRAT_KEYS): - letter = get_column_letter(2 + i) - strat_cols[strat] = letter - ws[f"{letter}4"] = STRAT_LABELS[strat] - ws["G4"] = "Cum citesc" - for letter in ["A"] + list(strat_cols.values()) + ["G"]: - c = ws[f"{letter}4"] - c.font = HEADER_FONT - c.fill = HEADER_FILL - c.alignment = CENTER - c.border = BORDER - - # Ranges per strategie - R = {s: _range(f"R_{s}") for s in STRAT_KEYS} - D = {s: _range(f"$_{s}") for s in STRAT_KEYS} - W = {s: _range(f"Win_{s}") for s in STRAT_KEYS} - BAL = {s: _range(f"Bal_{s}") for s in STRAT_KEYS} - DD = {s: _range(f"DD_{s}") for s in STRAT_KEYS} - OUTCOME_RANGE = _range("Outcome") - - # Metric rows — fiecare metric e un dict cu per-strategy formula + format - metrics: list[tuple[str, callable, str]] = [ - # (label, fn(strat_key) -> formula, number_format) - ("Trades Placed", lambda s: f'=COUNTA({OUTCOME_RANGE})', "0"), - ("Wins", lambda s: f'=COUNTIF({W[s]},1)', "0"), - ("Average Win ($)", lambda s: f'=IFERROR(AVERAGEIF({D[s]},">0"),0)', '"$"#,##0.00'), - ("Average Loss ($)", lambda s: f'=IFERROR(AVERAGEIF({D[s]},"<0"),0)', '"$"#,##0.00'), - ("Best Trade ($)", lambda s: f'=IFERROR(MAX({D[s]}),0)', '"$"#,##0.00'), - ("Worst Trade ($)", lambda s: f'=IFERROR(MIN({D[s]}),0)', '"$"#,##0.00'), - ("Profit Factor", lambda s: f'=IFERROR(SUMIF({D[s]},">0")/ABS(SUMIF({D[s]},"<0")),0)', "0.00"), - # Win Ratio: depends on Wins + Trades Placed — handled after metrics list (placeholder) - ("Win Ratio", lambda s: None, "0.0%"), - # Risk:Reward — placeholder; bazat pe rândurile Avg Win/Loss - ("Risk:Reward", lambda s: None, "0.00"), - ("Expectancy (R)", lambda s: f'=IFERROR(AVERAGE({R[s]}),0)', "+0.000;-0.000;0.000"), - ("Expectancy ($)", lambda s: f'=IFERROR(AVERAGE({D[s]}),0)', '"$"#,##0.00'), - ("Cumulative P&L ($)", lambda s: f'=SUM({D[s]})', '"$"#,##0.00'), - # HWM — placeholder cu ref la Trades Placed (row 5) - ("HWM Balance ($)", lambda s: None, '"$"#,##0.00'), - ("Max Drawdown ($)", lambda s: f'=IFERROR(MAX({DD[s]}),0)', '"$"#,##0.00'), - ] - - # Determine row indexes pentru formule speciale (depind de poziție) - label_to_row = {label: 5 + idx for idx, (label, _, _) in enumerate(metrics)} - trades_row = label_to_row["Trades Placed"] - wins_row = label_to_row["Wins"] - avg_win_row = label_to_row["Average Win ($)"] - avg_loss_row = label_to_row["Average Loss ($)"] - - for idx, (label, fn, fmt) in enumerate(metrics): - r = 5 + idx - ws[f"A{r}"] = label - ws[f"A{r}"].font = Font(name="Calibri", size=11, bold=True) - ws[f"A{r}"].border = BORDER - ws[f"A{r}"].alignment = LEFT - for strat in STRAT_KEYS: - letter = strat_cols[strat] - if label == "Win Ratio": - formula = f"=IFERROR({letter}{wins_row}/{letter}{trades_row},0)" - elif label == "Risk:Reward": - formula = f"=IFERROR({letter}{avg_win_row}/ABS({letter}{avg_loss_row}),0)" - elif label == "HWM Balance ($)": - formula = ( - f"=IF({letter}{trades_row}=0,Config!$B$4,MAX({BAL[strat]}))" - ) - else: - formula = fn(strat) - cell = ws[f"{letter}{r}"] - cell.value = formula - cell.number_format = fmt - cell.fill = DERIVED_FILL - cell.border = BORDER - cell.alignment = RIGHT - # Coloana G — interpretare narativă + exemplu numeric - hint_cell = ws[f"G{r}"] - hint_cell.value = METRIC_HINTS.get(label, "") - hint_cell.font = Font(name="Calibri", size=10, color="595959") - hint_cell.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True) - hint_cell.border = BORDER - - # ---- FERESTRE CANDIDATE x STRATEGIE ---- - # Tabel principal pentru alegerea ferestrei tradabile. Drawdown-ul este - # calculat cu helper-e ascunse pe fereastra curenta, nu din DD global. - # DASH_WIN_COL: mapă nume → literă, ca să eliminăm hardcoding-ul de litere. - DASH_WIN_HEADERS = [ - "Fereastra", "Start", "End", "Filtru", "Strategie", - "N", "Wins", "WR", "Expectancy R", "Expectancy $", "Profit Factor", - "Cum P&L $", "Max Drawdown $", "Worst Daily Loss Prop $", - "Max Drawdown Prop $", "Daily Breach", "Max Loss Breach", - "Status Prop", "Status Edge", "Score_Toate", "Score_Prima", - ] - DASH_WIN_COL = { - name: get_column_letter(i + 1) for i, name in enumerate(DASH_WIN_HEADERS) - } - last_dash_col = DASH_WIN_COL[DASH_WIN_HEADERS[-1]] - - window_title_row = 5 + len(metrics) + 2 - ws[f"A{window_title_row}"] = "FERESTRE CANDIDATE x STRATEGIE" - ws[f"A{window_title_row}"].font = SUBTITLE_FONT - ws.merge_cells(f"A{window_title_row}:{last_dash_col}{window_title_row}") - - window_header_row = window_title_row + 1 - for col_idx, header in enumerate(DASH_WIN_HEADERS, start=1): - c = ws.cell(row=window_header_row, column=col_idx, value=header) - c.font = HEADER_FONT - c.fill = HEADER_FILL - c.alignment = CENTER - c.border = BORDER - - TIME_RANGE = _range("Ora RO") - PROP_D = {s: _range(f"$Prop_{s}") for s in STRAT_KEYS} - helper_start_col = 27 # AA, ascuns. - - def _emit_window_helpers( - visible_row: int, strat: str, combo_idx: int, - win_idx: int, use_prima: bool = False, - ) -> dict[str, str]: - base_col = helper_start_col + combo_idx * 7 - helper_names = ["Cum", "Peak", "DD", "DailyProp", "CumProp", "PeakProp", "DDProp"] - cols = {name: get_column_letter(base_col + idx) for idx, name in enumerate(helper_names)} - for idx, name in enumerate(helper_names): - col = get_column_letter(base_col + idx) - ws[f"{col}1"] = f"{name}_{visible_row}" - ws.column_dimensions[col].hidden = True - ws.column_dimensions[col].width = 3 - - start_cell = f"$B${visible_row}" - end_cell = f"$C${visible_row}" - dollar_col = COL[f"$_{strat}"] - prop_col = COL[f"$Prop_{strat}"] - time_col = COL["Ora RO"] - date_col = COL["Data"] - outcome_col = COL["Outcome"] - prima_col = COL[f"PrimaWin_{win_idx}"] if use_prima else None - - for helper_row, trade_row in enumerate(range(2, MAX_ROWS + 2), start=2): - in_window_base = ( - f'AND(Trades!${outcome_col}{trade_row}<>"",' - f"Trades!${time_col}{trade_row}>={start_cell}," - f"Trades!${time_col}{trade_row}<{end_cell})" - ) - if use_prima: - in_window = ( - f"AND({in_window_base}," - f"Trades!${prima_col}{trade_row}=1)" - ) - else: - in_window = in_window_base - dollar = f"Trades!${dollar_col}{trade_row}" - prop = f"Trades!${prop_col}{trade_row}" - if helper_row == 2: - ws[f"{cols['Cum']}{helper_row}"] = f"=IF({in_window},{dollar},0)" - ws[f"{cols['Peak']}{helper_row}"] = f"=MAX(0,{cols['Cum']}{helper_row})" - ws[f"{cols['CumProp']}{helper_row}"] = f"=IF({in_window},{prop},0)" - ws[f"{cols['PeakProp']}{helper_row}"] = f"=MAX(0,{cols['CumProp']}{helper_row})" - else: - prev = helper_row - 1 - ws[f"{cols['Cum']}{helper_row}"] = ( - f"={cols['Cum']}{prev}+IF({in_window},{dollar},0)" - ) - ws[f"{cols['Peak']}{helper_row}"] = ( - f"=MAX({cols['Peak']}{prev},{cols['Cum']}{helper_row})" - ) - ws[f"{cols['CumProp']}{helper_row}"] = ( - f"={cols['CumProp']}{prev}+IF({in_window},{prop},0)" - ) - ws[f"{cols['PeakProp']}{helper_row}"] = ( - f"=MAX({cols['PeakProp']}{prev},{cols['CumProp']}{helper_row})" - ) - ws[f"{cols['DD']}{helper_row}"] = ( - f"={cols['Peak']}{helper_row}-{cols['Cum']}{helper_row}" - ) - ws[f"{cols['DDProp']}{helper_row}"] = ( - f"={cols['PeakProp']}{helper_row}-{cols['CumProp']}{helper_row}" - ) - ws[f"{cols['DailyProp']}{helper_row}"] = ( - f'=IF({in_window},' - f'SUMIFS(Trades!${prop_col}$2:Trades!${prop_col}{trade_row},' - f'Trades!${date_col}$2:Trades!${date_col}{trade_row},Trades!${date_col}{trade_row},' - f'Trades!${time_col}$2:Trades!${time_col}{trade_row},">="&{start_cell},' - f'Trades!${time_col}$2:Trades!${time_col}{trade_row},"<"&{end_cell}),' - f'"")' - ) - return cols - - pass_fill = PatternFill("solid", fgColor="C6EFCE") - fail_fill = PatternFill("solid", fgColor="FFC7CE") - warn_fill = PatternFill("solid", fgColor="FFEB9C") - combo_rows: list[int] = [] - combo_idx = 0 - row = window_header_row + 1 - - # Pre-compute column letters from DASH_WIN_COL for legibility - A_ = DASH_WIN_COL["Fereastra"] - B_ = DASH_WIN_COL["Start"] - C_ = DASH_WIN_COL["End"] - D_ = DASH_WIN_COL["Filtru"] - E_ = DASH_WIN_COL["Strategie"] - F_ = DASH_WIN_COL["N"] - G_ = DASH_WIN_COL["Wins"] - H_ = DASH_WIN_COL["WR"] - I_ = DASH_WIN_COL["Expectancy R"] - J_ = DASH_WIN_COL["Expectancy $"] - K_ = DASH_WIN_COL["Profit Factor"] - L_ = DASH_WIN_COL["Cum P&L $"] - M_ = DASH_WIN_COL["Max Drawdown $"] - N_ = DASH_WIN_COL["Worst Daily Loss Prop $"] - O_ = DASH_WIN_COL["Max Drawdown Prop $"] - P_ = DASH_WIN_COL["Daily Breach"] - Q_ = DASH_WIN_COL["Max Loss Breach"] - R_LET = DASH_WIN_COL["Status Prop"] - S_LET = DASH_WIN_COL["Status Edge"] - T_LET = DASH_WIN_COL["Score_Toate"] - U_LET = DASH_WIN_COL["Score_Prima"] - - FILTERS = [("Toate", False), ("Prima", True)] - - for win_idx, (label, start_time, end_time) in enumerate(TRADABLE_WINDOWS): - for strat in STRAT_KEYS: - for filter_label, use_prima in FILTERS: - helper_cols = _emit_window_helpers( - row, strat, combo_idx, win_idx=win_idx, use_prima=use_prima, - ) - prima_range = ( - _range(f"PrimaWin_{win_idx}") if use_prima else None - ) - extra = f",{prima_range},1" if use_prima else "" - - ws[f"{A_}{row}"] = label - ws[f"{B_}{row}"] = start_time - ws[f"{C_}{row}"] = end_time - ws[f"{D_}{row}"] = filter_label - ws[f"{E_}{row}"] = STRAT_LABELS[strat] - ws[f"{F_}{row}"] = ( - f'=COUNTIFS({OUTCOME_RANGE},"<>",' - f'{TIME_RANGE},">="&{B_}{row},{TIME_RANGE},"<"&{C_}{row}{extra})' - ) - ws[f"{G_}{row}"] = ( - f'=COUNTIFS({W[strat]},1,{OUTCOME_RANGE},"<>",' - f'{TIME_RANGE},">="&{B_}{row},{TIME_RANGE},"<"&{C_}{row}{extra})' - ) - ws[f"{H_}{row}"] = f"=IFERROR({G_}{row}/{F_}{row},0)" - ws[f"{I_}{row}"] = ( - f'=IFERROR(AVERAGEIFS({R[strat]},{OUTCOME_RANGE},"<>",' - f'{TIME_RANGE},">="&{B_}{row},{TIME_RANGE},"<"&{C_}{row}{extra}),0)' - ) - ws[f"{J_}{row}"] = ( - f'=IFERROR(AVERAGEIFS({D[strat]},{OUTCOME_RANGE},"<>",' - f'{TIME_RANGE},">="&{B_}{row},{TIME_RANGE},"<"&{C_}{row}{extra}),0)' - ) - ws[f"{K_}{row}"] = ( - f'=IFERROR(SUMIFS({D[strat]},{D[strat]},">0",{OUTCOME_RANGE},"<>",' - f'{TIME_RANGE},">="&{B_}{row},{TIME_RANGE},"<"&{C_}{row}{extra})/' - f'ABS(SUMIFS({D[strat]},{D[strat]},"<0",{OUTCOME_RANGE},"<>",' - f'{TIME_RANGE},">="&{B_}{row},{TIME_RANGE},"<"&{C_}{row}{extra})),0)' - ) - ws[f"{L_}{row}"] = ( - f'=SUMIFS({D[strat]},{OUTCOME_RANGE},"<>",' - f'{TIME_RANGE},">="&{B_}{row},{TIME_RANGE},"<"&{C_}{row}{extra})' - ) - ws[f"{M_}{row}"] = ( - f'=IFERROR(MAX({helper_cols["DD"]}2:{helper_cols["DD"]}{MAX_ROWS + 1}),0)' - ) - ws[f"{N_}{row}"] = ( - f'=IFERROR(MIN({helper_cols["DailyProp"]}2:' - f'{helper_cols["DailyProp"]}{MAX_ROWS + 1}),0)' - ) - ws[f"{O_}{row}"] = ( - f'=IFERROR(MAX({helper_cols["DDProp"]}2:' - f'{helper_cols["DDProp"]}{MAX_ROWS + 1}),0)' - ) - ws[f"{P_}{row}"] = f'=IF({N_}{row}<-Config!$B$13,"DA","NU")' - ws[f"{Q_}{row}"] = f'=IF({O_}{row}>Config!$B$15,"DA","NU")' - ws[f"{R_LET}{row}"] = ( - f'=IF(OR({P_}{row}="DA",{Q_}{row}="DA"),' - f'"CONT PIERDUT","CONFORM")' - ) - ws[f"{S_LET}{row}"] = ( - f'=IF({F_}{row}<1,"",' - f'IF(OR({P_}{row}="DA",{Q_}{row}="DA"),"BREACH",' - f'IF(AND({F_}{row}>=40,{H_}{row}>=55%,{I_}{row}>=0.2),' - f'"CANDIDAT","PRE-CANDIDAT")))' - ) - ws[f"{T_LET}{row}"] = ( - f'=IF(OR({F_}{row}<1,{D_}{row}<>"Toate",' - f'{P_}{row}="DA",{Q_}{row}="DA"),-1E+12,' - f'{I_}{row}*20000+MIN({F_}{row},150)*100+' - f'{K_}{row}*1500+{L_}{row}-{M_}{row}-{O_}{row}/10)' - ) - ws[f"{U_LET}{row}"] = ( - f'=IF(OR({F_}{row}<1,{D_}{row}<>"Prima",' - f'{P_}{row}="DA",{Q_}{row}="DA"),-1E+12,' - f'{I_}{row}*20000+MIN({F_}{row},150)*100+' - f'{K_}{row}*1500+{L_}{row}-{M_}{row}-{O_}{row}/10)' - ) - combo_rows.append(row) - combo_idx += 1 - row += 1 - - # Indici 1-based ai coloanelor centrate - center_idx = { - DASH_WIN_HEADERS.index(name) + 1 - for name in ("Fereastra", "Filtru", "Strategie", - "Daily Breach", "Max Loss Breach", - "Status Prop", "Status Edge") - } - # Primele 5 coloane (Fereastra, Start, End, Filtru, Strategie) nu primesc fill derivat - no_fill_idx = set(range(1, 6)) - for r in combo_rows: - for c in range(1, len(DASH_WIN_HEADERS) + 1): - cell = ws.cell(row=r, column=c) - cell.border = BORDER - cell.alignment = CENTER if c in center_idx else RIGHT - if c not in no_fill_idx: - cell.fill = DERIVED_FILL - ws[f"{B_}{r}"].number_format = "hh:mm" - ws[f"{C_}{r}"].number_format = "hh:mm" - ws[f"{F_}{r}"].number_format = "0" - ws[f"{G_}{r}"].number_format = "0" - ws[f"{H_}{r}"].number_format = "0.0%" - ws[f"{I_}{r}"].number_format = "+0.000;-0.000;0.000" - for c_letter in (J_, L_, M_, N_, O_): - ws[f"{c_letter}{r}"].number_format = '"$"#,##0.00' - ws[f"{K_}{r}"].number_format = "0.00" - # Score_Toate și Score_Prima ascunse - ws.column_dimensions[T_LET].hidden = True - ws.column_dimensions[U_LET].hidden = True - - if combo_rows: - first_combo = combo_rows[0] - last_combo = combo_rows[-1] - status_rng = f"{R_LET}{first_combo}:{S_LET}{last_combo}" - ws.conditional_formatting.add( - status_rng, CellIsRule(operator="equal", formula=['"CONFORM"'], fill=pass_fill) - ) - ws.conditional_formatting.add( - status_rng, CellIsRule(operator="equal", formula=['"CANDIDAT"'], fill=pass_fill) - ) - ws.conditional_formatting.add( - status_rng, CellIsRule(operator="equal", formula=['"CONT PIERDUT"'], fill=fail_fill) - ) - ws.conditional_formatting.add( - status_rng, CellIsRule(operator="equal", formula=['"BREACH"'], fill=fail_fill) - ) - ws.conditional_formatting.add( - status_rng, CellIsRule(operator="equal", formula=['"PRE-CANDIDAT"'], fill=warn_fill) - ) - - # ---- TOP CANDIDATE — două sub-secțiuni: Toate + Prima ---- - # Score_Toate (col T) și Score_Prima (col U) sunt populate condițional pe Filtru; - # LARGE pe coloana corespunzătoare extrage doar rândurile relevante. - top_headers = [ - "#", "Fereastra", "Filtru", "Strategie", "N", "WR", "Expectancy R", - "Profit Factor", "Cum P&L $", "Max DD Prop $", "Status Edge", - ] - # Mapă coloană target din TOP → header din DASH_WIN_COL - top_source_names = [ - "Fereastra", "Filtru", "Strategie", "N", "WR", "Expectancy R", - "Profit Factor", "Cum P&L $", "Max Drawdown Prop $", "Status Edge", - ] - top_target_letters = ["B", "C", "D", "E", "F", "G", "H", "I", "J", "K"] - - def _emit_top_subsection(start_row: int, title: str, note: str, - score_col: str, count: int = 20) -> int: - ws[f"A{start_row}"] = title - ws[f"A{start_row}"].font = SUBTITLE_FONT - ws.merge_cells(f"A{start_row}:K{start_row}") - note_row = start_row + 1 - ws[f"A{note_row}"] = note - ws[f"A{note_row}"].font = Font( - name="Calibri", size=10, italic=True, color="595959" - ) - ws[f"A{note_row}"].alignment = Alignment( - horizontal="left", vertical="center", wrap_text=True - ) - ws.merge_cells(f"A{note_row}:K{note_row}") - header_row = note_row + 1 - for col_idx, header in enumerate(top_headers, start=1): - c = ws.cell(row=header_row, column=col_idx, value=header) - c.font = HEADER_FONT - c.fill = HEADER_FILL - c.alignment = CENTER - c.border = BORDER - - for idx in range(1, count + 1): - r = header_row + idx - ws[f"A{r}"] = idx - if combo_rows: - rank_formula = ( - f"LARGE(${score_col}${first_combo}:${score_col}${last_combo},{idx})" - ) - match_formula = ( - f"MATCH({rank_formula}," - f"${score_col}${first_combo}:${score_col}${last_combo},0)" - ) - for target, source_name in zip(top_target_letters, top_source_names): - source = DASH_WIN_COL[source_name] - ws[f"{target}{r}"] = ( - f'=IFERROR(IF({rank_formula}<=-1E+11,"",' - f'INDEX(${source}${first_combo}:${source}${last_combo},' - f'{match_formula})),"")' - ) - for c in range(1, len(top_headers) + 1): - cell = ws.cell(row=r, column=c) - cell.border = BORDER - cell.alignment = RIGHT if c not in (2, 3, 4, 11) else CENTER - # Number formats — coloanele după shift cu +1 (Filtru e nou D): - # E=N, F=WR, G=ExpR, H=PF, I=CumPL, J=MaxDDProp, K=StatusEdge - ws[f"F{r}"].number_format = "0.0%" - ws[f"G{r}"].number_format = "+0.000;-0.000;0.000" - ws[f"H{r}"].number_format = "0.00" - ws[f"I{r}"].number_format = '"$"#,##0.00' - ws[f"J{r}"].number_format = '"$"#,##0.00' - - # CF pe Status Edge (col K) - top_status_rng = f"K{header_row + 1}:K{header_row + count}" - ws.conditional_formatting.add( - top_status_rng, - CellIsRule(operator="equal", formula=['"CANDIDAT"'], fill=pass_fill), - ) - ws.conditional_formatting.add( - top_status_rng, - CellIsRule(operator="equal", formula=['"PRE-CANDIDAT"'], fill=warn_fill), - ) - ws.conditional_formatting.add( - top_status_rng, - CellIsRule(operator="equal", formula=['"BREACH"'], fill=fail_fill), - ) - return header_row + count - - top_title_row = row + 2 - after_top_toate = _emit_top_subsection( - top_title_row, - "TOP 20 FERESTRE — Toate trade-urile", - ( - "Top 20 după scor compus, calculat pe rândurile cu Filtru=Toate. " - "EXCLUDE ferestrele cu Daily Breach=DA sau Max Loss Breach=DA (ar fi pierdut contul prop). " - "Scor = ExpR×20000 + MIN(N,150)×100 + PF×1500 + CumP&L − MaxDD − MaxDDProp/10. " - "Bonusul N (capat la 150) favorizează ferestrele cu sample mai mare, statistic mai fiabile. " - "CANDIDAT = îndeplinește pragurile (N≥40, WR≥55%, ExpR≥0.2). " - "PRE-CANDIDAT = N≥1 fără breach dar sub praguri." - ), - score_col=T_LET, - ) - after_top_prima = _emit_top_subsection( - after_top_toate + 2, - "TOP 20 FERESTRE — Prima per Indicator", - ( - "Top 20 după scor compus, calculat pe rândurile cu Filtru=Prima (doar primul " - "trade pe (Data, Indicator) în fiecare fereastră). EXCLUDE ferestrele cu Daily " - "Breach=DA sau Max Loss Breach=DA. Util pentru a vedea dacă filtrul Prima " - "identifică ferestre mai eficiente decât Toate." - ), - score_col=U_LET, - ) - - # Conditional formatting reutilizabil pentru celulele Cum $ - bd_green = PatternFill("solid", fgColor="C6EFCE") - bd_red = PatternFill("solid", fgColor="FFC7CE") - - # Helper pentru breakdown wide: rânduri = items, coloane = 5 strategii Cum $ + N total - def _emit_breakdown_strats( - start_row: int, title: str, first_col_label: str, - items: list[str], item_range: str, - ) -> int: - # Layout: A=item, B..F=5 strategii (Cum $), G=N total - last_col_idx = 1 + len(STRAT_KEYS) + 1 # A + 5 strategii + N - last_letter = get_column_letter(last_col_idx) - ws[f"A{start_row}"] = title - ws[f"A{start_row}"].font = SUBTITLE_FONT - ws.merge_cells(f"A{start_row}:{last_letter}{start_row}") - headers = [first_col_label] + [STRAT_LABELS[s] for s in STRAT_KEYS] + ["N total"] - for col_idx, h in enumerate(headers, start=1): - c = ws.cell(row=start_row + 1, column=col_idx, value=h) - c.font = HEADER_FONT - c.fill = HEADER_FILL - c.alignment = CENTER - c.border = BORDER - strat_letters = [get_column_letter(2 + i) for i in range(len(STRAT_KEYS))] - n_letter = get_column_letter(last_col_idx) - for i, item in enumerate(items): - r = start_row + 2 + i - ws[f"A{r}"] = item - for idx, strat in enumerate(STRAT_KEYS): - cl = strat_letters[idx] - ws[f"{cl}{r}"] = f'=SUMIFS({D[strat]},{item_range},"{item}")' - ws[f"{cl}{r}"].number_format = '"$"#,##0.00' - ws[f"{n_letter}{r}"] = f'=COUNTIF({item_range},"{item}")' - ws[f"{n_letter}{r}"].number_format = "0" - for col_idx in range(1, last_col_idx + 1): - cell = ws.cell(row=r, column=col_idx) - cell.border = BORDER - cell.alignment = LEFT if col_idx == 1 else RIGHT - if 2 <= col_idx <= 1 + len(STRAT_KEYS): - cell.fill = DERIVED_FILL - # CF pe coloanele 5 strategii: verde >0, roșu <0 - if items: - first_data_row = start_row + 2 - last_data_row = start_row + 1 + len(items) - cf_rng = ( - f"{strat_letters[0]}{first_data_row}:" - f"{strat_letters[-1]}{last_data_row}" - ) - ws.conditional_formatting.add( - cf_rng, CellIsRule(operator="greaterThan", formula=["0"], fill=bd_green) - ) - ws.conditional_formatting.add( - cf_rng, CellIsRule(operator="lessThan", formula=["0"], fill=bd_red) - ) - return start_row + 1 + len(items) - - # Breakdowns — toate cele 5 strategii vizibile, Cum P&L $ per strategie - start = after_top_prima + 2 - after_strat = _emit_breakdown_strats( - start + 2, "PER STRATEGIE — Cum P&L $ per strategie", "Strategie", - STRATEGIES, _range("Strategie"), - ) - after_ind = _emit_breakdown_strats( - after_strat + 2, "PER INDICATOR — Cum P&L $ per strategie", "Indicator", - INDICATORS, _range("Indicator"), - ) - after_dir = _emit_breakdown_strats( - after_ind + 2, "PER DIRECȚIE — Cum P&L $ per strategie", "Direcție", - DIRECTIONS, _range("Direcție"), - ) - - # ---- PROP FIRM COMPLIANCE ---- - PROP_RANGES = { - "dollar": {s: _range(f"$Prop_{s}") for s in STRAT_KEYS}, - "balance": {s: _range(f"BalProp_{s}") for s in STRAT_KEYS}, - "daily": {s: _range(f"DailyPL_{s}") for s in STRAT_KEYS}, - "dd": {s: _range(f"DDProp_{s}") for s in STRAT_KEYS}, - } - - prop_title_row = after_dir + 2 - ws[f"A{prop_title_row}"] = "PROP FIRM COMPLIANCE" - ws[f"A{prop_title_row}"].font = SUBTITLE_FONT - ws.merge_cells(f"A{prop_title_row}:G{prop_title_row}") - - # Header pentru tabel - prop_header_row = prop_title_row + 1 - ws[f"A{prop_header_row}"] = "Metric" - for strat in STRAT_KEYS: - ws[f"{strat_cols[strat]}{prop_header_row}"] = STRAT_LABELS[strat] - ws[f"G{prop_header_row}"] = "Cum citesc" - for letter in ["A"] + list(strat_cols.values()) + ["G"]: - c = ws[f"{letter}{prop_header_row}"] - c.font = HEADER_FONT - c.fill = HEADER_FILL - c.alignment = CENTER - c.border = BORDER - - # Definițiile rândurilor — (label, formula_fn(strat), number_format) - fail_pass_fmt = "@" - prop_metrics: list[tuple[str, callable, str]] = [ - ( - "Account Prop Start ($)", - lambda s: "=Config!$B$9", - '"$"#,##0', - ), - ( - "$ risc la 1% preț ($)", - lambda s: "=Config!$B$27", - '"$"#,##0.00', - ), - ( - "Cumulative P&L Prop ($)", - lambda s: f"=SUM({PROP_RANGES['dollar'][s]})", - '"$"#,##0.00', - ), - ( - "Final Balance Prop ($)", - lambda s: f"=Config!$B$9+SUM({PROP_RANGES['dollar'][s]})", - '"$"#,##0.00', - ), - ( - "Worst Daily Loss ($)", - lambda s: f"=IFERROR(MIN({PROP_RANGES['daily'][s]}),0)", - '"$"#,##0.00', - ), - # placeholder pentru Daily Status — depinde de Worst Daily de mai sus - ("Daily Limit Status", lambda s: None, fail_pass_fmt), - ( - "Max Account Drawdown ($)", - lambda s: f"=IFERROR(MAX({PROP_RANGES['dd'][s]}),0)", - '"$"#,##0.00', - ), - # placeholder pentru Max Status — depinde de Max DD de mai sus - ("Max Loss Status", lambda s: None, fail_pass_fmt), - # placeholder pentru Overall — depinde de cele două statuses - ("Overall Prop Status", lambda s: None, fail_pass_fmt), - ] - - prop_label_to_row = { - label: prop_header_row + 1 + idx - for idx, (label, _, _) in enumerate(prop_metrics) - } - worst_daily_row = prop_label_to_row["Worst Daily Loss ($)"] - daily_status_row = prop_label_to_row["Daily Limit Status"] - max_dd_row = prop_label_to_row["Max Account Drawdown ($)"] - max_status_row = prop_label_to_row["Max Loss Status"] - - for idx, (label, fn, fmt) in enumerate(prop_metrics): - r = prop_header_row + 1 + idx - ws[f"A{r}"] = label - ws[f"A{r}"].font = Font(name="Calibri", size=11, bold=True) - ws[f"A{r}"].border = BORDER - ws[f"A{r}"].alignment = LEFT - for strat in STRAT_KEYS: - letter = strat_cols[strat] - if label == "Daily Limit Status": - formula = ( - f'=IF({letter}{worst_daily_row}<-Config!$B$13,"FAIL","PASS")' - ) - elif label == "Max Loss Status": - formula = ( - f'=IF({letter}{max_dd_row}>Config!$B$15,"FAIL","PASS")' - ) - elif label == "Overall Prop Status": - formula = ( - f'=IF(OR({letter}{daily_status_row}="FAIL",' - f'{letter}{max_status_row}="FAIL"),' - f'"CONT PIERDUT","CONFORM")' - ) - else: - formula = fn(strat) - cell = ws[f"{letter}{r}"] - cell.value = formula - cell.number_format = fmt - cell.fill = DERIVED_FILL - cell.border = BORDER - cell.alignment = RIGHT if fmt != fail_pass_fmt else CENTER - # Hint în coloana G - hint_cell = ws[f"G{r}"] - hint_cell.value = METRIC_HINTS.get(label, "") - hint_cell.font = Font(name="Calibri", size=10, color="595959") - hint_cell.alignment = Alignment( - horizontal="left", vertical="top", wrap_text=True - ) - hint_cell.border = BORDER - - # Conditional formatting pe status rows — verde PASS/CONFORM, roșu FAIL/CONT PIERDUT - pass_fill = PatternFill("solid", fgColor="C6EFCE") - fail_fill = PatternFill("solid", fgColor="FFC7CE") - for status_row in (daily_status_row, max_status_row): - rng = ( - f"{strat_cols[STRAT_KEYS[0]]}{status_row}:" - f"{strat_cols[STRAT_KEYS[-1]]}{status_row}" - ) - ws.conditional_formatting.add( - rng, CellIsRule(operator="equal", formula=['"PASS"'], fill=pass_fill) - ) - ws.conditional_formatting.add( - rng, CellIsRule(operator="equal", formula=['"FAIL"'], fill=fail_fill) - ) - overall_row = prop_label_to_row["Overall Prop Status"] - overall_rng = ( - f"{strat_cols[STRAT_KEYS[0]]}{overall_row}:" - f"{strat_cols[STRAT_KEYS[-1]]}{overall_row}" - ) - ws.conditional_formatting.add( - overall_rng, - CellIsRule(operator="equal", formula=['"CONFORM"'], fill=pass_fill), - ) - ws.conditional_formatting.add( - overall_rng, - CellIsRule( - operator="equal", formula=['"CONT PIERDUT"'], fill=fail_fill - ), - ) - - # Înălțime rânduri prop (cu hint multi-line) - for r in range(prop_header_row + 1, prop_header_row + 1 + len(prop_metrics)): - ws.row_dimensions[r].height = 60 - - # ---- PROP FIRM COMPLIANCE per FEREASTRĂ × STRATEGIE ---- - # Reshape compliance: rânduri = combo (fereastră × strategie × filtru), - # coloane = metrici compliance. Datele referențiate prin DASH_WIN_COL. - if combo_rows: - win_prop_title_row = prop_header_row + 1 + len(prop_metrics) + 2 - ws[f"A{win_prop_title_row}"] = "PROP FIRM COMPLIANCE — per FEREASTRĂ × STRATEGIE" - ws[f"A{win_prop_title_row}"].font = SUBTITLE_FONT - ws.merge_cells(f"A{win_prop_title_row}:H{win_prop_title_row}") - - win_prop_note_row = win_prop_title_row + 1 - ws[f"A{win_prop_note_row}"] = ( - "Defalcat pe fiecare combinație de fereastră tradabilă × strategie management × filtru. " - "CONFORM = ar fi supraviețuit pe contul de prop pe acel slot." - ) - ws[f"A{win_prop_note_row}"].font = Font(name="Calibri", size=10, italic=True, color="595959") - ws[f"A{win_prop_note_row}"].alignment = Alignment(horizontal="left", vertical="center", wrap_text=True) - ws.merge_cells(f"A{win_prop_note_row}:H{win_prop_note_row}") - - win_prop_header_row = win_prop_note_row + 1 - win_prop_headers = [ - "Fereastra", "Filtru", "Strategie", "Worst Daily Prop $", "Max DD Prop $", - "Daily Breach", "Max Breach", "Overall Prop", - ] - for col_idx, h in enumerate(win_prop_headers, start=1): - c = ws.cell(row=win_prop_header_row, column=col_idx, value=h) - c.font = HEADER_FONT - c.fill = HEADER_FILL - c.alignment = CENTER - c.border = BORDER - - # source cols din FERESTRE CANDIDATE (via DASH_WIN_COL) - source_names = [ - "Fereastra", "Filtru", "Strategie", - "Worst Daily Loss Prop $", "Max Drawdown Prop $", - "Daily Breach", "Max Loss Breach", "Status Prop", - ] - source_cols = [DASH_WIN_COL[name] for name in source_names] - for offset, combo_row in enumerate(combo_rows, start=1): - r = win_prop_header_row + offset - for col_idx, source in enumerate(source_cols, start=1): - target = get_column_letter(col_idx) - ws[f"{target}{r}"] = f"={source}{combo_row}" - cell = ws[f"{target}{r}"] - cell.border = BORDER - cell.fill = DERIVED_FILL - cell.alignment = CENTER if col_idx in (1, 2, 3, 6, 7, 8) else RIGHT - ws[f"D{r}"].number_format = '"$"#,##0.00' - ws[f"E{r}"].number_format = '"$"#,##0.00' - - # CF pe Overall Prop (col H) și pe Daily/Max Breach (cols F, G) - win_prop_first = win_prop_header_row + 1 - win_prop_last = win_prop_header_row + len(combo_rows) - overall_rng_win = f"H{win_prop_first}:H{win_prop_last}" - ws.conditional_formatting.add( - overall_rng_win, CellIsRule(operator="equal", formula=['"CONFORM"'], fill=pass_fill) - ) - ws.conditional_formatting.add( - overall_rng_win, CellIsRule(operator="equal", formula=['"CONT PIERDUT"'], fill=fail_fill) - ) - breach_rng_win = f"F{win_prop_first}:G{win_prop_last}" - ws.conditional_formatting.add( - breach_rng_win, CellIsRule(operator="equal", formula=['"DA"'], fill=fail_fill) - ) - ws.conditional_formatting.add( - breach_rng_win, CellIsRule(operator="equal", formula=['"NU"'], fill=pass_fill) - ) - - # Column widths — aliniate cu DASH_WIN_COL (A=Fereastra ... U=Score_Prima) - widths = { - DASH_WIN_COL["Fereastra"]: 18, - DASH_WIN_COL["Start"]: 10, - DASH_WIN_COL["End"]: 18, - DASH_WIN_COL["Filtru"]: 10, - DASH_WIN_COL["Strategie"]: 16, - DASH_WIN_COL["N"]: 8, - DASH_WIN_COL["Wins"]: 8, - DASH_WIN_COL["WR"]: 10, - DASH_WIN_COL["Expectancy R"]: 13, - DASH_WIN_COL["Expectancy $"]: 13, - DASH_WIN_COL["Profit Factor"]: 12, - DASH_WIN_COL["Cum P&L $"]: 13, - DASH_WIN_COL["Max Drawdown $"]: 15, - DASH_WIN_COL["Worst Daily Loss Prop $"]: 20, - DASH_WIN_COL["Max Drawdown Prop $"]: 18, - DASH_WIN_COL["Daily Breach"]: 13, - DASH_WIN_COL["Max Loss Breach"]: 14, - DASH_WIN_COL["Status Prop"]: 15, - DASH_WIN_COL["Status Edge"]: 13, - DASH_WIN_COL["Score_Toate"]: 8, - DASH_WIN_COL["Score_Prima"]: 8, - } - for col, w in widths.items(): - ws.column_dimensions[col].width = w - - # Row height pentru rândurile cu hint (cu wrap) — explicații multi-line - for r in range(5, 5 + len(metrics)): - ws.row_dimensions[r].height = 75 - - # Notă: graficele de echitate au fost eliminate (nu sunt folosite). Dashboard-ul - # rămâne pur tabelar — metrici + breakdown-uri + ferestre + compliance prop. - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - - -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) - wb.active = wb.sheetnames.index("Trades") - return wb - - -def main() -> int: - OUTPUT.parent.mkdir(parents=True, exist_ok=True) - if OUTPUT.exists(): - timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") - backup = OUTPUT.with_name(f"{OUTPUT.stem}.backup-{timestamp}{OUTPUT.suffix}") - shutil.copy2(OUTPUT, backup) - print(f"Backup {backup}") - wb = build_workbook() - wb.save(OUTPUT) - print(f"Wrote {OUTPUT}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) +"""Generator pentru data/backtest.xlsx. + +5 strategii de management comparate side-by-side pe semnale blackbox: + - TP0 only : 100% close la TP0 + - TP1 only : 100% OCO la SL/TP1 + - TP2 only : 100% OCO la SL/TP2 + - Hybrid + BE : 50% TP0 + mut SL la BE + 50% TP1 (recomandat de trader) + - Hybrid no BE : 50% TP0 + 50% TP1, fără BE (control pentru a izola valoarea BE-ului) + +Rulare: + pip install openpyxl + python scripts/generate_template.py +""" + +from __future__ import annotations + +import shutil +from datetime import date, datetime, time, timedelta +from pathlib import Path + +from openpyxl import Workbook +from openpyxl.formatting.rule import CellIsRule +from openpyxl.styles import Alignment, Border, Font, PatternFill, Side +from openpyxl.utils import get_column_letter +from openpyxl.worksheet.datavalidation import DataValidation + + +OUTPUT = Path(__file__).resolve().parent.parent / "data" / "backtest.xlsx" +MAX_ROWS = 500 # rânduri pre-completate cu formule în sheet-ul Trades + +# --------------------------------------------------------------------------- +# Styles +# --------------------------------------------------------------------------- + +HEADER_FILL = PatternFill("solid", fgColor="1F3864") +HEADER_FONT = Font(name="Calibri", size=11, bold=True, color="FFFFFF") +INPUT_FILL = PatternFill("solid", fgColor="FFF8E1") +DERIVED_FILL = PatternFill("solid", fgColor="E8F1FA") +HIDDEN_FILL = PatternFill("solid", fgColor="F0F0F0") +TITLE_FONT = Font(name="Calibri", size=16, bold=True, color="1F3864") +SUBTITLE_FONT = Font(name="Calibri", size=12, bold=True, color="1F3864") +THIN = Side(border_style="thin", color="BFBFBF") +BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN) +CENTER = Alignment(horizontal="center", vertical="center") +LEFT = Alignment(horizontal="left", vertical="center") +RIGHT = Alignment(horizontal="right", vertical="center") + + +# --------------------------------------------------------------------------- +# Lists +# --------------------------------------------------------------------------- + +STRATEGIES = ["M2D", "EMA cross", "Order block", "Liquidity sweep", "Custom"] +SESSIONS = ["A1", "A2", "A3", "B", "C", "D", "Other"] +INDICATORS = ["DIA", "US30", "SPY", "QQQ", "ES", "NQ"] +TIMEFRAMES = ["1min", "3min", "15min"] +DIRECTIONS = ["Buy", "Sell"] +OUTCOMES = ["SL", "TP0", "TP1", "TP2"] + +# Cele 5 strategii de management (sufix folosit în numele coloanelor) + label friendly +STRAT_KEYS = ["tp0only", "tp1only", "tp2only", "hybrid_be", "hybrid_nobe"] +STRAT_LABELS = { + "tp0only": "TP0 only", + "tp1only": "TP1 only", + "tp2only": "TP2 only", + "hybrid_be": "Hybrid + BE", + "hybrid_nobe": "Hybrid no BE", +} + +# --------------------------------------------------------------------------- +# Trades sheet — schema +# --------------------------------------------------------------------------- + +def _candidate_windows() -> list[tuple[str, time, time]]: + """Ferestre suprapuse intre 16:30 si 23:00, evaluate pe ora Romaniei.""" + base = datetime(2000, 1, 1, 16, 30) + last_start = datetime(2000, 1, 1, 22, 0) + hard_ends = [ + datetime(2000, 1, 1, 22, 45), + datetime(2000, 1, 1, 23, 0), + ] + durations = [timedelta(minutes=m) for m in (60, 90, 120, 180)] + seen: set[tuple[time, time]] = set() + windows: list[tuple[str, time, time]] = [] + + start = base + while start <= last_start: + ends = [start + d for d in durations] + ends += [end for end in hard_ends if end - start >= timedelta(minutes=60)] + for end in ends: + if end > hard_ends[-1]: + continue + key = (start.time(), end.time()) + if key in seen: + continue + seen.add(key) + windows.append((f"{start:%H:%M}-{end:%H:%M}", start.time(), end.time())) + start += timedelta(minutes=30) + return windows + + +TRADABLE_WINDOWS = _candidate_windows() + + +INPUT_HEADERS = [ + "#", "Data", "Ora RO", "Zi", "Sesiune", + "Strategie", "Indicator", "TF", + "Direcție", "SL %", "TP0 %", "TP1 %", "TP2 %", + "Outcome", "Notes", +] +DERIVED_HEADERS = ( + ["SL $", "SL $ Prop"] + + + [f"R_{s}" for s in STRAT_KEYS] + + [f"$_{s}" for s in STRAT_KEYS] + + [f"Bal_{s}" for s in STRAT_KEYS] + + [f"$Prop_{s}" for s in STRAT_KEYS] + + [f"BalProp_{s}" for s in STRAT_KEYS] +) +PRIMA_HELPERS = [f"PrimaWin_{idx}" for idx in range(len(TRADABLE_WINDOWS))] + +HELPER_HEADERS = ( + [f"Win_{s}" for s in STRAT_KEYS] + + [f"Peak_{s}" for s in STRAT_KEYS] + + [f"DD_{s}" for s in STRAT_KEYS] + + [f"DailyPL_{s}" for s in STRAT_KEYS] + + [f"PeakProp_{s}" for s in STRAT_KEYS] + + [f"DDProp_{s}" for s in STRAT_KEYS] + + PRIMA_HELPERS +) +TRADES_HEADERS = INPUT_HEADERS + DERIVED_HEADERS + HELPER_HEADERS + +# Mapă nume → literă coloană Excel +COL = {name: get_column_letter(i + 1) for i, name in enumerate(TRADES_HEADERS)} + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _col_to_int(letter: str) -> int: + n = 0 + for ch in letter: + n = n * 26 + (ord(ch) - ord("A") + 1) + return n + + +# --------------------------------------------------------------------------- +# Config sheet +# --------------------------------------------------------------------------- + + +def build_config(wb: Workbook) -> None: + ws = wb.create_sheet("Config", 0) + ws.sheet_view.showGridLines = False + + ws["A1"] = "Config — editează doar celulele galbene" + ws["A1"].font = TITLE_FONT + ws.merge_cells("A1:C1") + + ws["A3"] = "Setting" + ws["B3"] = "Value" + ws["C3"] = "Note" + for c in ("A3", "B3", "C3"): + ws[c].font = HEADER_FONT + ws[c].fill = HEADER_FILL + ws[c].alignment = CENTER + + ws["A4"] = "Account Size Start ($)" + ws["B4"] = 10000 + ws["C4"] = "Balanța inițială pentru calcule $ și HWM (model abstract)" + + ws["A5"] = "Risk reper (%)" + ws["B5"] = 1.0 + ws["C5"] = "Reper opțional; $_* se calculează din SL% × Account Size Start" + + ws["A6"] = "Risk reper ($)" + ws["B6"] = "=B4*B5/100" + ws["C6"] = "Auto — informativ; nu este folosit în formulele $_*" + + for r in (4, 5): + ws.cell(row=r, column=2).fill = INPUT_FILL + ws.cell(row=r, column=2).border = BORDER + ws["B6"].fill = DERIVED_FILL + ws["B6"].border = BORDER + ws["B4"].number_format = "$#,##0" + ws["B5"].number_format = '0.0"%"' + ws["B6"].number_format = "$#,##0.00" + + # ---- Bloc Cont real TradeLocker / Prop Firm ---- + # Lanț de calcul transparent: Lot size -> $/punct -> $ risc la 1% mișcare preț. + # Toate coloanele $ din Trades folosesc B10*B11 (= $ risc la 1% preț pe poziția ta). + # Scalare la 100k: schimbi B9 (Account) și B10 (Lot size) — restul se recalculează. + ws["A8"] = "Cont real TradeLocker (prop firm)" + ws["A8"].font = SUBTITLE_FONT + ws.merge_cells("A8:C8") + + ws["A9"] = "Account Start ($)" + ws["B9"] = 50000 + ws["C9"] = "Balanța contului. Schimbă în 100000 pentru un cont de 100k." + + ws["A10"] = "Lot size per semnal (US30)" + ws["B10"] = 0.08 + ws["C10"] = ( + "Câte loturi US30 intri pe fiecare semnal (TradeLocker). " + "Pe 100k cu același risc %: 0.16." + ) + + ws["A11"] = "$ per 1% preț, la 1.0 lot" + ws["B11"] = "=B24*B25/100" + ws["C11"] = ( + "Auto = ($/punct la 1 lot, B24) × (Preț reper, B25 / 100). " + "Înmulțit cu Lot size (B10) dă riscul real per 1% — vezi B27." + ) + + ws["A12"] = "Daily Loss Limit (%)" + ws["B12"] = 4.0 + ws["C12"] = "Limită zilnică prop firm; depășire = cont mort" + + ws["A13"] = "Daily Loss Limit ($)" + ws["B13"] = "=B9*B12/100" + ws["C13"] = "Auto — derivat din Account Start (B9) și %" + + ws["A14"] = "Max Loss Limit (%)" + ws["B14"] = 7.0 + ws["C14"] = "Limită totală pe cont; depășire = cont mort" + + ws["A15"] = "Max Loss Limit ($)" + ws["B15"] = "=B9*B14/100" + ws["C15"] = "Auto — derivat din Account Start (B9) și %" + + # Escape hatch performanță: activează/dezactivează filtrul Prima per Indicator + ws["A17"] = "Activează filtru Prima" + ws["B17"] = "DA" + ws["C17"] = ( + "DA = adaugi rândurile Prima în window grid. " + "NU = doar Toate (workbook mai rapid)." + ) + ws["B17"].fill = INPUT_FILL + ws["B17"].border = BORDER + ws["B17"].alignment = CENTER + dv_prima = DataValidation( + type="list", formula1='"DA,NU"', allow_blank=False, + ) + dv_prima.add("B17") + ws.add_data_validation(dv_prima) + + # ---- Calibrare $/punct dintr-un ordin reper + derivate informative ---- + # Helper universal: dintr-un singur ordin TradeLocker (orice indicator) derivi + # $/punct, apoi $/1% preț. Pentru alt indicator schimbi cele 4 inputuri reper + # (B19-B22) + Preț reper curent (B25). Restul se recalculează singur. + ws["A18"] = "— Calibrare $/punct (dintr-un ordin reper TradeLocker) —" + ws["A18"].font = Font(name="Calibri", size=10, italic=True, bold=True, color="1F3864") + ws.merge_cells("A18:C18") + + ws["A19"] = "Preț entry (ordin reper)" + ws["B19"] = 50680.15 + ws["C19"] = "Prețul de intrare afișat în ticketul TradeLocker (orice ordin reper)." + + ws["A20"] = "Preț SL (ordin reper)" + ws["B20"] = 50618.24 + ws["C20"] = "Prețul SL din același ticket." + + ws["A21"] = "$ risc afișat (ordin reper)" + ws["B21"] = 495.24 + ws["C21"] = "Cifra $ pe care TradeLocker o arată la SL pentru ordinul reper." + + ws["A22"] = "Lot size (ordin reper)" + ws["B22"] = 0.08 + ws["C22"] = "Câte loturi avea ordinul reper (poate diferi de lotul tău curent, B10)." + + ws["A23"] = "↳ Distanță reper (puncte)" + ws["B23"] = "=ABS(B19-B20)" + ws["C23"] = "Auto = |entry − SL|. Aici ⇒ 61.91 puncte." + + ws["A24"] = "↳ $ per punct la 1.0 lot" + ws["B24"] = "=IF(OR(B23=0,B22=0),0,B21/B23/B22)" + ws["C24"] = ( + "Auto = $risc ÷ distanță ÷ lot reper. Pe US30 ⇒ $100/punct la 1 lot. " + "Asta intră în B11. Pe alt indicator se recalibrează singur." + ) + + ws["A25"] = "Preț reper (curent)" + ws["B25"] = 50700 + ws["C25"] = ( + "Nivelul curent al indicatorului; convertește % mișcare preț în puncte/$. " + "Pentru alt indicator pui prețul lui." + ) + + ws["A26"] = "↳ $ per punct (la lotul tău)" + ws["B26"] = "=B10*B24" + ws["C26"] = "Auto = Lot size (B10) × $/punct la 1 lot (B24). La 0.08 loturi ⇒ $8/punct." + + ws["A27"] = "↳ $ risc la 1% mișcare preț" + ws["B27"] = "=B10*B11" + ws["C27"] = ( + "Auto — INIMA calculului. $ pe trade = R × SL% × această valoare. " + "La 0.08 loturi US30 ⇒ ~$4,056." + ) + + for r in (9, 10, 12, 14, 19, 20, 21, 22, 25): # inputuri galbene + ws.cell(row=r, column=2).fill = INPUT_FILL + ws.cell(row=r, column=2).border = BORDER + for r in (11, 13, 15, 23, 24, 26, 27): # derived blue + ws.cell(row=r, column=2).fill = DERIVED_FILL + ws.cell(row=r, column=2).border = BORDER + + ws["B9"].number_format = "$#,##0" + ws["B10"].number_format = "0.00" + ws["B11"].number_format = "$#,##0" + ws["B12"].number_format = '0.0"%"' + ws["B13"].number_format = "$#,##0" + ws["B14"].number_format = '0.0"%"' + ws["B15"].number_format = "$#,##0" + ws["B19"].number_format = "#,##0.00" + ws["B20"].number_format = "#,##0.00" + ws["B21"].number_format = '"$"#,##0.00' + ws["B22"].number_format = "0.00" + ws["B23"].number_format = "#,##0.00" + ws["B24"].number_format = '"$"#,##0.00' + ws["B25"].number_format = "#,##0" + ws["B26"].number_format = '"$"#,##0.00' + ws["B27"].number_format = '"$"#,##0.00' + + # ---- Utilitar: calcul preț SL pentru TradeLocker (pas cu pas) ---- + # Forward helper: din SL% (TradeStation) + preț intrare (TradeLocker) + direcție + # → prețul exact la care pui SL în TradeLocker. Celule intermediare vizibile. + ws["A29"] = "— Utilitar: ce prețuri SL/TP pun în TradeLocker —" + ws["A29"].font = Font(name="Calibri", size=10, italic=True, bold=True, color="1F3864") + ws.merge_cells("A29:C29") + + ws["A30"] = "SL % (din TradeStation)" + ws["B30"] = 0.08 + ws["C30"] = "Procentul SL al semnalului (% mișcare de preț), ex: 0.08." + + ws["A31"] = "Preț intrare (TradeLocker)" + ws["B31"] = 50649.50 + ws["C31"] = "Prețul tău de intrare US30 (nu prețul reper de la B25)." + + ws["A32"] = "Direcție" + ws["B32"] = "Sell" + ws["C32"] = "Sell ⇒ SL DEASUPRA intrării. Buy ⇒ SL DEDESUBT." + + ws["A33"] = "↳ Distanță SL (puncte)" + ws["B33"] = "=B31*B30/100" + ws["C33"] = "Auto = Preț intrare × SL% / 100. Ex: 50649.50 × 0.08/100 ⇒ ~40.5 puncte." + + ws["A34"] = "↳ PREȚ SL de setat în TradeLocker" + ws["B34"] = '=IF(B32="Sell",B31+B33,B31-B33)' + ws["C34"] = "Sell: intrare + distanță. Buy: intrare − distanță. ASTA pui în TradeLocker." + + ws["A35"] = "↳ $ risc la acest SL" + ws["B35"] = "=B33*B26" + ws["C35"] = ( + "Auto = Distanță (puncte) × $/punct la lotul tău (B26). " + "Verificare: ar trebui să se potrivească cu cifra $ din ticketul TradeLocker." + ) + + for r in (30, 31, 32): # inputuri galbene + ws.cell(row=r, column=2).fill = INPUT_FILL + ws.cell(row=r, column=2).border = BORDER + for r in (33, 34, 35): # derived blue + ws.cell(row=r, column=2).fill = DERIVED_FILL + ws.cell(row=r, column=2).border = BORDER + # Output principal evidențiat + ws["A34"].font = Font(name="Calibri", size=11, bold=True, color="1F3864") + ws["B34"].font = Font(name="Calibri", size=11, bold=True) + ws["B32"].alignment = CENTER + + ws["B30"].number_format = '0.000"%"' + ws["B31"].number_format = "#,##0.00" + ws["B33"].number_format = "#,##0.00" + ws["B34"].number_format = "#,##0.00" + ws["B35"].number_format = '"$"#,##0.00' + + # --- TP0/TP1/TP2: aceeași logică, dar în direcția profitului (opus SL) --- + ws["A36"] = "TP0 % (din TradeStation)" + ws["B36"] = 0.03 + ws["C36"] = "Procentul TP0 al semnalului (% mișcare de preț)." + + ws["A37"] = "TP1 % (din TradeStation)" + ws["B37"] = 0.06 + ws["C37"] = "Procentul TP1 al semnalului." + + ws["A38"] = "TP2 % (din TradeStation)" + ws["B38"] = 0.08 + ws["C38"] = "Procentul TP2 al semnalului." + + ws["A39"] = "↳ PREȚ TP0 de setat" + ws["B39"] = '=IF(B32="Sell",B31-B31*B36/100,B31+B31*B36/100)' + ws["C39"] = ( + "Distanță = intrare × TP0% / 100. Sell: intrare − distanță. " + "Buy: intrare + distanță (TP-ul e opus SL-ului)." + ) + + ws["A40"] = "↳ PREȚ TP1 de setat" + ws["B40"] = '=IF(B32="Sell",B31-B31*B37/100,B31+B31*B37/100)' + ws["C40"] = "La fel, cu TP1%." + + ws["A41"] = "↳ PREȚ TP2 de setat" + ws["B41"] = '=IF(B32="Sell",B31-B31*B38/100,B31+B31*B38/100)' + ws["C41"] = "La fel, cu TP2%." + + for r in (36, 37, 38): # inputuri galbene + ws.cell(row=r, column=2).fill = INPUT_FILL + ws.cell(row=r, column=2).border = BORDER + ws.cell(row=r, column=2).number_format = '0.000"%"' + for r in (39, 40, 41): # output evidențiat (albastru, bold) + ws.cell(row=r, column=2).fill = DERIVED_FILL + ws.cell(row=r, column=2).border = BORDER + ws.cell(row=r, column=2).number_format = "#,##0.00" + ws.cell(row=r, column=2).font = Font(name="Calibri", size=11, bold=True) + ws.cell(row=r, column=1).font = Font( + name="Calibri", size=11, bold=True, color="1F3864" + ) + + dv_dir = DataValidation(type="list", formula1='"Buy,Sell"', allow_blank=False) + dv_dir.add("B32") + ws.add_data_validation(dv_dir) + + # Liste dropdown — coloanele E–J (6 coloane) + list_columns = [ + ("Strategii", STRATEGIES), + ("Sesiuni (auto)", SESSIONS), + ("Indicatori", INDICATORS), + ("TF", TIMEFRAMES), + ("Direcție", DIRECTIONS), + ("Outcome", OUTCOMES), + ] + for col_idx, (label, values) in enumerate(list_columns, start=5): + cell = ws.cell(row=3, column=col_idx, value=label) + cell.font = HEADER_FONT + cell.fill = HEADER_FILL + cell.alignment = CENTER + for row_idx, v in enumerate(values, start=4): + c = ws.cell(row=row_idx, column=col_idx, value=v) + c.alignment = CENTER + + widths = { + "A": 30, "B": 14, "C": 40, "D": 2, + "E": 14, "F": 14, "G": 13, "H": 10, "I": 10, "J": 12, + } + for col, w in widths.items(): + ws.column_dimensions[col].width = w + + +# --------------------------------------------------------------------------- +# Formula builders pentru Trades sheet +# --------------------------------------------------------------------------- + + +def _f_day(r: int) -> str: + d = f'{COL["Data"]}{r}' + return ( + f'=IF({d}="","",' + f'CHOOSE(WEEKDAY({d},2),"Lu","Ma","Mi","Jo","Vi","Sa","Du"))' + ) + + +def _f_session(r: int) -> str: + """Derivă Sesiunea M2D din Data + Ora RO.""" + d = f'{COL["Data"]}{r}' + t = f'{COL["Ora RO"]}{r}' + wd = f"WEEKDAY({d},2)" + mid_week = f"AND({wd}>=2,{wd}<=4)" + return ( + f'=IF(OR({d}="",{t}=""),"",' + f"IF(OR({wd}=1,{wd}=5),\"D\"," + f'IF(AND({t}>=TIME(15,30,0),{t}=TIME(16,35,0),{t}=TIME(17,0,0),{t}=TIME(18,0,0),{t}=TIME(22,0,0),{t} str: + """1 dacă trade-ul este prima cronologic pe (Data, Indicator) ÎN fereastra cu indexul win_idx. + + Guard suplimentar pe Config!B17 (escape hatch performanță): dacă utilizatorul + setează "NU", toate PrimaWin_* devin 0 instant, fără recalcularea COUNTIFS. + Outcome inclus în COUNTIFS ca să nu blocheze rândurile parțial completate. + """ + _, start_t, end_t = TRADABLE_WINDOWS[win_idx] + start_s = f"TIME({start_t.hour},{start_t.minute},0)" + end_s = f"TIME({end_t.hour},{end_t.minute},0)" + d = f'{COL["Data"]}{r}' + ind = f'{COL["Indicator"]}{r}' + t = f'{COL["Ora RO"]}{r}' + o = f'{COL["Outcome"]}{r}' + data_rng = _range("Data") + ind_rng = _range("Indicator") + ora_rng = _range("Ora RO") + outcome_rng = _range("Outcome") + return ( + f'=IF(Config!$B$17<>"DA",0,' + f'IF(OR({d}="",{t}="",{ind}="",{o}=""),0,' + f'IF(AND({t}>={start_s},{t}<{end_s}),' + f'IF(COUNTIFS({data_rng},{d},{ind_rng},{ind},' + f'{ora_rng},">="&{start_s},{ora_rng},"<"&{end_s},' + f'{ora_rng},"<"&{t},' + f'{outcome_rng},"<>"' + f')=0,1,0),0)))' + ) + + +def _f_r_tp0only(r: int) -> str: + o = f'{COL["Outcome"]}{r}' + sl = f'{COL["SL %"]}{r}' + tp0 = f'{COL["TP0 %"]}{r}' + return f'=IF({o}="","",IF({o}="SL",-1,{tp0}/{sl}))' + + +def _f_r_tp1only(r: int) -> str: + o = f'{COL["Outcome"]}{r}' + sl = f'{COL["SL %"]}{r}' + tp1 = f'{COL["TP1 %"]}{r}' + return ( + f'=IF({o}="","",' + f'IF(OR({o}="SL",{o}="TP0"),-1,{tp1}/{sl}))' + ) + + +def _f_r_tp2only(r: int) -> str: + o = f'{COL["Outcome"]}{r}' + sl = f'{COL["SL %"]}{r}' + tp2 = f'{COL["TP2 %"]}{r}' + return f'=IF({o}="","",IF({o}="TP2",{tp2}/{sl},-1))' + + +def _f_r_hybrid_be(r: int) -> str: + o = f'{COL["Outcome"]}{r}' + sl = f'{COL["SL %"]}{r}' + tp0 = f'{COL["TP0 %"]}{r}' + tp1 = f'{COL["TP1 %"]}{r}' + return ( + f'=IF({o}="","",' + f'IF({o}="SL",-1,' + f'IF({o}="TP0",0.5*{tp0}/{sl},' + f'0.5*({tp0}+{tp1})/{sl})))' + ) + + +def _f_r_hybrid_nobe(r: int) -> str: + o = f'{COL["Outcome"]}{r}' + sl = f'{COL["SL %"]}{r}' + tp0 = f'{COL["TP0 %"]}{r}' + tp1 = f'{COL["TP1 %"]}{r}' + return ( + f'=IF({o}="","",' + f'IF({o}="SL",-1,' + f'IF({o}="TP0",0.5*{tp0}/{sl}-0.5,' + f'0.5*({tp0}+{tp1})/{sl})))' + ) + + +R_FN: dict[str, callable] = { + "tp0only": _f_r_tp0only, + "tp1only": _f_r_tp1only, + "tp2only": _f_r_tp2only, + "hybrid_be": _f_r_hybrid_be, + "hybrid_nobe": _f_r_hybrid_nobe, +} + + +def _f_dollar(r: int, r_col: str) -> str: + """$ P&L per trade = R × SL% × Contracte × $/1% per contract (TradeLocker real).""" + rc = f"{COL[r_col]}{r}" + sl = f"{COL['SL %']}{r}" + return f'=IF({rc}="","",{rc}*{sl}*Config!$B$10*Config!$B$11)' + + +def _f_sl_dollar(r: int) -> str: + """SL $ = SL% × Contracte × $/1% per contract.""" + sl = f"{COL['SL %']}{r}" + return f'=IF({sl}="","",{sl}*Config!$B$10*Config!$B$11)' + + +def _f_sl_dollar_prop(r: int) -> str: + """SL $ pe contul de prop — același cont real, formula identică cu SL $.""" + sl = f"{COL['SL %']}{r}" + return f'=IF({sl}="","",{sl}*Config!$B$10*Config!$B$11)' + + +def _f_balance(r: int, dollar_col: str) -> str: + dc = COL[dollar_col] + return f'=IF({dc}{r}="","",Config!$B$4 + SUM(${dc}$2:{dc}{r}))' + + +def _f_win(r: int, r_col: str) -> str: + rc = f"{COL[r_col]}{r}" + return f'=IF({rc}="","",IF({rc}>0,1,0))' + + +def _f_peak(r: int, balance_col: str, peak_col: str) -> str: + bc = COL[balance_col] + pc = COL[peak_col] + if r == 2: + return f'=IF({bc}{r}="","",{bc}{r})' + return ( + f'=IF({bc}{r}="","",' + f'IF({pc}{r-1}="",{bc}{r},MAX({pc}{r-1},{bc}{r})))' + ) + + +def _f_drawdown(r: int, peak_col: str, balance_col: str) -> str: + pc = f"{COL[peak_col]}{r}" + bc = f"{COL[balance_col]}{r}" + return f'=IF({bc}="","",{pc}-{bc})' + + +def _f_dollar_prop(r: int, r_col: str) -> str: + """$ P&L pe contul de prop — același calcul ca _f_dollar (cont real TradeLocker). + + Diferența între cont abstract și prop e doar balanța de start; $-ul per trade + e identic pentru că reflectă realitatea contractelor tranzacționate. + """ + rc = f"{COL[r_col]}{r}" + sl = f"{COL['SL %']}{r}" + return f'=IF({rc}="","",{rc}*{sl}*Config!$B$10*Config!$B$11)' + + +def _f_balance_prop(r: int, dollar_col: str) -> str: + dc = COL[dollar_col] + return f'=IF({dc}{r}="","",Config!$B$9 + SUM(${dc}$2:{dc}{r}))' + + +def _f_daily_pl(r: int, dollar_col: str) -> str: + """Cumul P&L pe ziua curentă (până la rândul r inclusiv).""" + dc = COL[dollar_col] + d_col = COL["Data"] + d = f"{d_col}{r}" + return ( + f'=IF(OR({dc}{r}="",{d}=""),"",' + f'SUMIFS(${dc}$2:{dc}{r},${d_col}$2:{d_col}{r},{d}))' + ) + + +# --------------------------------------------------------------------------- +# Trades sheet +# --------------------------------------------------------------------------- + + +def build_trades(wb: Workbook) -> None: + ws = wb.create_sheet("Trades", 1) + ws.sheet_view.showGridLines = False + ws.freeze_panes = "B2" + + # Headers + for col_idx, header in enumerate(TRADES_HEADERS, start=1): + cell = ws.cell(row=1, column=col_idx, value=header) + cell.font = HEADER_FONT + cell.fill = HEADER_FILL + cell.alignment = CENTER + cell.border = BORDER + + # Formule pe toate rândurile pre-pregătite + for r in range(2, MAX_ROWS + 2): + ws.cell(row=r, column=1, value="=ROW()-1") + ws[f'{COL["Zi"]}{r}'] = _f_day(r) + ws[f'{COL["Sesiune"]}{r}'] = _f_session(r) + ws[f'{COL["SL $"]}{r}'] = _f_sl_dollar(r) + ws[f'{COL["SL $ Prop"]}{r}'] = _f_sl_dollar_prop(r) + + for strat in STRAT_KEYS: + ws[f'{COL[f"R_{strat}"]}{r}'] = R_FN[strat](r) + ws[f'{COL[f"$_{strat}"]}{r}'] = _f_dollar(r, f"R_{strat}") + ws[f'{COL[f"Bal_{strat}"]}{r}'] = _f_balance(r, f"$_{strat}") + ws[f'{COL[f"Win_{strat}"]}{r}'] = _f_win(r, f"R_{strat}") + ws[f'{COL[f"Peak_{strat}"]}{r}'] = _f_peak( + r, f"Bal_{strat}", f"Peak_{strat}" + ) + ws[f'{COL[f"DD_{strat}"]}{r}'] = _f_drawdown( + r, f"Peak_{strat}", f"Bal_{strat}" + ) + # Prop firm tracking — paralel cu modelul abstract + ws[f'{COL[f"$Prop_{strat}"]}{r}'] = _f_dollar_prop(r, f"R_{strat}") + ws[f'{COL[f"BalProp_{strat}"]}{r}'] = _f_balance_prop(r, f"$Prop_{strat}") + ws[f'{COL[f"DailyPL_{strat}"]}{r}'] = _f_daily_pl(r, f"$Prop_{strat}") + ws[f'{COL[f"PeakProp_{strat}"]}{r}'] = _f_peak( + r, f"BalProp_{strat}", f"PeakProp_{strat}" + ) + ws[f'{COL[f"DDProp_{strat}"]}{r}'] = _f_drawdown( + r, f"PeakProp_{strat}", f"BalProp_{strat}" + ) + + # Coloanele PrimaWin_ — 1 dacă trade-ul e prima per (Data, Indicator) în fereastră + for win_idx in range(len(TRADABLE_WINDOWS)): + ws[f'{COL[f"PrimaWin_{win_idx}"]}{r}'] = _f_prima_in_window(r, win_idx) + + # Sample rows + SAMPLE_ROWS = [ + # (data, ora, strat, ind, tf, dir, sl, tp0, tp1, tp2, outcome, notes) + (date(2026, 5, 13), time(17, 33), "M2D", "DIA", "1min", "Sell", 0.30, 0.10, 0.15, 0.30, "TP1", "Prima DIA în 16:30-18:00"), + (date(2026, 5, 13), time(17, 50), "M2D", "DIA", "1min", "Buy", 0.25, 0.10, 0.15, 0.25, "SL", "DIA a doua oară — NU Prima în 16:30-18:00, dar Prima în 17:30-19:00"), + (date(2026, 5, 13), time(17, 34), "M2D", "SPY", "1min", "Sell", 0.20, 0.08, 0.12, 0.20, "TP1", "SPY — indicator diferit, Prima independent"), + (date(2026, 5, 13), time(17, 40), "M2D", "DIA", "1min", "Sell", 0.20, 0.08, 0.12, 0.20, "", "Outcome gol — test D1: NU blochează Prima pentru row 2/3"), + (date(2026, 5, 14), time(22, 15), "M2D", "DIA", "1min", "Sell", 0.30, 0.10, 0.15, 0.30, "TP0", "Zi diferită — Prima reset per (Data, Indicator, Fereastră)"), + ] + for offset, sample in enumerate(SAMPLE_ROWS): + r = 2 + offset + data_v, ora, strat_v, ind, tf, dirn, sl, tp0, tp1, tp2, outcome, notes = sample + ws[f"B{r}"] = data_v + ws[f"C{r}"] = ora + ws[f'{COL["Strategie"]}{r}'] = strat_v + ws[f'{COL["Indicator"]}{r}'] = ind + ws[f'{COL["TF"]}{r}'] = tf + ws[f'{COL["Direcție"]}{r}'] = dirn + ws[f'{COL["SL %"]}{r}'] = sl + ws[f'{COL["TP0 %"]}{r}'] = tp0 + ws[f'{COL["TP1 %"]}{r}'] = tp1 + ws[f'{COL["TP2 %"]}{r}'] = tp2 + ws[f'{COL["Outcome"]}{r}'] = outcome + ws[f'{COL["Notes"]}{r}'] = notes + + # Number formats + for col_name in ("SL %", "TP0 %", "TP1 %", "TP2 %"): + for r in range(2, MAX_ROWS + 2): + ws[f"{COL[col_name]}{r}"].number_format = '0.000"%"' + + for col_name in ("SL $", "SL $ Prop"): + for r in range(2, MAX_ROWS + 2): + ws[f"{COL[col_name]}{r}"].number_format = '"$"#,##0.00' + + for strat in STRAT_KEYS: + for r in range(2, MAX_ROWS + 2): + ws[f"{COL[f'R_{strat}']}{r}"].number_format = "+0.000;-0.000;0.000" + for prefix in ( + "$_", "Bal_", "Peak_", "DD_", + "$Prop_", "BalProp_", "DailyPL_", "PeakProp_", "DDProp_", + ): + ws[f"{COL[f'{prefix}{strat}']}{r}"].number_format = '"$"#,##0.00' + + for r in range(2, MAX_ROWS + 2): + ws[f"B{r}"].number_format = "yyyy-mm-dd" + + # Coloring + input_letters = { + COL[n] + for n in ( + "Data", "Ora RO", "Strategie", "Indicator", "TF", + "Direcție", "SL %", "TP0 %", "TP1 %", "TP2 %", + "Outcome", "Notes", + ) + } + derived_letters = {COL["Zi"], COL["Sesiune"], COL["SL $"], COL["SL $ Prop"]} + for strat in STRAT_KEYS: + for prefix in ("R_", "$_", "Bal_", "$Prop_", "BalProp_"): + derived_letters.add(COL[f"{prefix}{strat}"]) + helper_letters = set() + for strat in STRAT_KEYS: + for prefix in ("Win_", "Peak_", "DD_", "DailyPL_", "PeakProp_", "DDProp_"): + helper_letters.add(COL[f"{prefix}{strat}"]) + for win_idx in range(len(TRADABLE_WINDOWS)): + helper_letters.add(COL[f"PrimaWin_{win_idx}"]) + + for r in range(2, MAX_ROWS + 2): + for cl in input_letters: + ws[f"{cl}{r}"].fill = INPUT_FILL + for cl in derived_letters: + ws[f"{cl}{r}"].fill = DERIVED_FILL + for cl in helper_letters: + ws[f"{cl}{r}"].fill = HIDDEN_FILL + + # Column widths + widths = { + "A": 5, "B": 12, "C": 9, "D": 5, "E": 9, + "F": 12, "G": 11, "H": 8, "I": 9, + "J": 9, "K": 9, "L": 9, "M": 9, + "N": 11, "O": 28, + } + for col, w in widths.items(): + ws.column_dimensions[col].width = w + for col_name in ("SL $", "SL $ Prop"): + ws.column_dimensions[COL[col_name]].width = 12 + # Derived + helper: width 11 + for strat in STRAT_KEYS: + for prefix in ( + "R_", "$_", "Bal_", "Win_", "Peak_", "DD_", + "$Prop_", "BalProp_", "DailyPL_", "PeakProp_", "DDProp_", + ): + ws.column_dimensions[COL[f"{prefix}{strat}"]].width = 11 + + # Ascund helper-ele prop firm într-un outline collapsible + for strat in STRAT_KEYS: + for prefix in ("DailyPL_", "PeakProp_", "DDProp_"): + cl = COL[f"{prefix}{strat}"] + ws.column_dimensions[cl].outlineLevel = 1 + ws.column_dimensions[cl].hidden = True + + # Helper-ele PrimaWin_ — ~40 coloane la sfârșit, ascunse în outline + for win_idx in range(len(TRADABLE_WINDOWS)): + cl = COL[f"PrimaWin_{win_idx}"] + ws.column_dimensions[cl].width = 3 + ws.column_dimensions[cl].outlineLevel = 1 + ws.column_dimensions[cl].hidden = True + + # Data validation dropdowns + def _add_dv(col_name: str, source: str) -> None: + cl = COL[col_name] + dv = DataValidation( + type="list", formula1=source, + allow_blank=True, showErrorMessage=True, + ) + dv.error = "Valoare invalidă — folosește dropdown-ul." + dv.errorTitle = "Input invalid" + dv.add(f"{cl}2:{cl}{MAX_ROWS + 1}") + ws.add_data_validation(dv) + + # Config columns: E=Strategii, F=Sesiuni, G=Indicatori, H=TF, I=Direcție, J=Outcome + _add_dv("Strategie", "=Config!$E$4:$E$8") + _add_dv("Indicator", "=Config!$G$4:$G$9") + _add_dv("TF", "=Config!$H$4:$H$6") + _add_dv("Direcție", "=Config!$I$4:$I$5") + _add_dv("Outcome", "=Config!$J$4:$J$7") + + # Conditional formatting pe coloanele R (5 strategii) + green_fill = PatternFill("solid", fgColor="C6EFCE") + red_fill = PatternFill("solid", fgColor="FFC7CE") + grey_fill = PatternFill("solid", fgColor="D9D9D9") + for strat in STRAT_KEYS: + cl = COL[f"R_{strat}"] + rng = f"{cl}2:{cl}{MAX_ROWS + 1}" + ws.conditional_formatting.add( + rng, CellIsRule(operator="greaterThan", formula=["0"], fill=green_fill) + ) + ws.conditional_formatting.add( + rng, CellIsRule(operator="lessThan", formula=["0"], fill=red_fill) + ) + ws.conditional_formatting.add( + rng, CellIsRule(operator="equal", formula=["0"], fill=grey_fill) + ) + + +# --------------------------------------------------------------------------- +# Dashboard sheet +# --------------------------------------------------------------------------- + + +def _range(col_name: str) -> str: + cl = COL[col_name] + return f"Trades!${cl}$2:${cl}${MAX_ROWS + 1}" + + +METRIC_HINTS: dict[str, str] = { + "Trades Placed": ( + "Câte trade-uri ai logat în total.\n" + "Cu cât N e mai mare, cu atât celelalte metrici sunt mai de încredere.\n" + "Exemplu: la N=10 Win Ratio e zgomot pur, la N=40 începe să aibă semnal, la N=100 e solid." + ), + "Wins": ( + "Câte trade-uri s-au închis pe plus (R > 0).\n" + "Singur nu spune nimic — privește-l raportat la total (vezi Win Ratio mai jos)." + ), + "Win Ratio": ( + "Procentul de trade-uri câștigătoare. WR = 60% înseamnă 6 wins din 10 trade-uri.\n" + "Singur NU spune dacă strategia e profitabilă — citește-l împreună cu R:R de pe rândul următor." + ), + "Average Win ($)": ( + "Câștigul mediu pe trade-urile pozitive.\n" + "Comparat cu Average Loss îți spune cât de mari sunt câștigurile vs pierderile.\n" + "Exemplu: 4 wins de $50 și 2 wins de $80 — Average Win = $60." + ), + "Average Loss ($)": ( + "Pierderea medie pe trade-urile negative (cifra apare cu minus).\n" + "În dolari reali, −1R depinde de SL%: pierdere ≈ SL% × $ risc la 1% preț (Config B27).\n" + "Dacă e mult mai mare decât riscul calculat din SL, ai SL-uri sărite (slippage, gap-uri)." + ), + "Best Trade ($)": ( + "Cel mai mare câștig individual.\n" + "Dacă majoritatea profitului total vine dintr-un singur trade outlier, edge-ul e fragil — " + "elimini acel trade și strategia devine pierzătoare." + ), + "Worst Trade ($)": ( + "Cea mai mare pierdere individuală.\n" + "Ar trebui să fie aproximativ egală cu −1R calculat din SL% × $ risc la 1% preț (Config B27).\n" + "La 0.08 loturi US30: SL=0.30% × ~$4,056 ≈ −$1,217. Dacă e mult mai mare, ai slippage/gap." + ), + "Profit Factor": ( + "Total bani câștigați împărțit la total bani pierduți (în valoare absolută).\n" + "Sub 1.0 = pierzi pe ansamblu. Peste 1.5 = solid. Peste 2.0 = câștigi de 2× cât pierzi.\n" + "Exemplu: 4 wins de $50 (= $200) + 6 losses de $30 (= $180) — PF = 200÷180 = 1.11, profitabil marginal." + ), + "Risk:Reward": ( + "De câte ori e mai mare câștigul mediu decât pierderea medie.\n" + "R:R = 2 înseamnă: când câștigi, câștigi $2; când pierzi, pierzi $1.\n" + "Cu R:R mare poți avea Win Ratio mic și tot să faci bani." + ), + "Expectancy (R)": ( + "Cât câștigi în medie pe UN trade, exprimat în R.\n" + "+0.30R = câștigi 0.30 × riscul mediu al trade-urilor.\n" + "−0.10R = pierzi 0.10 × riscul mediu al trade-urilor.\n" + "Pragul de GO LIVE: +0.20R sau mai mult." + ), + "Expectancy ($)": ( + "Aceeași expectancy convertită în dolari, folosind SL% × $ risc la 1% preț (Config B27).\n" + "Util ca să vezi cât câștigi în medie pe trade în bani reali (TradeLocker), nu doar în R." + ), + "Cumulative P&L ($)": ( + "Suma profitului și pierderii pe toate trade-urile logate.\n" + "E ce-ai avea în plus (sau minus) față de balanța de start din Config." + ), + "HWM Balance ($)": ( + "Highest Watermark — cea mai mare balanță atinsă vreodată în jurnal.\n" + "Punct de referință pentru calculul drawdown-ului." + ), + "Max Drawdown ($)": ( + "Cea mai mare cădere ($) din vârf la fundul ulterior al balanței. Măsoară durerea psihologică maximă.\n" + "Exemplu: ai urcat la $11,500, ai coborât la $9,800 — DD = $1,700, adică 17% din peak.\n" + "Un drawdown mare la backtest e foarte greu de tolerat în live cu bani reali — așteaptă-te să renunți." + ), + # ---- Prop firm metrics ---- + "Account Prop Start ($)": ( + "Capitalul de start al contului de prop firm (default $50,000).\n" + "Editabil în Config B9." + ), + "$ risc la 1% preț ($)": ( + "Câți $ riști pe poziția ta reală la 1% mișcare de preț (Config B27).\n" + "Lanț: Lot size (B10) × $/punct la 1 lot (B24) = $/punct; × Preț reper (B25)/100 = $ per 1%.\n" + "$/punct la 1 lot (B24) e calibrat dintr-un ordin reper TradeLocker — merge pe orice indicator.\n" + "La 0.08 loturi US30 ⇒ ~$4,056. Pe 100k cu lot 0.16 se dublează (~$8,112)." + ), + "Cumulative P&L Prop ($)": ( + "Profitul total al contului de prop pe traseul logat.\n" + "Reflectă $ real (SL% × Contracte × $/1% per contract), nu un procent abstract din cont.\n" + "Adunat peste $50,000 dă balanța finală reală." + ), + "Final Balance Prop ($)": ( + "Balanța finală a contului de prop = Account Start (Config B9) + Cumulative P&L Prop.\n" + "Compar-o cu pragul de stop-out: Account Start − Max Loss Limit $ (Config B15)." + ), + "Worst Daily Loss ($)": ( + "Cea mai proastă pierdere cumulativă într-o zi calendaristică.\n" + "Dacă e mai mică decât −Daily Loss Limit $ (Config B13), ai depășit limita zilnică — cont mort.\n" + "Atenție: un singur breach = pierdere cont, indiferent dacă ai recuperat ulterior." + ), + "Daily Limit Status": ( + "PASS dacă nicio zi nu a depășit Daily Loss Limit $ (Config B13, auto din Account × %).\n" + "FAIL = strategia ar fi pierdut contul prin daily breach pe traseul logat." + ), + "Max Account Drawdown ($)": ( + "Cea mai mare cădere de la peak pe contul de prop.\n" + "Dacă > Max Loss Limit $ (Config B15, auto din Account × %), ai depășit limita — cont mort." + ), + "Max Loss Status": ( + "PASS dacă Max Account Drawdown ≤ Max Loss Limit $ (Config B15).\n" + "FAIL = strategia ar fi pierdut contul prin drawdown cumulativ." + ), + "Overall Prop Status": ( + "CONFORM = strategia ar fi supraviețuit pe contul de prop pe traseul logat.\n" + "CONT PIERDUT = cel puțin o breach (daily sau max) — strategia nu e viabilă pe acest cont prop." + ), +} + + +def build_dashboard(wb: Workbook) -> None: + ws = wb.create_sheet("Dashboard", 2) + ws.sheet_view.showGridLines = False + + ws["A1"] = "Backtest Dashboard" + ws["A1"].font = TITLE_FONT + ws.merge_cells("A1:G1") + + ws["A2"] = ( + "Comparație 5 strategii management — pe aceleași semnale blackbox" + ) + ws["A2"].font = Font(name="Calibri", size=10, italic=True, color="595959") + ws.merge_cells("A2:G2") + + # Row 4: headers (5 columns B-F pentru strategii + G pentru "Cum citesc") + ws["A4"] = "Metric" + strat_cols = {} # strat_key → column letter (B/C/D/E/F) + for i, strat in enumerate(STRAT_KEYS): + letter = get_column_letter(2 + i) + strat_cols[strat] = letter + ws[f"{letter}4"] = STRAT_LABELS[strat] + ws["G4"] = "Cum citesc" + for letter in ["A"] + list(strat_cols.values()) + ["G"]: + c = ws[f"{letter}4"] + c.font = HEADER_FONT + c.fill = HEADER_FILL + c.alignment = CENTER + c.border = BORDER + + # Ranges per strategie + R = {s: _range(f"R_{s}") for s in STRAT_KEYS} + D = {s: _range(f"$_{s}") for s in STRAT_KEYS} + W = {s: _range(f"Win_{s}") for s in STRAT_KEYS} + BAL = {s: _range(f"Bal_{s}") for s in STRAT_KEYS} + DD = {s: _range(f"DD_{s}") for s in STRAT_KEYS} + OUTCOME_RANGE = _range("Outcome") + + # Metric rows — fiecare metric e un dict cu per-strategy formula + format + metrics: list[tuple[str, callable, str]] = [ + # (label, fn(strat_key) -> formula, number_format) + ("Trades Placed", lambda s: f'=COUNTA({OUTCOME_RANGE})', "0"), + ("Wins", lambda s: f'=COUNTIF({W[s]},1)', "0"), + ("Average Win ($)", lambda s: f'=IFERROR(AVERAGEIF({D[s]},">0"),0)', '"$"#,##0.00'), + ("Average Loss ($)", lambda s: f'=IFERROR(AVERAGEIF({D[s]},"<0"),0)', '"$"#,##0.00'), + ("Best Trade ($)", lambda s: f'=IFERROR(MAX({D[s]}),0)', '"$"#,##0.00'), + ("Worst Trade ($)", lambda s: f'=IFERROR(MIN({D[s]}),0)', '"$"#,##0.00'), + ("Profit Factor", lambda s: f'=IFERROR(SUMIF({D[s]},">0")/ABS(SUMIF({D[s]},"<0")),0)', "0.00"), + # Win Ratio: depends on Wins + Trades Placed — handled after metrics list (placeholder) + ("Win Ratio", lambda s: None, "0.0%"), + # Risk:Reward — placeholder; bazat pe rândurile Avg Win/Loss + ("Risk:Reward", lambda s: None, "0.00"), + ("Expectancy (R)", lambda s: f'=IFERROR(AVERAGE({R[s]}),0)', "+0.000;-0.000;0.000"), + ("Expectancy ($)", lambda s: f'=IFERROR(AVERAGE({D[s]}),0)', '"$"#,##0.00'), + ("Cumulative P&L ($)", lambda s: f'=SUM({D[s]})', '"$"#,##0.00'), + # HWM — placeholder cu ref la Trades Placed (row 5) + ("HWM Balance ($)", lambda s: None, '"$"#,##0.00'), + ("Max Drawdown ($)", lambda s: f'=IFERROR(MAX({DD[s]}),0)', '"$"#,##0.00'), + ] + + # Determine row indexes pentru formule speciale (depind de poziție) + label_to_row = {label: 5 + idx for idx, (label, _, _) in enumerate(metrics)} + trades_row = label_to_row["Trades Placed"] + wins_row = label_to_row["Wins"] + avg_win_row = label_to_row["Average Win ($)"] + avg_loss_row = label_to_row["Average Loss ($)"] + + for idx, (label, fn, fmt) in enumerate(metrics): + r = 5 + idx + ws[f"A{r}"] = label + ws[f"A{r}"].font = Font(name="Calibri", size=11, bold=True) + ws[f"A{r}"].border = BORDER + ws[f"A{r}"].alignment = LEFT + for strat in STRAT_KEYS: + letter = strat_cols[strat] + if label == "Win Ratio": + formula = f"=IFERROR({letter}{wins_row}/{letter}{trades_row},0)" + elif label == "Risk:Reward": + formula = f"=IFERROR({letter}{avg_win_row}/ABS({letter}{avg_loss_row}),0)" + elif label == "HWM Balance ($)": + formula = ( + f"=IF({letter}{trades_row}=0,Config!$B$4,MAX({BAL[strat]}))" + ) + else: + formula = fn(strat) + cell = ws[f"{letter}{r}"] + cell.value = formula + cell.number_format = fmt + cell.fill = DERIVED_FILL + cell.border = BORDER + cell.alignment = RIGHT + # Coloana G — interpretare narativă + exemplu numeric + hint_cell = ws[f"G{r}"] + hint_cell.value = METRIC_HINTS.get(label, "") + hint_cell.font = Font(name="Calibri", size=10, color="595959") + hint_cell.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True) + hint_cell.border = BORDER + + # Conditional formatting reutilizabil pentru celulele Cum $ + bd_green = PatternFill("solid", fgColor="C6EFCE") + bd_red = PatternFill("solid", fgColor="FFC7CE") + + # Helper pentru breakdown wide: rânduri = items, coloane = 5 strategii Cum $ + N total + def _emit_breakdown_strats( + start_row: int, title: str, first_col_label: str, + items: list[str], item_range: str, + ) -> int: + # Layout: A=item, B..F=5 strategii (Cum $), G=N total + last_col_idx = 1 + len(STRAT_KEYS) + 1 # A + 5 strategii + N + last_letter = get_column_letter(last_col_idx) + ws[f"A{start_row}"] = title + ws[f"A{start_row}"].font = SUBTITLE_FONT + ws.merge_cells(f"A{start_row}:{last_letter}{start_row}") + headers = [first_col_label] + [STRAT_LABELS[s] for s in STRAT_KEYS] + ["N total"] + for col_idx, h in enumerate(headers, start=1): + c = ws.cell(row=start_row + 1, column=col_idx, value=h) + c.font = HEADER_FONT + c.fill = HEADER_FILL + c.alignment = CENTER + c.border = BORDER + strat_letters = [get_column_letter(2 + i) for i in range(len(STRAT_KEYS))] + n_letter = get_column_letter(last_col_idx) + for i, item in enumerate(items): + r = start_row + 2 + i + ws[f"A{r}"] = item + for idx, strat in enumerate(STRAT_KEYS): + cl = strat_letters[idx] + ws[f"{cl}{r}"] = f'=SUMIFS({D[strat]},{item_range},"{item}")' + ws[f"{cl}{r}"].number_format = '"$"#,##0.00' + ws[f"{n_letter}{r}"] = f'=COUNTIF({item_range},"{item}")' + ws[f"{n_letter}{r}"].number_format = "0" + for col_idx in range(1, last_col_idx + 1): + cell = ws.cell(row=r, column=col_idx) + cell.border = BORDER + cell.alignment = LEFT if col_idx == 1 else RIGHT + if 2 <= col_idx <= 1 + len(STRAT_KEYS): + cell.fill = DERIVED_FILL + # CF pe coloanele 5 strategii: verde >0, roșu <0 + if items: + first_data_row = start_row + 2 + last_data_row = start_row + 1 + len(items) + cf_rng = ( + f"{strat_letters[0]}{first_data_row}:" + f"{strat_letters[-1]}{last_data_row}" + ) + ws.conditional_formatting.add( + cf_rng, CellIsRule(operator="greaterThan", formula=["0"], fill=bd_green) + ) + ws.conditional_formatting.add( + cf_rng, CellIsRule(operator="lessThan", formula=["0"], fill=bd_red) + ) + return start_row + 1 + len(items) + + # Breakdowns — toate cele 5 strategii vizibile, Cum P&L $ per strategie + start = 5 + len(metrics) + 2 + after_strat = _emit_breakdown_strats( + start + 2, "PER STRATEGIE — Cum P&L $ per strategie", "Strategie", + STRATEGIES, _range("Strategie"), + ) + after_ind = _emit_breakdown_strats( + after_strat + 2, "PER INDICATOR — Cum P&L $ per strategie", "Indicator", + INDICATORS, _range("Indicator"), + ) + after_dir = _emit_breakdown_strats( + after_ind + 2, "PER DIRECȚIE — Cum P&L $ per strategie", "Direcție", + DIRECTIONS, _range("Direcție"), + ) + + # ---- PROP FIRM COMPLIANCE ---- + PROP_RANGES = { + "dollar": {s: _range(f"$Prop_{s}") for s in STRAT_KEYS}, + "balance": {s: _range(f"BalProp_{s}") for s in STRAT_KEYS}, + "daily": {s: _range(f"DailyPL_{s}") for s in STRAT_KEYS}, + "dd": {s: _range(f"DDProp_{s}") for s in STRAT_KEYS}, + } + + prop_title_row = after_dir + 2 + ws[f"A{prop_title_row}"] = "PROP FIRM COMPLIANCE" + ws[f"A{prop_title_row}"].font = SUBTITLE_FONT + ws.merge_cells(f"A{prop_title_row}:G{prop_title_row}") + + # Header pentru tabel + prop_header_row = prop_title_row + 1 + ws[f"A{prop_header_row}"] = "Metric" + for strat in STRAT_KEYS: + ws[f"{strat_cols[strat]}{prop_header_row}"] = STRAT_LABELS[strat] + ws[f"G{prop_header_row}"] = "Cum citesc" + for letter in ["A"] + list(strat_cols.values()) + ["G"]: + c = ws[f"{letter}{prop_header_row}"] + c.font = HEADER_FONT + c.fill = HEADER_FILL + c.alignment = CENTER + c.border = BORDER + + # Definițiile rândurilor — (label, formula_fn(strat), number_format) + fail_pass_fmt = "@" + prop_metrics: list[tuple[str, callable, str]] = [ + ( + "Account Prop Start ($)", + lambda s: "=Config!$B$9", + '"$"#,##0', + ), + ( + "$ risc la 1% preț ($)", + lambda s: "=Config!$B$27", + '"$"#,##0.00', + ), + ( + "Cumulative P&L Prop ($)", + lambda s: f"=SUM({PROP_RANGES['dollar'][s]})", + '"$"#,##0.00', + ), + ( + "Final Balance Prop ($)", + lambda s: f"=Config!$B$9+SUM({PROP_RANGES['dollar'][s]})", + '"$"#,##0.00', + ), + ( + "Worst Daily Loss ($)", + lambda s: f"=IFERROR(MIN({PROP_RANGES['daily'][s]}),0)", + '"$"#,##0.00', + ), + # placeholder pentru Daily Status — depinde de Worst Daily de mai sus + ("Daily Limit Status", lambda s: None, fail_pass_fmt), + ( + "Max Account Drawdown ($)", + lambda s: f"=IFERROR(MAX({PROP_RANGES['dd'][s]}),0)", + '"$"#,##0.00', + ), + # placeholder pentru Max Status — depinde de Max DD de mai sus + ("Max Loss Status", lambda s: None, fail_pass_fmt), + # placeholder pentru Overall — depinde de cele două statuses + ("Overall Prop Status", lambda s: None, fail_pass_fmt), + ] + + prop_label_to_row = { + label: prop_header_row + 1 + idx + for idx, (label, _, _) in enumerate(prop_metrics) + } + worst_daily_row = prop_label_to_row["Worst Daily Loss ($)"] + daily_status_row = prop_label_to_row["Daily Limit Status"] + max_dd_row = prop_label_to_row["Max Account Drawdown ($)"] + max_status_row = prop_label_to_row["Max Loss Status"] + + for idx, (label, fn, fmt) in enumerate(prop_metrics): + r = prop_header_row + 1 + idx + ws[f"A{r}"] = label + ws[f"A{r}"].font = Font(name="Calibri", size=11, bold=True) + ws[f"A{r}"].border = BORDER + ws[f"A{r}"].alignment = LEFT + for strat in STRAT_KEYS: + letter = strat_cols[strat] + if label == "Daily Limit Status": + formula = ( + f'=IF({letter}{worst_daily_row}<-Config!$B$13,"FAIL","PASS")' + ) + elif label == "Max Loss Status": + formula = ( + f'=IF({letter}{max_dd_row}>Config!$B$15,"FAIL","PASS")' + ) + elif label == "Overall Prop Status": + formula = ( + f'=IF(OR({letter}{daily_status_row}="FAIL",' + f'{letter}{max_status_row}="FAIL"),' + f'"CONT PIERDUT","CONFORM")' + ) + else: + formula = fn(strat) + cell = ws[f"{letter}{r}"] + cell.value = formula + cell.number_format = fmt + cell.fill = DERIVED_FILL + cell.border = BORDER + cell.alignment = RIGHT if fmt != fail_pass_fmt else CENTER + # Hint în coloana G + hint_cell = ws[f"G{r}"] + hint_cell.value = METRIC_HINTS.get(label, "") + hint_cell.font = Font(name="Calibri", size=10, color="595959") + hint_cell.alignment = Alignment( + horizontal="left", vertical="top", wrap_text=True + ) + hint_cell.border = BORDER + + # Conditional formatting pe status rows — verde PASS/CONFORM, roșu FAIL/CONT PIERDUT + pass_fill = PatternFill("solid", fgColor="C6EFCE") + fail_fill = PatternFill("solid", fgColor="FFC7CE") + for status_row in (daily_status_row, max_status_row): + rng = ( + f"{strat_cols[STRAT_KEYS[0]]}{status_row}:" + f"{strat_cols[STRAT_KEYS[-1]]}{status_row}" + ) + ws.conditional_formatting.add( + rng, CellIsRule(operator="equal", formula=['"PASS"'], fill=pass_fill) + ) + ws.conditional_formatting.add( + rng, CellIsRule(operator="equal", formula=['"FAIL"'], fill=fail_fill) + ) + overall_row = prop_label_to_row["Overall Prop Status"] + overall_rng = ( + f"{strat_cols[STRAT_KEYS[0]]}{overall_row}:" + f"{strat_cols[STRAT_KEYS[-1]]}{overall_row}" + ) + ws.conditional_formatting.add( + overall_rng, + CellIsRule(operator="equal", formula=['"CONFORM"'], fill=pass_fill), + ) + ws.conditional_formatting.add( + overall_rng, + CellIsRule( + operator="equal", formula=['"CONT PIERDUT"'], fill=fail_fill + ), + ) + + # Înălțime rânduri prop (cu hint multi-line) + for r in range(prop_header_row + 1, prop_header_row + 1 + len(prop_metrics)): + ws.row_dimensions[r].height = 60 + + # Column widths — metric table + breakdowns + prop compliance + for _c, _w in {"A": 24, "B": 15, "C": 15, "D": 15, + "E": 15, "F": 15, "G": 60}.items(): + ws.column_dimensions[_c].width = _w + + # Row height pentru rândurile cu hint (cu wrap) — explicații multi-line + for r in range(5, 5 + len(metrics)): + ws.row_dimensions[r].height = 75 + + # Notă: graficele de echitate au fost eliminate (nu sunt folosite). Dashboard-ul + # rămâne pur tabelar — metrici + breakdown-uri + ferestre + compliance prop. + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +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) + wb.active = wb.sheetnames.index("Trades") + return wb + + +def main() -> int: + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + if OUTPUT.exists(): + timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + backup = OUTPUT.with_name(f"{OUTPUT.stem}.backup-{timestamp}{OUTPUT.suffix}") + shutil.copy2(OUTPUT, backup) + print(f"Backup {backup}") + wb = build_workbook() + wb.save(OUTPUT) + print(f"Wrote {OUTPUT}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())