11from __future__ import annotations
22
3+ from datetime import date , datetime
34from typing import Any , Callable
45
56from quant_platform_kit .common .models import ExecutionReport , OrderIntent
@@ -24,23 +25,203 @@ def _build_stock_contract(
2425 return stock_factory (symbol , exchange , currency )
2526
2627
28+ def _normalize_option_expiration (value : Any ) -> str :
29+ text = str (value or "" ).strip ()
30+ if len (text ) == 8 and text .isdigit ():
31+ return text
32+ if not text :
33+ raise ValueError ("Option OrderIntent.metadata.expiration is required." )
34+ if isinstance (value , datetime ):
35+ return value .date ().strftime ("%Y%m%d" )
36+ if isinstance (value , date ):
37+ return value .strftime ("%Y%m%d" )
38+ try :
39+ return datetime .fromisoformat (text [:10 ]).date ().strftime ("%Y%m%d" )
40+ except ValueError as exc :
41+ raise ValueError (f"Invalid option expiration: { value !r} " ) from exc
42+
43+
44+ def _normalize_option_right (value : Any ) -> str :
45+ text = str (value or "" ).strip ().upper ()
46+ if text in {"CALL" , "C" }:
47+ return "C"
48+ if text in {"PUT" , "P" }:
49+ return "P"
50+ raise ValueError ("Option OrderIntent.metadata.right must be C/call or P/put." )
51+
52+
53+ def _build_option_contract (
54+ order_intent : OrderIntent ,
55+ * ,
56+ option_factory : Callable [..., Any ] | None = None ,
57+ exchange : str = "SMART" ,
58+ currency : str = "USD" ,
59+ ) -> Any :
60+ metadata = dict (order_intent .metadata or {})
61+ underlier = str (metadata .get ("underlier" ) or order_intent .symbol or "" ).strip ().upper ()
62+ if not underlier :
63+ raise ValueError ("Option OrderIntent requires symbol or metadata.underlier." )
64+ expiration = _normalize_option_expiration (metadata .get ("expiration" ))
65+ right = _normalize_option_right (metadata .get ("right" ))
66+ try :
67+ strike = float (metadata .get ("strike" ))
68+ except (TypeError , ValueError ) as exc :
69+ raise ValueError ("Option OrderIntent.metadata.strike is required." ) from exc
70+ if strike <= 0.0 :
71+ raise ValueError ("Option OrderIntent.metadata.strike must be positive." )
72+ if option_factory is None :
73+ from ib_insync import Option
74+
75+ option_factory = Option
76+ return option_factory (
77+ underlier ,
78+ expiration ,
79+ strike ,
80+ right ,
81+ exchange = exchange ,
82+ currency = currency ,
83+ )
84+
85+
86+ def _is_option_intent (order_intent : OrderIntent ) -> bool :
87+ metadata = dict (order_intent .metadata or {})
88+ return (
89+ str (metadata .get ("asset_class" ) or "" ).strip ().lower () == "option"
90+ or str (metadata .get ("security_type" ) or "" ).strip ().upper () == "OPT"
91+ or str (metadata .get ("security_type" ) or "" ).strip ().upper () == "BAG"
92+ or str (metadata .get ("intent_type" ) or "" ).strip () == "single_leg_option"
93+ or str (metadata .get ("intent_type" ) or "" ).strip () == "multi_leg_option"
94+ )
95+
96+
97+ def _is_combo_option_intent (order_intent : OrderIntent ) -> bool :
98+ metadata = dict (order_intent .metadata or {})
99+ return (
100+ str (metadata .get ("asset_class" ) or "" ).strip ().lower () == "option"
101+ and str (metadata .get ("intent_type" ) or "" ).strip () == "multi_leg_option"
102+ )
103+
104+
105+ def _leg_action (value : Any ) -> str :
106+ text = str (value or "" ).strip ().lower ()
107+ if text .startswith ("buy" ):
108+ return "BUY"
109+ if text .startswith ("sell" ):
110+ return "SELL"
111+ raise ValueError (f"Unsupported option combo leg action: { value !r} " )
112+
113+
114+ def _build_option_combo_contract (
115+ ib : Any ,
116+ order_intent : OrderIntent ,
117+ * ,
118+ option_factory : Callable [..., Any ] | None = None ,
119+ combo_contract_factory : Callable [..., Any ] | None = None ,
120+ combo_leg_factory : Callable [..., Any ] | None = None ,
121+ exchange : str = "SMART" ,
122+ currency : str = "USD" ,
123+ ) -> Any :
124+ metadata = dict (order_intent .metadata or {})
125+ underlier = str (metadata .get ("underlier" ) or order_intent .symbol or "" ).strip ().upper ()
126+ legs = tuple (metadata .get ("legs" ) or ())
127+ if not underlier or not legs :
128+ raise ValueError ("Multi-leg option OrderIntent requires metadata.underlier and metadata.legs." )
129+ if combo_contract_factory is None :
130+ from ib_insync import Contract
131+
132+ combo_contract_factory = Contract
133+ if combo_leg_factory is None :
134+ from ib_insync import ComboLeg
135+
136+ combo_leg_factory = ComboLeg
137+
138+ combo_legs = []
139+ for leg in legs :
140+ if not isinstance (leg , dict ):
141+ raise ValueError ("Option combo legs must be mappings." )
142+ option_contract = _build_option_contract (
143+ OrderIntent (
144+ symbol = underlier ,
145+ side = _leg_action (leg .get ("action" )),
146+ quantity = 1 ,
147+ metadata = {
148+ "underlier" : underlier ,
149+ "expiration" : leg .get ("expiration" ) or metadata .get ("expiration" ),
150+ "right" : leg .get ("right" ),
151+ "strike" : leg .get ("strike" ),
152+ },
153+ ),
154+ option_factory = option_factory ,
155+ exchange = exchange ,
156+ currency = currency ,
157+ )
158+ qualified = ib .qualifyContracts (option_contract )
159+ qualified_contract = qualified [0 ] if qualified else option_contract
160+ con_id = getattr (qualified_contract , "conId" , None )
161+ if con_id is None :
162+ raise ValueError ("Qualified option combo leg did not expose conId." )
163+ combo_legs .append (
164+ combo_leg_factory (
165+ conId = con_id ,
166+ ratio = int (leg .get ("ratio" ) or 1 ),
167+ action = _leg_action (leg .get ("action" )),
168+ exchange = exchange ,
169+ )
170+ )
171+
172+ contract = combo_contract_factory ()
173+ contract .symbol = underlier
174+ contract .secType = "BAG"
175+ contract .exchange = exchange
176+ contract .currency = currency
177+ contract .comboLegs = combo_legs
178+ return contract
179+
180+
181+ def _normalize_order_side (side : str ) -> str :
182+ text = str (side or "" ).strip ().lower ()
183+ if text .startswith ("buy" ):
184+ return "BUY"
185+ if text .startswith ("sell" ):
186+ return "SELL"
187+ raise ValueError (f"Unsupported order side: { side !r} " )
188+
189+
27190def submit_order_intent (
28191 ib : Any ,
29192 order_intent : OrderIntent ,
30193 * ,
31194 account_id : str | None = None ,
32195 wait_seconds : float = 1.0 ,
33196 stock_factory : Callable [..., Any ] | None = None ,
197+ option_factory : Callable [..., Any ] | None = None ,
198+ combo_contract_factory : Callable [..., Any ] | None = None ,
199+ combo_leg_factory : Callable [..., Any ] | None = None ,
34200 market_order_factory : Callable [..., Any ] | None = None ,
35201 limit_order_factory : Callable [..., Any ] | None = None ,
36202) -> ExecutionReport :
37- contract = _build_stock_contract (
38- order_intent .symbol ,
39- stock_factory = stock_factory ,
40- )
203+ metadata = dict (order_intent .metadata or {})
204+ if _is_combo_option_intent (order_intent ):
205+ contract = _build_option_combo_contract (
206+ ib ,
207+ order_intent ,
208+ option_factory = option_factory ,
209+ combo_contract_factory = combo_contract_factory ,
210+ combo_leg_factory = combo_leg_factory ,
211+ )
212+ elif _is_option_intent (order_intent ):
213+ contract = _build_option_contract (
214+ order_intent ,
215+ option_factory = option_factory ,
216+ )
217+ else :
218+ contract = _build_stock_contract (
219+ order_intent .symbol ,
220+ stock_factory = stock_factory ,
221+ )
41222 ib .qualifyContracts (contract )
42223
43- side = order_intent .side . upper ( )
224+ side = _normalize_order_side ( order_intent .side )
44225 order_type = order_intent .order_type .lower ()
45226 if order_type == "market" :
46227 if market_order_factory is None :
@@ -90,5 +271,12 @@ def submit_order_intent(
90271 "order_type" : order_type ,
91272 "time_in_force" : getattr (order , "tif" , None ),
92273 "account_id" : resolved_account_id ,
274+ "asset_class" : metadata .get ("asset_class" ),
275+ "intent_type" : metadata .get ("intent_type" ),
276+ "underlier" : metadata .get ("underlier" ),
277+ "right" : metadata .get ("right" ),
278+ "expiration" : metadata .get ("expiration" ),
279+ "strike" : metadata .get ("strike" ),
280+ "legs" : metadata .get ("legs" ),
93281 },
94282 )
0 commit comments