@@ -64,9 +64,44 @@ def check_order_submitted(report, *, translator):
6464 return False , f"❌ { translator ('failed' , reason = status )} "
6565
6666
67- def get_available_buying_power (ib , fallback_buying_power ):
67+ def _normalize_account_ids (account_ids = None ) -> tuple [str , ...]:
68+ if account_ids is None :
69+ return ()
70+ if isinstance (account_ids , str ):
71+ candidates = [account_ids ]
72+ else :
73+ candidates = list (account_ids )
74+ normalized = []
75+ for candidate in candidates :
76+ text = str (candidate or "" ).strip ()
77+ if text :
78+ normalized .append (text )
79+ return tuple (dict .fromkeys (normalized ))
80+
81+
82+ def _matches_account (account_id : str | None , selected_account_ids : tuple [str , ...]) -> bool :
83+ if not selected_account_ids :
84+ return True
85+ return str (account_id or "" ).strip () in selected_account_ids
86+
87+
88+ def _resolve_order_account_id (account_ids = None ) -> str | None :
89+ normalized = _normalize_account_ids (account_ids )
90+ if len (normalized ) > 1 :
91+ raise ValueError (
92+ "IBKR live order routing requires a single account_id per runtime service; "
93+ f"got { len (normalized )} account_ids."
94+ )
95+ return normalized [0 ] if normalized else None
96+
97+
98+ def get_available_buying_power (ib , fallback_buying_power , * , account_ids = None ):
99+ selected_account_ids = _normalize_account_ids (account_ids )
68100 buying_power = fallback_buying_power
69101 for account_value in ib .accountValues ():
102+ account_id = str (getattr (account_value , "account" , "" ) or "" ).strip () or None
103+ if not _matches_account (account_id , selected_account_ids ):
104+ continue
70105 if account_value .tag == "AvailableFunds" and account_value .currency == "USD" :
71106 buying_power = float (account_value .value )
72107 return buying_power
@@ -101,9 +136,24 @@ def _extract_open_order_status(order_like: Any) -> str:
101136 return str (status or "" ).strip ()
102137
103138
104- def _collect_pending_symbols (ib , symbols : set [str ]) -> tuple [str , ...]:
139+ def _extract_open_order_account (order_like : Any ) -> str | None :
140+ order = getattr (order_like , "order" , None )
141+ for candidate in (
142+ getattr (order , "account" , None ),
143+ getattr (order_like , "account" , None ),
144+ ):
145+ text = str (candidate or "" ).strip ()
146+ if text :
147+ return text
148+ return None
149+
150+
151+ def _collect_pending_symbols (ib , symbols : set [str ], * , account_ids = None ) -> tuple [str , ...]:
152+ selected_account_ids = _normalize_account_ids (account_ids )
105153 pending = []
106154 for order_like in _iter_open_orders (ib ):
155+ if not _matches_account (_extract_open_order_account (order_like ), selected_account_ids ):
156+ continue
107157 status = _extract_open_order_status (order_like )
108158 if status in {"Cancelled" , "ApiCancelled" , "Inactive" , "Filled" }:
109159 continue
@@ -127,6 +177,19 @@ def _extract_fill_symbol(fill_like: Any) -> str | None:
127177 return symbol_text or None
128178
129179
180+ def _extract_fill_account (fill_like : Any ) -> str | None :
181+ execution = getattr (fill_like , "execution" , None )
182+ for candidate in (
183+ getattr (execution , "acctNumber" , None ),
184+ getattr (execution , "account" , None ),
185+ getattr (fill_like , "account" , None ),
186+ ):
187+ text = str (candidate or "" ).strip ()
188+ if text :
189+ return text
190+ return None
191+
192+
130193def _normalize_date_like (value : Any ) -> str | None :
131194 if value in {None , "" }:
132195 return None
@@ -150,11 +213,20 @@ def _extract_fill_date(fill_like: Any) -> str | None:
150213 return None
151214
152215
153- def _collect_same_day_filled_symbols (ib , symbols : set [str ], trade_date : str | None ) -> tuple [str , ...]:
216+ def _collect_same_day_filled_symbols (
217+ ib ,
218+ symbols : set [str ],
219+ trade_date : str | None ,
220+ * ,
221+ account_ids = None ,
222+ ) -> tuple [str , ...]:
154223 if not trade_date :
155224 return ()
225+ selected_account_ids = _normalize_account_ids (account_ids )
156226 matched = []
157227 for fill_like in _iter_fills (ib ):
228+ if not _matches_account (_extract_fill_account (fill_like ), selected_account_ids ):
229+ continue
158230 symbol = _extract_fill_symbol (fill_like )
159231 if not symbol or symbol not in symbols :
160232 continue
@@ -425,9 +497,15 @@ def execute_rebalance(
425497 safe_haven_symbols = tuple (allocation ["safe_haven_symbols" ])
426498 safe_haven_symbol = safe_haven_symbols [0 ] if safe_haven_symbols else None
427499 equity = account_values .get ("equity" , 0 )
500+ normalized_account_ids = _normalize_account_ids (account_ids )
501+ order_account_id = _resolve_order_account_id (normalized_account_ids )
428502 execution_summary = {
429503 "mode" : "dry_run" if dry_run_only else "paper" ,
430504 "strategy_profile" : strategy_profile ,
505+ "account_group" : account_group ,
506+ "account_ids" : list (normalized_account_ids ),
507+ "order_account_id" : order_account_id ,
508+ "service_name" : service_name ,
431509 "trade_date" : trade_date ,
432510 "snapshot_as_of" : snapshot_date ,
433511 "safe_haven_symbol" : safe_haven_symbol ,
@@ -508,7 +586,7 @@ def execute_rebalance(
508586 (sum (current_mv .values ()) - current_safe_haven_mv ) / equity
509587 )
510588
511- pending_symbols = _collect_pending_symbols (ib , set (all_symbols ))
589+ pending_symbols = _collect_pending_symbols (ib , set (all_symbols ), account_ids = normalized_account_ids )
512590 if pending_symbols :
513591 reason = f"pending_orders_detected:{ ',' .join (pending_symbols )} "
514592 execution_summary ["execution_status" ] = "blocked"
@@ -597,6 +675,7 @@ def execute_rebalance(
597675 anticipated_buying_power = get_available_buying_power (
598676 ib ,
599677 account_values .get ("buying_power" , 0 ),
678+ account_ids = normalized_account_ids ,
600679 )
601680 has_buy_plan = False
602681 for symbol , target in target_mv .items ():
@@ -655,7 +734,12 @@ def execute_rebalance(
655734 trade_logs .append (translator ("failed" , reason = reason ))
656735 return _finalize_result (trade_logs , execution_summary , return_summary = return_summary )
657736
658- same_day_filled_symbols = _collect_same_day_filled_symbols (ib , set (all_symbols ), trade_date )
737+ same_day_filled_symbols = _collect_same_day_filled_symbols (
738+ ib ,
739+ set (all_symbols ),
740+ trade_date ,
741+ account_ids = normalized_account_ids ,
742+ )
659743 if same_day_filled_symbols :
660744 reason = f"same_day_fills_detected:{ ',' .join (same_day_filled_symbols )} "
661745 execution_summary ["execution_status" ] = "blocked"
@@ -685,7 +769,7 @@ def execute_rebalance(
685769 strategy_profile = strategy_profile ,
686770 account_group = account_group ,
687771 service_name = service_name ,
688- account_ids = tuple ( account_ids or ()) ,
772+ account_ids = normalized_account_ids ,
689773 trade_date = trade_date ,
690774 snapshot_date = snapshot_date ,
691775 target_hash = target_hash ,
@@ -754,7 +838,12 @@ def execute_rebalance(
754838 continue
755839 report = submit_order_intent (
756840 ib ,
757- order_intent_cls (symbol = symbol , side = "sell" , quantity = qty ),
841+ order_intent_cls (
842+ symbol = symbol ,
843+ side = "sell" ,
844+ quantity = qty ,
845+ account_id = order_account_id ,
846+ ),
758847 )
759848 ok , status_msg = check_order_submitted (report , translator = translator )
760849 status = str (getattr (report , "status" , "" ) or "" )
@@ -784,6 +873,7 @@ def execute_rebalance(
784873 buying_power = anticipated_buying_power if not sell_executed else get_available_buying_power (
785874 ib ,
786875 account_values .get ("buying_power" , 0 ),
876+ account_ids = normalized_account_ids ,
787877 )
788878
789879 for symbol , target in target_mv .items ():
@@ -830,6 +920,7 @@ def execute_rebalance(
830920 order_type = "limit" ,
831921 limit_price = limit_price ,
832922 time_in_force = "DAY" ,
923+ account_id = order_account_id ,
833924 ),
834925 )
835926 ok , status_msg = check_order_submitted (report , translator = translator )
0 commit comments