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
@@ -584,6 +586,72 @@ async def place_bracket_order(
584586 )
585587
586588
589+ async def place_option_combo_order (
590+ ctx : SchwabContext ,
591+ account_hash : Annotated [str , "Account hash for the Schwab account" ],
592+ legs : Annotated [
593+ list [dict [str , Any ]],
594+ "List of option legs. Each leg requires: 'symbol' (str), 'quantity' (int), 'instruction' (BUY_TO_OPEN/SELL_TO_OPEN/BUY_TO_CLOSE/SELL_TO_CLOSE)." ,
595+ ],
596+ order_type : Annotated [str , "Combo order type: NET_CREDIT, NET_DEBIT, NET_ZERO, or MARKET" ],
597+ price : Annotated [
598+ float | None ,
599+ "Net price for the combo (required for NET_CREDIT/NET_DEBIT; omit for MARKET/NET_ZERO)." ,
600+ ] = None ,
601+ session : Annotated [
602+ str | None , "Trading session: NORMAL (default), AM, PM, or SEAMLESS"
603+ ] = "NORMAL" ,
604+ duration : Annotated [
605+ str | None , "Order duration: DAY (default) or GOOD_TILL_CANCEL"
606+ ] = "DAY" ,
607+ complex_order_strategy_type : Annotated [
608+ str | None ,
609+ "Optional complex type: IRON_CONDOR, VERTICAL, CALENDAR, CUSTOM, etc. Defaults to CUSTOM." ,
610+ ] = "CUSTOM" ,
611+ ) -> JSONType :
612+ """
613+ Places a single multi-leg option order (combo/spread) with a net price.
614+
615+ - Submit multiple option legs in one order payload using a single net
616+ price for LIMIT orders.
617+ - Each leg must include: instruction, symbol, quantity.
618+ - Example legs item: {"instruction": "SELL_TO_OPEN", "symbol": "SPY 251121C500", "quantity": 1}
619+
620+ Notes:
621+ - LIMIT is recommended for combos; MARKET support may vary by account/venue.
622+ - The API infers debit/credit from leg directions; pass a positive price.
623+ *Write operation.*
624+ """
625+ if not legs or len (legs ) < 2 :
626+ raise ValueError ("Provide at least two option legs for a combo order" )
627+
628+ # Build a single order with multiple option legs
629+ builder = OrderBuilder (enforce_enums = False ).set_order_strategy_type ("SINGLE" )
630+
631+ # Apply session/duration consistently with other tools
632+ builder = _apply_order_settings (builder , session , duration )
633+
634+ # complex order type helps the API validate multi-leg intent
635+ if complex_order_strategy_type :
636+ builder = builder .set_complex_order_strategy_type (complex_order_strategy_type .upper ())
637+
638+ # Set order type and net price
639+ builder = builder .set_order_type (order_type .upper ())
640+ if price is not None :
641+ builder = builder .set_price (price ) # net debit/credit as positive number
642+
643+ for leg in legs :
644+ builder = builder .add_option_leg (
645+ leg ["instruction" ],
646+ leg ["symbol" ],
647+ leg ["quantity" ],
648+ )
649+
650+ return await call (
651+ ctx .orders .place_order , account_hash = account_hash , order_spec = builder .build ()
652+ )
653+
654+
587655_READ_ONLY_TOOLS = (
588656 get_order ,
589657 get_orders ,
@@ -599,6 +667,7 @@ async def place_bracket_order(
599667 place_one_cancels_other_order ,
600668 place_first_triggers_second_order ,
601669 place_bracket_order ,
670+ place_option_combo_order ,
602671)
603672
604673
0 commit comments