- generate_dashboard.py: adauga build_ferestre() (auto-scan edge x durata x fiabilitate, nimic hardcodat) + sheet date_grafic; scoate grila de ferestre pe formule din build_dashboard() - sterge scripts/generate_ferestre_v2.py si data/Ferestre_v2.xlsx (inlocuite) - generate_template.py: Dashboard pur-tabular (fara grila ferestre pe formule) - CLAUDE.md: documenteaza modelul combinat (un fisier Dashboard.xlsx) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1321 lines
52 KiB
Python
1321 lines
52 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.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 real TradeLocker / Prop Firm ----
|
||
# Lanț de calcul transparent: Lot size -> $/punct -> $ risc la 1% mișcare preț.
|
||
# Toate coloanele $ din Trades folosesc B10*B11 (= $ risc la 1% preț pe poziția ta).
|
||
# Scalare la 100k: schimbi B9 (Account) și B10 (Lot size) — restul se recalculează.
|
||
ws["A8"] = "Cont real TradeLocker (prop firm)"
|
||
ws["A8"].font = SUBTITLE_FONT
|
||
ws.merge_cells("A8:C8")
|
||
|
||
ws["A9"] = "Account Start ($)"
|
||
ws["B9"] = 50000
|
||
ws["C9"] = "Balanța contului. Schimbă în 100000 pentru un cont de 100k."
|
||
|
||
ws["A10"] = "Lot size per semnal (US30)"
|
||
ws["B10"] = 0.08
|
||
ws["C10"] = (
|
||
"Câte loturi US30 intri pe fiecare semnal (TradeLocker). "
|
||
"Pe 100k cu același risc %: 0.16."
|
||
)
|
||
|
||
ws["A11"] = "$ per 1% preț, la 1.0 lot"
|
||
ws["B11"] = "=B24*B25/100"
|
||
ws["C11"] = (
|
||
"Auto = ($/punct la 1 lot, B24) × (Preț reper, B25 / 100). "
|
||
"Înmulțit cu Lot size (B10) dă riscul real per 1% — vezi B27."
|
||
)
|
||
|
||
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 Account Start (B9) și %"
|
||
|
||
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 Account Start (B9) și %"
|
||
|
||
# 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)
|
||
|
||
# ---- Calibrare $/punct dintr-un ordin reper + derivate informative ----
|
||
# Helper universal: dintr-un singur ordin TradeLocker (orice indicator) derivi
|
||
# $/punct, apoi $/1% preț. Pentru alt indicator schimbi cele 4 inputuri reper
|
||
# (B19-B22) + Preț reper curent (B25). Restul se recalculează singur.
|
||
ws["A18"] = "— Calibrare $/punct (dintr-un ordin reper TradeLocker) —"
|
||
ws["A18"].font = Font(name="Calibri", size=10, italic=True, bold=True, color="1F3864")
|
||
ws.merge_cells("A18:C18")
|
||
|
||
ws["A19"] = "Preț entry (ordin reper)"
|
||
ws["B19"] = 50680.15
|
||
ws["C19"] = "Prețul de intrare afișat în ticketul TradeLocker (orice ordin reper)."
|
||
|
||
ws["A20"] = "Preț SL (ordin reper)"
|
||
ws["B20"] = 50618.24
|
||
ws["C20"] = "Prețul SL din același ticket."
|
||
|
||
ws["A21"] = "$ risc afișat (ordin reper)"
|
||
ws["B21"] = 495.24
|
||
ws["C21"] = "Cifra $ pe care TradeLocker o arată la SL pentru ordinul reper."
|
||
|
||
ws["A22"] = "Lot size (ordin reper)"
|
||
ws["B22"] = 0.08
|
||
ws["C22"] = "Câte loturi avea ordinul reper (poate diferi de lotul tău curent, B10)."
|
||
|
||
ws["A23"] = "↳ Distanță reper (puncte)"
|
||
ws["B23"] = "=ABS(B19-B20)"
|
||
ws["C23"] = "Auto = |entry − SL|. Aici ⇒ 61.91 puncte."
|
||
|
||
ws["A24"] = "↳ $ per punct la 1.0 lot"
|
||
ws["B24"] = "=IF(OR(B23=0,B22=0),0,B21/B23/B22)"
|
||
ws["C24"] = (
|
||
"Auto = $risc ÷ distanță ÷ lot reper. Pe US30 ⇒ $100/punct la 1 lot. "
|
||
"Asta intră în B11. Pe alt indicator se recalibrează singur."
|
||
)
|
||
|
||
ws["A25"] = "Preț reper (curent)"
|
||
ws["B25"] = 50700
|
||
ws["C25"] = (
|
||
"Nivelul curent al indicatorului; convertește % mișcare preț în puncte/$. "
|
||
"Pentru alt indicator pui prețul lui."
|
||
)
|
||
|
||
ws["A26"] = "↳ $ per punct (la lotul tău)"
|
||
ws["B26"] = "=B10*B24"
|
||
ws["C26"] = "Auto = Lot size (B10) × $/punct la 1 lot (B24). La 0.08 loturi ⇒ $8/punct."
|
||
|
||
ws["A27"] = "↳ $ risc la 1% mișcare preț"
|
||
ws["B27"] = "=B10*B11"
|
||
ws["C27"] = (
|
||
"Auto — INIMA calculului. $ pe trade = R × SL% × această valoare. "
|
||
"La 0.08 loturi US30 ⇒ ~$4,056."
|
||
)
|
||
|
||
for r in (9, 10, 12, 14, 19, 20, 21, 22, 25): # inputuri galbene
|
||
ws.cell(row=r, column=2).fill = INPUT_FILL
|
||
ws.cell(row=r, column=2).border = BORDER
|
||
for r in (11, 13, 15, 23, 24, 26, 27): # 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.00"
|
||
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"
|
||
ws["B19"].number_format = "#,##0.00"
|
||
ws["B20"].number_format = "#,##0.00"
|
||
ws["B21"].number_format = '"$"#,##0.00'
|
||
ws["B22"].number_format = "0.00"
|
||
ws["B23"].number_format = "#,##0.00"
|
||
ws["B24"].number_format = '"$"#,##0.00'
|
||
ws["B25"].number_format = "#,##0"
|
||
ws["B26"].number_format = '"$"#,##0.00'
|
||
ws["B27"].number_format = '"$"#,##0.00'
|
||
|
||
# ---- Utilitar: calcul preț SL pentru TradeLocker (pas cu pas) ----
|
||
# Forward helper: din SL% (TradeStation) + preț intrare (TradeLocker) + direcție
|
||
# → prețul exact la care pui SL în TradeLocker. Celule intermediare vizibile.
|
||
ws["A29"] = "— Utilitar: ce prețuri SL/TP pun în TradeLocker —"
|
||
ws["A29"].font = Font(name="Calibri", size=10, italic=True, bold=True, color="1F3864")
|
||
ws.merge_cells("A29:C29")
|
||
|
||
ws["A30"] = "SL % (din TradeStation)"
|
||
ws["B30"] = 0.08
|
||
ws["C30"] = "Procentul SL al semnalului (% mișcare de preț), ex: 0.08."
|
||
|
||
ws["A31"] = "Preț intrare (TradeLocker)"
|
||
ws["B31"] = 50649.50
|
||
ws["C31"] = "Prețul tău de intrare US30 (nu prețul reper de la B25)."
|
||
|
||
ws["A32"] = "Direcție"
|
||
ws["B32"] = "Sell"
|
||
ws["C32"] = "Sell ⇒ SL DEASUPRA intrării. Buy ⇒ SL DEDESUBT."
|
||
|
||
ws["A33"] = "↳ Distanță SL (puncte)"
|
||
ws["B33"] = "=B31*B30/100"
|
||
ws["C33"] = "Auto = Preț intrare × SL% / 100. Ex: 50649.50 × 0.08/100 ⇒ ~40.5 puncte."
|
||
|
||
ws["A34"] = "↳ PREȚ SL de setat în TradeLocker"
|
||
ws["B34"] = '=IF(B32="Sell",B31+B33,B31-B33)'
|
||
ws["C34"] = "Sell: intrare + distanță. Buy: intrare − distanță. ASTA pui în TradeLocker."
|
||
|
||
ws["A35"] = "↳ $ risc la acest SL"
|
||
ws["B35"] = "=B33*B26"
|
||
ws["C35"] = (
|
||
"Auto = Distanță (puncte) × $/punct la lotul tău (B26). "
|
||
"Verificare: ar trebui să se potrivească cu cifra $ din ticketul TradeLocker."
|
||
)
|
||
|
||
for r in (30, 31, 32): # inputuri galbene
|
||
ws.cell(row=r, column=2).fill = INPUT_FILL
|
||
ws.cell(row=r, column=2).border = BORDER
|
||
for r in (33, 34, 35): # derived blue
|
||
ws.cell(row=r, column=2).fill = DERIVED_FILL
|
||
ws.cell(row=r, column=2).border = BORDER
|
||
# Output principal evidențiat
|
||
ws["A34"].font = Font(name="Calibri", size=11, bold=True, color="1F3864")
|
||
ws["B34"].font = Font(name="Calibri", size=11, bold=True)
|
||
ws["B32"].alignment = CENTER
|
||
|
||
ws["B30"].number_format = '0.000"%"'
|
||
ws["B31"].number_format = "#,##0.00"
|
||
ws["B33"].number_format = "#,##0.00"
|
||
ws["B34"].number_format = "#,##0.00"
|
||
ws["B35"].number_format = '"$"#,##0.00'
|
||
|
||
# --- TP0/TP1/TP2: aceeași logică, dar în direcția profitului (opus SL) ---
|
||
ws["A36"] = "TP0 % (din TradeStation)"
|
||
ws["B36"] = 0.03
|
||
ws["C36"] = "Procentul TP0 al semnalului (% mișcare de preț)."
|
||
|
||
ws["A37"] = "TP1 % (din TradeStation)"
|
||
ws["B37"] = 0.06
|
||
ws["C37"] = "Procentul TP1 al semnalului."
|
||
|
||
ws["A38"] = "TP2 % (din TradeStation)"
|
||
ws["B38"] = 0.08
|
||
ws["C38"] = "Procentul TP2 al semnalului."
|
||
|
||
ws["A39"] = "↳ PREȚ TP0 de setat"
|
||
ws["B39"] = '=IF(B32="Sell",B31-B31*B36/100,B31+B31*B36/100)'
|
||
ws["C39"] = (
|
||
"Distanță = intrare × TP0% / 100. Sell: intrare − distanță. "
|
||
"Buy: intrare + distanță (TP-ul e opus SL-ului)."
|
||
)
|
||
|
||
ws["A40"] = "↳ PREȚ TP1 de setat"
|
||
ws["B40"] = '=IF(B32="Sell",B31-B31*B37/100,B31+B31*B37/100)'
|
||
ws["C40"] = "La fel, cu TP1%."
|
||
|
||
ws["A41"] = "↳ PREȚ TP2 de setat"
|
||
ws["B41"] = '=IF(B32="Sell",B31-B31*B38/100,B31+B31*B38/100)'
|
||
ws["C41"] = "La fel, cu TP2%."
|
||
|
||
for r in (36, 37, 38): # inputuri galbene
|
||
ws.cell(row=r, column=2).fill = INPUT_FILL
|
||
ws.cell(row=r, column=2).border = BORDER
|
||
ws.cell(row=r, column=2).number_format = '0.000"%"'
|
||
for r in (39, 40, 41): # output evidențiat (albastru, bold)
|
||
ws.cell(row=r, column=2).fill = DERIVED_FILL
|
||
ws.cell(row=r, column=2).border = BORDER
|
||
ws.cell(row=r, column=2).number_format = "#,##0.00"
|
||
ws.cell(row=r, column=2).font = Font(name="Calibri", size=11, bold=True)
|
||
ws.cell(row=r, column=1).font = Font(
|
||
name="Calibri", size=11, bold=True, color="1F3864"
|
||
)
|
||
|
||
dv_dir = DataValidation(type="list", formula1='"Buy,Sell"', allow_blank=False)
|
||
dv_dir.add("B32")
|
||
ws.add_data_validation(dv_dir)
|
||
|
||
# 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": 30, "B": 14, "C": 40, "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% × $ risc la 1% preț (Config B27).\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% × $ risc la 1% preț (Config B27).\n"
|
||
"La 0.08 loturi US30: SL=0.30% × ~$4,056 ≈ −$1,217. Dacă e mult 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% × $ risc la 1% preț (Config B27).\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."
|
||
),
|
||
"$ risc la 1% preț ($)": (
|
||
"Câți $ riști pe poziția ta reală la 1% mișcare de preț (Config B27).\n"
|
||
"Lanț: Lot size (B10) × $/punct la 1 lot (B24) = $/punct; × Preț reper (B25)/100 = $ per 1%.\n"
|
||
"$/punct la 1 lot (B24) e calibrat dintr-un ordin reper TradeLocker — merge pe orice indicator.\n"
|
||
"La 0.08 loturi US30 ⇒ ~$4,056. Pe 100k cu lot 0.16 se dublează (~$8,112)."
|
||
),
|
||
"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 = Account Start (Config B9) + Cumulative P&L Prop.\n"
|
||
"Compar-o cu pragul de stop-out: Account Start − Max Loss Limit $ (Config B15)."
|
||
),
|
||
"Worst Daily Loss ($)": (
|
||
"Cea mai proastă pierdere cumulativă într-o zi calendaristică.\n"
|
||
"Dacă e mai mică decât −Daily Loss Limit $ (Config B13), ai depășit limita zilnică — 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 $ (Config B13, auto din Account × %).\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ă > Max Loss Limit $ (Config B15, auto din Account × %), ai depășit limita — cont mort."
|
||
),
|
||
"Max Loss Status": (
|
||
"PASS dacă Max Account Drawdown ≤ Max Loss Limit $ (Config B15).\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
|
||
|
||
# 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 = 5 + len(metrics) + 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',
|
||
),
|
||
(
|
||
"$ risc la 1% preț ($)",
|
||
lambda s: "=Config!$B$27",
|
||
'"$"#,##0.00',
|
||
),
|
||
(
|
||
"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
|
||
|
||
# Column widths — metric table + breakdowns + prop compliance
|
||
for _c, _w in {"A": 24, "B": 15, "C": 15, "D": 15,
|
||
"E": 15, "F": 15, "G": 60}.items():
|
||
ws.column_dimensions[_c].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
|
||
|
||
# Notă: graficele de echitate au fost eliminate (nu sunt folosite). Dashboard-ul
|
||
# rămâne pur tabelar — metrici + breakdown-uri + ferestre + compliance prop.
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Main
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def build_workbook() -> Workbook:
|
||
# backtest.xlsx = doar Config + Trades (fișierul editat zilnic, ușor/rapid).
|
||
# Dashboard-ul trăiește separat în data/Dashboard.xlsx, generat la comandă de
|
||
# scripts/generate_dashboard.py (vezi refresh_dashboard.bat). build_dashboard()
|
||
# rămâne aici și e refolosit de acel script.
|
||
wb = Workbook()
|
||
default = wb.active
|
||
wb.remove(default)
|
||
build_config(wb)
|
||
build_trades(wb)
|
||
wb.active = wb.sheetnames.index("Trades")
|
||
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())
|