diff --git a/.gitignore b/.gitignore index 3a5d9ef..e0512f6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ venv/ # Excel temp/lock files ~$*.xlsx *.xlsx.bak +data/backtest.backup-*.xlsx # OS / editor .DS_Store diff --git a/data/backtest.backup-20260521-002847.xlsx b/data/backtest.backup-20260521-002847.xlsx deleted file mode 100644 index 49884d5..0000000 Binary files a/data/backtest.backup-20260521-002847.xlsx and /dev/null differ diff --git a/data/backtest.backup-20260521-024246.xlsx b/data/backtest.backup-20260521-024246.xlsx deleted file mode 100644 index cc0aa56..0000000 Binary files a/data/backtest.backup-20260521-024246.xlsx and /dev/null differ diff --git a/data/backtest.xlsx b/data/backtest.xlsx index c1dd449..2f4b366 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 88359f3..6ac1796 100644 --- a/scripts/generate_template.py +++ b/scripts/generate_template.py @@ -118,6 +118,8 @@ DERIVED_HEADERS = ( + [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] @@ -125,6 +127,7 @@ HELPER_HEADERS = ( + [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 @@ -234,6 +237,22 @@ def build_config(wb: Workbook) -> None: 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), @@ -291,6 +310,36 @@ def _f_session(r: int) -> str: ) +def _f_prima_in_window(r: int, win_idx: int) -> 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}' @@ -471,19 +520,34 @@ def build_trades(wb: Workbook) -> None: 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" + # 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 %"): @@ -523,6 +587,8 @@ def build_trades(wb: Workbook) -> None: 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: @@ -558,6 +624,13 @@ def build_trades(wb: Workbook) -> None: 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] @@ -816,20 +889,26 @@ def build_dashboard(wb: Workbook) -> None: # ---- 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}:R{window_title_row}") + ws.merge_cells(f"A{window_title_row}:{last_dash_col}{window_title_row}") window_header_row = window_title_row + 1 - window_headers = [ - "Fereastra", "Start", "End", "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", - ] - for col_idx, header in enumerate(window_headers, start=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 @@ -840,7 +919,10 @@ def build_dashboard(wb: Workbook) -> None: 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) -> dict[str, str]: + 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)} @@ -857,13 +939,21 @@ def build_dashboard(wb: Workbook) -> None: 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 = ( + 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: @@ -907,89 +997,143 @@ def build_dashboard(wb: Workbook) -> None: combo_rows: list[int] = [] combo_idx = 0 row = window_header_row + 1 - for label, start_time, end_time in TRADABLE_WINDOWS: - for strat in STRAT_KEYS: - helper_cols = _emit_window_helpers(row, strat, combo_idx) - ws[f"A{row}"] = label - ws[f"B{row}"] = start_time - ws[f"C{row}"] = end_time - ws[f"D{row}"] = STRAT_LABELS[strat] - ws[f"E{row}"] = ( - f'=COUNTIFS({OUTCOME_RANGE},"<>",{TIME_RANGE},">="&B{row},' - f'{TIME_RANGE},"<"&C{row})' - ) - ws[f"F{row}"] = ( - f'=COUNTIFS({W[strat]},1,{OUTCOME_RANGE},"<>",' - f'{TIME_RANGE},">="&B{row},{TIME_RANGE},"<"&C{row})' - ) - ws[f"G{row}"] = f"=IFERROR(F{row}/E{row},0)" - ws[f"H{row}"] = ( - f'=IFERROR(AVERAGEIFS({R[strat]},{OUTCOME_RANGE},"<>",' - f'{TIME_RANGE},">="&B{row},{TIME_RANGE},"<"&C{row}),0)' - ) - ws[f"I{row}"] = ( - f'=IFERROR(AVERAGEIFS({D[strat]},{OUTCOME_RANGE},"<>",' - f'{TIME_RANGE},">="&B{row},{TIME_RANGE},"<"&C{row}),0)' - ) - ws[f"J{row}"] = ( - f'=IFERROR(SUMIFS({D[strat]},{D[strat]},">0",{OUTCOME_RANGE},"<>",' - f'{TIME_RANGE},">="&B{row},{TIME_RANGE},"<"&C{row})/' - f'ABS(SUMIFS({D[strat]},{D[strat]},"<0",{OUTCOME_RANGE},"<>",' - f'{TIME_RANGE},">="&B{row},{TIME_RANGE},"<"&C{row})),0)' - ) - ws[f"K{row}"] = ( - f'=SUMIFS({D[strat]},{OUTCOME_RANGE},"<>",' - f'{TIME_RANGE},">="&B{row},{TIME_RANGE},"<"&C{row})' - ) - ws[f"L{row}"] = ( - f'=IFERROR(MAX({helper_cols["DD"]}2:{helper_cols["DD"]}{MAX_ROWS + 1}),0)' - ) - ws[f"M{row}"] = ( - f'=IFERROR(MIN({helper_cols["DailyProp"]}2:' - f'{helper_cols["DailyProp"]}{MAX_ROWS + 1}),0)' - ) - ws[f"N{row}"] = ( - f'=IFERROR(MAX({helper_cols["DDProp"]}2:' - f'{helper_cols["DDProp"]}{MAX_ROWS + 1}),0)' - ) - ws[f"O{row}"] = f'=IF(M{row}<-Config!$B$13,"DA","NU")' - 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(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(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 - 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(window_headers) + 1): + for c in range(1, len(DASH_WIN_HEADERS) + 1): cell = ws.cell(row=r, column=c) cell.border = BORDER - cell.alignment = RIGHT if c not in (1, 4, 15, 16, 17, 18) else CENTER - if c not in (1, 2, 3, 4): + cell.alignment = CENTER if c in center_idx else RIGHT + if c not in no_fill_idx: cell.fill = DERIVED_FILL - for c in ("B", "C"): - ws[f"{c}{r}"].number_format = "hh:mm" - ws[f"E{r}"].number_format = "0" - ws[f"F{r}"].number_format = "0" - ws[f"G{r}"].number_format = "0.0%" - ws[f"H{r}"].number_format = "+0.000;-0.000;0.000" - for c in ("I", "K", "L", "M", "N"): - ws[f"{c}{r}"].number_format = '"$"#,##0.00' - ws[f"J{r}"].number_format = "0.00" - ws.column_dimensions["S"].hidden = True + 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"Q{first_combo}:R{last_combo}" + 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) ) @@ -1006,65 +1150,108 @@ def build_dashboard(wb: Workbook) -> None: 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_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 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", "Strategie", "N", "WR", "Expectancy R", + "#", "Fereastra", "Filtru", "Strategie", "N", "WR", "Expectancy R", "Profit Factor", "Cum P&L $", "Max DD Prop $", "Status Edge", ] - for col_idx, header in enumerate(top_headers, start=1): - c = ws.cell(row=top_header_row, column=col_idx, value=header) - c.font = HEADER_FONT - c.fill = HEADER_FILL - c.alignment = CENTER - c.border = BORDER + # 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"] - for idx in range(1, 11): - r = top_header_row + idx - ws[f"A{r}"] = idx - if combo_rows: - rank_formula = f"LARGE($S${first_combo}:$S${last_combo},{idx})" - match_formula = f"MATCH({rank_formula},$S${first_combo}:$S${last_combo},0)" - for target, source in zip( - ["B", "C", "D", "E", "F", "G", "H", "I", "J"], - ["A", "D", "E", "G", "H", "J", "K", "N", "R"], - ): - ws[f"{target}{r}"] = ( - f'=IFERROR(IF({rank_formula}<=-1E+11,"",' - f'INDEX(${source}${first_combo}:${source}${last_combo},{match_formula})),"")' + 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})" ) - 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, 10) else CENTER - ws[f"E{r}"].number_format = "0.0%" - ws[f"F{r}"].number_format = "+0.000;-0.000;0.000" - ws[f"G{r}"].number_format = "0.00" - ws[f"H{r}"].number_format = '"$"#,##0.00' - ws[f"I{r}"].number_format = '"$"#,##0.00' + 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 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) + # 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, ) - 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) + 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 $ @@ -1123,7 +1310,7 @@ def build_dashboard(wb: Workbook) -> None: return start_row + 1 + len(items) # Breakdowns — toate cele 5 strategii vizibile, Cum P&L $ per strategie - start = top_header_row + 13 + start = after_top_prima + 2 after_strat = _emit_breakdown_strats( start + 2, "PER STRATEGIE — Cum P&L $ per strategie", "Strategie", STRATEGIES, _range("Strategie"), @@ -1287,26 +1474,26 @@ def build_dashboard(wb: Workbook) -> None: 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). + # 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}:G{win_prop_title_row}") + 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. " + "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}:G{win_prop_note_row}") + 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", "Strategie", "Worst Daily Prop $", "Max DD Prop $", + "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): @@ -1316,9 +1503,13 @@ def build_dashboard(wb: Workbook) -> None: 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"] + # 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): @@ -1327,21 +1518,21 @@ def build_dashboard(wb: Workbook) -> None: 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' + 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 G) și pe Daily/Max Breach (cols E, F) + # 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"G{win_prop_first}:G{win_prop_last}" + 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"E{win_prop_first}:F{win_prop_last}" + 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) ) @@ -1349,11 +1540,29 @@ def build_dashboard(wb: Workbook) -> None: breach_rng_win, CellIsRule(operator="equal", formula=['"NU"'], fill=pass_fill) ) - # Column widths + # Column widths — aliniate cu DASH_WIN_COL (A=Fereastra ... U=Score_Prima) widths = { - "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, + 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 @@ -1384,7 +1593,7 @@ def build_dashboard(wb: Workbook) -> None: min_row=2, max_row=MAX_ROWS + 1, ) chart.set_categories(cats) - ws.add_chart(chart, "U4") + ws.add_chart(chart, "V4") # Equity curve prop — al doilea chart, separat de modelul abstract chart_prop = LineChart() @@ -1404,7 +1613,7 @@ def build_dashboard(wb: Workbook) -> None: ) chart_prop.add_data(data_prop, titles_from_data=True) chart_prop.set_categories(cats) - ws.add_chart(chart_prop, "U30") + ws.add_chart(chart_prop, "V30") # --------------------------------------------------------------------------- diff --git a/scripts/verify_template.py b/scripts/verify_template.py new file mode 100644 index 0000000..dc5efde --- /dev/null +++ b/scripts/verify_template.py @@ -0,0 +1,237 @@ +"""Smoke test for data/backtest.xlsx after regeneration. + +Asserts column positions, sample row formula structure, and TOP CANDIDATE +formula sources. Run AFTER `python scripts/generate_template.py`. + +Exit codes: 0 = all PASS, 1 = at least one FAIL, 2 = workbook missing. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +from openpyxl import load_workbook +from openpyxl.utils import get_column_letter + +# Romanian diacritics in test names — force UTF-8 stdout on Windows cp1252. +try: + sys.stdout.reconfigure(encoding="utf-8") +except (AttributeError, OSError): + pass + +OUTPUT = Path(__file__).resolve().parent.parent / "data" / "backtest.xlsx" + +FAILURES: list[str] = [] + + +def _report(name: str, ok: bool, detail: str = "") -> None: + tag = "[PASS]" if ok else "[FAIL]" + msg = f"{tag} {name}" + if detail: + msg += f" -- {detail}" + print(msg) + if not ok: + FAILURES.append(name) + + +def _find_row_with(ws, col_letter: str, needle: str) -> int | None: + """Return 1-based row where ws[col_letter+row].value == needle, else None.""" + for row in range(1, ws.max_row + 1): + val = ws[f"{col_letter}{row}"].value + if val == needle: + return row + return None + + +def _find_col_for_header(ws, row: int, needle: str) -> str | None: + for col_idx in range(1, ws.max_column + 1): + if ws.cell(row=row, column=col_idx).value == needle: + return get_column_letter(col_idx) + return None + + +def check_window_grid_headers(wb) -> None: + ws = wb["Dashboard"] + header_row = _find_row_with(ws, "A", "Fereastra") + if header_row is None: + _report("Dashboard window grid header row", False, "no row with A='Fereastra'") + return + _report("Dashboard window grid header row", True, f"row {header_row}") + + expected = { + "A": "Fereastra", + "D": "Filtru", + "E": "Strategie", + "F": "N", + "S": "Status Edge", + "T": "Score_Toate", + "U": "Score_Prima", + } + for col, want in expected.items(): + got = ws[f"{col}{header_row}"].value + if got == want: + _report(f"Dashboard header {col}={want!r}", True) + else: + actual_col = _find_col_for_header(ws, header_row, want) + detail = ( + f"got {got!r} at {col}; expected {want!r}. " + f"Header {want!r} actually at column {actual_col}" + ) + _report(f"Dashboard header {col}={want!r}", False, detail) + + +def check_hidden_columns(wb) -> None: + ws = wb["Dashboard"] + for col in ("T", "U"): + hidden = ws.column_dimensions[col].hidden is True + _report( + f"Dashboard column {col} hidden", + hidden, + "" if hidden else f"hidden={ws.column_dimensions[col].hidden!r}", + ) + + +def check_primawin_headers(wb) -> tuple[str | None, int]: + """Return (column letter of PrimaWin_0, count of PrimaWin_* headers).""" + ws = wb["Trades"] + win0_col: str | None = None + count = 0 + for col_idx in range(1, ws.max_column + 1): + v = ws.cell(row=1, column=col_idx).value + if isinstance(v, str) and v.startswith("PrimaWin_"): + count += 1 + if v == "PrimaWin_0": + win0_col = get_column_letter(col_idx) + if count == 0: + _report("Trades has PrimaWin_* headers", False, "none found") + else: + _report( + "Trades has PrimaWin_* headers", + True, + f"{count} columns; PrimaWin_0 at column {win0_col}", + ) + return win0_col, count + + +def check_primawin_formula(wb, win0_col: str | None) -> None: + if win0_col is None: + _report("PrimaWin_0 sample row formula", False, "no PrimaWin_0 column") + return + ws = wb["Trades"] + formula = ws[f"{win0_col}2"].value + if not isinstance(formula, str): + _report( + "PrimaWin_0 sample row formula", + False, + f"row 2 value is {formula!r}, not a formula", + ) + return + must_have = ["TIME(16,30,0)", "Config!$B$17", "COUNTIFS"] + missing = [token for token in must_have if token not in formula] + if missing: + _report( + "PrimaWin_0 sample row formula", + False, + f"missing tokens: {missing}; formula starts: {formula[:120]!r}", + ) + else: + _report("PrimaWin_0 sample row formula", True, "TIME(16,30,0) + Config!$B$17 present") + + +def check_top_candidate_sources(wb) -> None: + ws = wb["Dashboard"] + title_toate = "TOP 5 FERESTRE — Toate trade-urile" + title_prima = "TOP 5 FERESTRE — Prima per Indicator" + + # Fallback: scan loosely if the em-dash doesn't match. + def _find_title(prefix: str) -> int | None: + for row in range(1, ws.max_row + 1): + v = ws.cell(row=row, column=1).value + if isinstance(v, str) and v.startswith(prefix): + return row + return None + + cases = [ + ("TOP 5 Toate", title_toate, "Toate", "LARGE($T$"), + ("TOP 5 Prima", title_prima, "Prima per Indicator", "LARGE($U$"), + ] + for name, exact_title, fallback_prefix, expected_token in cases: + title_row = _find_row_with(ws, "A", exact_title) + if title_row is None: + title_row = _find_title(f"TOP 5 FERESTRE") + # Narrow: must mention the discriminator + title_row = None + for row in range(1, ws.max_row + 1): + v = ws.cell(row=row, column=1).value + if isinstance(v, str) and "TOP 5 FERESTRE" in v and fallback_prefix in v: + title_row = row + break + if title_row is None: + _report(f"{name} title found", False, f"no row containing 'TOP 5 FERESTRE' + {fallback_prefix!r}") + continue + _report(f"{name} title found", True, f"row {title_row}") + + # Look at the next few rows for a LARGE() formula in any column. + found = False + for offset in (1, 2, 3): + scan_row = title_row + offset + for col_idx in range(1, ws.max_column + 1): + cell = ws.cell(row=scan_row, column=col_idx).value + if isinstance(cell, str) and expected_token in cell: + _report( + f"{name} uses {expected_token}", + True, + f"row {scan_row} col {get_column_letter(col_idx)}", + ) + found = True + break + if found: + break + if not found: + # Sample a formula for diagnostics. + sample = ws.cell(row=title_row + 1, column=1).value + _report( + f"{name} uses {expected_token}", + False, + f"token not found in rows {title_row+1}..{title_row+3}; sample A: {sample!r}", + ) + + +def check_config_escape_hatch(wb) -> None: + ws = wb["Config"] + a17 = ws["A17"].value + b17 = ws["B17"].value + _report( + "Config!A17 = 'Activează filtru Prima'", + a17 == "Activează filtru Prima", + f"got {a17!r}", + ) + _report("Config!B17 = 'DA'", b17 == "DA", f"got {b17!r}") + + +def main() -> int: + if not OUTPUT.exists(): + print(f"Workbook not found: {OUTPUT}") + print("RUN python scripts/generate_template.py FIRST") + return 2 + + wb = load_workbook(OUTPUT, data_only=False) + + print(f"Verifying {OUTPUT}") + print("-" * 60) + check_window_grid_headers(wb) + check_hidden_columns(wb) + win0_col, _ = check_primawin_headers(wb) + check_primawin_formula(wb, win0_col) + check_top_candidate_sources(wb) + check_config_escape_hatch(wb) + print("-" * 60) + if FAILURES: + print(f"{len(FAILURES)} FAIL: {FAILURES}") + return 1 + print("ALL CHECKS PASSED") + return 0 + + +if __name__ == "__main__": + sys.exit(main())