filtru Prima per Indicator pe grid FERESTRE CANDIDATE x STRATEGIE
- PrimaWin_<idx> helper columns in Trades (per fereastră) - DASH_WIN_COL dict refactor (zero hardcoded litere) - Coloana Filtru (D) în window grid: rânduri Toate/Prima paralele - Score split T (Score_Toate) + U (Score_Prima), ambele hidden - TOP CANDIDATE: 2 sub-secțiuni — TOP 5 Toate + TOP 5 Prima - Config!B17 escape hatch (DA/NU) pentru performanță - 5 sample rows care exercită Prima edge cases - scripts/verify_template.py: 18 aserțiuni smoke test - .gitignore: data/backtest.backup-*.xlsx - sterse 2 backup-uri vechi din git Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@ venv/
|
||||
# Excel temp/lock files
|
||||
~$*.xlsx
|
||||
*.xlsx.bak
|
||||
data/backtest.backup-*.xlsx
|
||||
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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_<idx> — 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_<idx> — ~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")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
237
scripts/verify_template.py
Normal file
237
scripts/verify_template.py
Normal file
@@ -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())
|
||||
Reference in New Issue
Block a user