# -*- 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()