diff --git a/data/backtest.corrupt-20260521-004058.xlsx b/data/backtest.corrupt-20260521-004058.xlsx deleted file mode 100644 index 59e6349..0000000 Binary files a/data/backtest.corrupt-20260521-004058.xlsx and /dev/null differ diff --git a/data/backtest.xlsx b/data/backtest.xlsx index 7b95ec6..cc0aa56 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 d84b177..1aed1bc 100644 --- a/scripts/generate_template.py +++ b/scripts/generate_template.py @@ -14,7 +14,8 @@ Rulare: from __future__ import annotations -from datetime import date, time +import shutil +from datetime import date, datetime, time, timedelta from pathlib import Path from openpyxl import Workbook @@ -71,6 +72,37 @@ STRAT_LABELS = { # Trades sheet — schema # --------------------------------------------------------------------------- +def _candidate_windows() -> list[tuple[str, time, time]]: + """Ferestre suprapuse intre 16:30 si 23:00, evaluate pe ora Romaniei.""" + base = datetime(2000, 1, 1, 16, 30) + last_start = datetime(2000, 1, 1, 22, 0) + hard_ends = [ + datetime(2000, 1, 1, 22, 45), + datetime(2000, 1, 1, 23, 0), + ] + durations = [timedelta(minutes=m) for m in (60, 90, 120, 180)] + seen: set[tuple[time, time]] = set() + windows: list[tuple[str, time, time]] = [] + + start = base + while start <= last_start: + ends = [start + d for d in durations] + ends += [end for end in hard_ends if end - start >= timedelta(minutes=60)] + for end in ends: + if end > hard_ends[-1]: + continue + key = (start.time(), end.time()) + if key in seen: + continue + seen.add(key) + windows.append((f"{start:%H:%M}-{end:%H:%M}", start.time(), end.time())) + start += timedelta(minutes=30) + return windows + + +TRADABLE_WINDOWS = _candidate_windows() + + INPUT_HEADERS = [ "#", "Data", "Ora RO", "Zi", "Sesiune", "Strategie", "Indicator", "TF", @@ -78,6 +110,8 @@ INPUT_HEADERS = [ "Outcome", "Notes", ] DERIVED_HEADERS = ( + ["SL $", "SL $ Prop"] + + [f"R_{s}" for s in STRAT_KEYS] + [f"$_{s}" for s in STRAT_KEYS] + [f"Bal_{s}" for s in STRAT_KEYS] @@ -323,6 +357,16 @@ def _f_dollar(r: int, r_col: str) -> str: return f'=IF({rc}="","",{rc}*{sl}/100*Config!$B$4)' +def _f_sl_dollar(r: int) -> str: + sl = f"{COL['SL %']}{r}" + return f'=IF({sl}="","",{sl}/100*Config!$B$4)' + + +def _f_sl_dollar_prop(r: int) -> str: + sl = f"{COL['SL %']}{r}" + return f'=IF({sl}="","",{sl}/100*Config!$B$11)' + + def _f_balance(r: int, dollar_col: str) -> str: dc = COL[dollar_col] return f'=IF({dc}{r}="","",Config!$B$4 + SUM(${dc}$2:{dc}{r}))' @@ -396,6 +440,8 @@ def build_trades(wb: Workbook) -> None: ws.cell(row=r, column=1, value="=ROW()-1") ws[f'{COL["Zi"]}{r}'] = _f_day(r) ws[f'{COL["Sesiune"]}{r}'] = _f_session(r) + ws[f'{COL["SL $"]}{r}'] = _f_sl_dollar(r) + ws[f'{COL["SL $ Prop"]}{r}'] = _f_sl_dollar_prop(r) for strat in STRAT_KEYS: ws[f'{COL[f"R_{strat}"]}{r}'] = R_FN[strat](r) @@ -438,6 +484,10 @@ def build_trades(wb: Workbook) -> None: for r in range(2, MAX_ROWS + 2): ws[f"{COL[col_name]}{r}"].number_format = '0.000"%"' + for col_name in ("SL $", "SL $ Prop"): + for r in range(2, MAX_ROWS + 2): + ws[f"{COL[col_name]}{r}"].number_format = '"$"#,##0.00' + for strat in STRAT_KEYS: for r in range(2, MAX_ROWS + 2): ws[f"{COL[f'R_{strat}']}{r}"].number_format = "+0.000;-0.000;0.000" @@ -459,7 +509,7 @@ def build_trades(wb: Workbook) -> None: "Outcome", "Notes", ) } - derived_letters = {COL["Zi"], COL["Sesiune"]} + derived_letters = {COL["Zi"], COL["Sesiune"], COL["SL $"], COL["SL $ Prop"]} for strat in STRAT_KEYS: for prefix in ("R_", "$_", "Bal_", "$Prop_", "BalProp_"): derived_letters.add(COL[f"{prefix}{strat}"]) @@ -485,6 +535,8 @@ def build_trades(wb: Workbook) -> None: } for col, w in widths.items(): ws.column_dimensions[col].width = w + for col_name in ("SL $", "SL $ Prop"): + ws.column_dimensions[COL[col_name]].width = 12 # Derived + helper: width 11 for strat in STRAT_KEYS: for prefix in ( @@ -754,6 +806,234 @@ def build_dashboard(wb: Workbook) -> None: hint_cell.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True) hint_cell.border = BORDER + # ---- FERESTRE CANDIDATE x STRATEGIE ---- + # Tabel principal pentru alegerea ferestrei tradabile. Drawdown-ul este + # calculat cu helper-e ascunse pe fereastra curenta, nu din DD global. + 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}") + + 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): + c = ws.cell(row=window_header_row, column=col_idx, value=header) + c.font = HEADER_FONT + c.fill = HEADER_FILL + c.alignment = CENTER + c.border = BORDER + + TIME_RANGE = _range("Ora RO") + PROP_D = {s: _range(f"$Prop_{s}") for s in STRAT_KEYS} + helper_start_col = 27 # AA, ascuns. + + def _emit_window_helpers(visible_row: int, strat: str, combo_idx: int) -> dict[str, str]: + base_col = helper_start_col + combo_idx * 7 + helper_names = ["Cum", "Peak", "DD", "DailyProp", "CumProp", "PeakProp", "DDProp"] + cols = {name: get_column_letter(base_col + idx) for idx, name in enumerate(helper_names)} + for idx, name in enumerate(helper_names): + col = get_column_letter(base_col + idx) + ws[f"{col}1"] = f"{name}_{visible_row}" + ws.column_dimensions[col].hidden = True + ws.column_dimensions[col].width = 3 + + start_cell = f"$B${visible_row}" + end_cell = f"$C${visible_row}" + dollar_col = COL[f"$_{strat}"] + prop_col = COL[f"$Prop_{strat}"] + time_col = COL["Ora RO"] + date_col = COL["Data"] + outcome_col = COL["Outcome"] + + for helper_row, trade_row in enumerate(range(2, MAX_ROWS + 2), start=2): + in_window = ( + f'AND(Trades!${outcome_col}{trade_row}<>"",' + f"Trades!${time_col}{trade_row}>={start_cell}," + f"Trades!${time_col}{trade_row}<{end_cell})" + ) + dollar = f"Trades!${dollar_col}{trade_row}" + prop = f"Trades!${prop_col}{trade_row}" + if helper_row == 2: + ws[f"{cols['Cum']}{helper_row}"] = f"=IF({in_window},{dollar},0)" + ws[f"{cols['Peak']}{helper_row}"] = f"=MAX(0,{cols['Cum']}{helper_row})" + ws[f"{cols['CumProp']}{helper_row}"] = f"=IF({in_window},{prop},0)" + ws[f"{cols['PeakProp']}{helper_row}"] = f"=MAX(0,{cols['CumProp']}{helper_row})" + else: + prev = helper_row - 1 + ws[f"{cols['Cum']}{helper_row}"] = ( + f"={cols['Cum']}{prev}+IF({in_window},{dollar},0)" + ) + ws[f"{cols['Peak']}{helper_row}"] = ( + f"=MAX({cols['Peak']}{prev},{cols['Cum']}{helper_row})" + ) + ws[f"{cols['CumProp']}{helper_row}"] = ( + f"={cols['CumProp']}{prev}+IF({in_window},{prop},0)" + ) + ws[f"{cols['PeakProp']}{helper_row}"] = ( + f"=MAX({cols['PeakProp']}{prev},{cols['CumProp']}{helper_row})" + ) + ws[f"{cols['DD']}{helper_row}"] = ( + f"={cols['Peak']}{helper_row}-{cols['Cum']}{helper_row}" + ) + ws[f"{cols['DDProp']}{helper_row}"] = ( + f"={cols['PeakProp']}{helper_row}-{cols['CumProp']}{helper_row}" + ) + ws[f"{cols['DailyProp']}{helper_row}"] = ( + f'=IF({in_window},' + f'SUMIFS(Trades!${prop_col}$2:Trades!${prop_col}{trade_row},' + f'Trades!${date_col}$2:Trades!${date_col}{trade_row},Trades!${date_col}{trade_row},' + f'Trades!${time_col}$2:Trades!${time_col}{trade_row},">="&{start_cell},' + f'Trades!${time_col}$2:Trades!${time_col}{trade_row},"<"&{end_cell}),' + f'"")' + ) + return cols + + pass_fill = PatternFill("solid", fgColor="C6EFCE") + fail_fill = PatternFill("solid", fgColor="FFC7CE") + warn_fill = PatternFill("solid", fgColor="FFEB9C") + combo_rows: list[int] = [] + combo_idx = 0 + row = window_header_row + 1 + 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(AND(E{row}>=40,G{row}>=55%,H{row}>=0.2,' + f'O{row}="NU",P{row}="NU"),"CANDIDAT","NU")' + ) + ws[f"S{row}"] = ( + f'=IF(R{row}="CANDIDAT",H{row}*100000+J{row}*1000+K{row}-L{row}-N{row}/10,-1)' + ) + combo_rows.append(row) + combo_idx += 1 + row += 1 + + for r in combo_rows: + for c in range(1, len(window_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.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 + + if combo_rows: + first_combo = combo_rows[0] + last_combo = combo_rows[-1] + status_rng = f"Q{first_combo}:R{last_combo}" + ws.conditional_formatting.add( + status_rng, CellIsRule(operator="equal", formula=['"CONFORM"'], fill=pass_fill) + ) + ws.conditional_formatting.add( + status_rng, CellIsRule(operator="equal", formula=['"CANDIDAT"'], fill=pass_fill) + ) + ws.conditional_formatting.add( + status_rng, CellIsRule(operator="equal", formula=['"CONT PIERDUT"'], fill=fail_fill) + ) + ws.conditional_formatting.add( + status_rng, CellIsRule(operator="equal", formula=['"NU"'], 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_headers = [ + "#", "Fereastra", "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 + + 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}<0,"",' + f'INDEX(${source}${first_combo}:${source}${last_combo},{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, 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' + # Helper pentru a emite un block breakdown (per Sesiune / Strategie / etc.) def _emit_breakdown( start_row: int, title: str, first_col_label: str, @@ -791,11 +1071,8 @@ def build_dashboard(wb: Workbook) -> None: # 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, - ) + 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, @@ -959,7 +1236,11 @@ def build_dashboard(wb: Workbook) -> None: ws.row_dimensions[r].height = 60 # Column widths - widths = {"A": 22, "B": 14, "C": 14, "D": 14, "E": 16, "F": 16, "G": 75} + widths = { + "A": 18, "B": 10, "C": 10, "D": 16, "E": 8, "F": 8, "G": 10, + "H": 13, "I": 13, "J": 12, "K": 13, "L": 15, "M": 20, + "N": 18, "O": 13, "P": 14, "Q": 15, "R": 13, "S": 8, + } for col, w in widths.items(): ws.column_dimensions[col].width = w @@ -989,7 +1270,7 @@ def build_dashboard(wb: Workbook) -> None: min_row=2, max_row=MAX_ROWS + 1, ) chart.set_categories(cats) - ws.add_chart(chart, "H4") + ws.add_chart(chart, "U4") # Equity curve prop — al doilea chart, separat de modelul abstract chart_prop = LineChart() @@ -1009,7 +1290,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, "H30") + ws.add_chart(chart_prop, "U30") # --------------------------------------------------------------------------- @@ -1030,6 +1311,11 @@ def build_workbook() -> Workbook: def main() -> int: OUTPUT.parent.mkdir(parents=True, exist_ok=True) + if OUTPUT.exists(): + timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + backup = OUTPUT.with_name(f"{OUTPUT.stem}.backup-{timestamp}{OUTPUT.suffix}") + shutil.copy2(OUTPUT, backup) + print(f"Backup {backup}") wb = build_workbook() wb.save(OUTPUT) print(f"Wrote {OUTPUT}")