@@ -63,6 +63,53 @@ def _number(value: object, *, field_name: str) -> float:
6363 raise IBKRReconciliationReadError (f"IBKR reconciliation is missing { field_name } ." ) from exc
6464
6565
66+ def _order_identity_integer (value : object , * , field_name : str ) -> int :
67+ if isinstance (value , bool ):
68+ raise IBKRReconciliationReadError (
69+ f"IBKR reconciliation order identity has an invalid { field_name } ."
70+ )
71+ try :
72+ return int (str (value ).strip (), 10 )
73+ except (TypeError , ValueError ) as exc :
74+ raise IBKRReconciliationReadError (
75+ f"IBKR reconciliation order identity has an invalid { field_name } ."
76+ ) from exc
77+
78+
79+ def build_canonical_order_key (
80+ * ,
81+ account_id : object ,
82+ client_id : object ,
83+ order_id : object ,
84+ perm_id : object ,
85+ ) -> str :
86+ """Build the account-scoped IBKR key used by submit and reconciliation paths."""
87+
88+ account = _text (account_id )
89+ if not account or "|" in account or any (ord (character ) < 32 for character in account ):
90+ raise IBKRReconciliationReadError (
91+ "IBKR reconciliation order identity has an invalid account id."
92+ )
93+ normalized_order_id = _order_identity_integer (order_id , field_name = "order id" )
94+ if normalized_order_id <= 0 :
95+ normalized_perm_id = _order_identity_integer (perm_id , field_name = "perm id" )
96+ if normalized_perm_id <= 0 :
97+ raise IBKRReconciliationReadError (
98+ "IBKR reconciliation order identity requires a positive perm id for a manual order."
99+ )
100+ return f"ibkr|account={ account } |perm_id={ normalized_perm_id } "
101+
102+ normalized_client_id = _order_identity_integer (client_id , field_name = "client id" )
103+ if normalized_client_id < 0 :
104+ raise IBKRReconciliationReadError (
105+ "IBKR reconciliation order identity requires a non-negative client id."
106+ )
107+ return (
108+ f"ibkr|account={ account } |client_id={ normalized_client_id } "
109+ f"|order_id={ normalized_order_id } "
110+ )
111+
112+
66113def normalize_account_ids (account_ids : Iterable [str ] | str | None ) -> tuple [str , ...]:
67114 if account_ids is None :
68115 return ()
@@ -165,6 +212,12 @@ def _normalise_open_trade(trade: Any, *, selected_account_ids: tuple[str, ...])
165212 raise IBKRReconciliationReadError ("IBKR reconciliation received an open order without a contract." )
166213 return {
167214 "account" : _text (account_id ),
215+ "order_key" : build_canonical_order_key (
216+ account_id = account_id ,
217+ client_id = getattr (order , "clientId" , None ),
218+ order_id = getattr (order , "orderId" , None ),
219+ perm_id = getattr (order , "permId" , None ),
220+ ),
168221 "contract" : _safe_contract_fields (contract ),
169222 "perm_id" : _text (getattr (order , "permId" , "" )),
170223 "action" : _text (getattr (order , "action" , "" )).upper (),
@@ -201,6 +254,12 @@ def _normalise_execution(fill: Any, *, selected_account_ids: tuple[str, ...]) ->
201254 raise IBKRReconciliationReadError ("IBKR reconciliation received an incomplete execution record." )
202255 return {
203256 "account" : _text (account_id ),
257+ "order_key" : build_canonical_order_key (
258+ account_id = account_id ,
259+ client_id = getattr (execution , "clientId" , None ),
260+ order_id = getattr (execution , "orderId" , None ),
261+ perm_id = getattr (execution , "permId" , None ),
262+ ),
204263 "contract" : _safe_contract_fields (contract ),
205264 "execution_id" : _text (getattr (execution , "execId" , "" )),
206265 "order_id" : _text (getattr (execution , "orderId" , "" )),
@@ -504,6 +563,7 @@ def matches(key: str, actual_digest: str) -> bool:
504563 "IBKRReconciliationCandidate" ,
505564 "IBKRReconciliationObservations" ,
506565 "IBKRReconciliationReadError" ,
566+ "build_canonical_order_key" ,
507567 "build_reconciliation_candidate" ,
508568 "collect_read_only_reconciliation_observations" ,
509569 "normalize_account_ids" ,
0 commit comments