"""Generator pentru data/backtest.xlsx. 5 strategii de management comparate side-by-side pe semnale blackbox: - TP0 only : 100% close la TP0 - TP1 only : 100% OCO la SL/TP1 - TP2 only : 100% OCO la SL/TP2 - Hybrid + BE : 50% TP0 + mut SL la BE + 50% TP1 (recomandat de trader) - Hybrid no BE : 50% TP0 + 50% TP1, fără BE (control pentru a izola valoarea BE-ului) Rulare: pip install openpyxl python scripts/generate_template.py """ from __future__ import annotations import shutil from datetime import date, datetime, time, timedelta from pathlib import Path from openpyxl import Workbook from openpyxl.formatting.rule import CellIsRule from openpyxl.styles import Alignment, Border, Font, PatternFill, Side from openpyxl.utils import get_column_letter from openpyxl.worksheet.datavalidation import DataValidation OUTPUT = Path(__file__).resolve().parent.parent / "data" / "backtest.xlsx" MAX_ROWS = 500 # rânduri pre-completate cu formule în sheet-ul Trades # --------------------------------------------------------------------------- # Styles # --------------------------------------------------------------------------- HEADER_FILL = PatternFill("solid", fgColor="1F3864") HEADER_FONT = Font(name="Calibri", size=11, bold=True, color="FFFFFF") INPUT_FILL = PatternFill("solid", fgColor="FFF8E1") DERIVED_FILL = PatternFill("solid", fgColor="E8F1FA") HIDDEN_FILL = PatternFill("solid", fgColor="F0F0F0") TITLE_FONT = Font(name="Calibri", size=16, bold=True, color="1F3864") SUBTITLE_FONT = Font(name="Calibri", size=12, bold=True, color="1F3864") THIN = Side(border_style="thin", color="BFBFBF") BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN) CENTER = Alignment(horizontal="center", vertical="center") LEFT = Alignment(horizontal="left", vertical="center") RIGHT = Alignment(horizontal="right", vertical="center") # --------------------------------------------------------------------------- # Lists # --------------------------------------------------------------------------- STRATEGIES = ["M2D", "EMA cross", "Order block", "Liquidity sweep", "Custom"] SESSIONS = ["A1", "A2", "A3", "B", "C", "D", "Other"] INDICATORS = ["DIA", "US30", "SPY", "QQQ", "ES", "NQ"] TIMEFRAMES = ["1min", "3min", "15min"] DIRECTIONS = ["Buy", "Sell"] OUTCOMES = ["SL", "TP0", "TP1", "TP2"] # Cele 5 strategii de management (sufix folosit în numele coloanelor) + label friendly STRAT_KEYS = ["tp0only", "tp1only", "tp2only", "hybrid_be", "hybrid_nobe"] STRAT_LABELS = { "tp0only": "TP0 only", "tp1only": "TP1 only", "tp2only": "TP2 only", "hybrid_be": "Hybrid + BE", "hybrid_nobe": "Hybrid no BE", } # --------------------------------------------------------------------------- # Trades sheet — schema # --------------------------------------------------------------------------- def _candidate_windows() -> list[tuple[str, time, time]]: """Ferestre suprapuse intre 16:30 si 23:00, evaluate pe ora Romaniei.""" base = datetime(2000, 1, 1, 16, 30) last_start = datetime(2000, 1, 1, 22, 0) hard_ends = [ datetime(2000, 1, 1, 22, 45), datetime(2000, 1, 1, 23, 0), ] durations = [timedelta(minutes=m) for m in (60, 90, 120, 180)] seen: set[tuple[time, time]] = set() windows: list[tuple[str, time, time]] = [] start = base while start <= last_start: ends = [start + d for d in durations] ends += [end for end in hard_ends if end - start >= timedelta(minutes=60)] for end in ends: if end > hard_ends[-1]: continue key = (start.time(), end.time()) if key in seen: continue seen.add(key) windows.append((f"{start:%H:%M}-{end:%H:%M}", start.time(), end.time())) start += timedelta(minutes=30) return windows TRADABLE_WINDOWS = _candidate_windows() INPUT_HEADERS = [ "#", "Data", "Ora RO", "Zi", "Sesiune", "Strategie", "Indicator", "TF", "Direcție", "SL %", "TP0 %", "TP1 %", "TP2 %", "Outcome", "Notes", ] DERIVED_HEADERS = ( ["SL $", "SL $ Prop"] + [f"R_{s}" for s in STRAT_KEYS] + [f"$_{s}" for s in STRAT_KEYS] + [f"Bal_{s}" for s in STRAT_KEYS] + [f"$Prop_{s}" for s in STRAT_KEYS] + [f"BalProp_{s}" for s in STRAT_KEYS] ) PRIMA_HELPERS = [f"PrimaWin_{idx}" for idx in range(len(TRADABLE_WINDOWS))] HELPER_HEADERS = ( [f"Win_{s}" for s in STRAT_KEYS] + [f"Peak_{s}" for s in STRAT_KEYS] + [f"DD_{s}" for s in STRAT_KEYS] + [f"DailyPL_{s}" for s in STRAT_KEYS] + [f"PeakProp_{s}" for s in STRAT_KEYS] + [f"DDProp_{s}" for s in STRAT_KEYS] + PRIMA_HELPERS ) TRADES_HEADERS = INPUT_HEADERS + DERIVED_HEADERS + HELPER_HEADERS # Mapă nume → literă coloană Excel COL = {name: get_column_letter(i + 1) for i, name in enumerate(TRADES_HEADERS)} # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _col_to_int(letter: str) -> int: n = 0 for ch in letter: n = n * 26 + (ord(ch) - ord("A") + 1) return n # --------------------------------------------------------------------------- # Config sheet # --------------------------------------------------------------------------- def build_config(wb: Workbook) -> None: ws = wb.create_sheet("Config", 0) ws.sheet_view.showGridLines = False ws["A1"] = "Config — editează doar celulele galbene" ws["A1"].font = TITLE_FONT ws.merge_cells("A1:C1") ws["A3"] = "Setting" ws["B3"] = "Value" ws["C3"] = "Note" for c in ("A3", "B3", "C3"): ws[c].font = HEADER_FONT ws[c].fill = HEADER_FILL ws[c].alignment = CENTER ws["A4"] = "Account Size Start ($)" ws["B4"] = 10000 ws["C4"] = "Balanța inițială pentru calcule $ și HWM (model abstract)" ws["A5"] = "Risk reper (%)" ws["B5"] = 1.0 ws["C5"] = "Reper opțional; $_* se calculează din SL% × Account Size Start" ws["A6"] = "Risk reper ($)" ws["B6"] = "=B4*B5/100" ws["C6"] = "Auto — informativ; nu este folosit în formulele $_*" for r in (4, 5): ws.cell(row=r, column=2).fill = INPUT_FILL ws.cell(row=r, column=2).border = BORDER ws["B6"].fill = DERIVED_FILL ws["B6"].border = BORDER ws["B4"].number_format = "$#,##0" ws["B5"].number_format = '0.0"%"' ws["B6"].number_format = "$#,##0.00" # ---- Bloc Cont real TradeLocker / Prop Firm ---- # Lanț de calcul transparent: Lot size -> $/punct -> $ risc la 1% mișcare preț. # Toate coloanele $ din Trades folosesc B10*B11 (= $ risc la 1% preț pe poziția ta). # Scalare la 100k: schimbi B9 (Account) și B10 (Lot size) — restul se recalculează. ws["A8"] = "Cont real TradeLocker (prop firm)" ws["A8"].font = SUBTITLE_FONT ws.merge_cells("A8:C8") ws["A9"] = "Account Start ($)" ws["B9"] = 50000 ws["C9"] = "Balanța contului. Schimbă în 100000 pentru un cont de 100k." ws["A10"] = "Lot size per semnal (US30)" ws["B10"] = 0.08 ws["C10"] = ( "Câte loturi US30 intri pe fiecare semnal (TradeLocker). " "Pe 100k cu același risc %: 0.16." ) ws["A11"] = "$ per 1% preț, la 1.0 lot" ws["B11"] = "=B24*B25/100" ws["C11"] = ( "Auto = ($/punct la 1 lot, B24) × (Preț reper, B25 / 100). " "Înmulțit cu Lot size (B10) dă riscul real per 1% — vezi B27." ) ws["A12"] = "Daily Loss Limit (%)" ws["B12"] = 4.0 ws["C12"] = "Limită zilnică prop firm; depășire = cont mort" ws["A13"] = "Daily Loss Limit ($)" ws["B13"] = "=B9*B12/100" ws["C13"] = "Auto — derivat din Account Start (B9) și %" ws["A14"] = "Max Loss Limit (%)" ws["B14"] = 7.0 ws["C14"] = "Limită totală pe cont; depășire = cont mort" ws["A15"] = "Max Loss Limit ($)" ws["B15"] = "=B9*B14/100" ws["C15"] = "Auto — derivat din Account Start (B9) și %" # Escape hatch performanță: activează/dezactivează filtrul Prima per Indicator ws["A17"] = "Activează filtru Prima" ws["B17"] = "DA" ws["C17"] = ( "DA = adaugi rândurile Prima în window grid. " "NU = doar Toate (workbook mai rapid)." ) ws["B17"].fill = INPUT_FILL ws["B17"].border = BORDER ws["B17"].alignment = CENTER dv_prima = DataValidation( type="list", formula1='"DA,NU"', allow_blank=False, ) dv_prima.add("B17") ws.add_data_validation(dv_prima) # ---- Calibrare $/punct dintr-un ordin reper + derivate informative ---- # Helper universal: dintr-un singur ordin TradeLocker (orice indicator) derivi # $/punct, apoi $/1% preț. Pentru alt indicator schimbi cele 4 inputuri reper # (B19-B22) + Preț reper curent (B25). Restul se recalculează singur. ws["A18"] = "— Calibrare $/punct (dintr-un ordin reper TradeLocker) —" ws["A18"].font = Font(name="Calibri", size=10, italic=True, bold=True, color="1F3864") ws.merge_cells("A18:C18") ws["A19"] = "Preț entry (ordin reper)" ws["B19"] = 50680.15 ws["C19"] = "Prețul de intrare afișat în ticketul TradeLocker (orice ordin reper)." ws["A20"] = "Preț SL (ordin reper)" ws["B20"] = 50618.24 ws["C20"] = "Prețul SL din același ticket." ws["A21"] = "$ risc afișat (ordin reper)" ws["B21"] = 495.24 ws["C21"] = "Cifra $ pe care TradeLocker o arată la SL pentru ordinul reper." ws["A22"] = "Lot size (ordin reper)" ws["B22"] = 0.08 ws["C22"] = "Câte loturi avea ordinul reper (poate diferi de lotul tău curent, B10)." ws["A23"] = "↳ Distanță reper (puncte)" ws["B23"] = "=ABS(B19-B20)" ws["C23"] = "Auto = |entry − SL|. Aici ⇒ 61.91 puncte." ws["A24"] = "↳ $ per punct la 1.0 lot" ws["B24"] = "=IF(OR(B23=0,B22=0),0,B21/B23/B22)" ws["C24"] = ( "Auto = $risc ÷ distanță ÷ lot reper. Pe US30 ⇒ $100/punct la 1 lot. " "Asta intră în B11. Pe alt indicator se recalibrează singur." ) ws["A25"] = "Preț reper (curent)" ws["B25"] = 50700 ws["C25"] = ( "Nivelul curent al indicatorului; convertește % mișcare preț în puncte/$. " "Pentru alt indicator pui prețul lui." ) ws["A26"] = "↳ $ per punct (la lotul tău)" ws["B26"] = "=B10*B24" ws["C26"] = "Auto = Lot size (B10) × $/punct la 1 lot (B24). La 0.08 loturi ⇒ $8/punct." ws["A27"] = "↳ $ risc la 1% mișcare preț" ws["B27"] = "=B10*B11" ws["C27"] = ( "Auto — INIMA calculului. $ pe trade = R × SL% × această valoare. " "La 0.08 loturi US30 ⇒ ~$4,056." ) for r in (9, 10, 12, 14, 19, 20, 21, 22, 25): # inputuri galbene ws.cell(row=r, column=2).fill = INPUT_FILL ws.cell(row=r, column=2).border = BORDER for r in (11, 13, 15, 23, 24, 26, 27): # derived blue ws.cell(row=r, column=2).fill = DERIVED_FILL ws.cell(row=r, column=2).border = BORDER ws["B9"].number_format = "$#,##0" ws["B10"].number_format = "0.00" ws["B11"].number_format = "$#,##0" ws["B12"].number_format = '0.0"%"' ws["B13"].number_format = "$#,##0" ws["B14"].number_format = '0.0"%"' ws["B15"].number_format = "$#,##0" ws["B19"].number_format = "#,##0.00" ws["B20"].number_format = "#,##0.00" ws["B21"].number_format = '"$"#,##0.00' ws["B22"].number_format = "0.00" ws["B23"].number_format = "#,##0.00" ws["B24"].number_format = '"$"#,##0.00' ws["B25"].number_format = "#,##0" ws["B26"].number_format = '"$"#,##0.00' ws["B27"].number_format = '"$"#,##0.00' # ---- Utilitar: calcul preț SL pentru TradeLocker (pas cu pas) ---- # Forward helper: din SL% (TradeStation) + preț intrare (TradeLocker) + direcție # → prețul exact la care pui SL în TradeLocker. Celule intermediare vizibile. ws["A29"] = "— Utilitar: ce prețuri SL/TP pun în TradeLocker —" ws["A29"].font = Font(name="Calibri", size=10, italic=True, bold=True, color="1F3864") ws.merge_cells("A29:C29") ws["A30"] = "SL % (din TradeStation)" ws["B30"] = 0.08 ws["C30"] = "Procentul SL al semnalului (% mișcare de preț), ex: 0.08." ws["A31"] = "Preț intrare (TradeLocker)" ws["B31"] = 50649.50 ws["C31"] = "Prețul tău de intrare US30 (nu prețul reper de la B25)." ws["A32"] = "Direcție" ws["B32"] = "Sell" ws["C32"] = "Sell ⇒ SL DEASUPRA intrării. Buy ⇒ SL DEDESUBT." ws["A33"] = "↳ Distanță SL (puncte)" ws["B33"] = "=B31*B30/100" ws["C33"] = "Auto = Preț intrare × SL% / 100. Ex: 50649.50 × 0.08/100 ⇒ ~40.5 puncte." ws["A34"] = "↳ PREȚ SL de setat în TradeLocker" ws["B34"] = '=IF(B32="Sell",B31+B33,B31-B33)' ws["C34"] = "Sell: intrare + distanță. Buy: intrare − distanță. ASTA pui în TradeLocker." ws["A35"] = "↳ $ risc la acest SL" ws["B35"] = "=B33*B26" ws["C35"] = ( "Auto = Distanță (puncte) × $/punct la lotul tău (B26). " "Verificare: ar trebui să se potrivească cu cifra $ din ticketul TradeLocker." ) for r in (30, 31, 32): # inputuri galbene ws.cell(row=r, column=2).fill = INPUT_FILL ws.cell(row=r, column=2).border = BORDER for r in (33, 34, 35): # derived blue ws.cell(row=r, column=2).fill = DERIVED_FILL ws.cell(row=r, column=2).border = BORDER # Output principal evidențiat ws["A34"].font = Font(name="Calibri", size=11, bold=True, color="1F3864") ws["B34"].font = Font(name="Calibri", size=11, bold=True) ws["B32"].alignment = CENTER ws["B30"].number_format = '0.000"%"' ws["B31"].number_format = "#,##0.00" ws["B33"].number_format = "#,##0.00" ws["B34"].number_format = "#,##0.00" ws["B35"].number_format = '"$"#,##0.00' # --- TP0/TP1/TP2: aceeași logică, dar în direcția profitului (opus SL) --- ws["A36"] = "TP0 % (din TradeStation)" ws["B36"] = 0.03 ws["C36"] = "Procentul TP0 al semnalului (% mișcare de preț)." ws["A37"] = "TP1 % (din TradeStation)" ws["B37"] = 0.06 ws["C37"] = "Procentul TP1 al semnalului." ws["A38"] = "TP2 % (din TradeStation)" ws["B38"] = 0.08 ws["C38"] = "Procentul TP2 al semnalului." ws["A39"] = "↳ PREȚ TP0 de setat" ws["B39"] = '=IF(B32="Sell",B31-B31*B36/100,B31+B31*B36/100)' ws["C39"] = ( "Distanță = intrare × TP0% / 100. Sell: intrare − distanță. " "Buy: intrare + distanță (TP-ul e opus SL-ului)." ) ws["A40"] = "↳ PREȚ TP1 de setat" ws["B40"] = '=IF(B32="Sell",B31-B31*B37/100,B31+B31*B37/100)' ws["C40"] = "La fel, cu TP1%." ws["A41"] = "↳ PREȚ TP2 de setat" ws["B41"] = '=IF(B32="Sell",B31-B31*B38/100,B31+B31*B38/100)' ws["C41"] = "La fel, cu TP2%." for r in (36, 37, 38): # inputuri galbene ws.cell(row=r, column=2).fill = INPUT_FILL ws.cell(row=r, column=2).border = BORDER ws.cell(row=r, column=2).number_format = '0.000"%"' for r in (39, 40, 41): # output evidențiat (albastru, bold) ws.cell(row=r, column=2).fill = DERIVED_FILL ws.cell(row=r, column=2).border = BORDER ws.cell(row=r, column=2).number_format = "#,##0.00" ws.cell(row=r, column=2).font = Font(name="Calibri", size=11, bold=True) ws.cell(row=r, column=1).font = Font( name="Calibri", size=11, bold=True, color="1F3864" ) dv_dir = DataValidation(type="list", formula1='"Buy,Sell"', allow_blank=False) dv_dir.add("B32") ws.add_data_validation(dv_dir) # Liste dropdown — coloanele E–J (6 coloane) list_columns = [ ("Strategii", STRATEGIES), ("Sesiuni (auto)", SESSIONS), ("Indicatori", INDICATORS), ("TF", TIMEFRAMES), ("Direcție", DIRECTIONS), ("Outcome", OUTCOMES), ] for col_idx, (label, values) in enumerate(list_columns, start=5): cell = ws.cell(row=3, column=col_idx, value=label) cell.font = HEADER_FONT cell.fill = HEADER_FILL cell.alignment = CENTER for row_idx, v in enumerate(values, start=4): c = ws.cell(row=row_idx, column=col_idx, value=v) c.alignment = CENTER widths = { "A": 30, "B": 14, "C": 40, "D": 2, "E": 14, "F": 14, "G": 13, "H": 10, "I": 10, "J": 12, } for col, w in widths.items(): ws.column_dimensions[col].width = w # --------------------------------------------------------------------------- # Formula builders pentru Trades sheet # --------------------------------------------------------------------------- def _f_day(r: int) -> str: d = f'{COL["Data"]}{r}' return ( f'=IF({d}="","",' f'CHOOSE(WEEKDAY({d},2),"Lu","Ma","Mi","Jo","Vi","Sa","Du"))' ) def _f_session(r: int) -> str: """Derivă Sesiunea M2D din Data + Ora RO.""" d = f'{COL["Data"]}{r}' t = f'{COL["Ora RO"]}{r}' wd = f"WEEKDAY({d},2)" mid_week = f"AND({wd}>=2,{wd}<=4)" return ( f'=IF(OR({d}="",{t}=""),"",' f"IF(OR({wd}=1,{wd}=5),\"D\"," f'IF(AND({t}>=TIME(15,30,0),{t}=TIME(16,35,0),{t}=TIME(17,0,0),{t}=TIME(18,0,0),{t}=TIME(22,0,0),{t} str: """1 dacă trade-ul este prima cronologic pe (Data, Indicator) ÎN fereastra cu indexul win_idx. Guard suplimentar pe Config!B17 (escape hatch performanță): dacă utilizatorul setează "NU", toate PrimaWin_* devin 0 instant, fără recalcularea COUNTIFS. Outcome inclus în COUNTIFS ca să nu blocheze rândurile parțial completate. """ _, start_t, end_t = TRADABLE_WINDOWS[win_idx] start_s = f"TIME({start_t.hour},{start_t.minute},0)" end_s = f"TIME({end_t.hour},{end_t.minute},0)" d = f'{COL["Data"]}{r}' ind = f'{COL["Indicator"]}{r}' t = f'{COL["Ora RO"]}{r}' o = f'{COL["Outcome"]}{r}' data_rng = _range("Data") ind_rng = _range("Indicator") ora_rng = _range("Ora RO") outcome_rng = _range("Outcome") return ( f'=IF(Config!$B$17<>"DA",0,' f'IF(OR({d}="",{t}="",{ind}="",{o}=""),0,' f'IF(AND({t}>={start_s},{t}<{end_s}),' f'IF(COUNTIFS({data_rng},{d},{ind_rng},{ind},' f'{ora_rng},">="&{start_s},{ora_rng},"<"&{end_s},' f'{ora_rng},"<"&{t},' f'{outcome_rng},"<>"' f')=0,1,0),0)))' ) def _f_r_tp0only(r: int) -> str: o = f'{COL["Outcome"]}{r}' sl = f'{COL["SL %"]}{r}' tp0 = f'{COL["TP0 %"]}{r}' return f'=IF({o}="","",IF({o}="SL",-1,{tp0}/{sl}))' def _f_r_tp1only(r: int) -> str: o = f'{COL["Outcome"]}{r}' sl = f'{COL["SL %"]}{r}' tp1 = f'{COL["TP1 %"]}{r}' return ( f'=IF({o}="","",' f'IF(OR({o}="SL",{o}="TP0"),-1,{tp1}/{sl}))' ) def _f_r_tp2only(r: int) -> str: o = f'{COL["Outcome"]}{r}' sl = f'{COL["SL %"]}{r}' tp2 = f'{COL["TP2 %"]}{r}' return f'=IF({o}="","",IF({o}="TP2",{tp2}/{sl},-1))' def _f_r_hybrid_be(r: int) -> str: o = f'{COL["Outcome"]}{r}' sl = f'{COL["SL %"]}{r}' tp0 = f'{COL["TP0 %"]}{r}' tp1 = f'{COL["TP1 %"]}{r}' return ( f'=IF({o}="","",' f'IF({o}="SL",-1,' f'IF({o}="TP0",0.5*{tp0}/{sl},' f'0.5*({tp0}+{tp1})/{sl})))' ) def _f_r_hybrid_nobe(r: int) -> str: o = f'{COL["Outcome"]}{r}' sl = f'{COL["SL %"]}{r}' tp0 = f'{COL["TP0 %"]}{r}' tp1 = f'{COL["TP1 %"]}{r}' return ( f'=IF({o}="","",' f'IF({o}="SL",-1,' f'IF({o}="TP0",0.5*{tp0}/{sl}-0.5,' f'0.5*({tp0}+{tp1})/{sl})))' ) R_FN: dict[str, callable] = { "tp0only": _f_r_tp0only, "tp1only": _f_r_tp1only, "tp2only": _f_r_tp2only, "hybrid_be": _f_r_hybrid_be, "hybrid_nobe": _f_r_hybrid_nobe, } def _f_dollar(r: int, r_col: str) -> str: """$ P&L per trade = R × SL% × Contracte × $/1% per contract (TradeLocker real).""" rc = f"{COL[r_col]}{r}" sl = f"{COL['SL %']}{r}" return f'=IF({rc}="","",{rc}*{sl}*Config!$B$10*Config!$B$11)' def _f_sl_dollar(r: int) -> str: """SL $ = SL% × Contracte × $/1% per contract.""" sl = f"{COL['SL %']}{r}" return f'=IF({sl}="","",{sl}*Config!$B$10*Config!$B$11)' def _f_sl_dollar_prop(r: int) -> str: """SL $ pe contul de prop — același cont real, formula identică cu SL $.""" sl = f"{COL['SL %']}{r}" return f'=IF({sl}="","",{sl}*Config!$B$10*Config!$B$11)' def _f_balance(r: int, dollar_col: str) -> str: dc = COL[dollar_col] return f'=IF({dc}{r}="","",Config!$B$4 + SUM(${dc}$2:{dc}{r}))' def _f_win(r: int, r_col: str) -> str: rc = f"{COL[r_col]}{r}" return f'=IF({rc}="","",IF({rc}>0,1,0))' def _f_peak(r: int, balance_col: str, peak_col: str) -> str: bc = COL[balance_col] pc = COL[peak_col] if r == 2: return f'=IF({bc}{r}="","",{bc}{r})' return ( f'=IF({bc}{r}="","",' f'IF({pc}{r-1}="",{bc}{r},MAX({pc}{r-1},{bc}{r})))' ) def _f_drawdown(r: int, peak_col: str, balance_col: str) -> str: pc = f"{COL[peak_col]}{r}" bc = f"{COL[balance_col]}{r}" return f'=IF({bc}="","",{pc}-{bc})' def _f_dollar_prop(r: int, r_col: str) -> str: """$ P&L pe contul de prop — același calcul ca _f_dollar (cont real TradeLocker). Diferența între cont abstract și prop e doar balanța de start; $-ul per trade e identic pentru că reflectă realitatea contractelor tranzacționate. """ rc = f"{COL[r_col]}{r}" sl = f"{COL['SL %']}{r}" return f'=IF({rc}="","",{rc}*{sl}*Config!$B$10*Config!$B$11)' def _f_balance_prop(r: int, dollar_col: str) -> str: dc = COL[dollar_col] return f'=IF({dc}{r}="","",Config!$B$9 + SUM(${dc}$2:{dc}{r}))' def _f_daily_pl(r: int, dollar_col: str) -> str: """Cumul P&L pe ziua curentă (până la rândul r inclusiv).""" dc = COL[dollar_col] d_col = COL["Data"] d = f"{d_col}{r}" return ( f'=IF(OR({dc}{r}="",{d}=""),"",' f'SUMIFS(${dc}$2:{dc}{r},${d_col}$2:{d_col}{r},{d}))' ) # --------------------------------------------------------------------------- # Trades sheet # --------------------------------------------------------------------------- def build_trades(wb: Workbook) -> None: ws = wb.create_sheet("Trades", 1) ws.sheet_view.showGridLines = False ws.freeze_panes = "B2" # Headers for col_idx, header in enumerate(TRADES_HEADERS, start=1): cell = ws.cell(row=1, column=col_idx, value=header) cell.font = HEADER_FONT cell.fill = HEADER_FILL cell.alignment = CENTER cell.border = BORDER # Formule pe toate rândurile pre-pregătite for r in range(2, MAX_ROWS + 2): ws.cell(row=r, column=1, value="=ROW()-1") ws[f'{COL["Zi"]}{r}'] = _f_day(r) ws[f'{COL["Sesiune"]}{r}'] = _f_session(r) ws[f'{COL["SL $"]}{r}'] = _f_sl_dollar(r) ws[f'{COL["SL $ Prop"]}{r}'] = _f_sl_dollar_prop(r) for strat in STRAT_KEYS: ws[f'{COL[f"R_{strat}"]}{r}'] = R_FN[strat](r) ws[f'{COL[f"$_{strat}"]}{r}'] = _f_dollar(r, f"R_{strat}") ws[f'{COL[f"Bal_{strat}"]}{r}'] = _f_balance(r, f"$_{strat}") ws[f'{COL[f"Win_{strat}"]}{r}'] = _f_win(r, f"R_{strat}") ws[f'{COL[f"Peak_{strat}"]}{r}'] = _f_peak( r, f"Bal_{strat}", f"Peak_{strat}" ) ws[f'{COL[f"DD_{strat}"]}{r}'] = _f_drawdown( r, f"Peak_{strat}", f"Bal_{strat}" ) # Prop firm tracking — paralel cu modelul abstract ws[f'{COL[f"$Prop_{strat}"]}{r}'] = _f_dollar_prop(r, f"R_{strat}") ws[f'{COL[f"BalProp_{strat}"]}{r}'] = _f_balance_prop(r, f"$Prop_{strat}") ws[f'{COL[f"DailyPL_{strat}"]}{r}'] = _f_daily_pl(r, f"$Prop_{strat}") ws[f'{COL[f"PeakProp_{strat}"]}{r}'] = _f_peak( r, f"BalProp_{strat}", f"PeakProp_{strat}" ) ws[f'{COL[f"DDProp_{strat}"]}{r}'] = _f_drawdown( r, f"PeakProp_{strat}", f"BalProp_{strat}" ) # Coloanele PrimaWin_ — 1 dacă trade-ul e prima per (Data, Indicator) în fereastră for win_idx in range(len(TRADABLE_WINDOWS)): ws[f'{COL[f"PrimaWin_{win_idx}"]}{r}'] = _f_prima_in_window(r, win_idx) # Sample rows SAMPLE_ROWS = [ # (data, ora, strat, ind, tf, dir, sl, tp0, tp1, tp2, outcome, notes) (date(2026, 5, 13), time(17, 33), "M2D", "DIA", "1min", "Sell", 0.30, 0.10, 0.15, 0.30, "TP1", "Prima DIA în 16:30-18:00"), (date(2026, 5, 13), time(17, 50), "M2D", "DIA", "1min", "Buy", 0.25, 0.10, 0.15, 0.25, "SL", "DIA a doua oară — NU Prima în 16:30-18:00, dar Prima în 17:30-19:00"), (date(2026, 5, 13), time(17, 34), "M2D", "SPY", "1min", "Sell", 0.20, 0.08, 0.12, 0.20, "TP1", "SPY — indicator diferit, Prima independent"), (date(2026, 5, 13), time(17, 40), "M2D", "DIA", "1min", "Sell", 0.20, 0.08, 0.12, 0.20, "", "Outcome gol — test D1: NU blochează Prima pentru row 2/3"), (date(2026, 5, 14), time(22, 15), "M2D", "DIA", "1min", "Sell", 0.30, 0.10, 0.15, 0.30, "TP0", "Zi diferită — Prima reset per (Data, Indicator, Fereastră)"), ] for offset, sample in enumerate(SAMPLE_ROWS): r = 2 + offset data_v, ora, strat_v, ind, tf, dirn, sl, tp0, tp1, tp2, outcome, notes = sample ws[f"B{r}"] = data_v ws[f"C{r}"] = ora ws[f'{COL["Strategie"]}{r}'] = strat_v ws[f'{COL["Indicator"]}{r}'] = ind ws[f'{COL["TF"]}{r}'] = tf ws[f'{COL["Direcție"]}{r}'] = dirn ws[f'{COL["SL %"]}{r}'] = sl ws[f'{COL["TP0 %"]}{r}'] = tp0 ws[f'{COL["TP1 %"]}{r}'] = tp1 ws[f'{COL["TP2 %"]}{r}'] = tp2 ws[f'{COL["Outcome"]}{r}'] = outcome ws[f'{COL["Notes"]}{r}'] = notes # Number formats for col_name in ("SL %", "TP0 %", "TP1 %", "TP2 %"): for r in range(2, MAX_ROWS + 2): ws[f"{COL[col_name]}{r}"].number_format = '0.000"%"' for col_name in ("SL $", "SL $ Prop"): for r in range(2, MAX_ROWS + 2): ws[f"{COL[col_name]}{r}"].number_format = '"$"#,##0.00' for strat in STRAT_KEYS: for r in range(2, MAX_ROWS + 2): ws[f"{COL[f'R_{strat}']}{r}"].number_format = "+0.000;-0.000;0.000" for prefix in ( "$_", "Bal_", "Peak_", "DD_", "$Prop_", "BalProp_", "DailyPL_", "PeakProp_", "DDProp_", ): ws[f"{COL[f'{prefix}{strat}']}{r}"].number_format = '"$"#,##0.00' for r in range(2, MAX_ROWS + 2): ws[f"B{r}"].number_format = "yyyy-mm-dd" # Coloring input_letters = { COL[n] for n in ( "Data", "Ora RO", "Strategie", "Indicator", "TF", "Direcție", "SL %", "TP0 %", "TP1 %", "TP2 %", "Outcome", "Notes", ) } derived_letters = {COL["Zi"], COL["Sesiune"], COL["SL $"], COL["SL $ Prop"]} for strat in STRAT_KEYS: for prefix in ("R_", "$_", "Bal_", "$Prop_", "BalProp_"): derived_letters.add(COL[f"{prefix}{strat}"]) helper_letters = set() for strat in STRAT_KEYS: for prefix in ("Win_", "Peak_", "DD_", "DailyPL_", "PeakProp_", "DDProp_"): helper_letters.add(COL[f"{prefix}{strat}"]) for win_idx in range(len(TRADABLE_WINDOWS)): helper_letters.add(COL[f"PrimaWin_{win_idx}"]) for r in range(2, MAX_ROWS + 2): for cl in input_letters: ws[f"{cl}{r}"].fill = INPUT_FILL for cl in derived_letters: ws[f"{cl}{r}"].fill = DERIVED_FILL for cl in helper_letters: ws[f"{cl}{r}"].fill = HIDDEN_FILL # Column widths widths = { "A": 5, "B": 12, "C": 9, "D": 5, "E": 9, "F": 12, "G": 11, "H": 8, "I": 9, "J": 9, "K": 9, "L": 9, "M": 9, "N": 11, "O": 28, } for col, w in widths.items(): ws.column_dimensions[col].width = w for col_name in ("SL $", "SL $ Prop"): ws.column_dimensions[COL[col_name]].width = 12 # Derived + helper: width 11 for strat in STRAT_KEYS: for prefix in ( "R_", "$_", "Bal_", "Win_", "Peak_", "DD_", "$Prop_", "BalProp_", "DailyPL_", "PeakProp_", "DDProp_", ): ws.column_dimensions[COL[f"{prefix}{strat}"]].width = 11 # Ascund helper-ele prop firm într-un outline collapsible for strat in STRAT_KEYS: for prefix in ("DailyPL_", "PeakProp_", "DDProp_"): cl = COL[f"{prefix}{strat}"] ws.column_dimensions[cl].outlineLevel = 1 ws.column_dimensions[cl].hidden = True # Helper-ele PrimaWin_ — ~40 coloane la sfârșit, ascunse în outline for win_idx in range(len(TRADABLE_WINDOWS)): cl = COL[f"PrimaWin_{win_idx}"] ws.column_dimensions[cl].width = 3 ws.column_dimensions[cl].outlineLevel = 1 ws.column_dimensions[cl].hidden = True # Data validation dropdowns def _add_dv(col_name: str, source: str) -> None: cl = COL[col_name] dv = DataValidation( type="list", formula1=source, allow_blank=True, showErrorMessage=True, ) dv.error = "Valoare invalidă — folosește dropdown-ul." dv.errorTitle = "Input invalid" dv.add(f"{cl}2:{cl}{MAX_ROWS + 1}") ws.add_data_validation(dv) # Config columns: E=Strategii, F=Sesiuni, G=Indicatori, H=TF, I=Direcție, J=Outcome _add_dv("Strategie", "=Config!$E$4:$E$8") _add_dv("Indicator", "=Config!$G$4:$G$9") _add_dv("TF", "=Config!$H$4:$H$6") _add_dv("Direcție", "=Config!$I$4:$I$5") _add_dv("Outcome", "=Config!$J$4:$J$7") # Conditional formatting pe coloanele R (5 strategii) green_fill = PatternFill("solid", fgColor="C6EFCE") red_fill = PatternFill("solid", fgColor="FFC7CE") grey_fill = PatternFill("solid", fgColor="D9D9D9") for strat in STRAT_KEYS: cl = COL[f"R_{strat}"] rng = f"{cl}2:{cl}{MAX_ROWS + 1}" ws.conditional_formatting.add( rng, CellIsRule(operator="greaterThan", formula=["0"], fill=green_fill) ) ws.conditional_formatting.add( rng, CellIsRule(operator="lessThan", formula=["0"], fill=red_fill) ) ws.conditional_formatting.add( rng, CellIsRule(operator="equal", formula=["0"], fill=grey_fill) ) # --------------------------------------------------------------------------- # Dashboard sheet # --------------------------------------------------------------------------- def _range(col_name: str) -> str: cl = COL[col_name] return f"Trades!${cl}$2:${cl}${MAX_ROWS + 1}" METRIC_HINTS: dict[str, str] = { "Trades Placed": ( "Câte trade-uri ai logat în total.\n" "Cu cât N e mai mare, cu atât celelalte metrici sunt mai de încredere.\n" "Exemplu: la N=10 Win Ratio e zgomot pur, la N=40 începe să aibă semnal, la N=100 e solid." ), "Wins": ( "Câte trade-uri s-au închis pe plus (R > 0).\n" "Singur nu spune nimic — privește-l raportat la total (vezi Win Ratio mai jos)." ), "Win Ratio": ( "Procentul de trade-uri câștigătoare. WR = 60% înseamnă 6 wins din 10 trade-uri.\n" "Singur NU spune dacă strategia e profitabilă — citește-l împreună cu R:R de pe rândul următor." ), "Average Win ($)": ( "Câștigul mediu pe trade-urile pozitive.\n" "Comparat cu Average Loss îți spune cât de mari sunt câștigurile vs pierderile.\n" "Exemplu: 4 wins de $50 și 2 wins de $80 — Average Win = $60." ), "Average Loss ($)": ( "Pierderea medie pe trade-urile negative (cifra apare cu minus).\n" "În dolari reali, −1R depinde de SL%: pierdere ≈ SL% × $ risc la 1% preț (Config B27).\n" "Dacă e mult mai mare decât riscul calculat din SL, ai SL-uri sărite (slippage, gap-uri)." ), "Best Trade ($)": ( "Cel mai mare câștig individual.\n" "Dacă majoritatea profitului total vine dintr-un singur trade outlier, edge-ul e fragil — " "elimini acel trade și strategia devine pierzătoare." ), "Worst Trade ($)": ( "Cea mai mare pierdere individuală.\n" "Ar trebui să fie aproximativ egală cu −1R calculat din SL% × $ risc la 1% preț (Config B27).\n" "La 0.08 loturi US30: SL=0.30% × ~$4,056 ≈ −$1,217. Dacă e mult mai mare, ai slippage/gap." ), "Profit Factor": ( "Total bani câștigați împărțit la total bani pierduți (în valoare absolută).\n" "Sub 1.0 = pierzi pe ansamblu. Peste 1.5 = solid. Peste 2.0 = câștigi de 2× cât pierzi.\n" "Exemplu: 4 wins de $50 (= $200) + 6 losses de $30 (= $180) — PF = 200÷180 = 1.11, profitabil marginal." ), "Risk:Reward": ( "De câte ori e mai mare câștigul mediu decât pierderea medie.\n" "R:R = 2 înseamnă: când câștigi, câștigi $2; când pierzi, pierzi $1.\n" "Cu R:R mare poți avea Win Ratio mic și tot să faci bani." ), "Expectancy (R)": ( "Cât câștigi în medie pe UN trade, exprimat în R.\n" "+0.30R = câștigi 0.30 × riscul mediu al trade-urilor.\n" "−0.10R = pierzi 0.10 × riscul mediu al trade-urilor.\n" "Pragul de GO LIVE: +0.20R sau mai mult." ), "Expectancy ($)": ( "Aceeași expectancy convertită în dolari, folosind SL% × $ risc la 1% preț (Config B27).\n" "Util ca să vezi cât câștigi în medie pe trade în bani reali (TradeLocker), nu doar în R." ), "Cumulative P&L ($)": ( "Suma profitului și pierderii pe toate trade-urile logate.\n" "E ce-ai avea în plus (sau minus) față de balanța de start din Config." ), "HWM Balance ($)": ( "Highest Watermark — cea mai mare balanță atinsă vreodată în jurnal.\n" "Punct de referință pentru calculul drawdown-ului." ), "Max Drawdown ($)": ( "Cea mai mare cădere ($) din vârf la fundul ulterior al balanței. Măsoară durerea psihologică maximă.\n" "Exemplu: ai urcat la $11,500, ai coborât la $9,800 — DD = $1,700, adică 17% din peak.\n" "Un drawdown mare la backtest e foarte greu de tolerat în live cu bani reali — așteaptă-te să renunți." ), # ---- Prop firm metrics ---- "Account Prop Start ($)": ( "Capitalul de start al contului de prop firm (default $50,000).\n" "Editabil în Config B9." ), "$ risc la 1% preț ($)": ( "Câți $ riști pe poziția ta reală la 1% mișcare de preț (Config B27).\n" "Lanț: Lot size (B10) × $/punct la 1 lot (B24) = $/punct; × Preț reper (B25)/100 = $ per 1%.\n" "$/punct la 1 lot (B24) e calibrat dintr-un ordin reper TradeLocker — merge pe orice indicator.\n" "La 0.08 loturi US30 ⇒ ~$4,056. Pe 100k cu lot 0.16 se dublează (~$8,112)." ), "Cumulative P&L Prop ($)": ( "Profitul total al contului de prop pe traseul logat.\n" "Reflectă $ real (SL% × Contracte × $/1% per contract), nu un procent abstract din cont.\n" "Adunat peste $50,000 dă balanța finală reală." ), "Final Balance Prop ($)": ( "Balanța finală a contului de prop = Account Start (Config B9) + Cumulative P&L Prop.\n" "Compar-o cu pragul de stop-out: Account Start − Max Loss Limit $ (Config B15)." ), "Worst Daily Loss ($)": ( "Cea mai proastă pierdere cumulativă într-o zi calendaristică.\n" "Dacă e mai mică decât −Daily Loss Limit $ (Config B13), ai depășit limita zilnică — cont mort.\n" "Atenție: un singur breach = pierdere cont, indiferent dacă ai recuperat ulterior." ), "Daily Limit Status": ( "PASS dacă nicio zi nu a depășit Daily Loss Limit $ (Config B13, auto din Account × %).\n" "FAIL = strategia ar fi pierdut contul prin daily breach pe traseul logat." ), "Max Account Drawdown ($)": ( "Cea mai mare cădere de la peak pe contul de prop.\n" "Dacă > Max Loss Limit $ (Config B15, auto din Account × %), ai depășit limita — cont mort." ), "Max Loss Status": ( "PASS dacă Max Account Drawdown ≤ Max Loss Limit $ (Config B15).\n" "FAIL = strategia ar fi pierdut contul prin drawdown cumulativ." ), "Overall Prop Status": ( "CONFORM = strategia ar fi supraviețuit pe contul de prop pe traseul logat.\n" "CONT PIERDUT = cel puțin o breach (daily sau max) — strategia nu e viabilă pe acest cont prop." ), } def build_dashboard(wb: Workbook) -> None: ws = wb.create_sheet("Dashboard", 2) ws.sheet_view.showGridLines = False ws["A1"] = "Backtest Dashboard" ws["A1"].font = TITLE_FONT ws.merge_cells("A1:G1") ws["A2"] = ( "Comparație 5 strategii management — pe aceleași semnale blackbox" ) ws["A2"].font = Font(name="Calibri", size=10, italic=True, color="595959") ws.merge_cells("A2:G2") # Row 4: headers (5 columns B-F pentru strategii + G pentru "Cum citesc") ws["A4"] = "Metric" strat_cols = {} # strat_key → column letter (B/C/D/E/F) for i, strat in enumerate(STRAT_KEYS): letter = get_column_letter(2 + i) strat_cols[strat] = letter ws[f"{letter}4"] = STRAT_LABELS[strat] ws["G4"] = "Cum citesc" for letter in ["A"] + list(strat_cols.values()) + ["G"]: c = ws[f"{letter}4"] c.font = HEADER_FONT c.fill = HEADER_FILL c.alignment = CENTER c.border = BORDER # Ranges per strategie R = {s: _range(f"R_{s}") for s in STRAT_KEYS} D = {s: _range(f"$_{s}") for s in STRAT_KEYS} W = {s: _range(f"Win_{s}") for s in STRAT_KEYS} BAL = {s: _range(f"Bal_{s}") for s in STRAT_KEYS} DD = {s: _range(f"DD_{s}") for s in STRAT_KEYS} OUTCOME_RANGE = _range("Outcome") # Metric rows — fiecare metric e un dict cu per-strategy formula + format metrics: list[tuple[str, callable, str]] = [ # (label, fn(strat_key) -> formula, number_format) ("Trades Placed", lambda s: f'=COUNTA({OUTCOME_RANGE})', "0"), ("Wins", lambda s: f'=COUNTIF({W[s]},1)', "0"), ("Average Win ($)", lambda s: f'=IFERROR(AVERAGEIF({D[s]},">0"),0)', '"$"#,##0.00'), ("Average Loss ($)", lambda s: f'=IFERROR(AVERAGEIF({D[s]},"<0"),0)', '"$"#,##0.00'), ("Best Trade ($)", lambda s: f'=IFERROR(MAX({D[s]}),0)', '"$"#,##0.00'), ("Worst Trade ($)", lambda s: f'=IFERROR(MIN({D[s]}),0)', '"$"#,##0.00'), ("Profit Factor", lambda s: f'=IFERROR(SUMIF({D[s]},">0")/ABS(SUMIF({D[s]},"<0")),0)', "0.00"), # Win Ratio: depends on Wins + Trades Placed — handled after metrics list (placeholder) ("Win Ratio", lambda s: None, "0.0%"), # Risk:Reward — placeholder; bazat pe rândurile Avg Win/Loss ("Risk:Reward", lambda s: None, "0.00"), ("Expectancy (R)", lambda s: f'=IFERROR(AVERAGE({R[s]}),0)', "+0.000;-0.000;0.000"), ("Expectancy ($)", lambda s: f'=IFERROR(AVERAGE({D[s]}),0)', '"$"#,##0.00'), ("Cumulative P&L ($)", lambda s: f'=SUM({D[s]})', '"$"#,##0.00'), # HWM — placeholder cu ref la Trades Placed (row 5) ("HWM Balance ($)", lambda s: None, '"$"#,##0.00'), ("Max Drawdown ($)", lambda s: f'=IFERROR(MAX({DD[s]}),0)', '"$"#,##0.00'), ] # Determine row indexes pentru formule speciale (depind de poziție) label_to_row = {label: 5 + idx for idx, (label, _, _) in enumerate(metrics)} trades_row = label_to_row["Trades Placed"] wins_row = label_to_row["Wins"] avg_win_row = label_to_row["Average Win ($)"] avg_loss_row = label_to_row["Average Loss ($)"] for idx, (label, fn, fmt) in enumerate(metrics): r = 5 + idx ws[f"A{r}"] = label ws[f"A{r}"].font = Font(name="Calibri", size=11, bold=True) ws[f"A{r}"].border = BORDER ws[f"A{r}"].alignment = LEFT for strat in STRAT_KEYS: letter = strat_cols[strat] if label == "Win Ratio": formula = f"=IFERROR({letter}{wins_row}/{letter}{trades_row},0)" elif label == "Risk:Reward": formula = f"=IFERROR({letter}{avg_win_row}/ABS({letter}{avg_loss_row}),0)" elif label == "HWM Balance ($)": formula = ( f"=IF({letter}{trades_row}=0,Config!$B$4,MAX({BAL[strat]}))" ) else: formula = fn(strat) cell = ws[f"{letter}{r}"] cell.value = formula cell.number_format = fmt cell.fill = DERIVED_FILL cell.border = BORDER cell.alignment = RIGHT # Coloana G — interpretare narativă + exemplu numeric hint_cell = ws[f"G{r}"] hint_cell.value = METRIC_HINTS.get(label, "") hint_cell.font = Font(name="Calibri", size=10, color="595959") hint_cell.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True) hint_cell.border = BORDER # ---- FERESTRE CANDIDATE x STRATEGIE ---- # Tabel principal pentru alegerea ferestrei tradabile. Drawdown-ul este # calculat cu helper-e ascunse pe fereastra curenta, nu din DD global. # DASH_WIN_COL: mapă nume → literă, ca să eliminăm hardcoding-ul de litere. DASH_WIN_HEADERS = [ "Fereastra", "Start", "End", "Filtru", "Strategie", "N", "Wins", "WR", "Expectancy R", "Expectancy $", "Profit Factor", "Cum P&L $", "Max Drawdown $", "Worst Daily Loss Prop $", "Max Drawdown Prop $", "Daily Breach", "Max Loss Breach", "Status Prop", "Status Edge", "Score_Toate", "Score_Prima", ] DASH_WIN_COL = { name: get_column_letter(i + 1) for i, name in enumerate(DASH_WIN_HEADERS) } last_dash_col = DASH_WIN_COL[DASH_WIN_HEADERS[-1]] window_title_row = 5 + len(metrics) + 2 ws[f"A{window_title_row}"] = "FERESTRE CANDIDATE x STRATEGIE" ws[f"A{window_title_row}"].font = SUBTITLE_FONT ws.merge_cells(f"A{window_title_row}:{last_dash_col}{window_title_row}") window_header_row = window_title_row + 1 for col_idx, header in enumerate(DASH_WIN_HEADERS, start=1): c = ws.cell(row=window_header_row, column=col_idx, value=header) c.font = HEADER_FONT c.fill = HEADER_FILL c.alignment = CENTER c.border = BORDER TIME_RANGE = _range("Ora RO") PROP_D = {s: _range(f"$Prop_{s}") for s in STRAT_KEYS} helper_start_col = 27 # AA, ascuns. def _emit_window_helpers( visible_row: int, strat: str, combo_idx: int, win_idx: int, use_prima: bool = False, ) -> dict[str, str]: base_col = helper_start_col + combo_idx * 7 helper_names = ["Cum", "Peak", "DD", "DailyProp", "CumProp", "PeakProp", "DDProp"] cols = {name: get_column_letter(base_col + idx) for idx, name in enumerate(helper_names)} for idx, name in enumerate(helper_names): col = get_column_letter(base_col + idx) ws[f"{col}1"] = f"{name}_{visible_row}" ws.column_dimensions[col].hidden = True ws.column_dimensions[col].width = 3 start_cell = f"$B${visible_row}" end_cell = f"$C${visible_row}" dollar_col = COL[f"$_{strat}"] prop_col = COL[f"$Prop_{strat}"] time_col = COL["Ora RO"] date_col = COL["Data"] outcome_col = COL["Outcome"] prima_col = COL[f"PrimaWin_{win_idx}"] if use_prima else None for helper_row, trade_row in enumerate(range(2, MAX_ROWS + 2), start=2): in_window_base = ( f'AND(Trades!${outcome_col}{trade_row}<>"",' f"Trades!${time_col}{trade_row}>={start_cell}," f"Trades!${time_col}{trade_row}<{end_cell})" ) if use_prima: in_window = ( f"AND({in_window_base}," f"Trades!${prima_col}{trade_row}=1)" ) else: in_window = in_window_base dollar = f"Trades!${dollar_col}{trade_row}" prop = f"Trades!${prop_col}{trade_row}" if helper_row == 2: ws[f"{cols['Cum']}{helper_row}"] = f"=IF({in_window},{dollar},0)" ws[f"{cols['Peak']}{helper_row}"] = f"=MAX(0,{cols['Cum']}{helper_row})" ws[f"{cols['CumProp']}{helper_row}"] = f"=IF({in_window},{prop},0)" ws[f"{cols['PeakProp']}{helper_row}"] = f"=MAX(0,{cols['CumProp']}{helper_row})" else: prev = helper_row - 1 ws[f"{cols['Cum']}{helper_row}"] = ( f"={cols['Cum']}{prev}+IF({in_window},{dollar},0)" ) ws[f"{cols['Peak']}{helper_row}"] = ( f"=MAX({cols['Peak']}{prev},{cols['Cum']}{helper_row})" ) ws[f"{cols['CumProp']}{helper_row}"] = ( f"={cols['CumProp']}{prev}+IF({in_window},{prop},0)" ) ws[f"{cols['PeakProp']}{helper_row}"] = ( f"=MAX({cols['PeakProp']}{prev},{cols['CumProp']}{helper_row})" ) ws[f"{cols['DD']}{helper_row}"] = ( f"={cols['Peak']}{helper_row}-{cols['Cum']}{helper_row}" ) ws[f"{cols['DDProp']}{helper_row}"] = ( f"={cols['PeakProp']}{helper_row}-{cols['CumProp']}{helper_row}" ) ws[f"{cols['DailyProp']}{helper_row}"] = ( f'=IF({in_window},' f'SUMIFS(Trades!${prop_col}$2:Trades!${prop_col}{trade_row},' f'Trades!${date_col}$2:Trades!${date_col}{trade_row},Trades!${date_col}{trade_row},' f'Trades!${time_col}$2:Trades!${time_col}{trade_row},">="&{start_cell},' f'Trades!${time_col}$2:Trades!${time_col}{trade_row},"<"&{end_cell}),' f'"")' ) return cols pass_fill = PatternFill("solid", fgColor="C6EFCE") fail_fill = PatternFill("solid", fgColor="FFC7CE") warn_fill = PatternFill("solid", fgColor="FFEB9C") combo_rows: list[int] = [] combo_idx = 0 row = window_header_row + 1 # Pre-compute column letters from DASH_WIN_COL for legibility A_ = DASH_WIN_COL["Fereastra"] B_ = DASH_WIN_COL["Start"] C_ = DASH_WIN_COL["End"] D_ = DASH_WIN_COL["Filtru"] E_ = DASH_WIN_COL["Strategie"] F_ = DASH_WIN_COL["N"] G_ = DASH_WIN_COL["Wins"] H_ = DASH_WIN_COL["WR"] I_ = DASH_WIN_COL["Expectancy R"] J_ = DASH_WIN_COL["Expectancy $"] K_ = DASH_WIN_COL["Profit Factor"] L_ = DASH_WIN_COL["Cum P&L $"] M_ = DASH_WIN_COL["Max Drawdown $"] N_ = DASH_WIN_COL["Worst Daily Loss Prop $"] O_ = DASH_WIN_COL["Max Drawdown Prop $"] P_ = DASH_WIN_COL["Daily Breach"] Q_ = DASH_WIN_COL["Max Loss Breach"] R_LET = DASH_WIN_COL["Status Prop"] S_LET = DASH_WIN_COL["Status Edge"] T_LET = DASH_WIN_COL["Score_Toate"] U_LET = DASH_WIN_COL["Score_Prima"] FILTERS = [("Toate", False), ("Prima", True)] for win_idx, (label, start_time, end_time) in enumerate(TRADABLE_WINDOWS): for strat in STRAT_KEYS: for filter_label, use_prima in FILTERS: helper_cols = _emit_window_helpers( row, strat, combo_idx, win_idx=win_idx, use_prima=use_prima, ) prima_range = ( _range(f"PrimaWin_{win_idx}") if use_prima else None ) extra = f",{prima_range},1" if use_prima else "" ws[f"{A_}{row}"] = label ws[f"{B_}{row}"] = start_time ws[f"{C_}{row}"] = end_time ws[f"{D_}{row}"] = filter_label ws[f"{E_}{row}"] = STRAT_LABELS[strat] ws[f"{F_}{row}"] = ( f'=COUNTIFS({OUTCOME_RANGE},"<>",' f'{TIME_RANGE},">="&{B_}{row},{TIME_RANGE},"<"&{C_}{row}{extra})' ) ws[f"{G_}{row}"] = ( f'=COUNTIFS({W[strat]},1,{OUTCOME_RANGE},"<>",' f'{TIME_RANGE},">="&{B_}{row},{TIME_RANGE},"<"&{C_}{row}{extra})' ) ws[f"{H_}{row}"] = f"=IFERROR({G_}{row}/{F_}{row},0)" ws[f"{I_}{row}"] = ( f'=IFERROR(AVERAGEIFS({R[strat]},{OUTCOME_RANGE},"<>",' f'{TIME_RANGE},">="&{B_}{row},{TIME_RANGE},"<"&{C_}{row}{extra}),0)' ) ws[f"{J_}{row}"] = ( f'=IFERROR(AVERAGEIFS({D[strat]},{OUTCOME_RANGE},"<>",' f'{TIME_RANGE},">="&{B_}{row},{TIME_RANGE},"<"&{C_}{row}{extra}),0)' ) ws[f"{K_}{row}"] = ( f'=IFERROR(SUMIFS({D[strat]},{D[strat]},">0",{OUTCOME_RANGE},"<>",' f'{TIME_RANGE},">="&{B_}{row},{TIME_RANGE},"<"&{C_}{row}{extra})/' f'ABS(SUMIFS({D[strat]},{D[strat]},"<0",{OUTCOME_RANGE},"<>",' f'{TIME_RANGE},">="&{B_}{row},{TIME_RANGE},"<"&{C_}{row}{extra})),0)' ) ws[f"{L_}{row}"] = ( f'=SUMIFS({D[strat]},{OUTCOME_RANGE},"<>",' f'{TIME_RANGE},">="&{B_}{row},{TIME_RANGE},"<"&{C_}{row}{extra})' ) ws[f"{M_}{row}"] = ( f'=IFERROR(MAX({helper_cols["DD"]}2:{helper_cols["DD"]}{MAX_ROWS + 1}),0)' ) ws[f"{N_}{row}"] = ( f'=IFERROR(MIN({helper_cols["DailyProp"]}2:' f'{helper_cols["DailyProp"]}{MAX_ROWS + 1}),0)' ) ws[f"{O_}{row}"] = ( f'=IFERROR(MAX({helper_cols["DDProp"]}2:' f'{helper_cols["DDProp"]}{MAX_ROWS + 1}),0)' ) ws[f"{P_}{row}"] = f'=IF({N_}{row}<-Config!$B$13,"DA","NU")' ws[f"{Q_}{row}"] = f'=IF({O_}{row}>Config!$B$15,"DA","NU")' ws[f"{R_LET}{row}"] = ( f'=IF(OR({P_}{row}="DA",{Q_}{row}="DA"),' f'"CONT PIERDUT","CONFORM")' ) ws[f"{S_LET}{row}"] = ( f'=IF({F_}{row}<1,"",' f'IF(OR({P_}{row}="DA",{Q_}{row}="DA"),"BREACH",' f'IF(AND({F_}{row}>=40,{H_}{row}>=55%,{I_}{row}>=0.2),' f'"CANDIDAT","PRE-CANDIDAT")))' ) ws[f"{T_LET}{row}"] = ( f'=IF(OR({F_}{row}<1,{D_}{row}<>"Toate",' f'{P_}{row}="DA",{Q_}{row}="DA"),-1E+12,' f'{I_}{row}*20000+MIN({F_}{row},150)*100+' f'{K_}{row}*1500+{L_}{row}-{M_}{row}-{O_}{row}/10)' ) ws[f"{U_LET}{row}"] = ( f'=IF(OR({F_}{row}<1,{D_}{row}<>"Prima",' f'{P_}{row}="DA",{Q_}{row}="DA"),-1E+12,' f'{I_}{row}*20000+MIN({F_}{row},150)*100+' f'{K_}{row}*1500+{L_}{row}-{M_}{row}-{O_}{row}/10)' ) combo_rows.append(row) combo_idx += 1 row += 1 # Indici 1-based ai coloanelor centrate center_idx = { DASH_WIN_HEADERS.index(name) + 1 for name in ("Fereastra", "Filtru", "Strategie", "Daily Breach", "Max Loss Breach", "Status Prop", "Status Edge") } # Primele 5 coloane (Fereastra, Start, End, Filtru, Strategie) nu primesc fill derivat no_fill_idx = set(range(1, 6)) for r in combo_rows: for c in range(1, len(DASH_WIN_HEADERS) + 1): cell = ws.cell(row=r, column=c) cell.border = BORDER cell.alignment = CENTER if c in center_idx else RIGHT if c not in no_fill_idx: cell.fill = DERIVED_FILL ws[f"{B_}{r}"].number_format = "hh:mm" ws[f"{C_}{r}"].number_format = "hh:mm" ws[f"{F_}{r}"].number_format = "0" ws[f"{G_}{r}"].number_format = "0" ws[f"{H_}{r}"].number_format = "0.0%" ws[f"{I_}{r}"].number_format = "+0.000;-0.000;0.000" for c_letter in (J_, L_, M_, N_, O_): ws[f"{c_letter}{r}"].number_format = '"$"#,##0.00' ws[f"{K_}{r}"].number_format = "0.00" # Score_Toate și Score_Prima ascunse ws.column_dimensions[T_LET].hidden = True ws.column_dimensions[U_LET].hidden = True if combo_rows: first_combo = combo_rows[0] last_combo = combo_rows[-1] status_rng = f"{R_LET}{first_combo}:{S_LET}{last_combo}" ws.conditional_formatting.add( status_rng, CellIsRule(operator="equal", formula=['"CONFORM"'], fill=pass_fill) ) ws.conditional_formatting.add( status_rng, CellIsRule(operator="equal", formula=['"CANDIDAT"'], fill=pass_fill) ) ws.conditional_formatting.add( status_rng, CellIsRule(operator="equal", formula=['"CONT PIERDUT"'], fill=fail_fill) ) ws.conditional_formatting.add( status_rng, CellIsRule(operator="equal", formula=['"BREACH"'], fill=fail_fill) ) ws.conditional_formatting.add( status_rng, CellIsRule(operator="equal", formula=['"PRE-CANDIDAT"'], fill=warn_fill) ) # ---- TOP CANDIDATE — două sub-secțiuni: Toate + Prima ---- # Score_Toate (col T) și Score_Prima (col U) sunt populate condițional pe Filtru; # LARGE pe coloana corespunzătoare extrage doar rândurile relevante. top_headers = [ "#", "Fereastra", "Filtru", "Strategie", "N", "WR", "Expectancy R", "Profit Factor", "Cum P&L $", "Max DD Prop $", "Status Edge", ] # Mapă coloană target din TOP → header din DASH_WIN_COL top_source_names = [ "Fereastra", "Filtru", "Strategie", "N", "WR", "Expectancy R", "Profit Factor", "Cum P&L $", "Max Drawdown Prop $", "Status Edge", ] top_target_letters = ["B", "C", "D", "E", "F", "G", "H", "I", "J", "K"] def _emit_top_subsection(start_row: int, title: str, note: str, score_col: str, count: int = 20) -> int: ws[f"A{start_row}"] = title ws[f"A{start_row}"].font = SUBTITLE_FONT ws.merge_cells(f"A{start_row}:K{start_row}") note_row = start_row + 1 ws[f"A{note_row}"] = note ws[f"A{note_row}"].font = Font( name="Calibri", size=10, italic=True, color="595959" ) ws[f"A{note_row}"].alignment = Alignment( horizontal="left", vertical="center", wrap_text=True ) ws.merge_cells(f"A{note_row}:K{note_row}") header_row = note_row + 1 for col_idx, header in enumerate(top_headers, start=1): c = ws.cell(row=header_row, column=col_idx, value=header) c.font = HEADER_FONT c.fill = HEADER_FILL c.alignment = CENTER c.border = BORDER for idx in range(1, count + 1): r = header_row + idx ws[f"A{r}"] = idx if combo_rows: rank_formula = ( f"LARGE(${score_col}${first_combo}:${score_col}${last_combo},{idx})" ) match_formula = ( f"MATCH({rank_formula}," f"${score_col}${first_combo}:${score_col}${last_combo},0)" ) for target, source_name in zip(top_target_letters, top_source_names): source = DASH_WIN_COL[source_name] ws[f"{target}{r}"] = ( f'=IFERROR(IF({rank_formula}<=-1E+11,"",' f'INDEX(${source}${first_combo}:${source}${last_combo},' f'{match_formula})),"")' ) for c in range(1, len(top_headers) + 1): cell = ws.cell(row=r, column=c) cell.border = BORDER cell.alignment = RIGHT if c not in (2, 3, 4, 11) else CENTER # Number formats — coloanele după shift cu +1 (Filtru e nou D): # E=N, F=WR, G=ExpR, H=PF, I=CumPL, J=MaxDDProp, K=StatusEdge ws[f"F{r}"].number_format = "0.0%" ws[f"G{r}"].number_format = "+0.000;-0.000;0.000" ws[f"H{r}"].number_format = "0.00" ws[f"I{r}"].number_format = '"$"#,##0.00' ws[f"J{r}"].number_format = '"$"#,##0.00' # CF pe Status Edge (col K) top_status_rng = f"K{header_row + 1}:K{header_row + count}" ws.conditional_formatting.add( top_status_rng, CellIsRule(operator="equal", formula=['"CANDIDAT"'], fill=pass_fill), ) ws.conditional_formatting.add( top_status_rng, CellIsRule(operator="equal", formula=['"PRE-CANDIDAT"'], fill=warn_fill), ) ws.conditional_formatting.add( top_status_rng, CellIsRule(operator="equal", formula=['"BREACH"'], fill=fail_fill), ) return header_row + count top_title_row = row + 2 after_top_toate = _emit_top_subsection( top_title_row, "TOP 20 FERESTRE — Toate trade-urile", ( "Top 20 după scor compus, calculat pe rândurile cu Filtru=Toate. " "EXCLUDE ferestrele cu Daily Breach=DA sau Max Loss Breach=DA (ar fi pierdut contul prop). " "Scor = ExpR×20000 + MIN(N,150)×100 + PF×1500 + CumP&L − MaxDD − MaxDDProp/10. " "Bonusul N (capat la 150) favorizează ferestrele cu sample mai mare, statistic mai fiabile. " "CANDIDAT = îndeplinește pragurile (N≥40, WR≥55%, ExpR≥0.2). " "PRE-CANDIDAT = N≥1 fără breach dar sub praguri." ), score_col=T_LET, ) after_top_prima = _emit_top_subsection( after_top_toate + 2, "TOP 20 FERESTRE — Prima per Indicator", ( "Top 20 după scor compus, calculat pe rândurile cu Filtru=Prima (doar primul " "trade pe (Data, Indicator) în fiecare fereastră). EXCLUDE ferestrele cu Daily " "Breach=DA sau Max Loss Breach=DA. Util pentru a vedea dacă filtrul Prima " "identifică ferestre mai eficiente decât Toate." ), score_col=U_LET, ) # Conditional formatting reutilizabil pentru celulele Cum $ bd_green = PatternFill("solid", fgColor="C6EFCE") bd_red = PatternFill("solid", fgColor="FFC7CE") # Helper pentru breakdown wide: rânduri = items, coloane = 5 strategii Cum $ + N total def _emit_breakdown_strats( start_row: int, title: str, first_col_label: str, items: list[str], item_range: str, ) -> int: # Layout: A=item, B..F=5 strategii (Cum $), G=N total last_col_idx = 1 + len(STRAT_KEYS) + 1 # A + 5 strategii + N last_letter = get_column_letter(last_col_idx) ws[f"A{start_row}"] = title ws[f"A{start_row}"].font = SUBTITLE_FONT ws.merge_cells(f"A{start_row}:{last_letter}{start_row}") headers = [first_col_label] + [STRAT_LABELS[s] for s in STRAT_KEYS] + ["N total"] for col_idx, h in enumerate(headers, start=1): c = ws.cell(row=start_row + 1, column=col_idx, value=h) c.font = HEADER_FONT c.fill = HEADER_FILL c.alignment = CENTER c.border = BORDER strat_letters = [get_column_letter(2 + i) for i in range(len(STRAT_KEYS))] n_letter = get_column_letter(last_col_idx) for i, item in enumerate(items): r = start_row + 2 + i ws[f"A{r}"] = item for idx, strat in enumerate(STRAT_KEYS): cl = strat_letters[idx] ws[f"{cl}{r}"] = f'=SUMIFS({D[strat]},{item_range},"{item}")' ws[f"{cl}{r}"].number_format = '"$"#,##0.00' ws[f"{n_letter}{r}"] = f'=COUNTIF({item_range},"{item}")' ws[f"{n_letter}{r}"].number_format = "0" for col_idx in range(1, last_col_idx + 1): cell = ws.cell(row=r, column=col_idx) cell.border = BORDER cell.alignment = LEFT if col_idx == 1 else RIGHT if 2 <= col_idx <= 1 + len(STRAT_KEYS): cell.fill = DERIVED_FILL # CF pe coloanele 5 strategii: verde >0, roșu <0 if items: first_data_row = start_row + 2 last_data_row = start_row + 1 + len(items) cf_rng = ( f"{strat_letters[0]}{first_data_row}:" f"{strat_letters[-1]}{last_data_row}" ) ws.conditional_formatting.add( cf_rng, CellIsRule(operator="greaterThan", formula=["0"], fill=bd_green) ) ws.conditional_formatting.add( cf_rng, CellIsRule(operator="lessThan", formula=["0"], fill=bd_red) ) return start_row + 1 + len(items) # Breakdowns — toate cele 5 strategii vizibile, Cum P&L $ per strategie start = after_top_prima + 2 after_strat = _emit_breakdown_strats( start + 2, "PER STRATEGIE — Cum P&L $ per strategie", "Strategie", STRATEGIES, _range("Strategie"), ) after_ind = _emit_breakdown_strats( after_strat + 2, "PER INDICATOR — Cum P&L $ per strategie", "Indicator", INDICATORS, _range("Indicator"), ) after_dir = _emit_breakdown_strats( after_ind + 2, "PER DIRECȚIE — Cum P&L $ per strategie", "Direcție", DIRECTIONS, _range("Direcție"), ) # ---- PROP FIRM COMPLIANCE ---- PROP_RANGES = { "dollar": {s: _range(f"$Prop_{s}") for s in STRAT_KEYS}, "balance": {s: _range(f"BalProp_{s}") for s in STRAT_KEYS}, "daily": {s: _range(f"DailyPL_{s}") for s in STRAT_KEYS}, "dd": {s: _range(f"DDProp_{s}") for s in STRAT_KEYS}, } prop_title_row = after_dir + 2 ws[f"A{prop_title_row}"] = "PROP FIRM COMPLIANCE" ws[f"A{prop_title_row}"].font = SUBTITLE_FONT ws.merge_cells(f"A{prop_title_row}:G{prop_title_row}") # Header pentru tabel prop_header_row = prop_title_row + 1 ws[f"A{prop_header_row}"] = "Metric" for strat in STRAT_KEYS: ws[f"{strat_cols[strat]}{prop_header_row}"] = STRAT_LABELS[strat] ws[f"G{prop_header_row}"] = "Cum citesc" for letter in ["A"] + list(strat_cols.values()) + ["G"]: c = ws[f"{letter}{prop_header_row}"] c.font = HEADER_FONT c.fill = HEADER_FILL c.alignment = CENTER c.border = BORDER # Definițiile rândurilor — (label, formula_fn(strat), number_format) fail_pass_fmt = "@" prop_metrics: list[tuple[str, callable, str]] = [ ( "Account Prop Start ($)", lambda s: "=Config!$B$9", '"$"#,##0', ), ( "$ risc la 1% preț ($)", lambda s: "=Config!$B$27", '"$"#,##0.00', ), ( "Cumulative P&L Prop ($)", lambda s: f"=SUM({PROP_RANGES['dollar'][s]})", '"$"#,##0.00', ), ( "Final Balance Prop ($)", lambda s: f"=Config!$B$9+SUM({PROP_RANGES['dollar'][s]})", '"$"#,##0.00', ), ( "Worst Daily Loss ($)", lambda s: f"=IFERROR(MIN({PROP_RANGES['daily'][s]}),0)", '"$"#,##0.00', ), # placeholder pentru Daily Status — depinde de Worst Daily de mai sus ("Daily Limit Status", lambda s: None, fail_pass_fmt), ( "Max Account Drawdown ($)", lambda s: f"=IFERROR(MAX({PROP_RANGES['dd'][s]}),0)", '"$"#,##0.00', ), # placeholder pentru Max Status — depinde de Max DD de mai sus ("Max Loss Status", lambda s: None, fail_pass_fmt), # placeholder pentru Overall — depinde de cele două statuses ("Overall Prop Status", lambda s: None, fail_pass_fmt), ] prop_label_to_row = { label: prop_header_row + 1 + idx for idx, (label, _, _) in enumerate(prop_metrics) } worst_daily_row = prop_label_to_row["Worst Daily Loss ($)"] daily_status_row = prop_label_to_row["Daily Limit Status"] max_dd_row = prop_label_to_row["Max Account Drawdown ($)"] max_status_row = prop_label_to_row["Max Loss Status"] for idx, (label, fn, fmt) in enumerate(prop_metrics): r = prop_header_row + 1 + idx ws[f"A{r}"] = label ws[f"A{r}"].font = Font(name="Calibri", size=11, bold=True) ws[f"A{r}"].border = BORDER ws[f"A{r}"].alignment = LEFT for strat in STRAT_KEYS: letter = strat_cols[strat] if label == "Daily Limit Status": formula = ( f'=IF({letter}{worst_daily_row}<-Config!$B$13,"FAIL","PASS")' ) elif label == "Max Loss Status": formula = ( f'=IF({letter}{max_dd_row}>Config!$B$15,"FAIL","PASS")' ) elif label == "Overall Prop Status": formula = ( f'=IF(OR({letter}{daily_status_row}="FAIL",' f'{letter}{max_status_row}="FAIL"),' f'"CONT PIERDUT","CONFORM")' ) else: formula = fn(strat) cell = ws[f"{letter}{r}"] cell.value = formula cell.number_format = fmt cell.fill = DERIVED_FILL cell.border = BORDER cell.alignment = RIGHT if fmt != fail_pass_fmt else CENTER # Hint în coloana G hint_cell = ws[f"G{r}"] hint_cell.value = METRIC_HINTS.get(label, "") hint_cell.font = Font(name="Calibri", size=10, color="595959") hint_cell.alignment = Alignment( horizontal="left", vertical="top", wrap_text=True ) hint_cell.border = BORDER # Conditional formatting pe status rows — verde PASS/CONFORM, roșu FAIL/CONT PIERDUT pass_fill = PatternFill("solid", fgColor="C6EFCE") fail_fill = PatternFill("solid", fgColor="FFC7CE") for status_row in (daily_status_row, max_status_row): rng = ( f"{strat_cols[STRAT_KEYS[0]]}{status_row}:" f"{strat_cols[STRAT_KEYS[-1]]}{status_row}" ) ws.conditional_formatting.add( rng, CellIsRule(operator="equal", formula=['"PASS"'], fill=pass_fill) ) ws.conditional_formatting.add( rng, CellIsRule(operator="equal", formula=['"FAIL"'], fill=fail_fill) ) overall_row = prop_label_to_row["Overall Prop Status"] overall_rng = ( f"{strat_cols[STRAT_KEYS[0]]}{overall_row}:" f"{strat_cols[STRAT_KEYS[-1]]}{overall_row}" ) ws.conditional_formatting.add( overall_rng, CellIsRule(operator="equal", formula=['"CONFORM"'], fill=pass_fill), ) ws.conditional_formatting.add( overall_rng, CellIsRule( operator="equal", formula=['"CONT PIERDUT"'], fill=fail_fill ), ) # Înălțime rânduri prop (cu hint multi-line) for r in range(prop_header_row + 1, prop_header_row + 1 + len(prop_metrics)): ws.row_dimensions[r].height = 60 # ---- PROP FIRM COMPLIANCE per FEREASTRĂ × STRATEGIE ---- # Reshape compliance: rânduri = combo (fereastră × strategie × filtru), # coloane = metrici compliance. Datele referențiate prin DASH_WIN_COL. if combo_rows: win_prop_title_row = prop_header_row + 1 + len(prop_metrics) + 2 ws[f"A{win_prop_title_row}"] = "PROP FIRM COMPLIANCE — per FEREASTRĂ × STRATEGIE" ws[f"A{win_prop_title_row}"].font = SUBTITLE_FONT ws.merge_cells(f"A{win_prop_title_row}:H{win_prop_title_row}") win_prop_note_row = win_prop_title_row + 1 ws[f"A{win_prop_note_row}"] = ( "Defalcat pe fiecare combinație de fereastră tradabilă × strategie management × filtru. " "CONFORM = ar fi supraviețuit pe contul de prop pe acel slot." ) ws[f"A{win_prop_note_row}"].font = Font(name="Calibri", size=10, italic=True, color="595959") ws[f"A{win_prop_note_row}"].alignment = Alignment(horizontal="left", vertical="center", wrap_text=True) ws.merge_cells(f"A{win_prop_note_row}:H{win_prop_note_row}") win_prop_header_row = win_prop_note_row + 1 win_prop_headers = [ "Fereastra", "Filtru", "Strategie", "Worst Daily Prop $", "Max DD Prop $", "Daily Breach", "Max Breach", "Overall Prop", ] for col_idx, h in enumerate(win_prop_headers, start=1): c = ws.cell(row=win_prop_header_row, column=col_idx, value=h) c.font = HEADER_FONT c.fill = HEADER_FILL c.alignment = CENTER c.border = BORDER # source cols din FERESTRE CANDIDATE (via DASH_WIN_COL) source_names = [ "Fereastra", "Filtru", "Strategie", "Worst Daily Loss Prop $", "Max Drawdown Prop $", "Daily Breach", "Max Loss Breach", "Status Prop", ] source_cols = [DASH_WIN_COL[name] for name in source_names] for offset, combo_row in enumerate(combo_rows, start=1): r = win_prop_header_row + offset for col_idx, source in enumerate(source_cols, start=1): target = get_column_letter(col_idx) ws[f"{target}{r}"] = f"={source}{combo_row}" cell = ws[f"{target}{r}"] cell.border = BORDER cell.fill = DERIVED_FILL cell.alignment = CENTER if col_idx in (1, 2, 3, 6, 7, 8) else RIGHT ws[f"D{r}"].number_format = '"$"#,##0.00' ws[f"E{r}"].number_format = '"$"#,##0.00' # CF pe Overall Prop (col H) și pe Daily/Max Breach (cols F, G) win_prop_first = win_prop_header_row + 1 win_prop_last = win_prop_header_row + len(combo_rows) overall_rng_win = f"H{win_prop_first}:H{win_prop_last}" ws.conditional_formatting.add( overall_rng_win, CellIsRule(operator="equal", formula=['"CONFORM"'], fill=pass_fill) ) ws.conditional_formatting.add( overall_rng_win, CellIsRule(operator="equal", formula=['"CONT PIERDUT"'], fill=fail_fill) ) breach_rng_win = f"F{win_prop_first}:G{win_prop_last}" ws.conditional_formatting.add( breach_rng_win, CellIsRule(operator="equal", formula=['"DA"'], fill=fail_fill) ) ws.conditional_formatting.add( breach_rng_win, CellIsRule(operator="equal", formula=['"NU"'], fill=pass_fill) ) # Column widths — aliniate cu DASH_WIN_COL (A=Fereastra ... U=Score_Prima) widths = { DASH_WIN_COL["Fereastra"]: 18, DASH_WIN_COL["Start"]: 10, DASH_WIN_COL["End"]: 18, DASH_WIN_COL["Filtru"]: 10, DASH_WIN_COL["Strategie"]: 16, DASH_WIN_COL["N"]: 8, DASH_WIN_COL["Wins"]: 8, DASH_WIN_COL["WR"]: 10, DASH_WIN_COL["Expectancy R"]: 13, DASH_WIN_COL["Expectancy $"]: 13, DASH_WIN_COL["Profit Factor"]: 12, DASH_WIN_COL["Cum P&L $"]: 13, DASH_WIN_COL["Max Drawdown $"]: 15, DASH_WIN_COL["Worst Daily Loss Prop $"]: 20, DASH_WIN_COL["Max Drawdown Prop $"]: 18, DASH_WIN_COL["Daily Breach"]: 13, DASH_WIN_COL["Max Loss Breach"]: 14, DASH_WIN_COL["Status Prop"]: 15, DASH_WIN_COL["Status Edge"]: 13, DASH_WIN_COL["Score_Toate"]: 8, DASH_WIN_COL["Score_Prima"]: 8, } for col, w in widths.items(): ws.column_dimensions[col].width = w # Row height pentru rândurile cu hint (cu wrap) — explicații multi-line for r in range(5, 5 + len(metrics)): ws.row_dimensions[r].height = 75 # Notă: graficele de echitate au fost eliminate (nu sunt folosite). Dashboard-ul # rămâne pur tabelar — metrici + breakdown-uri + ferestre + compliance prop. # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def build_workbook() -> Workbook: # backtest.xlsx = doar Config + Trades (fișierul editat zilnic, ușor/rapid). # Dashboard-ul trăiește separat în data/Dashboard.xlsx, generat la comandă de # scripts/generate_dashboard.py (vezi refresh_dashboard.bat). build_dashboard() # rămâne aici și e refolosit de acel script. wb = Workbook() default = wb.active wb.remove(default) build_config(wb) build_trades(wb) wb.active = wb.sheetnames.index("Trades") return wb def main() -> int: OUTPUT.parent.mkdir(parents=True, exist_ok=True) if OUTPUT.exists(): timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") backup = OUTPUT.with_name(f"{OUTPUT.stem}.backup-{timestamp}{OUTPUT.suffix}") shutil.copy2(OUTPUT, backup) print(f"Backup {backup}") wb = build_workbook() wb.save(OUTPUT) print(f"Wrote {OUTPUT}") return 0 if __name__ == "__main__": raise SystemExit(main())