88from schwab .orders .common import first_triggers_second as trigger_builder
99from schwab .orders .common import one_cancels_other as oco_builder
1010from schwab .orders .options import OptionSymbol
11+ from schwab .orders .generic import OrderBuilder
12+ from schwab .orders .common import ComplexOrderStrategyType
1113
1214from schwab_mcp .context import SchwabContext
1315from schwab_mcp .tools ._registration import register_tool
@@ -606,6 +608,72 @@ async def place_bracket_order(
606608 )
607609
608610
611+ async def place_option_combo_order (
612+ ctx : SchwabContext ,
613+ account_hash : Annotated [str , "Account hash for the Schwab account" ],
614+ legs : Annotated [
615+ list [dict [str , Any ]],
616+ "List of option legs. Each leg requires: 'symbol' (str), 'quantity' (int), 'instruction' (BUY_TO_OPEN/SELL_TO_OPEN/BUY_TO_CLOSE/SELL_TO_CLOSE)." ,
617+ ],
618+ order_type : Annotated [str , "Combo order type: NET_CREDIT, NET_DEBIT, NET_ZERO, or MARKET" ],
619+ price : Annotated [
620+ float | None ,
621+ "Net price for the combo (required for NET_CREDIT/NET_DEBIT; omit for MARKET/NET_ZERO)." ,
622+ ] = None ,
623+ session : Annotated [
624+ str | None , "Trading session: NORMAL (default), AM, PM, or SEAMLESS"
625+ ] = "NORMAL" ,
626+ duration : Annotated [
627+ str | None , "Order duration: DAY (default) or GOOD_TILL_CANCEL"
628+ ] = "DAY" ,
629+ complex_order_strategy_type : Annotated [
630+ str | None ,
631+ "Optional complex type: IRON_CONDOR, VERTICAL, CALENDAR, CUSTOM, etc. Defaults to CUSTOM." ,
632+ ] = "CUSTOM" ,
633+ ) -> JSONType :
634+ """
635+ Places a single multi-leg option order (combo/spread) with a net price.
636+
637+ - Submit multiple option legs in one order payload using a single net
638+ price for LIMIT orders.
639+ - Each leg must include: instruction, symbol, quantity.
640+ - Example legs item: {"instruction": "SELL_TO_OPEN", "symbol": "SPY 251121C500", "quantity": 1}
641+
642+ Notes:
643+ - LIMIT is recommended for combos; MARKET support may vary by account/venue.
644+ - The API infers debit/credit from leg directions; pass a positive price.
645+ *Write operation.*
646+ """
647+ if not legs or len (legs ) < 2 :
648+ raise ValueError ("Provide at least two option legs for a combo order" )
649+
650+ # Build a single order with multiple option legs
651+ builder = OrderBuilder (enforce_enums = False ).set_order_strategy_type ("SINGLE" )
652+
653+ # Apply session/duration consistently with other tools
654+ builder = _apply_order_settings (builder , session , duration )
655+
656+ # complex order type helps the API validate multi-leg intent
657+ if complex_order_strategy_type :
658+ builder = builder .set_complex_order_strategy_type (complex_order_strategy_type .upper ())
659+
660+ # Set order type and net price
661+ builder = builder .set_order_type (order_type .upper ())
662+ if price is not None :
663+ builder = builder .set_price (price ) # net debit/credit as positive number
664+
665+ for leg in legs :
666+ builder = builder .add_option_leg (
667+ leg ["instruction" ],
668+ leg ["symbol" ],
669+ leg ["quantity" ],
670+ )
671+
672+ return await call (
673+ ctx .orders .place_order , account_hash = account_hash , order_spec = builder .build ()
674+ )
675+
676+
609677_READ_ONLY_TOOLS = (
610678 get_order ,
611679 get_orders ,
@@ -621,6 +689,7 @@ async def place_bracket_order(
621689 place_one_cancels_other_order ,
622690 place_first_triggers_second_order ,
623691 place_bracket_order ,
692+ place_option_combo_order ,
624693)
625694
626695
0 commit comments