diff --git a/data/backtest.backup-20260521-024246.xlsx b/data/backtest.backup-20260521-024246.xlsx new file mode 100644 index 0000000..cc0aa56 Binary files /dev/null and b/data/backtest.backup-20260521-024246.xlsx differ diff --git a/data/backtest.xlsx b/data/backtest.xlsx index cc0aa56..6cc53cc 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 1aed1bc..88359f3 100644 --- a/scripts/generate_template.py +++ b/scripts/generate_template.py @@ -195,13 +195,13 @@ def build_config(wb: Workbook) -> None: 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["A10"] = "Contracte per trade" + ws["B10"] = 1 + ws["C10"] = "Număr de contracte tranzacționate per semnal (TradeLocker)" - ws["A11"] = "Position Size ($)" - ws["B11"] = "=B9*B10/100" - ws["C11"] = "Auto — notional efectiv pe trade" + 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 @@ -219,15 +219,15 @@ def build_config(wb: Workbook) -> None: ws["B15"] = "=B9*B14/100" ws["C15"] = "Auto — derivat din B9 și B14" - for r in (9, 10, 12, 14): # inputuri galbene + 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 (11, 13, 15): # derived blue + 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["B10"].number_format = "0" ws["B11"].number_format = "$#,##0" ws["B12"].number_format = '0.0"%"' ws["B13"].number_format = "$#,##0" @@ -351,20 +351,22 @@ R_FN: dict[str, callable] = { def _f_dollar(r: int, r_col: str) -> str: - """$ P&L pe contul abstract. Variabil per trade = R × SL%/100 × Account Size.""" + """$ 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}/100*Config!$B$4)' + 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}/100*Config!$B$4)' + 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}/100*Config!$B$11)' + return f'=IF({sl}="","",{sl}*Config!$B$10*Config!$B$11)' def _f_balance(r: int, dollar_col: str) -> str: @@ -395,10 +397,14 @@ def _f_drawdown(r: int, peak_col: str, balance_col: str) -> str: def _f_dollar_prop(r: int, r_col: str) -> str: - """$ P&L pe contul de prop. Variabil per trade = R × SL%/100 × Position Size.""" + """$ 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}/100*Config!$B$11)' + return f'=IF({rc}="","",{rc}*{sl}*Config!$B$10*Config!$B$11)' def _f_balance_prop(r: int, dollar_col: str) -> str: @@ -620,7 +626,7 @@ METRIC_HINTS: dict[str, str] = { ), "Average Loss ($)": ( "Pierderea medie pe trade-urile negative (cifra apare cu minus).\n" - "În dolari reali, −1R depinde de SL%: pierdere ≈ SL% × Account Size Start.\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 ($)": ( @@ -630,8 +636,8 @@ METRIC_HINTS: dict[str, str] = { ), "Worst Trade ($)": ( "Cea mai mare pierdere individuală.\n" - "Ar trebui să fie aproximativ egală cu −1R calculat din SL% × Account Size Start.\n" - "Dacă e semnificativ mai mare, ai depășit risk-ul plănuit — SL ratat, slippage, gap overnight." + "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" @@ -650,8 +656,8 @@ METRIC_HINTS: dict[str, str] = { "Pragul de GO LIVE: +0.20R sau mai mult." ), "Expectancy ($)": ( - "Aceeași expectancy convertită în dolari, folosind SL% × Account Size Start per trade.\n" - "Util ca să vezi cât câștigi în medie pe trade în bani reali, nu doar în R." + "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" @@ -672,13 +678,14 @@ METRIC_HINTS: dict[str, str] = { "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)." + "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% × Position Size per trade), NU R-multiple-ul abstract de mai sus.\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 ($)": ( @@ -949,11 +956,13 @@ def build_dashboard(wb: Workbook) -> None: ws[f"P{row}"] = f'=IF(N{row}>Config!$B$15,"DA","NU")' ws[f"Q{row}"] = f'=IF(OR(O{row}="DA",P{row}="DA"),"CONT PIERDUT","CONFORM")' ws[f"R{row}"] = ( - f'=IF(AND(E{row}>=40,G{row}>=55%,H{row}>=0.2,' - f'O{row}="NU",P{row}="NU"),"CANDIDAT","NU")' + f'=IF(E{row}<1,"",' + f'IF(OR(O{row}="DA",P{row}="DA"),"BREACH",' + f'IF(AND(E{row}>=40,G{row}>=55%,H{row}>=0.2),"CANDIDAT","PRE-CANDIDAT")))' ) ws[f"S{row}"] = ( - f'=IF(R{row}="CANDIDAT",H{row}*100000+J{row}*1000+K{row}-L{row}-N{row}/10,-1)' + f'=IF(E{row}<1,-1E+12,' + f'H{row}*100000+J{row}*1000+K{row}-L{row}-N{row}/10)' ) combo_rows.append(row) combo_idx += 1 @@ -991,14 +1000,26 @@ def build_dashboard(wb: Workbook) -> None: status_rng, CellIsRule(operator="equal", formula=['"CONT PIERDUT"'], fill=fail_fill) ) ws.conditional_formatting.add( - status_rng, CellIsRule(operator="equal", formula=['"NU"'], fill=warn_fill) + 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_title_row = row + 2 ws[f"A{top_title_row}"] = "TOP CANDIDATE" ws[f"A{top_title_row}"].font = SUBTITLE_FONT ws.merge_cells(f"A{top_title_row}:J{top_title_row}") - top_header_row = top_title_row + 1 + top_note_row = top_title_row + 1 + ws[f"A{top_note_row}"] = ( + "Top 10 ferestre după scor compus. CANDIDAT = îndeplinește toate pragurile " + "(N≥40, WR≥55%, ExpR≥0.2, no breach). PRE-CANDIDAT = N≥1 fără breach dar sub praguri. " + "BREACH = ar fi pierdut contul prop." + ) + ws[f"A{top_note_row}"].font = Font(name="Calibri", size=10, italic=True, color="595959") + ws[f"A{top_note_row}"].alignment = Alignment(horizontal="left", vertical="center", wrap_text=True) + ws.merge_cells(f"A{top_note_row}:J{top_note_row}") + top_header_row = top_note_row + 1 top_headers = [ "#", "Fereastra", "Strategie", "N", "WR", "Expectancy R", "Profit Factor", "Cum P&L $", "Max DD Prop $", "Status Edge", @@ -1021,7 +1042,7 @@ def build_dashboard(wb: Workbook) -> None: ["A", "D", "E", "G", "H", "J", "K", "N", "R"], ): ws[f"{target}{r}"] = ( - f'=IFERROR(IF({rank_formula}<0,"",' + f'=IFERROR(IF({rank_formula}<=-1E+11,"",' f'INDEX(${source}${first_combo}:${source}${last_combo},{match_formula})),"")' ) for c in range(1, len(top_headers) + 1): @@ -1034,56 +1055,86 @@ def build_dashboard(wb: Workbook) -> None: ws[f"H{r}"].number_format = '"$"#,##0.00' ws[f"I{r}"].number_format = '"$"#,##0.00' - # Helper pentru a emite un block breakdown (per Sesiune / Strategie / etc.) - def _emit_breakdown( + # CF pe coloana Status Edge din TOP CANDIDATE + top_status_rng = f"J{top_header_row + 1}:J{top_header_row + 10}" + 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) + ) + + # 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, overlay_strat: 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}:F{start_row}") - headers = [first_col_label, "N", "Wins", "WR", "Expectancy R", "Cum $"] + 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 - 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)' + 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[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) + 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 folosesc overlay-ul Hybrid+BE (recomandat de trader) - overlay = "hybrid_be" + # Breakdowns — toate cele 5 strategii vizibile, Cum P&L $ per strategie start = top_header_row + 13 - after_sess = start - after_strat = _emit_breakdown( - after_sess + 2, "PER STRATEGIE (overlay: Hybrid + BE)", "Strategie", - STRATEGIES, _range("Strategie"), overlay, + after_strat = _emit_breakdown_strats( + start + 2, "PER STRATEGIE — Cum P&L $ per strategie", "Strategie", + STRATEGIES, _range("Strategie"), ) - after_ind = _emit_breakdown( - after_strat + 2, "PER INDICATOR (overlay: Hybrid + BE)", "Indicator", - INDICATORS, _range("Indicator"), overlay, + after_ind = _emit_breakdown_strats( + after_strat + 2, "PER INDICATOR — Cum P&L $ per strategie", "Indicator", + INDICATORS, _range("Indicator"), ) - after_dir = _emit_breakdown( - after_ind + 2, "PER DIRECȚIE (overlay: Hybrid + BE)", "Direcție", - DIRECTIONS, _range("Direcție"), overlay, + 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 ---- @@ -1235,9 +1286,72 @@ def build_dashboard(wb: Workbook) -> None: 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), coloane = metrici compliance. + # Datele sunt referențiate direct din FERESTRE CANDIDATE (cols A, D, M, N, O, P, Q). + 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}:G{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. " + "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}:G{win_prop_note_row}") + + win_prop_header_row = win_prop_note_row + 1 + win_prop_headers = [ + "Fereastra", "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: A=Fereastra, D=Strategie, M=Worst Daily, + # N=Max DD, O=Daily Breach, P=Max Breach, Q=Overall Prop + source_cols = ["A", "D", "M", "N", "O", "P", "Q"] + 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, 5, 6, 7) else RIGHT + ws[f"C{r}"].number_format = '"$"#,##0.00' + ws[f"D{r}"].number_format = '"$"#,##0.00' + + # CF pe Overall Prop (col G) și pe Daily/Max Breach (cols E, F) + win_prop_first = win_prop_header_row + 1 + win_prop_last = win_prop_header_row + len(combo_rows) + overall_rng_win = f"G{win_prop_first}:G{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"E{win_prop_first}:F{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 widths = { - "A": 18, "B": 10, "C": 10, "D": 16, "E": 8, "F": 8, "G": 10, + "A": 18, "B": 10, "C": 18, "D": 16, "E": 13, "F": 13, "G": 16, "H": 13, "I": 13, "J": 12, "K": 13, "L": 15, "M": 20, "N": 18, "O": 13, "P": 14, "Q": 15, "R": 13, "S": 8, }