@@ -65,6 +65,28 @@ def _position_key(symbol: object, position_side: object = "") -> str:
6565 return f"{ base } ::{ side } " if side else base
6666
6767
68+ def _grid_order_identity (client_order_id : object ) -> tuple [int , str , str , int ] | None :
69+ """Return the stable grid-cell identity encoded by the V2 robot template."""
70+ parts = str (client_order_id or "" ).strip ().split ("-" )
71+ if len (parts ) != 5 or parts [0 ] != "grid" :
72+ return None
73+ try :
74+ cell_index = int (parts [1 ])
75+ cycle = int (parts [4 ])
76+ except (TypeError , ValueError ):
77+ return None
78+ position_side = _normalize_position_side (parts [2 ])
79+ phase = str (parts [3 ] or "" ).strip ().lower ()
80+ if (
81+ cell_index < 0
82+ or cycle < 1
83+ or not position_side
84+ or phase not in {"entry" , "exit" }
85+ ):
86+ return None
87+ return cell_index , position_side , phase , cycle
88+
89+
6890def _snapshot_state_value (value : Any ) -> Any :
6991 if isinstance (value , pd .Timestamp ):
7092 return {
@@ -597,6 +619,7 @@ def __init__(
597619 self .executions : list [dict [str , Any ]] = []
598620 self .closed_trades : list [dict [str , Any ]] = []
599621 self ._entries : dict [str , dict [str , Any ]] = {}
622+ self ._grid_entries : dict [str , dict [str , Any ]] = {}
600623 self ._protections : dict [str , ProtectionState ] = {}
601624 self .protection_events : list [dict [str , Any ]] = []
602625 self .order_ledger : list [dict [str , Any ]] = []
@@ -814,6 +837,7 @@ def execute(
814837 "commission" : fee ,
815838 "balance" : self .portfolio .total_value ,
816839 "reason" : order .reason ,
840+ "client_order_id" : str (order .client_order_id or "" ),
817841 "signal_time" : _backtest_time_iso (order .signal_time if order .signal_time is not None else timestamp ),
818842 "fill_reference" : fill_reference ,
819843 "reference_price" : open_price ,
@@ -1279,14 +1303,19 @@ def _record_closed_trade(
12791303 entry_quantity = max (float (entry .get ("quantity" ) or 0.0 ), closing_quantity )
12801304 entry_fee = float (entry .get ("commission" ) or 0.0 ) * closing_quantity / entry_quantity
12811305 direction = 1.0 if old_amount > 0 else - 1.0
1282- gross_profit = (float (execution ["price" ]) - float (entry .get ("price" ) or old_cost )) * closing_quantity * direction
1306+ gross_profit = (
1307+ float (execution ["price" ]) - float (entry .get ("price" ) or old_cost )
1308+ ) * closing_quantity * direction
12831309 profit = gross_profit - entry_fee - close_fee
1284- self .closed_trades .append ({
1310+ account_entry_price = float (entry .get ("price" ) or old_cost )
1311+ account_entry_time = str (entry .get ("time" ) or execution ["time" ])
1312+ grid_match = self ._consume_grid_entry (execution , closing_quantity , close_fee )
1313+ trade = {
12851314 "symbol" : symbol ,
12861315 "side" : str (entry .get ("side" ) or ("long" if old_amount > 0 else "short" )),
1287- "entry_time" : str ( entry . get ( "time" ) or execution [ "time" ]) ,
1316+ "entry_time" : account_entry_time ,
12881317 "exit_time" : str (execution ["time" ]),
1289- "entry_price" : float ( entry . get ( "price" ) or old_cost ) ,
1318+ "entry_price" : account_entry_price ,
12901319 "exit_price" : float (execution ["price" ]),
12911320 "quantity" : closing_quantity ,
12921321 "amount" : closing_quantity ,
@@ -1297,7 +1326,27 @@ def _record_closed_trade(
12971326 "commission" : entry_fee + close_fee ,
12981327 "balance" : float (execution .get ("balance" ) or 0.0 ),
12991328 "close_reason" : str (execution .get ("reason" ) or "strategy" ),
1300- })
1329+ "profit_basis" : "account_average" ,
1330+ }
1331+ if grid_match is not None :
1332+ trade .update ({
1333+ "entry_time" : grid_match ["entry_time" ],
1334+ "entry_price" : grid_match ["entry_price" ],
1335+ "gross_profit" : grid_match ["gross_profit" ],
1336+ "entry_commission" : grid_match ["entry_commission" ],
1337+ "commission" : grid_match ["commission" ],
1338+ "profit" : grid_match ["profit" ],
1339+ "matched_entry_price" : grid_match ["entry_price" ],
1340+ "grid_matched_profit" : grid_match ["profit" ],
1341+ "grid_cell_index" : grid_match ["cell_index" ],
1342+ "grid_cycle" : grid_match ["cycle" ],
1343+ "profit_basis" : "grid_cell" ,
1344+ "account_entry_time" : account_entry_time ,
1345+ "account_avg_entry_price" : account_entry_price ,
1346+ "account_gross_profit" : gross_profit ,
1347+ "account_realized_profit" : profit ,
1348+ })
1349+ self .closed_trades .append (trade )
13011350 remaining = max (0.0 , entry_quantity - closing_quantity )
13021351 if remaining > 1e-12 and old_amount * target_amount >= 0 :
13031352 entry ["quantity" ] = remaining
@@ -1326,6 +1375,90 @@ def _record_closed_trade(
13261375 "commission" : open_fee ,
13271376 "side" : opening_side ,
13281377 }
1378+ self ._record_grid_entry (execution , opening_quantity , open_fee )
1379+
1380+ @staticmethod
1381+ def _grid_entry_key (
1382+ position_key : str ,
1383+ identity : tuple [int , str , str , int ],
1384+ ) -> str :
1385+ cell_index , position_side , _ , cycle = identity
1386+ return f"{ position_key } |{ cell_index } |{ position_side } |{ cycle } "
1387+
1388+ def _record_grid_entry (
1389+ self ,
1390+ execution : Mapping [str , Any ],
1391+ quantity : float ,
1392+ commission : float ,
1393+ ) -> None :
1394+ identity = _grid_order_identity (execution .get ("client_order_id" ))
1395+ if identity is None or identity [2 ] != "entry" or quantity <= 1e-12 :
1396+ return
1397+ position_key = str (execution .get ("position_key" ) or execution .get ("symbol" ) or "" )
1398+ key = self ._grid_entry_key (position_key , identity )
1399+ current = self ._grid_entries .get (key )
1400+ if current is None :
1401+ self ._grid_entries [key ] = {
1402+ "cell_index" : identity [0 ],
1403+ "position_side" : identity [1 ],
1404+ "cycle" : identity [3 ],
1405+ "entry_time" : str (execution .get ("time" ) or "" ),
1406+ "entry_price" : float (execution .get ("price" ) or 0.0 ),
1407+ "quantity" : float (quantity ),
1408+ "commission" : float (commission ),
1409+ }
1410+ return
1411+ previous_quantity = float (current .get ("quantity" ) or 0.0 )
1412+ combined_quantity = previous_quantity + float (quantity )
1413+ if combined_quantity <= 1e-12 :
1414+ return
1415+ current ["entry_price" ] = (
1416+ float (current .get ("entry_price" ) or 0.0 ) * previous_quantity
1417+ + float (execution .get ("price" ) or 0.0 ) * float (quantity )
1418+ ) / combined_quantity
1419+ current ["quantity" ] = combined_quantity
1420+ current ["commission" ] = float (current .get ("commission" ) or 0.0 ) + float (commission )
1421+
1422+ def _consume_grid_entry (
1423+ self ,
1424+ execution : Mapping [str , Any ],
1425+ closing_quantity : float ,
1426+ close_commission : float ,
1427+ ) -> dict [str , Any ] | None :
1428+ identity = _grid_order_identity (execution .get ("client_order_id" ))
1429+ if identity is None or identity [2 ] != "exit" or closing_quantity <= 1e-12 :
1430+ return None
1431+ position_key = str (execution .get ("position_key" ) or execution .get ("symbol" ) or "" )
1432+ key = self ._grid_entry_key (position_key , identity )
1433+ entry = self ._grid_entries .get (key )
1434+ available = float ((entry or {}).get ("quantity" ) or 0.0 )
1435+ if entry is None or available + 1e-10 < closing_quantity :
1436+ return None
1437+
1438+ entry_commission_total = float (entry .get ("commission" ) or 0.0 )
1439+ entry_commission = entry_commission_total * closing_quantity / max (available , 1e-12 )
1440+ remaining = max (0.0 , available - closing_quantity )
1441+ if remaining <= 1e-12 :
1442+ self ._grid_entries .pop (key , None )
1443+ else :
1444+ entry ["quantity" ] = remaining
1445+ entry ["commission" ] = max (0.0 , entry_commission_total - entry_commission )
1446+
1447+ entry_price = float (entry .get ("entry_price" ) or 0.0 )
1448+ exit_price = float (execution .get ("price" ) or 0.0 )
1449+ direction = 1.0 if identity [1 ] == "long" else - 1.0
1450+ gross_profit = (exit_price - entry_price ) * closing_quantity * direction
1451+ commission = entry_commission + float (close_commission )
1452+ return {
1453+ "cell_index" : identity [0 ],
1454+ "cycle" : identity [3 ],
1455+ "entry_time" : str (entry .get ("entry_time" ) or execution .get ("time" ) or "" ),
1456+ "entry_price" : entry_price ,
1457+ "entry_commission" : entry_commission ,
1458+ "commission" : commission ,
1459+ "gross_profit" : gross_profit ,
1460+ "profit" : gross_profit - commission ,
1461+ }
13291462
13301463
13311464class StrategyV2BacktestRunner :
@@ -1591,6 +1724,19 @@ def _result(self) -> dict[str, Any]:
15911724 closed_trades = list (self .broker .closed_trades )
15921725 executions = list (self .broker .executions )
15931726 profits = [float (item .get ("profit" ) or 0.0 ) for item in closed_trades ]
1727+ account_realized_profits = [
1728+ float (
1729+ item .get ("account_realized_profit" )
1730+ if item .get ("account_realized_profit" ) is not None
1731+ else item .get ("profit" ) or 0.0
1732+ )
1733+ for item in closed_trades
1734+ ]
1735+ grid_matched_profits = [
1736+ float (item .get ("grid_matched_profit" ) or 0.0 )
1737+ for item in closed_trades
1738+ if item .get ("profit_basis" ) == "grid_cell"
1739+ ]
15941740 wins = [value for value in profits if value > 0 ]
15951741 losses = [value for value in profits if value < 0 ]
15961742 returns = pd .Series (values , dtype = "float64" ).pct_change ().dropna () if values else pd .Series (dtype = "float64" )
@@ -1677,6 +1823,14 @@ def _result(self) -> dict[str, Any]:
16771823 "worstTrade" : min (profits ) if profits else 0.0 ,
16781824 "avgTrade" : average_profit ,
16791825 "averageProfit" : average_profit ,
1826+ "accountRealizedProfit" : sum (account_realized_profits ),
1827+ "gridMatchedProfit" : sum (grid_matched_profits ),
1828+ "gridMatchedTradeCount" : len (grid_matched_profits ),
1829+ "tradeProfitBasis" : (
1830+ "grid_cell_when_available"
1831+ if grid_matched_profits
1832+ else "account_average"
1833+ ),
16801834 "totalProfit" : final - initial ,
16811835 "sharpeRatio" : sharpe_ratio ,
16821836 "annualizedReturn" : annualized_return ,
@@ -1713,7 +1867,14 @@ def _attribution(self, initial: float) -> dict[str, Any]:
17131867 commission_by_symbol [symbol ] = commission_by_symbol .get (symbol , 0.0 ) + float (execution .get ("commission" ) or 0.0 )
17141868 for trade in self .broker .closed_trades :
17151869 symbol = str (trade .get ("symbol" ) or "" )
1716- realized_by_symbol [symbol ] = realized_by_symbol .get (symbol , 0.0 ) + float (trade .get ("profit" ) or 0.0 )
1870+ account_profit = (
1871+ trade .get ("account_realized_profit" )
1872+ if trade .get ("account_realized_profit" ) is not None
1873+ else trade .get ("profit" )
1874+ )
1875+ realized_by_symbol [symbol ] = (
1876+ realized_by_symbol .get (symbol , 0.0 ) + float (account_profit or 0.0 )
1877+ )
17171878 rows = []
17181879 for symbol in sorted (set (commission_by_symbol ) | set (realized_by_symbol ) | set (self .broker .portfolio .positions )):
17191880 position = self .broker .portfolio .positions .get (symbol )
0 commit comments