diff --git a/data/backtest.backup-prop.xlsx b/data/backtest.backup-prop.xlsx new file mode 100644 index 0000000..0c6975b Binary files /dev/null and b/data/backtest.backup-prop.xlsx differ diff --git a/data/backtest.xlsx b/data/backtest.xlsx index a1521bb..1696992 100644 Binary files a/data/backtest.xlsx and b/data/backtest.xlsx differ diff --git a/scripts/generate_template.py b/scripts/generate_template.py index 1d6bd3d..aae7558 100644 --- a/scripts/generate_template.py +++ b/scripts/generate_template.py @@ -81,11 +81,16 @@ 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 @@ -128,7 +133,7 @@ def build_config(wb: Workbook) -> None: ws["A4"] = "Account Size Start ($)" ws["B4"] = 10000 - ws["C4"] = "Balanța inițială pentru calcule $ și HWM" + ws["C4"] = "Balanța inițială pentru calcule $ și HWM (model abstract)" ws["A5"] = "Risk per Trade (%)" ws["B5"] = 1.0 @@ -147,6 +152,54 @@ def build_config(wb: Workbook) -> None: 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), @@ -295,6 +348,29 @@ def _f_drawdown(r: int, peak_col: str, balance_col: str) -> str: 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 # --------------------------------------------------------------------------- @@ -330,6 +406,16 @@ def build_trades(wb: Workbook) -> None: 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) @@ -353,7 +439,10 @@ def build_trades(wb: Workbook) -> None: 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_"): + 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): @@ -370,12 +459,11 @@ def build_trades(wb: Workbook) -> None: } derived_letters = {COL["Zi"], COL["Sesiune"]} for strat in STRAT_KEYS: - derived_letters.add(COL[f"R_{strat}"]) - derived_letters.add(COL[f"$_{strat}"]) - derived_letters.add(COL[f"Bal_{strat}"]) + 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_"): + for prefix in ("Win_", "Peak_", "DD_", "DailyPL_", "PeakProp_", "DDProp_"): helper_letters.add(COL[f"{prefix}{strat}"]) for r in range(2, MAX_ROWS + 2): @@ -397,9 +485,19 @@ def build_trades(wb: Workbook) -> None: ws.column_dimensions[col].width = w # Derived + helper: width 11 for strat in STRAT_KEYS: - for prefix in ("R_", "$_", "Bal_", "Win_", "Peak_", "DD_"): + 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] @@ -514,6 +612,46 @@ METRIC_HINTS: dict[str, str] = { "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." + ), } @@ -664,11 +802,160 @@ def build_dashboard(wb: Workbook) -> None: after_strat + 2, "PER INDICATOR (overlay: Hybrid + BE)", "Indicator", INDICATORS, _range("Indicator"), overlay, ) - _emit_breakdown( + 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(): @@ -702,6 +989,26 @@ def build_dashboard(wb: Workbook) -> None: 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