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
|
# Excel temp/lock files
|
||||||
~$*.xlsx
|
~$*.xlsx
|
||||||
*.xlsx.bak
|
*.xlsx.bak
|
||||||
|
data/backtest.backup-*.xlsx
|
||||||
|
|
||||||
# OS / editor
|
# OS / editor
|
||||||
.DS_Store
|
.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"$Prop_{s}" for s in STRAT_KEYS]
|
||||||
+ [f"BalProp_{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 = (
|
HELPER_HEADERS = (
|
||||||
[f"Win_{s}" for s in STRAT_KEYS]
|
[f"Win_{s}" for s in STRAT_KEYS]
|
||||||
+ [f"Peak_{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"DailyPL_{s}" for s in STRAT_KEYS]
|
||||||
+ [f"PeakProp_{s}" for s in STRAT_KEYS]
|
+ [f"PeakProp_{s}" for s in STRAT_KEYS]
|
||||||
+ [f"DDProp_{s}" for s in STRAT_KEYS]
|
+ [f"DDProp_{s}" for s in STRAT_KEYS]
|
||||||
|
+ PRIMA_HELPERS
|
||||||
)
|
)
|
||||||
TRADES_HEADERS = INPUT_HEADERS + DERIVED_HEADERS + HELPER_HEADERS
|
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["B14"].number_format = '0.0"%"'
|
||||||
ws["B15"].number_format = "$#,##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)
|
# Liste dropdown — coloanele E–J (6 coloane)
|
||||||
list_columns = [
|
list_columns = [
|
||||||
("Strategii", STRATEGIES),
|
("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:
|
def _f_r_tp0only(r: int) -> str:
|
||||||
o = f'{COL["Outcome"]}{r}'
|
o = f'{COL["Outcome"]}{r}'
|
||||||
sl = f'{COL["SL %"]}{r}'
|
sl = f'{COL["SL %"]}{r}'
|
||||||
@@ -471,19 +520,34 @@ def build_trades(wb: Workbook) -> None:
|
|||||||
r, f"PeakProp_{strat}", f"BalProp_{strat}"
|
r, f"PeakProp_{strat}", f"BalProp_{strat}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sample row 2
|
# Coloanele PrimaWin_<idx> — 1 dacă trade-ul e prima per (Data, Indicator) în fereastră
|
||||||
ws["B2"] = date(2026, 5, 13)
|
for win_idx in range(len(TRADABLE_WINDOWS)):
|
||||||
ws["C2"] = time(17, 33)
|
ws[f'{COL[f"PrimaWin_{win_idx}"]}{r}'] = _f_prima_in_window(r, win_idx)
|
||||||
ws[f'{COL["Strategie"]}2'] = "M2D"
|
|
||||||
ws[f'{COL["Indicator"]}2'] = "DIA"
|
# Sample rows
|
||||||
ws[f'{COL["TF"]}2'] = "1min"
|
SAMPLE_ROWS = [
|
||||||
ws[f'{COL["Direcție"]}2'] = "Sell"
|
# (data, ora, strat, ind, tf, dir, sl, tp0, tp1, tp2, outcome, notes)
|
||||||
ws[f'{COL["SL %"]}2'] = 0.30
|
(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"),
|
||||||
ws[f'{COL["TP0 %"]}2'] = 0.10
|
(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"),
|
||||||
ws[f'{COL["TP1 %"]}2'] = 0.15
|
(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"),
|
||||||
ws[f'{COL["TP2 %"]}2'] = 0.30
|
(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"),
|
||||||
ws[f'{COL["Outcome"]}2'] = "TP1"
|
(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ă)"),
|
||||||
ws[f'{COL["Notes"]}2'] = "Exemplu — șterge când începi"
|
]
|
||||||
|
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
|
# Number formats
|
||||||
for col_name in ("SL %", "TP0 %", "TP1 %", "TP2 %"):
|
for col_name in ("SL %", "TP0 %", "TP1 %", "TP2 %"):
|
||||||
@@ -523,6 +587,8 @@ def build_trades(wb: Workbook) -> None:
|
|||||||
for strat in STRAT_KEYS:
|
for strat in STRAT_KEYS:
|
||||||
for prefix in ("Win_", "Peak_", "DD_", "DailyPL_", "PeakProp_", "DDProp_"):
|
for prefix in ("Win_", "Peak_", "DD_", "DailyPL_", "PeakProp_", "DDProp_"):
|
||||||
helper_letters.add(COL[f"{prefix}{strat}"])
|
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 r in range(2, MAX_ROWS + 2):
|
||||||
for cl in input_letters:
|
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].outlineLevel = 1
|
||||||
ws.column_dimensions[cl].hidden = True
|
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
|
# Data validation dropdowns
|
||||||
def _add_dv(col_name: str, source: str) -> None:
|
def _add_dv(col_name: str, source: str) -> None:
|
||||||
cl = COL[col_name]
|
cl = COL[col_name]
|
||||||
@@ -816,20 +889,26 @@ def build_dashboard(wb: Workbook) -> None:
|
|||||||
# ---- FERESTRE CANDIDATE x STRATEGIE ----
|
# ---- FERESTRE CANDIDATE x STRATEGIE ----
|
||||||
# Tabel principal pentru alegerea ferestrei tradabile. Drawdown-ul este
|
# Tabel principal pentru alegerea ferestrei tradabile. Drawdown-ul este
|
||||||
# calculat cu helper-e ascunse pe fereastra curenta, nu din DD global.
|
# 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
|
window_title_row = 5 + len(metrics) + 2
|
||||||
ws[f"A{window_title_row}"] = "FERESTRE CANDIDATE x STRATEGIE"
|
ws[f"A{window_title_row}"] = "FERESTRE CANDIDATE x STRATEGIE"
|
||||||
ws[f"A{window_title_row}"].font = SUBTITLE_FONT
|
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_header_row = window_title_row + 1
|
||||||
window_headers = [
|
for col_idx, header in enumerate(DASH_WIN_HEADERS, start=1):
|
||||||
"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 = ws.cell(row=window_header_row, column=col_idx, value=header)
|
||||||
c.font = HEADER_FONT
|
c.font = HEADER_FONT
|
||||||
c.fill = HEADER_FILL
|
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}
|
PROP_D = {s: _range(f"$Prop_{s}") for s in STRAT_KEYS}
|
||||||
helper_start_col = 27 # AA, ascuns.
|
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
|
base_col = helper_start_col + combo_idx * 7
|
||||||
helper_names = ["Cum", "Peak", "DD", "DailyProp", "CumProp", "PeakProp", "DDProp"]
|
helper_names = ["Cum", "Peak", "DD", "DailyProp", "CumProp", "PeakProp", "DDProp"]
|
||||||
cols = {name: get_column_letter(base_col + idx) for idx, name in enumerate(helper_names)}
|
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"]
|
time_col = COL["Ora RO"]
|
||||||
date_col = COL["Data"]
|
date_col = COL["Data"]
|
||||||
outcome_col = COL["Outcome"]
|
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):
|
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'AND(Trades!${outcome_col}{trade_row}<>"",'
|
||||||
f"Trades!${time_col}{trade_row}>={start_cell},"
|
f"Trades!${time_col}{trade_row}>={start_cell},"
|
||||||
f"Trades!${time_col}{trade_row}<{end_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}"
|
dollar = f"Trades!${dollar_col}{trade_row}"
|
||||||
prop = f"Trades!${prop_col}{trade_row}"
|
prop = f"Trades!${prop_col}{trade_row}"
|
||||||
if helper_row == 2:
|
if helper_row == 2:
|
||||||
@@ -907,89 +997,143 @@ def build_dashboard(wb: Workbook) -> None:
|
|||||||
combo_rows: list[int] = []
|
combo_rows: list[int] = []
|
||||||
combo_idx = 0
|
combo_idx = 0
|
||||||
row = window_header_row + 1
|
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 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 = ws.cell(row=r, column=c)
|
||||||
cell.border = BORDER
|
cell.border = BORDER
|
||||||
cell.alignment = RIGHT if c not in (1, 4, 15, 16, 17, 18) else CENTER
|
cell.alignment = CENTER if c in center_idx else RIGHT
|
||||||
if c not in (1, 2, 3, 4):
|
if c not in no_fill_idx:
|
||||||
cell.fill = DERIVED_FILL
|
cell.fill = DERIVED_FILL
|
||||||
for c in ("B", "C"):
|
ws[f"{B_}{r}"].number_format = "hh:mm"
|
||||||
ws[f"{c}{r}"].number_format = "hh:mm"
|
ws[f"{C_}{r}"].number_format = "hh:mm"
|
||||||
ws[f"E{r}"].number_format = "0"
|
ws[f"{F_}{r}"].number_format = "0"
|
||||||
ws[f"F{r}"].number_format = "0"
|
ws[f"{G_}{r}"].number_format = "0"
|
||||||
ws[f"G{r}"].number_format = "0.0%"
|
ws[f"{H_}{r}"].number_format = "0.0%"
|
||||||
ws[f"H{r}"].number_format = "+0.000;-0.000;0.000"
|
ws[f"{I_}{r}"].number_format = "+0.000;-0.000;0.000"
|
||||||
for c in ("I", "K", "L", "M", "N"):
|
for c_letter in (J_, L_, M_, N_, O_):
|
||||||
ws[f"{c}{r}"].number_format = '"$"#,##0.00'
|
ws[f"{c_letter}{r}"].number_format = '"$"#,##0.00'
|
||||||
ws[f"J{r}"].number_format = "0.00"
|
ws[f"{K_}{r}"].number_format = "0.00"
|
||||||
ws.column_dimensions["S"].hidden = True
|
# Score_Toate și Score_Prima ascunse
|
||||||
|
ws.column_dimensions[T_LET].hidden = True
|
||||||
|
ws.column_dimensions[U_LET].hidden = True
|
||||||
|
|
||||||
if combo_rows:
|
if combo_rows:
|
||||||
first_combo = combo_rows[0]
|
first_combo = combo_rows[0]
|
||||||
last_combo = combo_rows[-1]
|
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(
|
ws.conditional_formatting.add(
|
||||||
status_rng, CellIsRule(operator="equal", formula=['"CONFORM"'], fill=pass_fill)
|
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)
|
status_rng, CellIsRule(operator="equal", formula=['"PRE-CANDIDAT"'], fill=warn_fill)
|
||||||
)
|
)
|
||||||
|
|
||||||
top_title_row = row + 2
|
# ---- TOP CANDIDATE — două sub-secțiuni: Toate + Prima ----
|
||||||
ws[f"A{top_title_row}"] = "TOP CANDIDATE"
|
# Score_Toate (col T) și Score_Prima (col U) sunt populate condițional pe Filtru;
|
||||||
ws[f"A{top_title_row}"].font = SUBTITLE_FONT
|
# LARGE pe coloana corespunzătoare extrage doar rândurile relevante.
|
||||||
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_headers = [
|
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",
|
"Profit Factor", "Cum P&L $", "Max DD Prop $", "Status Edge",
|
||||||
]
|
]
|
||||||
for col_idx, header in enumerate(top_headers, start=1):
|
# Mapă coloană target din TOP → header din DASH_WIN_COL
|
||||||
c = ws.cell(row=top_header_row, column=col_idx, value=header)
|
top_source_names = [
|
||||||
c.font = HEADER_FONT
|
"Fereastra", "Filtru", "Strategie", "N", "WR", "Expectancy R",
|
||||||
c.fill = HEADER_FILL
|
"Profit Factor", "Cum P&L $", "Max Drawdown Prop $", "Status Edge",
|
||||||
c.alignment = CENTER
|
]
|
||||||
c.border = BORDER
|
top_target_letters = ["B", "C", "D", "E", "F", "G", "H", "I", "J", "K"]
|
||||||
|
|
||||||
for idx in range(1, 11):
|
def _emit_top_subsection(start_row: int, title: str, note: str,
|
||||||
r = top_header_row + idx
|
score_col: str, count: int = 5) -> int:
|
||||||
ws[f"A{r}"] = idx
|
ws[f"A{start_row}"] = title
|
||||||
if combo_rows:
|
ws[f"A{start_row}"].font = SUBTITLE_FONT
|
||||||
rank_formula = f"LARGE($S${first_combo}:$S${last_combo},{idx})"
|
ws.merge_cells(f"A{start_row}:K{start_row}")
|
||||||
match_formula = f"MATCH({rank_formula},$S${first_combo}:$S${last_combo},0)"
|
note_row = start_row + 1
|
||||||
for target, source in zip(
|
ws[f"A{note_row}"] = note
|
||||||
["B", "C", "D", "E", "F", "G", "H", "I", "J"],
|
ws[f"A{note_row}"].font = Font(
|
||||||
["A", "D", "E", "G", "H", "J", "K", "N", "R"],
|
name="Calibri", size=10, italic=True, color="595959"
|
||||||
):
|
)
|
||||||
ws[f"{target}{r}"] = (
|
ws[f"A{note_row}"].alignment = Alignment(
|
||||||
f'=IFERROR(IF({rank_formula}<=-1E+11,"",'
|
horizontal="left", vertical="center", wrap_text=True
|
||||||
f'INDEX(${source}${first_combo}:${source}${last_combo},{match_formula})),"")'
|
)
|
||||||
|
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):
|
match_formula = (
|
||||||
cell = ws.cell(row=r, column=c)
|
f"MATCH({rank_formula},"
|
||||||
cell.border = BORDER
|
f"${score_col}${first_combo}:${score_col}${last_combo},0)"
|
||||||
cell.alignment = RIGHT if c not in (2, 3, 10) else CENTER
|
)
|
||||||
ws[f"E{r}"].number_format = "0.0%"
|
for target, source_name in zip(top_target_letters, top_source_names):
|
||||||
ws[f"F{r}"].number_format = "+0.000;-0.000;0.000"
|
source = DASH_WIN_COL[source_name]
|
||||||
ws[f"G{r}"].number_format = "0.00"
|
ws[f"{target}{r}"] = (
|
||||||
ws[f"H{r}"].number_format = '"$"#,##0.00'
|
f'=IFERROR(IF({rank_formula}<=-1E+11,"",'
|
||||||
ws[f"I{r}"].number_format = '"$"#,##0.00'
|
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
|
# CF pe Status Edge (col K)
|
||||||
top_status_rng = f"J{top_header_row + 1}:J{top_header_row + 10}"
|
top_status_rng = f"K{header_row + 1}:K{header_row + count}"
|
||||||
ws.conditional_formatting.add(
|
ws.conditional_formatting.add(
|
||||||
top_status_rng, CellIsRule(operator="equal", formula=['"CANDIDAT"'], fill=pass_fill)
|
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(
|
after_top_prima = _emit_top_subsection(
|
||||||
top_status_rng, CellIsRule(operator="equal", formula=['"PRE-CANDIDAT"'], fill=warn_fill)
|
after_top_toate + 2,
|
||||||
)
|
"TOP 5 FERESTRE — Prima per Indicator",
|
||||||
ws.conditional_formatting.add(
|
(
|
||||||
top_status_rng, CellIsRule(operator="equal", formula=['"BREACH"'], fill=fail_fill)
|
"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 $
|
# Conditional formatting reutilizabil pentru celulele Cum $
|
||||||
@@ -1123,7 +1310,7 @@ def build_dashboard(wb: Workbook) -> None:
|
|||||||
return start_row + 1 + len(items)
|
return start_row + 1 + len(items)
|
||||||
|
|
||||||
# Breakdowns — toate cele 5 strategii vizibile, Cum P&L $ per strategie
|
# 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(
|
after_strat = _emit_breakdown_strats(
|
||||||
start + 2, "PER STRATEGIE — Cum P&L $ per strategie", "Strategie",
|
start + 2, "PER STRATEGIE — Cum P&L $ per strategie", "Strategie",
|
||||||
STRATEGIES, _range("Strategie"),
|
STRATEGIES, _range("Strategie"),
|
||||||
@@ -1287,26 +1474,26 @@ def build_dashboard(wb: Workbook) -> None:
|
|||||||
ws.row_dimensions[r].height = 60
|
ws.row_dimensions[r].height = 60
|
||||||
|
|
||||||
# ---- PROP FIRM COMPLIANCE per FEREASTRĂ × STRATEGIE ----
|
# ---- PROP FIRM COMPLIANCE per FEREASTRĂ × STRATEGIE ----
|
||||||
# Reshape compliance: rânduri = combo (fereastră × strategie), coloane = metrici compliance.
|
# Reshape compliance: rânduri = combo (fereastră × strategie × filtru),
|
||||||
# Datele sunt referențiate direct din FERESTRE CANDIDATE (cols A, D, M, N, O, P, Q).
|
# coloane = metrici compliance. Datele referențiate prin DASH_WIN_COL.
|
||||||
if combo_rows:
|
if combo_rows:
|
||||||
win_prop_title_row = prop_header_row + 1 + len(prop_metrics) + 2
|
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}"] = "PROP FIRM COMPLIANCE — per FEREASTRĂ × STRATEGIE"
|
||||||
ws[f"A{win_prop_title_row}"].font = SUBTITLE_FONT
|
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
|
win_prop_note_row = win_prop_title_row + 1
|
||||||
ws[f"A{win_prop_note_row}"] = (
|
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."
|
"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}"].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[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_header_row = win_prop_note_row + 1
|
||||||
win_prop_headers = [
|
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",
|
"Daily Breach", "Max Breach", "Overall Prop",
|
||||||
]
|
]
|
||||||
for col_idx, h in enumerate(win_prop_headers, start=1):
|
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.alignment = CENTER
|
||||||
c.border = BORDER
|
c.border = BORDER
|
||||||
|
|
||||||
# source cols din FERESTRE CANDIDATE: A=Fereastra, D=Strategie, M=Worst Daily,
|
# source cols din FERESTRE CANDIDATE (via DASH_WIN_COL)
|
||||||
# N=Max DD, O=Daily Breach, P=Max Breach, Q=Overall Prop
|
source_names = [
|
||||||
source_cols = ["A", "D", "M", "N", "O", "P", "Q"]
|
"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):
|
for offset, combo_row in enumerate(combo_rows, start=1):
|
||||||
r = win_prop_header_row + offset
|
r = win_prop_header_row + offset
|
||||||
for col_idx, source in enumerate(source_cols, start=1):
|
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 = ws[f"{target}{r}"]
|
||||||
cell.border = BORDER
|
cell.border = BORDER
|
||||||
cell.fill = DERIVED_FILL
|
cell.fill = DERIVED_FILL
|
||||||
cell.alignment = CENTER if col_idx in (1, 2, 5, 6, 7) else RIGHT
|
cell.alignment = CENTER if col_idx in (1, 2, 3, 6, 7, 8) else RIGHT
|
||||||
ws[f"C{r}"].number_format = '"$"#,##0.00'
|
|
||||||
ws[f"D{r}"].number_format = '"$"#,##0.00'
|
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_first = win_prop_header_row + 1
|
||||||
win_prop_last = win_prop_header_row + len(combo_rows)
|
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(
|
ws.conditional_formatting.add(
|
||||||
overall_rng_win, CellIsRule(operator="equal", formula=['"CONFORM"'], fill=pass_fill)
|
overall_rng_win, CellIsRule(operator="equal", formula=['"CONFORM"'], fill=pass_fill)
|
||||||
)
|
)
|
||||||
ws.conditional_formatting.add(
|
ws.conditional_formatting.add(
|
||||||
overall_rng_win, CellIsRule(operator="equal", formula=['"CONT PIERDUT"'], fill=fail_fill)
|
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(
|
ws.conditional_formatting.add(
|
||||||
breach_rng_win, CellIsRule(operator="equal", formula=['"DA"'], fill=fail_fill)
|
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)
|
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 = {
|
widths = {
|
||||||
"A": 18, "B": 10, "C": 18, "D": 16, "E": 13, "F": 13, "G": 16,
|
DASH_WIN_COL["Fereastra"]: 18,
|
||||||
"H": 13, "I": 13, "J": 12, "K": 13, "L": 15, "M": 20,
|
DASH_WIN_COL["Start"]: 10,
|
||||||
"N": 18, "O": 13, "P": 14, "Q": 15, "R": 13, "S": 8,
|
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():
|
for col, w in widths.items():
|
||||||
ws.column_dimensions[col].width = w
|
ws.column_dimensions[col].width = w
|
||||||
@@ -1384,7 +1593,7 @@ def build_dashboard(wb: Workbook) -> None:
|
|||||||
min_row=2, max_row=MAX_ROWS + 1,
|
min_row=2, max_row=MAX_ROWS + 1,
|
||||||
)
|
)
|
||||||
chart.set_categories(cats)
|
chart.set_categories(cats)
|
||||||
ws.add_chart(chart, "U4")
|
ws.add_chart(chart, "V4")
|
||||||
|
|
||||||
# Equity curve prop — al doilea chart, separat de modelul abstract
|
# Equity curve prop — al doilea chart, separat de modelul abstract
|
||||||
chart_prop = LineChart()
|
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.add_data(data_prop, titles_from_data=True)
|
||||||
chart_prop.set_categories(cats)
|
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