This commit is contained in:
Marius
2026-05-21 02:21:42 +03:00
parent 504ab69eff
commit 6057a94caa
3 changed files with 296 additions and 10 deletions

Binary file not shown.

View File

@@ -14,7 +14,8 @@ Rulare:
from __future__ import annotations from __future__ import annotations
from datetime import date, time import shutil
from datetime import date, datetime, time, timedelta
from pathlib import Path from pathlib import Path
from openpyxl import Workbook from openpyxl import Workbook
@@ -71,6 +72,37 @@ STRAT_LABELS = {
# Trades sheet — schema # 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 = [ INPUT_HEADERS = [
"#", "Data", "Ora RO", "Zi", "Sesiune", "#", "Data", "Ora RO", "Zi", "Sesiune",
"Strategie", "Indicator", "TF", "Strategie", "Indicator", "TF",
@@ -78,6 +110,8 @@ INPUT_HEADERS = [
"Outcome", "Notes", "Outcome", "Notes",
] ]
DERIVED_HEADERS = ( DERIVED_HEADERS = (
["SL $", "SL $ Prop"]
+
[f"R_{s}" for s in STRAT_KEYS] [f"R_{s}" for s in STRAT_KEYS]
+ [f"$_{s}" for s in STRAT_KEYS] + [f"$_{s}" for s in STRAT_KEYS]
+ [f"Bal_{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)' 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: def _f_balance(r: int, dollar_col: str) -> str:
dc = COL[dollar_col] dc = COL[dollar_col]
return f'=IF({dc}{r}="","",Config!$B$4 + SUM(${dc}$2:{dc}{r}))' 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.cell(row=r, column=1, value="=ROW()-1")
ws[f'{COL["Zi"]}{r}'] = _f_day(r) ws[f'{COL["Zi"]}{r}'] = _f_day(r)
ws[f'{COL["Sesiune"]}{r}'] = _f_session(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: for strat in STRAT_KEYS:
ws[f'{COL[f"R_{strat}"]}{r}'] = R_FN[strat](r) 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): for r in range(2, MAX_ROWS + 2):
ws[f"{COL[col_name]}{r}"].number_format = '0.000"%"' 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 strat in STRAT_KEYS:
for r in range(2, MAX_ROWS + 2): for r in range(2, MAX_ROWS + 2):
ws[f"{COL[f'R_{strat}']}{r}"].number_format = "+0.000;-0.000;0.000" 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", "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 strat in STRAT_KEYS:
for prefix in ("R_", "$_", "Bal_", "$Prop_", "BalProp_"): for prefix in ("R_", "$_", "Bal_", "$Prop_", "BalProp_"):
derived_letters.add(COL[f"{prefix}{strat}"]) derived_letters.add(COL[f"{prefix}{strat}"])
@@ -485,6 +535,8 @@ def build_trades(wb: Workbook) -> None:
} }
for col, w in widths.items(): for col, w in widths.items():
ws.column_dimensions[col].width = w 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 # Derived + helper: width 11
for strat in STRAT_KEYS: for strat in STRAT_KEYS:
for prefix in ( 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.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
hint_cell.border = BORDER 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.) # Helper pentru a emite un block breakdown (per Sesiune / Strategie / etc.)
def _emit_breakdown( def _emit_breakdown(
start_row: int, title: str, first_col_label: str, 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) # Breakdowns — toate folosesc overlay-ul Hybrid+BE (recomandat de trader)
overlay = "hybrid_be" overlay = "hybrid_be"
start = 5 + len(metrics) + 2 # 2 rânduri spațiu după tabelul de metrici start = top_header_row + 13
after_sess = _emit_breakdown( after_sess = start
start, "PER SESIUNE (overlay: Hybrid + BE)", "Sesiune",
SESSIONS, _range("Sesiune"), overlay,
)
after_strat = _emit_breakdown( after_strat = _emit_breakdown(
after_sess + 2, "PER STRATEGIE (overlay: Hybrid + BE)", "Strategie", after_sess + 2, "PER STRATEGIE (overlay: Hybrid + BE)", "Strategie",
STRATEGIES, _range("Strategie"), overlay, STRATEGIES, _range("Strategie"), overlay,
@@ -959,7 +1236,11 @@ def build_dashboard(wb: Workbook) -> None:
ws.row_dimensions[r].height = 60 ws.row_dimensions[r].height = 60
# Column widths # 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(): for col, w in widths.items():
ws.column_dimensions[col].width = w ws.column_dimensions[col].width = w
@@ -989,7 +1270,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, "H4") ws.add_chart(chart, "U4")
# 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()
@@ -1009,7 +1290,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, "H30") ws.add_chart(chart_prop, "U30")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -1030,6 +1311,11 @@ def build_workbook() -> Workbook:
def main() -> int: def main() -> int:
OUTPUT.parent.mkdir(parents=True, exist_ok=True) 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 = build_workbook()
wb.save(OUTPUT) wb.save(OUTPUT)
print(f"Wrote {OUTPUT}") print(f"Wrote {OUTPUT}")