"""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 from datetime import date, time 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 only", "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 # --------------------------------------------------------------------------- INPUT_HEADERS = [ "#", "Data", "Ora RO", "Zi", "Sesiune", "Strategie", "Indicator", "TF", "Direcție", "SL %", "TP0 %", "TP1 %", "TP2 %", "Outcome", "Notes", ] DERIVED_HEADERS = ( [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] ) 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] ) 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 per Trade (%)" ws["B5"] = 1.0 ws["C5"] = "% din account riscat per trade (= -1R)" ws["A6"] = "Risk per Trade ($)" ws["B6"] = "=B4*B5/100" ws["C6"] = "Auto — derivat din B4 și B5" 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"] = "Position Usage (%)" ws["B10"] = 80 ws["C10"] = "% din cont folosit ca notional (max contracte)" ws["A11"] = "Position Size ($)" ws["B11"] = "=B9*B10/100" ws["C11"] = "Auto — notional efectiv pe trade" 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, 12, 14): # 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): # 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" # 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: 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 only"),-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 only",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 only",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: rc = f"{COL[r_col]}{r}" return f'=IF({rc}="","",{rc}*Config!$B$6)' 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. Variabil per trade = R × SL%/100 × Position Size.""" rc = f"{COL[r_col]}{r}" sl = f"{COL['SL %']}{r}" return f'=IF({rc}="","",{rc}*{sl}/100*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) 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}" ) # Sample row 2 ws["B2"] = date(2026, 5, 13) ws["C2"] = time(17, 33) ws[f'{COL["Strategie"]}2'] = "M2D" ws[f'{COL["Indicator"]}2'] = "DIA" ws[f'{COL["TF"]}2'] = "1min" ws[f'{COL["Direcție"]}2'] = "Sell" ws[f'{COL["SL %"]}2'] = 0.30 ws[f'{COL["TP0 %"]}2'] = 0.10 ws[f'{COL["TP1 %"]}2'] = 0.15 ws[f'{COL["TP2 %"]}2'] = 0.30 ws[f'{COL["Outcome"]}2'] = "TP1" ws[f'{COL["Notes"]}2'] = "Exemplu — șterge când începi" # 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 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"]} 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 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 # 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 # 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" "Cu Risk per Trade fix, ar trebui să fie aproape de −1R în dolari.\n" "Dacă e mult mai mare decât Risk per Trade, 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 (Risk per Trade din Config).\n" "Dacă e semnificativ mai mare, ai depășit risk-ul plănuit — SL ratat, slippage, gap overnight." ), "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 bani câștigi în medie pe UN trade (în R; 1R = Risk per Trade, default $100).\n" "+0.30R = câștigi $30 pe trade. Pe 100 trade-uri: +$3.000.\n" "−0.10R = pierzi $10 pe trade. Pe 100 trade-uri: −$1.000.\n" "Pragul de GO LIVE: +0.20R sau mai mult." ), "Expectancy ($)": ( "Aceeași expectancy convertită în dolari, folosind Risk per Trade din Config.\n" "Util ca să vezi cât câștigi în medie pe trade în bani reali, 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 ($)": ( "Notional efectiv pe trade = Account Prop × Position Usage %.\n" "Default: 80% × $50,000 = $40,000. Pe DIA, ≈ 0.8 contracte din prețul curent.\n" "Pierderea pe SL = SL% × Position Size (NU procent fix din cont)." ), "Cumulative P&L Prop ($)": ( "Profitul total al contului de prop pe traseul logat.\n" "Reflectă $ real (SL% × Position Size per trade), NU R-multiple-ul abstract de mai sus.\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 # Helper pentru a emite un block breakdown (per Sesiune / Strategie / etc.) def _emit_breakdown( start_row: int, title: str, first_col_label: str, items: list[str], item_range: str, overlay_strat: str, ) -> int: ws[f"A{start_row}"] = title ws[f"A{start_row}"].font = SUBTITLE_FONT ws.merge_cells(f"A{start_row}:F{start_row}") headers = [first_col_label, "N", "Wins", "WR", "Expectancy R", "Cum $"] 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 for i, item in enumerate(items): r = start_row + 2 + i ws[f"A{r}"] = item ws[f"B{r}"] = f'=COUNTIF({item_range},"{item}")' ws[f"C{r}"] = f'=COUNTIFS({item_range},"{item}",{W[overlay_strat]},1)' ws[f"D{r}"] = f"=IFERROR(C{r}/B{r},0)" ws[f"E{r}"] = ( f'=IFERROR(AVERAGEIFS({R[overlay_strat]},{item_range},"{item}"),0)' ) ws[f"F{r}"] = f'=SUMIFS({D[overlay_strat]},{item_range},"{item}")' ws[f"B{r}"].number_format = "0" ws[f"C{r}"].number_format = "0" ws[f"D{r}"].number_format = "0.0%" ws[f"E{r}"].number_format = "+0.000;-0.000;0.000" ws[f"F{r}"].number_format = '"$"#,##0.00' for c in ("A", "B", "C", "D", "E", "F"): ws[f"{c}{r}"].border = BORDER ws[f"{c}{r}"].alignment = RIGHT if c != "A" else LEFT return start_row + 2 + len(items) # Breakdowns — toate folosesc overlay-ul Hybrid+BE (recomandat de trader) overlay = "hybrid_be" start = 5 + len(metrics) + 2 # 2 rânduri spațiu după tabelul de metrici after_sess = _emit_breakdown( start, "PER SESIUNE (overlay: Hybrid + BE)", "Sesiune", SESSIONS, _range("Sesiune"), overlay, ) after_strat = _emit_breakdown( after_sess + 2, "PER STRATEGIE (overlay: Hybrid + BE)", "Strategie", STRATEGIES, _range("Strategie"), overlay, ) after_ind = _emit_breakdown( after_strat + 2, "PER INDICATOR (overlay: Hybrid + BE)", "Indicator", INDICATORS, _range("Indicator"), overlay, ) after_dir = _emit_breakdown( after_ind + 2, "PER DIRECȚIE (overlay: Hybrid + BE)", "Direcție", DIRECTIONS, _range("Direcție"), overlay, ) # ---- 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 # Column widths widths = {"A": 22, "B": 14, "C": 14, "D": 14, "E": 16, "F": 16, "G": 75} 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, "H4") # 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, "H30") # --------------------------------------------------------------------------- # 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) wb = build_workbook() wb.save(OUTPUT) print(f"Wrote {OUTPUT}") return 0 if __name__ == "__main__": raise SystemExit(main())