Files
atm-backtesting/scripts/generate_template.py
Marius 017921794e reboot: replace vision pipeline with Excel-first manual journal
Pipeline-ul vision (screenshot extraction + CSV append + Python stats) era
greoi pentru backtest semi-manual. Înlocuit cu un singur template Excel
generat din openpyxl + Dashboard cu comparație 5 strategii management pe
aceleași semnale blackbox.

- Strategii: TP0 only / TP1 only / TP2 only / Hybrid+BE / Hybrid no BE
- Input minim (12 coloane galbene); Sesiune și Zi derivate auto din Data+Ora
- Dashboard cu coloana "Cum citesc" + secțiune Glosar cu exemple concrete
- Breakdowns PER SESIUNE / STRATEGIE / INDICATOR / DIRECȚIE
- Equity curve cu 5 linii

Eliminat: m2d-extractor agent, /backtest, /batch, /m2d-log, /stats slash
commands, scripts/{append_row,pl_calc,stats,manual_log,regenerate_md,
vision_schema,calendar_parse}.py, tests/, screenshots/, data/extractions/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 18:30:33 +03:00

732 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 EJ (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": "Numărul total de trade-uri logate",
"Wins": "Trade-uri cu R > 0",
"Win Ratio": "% wins. Singur NU spune mult — vezi împreună cu R:R și Expectancy",
"Average Win ($)": "Câștigul mediu pe trade winning",
"Average Loss ($)": "Pierderea medie pe trade losing",
"Best Trade ($)": "Cel mai mare câștig individual",
"Worst Trade ($)": "Cea mai mare pierdere individuală",
"Profit Factor": ">1.0 profitabil • >1.5 solid • >2.0 foarte bun • <1.0 pierzător",
"Risk:Reward": "Avg Win ÷ |Avg Loss|. >1 = câștig mediu > pierdere medie",
"Expectancy (R)": "★ STEAUA NORDULUI ★ >+0.20R = GO LIVE • negativ = ABANDON",
"Expectancy ($)": "Expectancy R convertit în $ (folosește Risk per Trade)",
"Cumulative P&L ($)": "P&L total în $ pe toate trade-urile",
"HWM Balance ($)": "Highest watermark — balanța de vârf atinsă",
"Max Drawdown ($)": "Cea mai mare cădere ($) din vârf la fund",
}
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"),
# Win Ratio: depends on rows above — handled after metrics list (placeholder)
("Win Ratio", lambda s: None, "0.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"),
# 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 scurtă
hint_cell = ws[f"G{r}"]
hint_cell.value = METRIC_HINTS.get(label, "")
hint_cell.font = Font(name="Calibri", size=10, italic=True, color="595959")
hint_cell.alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
hint_cell.border = BORDER
# ---- Glosar section: exemple concrete pentru metricile-cheie ----
glosar_start = 5 + len(metrics) + 2 # 2 rânduri spațiu după metrici
ws[f"A{glosar_start}"] = "📖 Glosar metrici — exemple concrete"
ws[f"A{glosar_start}"].font = SUBTITLE_FONT
ws.merge_cells(f"A{glosar_start}:G{glosar_start}")
glosar_entries = [
(
"Profit Factor",
"Suma câștigurilor ÷ |suma pierderilor|. Total cumulativ, nu mediu.",
"10 trade-uri: 4 wins de $50 (=$200) + 6 losses de $30 (=$180). PF = 200÷180 = 1.11 (marginal profitabil). La PF=2.0 câștigi de 2× cât pierzi în total.",
),
(
"Risk:Reward",
"Avg Win ÷ |Avg Loss|. Privește per-trade, nu total.",
"Avg win $50, avg loss $30 → R:R = 1.67. La R:R=2.0 ești profitabil chiar cu Win Ratio doar 40%. La R:R=0.5 ai nevoie de WR >67%.",
),
(
"Expectancy (R)",
"Câștigul mediu per trade exprimat în multipli de risc (R). CEA MAI ONESTĂ metrică — combină WR și R:R într-un singur număr.",
"10 trade-uri cu R = [+0.5, +0.5, +0.5, +0.5, 1, 1, 1, 1, 1, 1] → media = 0.30R (pierdere) chiar dacă WR=40%. Pragul GO LIVE din STOPPING_RULE.md: ≥ +0.20R.",
),
(
"Win Ratio (WR)",
"% trade-uri cu R > 0. ÎNȘELĂTOR singur — un WR mare cu R:R mic poate fi pierzător.",
"WR=70% pare excelent, dar dacă R:R=0.3 (câștigi $30, pierzi $100) → Expectancy = 0.7·30 0.3·100 = $9 per trade. Pierzător.",
),
(
"Max Drawdown",
"Cea mai mare cădere din vârful balanței la fundul ulterior. Măsoară 'durerea psihologică'.",
"Balance peak $11,500 → fund $9,800 → DD = $1,700 (17% din peak). DD mare la backtest = greu de tolerat în live.",
),
]
row = glosar_start + 1
for term, definition, example in glosar_entries:
ws[f"A{row}"] = term
ws[f"A{row}"].font = Font(name="Calibri", size=11, bold=True, color="1F3864")
ws[f"A{row}"].alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
ws[f"B{row}"] = definition
ws[f"B{row}"].font = Font(name="Calibri", size=10)
ws[f"B{row}"].alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
ws.merge_cells(f"B{row}:C{row}")
ws[f"D{row}"] = f"Exemplu: {example}"
ws[f"D{row}"].font = Font(name="Calibri", size=10, italic=True, color="595959")
ws[f"D{row}"].alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
ws.merge_cells(f"D{row}:G{row}")
ws.row_dimensions[row].height = 48
row += 1
glosar_end = row # primul rând după glosar
# 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 = glosar_end + 2 # 2 rânduri spațiu după glosar
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": 50}
for col, w in widths.items():
ws.column_dimensions[col].width = w
# Row height pentru rândurile cu hint (cu wrap)
for r in range(5, 5 + len(metrics)):
ws.row_dimensions[r].height = 22
# 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())