diff --git a/CLAUDE.md b/CLAUDE.md index 791af13..8297030 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,6 +57,20 @@ 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ă 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). + +**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). La date noi, caută valori TP/SL ≥1 sau TP0>TP1>TP2 inversate și confirmă cu Marius înainte de a corecta. + +**Findings curente (330 trade-uri, ian–mai 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), **B** 19:45–21:45 prima (cea mai robustă pe toate validările), **W** 19:15–22:15 prima (volum/bani max raportat la timp; +30 min până la 22:45 aduc doar ~+$61). Filtrele direcționale (buy) par mai bune dar pică out-of-sample. Edge subțire → ipoteze de confirmat live. + ## Reference docs - `strategie_M2D.md` — M2D setup rules (color-coded dot bands on TF mare/mic, SL/TP placement, session filters). diff --git a/data/Ferestre_v2.xlsx b/data/Ferestre_v2.xlsx new file mode 100644 index 0000000..24e2732 Binary files /dev/null and b/data/Ferestre_v2.xlsx differ diff --git a/scripts/generate_ferestre_v2.py b/scripts/generate_ferestre_v2.py new file mode 100644 index 0000000..32dcaeb --- /dev/null +++ b/scripts/generate_ferestre_v2.py @@ -0,0 +1,458 @@ +# -*- 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 + +SRC = "D:/PROIECTE/atm-backtesting/data/backtest.xlsx" +OUT = "D:/PROIECTE/atm-backtesting/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 [ + "1. Edge real dar MODEST. Pe toate cele 330 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()