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
|
||||
|
||||
`build_workbook()` builds `data/backtest.xlsx` = **2 sheets only** (Config + Trades). `build_dashboard()` still lives here but is **no longer added to backtest.xlsx** — it is reused by `scripts/generate_dashboard.py` to build the separate `data/Dashboard.xlsx` (see "Dashboard separat" below).
|
||||
`build_workbook()` builds `data/backtest.xlsx` = **2 sheets only** (Config + Trades). `build_dashboard()` still lives here but is **no longer added to backtest.xlsx** — it is reused by `scripts/generate_dashboard.py` to build the separate `data/Dashboard.xlsx` (see "Analiză combinată" below).
|
||||
|
||||
- **Config** sheet — editable params (Account Size, Risk %) and dropdown source lists.
|
||||
- **Trades** sheet — `MAX_ROWS=500` pre-populated rows. Yellow cells = user input (date, time, strategy, indicator, TF, direction, SL/TP %, outcome). Blue cells = derived via formula (Zi, Sesiune, then per-strategy `R_*`, `$_*`, `Bal_*`). Grey cells = helper columns (`Win_*`, `Peak_*`, `DD_*`) consumed by Dashboard.
|
||||
- **Dashboard** (`build_dashboard`, emitted into the separate `Dashboard.xlsx`) — reads from Trades ranges via `SUMIF`/`AVERAGEIF`/`COUNTIF`; renders metrics table, glossary, per-Session/Strategy/Indicator/Direction breakdowns, ferestre candidate, prop compliance. **No equity-curve chart** (removed — Dashboard is pure-tabular).
|
||||
- **Dashboard** (`build_dashboard`, emitted into the separate `Dashboard.xlsx`) — reads from Trades ranges via `SUMIF`/`AVERAGEIF`/`COUNTIF`; renders metrics table, glossary, per-Strategy/Indicator/Direction breakdowns, prop compliance. **No formula-based ferestre grid** (removed — that window scan is now the python auto-scan in the `Ferestre` sheet; see "Analiză combinată" below) and **no equity-curve chart** in the Dashboard sheet itself.
|
||||
|
||||
Column-name → letter mapping is held in the `COL` dict, built from `TRADES_HEADERS = INPUT_HEADERS + DERIVED_HEADERS + HELPER_HEADERS`. **Never hardcode column letters** — adding/reordering a header shifts every letter. Always look up via `COL["..."]`.
|
||||
|
||||
@@ -60,42 +60,42 @@ The `Sesiune` column is computed by `_f_session` from `Data` + `Ora RO` (Romania
|
||||
|
||||
`STOPPING_RULE.md` is a **signed document** (the user committed it as a commitment). It defines GO LIVE / EXTEND / ABANDON thresholds: `N≥40`, `WR≥55%`, `Expectancy≥+0.20R`. Treat these numbers as fixed unless the user explicitly asks to renegotiate them — do not "improve" them in passing.
|
||||
|
||||
## Ferestre v2 — analiză edge/fereastră (scripts/generate_ferestre_v2.py)
|
||||
## Analiză combinată — `Dashboard.xlsx` (scripts/generate_dashboard.py)
|
||||
|
||||
Analiză separată care găsește **fereastra de timp (ora RO) cu cel mai bun raport edge / nr. tranzacții / durată**, fără să breach-uiască contul prop. Citește `data/backtest.xlsx` **read-only** și scrie un fișier nou `data/Ferestre_v2.xlsx` (NU atinge workbook-ul cu tranzacții; date_grafic rămâne sheet vizibil ca să se randeze chart-ul).
|
||||
`backtest.xlsx` (editat zilnic) = doar Config + Trades (~0.8 MB, rapid la salvat). TOATĂ analiza trăiește într-un **singur** fișier read-only `data/Dashboard.xlsx`, generat la comandă din backtest.xlsx de **un singur script** `generate_dashboard.py`. (Istoric: erau două fișiere cu două scannere de ferestre — `Dashboard.xlsx` pe formule + `Ferestre_v2.xlsx` python cu ferestre A/B/W hardcodate; au fost combinate. `generate_ferestre_v2.py` a fost șters.)
|
||||
|
||||
**Reluare după ce Marius adaugă tranzacții noi:**
|
||||
```powershell
|
||||
python scripts/generate_ferestre_v2.py
|
||||
```
|
||||
Totul se recalculează automat din `backtest.xlsx` (R/$ deja calculate de Excel; scriptul nu recalculează formule). Conține: Concluzii, Tabel unic cu toate variantele, validări Forward 1 (lunar) / Forward 2 (train-test 70/30) / Walk-forward (3 felii) pe toate ferestrele, bootstrap CI, calendar, grafic echitate.
|
||||
|
||||
**ÎNAINTE de analiză — verifică typo-uri de tastare în Trades** (TP%/SL% cu zecimală lipsă umflă fals edge-ul). Cele găsite și corectate manual: #314 (TP2 17→0.17), #298 (TP0 0.5→0.05), #240 (TP1 0.8→0.08), #182 (ordine TP0/TP1 inversată → 0.06/0.10), #338 (TP1 0.011→0.11; era SL deci R neschimbat). La date noi, caută valori TP/SL ≥1 sau TP0>TP1>TP2 inversate și confirmă cu Marius înainte de a corecta.
|
||||
|
||||
**Findings curente (407 trade-uri, dec 2025–iun 2026, doar `hybrid_be` e pozitiv pe ansamblu ~+0.05R):** edge-ul vine din CÂND, nu din management; 18:00–19:00 RO = zonă moartă; ora de start optimă = 19:15. Trei configurații recomandate: **A** 19:15–20:15 (1h, edge max/timp min; ExpR +0.187), **B** 19:45–21:45 prima (cea mai robustă — ExpR +0.200, atinge pragul 0.20R, 100% bootstrap pozitiv, pozitivă în toate cele 7 luni, se întărește OOS), **W** 19:15–22:15 prima (volum/bani max raportat la timp; ExpR +0.135). Adăugarea lui decembrie a întărit B și a slăbit ușor A/W (dec a fost lună slabă pe start 19:15 și ferestre lungi). Filtrele direcționale (buy) par mai bune dar pică out-of-sample. Edge subțire → ipoteze de confirmat live.
|
||||
|
||||
## Dashboard separat (scripts/generate_dashboard.py)
|
||||
|
||||
`backtest.xlsx` ajunsese ~39 MB și se salva greu. Cauza NU erau tranzacțiile, ci **sheet-ul Dashboard** (~4.200 coloane-helper ascunse → ~2,1M celule cu formule în `calcChain.xml`). Soluție: Dashboard-ul a fost scos din fișierul editat zilnic într-un fișier separat, generat la comandă (același tipar ca Ferestre v2).
|
||||
|
||||
**Cele 3 fișiere:**
|
||||
- `data/backtest.xlsx` — **editat zilnic**, doar Config + Trades (~0.8 MB, rapid la salvat).
|
||||
- `data/Dashboard.xlsx` — **generat read-only** de `generate_dashboard.py` din backtest.xlsx. Conține un sheet `Trades` ascuns cu **valori statice** (copiate din cache-ul Excel) + sheet-ul Dashboard cu formulele reutilizate din `build_dashboard()`. Marius nu-l editează niciodată — se regenerează.
|
||||
- `data/Ferestre_v2.xlsx` — analiza edge/fereastră (separată, vezi mai sus).
|
||||
|
||||
**Reluare după tranzacții noi:**
|
||||
```powershell
|
||||
# întâi: deschide & SALVEAZĂ backtest.xlsx în Excel (populează cache-ul de valori R_/$_/Bal_)
|
||||
# întâi: deschide & SALVEAZĂ backtest.xlsx în Excel (populează cache-ul R_/$_/Bal_)
|
||||
python scripts/generate_dashboard.py # sau dublu-click refresh_dashboard.bat
|
||||
```
|
||||
`generate_dashboard.py` citește `backtest.xlsx` **read-only, `data_only=True`** — ia valorile DEJA calculate de Excel (nu recalculează formule). **Constrângere:** dacă nu ai salvat în Excel după ultima editare, cache-ul lipsește și Dashboard-ul iese gol (aceeași condiție ca Ferestre v2).
|
||||
`generate_dashboard.py` citește `backtest.xlsx` **read-only, `data_only=True`** — ia valorile DEJA calculate de Excel (nu recalculează formule). **Constrângere:** dacă nu ai salvat în Excel după ultima editare, cache-ul lipsește și analiza iese goală.
|
||||
|
||||
**Sheet-urile din `Dashboard.xlsx`:**
|
||||
- `Config` — inputurile reale (lot, limite prop) copiate din backtest + formule (`build_config()`).
|
||||
- `Trades` — snapshot static ascuns (valori, nu formule), ca să țină backtest.xlsx mic.
|
||||
- `Dashboard` — metrici 5 manageri + breakdown-uri (strategie/indicator/direcție) + prop compliance. **Formule** Excel (recalculează live la schimbarea Config). Grila veche de ferestre pe formule (`FERESTRE CANDIDATE` + `TOP 20`, ~mii de coloane-helper = cauza dimensiunii) a fost **scoasă** din `build_dashboard()`.
|
||||
- `Ferestre` — **auto-scan python** edge × durată × fiabilitate (valori statice). Vezi mai jos.
|
||||
- `date_grafic` — sursa curbei de echitate din sheet-ul Ferestre.
|
||||
|
||||
### Sheet-ul Ferestre — auto-scan (NIMIC hardcodat)
|
||||
|
||||
Construit de `build_ferestre()` în `generate_dashboard.py`. Găsește **fereastra (ora RO) cu cel mai bun edge** fără să spargă contul prop:
|
||||
- **Grilă generativă** la 15 min (start 16:30–22:00, durată 45min–capăt 23:00, filtru {toate, prima}), scanată pe **toate cele 5 manageri**; pe fiecare fereastră raportează managerul cu cel mai bun ExpR dintre cei non-breach. Grila la 15 min e **suprasetul** vechilor două grile (cea Dashboard la 30 min nici nu vedea ferestrele pe :15/:45).
|
||||
- **Recomandări derivate din date** (etichete = fereastra calculată, nu nume fixe), în familii cu gradații — funcția `recommend()`, praguri în constantele `EDGE_*`/`ROBUST_*`/`VOLUM_*`: **EDGE 45min/1h/1h30** (cea mai profitabilă fereastră la fix acea durată → cea mai mică = cea mai scurtă perioadă profitabilă), **ROBUST 1/2/3** (cel mai bun ExpR pozitiv în toate / ≥80% / ≥60% din luni, N≥40), **VOLUM (max N)** + **VOLUM compact** (cel mai mare volum profitabil, resp. cea mai scurtă fereastră tot cu volum relevant). Dedup pe (start,end) → un geam apare o singură dată. Câte se validează în adâncime = `FERESTRE_MAX_VARIANTS` (BRUT + recomandări + top-N profitabile).
|
||||
- **Validări** (recalculate de fiecare dată): Forward 1 (lunar), Forward 2 (train/test 70/30), walk-forward (3 felii), bootstrap CI 95%, calendar NFP, grafic echitate pe primele 2 recomandări.
|
||||
- Limitele prop (cont/daily/max) se citesc din Config (B9/B12/B14), fallback $50k/4%/7%.
|
||||
|
||||
**Fără concluzii hardcodate — NU le re-introduce.** Versiunea veche avea ferestrele A/B/W și concluziile lipite în script dintr-o analiză LLM anterioară; se învecheau la fiecare lot nou. Acum totul se re-derivă din date. Ferestrele bune reapar singure dacă rămân cele mai bune (ex. ROBUST 19:45–21:45 hybrid_be = fostul „B", re-găsit automat).
|
||||
|
||||
**Avertisment overfit:** scanul pe toate cele 5 manageri × mii de ferestre poate scoate în top combinații fragile (ex. `TP2 only` pe N mic = ExpR umflat de varianță). De-aia validările (bootstrap CI, lunar, train/test, walk-forward) sunt în același tabel — citește-le împreună cu ExpR-ul, nu doar vârful.
|
||||
|
||||
**ÎNAINTE de a interpreta — verifică typo-uri în Trades** (TP%/SL% cu zecimală lipsă umflă fals edge-ul). Cele găsite și corectate manual: #314 (TP2 17→0.17), #298 (TP0 0.5→0.05), #240 (TP1 0.8→0.08), #182 (ordine TP0/TP1 inversată → 0.06/0.10), #338 (TP1 0.011→0.11; era SL deci R neschimbat). La date noi, caută valori TP/SL ≥1 sau TP0>TP1>TP2 inversate și confirmă cu Marius înainte de a corecta.
|
||||
|
||||
**Sincronizare:** la comandă (rulezi scriptul/bat-ul), NU live. Marius a ales explicit acest model.
|
||||
|
||||
**Migrare unică — `scripts/strip_dashboard.py`:** scoate sheet-ul Dashboard din `backtest.xlsx` existent (39 MB → ~0.8 MB). NU folosește openpyxl pentru rescriere (ar șterge cele 12 dropdown-uri **x14** din Trades). Face **chirurgie pe zip**: elimină doar partea Dashboard + drawings/charts + `calcChain.xml` + definedName-ul orfan, lăsând XML-ul Config/Trades byte-cu-byte intact. Cere `--yes` și face backup automat. Alternativă 100% sigură: șterge tab-ul Dashboard manual în Excel (click-dreapta → Delete → Save). **Rulează doar cu acordul lui Marius** (e destructiv pe fișierul real).
|
||||
|
||||
**Escape hatch dimensiune Dashboard.xlsx:** `Config!B17` (Activează filtru Prima) = `NU` reduce drastic grid-ul de ferestre → Dashboard.xlsx mult mai mic/rapid.
|
||||
|
||||
## Reference docs
|
||||
|
||||
- `strategie_M2D.md` — M2D setup rules (color-coded dot bands on TF mare/mic, SL/TP placement, session filters).
|
||||
|
||||
Binary file not shown.
@@ -1,10 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Generează data/Dashboard.xlsx dintr-un snapshot al data/backtest.xlsx.
|
||||
|
||||
CITEȘTE backtest.xlsx (read-only, data_only=True) și SCRIE un fișier SEPARAT
|
||||
data/Dashboard.xlsx. NU atinge backtest.xlsx. Refolosește build_config() și
|
||||
build_dashboard() din generate_template.py — aceeași logică de Dashboard, dar
|
||||
pe un sheet Trades static (valori, fără formule), ca să țină backtest.xlsx mic.
|
||||
UN SINGUR fișier de analiză. Citește backtest.xlsx (read-only, data_only=True)
|
||||
și scrie data/Dashboard.xlsx cu TOATE analizele, în sheet-uri separate:
|
||||
|
||||
• Config — inputurile reale (lot, limite prop) + formule (din build_config).
|
||||
• Trades — snapshot static ascuns (valori R_/$_ deja calculate de Excel).
|
||||
• Dashboard — metrici 5 manageri + breakdown-uri + prop compliance (formule).
|
||||
• Ferestre — AUTO-SCAN edge × durată × fiabilitate (python, valori statice).
|
||||
Nicio fereastră hardcodată: grila se scanează la 15 min pe toate
|
||||
cele 5 manageri și recomandările se DERIVĂ din datele curente.
|
||||
• Toate ferestrele — grila completă scanată (toate ferestrele × 5 manageri) ca
|
||||
tabel plat cu AutoFilter, ca să filtrezi/sortezi singur.
|
||||
• date_grafic — sursa pentru curba de echitate din sheet-ul Ferestre.
|
||||
|
||||
Reruleaza prin refresh_dashboard.bat (sau direct):
|
||||
python scripts/generate_dashboard.py
|
||||
@@ -12,19 +20,35 @@ Reruleaza prin refresh_dashboard.bat (sau direct):
|
||||
IMPORTANT: deschide și SALVEAZĂ backtest.xlsx în Excel cel puțin o dată după
|
||||
ultima editare înainte de refresh. Scriptul citește valorile DEJA calculate de
|
||||
Excel (R_/$_/Bal_/helpere). Dacă nu ai salvat în Excel, cache-ul de valori
|
||||
lipsește și Dashboard-ul iese gol. (Aceeași constrângere ca Ferestre v2.)
|
||||
lipsește și analiza iese goală.
|
||||
|
||||
Istoric: înainte erau două fișiere (Dashboard.xlsx + Ferestre_v2.xlsx) cu două
|
||||
scannere de ferestre — unul pe formule (grila fixă din build_dashboard), altul pe
|
||||
python cu ferestre A/B/W hardcodate de o analiză LLM anterioară. Ambele au fost
|
||||
înlocuite cu un singur auto-scan generativ (grila la 15 min e suprasetul ambelor).
|
||||
"""
|
||||
|
||||
import statistics
|
||||
import random
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, time, date, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import openpyxl
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
from openpyxl.chart import LineChart, Reference
|
||||
from openpyxl.drawing.line import LineProperties
|
||||
from openpyxl.chart.shapes import GraphicalProperties
|
||||
|
||||
from generate_template import (
|
||||
build_config,
|
||||
build_dashboard,
|
||||
TRADES_HEADERS,
|
||||
MAX_ROWS,
|
||||
STRAT_KEYS,
|
||||
STRAT_LABELS,
|
||||
)
|
||||
|
||||
SRC = Path(__file__).resolve().parent.parent / "data" / "backtest.xlsx"
|
||||
@@ -90,6 +114,612 @@ def copy_trades_values(wb: Workbook, ws_src) -> None:
|
||||
ws.sheet_state = "hidden" # snapshot intern; Dashboard e singurul vizibil util
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# FERESTRE — auto-scan edge × durată × fiabilitate (python, valori statice)
|
||||
# ===========================================================================
|
||||
#
|
||||
# Înlocuiește vechiul Ferestre_v2.xlsx (ferestre A/B/W hardcodate). Aici NIMIC
|
||||
# nu e fixat: grila de ferestre candidate se generează parametric la 15 min,
|
||||
# fiecare e evaluată pe toate cele 5 manageri pe DATELE CURENTE, iar cele 3
|
||||
# recomandări (edge/durată, robust, volum) se aleg după criterii obiective.
|
||||
|
||||
ROLUNI = {1: "Ian", 2: "Feb", 3: "Mar", 4: "Apr", 5: "Mai", 6: "Iun",
|
||||
7: "Iul", 8: "Aug", 9: "Sep", 10: "Oct", 11: "Noi", 12: "Dec"}
|
||||
|
||||
# Parametrii grilei de scan (ora RO, în minute de la miezul nopții).
|
||||
SCAN_START_MIN = 16 * 60 + 30 # 16:30 — cel mai devreme start testat
|
||||
SCAN_LAST_START = 22 * 60 # 22:00 — cel mai târziu start testat
|
||||
SCAN_HARD_END = 23 * 60 # 23:00 — capăt maxim al oricărei ferestre
|
||||
SCAN_MIN_DUR = 45 # durată minimă fereastră (min)
|
||||
SCAN_STEP = 15 # rezoluție (min) — suprasetul vechilor grile
|
||||
SCAN_MIN_N = 30 # nr. minim de tranzacții ca o fereastră să conteze
|
||||
NBOOT = 5000 # re-eșantioane bootstrap pentru CI
|
||||
|
||||
# Câte ferestre intră în validarea detaliată din sheet-ul Ferestre (BRUT + cele 3
|
||||
# recomandări + restul = top-N profitabile auto-incluse). Pragurile decid ce e
|
||||
# "profitabilă" pentru auto-includere — ridică-le ca să vezi mai puține/mai multe.
|
||||
FERESTRE_MAX_VARIANTS = 20 # total maxim de rânduri validate (BRUT + recs + top scan)
|
||||
FERESTRE_TOP_MIN_N = 40 # N minim pentru o fereastră suplimentară auto-inclusă
|
||||
FERESTRE_TOP_MIN_EXPR = 0.10 # ExpR minim pentru "profitabilă" la auto-includere
|
||||
|
||||
|
||||
def load_trades(path: Path) -> list[dict]:
|
||||
"""Citește tranzacțiile cu outcome din backtest.xlsx (valori cache Excel)."""
|
||||
wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
|
||||
ws = wb["Trades"]
|
||||
it = ws.iter_rows(min_row=1, values_only=True)
|
||||
hdr = next(it)
|
||||
h = {x: i for i, x in enumerate(hdr) if x is not None}
|
||||
rows: list[dict] = []
|
||||
for row in it:
|
||||
o = row[h["Outcome"]]
|
||||
if o is None or str(o).strip() == "":
|
||||
continue
|
||||
t = row[h["Ora RO"]]
|
||||
if isinstance(t, (int, float)):
|
||||
hh = int(t); mm = round((t - hh) * 100); tt = time(hh, mm)
|
||||
else:
|
||||
p = str(t).split(":"); tt = time(int(p[0]), int(p[1]))
|
||||
d = datetime.fromisoformat(str(row[h["Data"]])).date()
|
||||
rec = {"d": d, "min": tt.hour * 60 + tt.minute,
|
||||
"t": tt.strftime("%H:%M"), "dir": row[h["Direcție"]]}
|
||||
for s in STRAT_KEYS:
|
||||
rec["R_" + s] = row[h["R_" + s]]
|
||||
rec["$_" + s] = row[h["$_" + s]]
|
||||
rows.append(rec)
|
||||
wb.close()
|
||||
rows.sort(key=lambda r: (r["d"], r["min"]))
|
||||
return rows
|
||||
|
||||
|
||||
# ---------- engine ----------
|
||||
def in_window(rows, s, e):
|
||||
return [r for r in rows if s <= r["min"] < e]
|
||||
|
||||
|
||||
def apply_filter(rows, f):
|
||||
"""'toate' = tot; 'prima' = prima tranzacție cronologic din fiecare zi."""
|
||||
if f == "toate":
|
||||
return rows
|
||||
byd = defaultdict(list)
|
||||
for r in rows:
|
||||
byd[r["d"]].append(r)
|
||||
out = []
|
||||
if f == "prima":
|
||||
for d, rs in byd.items():
|
||||
out.append(rs[0])
|
||||
out.sort(key=lambda r: (r["d"], r["min"]))
|
||||
return out
|
||||
|
||||
|
||||
def metrics(rows, strat, acct, daily, maxl):
|
||||
"""Metrici + flag breach prop pe un subset, pentru un manager."""
|
||||
n = len(rows)
|
||||
if n == 0:
|
||||
return None
|
||||
R = [r["R_" + strat] for r in rows]
|
||||
D = [r["$_" + strat] for r in rows]
|
||||
if any(x is None for x in R) or any(x is None for x in D):
|
||||
return None
|
||||
eq = acct; peak = acct; maxdd = 0.0
|
||||
cur = None; daycum = 0.0; daymin = 0.0; dbreach = False; mbreach = False
|
||||
for r in rows:
|
||||
if r["d"] != cur:
|
||||
cur = r["d"]; daycum = 0.0; daymin = 0.0
|
||||
daycum += r["$_" + strat]; daymin = min(daymin, daycum)
|
||||
if -daymin >= daily:
|
||||
dbreach = True
|
||||
eq += r["$_" + strat]; peak = max(peak, eq); maxdd = max(maxdd, peak - eq)
|
||||
if peak - eq >= maxl:
|
||||
mbreach = True
|
||||
return dict(n=n, wr=sum(1 for x in R if x > 0) / n * 100,
|
||||
exp=statistics.mean(R), totR=sum(R), totD=sum(D),
|
||||
maxdd=maxdd, breach=dbreach or mbreach)
|
||||
|
||||
|
||||
def monthly_expr(rows, strat) -> dict[str, float]:
|
||||
bym = defaultdict(list)
|
||||
for r in rows:
|
||||
bym[f"{r['d']:%Y-%m}"].append(r["R_" + strat])
|
||||
return {m: statistics.mean(v) for m, v in bym.items()}
|
||||
|
||||
|
||||
def bootstrap(rows, strat, nboot=NBOOT, seed=12345):
|
||||
"""Re-eșantionare cu înlocuire: CI 95% pentru ExpR."""
|
||||
rnd = random.Random(seed)
|
||||
R = [r["R_" + strat] for r in rows]
|
||||
n = len(R)
|
||||
if n == 0:
|
||||
return dict(expR_lo=0.0, expR_hi=0.0, p_pos=0.0)
|
||||
means = []
|
||||
for _ in range(nboot):
|
||||
means.append(sum(R[rnd.randrange(n)] for _ in range(n)) / n)
|
||||
means.sort()
|
||||
|
||||
def pct(p):
|
||||
return means[min(len(means) - 1, int(p * len(means)))]
|
||||
|
||||
return dict(expR_lo=pct(0.025), expR_hi=pct(0.975),
|
||||
p_pos=sum(1 for x in means if x > 0) / nboot * 100)
|
||||
|
||||
|
||||
# ---------- scan + recomandări ----------
|
||||
def scan(T, acct, daily, maxl):
|
||||
"""Scanează grila la 15 min × {toate, prima} × 5 manageri.
|
||||
|
||||
Pentru fiecare fereastră alege managerul cu cel mai bun ExpR dintre cei
|
||||
care NU sparg contul prop pe acea fereastră. Întoarce lista de candidați.
|
||||
"""
|
||||
cands = []
|
||||
for s in range(SCAN_START_MIN, SCAN_LAST_START + 1, SCAN_STEP):
|
||||
for e in range(s + SCAN_MIN_DUR, SCAN_HARD_END + 1, SCAN_STEP):
|
||||
for filt in ("toate", "prima"):
|
||||
sel = apply_filter(in_window(T, s, e), filt)
|
||||
if len(sel) < SCAN_MIN_N:
|
||||
continue
|
||||
best = None
|
||||
for st in STRAT_KEYS:
|
||||
m = metrics(sel, st, acct, daily, maxl)
|
||||
if m is None or m["breach"]:
|
||||
continue
|
||||
if best is None or m["exp"] > best[1]["exp"]:
|
||||
best = (st, m)
|
||||
if best is None:
|
||||
continue
|
||||
st, m = best
|
||||
mo = monthly_expr(sel, st)
|
||||
cands.append(dict(
|
||||
s=s, e=e, filt=filt, dur=e - s, strat=st,
|
||||
n=m["n"], wr=m["wr"], exp=m["exp"], totD=m["totD"],
|
||||
maxdd=m["maxdd"],
|
||||
mpos=sum(1 for v in mo.values() if v > 0), mtot=len(mo),
|
||||
))
|
||||
return cands
|
||||
|
||||
|
||||
def _pick(cands, predicate, key, taken):
|
||||
"""Cel mai bun candidat (după key, desc) care trece predicate și nu e deja luat.
|
||||
|
||||
Dedup pe (start, end) — un geam apare o singură dată în recomandări, indiferent
|
||||
de filtru/manager (ca ROBUST 2 să nu repete fereastra deja luată de EDGE etc.).
|
||||
"""
|
||||
pool = [c for c in cands
|
||||
if predicate(c) and (c["s"], c["e"]) not in taken]
|
||||
if not pool:
|
||||
return None
|
||||
best = max(pool, key=key)
|
||||
taken.add((best["s"], best["e"]))
|
||||
return best
|
||||
|
||||
|
||||
# Constante de reglare a recomandărilor (modifică-le ca să schimbi criteriile).
|
||||
EDGE_DURATIONS = [45, 60, 90] # paliere de durată (min): cea mai mică fereastră + 1h + 1h30
|
||||
EDGE_MIN_EXPR = 0.10 # ExpR minim ca o fereastră "edge" să fie profitabilă
|
||||
ROBUST_MIN_N = 40 # N minim pentru recomandările robust
|
||||
ROBUST_GRADES = [ # grade de consistență (fracțiune minimă de luni pozitive)
|
||||
("ROBUST 1 (toate lunile)", 1.00),
|
||||
("ROBUST 2 (≥80% luni)", 0.80),
|
||||
("ROBUST 3 (≥60% luni)", 0.60),
|
||||
]
|
||||
VOLUM_MIN_EXPR = 0.10 # ExpR minim ca o fereastră de volum să conteze
|
||||
VOLUM_MIN_N = 60 # N minim ca "VOLUM compact" să fie relevant statistic
|
||||
|
||||
|
||||
def recommend(cands):
|
||||
"""Derivă recomandările din scan (etichete = ferestrele calculate, nimic fix).
|
||||
|
||||
Familii: EDGE (cea mai mică fereastră profitabilă, pe paliere de durată),
|
||||
ROBUST 1/2/3 (grade de consistență lunară de la strict la lax) și VOLUM (cele
|
||||
mai multe tranzacții profitabile + cea mai densă fereastră). Pragurile sunt în
|
||||
constantele de mai sus. Dedup: fiecare fereastră apare o singură dată.
|
||||
"""
|
||||
taken: set = set()
|
||||
out = []
|
||||
|
||||
def add(role, predicate, key):
|
||||
c = _pick(cands, predicate, key, taken)
|
||||
if c:
|
||||
out.append((role, c))
|
||||
|
||||
# EDGE — cea mai mică perioadă profitabilă, pe paliere de durată (45min, 1h, 1h30).
|
||||
for dur in EDGE_DURATIONS:
|
||||
h, m = divmod(dur, 60)
|
||||
lab = f"EDGE {h}h{m:02d}" if h else f"EDGE {m}min"
|
||||
add(lab, lambda c, d=dur: c["dur"] == d and c["exp"] >= EDGE_MIN_EXPR,
|
||||
key=lambda c: (c["exp"], c["n"]))
|
||||
|
||||
# ROBUST — grade de consistență lunară (strict → lax).
|
||||
for role, frac in ROBUST_GRADES:
|
||||
add(role,
|
||||
lambda c, f=frac: c["n"] >= ROBUST_MIN_N and c["mpos"] / max(c["mtot"], 1) >= f,
|
||||
key=lambda c: (c["exp"], c["n"]))
|
||||
|
||||
# VOLUM — cel mai mare volum (relevant statistic) + cea mai SCURTĂ fereastră
|
||||
# care încă are volum relevant (N≥VOLUM_MIN_N).
|
||||
add("VOLUM (max N)", lambda c: c["exp"] >= VOLUM_MIN_EXPR,
|
||||
key=lambda c: (c["n"], -c["dur"]))
|
||||
add(f"VOLUM compact (N≥{VOLUM_MIN_N})",
|
||||
lambda c: c["exp"] >= VOLUM_MIN_EXPR and c["n"] >= VOLUM_MIN_N,
|
||||
key=lambda c: (-c["dur"], c["n"]))
|
||||
return out
|
||||
|
||||
|
||||
# ---------- styles (sheet Ferestre) ----------
|
||||
F_TITLE = Font(bold=True, size=14, color="1F4E78")
|
||||
F_SUB = Font(bold=True, size=11, color="1F4E78")
|
||||
F_HF = Font(bold=True, color="FFFFFF")
|
||||
F_HFILL = PatternFill("solid", fgColor="1F4E78")
|
||||
F_GOOD = PatternFill("solid", fgColor="C6EFCE")
|
||||
F_WARN = PatternFill("solid", fgColor="FFEB9C")
|
||||
F_BAD = PatternFill("solid", fgColor="FFC7CE")
|
||||
F_GREY = PatternFill("solid", fgColor="F2F2F2")
|
||||
F_CTR = Alignment(horizontal="center")
|
||||
F_LEFT = Alignment(horizontal="left", wrap_text=True)
|
||||
F_THIN = Border(left=Side(style="thin", color="BFBFBF"),
|
||||
right=Side(style="thin", color="BFBFBF"),
|
||||
top=Side(style="thin", color="BFBFBF"),
|
||||
bottom=Side(style="thin", color="BFBFBF"))
|
||||
|
||||
|
||||
def _fmt(m):
|
||||
return f"{m // 60:02d}:{m % 60:02d}"
|
||||
|
||||
|
||||
def _wlabel(cfg):
|
||||
return "(fără fer.)" if cfg["e"] - cfg["s"] >= 1440 else f"{_fmt(cfg['s'])}-{_fmt(cfg['e'])}"
|
||||
|
||||
|
||||
def _expcolor(e):
|
||||
return F_GOOD if e >= 0.10 else (F_WARN if e >= 0 else F_BAD)
|
||||
|
||||
|
||||
def build_ferestre(wb: Workbook, T: list[dict], acct, daily, maxl) -> None:
|
||||
"""Construiește sheet-urile Ferestre + date_grafic din auto-scan."""
|
||||
ws = wb.create_sheet("Ferestre")
|
||||
ws.sheet_view.showGridLines = False
|
||||
for i, w in enumerate([22, 14, 8, 9, 13, 7, 8, 9, 16, 9, 11, 10], 1):
|
||||
ws.column_dimensions[get_column_letter(i)].width = w
|
||||
|
||||
d0 = min(r["d"] for r in T); d1 = max(r["d"] for r in T)
|
||||
alld = sorted(set(r["d"] for r in T))
|
||||
months = sorted({f"{r['d']:%Y-%m}" for r in T})
|
||||
cut = alld[int(len(alld) * 0.70)]
|
||||
tr = [r for r in T if r["d"] < cut]
|
||||
te = [r for r in T if r["d"] >= cut]
|
||||
|
||||
cands = scan(T, acct, daily, maxl)
|
||||
recs = recommend(cands)
|
||||
|
||||
# BRUT (fără fereastră) — referință, managerul cel mai bun pe tot setul.
|
||||
brut_strat = max(
|
||||
STRAT_KEYS,
|
||||
key=lambda st: metrics(T, st, acct, daily, maxl)["exp"],
|
||||
)
|
||||
brut = dict(s=0, e=1440, filt="toate", strat=brut_strat)
|
||||
|
||||
# VARIANTE pentru tabel + validări: BRUT + 3 recomandări + top scan suplimentar.
|
||||
variants = [("BRUT (ref.)", brut, F_GREY)]
|
||||
taken = {(0, 1440)}
|
||||
for role, c in recs:
|
||||
variants.append((role, c, F_GOOD))
|
||||
taken.add((c["s"], c["e"]))
|
||||
for c in sorted(cands, key=lambda c: c["exp"], reverse=True):
|
||||
if len(variants) >= FERESTRE_MAX_VARIANTS:
|
||||
break
|
||||
if (c["s"], c["e"]) in taken:
|
||||
continue
|
||||
if c["n"] < FERESTRE_TOP_MIN_N or c["exp"] < FERESTRE_TOP_MIN_EXPR:
|
||||
continue
|
||||
taken.add((c["s"], c["e"]))
|
||||
variants.append(("top scan", c, None))
|
||||
|
||||
state = {"R": 1}
|
||||
|
||||
def put(r, c, v, font=None, fill=None, fmtn=None, align=None, border=False):
|
||||
cell = ws.cell(row=r, column=c, value=v)
|
||||
if font: cell.font = font
|
||||
if fill: cell.fill = fill
|
||||
if fmtn: cell.number_format = fmtn
|
||||
if align: cell.alignment = align
|
||||
if border: cell.border = F_THIN
|
||||
return cell
|
||||
|
||||
def title(txt):
|
||||
put(state["R"], 1, txt, F_SUB); state["R"] += 1
|
||||
|
||||
def headers(hs):
|
||||
for j, hh in enumerate(hs, 1):
|
||||
put(state["R"], j, hh, F_HF, F_HFILL, align=F_CTR, border=True)
|
||||
state["R"] += 1
|
||||
|
||||
def row(vals, fills=None, fmts=None):
|
||||
for j, v in enumerate(vals, 1):
|
||||
f = fills[j - 1] if fills else None
|
||||
nf = fmts[j - 1] if fmts else None
|
||||
put(state["R"], j, v, fill=f, fmtn=nf, border=True,
|
||||
align=F_CTR if j > 1 else F_LEFT)
|
||||
state["R"] += 1
|
||||
|
||||
def note(txt):
|
||||
put(state["R"], 1, txt, align=F_LEFT); state["R"] += 1
|
||||
|
||||
def blank():
|
||||
state["R"] += 1
|
||||
|
||||
def pad(vals, fills, fmts, n=12):
|
||||
while len(vals) < n:
|
||||
vals.append(""); fills.append(None); fmts.append(None)
|
||||
return vals, fills, fmts
|
||||
|
||||
def msel(rows, cfg):
|
||||
return metrics(apply_filter(in_window(rows, cfg["s"], cfg["e"]), cfg["filt"]),
|
||||
cfg["strat"], acct, daily, maxl)
|
||||
|
||||
# ---- titlu (fără note de intro — tabelul de mai jos vorbește) ----
|
||||
put(state["R"], 1, "FERESTRE — auto-scan edge × durată × fiabilitate", F_TITLE)
|
||||
state["R"] += 1
|
||||
blank()
|
||||
|
||||
# ---- TABEL UNIC ----
|
||||
title("TABEL — ferestrele de top din scan (rol + manager cel mai bun)")
|
||||
note(f"Roluri auto-derivate (nimic hardcodat): BRUT = fără fereastră (referință) · "
|
||||
f"EDGE 45min/1h/1h30 = cea mai profitabilă fereastră la EXACT acea durată (45min = cea mai scurtă perioadă profitabilă) · "
|
||||
f"ROBUST 1/2/3 = cel mai bun ExpR pozitiv în toate / ≥80% / ≥60% din luni (N≥{ROBUST_MIN_N}) · "
|
||||
f"VOLUM (max N) = cel mai mare volum profitabil (relevant statistic) · VOLUM compact = cea mai scurtă fereastră cu N≥{VOLUM_MIN_N} · "
|
||||
f"top scan = restul ferestrelor profitabile (N≥{FERESTRE_TOP_MIN_N}, ExpR≥{FERESTRE_TOP_MIN_EXPR:.2f}, non-breach) după ExpR, până la {FERESTRE_MAX_VARIANTS} total. "
|
||||
f"CI 95% = interval bootstrap · OOS = ExpR pe ultimele ~30% zile · Manager = cel mai bun dintre cei 5. "
|
||||
"Vezi 'Toate ferestrele' pentru grila completă filtrabilă.")
|
||||
headers(["Rol", "Fereastră", "Durată", "Filtru", "Manager", "N", "WR%",
|
||||
"ExpR", "CI 95% ExpR", "OOS", "$ total", "maxDD$"])
|
||||
for role, cfg, fill in variants:
|
||||
sel = apply_filter(in_window(T, cfg["s"], cfg["e"]), cfg["filt"])
|
||||
m = metrics(sel, cfg["strat"], acct, daily, maxl)
|
||||
b = bootstrap(sel, cfg["strat"])
|
||||
oosm = msel(te, cfg); oosx = oosm["exp"] if oosm else 0.0
|
||||
dd = cfg["e"] - cfg["s"]
|
||||
durs = "—" if dd >= 1440 else f"{dd // 60}h{dd % 60:02d}"
|
||||
row([role, _wlabel(cfg), durs, cfg["filt"], STRAT_LABELS[cfg["strat"]],
|
||||
m["n"], m["wr"], round(m["exp"], 3),
|
||||
f"[{b['expR_lo']:+.2f};{b['expR_hi']:+.2f}]", round(oosx, 3),
|
||||
round(m["totD"]), round(m["maxdd"])],
|
||||
fills=[fill, None, None, None, None, None, None, None, None,
|
||||
_expcolor(oosx), None, None],
|
||||
fmts=[None, None, None, None, None, "0", '0.0"%"', "0.000", None,
|
||||
"0.000", "$#,##0", "$#,##0"])
|
||||
blank()
|
||||
|
||||
# ---- explicații validări (definiții, NU concluzii) ----
|
||||
title("CE ÎNSEAMNĂ VALIDĂRILE")
|
||||
note("• Forward 1 (LUNAR): ExpR în fiecare lună separat. Pozitiv în toate = edge constant, nu noroc concentrat. N mic/lună → o tranzacție mișcă mult media.")
|
||||
note("• Forward 2 (TRAIN/TEST): primele 70% din zile = train, ultimele 30% = test (nevăzut la alegere). ExpR test ≈ train → robust; mult mai mic/negativ → overfit.")
|
||||
note("• Walk-forward (3 FELII): perioada în 3 bucăți cronologice. O regulă bună rămâne pozitivă în toate trei, nu doar la început.")
|
||||
note("• Culori: VERDE ≥0.10R · GALBEN 0–0.10R · ROȘU negativ · gol = nicio tranzacție în acea felie/lună.")
|
||||
blank()
|
||||
|
||||
# ---- FORWARD 1 — LUNAR ----
|
||||
mlabels = [ROLUNI[int(m[5:7])] for m in months]
|
||||
title("FORWARD 1 — consistență LUNARĂ (ExpR pe fiecare lună)")
|
||||
headers(["Variantă", "Fereastră"] + mlabels)
|
||||
for role, cfg, fill in variants:
|
||||
sel = apply_filter(in_window(T, cfg["s"], cfg["e"]), cfg["filt"])
|
||||
bym = defaultdict(list)
|
||||
for r in sel:
|
||||
bym[f"{r['d']:%Y-%m}"].append(r["R_" + cfg["strat"]])
|
||||
vals = [role, _wlabel(cfg)]; fills = [fill, None]; fmts = [None, None]
|
||||
for m in months:
|
||||
rr = bym.get(m, [])
|
||||
if rr:
|
||||
e = statistics.mean(rr)
|
||||
vals.append(round(e, 3)); fills.append(_expcolor(e)); fmts.append("0.000")
|
||||
else:
|
||||
vals.append(""); fills.append(F_GREY); fmts.append(None)
|
||||
row(*pad(vals, fills, fmts, n=2 + len(months)))
|
||||
blank()
|
||||
|
||||
# ---- FORWARD 2 — TRAIN/TEST ----
|
||||
title("FORWARD 2 — TRAIN/TEST 70/30")
|
||||
note(f"Train: {tr[0]['d']:%d.%m}–{cut:%d.%m} · Test/OOS: {cut:%d.%m}–{d1:%d.%m}. "
|
||||
"Verde la ExpR test = edge-ul a ținut pe date nevăzute. Δ≈0 sau pozitiv = stabil; foarte negativ = overfit.")
|
||||
headers(["Variantă", "Fereastră", "N train", "ExpR train", "N test",
|
||||
"ExpR test (OOS)", "Δ (test−train)"])
|
||||
for role, cfg, fill in variants:
|
||||
mtr = msel(tr, cfg); mte = msel(te, cfg)
|
||||
etr = mtr["exp"] if mtr else 0.0; ete = mte["exp"] if mte else 0.0
|
||||
ntr = mtr["n"] if mtr else 0; nte = mte["n"] if mte else 0
|
||||
row([role, _wlabel(cfg), ntr, round(etr, 3), nte, round(ete, 3),
|
||||
round(ete - etr, 3)],
|
||||
fills=[fill, None, None, None, None, _expcolor(ete), None],
|
||||
fmts=[None, None, "0", "0.000", "0", "0.000", "0.000"])
|
||||
blank()
|
||||
|
||||
# ---- WALK-FORWARD — 3 FELII ----
|
||||
n3 = len(alld) // 3
|
||||
P = [set(alld[:n3]), set(alld[n3:2 * n3]), set(alld[2 * n3:])]
|
||||
pr = [(alld[0], alld[n3 - 1]), (alld[n3], alld[2 * n3 - 1]), (alld[2 * n3], alld[-1])]
|
||||
title("WALK-FORWARD — edge pe 3 FELII cronologice")
|
||||
note("P1=%s–%s · P2=%s–%s · P3=%s–%s. Pozitiv (verde) în toate trei = edge stabil în timp." % (
|
||||
pr[0][0].strftime("%d.%m"), pr[0][1].strftime("%d.%m"),
|
||||
pr[1][0].strftime("%d.%m"), pr[1][1].strftime("%d.%m"),
|
||||
pr[2][0].strftime("%d.%m"), pr[2][1].strftime("%d.%m")))
|
||||
headers(["Variantă", "Fereastră", "P1 ExpR", "P2 ExpR", "P3 ExpR", "N total"])
|
||||
for role, cfg, fill in variants:
|
||||
sel = apply_filter(in_window(T, cfg["s"], cfg["e"]), cfg["filt"])
|
||||
vals = [role, _wlabel(cfg)]; fills = [fill, None]; fmts = [None, None]
|
||||
for ps in P:
|
||||
rr = [r for r in sel if r["d"] in ps]
|
||||
if rr:
|
||||
e = statistics.mean([x["R_" + cfg["strat"]] for x in rr])
|
||||
vals.append(round(e, 3)); fills.append(_expcolor(e)); fmts.append("0.000")
|
||||
else:
|
||||
vals.append(""); fills.append(F_GREY); fmts.append(None)
|
||||
vals.append(len(sel)); fills.append(None); fmts.append("0")
|
||||
row(vals, fills, fmts)
|
||||
blank()
|
||||
|
||||
# ---- CALENDAR (FOMC/NFP) ----
|
||||
title("CALENDAR EVENIMENTE — influență?")
|
||||
yrs = sorted({d.year for d in alld})
|
||||
|
||||
def first_fri(y, mo):
|
||||
d = date(y, mo, 1)
|
||||
while d.weekday() != 4:
|
||||
d += timedelta(days=1)
|
||||
return d
|
||||
|
||||
NFP = {first_fri(y, mo) for y in yrs for mo in range(1, 13)
|
||||
if date(y, mo, 1) <= d1}
|
||||
headers(["Grup", "N", "WR%", "ExpR (best mgr)"])
|
||||
|
||||
def grp(rows):
|
||||
if not rows:
|
||||
return 0, 0, 0
|
||||
best = max(STRAT_KEYS, key=lambda st: statistics.mean([r["R_" + st] for r in rows]))
|
||||
Rm = [r["R_" + best] for r in rows]
|
||||
return len(Rm), sum(1 for x in Rm if x > 0) / len(Rm) * 100, statistics.mean(Rm)
|
||||
|
||||
for label, rows in (("Zile NFP (prima vineri)", [r for r in T if r["d"] in NFP]),
|
||||
("Restul zilelor", [r for r in T if r["d"] not in NFP])):
|
||||
n, wr, ex = grp(rows)
|
||||
row([label, n, wr, round(ex, 3)],
|
||||
fmts=[None, "0", '0.0"%"', "0.000"])
|
||||
note("Prea puține zile de eveniment pentru o regulă de news-filter; aici doar ca verificare că nu strică edge-ul.")
|
||||
blank()
|
||||
|
||||
# ---- GRAFIC — curbă de echitate pe primele 2 recomandări ----
|
||||
def _find(role_name):
|
||||
for role, cfg, _ in variants:
|
||||
if role == role_name:
|
||||
return (role, cfg)
|
||||
return None
|
||||
|
||||
chart_variants = [v for v in (_find(ROBUST_GRADES[0][0]), _find("VOLUM (max N)")) if v]
|
||||
if len(chart_variants) < 2: # fallback: primele 2 recomandări non-BRUT
|
||||
for role, cfg, _ in variants:
|
||||
if role != "BRUT (ref.)" and (role, cfg) not in chart_variants:
|
||||
chart_variants.append((role, cfg))
|
||||
if len(chart_variants) == 2:
|
||||
break
|
||||
chart_variants = chart_variants[:2]
|
||||
if len(chart_variants) == 2:
|
||||
(r1, c1), (r2, c2) = chart_variants
|
||||
l1 = f"{r1} {_fmt(c1['s'])}-{_fmt(c1['e'])}"
|
||||
l2 = f"{r2} {_fmt(c2['s'])}-{_fmt(c2['e'])}"
|
||||
title(f"GRAFIC — curbă de echitate ($ cumulativ): {l1} vs {l2}")
|
||||
note("Aliniate pe dată. Compară câștigul și 'netezimea' celor mai bune două recomandări.")
|
||||
chart_anchor = f"A{state['R'] + 1}"
|
||||
|
||||
def daily_sum(cfg):
|
||||
sel = apply_filter(in_window(T, cfg["s"], cfg["e"]), cfg["filt"])
|
||||
byd = defaultdict(float)
|
||||
for r in sel:
|
||||
byd[r["d"]] += r["$_" + cfg["strat"]]
|
||||
return byd
|
||||
|
||||
cumA = daily_sum(c1); cumB = daily_sum(c2)
|
||||
ds = wb.create_sheet("date_grafic")
|
||||
ds.append(["Data", l1, l2])
|
||||
accA = 0.0; accB = 0.0
|
||||
for d in alld:
|
||||
accA += cumA.get(d, 0.0); accB += cumB.get(d, 0.0)
|
||||
ds.append([d, round(accA), round(accB)])
|
||||
nrows = len(alld)
|
||||
for rr in range(2, nrows + 2):
|
||||
ds.cell(row=rr, column=1).number_format = "dd.mm"
|
||||
|
||||
chart = LineChart()
|
||||
chart.title = f"Curbă de echitate ($ cumulativ) — {l1} vs {l2}"
|
||||
chart.style = 2
|
||||
chart.height = 9.5; chart.width = 24
|
||||
chart.y_axis.title = "$ cumulativ (cont prop)"
|
||||
chart.x_axis.title = "Data"
|
||||
chart.x_axis.number_format = "dd.mm"
|
||||
chart.x_axis.majorTimeUnit = "days"
|
||||
chart.x_axis.delete = False
|
||||
chart.y_axis.delete = False
|
||||
data = Reference(ds, min_col=2, max_col=3, min_row=1, max_row=nrows + 1)
|
||||
cats = Reference(ds, min_col=1, min_row=2, max_row=nrows + 1)
|
||||
chart.add_data(data, titles_from_data=True)
|
||||
chart.set_categories(cats)
|
||||
for s, color in zip(chart.series, ("2E7D32", "1F4E78")):
|
||||
s.graphicalProperties = GraphicalProperties()
|
||||
s.graphicalProperties.line = LineProperties(solidFill=color, w=20000)
|
||||
s.smooth = False
|
||||
ws.add_chart(chart, chart_anchor)
|
||||
ds.column_dimensions["A"].width = 11
|
||||
for col in ("B", "C"):
|
||||
ds.column_dimensions[col].width = 18
|
||||
|
||||
|
||||
def build_scan_table(wb: Workbook, T: list[dict], acct, daily, maxl) -> int:
|
||||
"""Sheet cu TOATE ferestrele scanate × toți 5 managerii — tabel plat filtrabil.
|
||||
|
||||
Spre deosebire de sheet-ul Ferestre (care arată doar top-ul + validările), aici
|
||||
e grila completă cu AutoFilter, ca Marius să filtreze singur după N, ExpR,
|
||||
breach, manager, durată etc. Sortat descrescător după ExpR.
|
||||
"""
|
||||
ws = wb.create_sheet("Toate ferestrele")
|
||||
ws.sheet_view.showGridLines = False
|
||||
|
||||
alld = sorted(set(r["d"] for r in T))
|
||||
cut = alld[int(len(alld) * 0.70)]
|
||||
te = [r for r in T if r["d"] >= cut]
|
||||
|
||||
head = ["Start", "End", "Durată min", "Filtru", "Manager", "N", "WR%",
|
||||
"ExpR", "OOS ExpR", "$ total", "maxDD$", "Breach", "Luni+", "Luni tot"]
|
||||
for j, h in enumerate(head, 1):
|
||||
c = ws.cell(1, j, h)
|
||||
c.font = F_HF; c.fill = F_HFILL; c.alignment = F_CTR; c.border = F_THIN
|
||||
|
||||
rows = []
|
||||
for s in range(SCAN_START_MIN, SCAN_LAST_START + 1, SCAN_STEP):
|
||||
for e in range(s + SCAN_MIN_DUR, SCAN_HARD_END + 1, SCAN_STEP):
|
||||
for filt in ("toate", "prima"):
|
||||
sel = apply_filter(in_window(T, s, e), filt)
|
||||
if not sel:
|
||||
continue
|
||||
seloos = apply_filter(in_window(te, s, e), filt)
|
||||
for st in STRAT_KEYS:
|
||||
m = metrics(sel, st, acct, daily, maxl)
|
||||
if m is None:
|
||||
continue
|
||||
mo = monthly_expr(sel, st)
|
||||
moos = metrics(seloos, st, acct, daily, maxl)
|
||||
rows.append((s, e, filt, st, m, mo, moos))
|
||||
|
||||
rows.sort(key=lambda x: x[4]["exp"], reverse=True)
|
||||
|
||||
r = 2
|
||||
for s, e, filt, st, m, mo, moos in rows:
|
||||
oos = round(moos["exp"], 3) if moos else ""
|
||||
vals = [_fmt(s), _fmt(e), e - s, filt, STRAT_LABELS[st], m["n"],
|
||||
m["wr"], round(m["exp"], 3), oos, round(m["totD"]),
|
||||
round(m["maxdd"]), "DA" if m["breach"] else "NU",
|
||||
sum(1 for v in mo.values() if v > 0), len(mo)]
|
||||
for j, v in enumerate(vals, 1):
|
||||
cell = ws.cell(r, j, v)
|
||||
cell.border = F_THIN
|
||||
cell.alignment = F_LEFT if j in (4, 5) else F_CTR
|
||||
ws.cell(r, 6).number_format = "0"
|
||||
ws.cell(r, 7).number_format = '0.0"%"'
|
||||
ws.cell(r, 8).number_format = "+0.000;-0.000;0.000"
|
||||
if moos:
|
||||
ws.cell(r, 9).number_format = "+0.000;-0.000;0.000"
|
||||
ws.cell(r, 10).number_format = "$#,##0"
|
||||
ws.cell(r, 11).number_format = "$#,##0"
|
||||
ws.cell(r, 8).fill = _expcolor(m["exp"])
|
||||
if m["breach"]:
|
||||
ws.cell(r, 12).fill = F_BAD
|
||||
r += 1
|
||||
|
||||
last = r - 1
|
||||
if last >= 2:
|
||||
ws.auto_filter.ref = f"A1:N{last}"
|
||||
ws.freeze_panes = "A2"
|
||||
for i, w in enumerate([7, 7, 11, 8, 14, 6, 8, 9, 10, 11, 11, 8, 7, 8], 1):
|
||||
ws.column_dimensions[get_column_letter(i)].width = w
|
||||
return len(rows)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not SRC.exists():
|
||||
print(f"EROARE: nu găsesc {SRC}")
|
||||
@@ -107,12 +737,30 @@ def main() -> int:
|
||||
build_config(wb) # Config la index 0 (cu formule)
|
||||
apply_config_inputs(wb, cfg_inputs)
|
||||
copy_trades_values(wb, wb_src["Trades"]) # Trades static la index 1 (ascuns)
|
||||
build_dashboard(wb) # Dashboard la index 2 — formule + charturi
|
||||
wb.active = wb.sheetnames.index("Dashboard")
|
||||
|
||||
build_dashboard(wb) # Dashboard la index 2 — formule
|
||||
wb_src.close()
|
||||
|
||||
# Ferestre — auto-scan (limitele prop din Config, cu fallback la default).
|
||||
acct = cfg_inputs.get(9) or 50000.0
|
||||
daily = acct * (cfg_inputs.get(12) or 4.0) / 100.0
|
||||
maxl = acct * (cfg_inputs.get(14) or 7.0) / 100.0
|
||||
T = load_trades(SRC)
|
||||
nscan = 0
|
||||
if T:
|
||||
build_ferestre(wb, T, acct, daily, maxl)
|
||||
nscan = build_scan_table(wb, T, acct, daily, maxl)
|
||||
else:
|
||||
print("ATENȚIE: niciun trade cu outcome în cache — sar peste sheet-urile de analiză.")
|
||||
|
||||
# Ordine logică: rezumat (Ferestre) → grila completă → datele chart-ului.
|
||||
order = ["Config", "Trades", "Dashboard", "Ferestre",
|
||||
"Toate ferestrele", "date_grafic"]
|
||||
wb._sheets.sort(key=lambda s: order.index(s.title) if s.title in order else 99)
|
||||
|
||||
wb.active = wb.sheetnames.index("Dashboard")
|
||||
wb.save(OUT)
|
||||
print(f"Scris {OUT}")
|
||||
print(f"Scris {OUT} ({len(T)} tranzacții, {nscan} rânduri scan, "
|
||||
f"sheet-uri: {wb.sheetnames})")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user