"""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.chart import LineChart, Reference 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 Prop Firm (separat de modelul abstract de mai sus) ---- ws["A8"] = "Cont Prop Firm" ws["A8"].font = SUBTITLE_FONT ws.merge_cells("A8:C8") ws["A9"] = "Account Prop Start ($)" ws["B9"] = 50000 ws["C9"] = "Balanța contului de prop" ws["A10"] = "Contracte per trade" ws["B10"] = 1 ws["C10"] = "Număr de contracte tranzacționate per semnal (TradeLocker)" ws["A11"] = "$ per 1% per contract" ws["B11"] = 10000 ws["C11"] = "Pe DIA: 0.10% = $1000 ⇒ 1% = $10,000 (1 contract notional ≈ $1M)" 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 B9 și B12" 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 B9 și B14" for r in (9, 10, 11, 12, 14): # inputuri galbene ws.cell(row=r, column=2).fill = INPUT_FILL ws.cell(row=r, column=2).border = BORDER for r in (13, 15): # 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" 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" # 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) # 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": 24, "B": 14, "C": 38, "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% × Contracte × $/1% per contract.\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% × Contracte × $/1% per contract.\n" "Pe TradeLocker DIA: SL=0.30%, 1 contract → ≈ −$3000. Dacă e 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% × Contracte × $/1% per contract.\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." ), "Position Size ($)": ( "Configurare contract real TradeLocker:\n" " • Contracte per trade (Config B10) — câte contracte tranzacționezi pe semnal.\n" " • $ per 1% per contract (Config B11) — pe DIA: 0.10% = $1000 → 1% = $10,000.\n" "Pierderea pe SL = SL% × Contracte × $/1% per contract. Pentru SL=0.30%, 1 contract → $3000." ), "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 = $50,000 + Cumulative P&L Prop.\n" "Compar-o cu pragul de stop-out al firmei de prop: $50,000 − $3,500 = $46,500." ), "Worst Daily Loss ($)": ( "Cea mai proastă pierdere cumulativă într-o zi calendaristică.\n" "Dacă e mai mică decât −$2,000, ai depășit Daily Loss Limit într-o zi — 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 ($2,000 default).\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ă > $3,500 (7% din $50k), ai depășit Max Loss Limit — cont mort." ), "Max Loss Status": ( "PASS dacă Max Account Drawdown ≤ $3,500.\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"),-1E+12,' f'{I_}{row}*100000+{K_}{row}*1000+{L_}{row}-{M_}{row}-{O_}{row}/10)' ) ws[f"{U_LET}{row}"] = ( f'=IF(OR({F_}{row}<1,{D_}{row}<>"Prima"),-1E+12,' f'{I_}{row}*100000+{K_}{row}*1000+{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 = 5) -> 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 5 FERESTRE — Toate trade-urile", ( "Top 5 după scor compus, calculat pe rândurile cu Filtru=Toate. " "CANDIDAT = îndeplinește pragurile (N≥40, WR≥55%, ExpR≥0.2, no breach). " "PRE-CANDIDAT = N≥1 fără breach dar sub praguri. BREACH = ar fi pierdut prop." ), score_col=T_LET, ) after_top_prima = _emit_top_subsection( after_top_toate + 2, "TOP 5 FERESTRE — Prima per Indicator", ( "Top 5 după scor compus, calculat pe rândurile cu Filtru=Prima (doar primul " "trade pe (Data, Indicator) în fiecare fereastră). 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', ), ( "Position Size ($)", lambda s: "=Config!$B$11", '"$"#,##0', ), ( "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 # Equity curve chart — 5 linii chart = LineChart() chart.title = "Equity Curve — 5 strategii" chart.style = 12 chart.y_axis.title = "Balance ($)" chart.x_axis.title = "Trade #" chart.height = 12 chart.width = 24 data = Reference( wb["Trades"], min_col=_col_to_int(COL[f"Bal_{STRAT_KEYS[0]}"]), max_col=_col_to_int(COL[f"Bal_{STRAT_KEYS[-1]}"]), min_row=1, max_row=MAX_ROWS + 1, ) chart.add_data(data, titles_from_data=True) cats = Reference( wb["Trades"], min_col=1, max_col=1, min_row=2, max_row=MAX_ROWS + 1, ) chart.set_categories(cats) ws.add_chart(chart, "V4") # Equity curve prop — al doilea chart, separat de modelul abstract chart_prop = LineChart() chart_prop.title = "Equity Curve — Prop ($50k start)" chart_prop.style = 12 chart_prop.y_axis.title = "Balance Prop ($)" chart_prop.x_axis.title = "Trade #" chart_prop.height = 12 chart_prop.width = 24 data_prop = Reference( wb["Trades"], min_col=_col_to_int(COL[f"BalProp_{STRAT_KEYS[0]}"]), max_col=_col_to_int(COL[f"BalProp_{STRAT_KEYS[-1]}"]), min_row=1, max_row=MAX_ROWS + 1, ) chart_prop.add_data(data_prop, titles_from_data=True) chart_prop.set_categories(cats) ws.add_chart(chart_prop, "V30") # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def build_workbook() -> Workbook: wb = Workbook() default = wb.active wb.remove(default) build_config(wb) build_trades(wb) build_dashboard(wb) wb.active = wb.sheetnames.index("Dashboard") 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())