diff --git a/data/backtest-atm-m2d-una-pe-seara.xlsx b/data/backtest-atm-m2d-una-pe-seara.xlsx index db886c1..0fa625d 100644 Binary files a/data/backtest-atm-m2d-una-pe-seara.xlsx and b/data/backtest-atm-m2d-una-pe-seara.xlsx differ diff --git a/data/backtest.xlsx b/data/backtest.xlsx index f728cac..bfed365 100644 Binary files a/data/backtest.xlsx and b/data/backtest.xlsx differ diff --git a/scripts/generate_template.py b/scripts/generate_template.py index 6ac1796..a3d7151 100644 --- a/scripts/generate_template.py +++ b/scripts/generate_template.py @@ -189,22 +189,31 @@ def build_config(wb: Workbook) -> None: ws["B5"].number_format = '0.0"%"' ws["B6"].number_format = "$#,##0.00" - # ---- Bloc Cont Prop Firm (separat de modelul abstract de mai sus) ---- - ws["A8"] = "Cont Prop Firm" + # ---- 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 Prop Start ($)" + ws["A9"] = "Account Start ($)" ws["B9"] = 50000 - ws["C9"] = "Balanța contului de prop" + ws["C9"] = "Balanța contului. Schimbă în 100000 pentru un cont de 100k." - ws["A10"] = "Contracte per trade" - ws["B10"] = 1 - ws["C10"] = "Număr de contracte tranzacționate per semnal (TradeLocker)" + 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% per contract" - ws["B11"] = 10000 - ws["C11"] = "Pe DIA: 0.10% = $1000 ⇒ 1% = $10,000 (1 contract notional ≈ $1M)" + 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 @@ -212,7 +221,7 @@ def build_config(wb: Workbook) -> None: ws["A13"] = "Daily Loss Limit ($)" ws["B13"] = "=B9*B12/100" - ws["C13"] = "Auto — derivat din B9 și B12" + ws["C13"] = "Auto — derivat din Account Start (B9) și %" ws["A14"] = "Max Loss Limit (%)" ws["B14"] = 7.0 @@ -220,22 +229,7 @@ def build_config(wb: Workbook) -> None: ws["A15"] = "Max Loss Limit ($)" ws["B15"] = "=B9*B14/100" - ws["C15"] = "Auto — derivat din B9 și B14" - - for r in (9, 10, 11, 12, 14): # inputuri galbene - ws.cell(row=r, column=2).fill = INPUT_FILL - ws.cell(row=r, column=2).border = BORDER - for r in (13, 15): # derived blue - ws.cell(row=r, column=2).fill = DERIVED_FILL - ws.cell(row=r, column=2).border = BORDER - - ws["B9"].number_format = "$#,##0" - ws["B10"].number_format = "0" - ws["B11"].number_format = "$#,##0" - ws["B12"].number_format = '0.0"%"' - ws["B13"].number_format = "$#,##0" - ws["B14"].number_format = '0.0"%"' - ws["B15"].number_format = "$#,##0" + ws["C15"] = "Auto — derivat din Account Start (B9) și %" # Escape hatch performanță: activează/dezactivează filtrul Prima per Indicator ws["A17"] = "Activează filtru Prima" @@ -253,6 +247,179 @@ def build_config(wb: Workbook) -> None: 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), @@ -272,7 +439,7 @@ def build_config(wb: Workbook) -> None: c.alignment = CENTER widths = { - "A": 24, "B": 14, "C": 38, "D": 2, + "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(): @@ -699,7 +866,7 @@ METRIC_HINTS: dict[str, str] = { ), "Average Loss ($)": ( "Pierderea medie pe trade-urile negative (cifra apare cu minus).\n" - "În dolari reali, −1R depinde de SL%: pierdere ≈ SL% × Contracte × $/1% per contract.\n" + "Î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 ($)": ( @@ -709,8 +876,8 @@ METRIC_HINTS: dict[str, str] = { ), "Worst Trade ($)": ( "Cea mai mare pierdere individuală.\n" - "Ar trebui să fie aproximativ egală cu −1R calculat din SL% × Contracte × $/1% per contract.\n" - "Pe TradeLocker DIA: SL=0.30%, 1 contract → ≈ −$3000. Dacă e mai mare, ai slippage/gap." + "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" @@ -729,7 +896,7 @@ METRIC_HINTS: dict[str, str] = { "Pragul de GO LIVE: +0.20R sau mai mult." ), "Expectancy ($)": ( - "Aceeași expectancy convertită în dolari, folosind SL% × Contracte × $/1% per contract.\n" + "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 ($)": ( @@ -750,11 +917,11 @@ METRIC_HINTS: dict[str, str] = { "Capitalul de start al contului de prop firm (default $50,000).\n" "Editabil în Config B9." ), - "Position Size ($)": ( - "Configurare contract real TradeLocker:\n" - " • Contracte per trade (Config B10) — câte contracte tranzacționezi pe semnal.\n" - " • $ per 1% per contract (Config B11) — pe DIA: 0.10% = $1000 → 1% = $10,000.\n" - "Pierderea pe SL = SL% × Contracte × $/1% per contract. Pentru SL=0.30%, 1 contract → $3000." + "$ 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" @@ -762,24 +929,24 @@ METRIC_HINTS: dict[str, str] = { "Adunat peste $50,000 dă balanța finală reală." ), "Final Balance Prop ($)": ( - "Balanța finală a contului de prop = $50,000 + Cumulative P&L Prop.\n" - "Compar-o cu pragul de stop-out al firmei de prop: $50,000 − $3,500 = $46,500." + "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 −$2,000, ai depășit Daily Loss Limit într-o zi — cont mort.\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 ($2,000 default).\n" + "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ă > $3,500 (7% din $50k), ai depășit Max Loss Limit — cont mort." + "Dacă > Max Loss Limit $ (Config B15, auto din Account × %), ai depășit limita — cont mort." ), "Max Loss Status": ( - "PASS dacă Max Account Drawdown ≤ $3,500.\n" + "PASS dacă Max Account Drawdown ≤ Max Loss Limit $ (Config B15).\n" "FAIL = strategia ar fi pierdut contul prin drawdown cumulativ." ), "Overall Prop Status": ( @@ -1090,12 +1257,16 @@ def build_dashboard(wb: Workbook) -> None: f'"CANDIDAT","PRE-CANDIDAT")))' ) ws[f"{T_LET}{row}"] = ( - f'=IF(OR({F_}{row}<1,{D_}{row}<>"Toate"),-1E+12,' - f'{I_}{row}*100000+{K_}{row}*1000+{L_}{row}-{M_}{row}-{O_}{row}/10)' + f'=IF(OR({F_}{row}<1,{D_}{row}<>"Toate",' + f'{P_}{row}="DA",{Q_}{row}="DA"),-1E+12,' + f'{I_}{row}*20000+MIN({F_}{row},150)*100+' + f'{K_}{row}*1500+{L_}{row}-{M_}{row}-{O_}{row}/10)' ) ws[f"{U_LET}{row}"] = ( - f'=IF(OR({F_}{row}<1,{D_}{row}<>"Prima"),-1E+12,' - f'{I_}{row}*100000+{K_}{row}*1000+{L_}{row}-{M_}{row}-{O_}{row}/10)' + f'=IF(OR({F_}{row}<1,{D_}{row}<>"Prima",' + f'{P_}{row}="DA",{Q_}{row}="DA"),-1E+12,' + f'{I_}{row}*20000+MIN({F_}{row},150)*100+' + f'{K_}{row}*1500+{L_}{row}-{M_}{row}-{O_}{row}/10)' ) combo_rows.append(row) combo_idx += 1 @@ -1165,7 +1336,7 @@ def build_dashboard(wb: Workbook) -> None: top_target_letters = ["B", "C", "D", "E", "F", "G", "H", "I", "J", "K"] def _emit_top_subsection(start_row: int, title: str, note: str, - score_col: str, count: int = 5) -> int: + score_col: str, count: int = 20) -> int: ws[f"A{start_row}"] = title ws[f"A{start_row}"].font = SUBTITLE_FONT ws.merge_cells(f"A{start_row}:K{start_row}") @@ -1235,21 +1406,25 @@ def build_dashboard(wb: Workbook) -> None: top_title_row = row + 2 after_top_toate = _emit_top_subsection( top_title_row, - "TOP 5 FERESTRE — Toate trade-urile", + "TOP 20 FERESTRE — Toate trade-urile", ( - "Top 5 după scor compus, calculat pe rândurile cu Filtru=Toate. " - "CANDIDAT = îndeplinește pragurile (N≥40, WR≥55%, ExpR≥0.2, no breach). " - "PRE-CANDIDAT = N≥1 fără breach dar sub praguri. BREACH = ar fi pierdut prop." + "Top 20 după scor compus, calculat pe rândurile cu Filtru=Toate. " + "EXCLUDE ferestrele cu Daily Breach=DA sau Max Loss Breach=DA (ar fi pierdut contul prop). " + "Scor = ExpR×20000 + MIN(N,150)×100 + PF×1500 + CumP&L − MaxDD − MaxDDProp/10. " + "Bonusul N (capat la 150) favorizează ferestrele cu sample mai mare, statistic mai fiabile. " + "CANDIDAT = îndeplinește pragurile (N≥40, WR≥55%, ExpR≥0.2). " + "PRE-CANDIDAT = N≥1 fără breach dar sub praguri." ), score_col=T_LET, ) after_top_prima = _emit_top_subsection( after_top_toate + 2, - "TOP 5 FERESTRE — Prima per Indicator", + "TOP 20 FERESTRE — Prima per Indicator", ( - "Top 5 după scor compus, calculat pe rândurile cu Filtru=Prima (doar primul " - "trade pe (Data, Indicator) în fiecare fereastră). Util pentru a vedea dacă " - "filtrul Prima identifică ferestre mai eficiente decât Toate." + "Top 20 după scor compus, calculat pe rândurile cu Filtru=Prima (doar primul " + "trade pe (Data, Indicator) în fiecare fereastră). EXCLUDE ferestrele cu Daily " + "Breach=DA sau Max Loss Breach=DA. Util pentru a vedea dacă filtrul Prima " + "identifică ferestre mai eficiente decât Toate." ), score_col=U_LET, ) @@ -1359,9 +1534,9 @@ def build_dashboard(wb: Workbook) -> None: '"$"#,##0', ), ( - "Position Size ($)", - lambda s: "=Config!$B$11", - '"$"#,##0', + "$ risc la 1% preț ($)", + lambda s: "=Config!$B$27", + '"$"#,##0.00', ), ( "Cumulative P&L Prop ($)", @@ -1597,7 +1772,7 @@ def build_dashboard(wb: Workbook) -> None: # Equity curve prop — al doilea chart, separat de modelul abstract chart_prop = LineChart() - chart_prop.title = "Equity Curve — Prop ($50k start)" + chart_prop.title = "Equity Curve — Prop (cont real)" chart_prop.style = 12 chart_prop.y_axis.title = "Balance Prop ($)" chart_prop.x_axis.title = "Trade #"