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

View File

@@ -14,7 +14,8 @@ Rulare:
from __future__ import annotations
from datetime import date, time
import shutil
from datetime import date, datetime, time, timedelta
from pathlib import Path
from openpyxl import Workbook
@@ -71,6 +72,37 @@ STRAT_LABELS = {
# Trades sheet — schema
# ---------------------------------------------------------------------------
def _candidate_windows() -> list[tuple[str, time, time]]:
"""Ferestre suprapuse intre 16:30 si 23:00, evaluate pe ora Romaniei."""
base = datetime(2000, 1, 1, 16, 30)
last_start = datetime(2000, 1, 1, 22, 0)
hard_ends = [
datetime(2000, 1, 1, 22, 45),
datetime(2000, 1, 1, 23, 0),
]
durations = [timedelta(minutes=m) for m in (60, 90, 120, 180)]
seen: set[tuple[time, time]] = set()
windows: list[tuple[str, time, time]] = []
start = base
while start <= last_start:
ends = [start + d for d in durations]
ends += [end for end in hard_ends if end - start >= timedelta(minutes=60)]
for end in ends:
if end > hard_ends[-1]:
continue
key = (start.time(), end.time())
if key in seen:
continue
seen.add(key)
windows.append((f"{start:%H:%M}-{end:%H:%M}", start.time(), end.time()))
start += timedelta(minutes=30)
return windows
TRADABLE_WINDOWS = _candidate_windows()
INPUT_HEADERS = [
"#", "Data", "Ora RO", "Zi", "Sesiune",
"Strategie", "Indicator", "TF",
@@ -78,6 +110,8 @@ INPUT_HEADERS = [
"Outcome", "Notes",
]
DERIVED_HEADERS = (
["SL $", "SL $ Prop"]
+
[f"R_{s}" for s in STRAT_KEYS]
+ [f"$_{s}" for s in STRAT_KEYS]
+ [f"Bal_{s}" for s in STRAT_KEYS]
@@ -323,6 +357,16 @@ def _f_dollar(r: int, r_col: str) -> str:
return f'=IF({rc}="","",{rc}*{sl}/100*Config!$B$4)'
def _f_sl_dollar(r: int) -> str:
sl = f"{COL['SL %']}{r}"
return f'=IF({sl}="","",{sl}/100*Config!$B$4)'
def _f_sl_dollar_prop(r: int) -> str:
sl = f"{COL['SL %']}{r}"
return f'=IF({sl}="","",{sl}/100*Config!$B$11)'
def _f_balance(r: int, dollar_col: str) -> str:
dc = COL[dollar_col]
return f'=IF({dc}{r}="","",Config!$B$4 + SUM(${dc}$2:{dc}{r}))'
@@ -396,6 +440,8 @@ def build_trades(wb: Workbook) -> None:
ws.cell(row=r, column=1, value="=ROW()-1")
ws[f'{COL["Zi"]}{r}'] = _f_day(r)
ws[f'{COL["Sesiune"]}{r}'] = _f_session(r)
ws[f'{COL["SL $"]}{r}'] = _f_sl_dollar(r)
ws[f'{COL["SL $ Prop"]}{r}'] = _f_sl_dollar_prop(r)
for strat in STRAT_KEYS:
ws[f'{COL[f"R_{strat}"]}{r}'] = R_FN[strat](r)
@@ -438,6 +484,10 @@ def build_trades(wb: Workbook) -> None:
for r in range(2, MAX_ROWS + 2):
ws[f"{COL[col_name]}{r}"].number_format = '0.000"%"'
for col_name in ("SL $", "SL $ Prop"):
for r in range(2, MAX_ROWS + 2):
ws[f"{COL[col_name]}{r}"].number_format = '"$"#,##0.00'
for strat in STRAT_KEYS:
for r in range(2, MAX_ROWS + 2):
ws[f"{COL[f'R_{strat}']}{r}"].number_format = "+0.000;-0.000;0.000"
@@ -459,7 +509,7 @@ def build_trades(wb: Workbook) -> None:
"Outcome", "Notes",
)
}
derived_letters = {COL["Zi"], COL["Sesiune"]}
derived_letters = {COL["Zi"], COL["Sesiune"], COL["SL $"], COL["SL $ Prop"]}
for strat in STRAT_KEYS:
for prefix in ("R_", "$_", "Bal_", "$Prop_", "BalProp_"):
derived_letters.add(COL[f"{prefix}{strat}"])
@@ -485,6 +535,8 @@ def build_trades(wb: Workbook) -> None:
}
for col, w in widths.items():
ws.column_dimensions[col].width = w
for col_name in ("SL $", "SL $ Prop"):
ws.column_dimensions[COL[col_name]].width = 12
# Derived + helper: width 11
for strat in STRAT_KEYS:
for prefix in (
@@ -754,6 +806,234 @@ def build_dashboard(wb: Workbook) -> None:
hint_cell.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
hint_cell.border = BORDER
# ---- FERESTRE CANDIDATE x STRATEGIE ----
# Tabel principal pentru alegerea ferestrei tradabile. Drawdown-ul este
# calculat cu helper-e ascunse pe fereastra curenta, nu din DD global.
window_title_row = 5 + len(metrics) + 2
ws[f"A{window_title_row}"] = "FERESTRE CANDIDATE x STRATEGIE"
ws[f"A{window_title_row}"].font = SUBTITLE_FONT
ws.merge_cells(f"A{window_title_row}:R{window_title_row}")
window_header_row = window_title_row + 1
window_headers = [
"Fereastra", "Start", "End", "Strategie", "N", "Wins", "WR",
"Expectancy R", "Expectancy $", "Profit Factor", "Cum P&L $",
"Max Drawdown $", "Worst Daily Loss Prop $", "Max Drawdown Prop $",
"Daily Breach", "Max Loss Breach", "Status Prop", "Status Edge",
"Score",
]
for col_idx, header in enumerate(window_headers, start=1):
c = ws.cell(row=window_header_row, column=col_idx, value=header)
c.font = HEADER_FONT
c.fill = HEADER_FILL
c.alignment = CENTER
c.border = BORDER
TIME_RANGE = _range("Ora RO")
PROP_D = {s: _range(f"$Prop_{s}") for s in STRAT_KEYS}
helper_start_col = 27 # AA, ascuns.
def _emit_window_helpers(visible_row: int, strat: str, combo_idx: int) -> dict[str, str]:
base_col = helper_start_col + combo_idx * 7
helper_names = ["Cum", "Peak", "DD", "DailyProp", "CumProp", "PeakProp", "DDProp"]
cols = {name: get_column_letter(base_col + idx) for idx, name in enumerate(helper_names)}
for idx, name in enumerate(helper_names):
col = get_column_letter(base_col + idx)
ws[f"{col}1"] = f"{name}_{visible_row}"
ws.column_dimensions[col].hidden = True
ws.column_dimensions[col].width = 3
start_cell = f"$B${visible_row}"
end_cell = f"$C${visible_row}"
dollar_col = COL[f"$_{strat}"]
prop_col = COL[f"$Prop_{strat}"]
time_col = COL["Ora RO"]
date_col = COL["Data"]
outcome_col = COL["Outcome"]
for helper_row, trade_row in enumerate(range(2, MAX_ROWS + 2), start=2):
in_window = (
f'AND(Trades!${outcome_col}{trade_row}<>"",'
f"Trades!${time_col}{trade_row}>={start_cell},"
f"Trades!${time_col}{trade_row}<{end_cell})"
)
dollar = f"Trades!${dollar_col}{trade_row}"
prop = f"Trades!${prop_col}{trade_row}"
if helper_row == 2:
ws[f"{cols['Cum']}{helper_row}"] = f"=IF({in_window},{dollar},0)"
ws[f"{cols['Peak']}{helper_row}"] = f"=MAX(0,{cols['Cum']}{helper_row})"
ws[f"{cols['CumProp']}{helper_row}"] = f"=IF({in_window},{prop},0)"
ws[f"{cols['PeakProp']}{helper_row}"] = f"=MAX(0,{cols['CumProp']}{helper_row})"
else:
prev = helper_row - 1
ws[f"{cols['Cum']}{helper_row}"] = (
f"={cols['Cum']}{prev}+IF({in_window},{dollar},0)"
)
ws[f"{cols['Peak']}{helper_row}"] = (
f"=MAX({cols['Peak']}{prev},{cols['Cum']}{helper_row})"
)
ws[f"{cols['CumProp']}{helper_row}"] = (
f"={cols['CumProp']}{prev}+IF({in_window},{prop},0)"
)
ws[f"{cols['PeakProp']}{helper_row}"] = (
f"=MAX({cols['PeakProp']}{prev},{cols['CumProp']}{helper_row})"
)
ws[f"{cols['DD']}{helper_row}"] = (
f"={cols['Peak']}{helper_row}-{cols['Cum']}{helper_row}"
)
ws[f"{cols['DDProp']}{helper_row}"] = (
f"={cols['PeakProp']}{helper_row}-{cols['CumProp']}{helper_row}"
)
ws[f"{cols['DailyProp']}{helper_row}"] = (
f'=IF({in_window},'
f'SUMIFS(Trades!${prop_col}$2:Trades!${prop_col}{trade_row},'
f'Trades!${date_col}$2:Trades!${date_col}{trade_row},Trades!${date_col}{trade_row},'
f'Trades!${time_col}$2:Trades!${time_col}{trade_row},">="&{start_cell},'
f'Trades!${time_col}$2:Trades!${time_col}{trade_row},"<"&{end_cell}),'
f'"")'
)
return cols
pass_fill = PatternFill("solid", fgColor="C6EFCE")
fail_fill = PatternFill("solid", fgColor="FFC7CE")
warn_fill = PatternFill("solid", fgColor="FFEB9C")
combo_rows: list[int] = []
combo_idx = 0
row = window_header_row + 1
for label, start_time, end_time in TRADABLE_WINDOWS:
for strat in STRAT_KEYS:
helper_cols = _emit_window_helpers(row, strat, combo_idx)
ws[f"A{row}"] = label
ws[f"B{row}"] = start_time
ws[f"C{row}"] = end_time
ws[f"D{row}"] = STRAT_LABELS[strat]
ws[f"E{row}"] = (
f'=COUNTIFS({OUTCOME_RANGE},"<>",{TIME_RANGE},">="&B{row},'
f'{TIME_RANGE},"<"&C{row})'
)
ws[f"F{row}"] = (
f'=COUNTIFS({W[strat]},1,{OUTCOME_RANGE},"<>",'
f'{TIME_RANGE},">="&B{row},{TIME_RANGE},"<"&C{row})'
)
ws[f"G{row}"] = f"=IFERROR(F{row}/E{row},0)"
ws[f"H{row}"] = (
f'=IFERROR(AVERAGEIFS({R[strat]},{OUTCOME_RANGE},"<>",'
f'{TIME_RANGE},">="&B{row},{TIME_RANGE},"<"&C{row}),0)'
)
ws[f"I{row}"] = (
f'=IFERROR(AVERAGEIFS({D[strat]},{OUTCOME_RANGE},"<>",'
f'{TIME_RANGE},">="&B{row},{TIME_RANGE},"<"&C{row}),0)'
)
ws[f"J{row}"] = (
f'=IFERROR(SUMIFS({D[strat]},{D[strat]},">0",{OUTCOME_RANGE},"<>",'
f'{TIME_RANGE},">="&B{row},{TIME_RANGE},"<"&C{row})/'
f'ABS(SUMIFS({D[strat]},{D[strat]},"<0",{OUTCOME_RANGE},"<>",'
f'{TIME_RANGE},">="&B{row},{TIME_RANGE},"<"&C{row})),0)'
)
ws[f"K{row}"] = (
f'=SUMIFS({D[strat]},{OUTCOME_RANGE},"<>",'
f'{TIME_RANGE},">="&B{row},{TIME_RANGE},"<"&C{row})'
)
ws[f"L{row}"] = (
f'=IFERROR(MAX({helper_cols["DD"]}2:{helper_cols["DD"]}{MAX_ROWS + 1}),0)'
)
ws[f"M{row}"] = (
f'=IFERROR(MIN({helper_cols["DailyProp"]}2:'
f'{helper_cols["DailyProp"]}{MAX_ROWS + 1}),0)'
)
ws[f"N{row}"] = (
f'=IFERROR(MAX({helper_cols["DDProp"]}2:'
f'{helper_cols["DDProp"]}{MAX_ROWS + 1}),0)'
)
ws[f"O{row}"] = f'=IF(M{row}<-Config!$B$13,"DA","NU")'
ws[f"P{row}"] = f'=IF(N{row}>Config!$B$15,"DA","NU")'
ws[f"Q{row}"] = f'=IF(OR(O{row}="DA",P{row}="DA"),"CONT PIERDUT","CONFORM")'
ws[f"R{row}"] = (
f'=IF(AND(E{row}>=40,G{row}>=55%,H{row}>=0.2,'
f'O{row}="NU",P{row}="NU"),"CANDIDAT","NU")'
)
ws[f"S{row}"] = (
f'=IF(R{row}="CANDIDAT",H{row}*100000+J{row}*1000+K{row}-L{row}-N{row}/10,-1)'
)
combo_rows.append(row)
combo_idx += 1
row += 1
for r in combo_rows:
for c in range(1, len(window_headers) + 1):
cell = ws.cell(row=r, column=c)
cell.border = BORDER
cell.alignment = RIGHT if c not in (1, 4, 15, 16, 17, 18) else CENTER
if c not in (1, 2, 3, 4):
cell.fill = DERIVED_FILL
for c in ("B", "C"):
ws[f"{c}{r}"].number_format = "hh:mm"
ws[f"E{r}"].number_format = "0"
ws[f"F{r}"].number_format = "0"
ws[f"G{r}"].number_format = "0.0%"
ws[f"H{r}"].number_format = "+0.000;-0.000;0.000"
for c in ("I", "K", "L", "M", "N"):
ws[f"{c}{r}"].number_format = '"$"#,##0.00'
ws[f"J{r}"].number_format = "0.00"
ws.column_dimensions["S"].hidden = True
if combo_rows:
first_combo = combo_rows[0]
last_combo = combo_rows[-1]
status_rng = f"Q{first_combo}:R{last_combo}"
ws.conditional_formatting.add(
status_rng, CellIsRule(operator="equal", formula=['"CONFORM"'], fill=pass_fill)
)
ws.conditional_formatting.add(
status_rng, CellIsRule(operator="equal", formula=['"CANDIDAT"'], fill=pass_fill)
)
ws.conditional_formatting.add(
status_rng, CellIsRule(operator="equal", formula=['"CONT PIERDUT"'], fill=fail_fill)
)
ws.conditional_formatting.add(
status_rng, CellIsRule(operator="equal", formula=['"NU"'], fill=warn_fill)
)
top_title_row = row + 2
ws[f"A{top_title_row}"] = "TOP CANDIDATE"
ws[f"A{top_title_row}"].font = SUBTITLE_FONT
ws.merge_cells(f"A{top_title_row}:J{top_title_row}")
top_header_row = top_title_row + 1
top_headers = [
"#", "Fereastra", "Strategie", "N", "WR", "Expectancy R",
"Profit Factor", "Cum P&L $", "Max DD Prop $", "Status Edge",
]
for col_idx, header in enumerate(top_headers, start=1):
c = ws.cell(row=top_header_row, column=col_idx, value=header)
c.font = HEADER_FONT
c.fill = HEADER_FILL
c.alignment = CENTER
c.border = BORDER
for idx in range(1, 11):
r = top_header_row + idx
ws[f"A{r}"] = idx
if combo_rows:
rank_formula = f"LARGE($S${first_combo}:$S${last_combo},{idx})"
match_formula = f"MATCH({rank_formula},$S${first_combo}:$S${last_combo},0)"
for target, source in zip(
["B", "C", "D", "E", "F", "G", "H", "I", "J"],
["A", "D", "E", "G", "H", "J", "K", "N", "R"],
):
ws[f"{target}{r}"] = (
f'=IFERROR(IF({rank_formula}<0,"",'
f'INDEX(${source}${first_combo}:${source}${last_combo},{match_formula})),"")'
)
for c in range(1, len(top_headers) + 1):
cell = ws.cell(row=r, column=c)
cell.border = BORDER
cell.alignment = RIGHT if c not in (2, 3, 10) else CENTER
ws[f"E{r}"].number_format = "0.0%"
ws[f"F{r}"].number_format = "+0.000;-0.000;0.000"
ws[f"G{r}"].number_format = "0.00"
ws[f"H{r}"].number_format = '"$"#,##0.00'
ws[f"I{r}"].number_format = '"$"#,##0.00'
# Helper pentru a emite un block breakdown (per Sesiune / Strategie / etc.)
def _emit_breakdown(
start_row: int, title: str, first_col_label: str,
@@ -791,11 +1071,8 @@ def build_dashboard(wb: Workbook) -> None:
# Breakdowns — toate folosesc overlay-ul Hybrid+BE (recomandat de trader)
overlay = "hybrid_be"
start = 5 + len(metrics) + 2 # 2 rânduri spațiu după tabelul de metrici
after_sess = _emit_breakdown(
start, "PER SESIUNE (overlay: Hybrid + BE)", "Sesiune",
SESSIONS, _range("Sesiune"), overlay,
)
start = top_header_row + 13
after_sess = start
after_strat = _emit_breakdown(
after_sess + 2, "PER STRATEGIE (overlay: Hybrid + BE)", "Strategie",
STRATEGIES, _range("Strategie"), overlay,
@@ -959,7 +1236,11 @@ def build_dashboard(wb: Workbook) -> None:
ws.row_dimensions[r].height = 60
# Column widths
widths = {"A": 22, "B": 14, "C": 14, "D": 14, "E": 16, "F": 16, "G": 75}
widths = {
"A": 18, "B": 10, "C": 10, "D": 16, "E": 8, "F": 8, "G": 10,
"H": 13, "I": 13, "J": 12, "K": 13, "L": 15, "M": 20,
"N": 18, "O": 13, "P": 14, "Q": 15, "R": 13, "S": 8,
}
for col, w in widths.items():
ws.column_dimensions[col].width = w
@@ -989,7 +1270,7 @@ def build_dashboard(wb: Workbook) -> None:
min_row=2, max_row=MAX_ROWS + 1,
)
chart.set_categories(cats)
ws.add_chart(chart, "H4")
ws.add_chart(chart, "U4")
# Equity curve prop — al doilea chart, separat de modelul abstract
chart_prop = LineChart()
@@ -1009,7 +1290,7 @@ def build_dashboard(wb: Workbook) -> None:
)
chart_prop.add_data(data_prop, titles_from_data=True)
chart_prop.set_categories(cats)
ws.add_chart(chart_prop, "H30")
ws.add_chart(chart_prop, "U30")
# ---------------------------------------------------------------------------
@@ -1030,6 +1311,11 @@ def build_workbook() -> Workbook:
def main() -> int:
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
if OUTPUT.exists():
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
backup = OUTPUT.with_name(f"{OUTPUT.stem}.backup-{timestamp}{OUTPUT.suffix}")
shutil.copy2(OUTPUT, backup)
print(f"Backup {backup}")
wb = build_workbook()
wb.save(OUTPUT)
print(f"Wrote {OUTPUT}")