- 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>
1650 lines
66 KiB
Python
1650 lines
66 KiB
Python
"""Generator pentru data/backtest.xlsx.
|
||
|
||
5 strategii de management comparate side-by-side pe semnale blackbox:
|
||
- TP0 only : 100% close la TP0
|
||
- TP1 only : 100% OCO la SL/TP1
|
||
- TP2 only : 100% OCO la SL/TP2
|
||
- Hybrid + BE : 50% TP0 + mut SL la BE + 50% TP1 (recomandat de trader)
|
||
- Hybrid no BE : 50% TP0 + 50% TP1, fără BE (control pentru a izola valoarea BE-ului)
|
||
|
||
Rulare:
|
||
pip install openpyxl
|
||
python scripts/generate_template.py
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import shutil
|
||
from datetime import date, datetime, time, timedelta
|
||
from pathlib import Path
|
||
|
||
from openpyxl import Workbook
|
||
from openpyxl.chart import LineChart, Reference
|
||
from openpyxl.formatting.rule import CellIsRule
|
||
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
||
from openpyxl.utils import get_column_letter
|
||
from openpyxl.worksheet.datavalidation import DataValidation
|
||
|
||
|
||
OUTPUT = Path(__file__).resolve().parent.parent / "data" / "backtest.xlsx"
|
||
MAX_ROWS = 500 # rânduri pre-completate cu formule în sheet-ul Trades
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Styles
|
||
# ---------------------------------------------------------------------------
|
||
|
||
HEADER_FILL = PatternFill("solid", fgColor="1F3864")
|
||
HEADER_FONT = Font(name="Calibri", size=11, bold=True, color="FFFFFF")
|
||
INPUT_FILL = PatternFill("solid", fgColor="FFF8E1")
|
||
DERIVED_FILL = PatternFill("solid", fgColor="E8F1FA")
|
||
HIDDEN_FILL = PatternFill("solid", fgColor="F0F0F0")
|
||
TITLE_FONT = Font(name="Calibri", size=16, bold=True, color="1F3864")
|
||
SUBTITLE_FONT = Font(name="Calibri", size=12, bold=True, color="1F3864")
|
||
THIN = Side(border_style="thin", color="BFBFBF")
|
||
BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
|
||
CENTER = Alignment(horizontal="center", vertical="center")
|
||
LEFT = Alignment(horizontal="left", vertical="center")
|
||
RIGHT = Alignment(horizontal="right", vertical="center")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Lists
|
||
# ---------------------------------------------------------------------------
|
||
|
||
STRATEGIES = ["M2D", "EMA cross", "Order block", "Liquidity sweep", "Custom"]
|
||
SESSIONS = ["A1", "A2", "A3", "B", "C", "D", "Other"]
|
||
INDICATORS = ["DIA", "US30", "SPY", "QQQ", "ES", "NQ"]
|
||
TIMEFRAMES = ["1min", "3min", "15min"]
|
||
DIRECTIONS = ["Buy", "Sell"]
|
||
OUTCOMES = ["SL", "TP0", "TP1", "TP2"]
|
||
|
||
# Cele 5 strategii de management (sufix folosit în numele coloanelor) + label friendly
|
||
STRAT_KEYS = ["tp0only", "tp1only", "tp2only", "hybrid_be", "hybrid_nobe"]
|
||
STRAT_LABELS = {
|
||
"tp0only": "TP0 only",
|
||
"tp1only": "TP1 only",
|
||
"tp2only": "TP2 only",
|
||
"hybrid_be": "Hybrid + BE",
|
||
"hybrid_nobe": "Hybrid no BE",
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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",
|
||
"Direcție", "SL %", "TP0 %", "TP1 %", "TP2 %",
|
||
"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]
|
||
+ [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]
|
||
+ [f"DD_{s}" for s in STRAT_KEYS]
|
||
+ [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
|
||
|
||
# Mapă nume → literă coloană Excel
|
||
COL = {name: get_column_letter(i + 1) for i, name in enumerate(TRADES_HEADERS)}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _col_to_int(letter: str) -> int:
|
||
n = 0
|
||
for ch in letter:
|
||
n = n * 26 + (ord(ch) - ord("A") + 1)
|
||
return n
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Config sheet
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def build_config(wb: Workbook) -> None:
|
||
ws = wb.create_sheet("Config", 0)
|
||
ws.sheet_view.showGridLines = False
|
||
|
||
ws["A1"] = "Config — editează doar celulele galbene"
|
||
ws["A1"].font = TITLE_FONT
|
||
ws.merge_cells("A1:C1")
|
||
|
||
ws["A3"] = "Setting"
|
||
ws["B3"] = "Value"
|
||
ws["C3"] = "Note"
|
||
for c in ("A3", "B3", "C3"):
|
||
ws[c].font = HEADER_FONT
|
||
ws[c].fill = HEADER_FILL
|
||
ws[c].alignment = CENTER
|
||
|
||
ws["A4"] = "Account Size Start ($)"
|
||
ws["B4"] = 10000
|
||
ws["C4"] = "Balanța inițială pentru calcule $ și HWM (model abstract)"
|
||
|
||
ws["A5"] = "Risk reper (%)"
|
||
ws["B5"] = 1.0
|
||
ws["C5"] = "Reper opțional; $_* se calculează din SL% × Account Size Start"
|
||
|
||
ws["A6"] = "Risk reper ($)"
|
||
ws["B6"] = "=B4*B5/100"
|
||
ws["C6"] = "Auto — informativ; nu este folosit în formulele $_*"
|
||
|
||
for r in (4, 5):
|
||
ws.cell(row=r, column=2).fill = INPUT_FILL
|
||
ws.cell(row=r, column=2).border = BORDER
|
||
ws["B6"].fill = DERIVED_FILL
|
||
ws["B6"].border = BORDER
|
||
ws["B4"].number_format = "$#,##0"
|
||
ws["B5"].number_format = '0.0"%"'
|
||
ws["B6"].number_format = "$#,##0.00"
|
||
|
||
# ---- Bloc Cont Prop Firm (separat de modelul abstract de mai sus) ----
|
||
ws["A8"] = "Cont Prop Firm"
|
||
ws["A8"].font = SUBTITLE_FONT
|
||
ws.merge_cells("A8:C8")
|
||
|
||
ws["A9"] = "Account Prop Start ($)"
|
||
ws["B9"] = 50000
|
||
ws["C9"] = "Balanța contului de prop"
|
||
|
||
ws["A10"] = "Contracte per trade"
|
||
ws["B10"] = 1
|
||
ws["C10"] = "Număr de contracte tranzacționate per semnal (TradeLocker)"
|
||
|
||
ws["A11"] = "$ per 1% per contract"
|
||
ws["B11"] = 10000
|
||
ws["C11"] = "Pe DIA: 0.10% = $1000 ⇒ 1% = $10,000 (1 contract notional ≈ $1M)"
|
||
|
||
ws["A12"] = "Daily Loss Limit (%)"
|
||
ws["B12"] = 4.0
|
||
ws["C12"] = "Limită zilnică prop firm; depășire = cont mort"
|
||
|
||
ws["A13"] = "Daily Loss Limit ($)"
|
||
ws["B13"] = "=B9*B12/100"
|
||
ws["C13"] = "Auto — derivat din B9 și B12"
|
||
|
||
ws["A14"] = "Max Loss Limit (%)"
|
||
ws["B14"] = 7.0
|
||
ws["C14"] = "Limită totală pe cont; depășire = cont mort"
|
||
|
||
ws["A15"] = "Max Loss Limit ($)"
|
||
ws["B15"] = "=B9*B14/100"
|
||
ws["C15"] = "Auto — derivat din B9 și B14"
|
||
|
||
for r in (9, 10, 11, 12, 14): # inputuri galbene
|
||
ws.cell(row=r, column=2).fill = INPUT_FILL
|
||
ws.cell(row=r, column=2).border = BORDER
|
||
for r in (13, 15): # derived blue
|
||
ws.cell(row=r, column=2).fill = DERIVED_FILL
|
||
ws.cell(row=r, column=2).border = BORDER
|
||
|
||
ws["B9"].number_format = "$#,##0"
|
||
ws["B10"].number_format = "0"
|
||
ws["B11"].number_format = "$#,##0"
|
||
ws["B12"].number_format = '0.0"%"'
|
||
ws["B13"].number_format = "$#,##0"
|
||
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),
|
||
("Sesiuni (auto)", SESSIONS),
|
||
("Indicatori", INDICATORS),
|
||
("TF", TIMEFRAMES),
|
||
("Direcție", DIRECTIONS),
|
||
("Outcome", OUTCOMES),
|
||
]
|
||
for col_idx, (label, values) in enumerate(list_columns, start=5):
|
||
cell = ws.cell(row=3, column=col_idx, value=label)
|
||
cell.font = HEADER_FONT
|
||
cell.fill = HEADER_FILL
|
||
cell.alignment = CENTER
|
||
for row_idx, v in enumerate(values, start=4):
|
||
c = ws.cell(row=row_idx, column=col_idx, value=v)
|
||
c.alignment = CENTER
|
||
|
||
widths = {
|
||
"A": 24, "B": 14, "C": 38, "D": 2,
|
||
"E": 14, "F": 14, "G": 13, "H": 10, "I": 10, "J": 12,
|
||
}
|
||
for col, w in widths.items():
|
||
ws.column_dimensions[col].width = w
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Formula builders pentru Trades sheet
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _f_day(r: int) -> str:
|
||
d = f'{COL["Data"]}{r}'
|
||
return (
|
||
f'=IF({d}="","",'
|
||
f'CHOOSE(WEEKDAY({d},2),"Lu","Ma","Mi","Jo","Vi","Sa","Du"))'
|
||
)
|
||
|
||
|
||
def _f_session(r: int) -> str:
|
||
"""Derivă Sesiunea M2D din Data + Ora RO."""
|
||
d = f'{COL["Data"]}{r}'
|
||
t = f'{COL["Ora RO"]}{r}'
|
||
wd = f"WEEKDAY({d},2)"
|
||
mid_week = f"AND({wd}>=2,{wd}<=4)"
|
||
return (
|
||
f'=IF(OR({d}="",{t}=""),"",'
|
||
f"IF(OR({wd}=1,{wd}=5),\"D\","
|
||
f'IF(AND({t}>=TIME(15,30,0),{t}<TIME(16,30,0)),"C",'
|
||
f'IF(AND({mid_week},{t}>=TIME(16,35,0),{t}<TIME(17,0,0)),"A1",'
|
||
f'IF(AND({mid_week},{t}>=TIME(17,0,0),{t}<TIME(18,0,0)),"A2",'
|
||
f'IF(AND({mid_week},{t}>=TIME(18,0,0),{t}<TIME(19,0,0)),"A3",'
|
||
f'IF(AND({mid_week},{t}>=TIME(22,0,0),{t}<TIME(22,45,0)),"B",'
|
||
f'"Other")))))))'
|
||
)
|
||
|
||
|
||
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}'
|
||
tp0 = f'{COL["TP0 %"]}{r}'
|
||
return f'=IF({o}="","",IF({o}="SL",-1,{tp0}/{sl}))'
|
||
|
||
|
||
def _f_r_tp1only(r: int) -> str:
|
||
o = f'{COL["Outcome"]}{r}'
|
||
sl = f'{COL["SL %"]}{r}'
|
||
tp1 = f'{COL["TP1 %"]}{r}'
|
||
return (
|
||
f'=IF({o}="","",'
|
||
f'IF(OR({o}="SL",{o}="TP0"),-1,{tp1}/{sl}))'
|
||
)
|
||
|
||
|
||
def _f_r_tp2only(r: int) -> str:
|
||
o = f'{COL["Outcome"]}{r}'
|
||
sl = f'{COL["SL %"]}{r}'
|
||
tp2 = f'{COL["TP2 %"]}{r}'
|
||
return f'=IF({o}="","",IF({o}="TP2",{tp2}/{sl},-1))'
|
||
|
||
|
||
def _f_r_hybrid_be(r: int) -> str:
|
||
o = f'{COL["Outcome"]}{r}'
|
||
sl = f'{COL["SL %"]}{r}'
|
||
tp0 = f'{COL["TP0 %"]}{r}'
|
||
tp1 = f'{COL["TP1 %"]}{r}'
|
||
return (
|
||
f'=IF({o}="","",'
|
||
f'IF({o}="SL",-1,'
|
||
f'IF({o}="TP0",0.5*{tp0}/{sl},'
|
||
f'0.5*({tp0}+{tp1})/{sl})))'
|
||
)
|
||
|
||
|
||
def _f_r_hybrid_nobe(r: int) -> str:
|
||
o = f'{COL["Outcome"]}{r}'
|
||
sl = f'{COL["SL %"]}{r}'
|
||
tp0 = f'{COL["TP0 %"]}{r}'
|
||
tp1 = f'{COL["TP1 %"]}{r}'
|
||
return (
|
||
f'=IF({o}="","",'
|
||
f'IF({o}="SL",-1,'
|
||
f'IF({o}="TP0",0.5*{tp0}/{sl}-0.5,'
|
||
f'0.5*({tp0}+{tp1})/{sl})))'
|
||
)
|
||
|
||
|
||
R_FN: dict[str, callable] = {
|
||
"tp0only": _f_r_tp0only,
|
||
"tp1only": _f_r_tp1only,
|
||
"tp2only": _f_r_tp2only,
|
||
"hybrid_be": _f_r_hybrid_be,
|
||
"hybrid_nobe": _f_r_hybrid_nobe,
|
||
}
|
||
|
||
|
||
def _f_dollar(r: int, r_col: str) -> str:
|
||
"""$ P&L per trade = R × SL% × Contracte × $/1% per contract (TradeLocker real)."""
|
||
rc = f"{COL[r_col]}{r}"
|
||
sl = f"{COL['SL %']}{r}"
|
||
return f'=IF({rc}="","",{rc}*{sl}*Config!$B$10*Config!$B$11)'
|
||
|
||
|
||
def _f_sl_dollar(r: int) -> str:
|
||
"""SL $ = SL% × Contracte × $/1% per contract."""
|
||
sl = f"{COL['SL %']}{r}"
|
||
return f'=IF({sl}="","",{sl}*Config!$B$10*Config!$B$11)'
|
||
|
||
|
||
def _f_sl_dollar_prop(r: int) -> str:
|
||
"""SL $ pe contul de prop — același cont real, formula identică cu SL $."""
|
||
sl = f"{COL['SL %']}{r}"
|
||
return f'=IF({sl}="","",{sl}*Config!$B$10*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}))'
|
||
|
||
|
||
def _f_win(r: int, r_col: str) -> str:
|
||
rc = f"{COL[r_col]}{r}"
|
||
return f'=IF({rc}="","",IF({rc}>0,1,0))'
|
||
|
||
|
||
def _f_peak(r: int, balance_col: str, peak_col: str) -> str:
|
||
bc = COL[balance_col]
|
||
pc = COL[peak_col]
|
||
if r == 2:
|
||
return f'=IF({bc}{r}="","",{bc}{r})'
|
||
return (
|
||
f'=IF({bc}{r}="","",'
|
||
f'IF({pc}{r-1}="",{bc}{r},MAX({pc}{r-1},{bc}{r})))'
|
||
)
|
||
|
||
|
||
def _f_drawdown(r: int, peak_col: str, balance_col: str) -> str:
|
||
pc = f"{COL[peak_col]}{r}"
|
||
bc = f"{COL[balance_col]}{r}"
|
||
return f'=IF({bc}="","",{pc}-{bc})'
|
||
|
||
|
||
def _f_dollar_prop(r: int, r_col: str) -> str:
|
||
"""$ P&L pe contul de prop — același calcul ca _f_dollar (cont real TradeLocker).
|
||
|
||
Diferența între cont abstract și prop e doar balanța de start; $-ul per trade
|
||
e identic pentru că reflectă realitatea contractelor tranzacționate.
|
||
"""
|
||
rc = f"{COL[r_col]}{r}"
|
||
sl = f"{COL['SL %']}{r}"
|
||
return f'=IF({rc}="","",{rc}*{sl}*Config!$B$10*Config!$B$11)'
|
||
|
||
|
||
def _f_balance_prop(r: int, dollar_col: str) -> str:
|
||
dc = COL[dollar_col]
|
||
return f'=IF({dc}{r}="","",Config!$B$9 + SUM(${dc}$2:{dc}{r}))'
|
||
|
||
|
||
def _f_daily_pl(r: int, dollar_col: str) -> str:
|
||
"""Cumul P&L pe ziua curentă (până la rândul r inclusiv)."""
|
||
dc = COL[dollar_col]
|
||
d_col = COL["Data"]
|
||
d = f"{d_col}{r}"
|
||
return (
|
||
f'=IF(OR({dc}{r}="",{d}=""),"",'
|
||
f'SUMIFS(${dc}$2:{dc}{r},${d_col}$2:{d_col}{r},{d}))'
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Trades sheet
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def build_trades(wb: Workbook) -> None:
|
||
ws = wb.create_sheet("Trades", 1)
|
||
ws.sheet_view.showGridLines = False
|
||
ws.freeze_panes = "B2"
|
||
|
||
# Headers
|
||
for col_idx, header in enumerate(TRADES_HEADERS, start=1):
|
||
cell = ws.cell(row=1, column=col_idx, value=header)
|
||
cell.font = HEADER_FONT
|
||
cell.fill = HEADER_FILL
|
||
cell.alignment = CENTER
|
||
cell.border = BORDER
|
||
|
||
# Formule pe toate rândurile pre-pregătite
|
||
for r in range(2, MAX_ROWS + 2):
|
||
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)
|
||
ws[f'{COL[f"$_{strat}"]}{r}'] = _f_dollar(r, f"R_{strat}")
|
||
ws[f'{COL[f"Bal_{strat}"]}{r}'] = _f_balance(r, f"$_{strat}")
|
||
ws[f'{COL[f"Win_{strat}"]}{r}'] = _f_win(r, f"R_{strat}")
|
||
ws[f'{COL[f"Peak_{strat}"]}{r}'] = _f_peak(
|
||
r, f"Bal_{strat}", f"Peak_{strat}"
|
||
)
|
||
ws[f'{COL[f"DD_{strat}"]}{r}'] = _f_drawdown(
|
||
r, f"Peak_{strat}", f"Bal_{strat}"
|
||
)
|
||
# Prop firm tracking — paralel cu modelul abstract
|
||
ws[f'{COL[f"$Prop_{strat}"]}{r}'] = _f_dollar_prop(r, f"R_{strat}")
|
||
ws[f'{COL[f"BalProp_{strat}"]}{r}'] = _f_balance_prop(r, f"$Prop_{strat}")
|
||
ws[f'{COL[f"DailyPL_{strat}"]}{r}'] = _f_daily_pl(r, f"$Prop_{strat}")
|
||
ws[f'{COL[f"PeakProp_{strat}"]}{r}'] = _f_peak(
|
||
r, f"BalProp_{strat}", f"PeakProp_{strat}"
|
||
)
|
||
ws[f'{COL[f"DDProp_{strat}"]}{r}'] = _f_drawdown(
|
||
r, f"PeakProp_{strat}", f"BalProp_{strat}"
|
||
)
|
||
|
||
# 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 %"):
|
||
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"
|
||
for prefix in (
|
||
"$_", "Bal_", "Peak_", "DD_",
|
||
"$Prop_", "BalProp_", "DailyPL_", "PeakProp_", "DDProp_",
|
||
):
|
||
ws[f"{COL[f'{prefix}{strat}']}{r}"].number_format = '"$"#,##0.00'
|
||
|
||
for r in range(2, MAX_ROWS + 2):
|
||
ws[f"B{r}"].number_format = "yyyy-mm-dd"
|
||
|
||
# Coloring
|
||
input_letters = {
|
||
COL[n]
|
||
for n in (
|
||
"Data", "Ora RO", "Strategie", "Indicator", "TF",
|
||
"Direcție", "SL %", "TP0 %", "TP1 %", "TP2 %",
|
||
"Outcome", "Notes",
|
||
)
|
||
}
|
||
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}"])
|
||
helper_letters = set()
|
||
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:
|
||
ws[f"{cl}{r}"].fill = INPUT_FILL
|
||
for cl in derived_letters:
|
||
ws[f"{cl}{r}"].fill = DERIVED_FILL
|
||
for cl in helper_letters:
|
||
ws[f"{cl}{r}"].fill = HIDDEN_FILL
|
||
|
||
# Column widths
|
||
widths = {
|
||
"A": 5, "B": 12, "C": 9, "D": 5, "E": 9,
|
||
"F": 12, "G": 11, "H": 8, "I": 9,
|
||
"J": 9, "K": 9, "L": 9, "M": 9,
|
||
"N": 11, "O": 28,
|
||
}
|
||
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 (
|
||
"R_", "$_", "Bal_", "Win_", "Peak_", "DD_",
|
||
"$Prop_", "BalProp_", "DailyPL_", "PeakProp_", "DDProp_",
|
||
):
|
||
ws.column_dimensions[COL[f"{prefix}{strat}"]].width = 11
|
||
|
||
# Ascund helper-ele prop firm într-un outline collapsible
|
||
for strat in STRAT_KEYS:
|
||
for prefix in ("DailyPL_", "PeakProp_", "DDProp_"):
|
||
cl = COL[f"{prefix}{strat}"]
|
||
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]
|
||
dv = DataValidation(
|
||
type="list", formula1=source,
|
||
allow_blank=True, showErrorMessage=True,
|
||
)
|
||
dv.error = "Valoare invalidă — folosește dropdown-ul."
|
||
dv.errorTitle = "Input invalid"
|
||
dv.add(f"{cl}2:{cl}{MAX_ROWS + 1}")
|
||
ws.add_data_validation(dv)
|
||
|
||
# Config columns: E=Strategii, F=Sesiuni, G=Indicatori, H=TF, I=Direcție, J=Outcome
|
||
_add_dv("Strategie", "=Config!$E$4:$E$8")
|
||
_add_dv("Indicator", "=Config!$G$4:$G$9")
|
||
_add_dv("TF", "=Config!$H$4:$H$6")
|
||
_add_dv("Direcție", "=Config!$I$4:$I$5")
|
||
_add_dv("Outcome", "=Config!$J$4:$J$7")
|
||
|
||
# Conditional formatting pe coloanele R (5 strategii)
|
||
green_fill = PatternFill("solid", fgColor="C6EFCE")
|
||
red_fill = PatternFill("solid", fgColor="FFC7CE")
|
||
grey_fill = PatternFill("solid", fgColor="D9D9D9")
|
||
for strat in STRAT_KEYS:
|
||
cl = COL[f"R_{strat}"]
|
||
rng = f"{cl}2:{cl}{MAX_ROWS + 1}"
|
||
ws.conditional_formatting.add(
|
||
rng, CellIsRule(operator="greaterThan", formula=["0"], fill=green_fill)
|
||
)
|
||
ws.conditional_formatting.add(
|
||
rng, CellIsRule(operator="lessThan", formula=["0"], fill=red_fill)
|
||
)
|
||
ws.conditional_formatting.add(
|
||
rng, CellIsRule(operator="equal", formula=["0"], fill=grey_fill)
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Dashboard sheet
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _range(col_name: str) -> str:
|
||
cl = COL[col_name]
|
||
return f"Trades!${cl}$2:${cl}${MAX_ROWS + 1}"
|
||
|
||
|
||
METRIC_HINTS: dict[str, str] = {
|
||
"Trades Placed": (
|
||
"Câte trade-uri ai logat în total.\n"
|
||
"Cu cât N e mai mare, cu atât celelalte metrici sunt mai de încredere.\n"
|
||
"Exemplu: la N=10 Win Ratio e zgomot pur, la N=40 începe să aibă semnal, la N=100 e solid."
|
||
),
|
||
"Wins": (
|
||
"Câte trade-uri s-au închis pe plus (R > 0).\n"
|
||
"Singur nu spune nimic — privește-l raportat la total (vezi Win Ratio mai jos)."
|
||
),
|
||
"Win Ratio": (
|
||
"Procentul de trade-uri câștigătoare. WR = 60% înseamnă 6 wins din 10 trade-uri.\n"
|
||
"Singur NU spune dacă strategia e profitabilă — citește-l împreună cu R:R de pe rândul următor."
|
||
),
|
||
"Average Win ($)": (
|
||
"Câștigul mediu pe trade-urile pozitive.\n"
|
||
"Comparat cu Average Loss îți spune cât de mari sunt câștigurile vs pierderile.\n"
|
||
"Exemplu: 4 wins de $50 și 2 wins de $80 — Average Win = $60."
|
||
),
|
||
"Average Loss ($)": (
|
||
"Pierderea medie pe trade-urile negative (cifra apare cu minus).\n"
|
||
"În dolari reali, −1R depinde de SL%: pierdere ≈ SL% × Contracte × $/1% per contract.\n"
|
||
"Dacă e mult mai mare decât riscul calculat din SL, ai SL-uri sărite (slippage, gap-uri)."
|
||
),
|
||
"Best Trade ($)": (
|
||
"Cel mai mare câștig individual.\n"
|
||
"Dacă majoritatea profitului total vine dintr-un singur trade outlier, edge-ul e fragil — "
|
||
"elimini acel trade și strategia devine pierzătoare."
|
||
),
|
||
"Worst Trade ($)": (
|
||
"Cea mai mare pierdere individuală.\n"
|
||
"Ar trebui să fie aproximativ egală cu −1R calculat din SL% × Contracte × $/1% per contract.\n"
|
||
"Pe TradeLocker DIA: SL=0.30%, 1 contract → ≈ −$3000. Dacă e mai mare, ai slippage/gap."
|
||
),
|
||
"Profit Factor": (
|
||
"Total bani câștigați împărțit la total bani pierduți (în valoare absolută).\n"
|
||
"Sub 1.0 = pierzi pe ansamblu. Peste 1.5 = solid. Peste 2.0 = câștigi de 2× cât pierzi.\n"
|
||
"Exemplu: 4 wins de $50 (= $200) + 6 losses de $30 (= $180) — PF = 200÷180 = 1.11, profitabil marginal."
|
||
),
|
||
"Risk:Reward": (
|
||
"De câte ori e mai mare câștigul mediu decât pierderea medie.\n"
|
||
"R:R = 2 înseamnă: când câștigi, câștigi $2; când pierzi, pierzi $1.\n"
|
||
"Cu R:R mare poți avea Win Ratio mic și tot să faci bani."
|
||
),
|
||
"Expectancy (R)": (
|
||
"Cât câștigi în medie pe UN trade, exprimat în R.\n"
|
||
"+0.30R = câștigi 0.30 × riscul mediu al trade-urilor.\n"
|
||
"−0.10R = pierzi 0.10 × riscul mediu al trade-urilor.\n"
|
||
"Pragul de GO LIVE: +0.20R sau mai mult."
|
||
),
|
||
"Expectancy ($)": (
|
||
"Aceeași expectancy convertită în dolari, folosind SL% × Contracte × $/1% per contract.\n"
|
||
"Util ca să vezi cât câștigi în medie pe trade în bani reali (TradeLocker), nu doar în R."
|
||
),
|
||
"Cumulative P&L ($)": (
|
||
"Suma profitului și pierderii pe toate trade-urile logate.\n"
|
||
"E ce-ai avea în plus (sau minus) față de balanța de start din Config."
|
||
),
|
||
"HWM Balance ($)": (
|
||
"Highest Watermark — cea mai mare balanță atinsă vreodată în jurnal.\n"
|
||
"Punct de referință pentru calculul drawdown-ului."
|
||
),
|
||
"Max Drawdown ($)": (
|
||
"Cea mai mare cădere ($) din vârf la fundul ulterior al balanței. Măsoară durerea psihologică maximă.\n"
|
||
"Exemplu: ai urcat la $11,500, ai coborât la $9,800 — DD = $1,700, adică 17% din peak.\n"
|
||
"Un drawdown mare la backtest e foarte greu de tolerat în live cu bani reali — așteaptă-te să renunți."
|
||
),
|
||
# ---- Prop firm metrics ----
|
||
"Account Prop Start ($)": (
|
||
"Capitalul de start al contului de prop firm (default $50,000).\n"
|
||
"Editabil în Config B9."
|
||
),
|
||
"Position Size ($)": (
|
||
"Configurare contract real TradeLocker:\n"
|
||
" • Contracte per trade (Config B10) — câte contracte tranzacționezi pe semnal.\n"
|
||
" • $ per 1% per contract (Config B11) — pe DIA: 0.10% = $1000 → 1% = $10,000.\n"
|
||
"Pierderea pe SL = SL% × Contracte × $/1% per contract. Pentru SL=0.30%, 1 contract → $3000."
|
||
),
|
||
"Cumulative P&L Prop ($)": (
|
||
"Profitul total al contului de prop pe traseul logat.\n"
|
||
"Reflectă $ real (SL% × Contracte × $/1% per contract), nu un procent abstract din cont.\n"
|
||
"Adunat peste $50,000 dă balanța finală reală."
|
||
),
|
||
"Final Balance Prop ($)": (
|
||
"Balanța finală a contului de prop = $50,000 + Cumulative P&L Prop.\n"
|
||
"Compar-o cu pragul de stop-out al firmei de prop: $50,000 − $3,500 = $46,500."
|
||
),
|
||
"Worst Daily Loss ($)": (
|
||
"Cea mai proastă pierdere cumulativă într-o zi calendaristică.\n"
|
||
"Dacă e mai mică decât −$2,000, ai depășit Daily Loss Limit într-o zi — cont mort.\n"
|
||
"Atenție: un singur breach = pierdere cont, indiferent dacă ai recuperat ulterior."
|
||
),
|
||
"Daily Limit Status": (
|
||
"PASS dacă nicio zi nu a depășit Daily Loss Limit ($2,000 default).\n"
|
||
"FAIL = strategia ar fi pierdut contul prin daily breach pe traseul logat."
|
||
),
|
||
"Max Account Drawdown ($)": (
|
||
"Cea mai mare cădere de la peak pe contul de prop.\n"
|
||
"Dacă > $3,500 (7% din $50k), ai depășit Max Loss Limit — cont mort."
|
||
),
|
||
"Max Loss Status": (
|
||
"PASS dacă Max Account Drawdown ≤ $3,500.\n"
|
||
"FAIL = strategia ar fi pierdut contul prin drawdown cumulativ."
|
||
),
|
||
"Overall Prop Status": (
|
||
"CONFORM = strategia ar fi supraviețuit pe contul de prop pe traseul logat.\n"
|
||
"CONT PIERDUT = cel puțin o breach (daily sau max) — strategia nu e viabilă pe acest cont prop."
|
||
),
|
||
}
|
||
|
||
|
||
def build_dashboard(wb: Workbook) -> None:
|
||
ws = wb.create_sheet("Dashboard", 2)
|
||
ws.sheet_view.showGridLines = False
|
||
|
||
ws["A1"] = "Backtest Dashboard"
|
||
ws["A1"].font = TITLE_FONT
|
||
ws.merge_cells("A1:G1")
|
||
|
||
ws["A2"] = (
|
||
"Comparație 5 strategii management — pe aceleași semnale blackbox"
|
||
)
|
||
ws["A2"].font = Font(name="Calibri", size=10, italic=True, color="595959")
|
||
ws.merge_cells("A2:G2")
|
||
|
||
# Row 4: headers (5 columns B-F pentru strategii + G pentru "Cum citesc")
|
||
ws["A4"] = "Metric"
|
||
strat_cols = {} # strat_key → column letter (B/C/D/E/F)
|
||
for i, strat in enumerate(STRAT_KEYS):
|
||
letter = get_column_letter(2 + i)
|
||
strat_cols[strat] = letter
|
||
ws[f"{letter}4"] = STRAT_LABELS[strat]
|
||
ws["G4"] = "Cum citesc"
|
||
for letter in ["A"] + list(strat_cols.values()) + ["G"]:
|
||
c = ws[f"{letter}4"]
|
||
c.font = HEADER_FONT
|
||
c.fill = HEADER_FILL
|
||
c.alignment = CENTER
|
||
c.border = BORDER
|
||
|
||
# Ranges per strategie
|
||
R = {s: _range(f"R_{s}") for s in STRAT_KEYS}
|
||
D = {s: _range(f"$_{s}") for s in STRAT_KEYS}
|
||
W = {s: _range(f"Win_{s}") for s in STRAT_KEYS}
|
||
BAL = {s: _range(f"Bal_{s}") for s in STRAT_KEYS}
|
||
DD = {s: _range(f"DD_{s}") for s in STRAT_KEYS}
|
||
OUTCOME_RANGE = _range("Outcome")
|
||
|
||
# Metric rows — fiecare metric e un dict cu per-strategy formula + format
|
||
metrics: list[tuple[str, callable, str]] = [
|
||
# (label, fn(strat_key) -> formula, number_format)
|
||
("Trades Placed", lambda s: f'=COUNTA({OUTCOME_RANGE})', "0"),
|
||
("Wins", lambda s: f'=COUNTIF({W[s]},1)', "0"),
|
||
("Average Win ($)", lambda s: f'=IFERROR(AVERAGEIF({D[s]},">0"),0)', '"$"#,##0.00'),
|
||
("Average Loss ($)", lambda s: f'=IFERROR(AVERAGEIF({D[s]},"<0"),0)', '"$"#,##0.00'),
|
||
("Best Trade ($)", lambda s: f'=IFERROR(MAX({D[s]}),0)', '"$"#,##0.00'),
|
||
("Worst Trade ($)", lambda s: f'=IFERROR(MIN({D[s]}),0)', '"$"#,##0.00'),
|
||
("Profit Factor", lambda s: f'=IFERROR(SUMIF({D[s]},">0")/ABS(SUMIF({D[s]},"<0")),0)', "0.00"),
|
||
# Win Ratio: depends on Wins + Trades Placed — handled after metrics list (placeholder)
|
||
("Win Ratio", lambda s: None, "0.0%"),
|
||
# Risk:Reward — placeholder; bazat pe rândurile Avg Win/Loss
|
||
("Risk:Reward", lambda s: None, "0.00"),
|
||
("Expectancy (R)", lambda s: f'=IFERROR(AVERAGE({R[s]}),0)', "+0.000;-0.000;0.000"),
|
||
("Expectancy ($)", lambda s: f'=IFERROR(AVERAGE({D[s]}),0)', '"$"#,##0.00'),
|
||
("Cumulative P&L ($)", lambda s: f'=SUM({D[s]})', '"$"#,##0.00'),
|
||
# HWM — placeholder cu ref la Trades Placed (row 5)
|
||
("HWM Balance ($)", lambda s: None, '"$"#,##0.00'),
|
||
("Max Drawdown ($)", lambda s: f'=IFERROR(MAX({DD[s]}),0)', '"$"#,##0.00'),
|
||
]
|
||
|
||
# Determine row indexes pentru formule speciale (depind de poziție)
|
||
label_to_row = {label: 5 + idx for idx, (label, _, _) in enumerate(metrics)}
|
||
trades_row = label_to_row["Trades Placed"]
|
||
wins_row = label_to_row["Wins"]
|
||
avg_win_row = label_to_row["Average Win ($)"]
|
||
avg_loss_row = label_to_row["Average Loss ($)"]
|
||
|
||
for idx, (label, fn, fmt) in enumerate(metrics):
|
||
r = 5 + idx
|
||
ws[f"A{r}"] = label
|
||
ws[f"A{r}"].font = Font(name="Calibri", size=11, bold=True)
|
||
ws[f"A{r}"].border = BORDER
|
||
ws[f"A{r}"].alignment = LEFT
|
||
for strat in STRAT_KEYS:
|
||
letter = strat_cols[strat]
|
||
if label == "Win Ratio":
|
||
formula = f"=IFERROR({letter}{wins_row}/{letter}{trades_row},0)"
|
||
elif label == "Risk:Reward":
|
||
formula = f"=IFERROR({letter}{avg_win_row}/ABS({letter}{avg_loss_row}),0)"
|
||
elif label == "HWM Balance ($)":
|
||
formula = (
|
||
f"=IF({letter}{trades_row}=0,Config!$B$4,MAX({BAL[strat]}))"
|
||
)
|
||
else:
|
||
formula = fn(strat)
|
||
cell = ws[f"{letter}{r}"]
|
||
cell.value = formula
|
||
cell.number_format = fmt
|
||
cell.fill = DERIVED_FILL
|
||
cell.border = BORDER
|
||
cell.alignment = RIGHT
|
||
# Coloana G — interpretare narativă + exemplu numeric
|
||
hint_cell = ws[f"G{r}"]
|
||
hint_cell.value = METRIC_HINTS.get(label, "")
|
||
hint_cell.font = Font(name="Calibri", size=10, color="595959")
|
||
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.
|
||
# 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}:{last_dash_col}{window_title_row}")
|
||
|
||
window_header_row = window_title_row + 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
|
||
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,
|
||
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)}
|
||
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"]
|
||
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_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:
|
||
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
|
||
|
||
# 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(DASH_WIN_HEADERS) + 1):
|
||
cell = ws.cell(row=r, column=c)
|
||
cell.border = BORDER
|
||
cell.alignment = CENTER if c in center_idx else RIGHT
|
||
if c not in no_fill_idx:
|
||
cell.fill = DERIVED_FILL
|
||
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"{R_LET}{first_combo}:{S_LET}{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=['"BREACH"'], fill=fail_fill)
|
||
)
|
||
ws.conditional_formatting.add(
|
||
status_rng, CellIsRule(operator="equal", formula=['"PRE-CANDIDAT"'], fill=warn_fill)
|
||
)
|
||
|
||
# ---- 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", "Filtru", "Strategie", "N", "WR", "Expectancy R",
|
||
"Profit Factor", "Cum P&L $", "Max DD Prop $", "Status Edge",
|
||
]
|
||
# 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"]
|
||
|
||
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})"
|
||
)
|
||
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 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,
|
||
)
|
||
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 $
|
||
bd_green = PatternFill("solid", fgColor="C6EFCE")
|
||
bd_red = PatternFill("solid", fgColor="FFC7CE")
|
||
|
||
# Helper pentru breakdown wide: rânduri = items, coloane = 5 strategii Cum $ + N total
|
||
def _emit_breakdown_strats(
|
||
start_row: int, title: str, first_col_label: str,
|
||
items: list[str], item_range: str,
|
||
) -> int:
|
||
# Layout: A=item, B..F=5 strategii (Cum $), G=N total
|
||
last_col_idx = 1 + len(STRAT_KEYS) + 1 # A + 5 strategii + N
|
||
last_letter = get_column_letter(last_col_idx)
|
||
ws[f"A{start_row}"] = title
|
||
ws[f"A{start_row}"].font = SUBTITLE_FONT
|
||
ws.merge_cells(f"A{start_row}:{last_letter}{start_row}")
|
||
headers = [first_col_label] + [STRAT_LABELS[s] for s in STRAT_KEYS] + ["N total"]
|
||
for col_idx, h in enumerate(headers, start=1):
|
||
c = ws.cell(row=start_row + 1, column=col_idx, value=h)
|
||
c.font = HEADER_FONT
|
||
c.fill = HEADER_FILL
|
||
c.alignment = CENTER
|
||
c.border = BORDER
|
||
strat_letters = [get_column_letter(2 + i) for i in range(len(STRAT_KEYS))]
|
||
n_letter = get_column_letter(last_col_idx)
|
||
for i, item in enumerate(items):
|
||
r = start_row + 2 + i
|
||
ws[f"A{r}"] = item
|
||
for idx, strat in enumerate(STRAT_KEYS):
|
||
cl = strat_letters[idx]
|
||
ws[f"{cl}{r}"] = f'=SUMIFS({D[strat]},{item_range},"{item}")'
|
||
ws[f"{cl}{r}"].number_format = '"$"#,##0.00'
|
||
ws[f"{n_letter}{r}"] = f'=COUNTIF({item_range},"{item}")'
|
||
ws[f"{n_letter}{r}"].number_format = "0"
|
||
for col_idx in range(1, last_col_idx + 1):
|
||
cell = ws.cell(row=r, column=col_idx)
|
||
cell.border = BORDER
|
||
cell.alignment = LEFT if col_idx == 1 else RIGHT
|
||
if 2 <= col_idx <= 1 + len(STRAT_KEYS):
|
||
cell.fill = DERIVED_FILL
|
||
# CF pe coloanele 5 strategii: verde >0, roșu <0
|
||
if items:
|
||
first_data_row = start_row + 2
|
||
last_data_row = start_row + 1 + len(items)
|
||
cf_rng = (
|
||
f"{strat_letters[0]}{first_data_row}:"
|
||
f"{strat_letters[-1]}{last_data_row}"
|
||
)
|
||
ws.conditional_formatting.add(
|
||
cf_rng, CellIsRule(operator="greaterThan", formula=["0"], fill=bd_green)
|
||
)
|
||
ws.conditional_formatting.add(
|
||
cf_rng, CellIsRule(operator="lessThan", formula=["0"], fill=bd_red)
|
||
)
|
||
return start_row + 1 + len(items)
|
||
|
||
# Breakdowns — toate cele 5 strategii vizibile, Cum P&L $ per strategie
|
||
start = after_top_prima + 2
|
||
after_strat = _emit_breakdown_strats(
|
||
start + 2, "PER STRATEGIE — Cum P&L $ per strategie", "Strategie",
|
||
STRATEGIES, _range("Strategie"),
|
||
)
|
||
after_ind = _emit_breakdown_strats(
|
||
after_strat + 2, "PER INDICATOR — Cum P&L $ per strategie", "Indicator",
|
||
INDICATORS, _range("Indicator"),
|
||
)
|
||
after_dir = _emit_breakdown_strats(
|
||
after_ind + 2, "PER DIRECȚIE — Cum P&L $ per strategie", "Direcție",
|
||
DIRECTIONS, _range("Direcție"),
|
||
)
|
||
|
||
# ---- PROP FIRM COMPLIANCE ----
|
||
PROP_RANGES = {
|
||
"dollar": {s: _range(f"$Prop_{s}") for s in STRAT_KEYS},
|
||
"balance": {s: _range(f"BalProp_{s}") for s in STRAT_KEYS},
|
||
"daily": {s: _range(f"DailyPL_{s}") for s in STRAT_KEYS},
|
||
"dd": {s: _range(f"DDProp_{s}") for s in STRAT_KEYS},
|
||
}
|
||
|
||
prop_title_row = after_dir + 2
|
||
ws[f"A{prop_title_row}"] = "PROP FIRM COMPLIANCE"
|
||
ws[f"A{prop_title_row}"].font = SUBTITLE_FONT
|
||
ws.merge_cells(f"A{prop_title_row}:G{prop_title_row}")
|
||
|
||
# Header pentru tabel
|
||
prop_header_row = prop_title_row + 1
|
||
ws[f"A{prop_header_row}"] = "Metric"
|
||
for strat in STRAT_KEYS:
|
||
ws[f"{strat_cols[strat]}{prop_header_row}"] = STRAT_LABELS[strat]
|
||
ws[f"G{prop_header_row}"] = "Cum citesc"
|
||
for letter in ["A"] + list(strat_cols.values()) + ["G"]:
|
||
c = ws[f"{letter}{prop_header_row}"]
|
||
c.font = HEADER_FONT
|
||
c.fill = HEADER_FILL
|
||
c.alignment = CENTER
|
||
c.border = BORDER
|
||
|
||
# Definițiile rândurilor — (label, formula_fn(strat), number_format)
|
||
fail_pass_fmt = "@"
|
||
prop_metrics: list[tuple[str, callable, str]] = [
|
||
(
|
||
"Account Prop Start ($)",
|
||
lambda s: "=Config!$B$9",
|
||
'"$"#,##0',
|
||
),
|
||
(
|
||
"Position Size ($)",
|
||
lambda s: "=Config!$B$11",
|
||
'"$"#,##0',
|
||
),
|
||
(
|
||
"Cumulative P&L Prop ($)",
|
||
lambda s: f"=SUM({PROP_RANGES['dollar'][s]})",
|
||
'"$"#,##0.00',
|
||
),
|
||
(
|
||
"Final Balance Prop ($)",
|
||
lambda s: f"=Config!$B$9+SUM({PROP_RANGES['dollar'][s]})",
|
||
'"$"#,##0.00',
|
||
),
|
||
(
|
||
"Worst Daily Loss ($)",
|
||
lambda s: f"=IFERROR(MIN({PROP_RANGES['daily'][s]}),0)",
|
||
'"$"#,##0.00',
|
||
),
|
||
# placeholder pentru Daily Status — depinde de Worst Daily de mai sus
|
||
("Daily Limit Status", lambda s: None, fail_pass_fmt),
|
||
(
|
||
"Max Account Drawdown ($)",
|
||
lambda s: f"=IFERROR(MAX({PROP_RANGES['dd'][s]}),0)",
|
||
'"$"#,##0.00',
|
||
),
|
||
# placeholder pentru Max Status — depinde de Max DD de mai sus
|
||
("Max Loss Status", lambda s: None, fail_pass_fmt),
|
||
# placeholder pentru Overall — depinde de cele două statuses
|
||
("Overall Prop Status", lambda s: None, fail_pass_fmt),
|
||
]
|
||
|
||
prop_label_to_row = {
|
||
label: prop_header_row + 1 + idx
|
||
for idx, (label, _, _) in enumerate(prop_metrics)
|
||
}
|
||
worst_daily_row = prop_label_to_row["Worst Daily Loss ($)"]
|
||
daily_status_row = prop_label_to_row["Daily Limit Status"]
|
||
max_dd_row = prop_label_to_row["Max Account Drawdown ($)"]
|
||
max_status_row = prop_label_to_row["Max Loss Status"]
|
||
|
||
for idx, (label, fn, fmt) in enumerate(prop_metrics):
|
||
r = prop_header_row + 1 + idx
|
||
ws[f"A{r}"] = label
|
||
ws[f"A{r}"].font = Font(name="Calibri", size=11, bold=True)
|
||
ws[f"A{r}"].border = BORDER
|
||
ws[f"A{r}"].alignment = LEFT
|
||
for strat in STRAT_KEYS:
|
||
letter = strat_cols[strat]
|
||
if label == "Daily Limit Status":
|
||
formula = (
|
||
f'=IF({letter}{worst_daily_row}<-Config!$B$13,"FAIL","PASS")'
|
||
)
|
||
elif label == "Max Loss Status":
|
||
formula = (
|
||
f'=IF({letter}{max_dd_row}>Config!$B$15,"FAIL","PASS")'
|
||
)
|
||
elif label == "Overall Prop Status":
|
||
formula = (
|
||
f'=IF(OR({letter}{daily_status_row}="FAIL",'
|
||
f'{letter}{max_status_row}="FAIL"),'
|
||
f'"CONT PIERDUT","CONFORM")'
|
||
)
|
||
else:
|
||
formula = fn(strat)
|
||
cell = ws[f"{letter}{r}"]
|
||
cell.value = formula
|
||
cell.number_format = fmt
|
||
cell.fill = DERIVED_FILL
|
||
cell.border = BORDER
|
||
cell.alignment = RIGHT if fmt != fail_pass_fmt else CENTER
|
||
# Hint în coloana G
|
||
hint_cell = ws[f"G{r}"]
|
||
hint_cell.value = METRIC_HINTS.get(label, "")
|
||
hint_cell.font = Font(name="Calibri", size=10, color="595959")
|
||
hint_cell.alignment = Alignment(
|
||
horizontal="left", vertical="top", wrap_text=True
|
||
)
|
||
hint_cell.border = BORDER
|
||
|
||
# Conditional formatting pe status rows — verde PASS/CONFORM, roșu FAIL/CONT PIERDUT
|
||
pass_fill = PatternFill("solid", fgColor="C6EFCE")
|
||
fail_fill = PatternFill("solid", fgColor="FFC7CE")
|
||
for status_row in (daily_status_row, max_status_row):
|
||
rng = (
|
||
f"{strat_cols[STRAT_KEYS[0]]}{status_row}:"
|
||
f"{strat_cols[STRAT_KEYS[-1]]}{status_row}"
|
||
)
|
||
ws.conditional_formatting.add(
|
||
rng, CellIsRule(operator="equal", formula=['"PASS"'], fill=pass_fill)
|
||
)
|
||
ws.conditional_formatting.add(
|
||
rng, CellIsRule(operator="equal", formula=['"FAIL"'], fill=fail_fill)
|
||
)
|
||
overall_row = prop_label_to_row["Overall Prop Status"]
|
||
overall_rng = (
|
||
f"{strat_cols[STRAT_KEYS[0]]}{overall_row}:"
|
||
f"{strat_cols[STRAT_KEYS[-1]]}{overall_row}"
|
||
)
|
||
ws.conditional_formatting.add(
|
||
overall_rng,
|
||
CellIsRule(operator="equal", formula=['"CONFORM"'], fill=pass_fill),
|
||
)
|
||
ws.conditional_formatting.add(
|
||
overall_rng,
|
||
CellIsRule(
|
||
operator="equal", formula=['"CONT PIERDUT"'], fill=fail_fill
|
||
),
|
||
)
|
||
|
||
# Înălțime rânduri prop (cu hint multi-line)
|
||
for r in range(prop_header_row + 1, prop_header_row + 1 + len(prop_metrics)):
|
||
ws.row_dimensions[r].height = 60
|
||
|
||
# ---- PROP FIRM COMPLIANCE per FEREASTRĂ × STRATEGIE ----
|
||
# 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}: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 × 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}:H{win_prop_note_row}")
|
||
|
||
win_prop_header_row = win_prop_note_row + 1
|
||
win_prop_headers = [
|
||
"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):
|
||
c = ws.cell(row=win_prop_header_row, column=col_idx, value=h)
|
||
c.font = HEADER_FONT
|
||
c.fill = HEADER_FILL
|
||
c.alignment = CENTER
|
||
c.border = BORDER
|
||
|
||
# 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):
|
||
target = get_column_letter(col_idx)
|
||
ws[f"{target}{r}"] = f"={source}{combo_row}"
|
||
cell = ws[f"{target}{r}"]
|
||
cell.border = BORDER
|
||
cell.fill = DERIVED_FILL
|
||
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 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"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"F{win_prop_first}:G{win_prop_last}"
|
||
ws.conditional_formatting.add(
|
||
breach_rng_win, CellIsRule(operator="equal", formula=['"DA"'], fill=fail_fill)
|
||
)
|
||
ws.conditional_formatting.add(
|
||
breach_rng_win, CellIsRule(operator="equal", formula=['"NU"'], fill=pass_fill)
|
||
)
|
||
|
||
# Column widths — aliniate cu DASH_WIN_COL (A=Fereastra ... U=Score_Prima)
|
||
widths = {
|
||
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
|
||
|
||
# Row height pentru rândurile cu hint (cu wrap) — explicații multi-line
|
||
for r in range(5, 5 + len(metrics)):
|
||
ws.row_dimensions[r].height = 75
|
||
|
||
# Equity curve chart — 5 linii
|
||
chart = LineChart()
|
||
chart.title = "Equity Curve — 5 strategii"
|
||
chart.style = 12
|
||
chart.y_axis.title = "Balance ($)"
|
||
chart.x_axis.title = "Trade #"
|
||
chart.height = 12
|
||
chart.width = 24
|
||
|
||
data = Reference(
|
||
wb["Trades"],
|
||
min_col=_col_to_int(COL[f"Bal_{STRAT_KEYS[0]}"]),
|
||
max_col=_col_to_int(COL[f"Bal_{STRAT_KEYS[-1]}"]),
|
||
min_row=1,
|
||
max_row=MAX_ROWS + 1,
|
||
)
|
||
chart.add_data(data, titles_from_data=True)
|
||
cats = Reference(
|
||
wb["Trades"], min_col=1, max_col=1,
|
||
min_row=2, max_row=MAX_ROWS + 1,
|
||
)
|
||
chart.set_categories(cats)
|
||
ws.add_chart(chart, "V4")
|
||
|
||
# Equity curve prop — al doilea chart, separat de modelul abstract
|
||
chart_prop = LineChart()
|
||
chart_prop.title = "Equity Curve — Prop ($50k start)"
|
||
chart_prop.style = 12
|
||
chart_prop.y_axis.title = "Balance Prop ($)"
|
||
chart_prop.x_axis.title = "Trade #"
|
||
chart_prop.height = 12
|
||
chart_prop.width = 24
|
||
|
||
data_prop = Reference(
|
||
wb["Trades"],
|
||
min_col=_col_to_int(COL[f"BalProp_{STRAT_KEYS[0]}"]),
|
||
max_col=_col_to_int(COL[f"BalProp_{STRAT_KEYS[-1]}"]),
|
||
min_row=1,
|
||
max_row=MAX_ROWS + 1,
|
||
)
|
||
chart_prop.add_data(data_prop, titles_from_data=True)
|
||
chart_prop.set_categories(cats)
|
||
ws.add_chart(chart_prop, "V30")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Main
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def build_workbook() -> Workbook:
|
||
wb = Workbook()
|
||
default = wb.active
|
||
wb.remove(default)
|
||
build_config(wb)
|
||
build_trades(wb)
|
||
build_dashboard(wb)
|
||
wb.active = wb.sheetnames.index("Dashboard")
|
||
return wb
|
||
|
||
|
||
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}")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|