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:
Marius
2026-06-03 01:43:02 +03:00
parent c7fe08c088
commit 1f45d77e4e
5 changed files with 2004 additions and 2281 deletions

View File

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

View File

@@ -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 00.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)", "Δ (testtrain)"])
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

View File

@@ -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:0021:00 RO. Ora 18:0019: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:1520:15 (1h, edge maxim/tranzacție, timp minim) · B = 19:4521:45 (2h, cel mai bun edge robust, trece pragul 0.20R) · W = 19:1522: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 9899% 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.100.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.11.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ă (617) → 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 (00.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. "
"Δ (testtrain) aproape de 0 sau pozitiv = stabil; foarte negativ = overfit.")
headers(["Variantă", "Fereastră", "N train", "ExpR train", "N test", "ExpR test (OOS)", "Δ (testtrain)", "", "", "", "", ""])
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:0019: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()

File diff suppressed because it is too large Load Diff