- Glosar separat eliminat — toate explicațiile sunt acum în coloana G de lângă fiecare metrică - Win Ratio mutat lângă R:R (rândul 12-13) ca să se citească împreună - Hint-uri rescrise în limbaj simplu cu exemple în dolari (fără jargon, fără emoji-uri) - Coloana G lățită la 75, înălțime rânduri 75 pentru text multi-line - Titluri Config + Dashboard fără emoji-uri Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
732 lines
26 KiB
Python
732 lines
26 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
|
||
|
||
from datetime import date, time
|
||
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 only", "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
|
||
# ---------------------------------------------------------------------------
|
||
|
||
INPUT_HEADERS = [
|
||
"#", "Data", "Ora RO", "Zi", "Sesiune",
|
||
"Strategie", "Indicator", "TF",
|
||
"Direcție", "SL %", "TP0 %", "TP1 %", "TP2 %",
|
||
"Outcome", "Notes",
|
||
]
|
||
DERIVED_HEADERS = (
|
||
[f"R_{s}" for s in STRAT_KEYS]
|
||
+ [f"$_{s}" for s in STRAT_KEYS]
|
||
+ [f"Bal_{s}" for s in STRAT_KEYS]
|
||
)
|
||
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]
|
||
)
|
||
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"
|
||
|
||
ws["A5"] = "Risk per Trade (%)"
|
||
ws["B5"] = 1.0
|
||
ws["C5"] = "% din account riscat per trade (= -1R)"
|
||
|
||
ws["A6"] = "Risk per Trade ($)"
|
||
ws["B6"] = "=B4*B5/100"
|
||
ws["C6"] = "Auto — derivat din B4 și B5"
|
||
|
||
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"
|
||
|
||
# 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_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 only"),-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 only",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 only",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:
|
||
rc = f"{COL[r_col]}{r}"
|
||
return f'=IF({rc}="","",{rc}*Config!$B$6)'
|
||
|
||
|
||
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})'
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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)
|
||
|
||
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}"
|
||
)
|
||
|
||
# Sample row 2
|
||
ws["B2"] = date(2026, 5, 13)
|
||
ws["C2"] = time(17, 33)
|
||
ws[f'{COL["Strategie"]}2'] = "M2D"
|
||
ws[f'{COL["Indicator"]}2'] = "DIA"
|
||
ws[f'{COL["TF"]}2'] = "1min"
|
||
ws[f'{COL["Direcție"]}2'] = "Sell"
|
||
ws[f'{COL["SL %"]}2'] = 0.30
|
||
ws[f'{COL["TP0 %"]}2'] = 0.10
|
||
ws[f'{COL["TP1 %"]}2'] = 0.15
|
||
ws[f'{COL["TP2 %"]}2'] = 0.30
|
||
ws[f'{COL["Outcome"]}2'] = "TP1"
|
||
ws[f'{COL["Notes"]}2'] = "Exemplu — șterge când începi"
|
||
|
||
# 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 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_"):
|
||
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"]}
|
||
for strat in STRAT_KEYS:
|
||
derived_letters.add(COL[f"R_{strat}"])
|
||
derived_letters.add(COL[f"$_{strat}"])
|
||
derived_letters.add(COL[f"Bal_{strat}"])
|
||
helper_letters = set()
|
||
for strat in STRAT_KEYS:
|
||
for prefix in ("Win_", "Peak_", "DD_"):
|
||
helper_letters.add(COL[f"{prefix}{strat}"])
|
||
|
||
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
|
||
# Derived + helper: width 11
|
||
for strat in STRAT_KEYS:
|
||
for prefix in ("R_", "$_", "Bal_", "Win_", "Peak_", "DD_"):
|
||
ws.column_dimensions[COL[f"{prefix}{strat}"]].width = 11
|
||
|
||
# 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"
|
||
"Cu Risk per Trade fix, ar trebui să fie aproape de −1R în dolari.\n"
|
||
"Dacă e mult mai mare decât Risk per Trade, 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 (Risk per Trade din Config).\n"
|
||
"Dacă e semnificativ mai mare, ai depășit risk-ul plănuit — SL ratat, slippage, gap overnight."
|
||
),
|
||
"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 bani câștigi în medie pe UN trade (în R; 1R = Risk per Trade, default $100).\n"
|
||
"+0.30R = câștigi $30 pe trade. Pe 100 trade-uri: +$3.000.\n"
|
||
"−0.10R = pierzi $10 pe trade. Pe 100 trade-uri: −$1.000.\n"
|
||
"Pragul de GO LIVE: +0.20R sau mai mult."
|
||
),
|
||
"Expectancy ($)": (
|
||
"Aceeași expectancy convertită în dolari, folosind Risk per Trade din Config.\n"
|
||
"Util ca să vezi cât câștigi în medie pe trade în bani reali, 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."
|
||
),
|
||
}
|
||
|
||
|
||
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
|
||
|
||
# Helper pentru a emite un block breakdown (per Sesiune / Strategie / etc.)
|
||
def _emit_breakdown(
|
||
start_row: int, title: str, first_col_label: str,
|
||
items: list[str], item_range: str, overlay_strat: str,
|
||
) -> int:
|
||
ws[f"A{start_row}"] = title
|
||
ws[f"A{start_row}"].font = SUBTITLE_FONT
|
||
ws.merge_cells(f"A{start_row}:F{start_row}")
|
||
headers = [first_col_label, "N", "Wins", "WR", "Expectancy R", "Cum $"]
|
||
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
|
||
for i, item in enumerate(items):
|
||
r = start_row + 2 + i
|
||
ws[f"A{r}"] = item
|
||
ws[f"B{r}"] = f'=COUNTIF({item_range},"{item}")'
|
||
ws[f"C{r}"] = f'=COUNTIFS({item_range},"{item}",{W[overlay_strat]},1)'
|
||
ws[f"D{r}"] = f"=IFERROR(C{r}/B{r},0)"
|
||
ws[f"E{r}"] = (
|
||
f'=IFERROR(AVERAGEIFS({R[overlay_strat]},{item_range},"{item}"),0)'
|
||
)
|
||
ws[f"F{r}"] = f'=SUMIFS({D[overlay_strat]},{item_range},"{item}")'
|
||
ws[f"B{r}"].number_format = "0"
|
||
ws[f"C{r}"].number_format = "0"
|
||
ws[f"D{r}"].number_format = "0.0%"
|
||
ws[f"E{r}"].number_format = "+0.000;-0.000;0.000"
|
||
ws[f"F{r}"].number_format = '"$"#,##0.00'
|
||
for c in ("A", "B", "C", "D", "E", "F"):
|
||
ws[f"{c}{r}"].border = BORDER
|
||
ws[f"{c}{r}"].alignment = RIGHT if c != "A" else LEFT
|
||
return start_row + 2 + len(items)
|
||
|
||
# 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,
|
||
)
|
||
after_strat = _emit_breakdown(
|
||
after_sess + 2, "PER STRATEGIE (overlay: Hybrid + BE)", "Strategie",
|
||
STRATEGIES, _range("Strategie"), overlay,
|
||
)
|
||
after_ind = _emit_breakdown(
|
||
after_strat + 2, "PER INDICATOR (overlay: Hybrid + BE)", "Indicator",
|
||
INDICATORS, _range("Indicator"), overlay,
|
||
)
|
||
_emit_breakdown(
|
||
after_ind + 2, "PER DIRECȚIE (overlay: Hybrid + BE)", "Direcție",
|
||
DIRECTIONS, _range("Direcție"), overlay,
|
||
)
|
||
|
||
# Column widths
|
||
widths = {"A": 22, "B": 14, "C": 14, "D": 14, "E": 16, "F": 16, "G": 75}
|
||
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, "H4")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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)
|
||
wb = build_workbook()
|
||
wb.save(OUTPUT)
|
||
print(f"Wrote {OUTPUT}")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|