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

@@ -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 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:
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

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