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) <noreply@anthropic.com>
This commit is contained in:
54
CLAUDE.md
54
CLAUDE.md
@@ -30,11 +30,11 @@ Three artifacts work together; understand all three before editing any:
|
|||||||
|
|
||||||
### 1. `scripts/generate_template.py` — the only code
|
### 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.
|
- **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.
|
- **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["..."]`.
|
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.
|
`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:**
|
**Reluare după ce Marius adaugă tranzacții noi:**
|
||||||
```powershell
|
```powershell
|
||||||
python scripts/generate_ferestre_v2.py
|
# întâi: deschide & SALVEAZĂ backtest.xlsx în Excel (populează cache-ul R_/$_/Bal_)
|
||||||
```
|
|
||||||
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_)
|
|
||||||
python scripts/generate_dashboard.py # sau dublu-click refresh_dashboard.bat
|
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.
|
**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).
|
**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
|
## Reference docs
|
||||||
|
|
||||||
- `strategie_M2D.md` — M2D setup rules (color-coded dot bands on TF mare/mic, SL/TP placement, session filters).
|
- `strategie_M2D.md` — M2D setup rules (color-coded dot bands on TF mare/mic, SL/TP placement, session filters).
|
||||||
|
|||||||
Binary file not shown.
@@ -1,10 +1,18 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Generează data/Dashboard.xlsx dintr-un snapshot al data/backtest.xlsx.
|
"""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
|
UN SINGUR fișier de analiză. Citește backtest.xlsx (read-only, data_only=True)
|
||||||
data/Dashboard.xlsx. NU atinge backtest.xlsx. Refolosește build_config() și
|
și scrie data/Dashboard.xlsx cu TOATE analizele, în sheet-uri separate:
|
||||||
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.
|
• 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):
|
Reruleaza prin refresh_dashboard.bat (sau direct):
|
||||||
python scripts/generate_dashboard.py
|
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ă
|
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
|
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
|
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
|
from pathlib import Path
|
||||||
|
|
||||||
import openpyxl
|
import openpyxl
|
||||||
from openpyxl import Workbook
|
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 (
|
from generate_template import (
|
||||||
build_config,
|
build_config,
|
||||||
build_dashboard,
|
build_dashboard,
|
||||||
TRADES_HEADERS,
|
TRADES_HEADERS,
|
||||||
MAX_ROWS,
|
MAX_ROWS,
|
||||||
|
STRAT_KEYS,
|
||||||
|
STRAT_LABELS,
|
||||||
)
|
)
|
||||||
|
|
||||||
SRC = Path(__file__).resolve().parent.parent / "data" / "backtest.xlsx"
|
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
|
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:
|
def main() -> int:
|
||||||
if not SRC.exists():
|
if not SRC.exists():
|
||||||
print(f"EROARE: nu găsesc {SRC}")
|
print(f"EROARE: nu găsesc {SRC}")
|
||||||
@@ -107,12 +737,30 @@ def main() -> int:
|
|||||||
build_config(wb) # Config la index 0 (cu formule)
|
build_config(wb) # Config la index 0 (cu formule)
|
||||||
apply_config_inputs(wb, cfg_inputs)
|
apply_config_inputs(wb, cfg_inputs)
|
||||||
copy_trades_values(wb, wb_src["Trades"]) # Trades static la index 1 (ascuns)
|
copy_trades_values(wb, wb_src["Trades"]) # Trades static la index 1 (ascuns)
|
||||||
build_dashboard(wb) # Dashboard la index 2 — formule + charturi
|
build_dashboard(wb) # Dashboard la index 2 — formule
|
||||||
wb.active = wb.sheetnames.index("Dashboard")
|
|
||||||
|
|
||||||
wb_src.close()
|
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)
|
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
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -1052,382 +1052,6 @@ def build_dashboard(wb: Workbook) -> None:
|
|||||||
hint_cell.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
|
hint_cell.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
|
||||||
hint_cell.border = BORDER
|
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 $
|
# Conditional formatting reutilizabil pentru celulele Cum $
|
||||||
bd_green = PatternFill("solid", fgColor="C6EFCE")
|
bd_green = PatternFill("solid", fgColor="C6EFCE")
|
||||||
bd_red = PatternFill("solid", fgColor="FFC7CE")
|
bd_red = PatternFill("solid", fgColor="FFC7CE")
|
||||||
@@ -1484,7 +1108,7 @@ def build_dashboard(wb: Workbook) -> None:
|
|||||||
return start_row + 1 + len(items)
|
return start_row + 1 + len(items)
|
||||||
|
|
||||||
# Breakdowns — toate cele 5 strategii vizibile, Cum P&L $ per strategie
|
# Breakdowns — toate cele 5 strategii vizibile, Cum P&L $ per strategie
|
||||||
start = after_top_prima + 2
|
start = 5 + len(metrics) + 2
|
||||||
after_strat = _emit_breakdown_strats(
|
after_strat = _emit_breakdown_strats(
|
||||||
start + 2, "PER STRATEGIE — Cum P&L $ per strategie", "Strategie",
|
start + 2, "PER STRATEGIE — Cum P&L $ per strategie", "Strategie",
|
||||||
STRATEGIES, _range("Strategie"),
|
STRATEGIES, _range("Strategie"),
|
||||||
@@ -1647,99 +1271,10 @@ def build_dashboard(wb: Workbook) -> None:
|
|||||||
for r in range(prop_header_row + 1, prop_header_row + 1 + len(prop_metrics)):
|
for r in range(prop_header_row + 1, prop_header_row + 1 + len(prop_metrics)):
|
||||||
ws.row_dimensions[r].height = 60
|
ws.row_dimensions[r].height = 60
|
||||||
|
|
||||||
# ---- PROP FIRM COMPLIANCE per FEREASTRĂ × STRATEGIE ----
|
# Column widths — metric table + breakdowns + prop compliance
|
||||||
# Reshape compliance: rânduri = combo (fereastră × strategie × filtru),
|
for _c, _w in {"A": 24, "B": 15, "C": 15, "D": 15,
|
||||||
# coloane = metrici compliance. Datele referențiate prin DASH_WIN_COL.
|
"E": 15, "F": 15, "G": 60}.items():
|
||||||
if combo_rows:
|
ws.column_dimensions[_c].width = _w
|
||||||
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
|
# Row height pentru rândurile cu hint (cu wrap) — explicații multi-line
|
||||||
for r in range(5, 5 + len(metrics)):
|
for r in range(5, 5 + len(metrics)):
|
||||||
|
|||||||
Reference in New Issue
Block a user