22from alpaca .trading .client import TradingClient
33from alpaca .trading .models import Position
44from dotenv import load_dotenv
5+ from alpaca .data .historical .option import OptionHistoricalDataClient
6+ from alpaca .data .requests import OptionChainRequest , OptionLatestQuoteRequest
7+ from alpaca .trading .requests import GetOptionContractsRequest
8+ from datetime import datetime , timedelta
59
610load_dotenv ()
711
@@ -20,9 +24,10 @@ def init_alpaca_client():
2024 return None
2125
2226
23- def place_calendar_spread_order (symbol , quantity , expiry_short , expiry_long , strike ):
27+ def place_calendar_spread_order (symbol , quantity , expiry_short , expiry_long , strike , limit_price = None ):
2428 """
2529 Place a call calendar spread (sell near-term call, buy longer-term call, same strike) using Alpaca's multi-leg order format.
30+ Optionally specify a limit_price for the spread order.
2631 """
2732 client = init_alpaca_client ()
2833 if not client :
@@ -59,6 +64,8 @@ def make_option_symbol(symbol, expiry, strike, callput):
5964 }
6065 ]
6166 }
67+ if limit_price is not None :
68+ order_data ["limit_price" ] = limit_price
6269 order = client .submit_order (order_data )
6370 print (f"Placed calendar spread order: { order } " )
6471 return order
@@ -136,4 +143,112 @@ def get_portfolio_value():
136143 return equity
137144 except Exception as e :
138145 print (f"Error fetching portfolio value: { e } " )
146+ return None
147+
148+
149+ def get_alpaca_option_chain (symbol ):
150+ """
151+ Fetch the option chain for a given symbol using Alpaca's REST API.
152+ Returns a dict: {expiry: {strike: {call: {...}, put: {...}}}}
153+ """
154+ try :
155+ from alpaca .trading .client import TradingClient
156+ from alpaca .trading .requests import GetOptionContractsRequest
157+ from datetime import datetime
158+ trading_client = TradingClient (API_KEY , API_SECRET , paper = PAPER )
159+ today = datetime .now ().date ()
160+ req = GetOptionContractsRequest (
161+ underlying_symbols = [symbol .upper ()],
162+ expiration_date_gte = today ,
163+ limit = 1000
164+ )
165+ response = trading_client .get_option_contracts (req )
166+ contracts = response .option_contracts or []
167+ # Organize by expiry and strike
168+ option_chain = {}
169+ for contract in contracts :
170+ expiry = contract .expiration_date .strftime ('%Y-%m-%d' )
171+ strike = float (contract .strike_price )
172+ cp = contract .type # 'call' or 'put'
173+ if expiry not in option_chain :
174+ option_chain [expiry ] = {}
175+ if strike not in option_chain [expiry ]:
176+ option_chain [expiry ][strike ] = {}
177+ option_chain [expiry ][strike ][cp ] = contract
178+ return option_chain
179+ except Exception as e :
180+ print (f"Error fetching Alpaca option chain for { symbol } : { e } " )
181+ return None
182+
183+
184+ def select_expiries_and_strike_alpaca (symbol , earnings_date ):
185+ """
186+ Use Alpaca's option chain to select front and back month expiries and ATM strike for the calendar spread.
187+ Returns (expiry_short, expiry_long, strike) or (None, None, None) if not found.
188+ """
189+ option_chain = get_alpaca_option_chain (symbol )
190+ if not option_chain :
191+ return None , None , None
192+ try :
193+ exp_dates = sorted ([datetime .strptime (d , "%Y-%m-%d" ).date () for d in option_chain .keys ()])
194+ # Find front month expiry (first after earnings)
195+ expiry_short = next ((d for d in exp_dates if d > earnings_date ), None )
196+ if not expiry_short :
197+ return None , None , None
198+ # Find back month expiry (closest to 30 days after front)
199+ target_back = expiry_short + timedelta (days = 30 )
200+ expiry_long = min ((d for d in exp_dates if d > expiry_short ), key = lambda d : abs ((d - target_back ).days ), default = None )
201+ if not expiry_long :
202+ return None , None , None
203+ # Get ATM strike (closest to underlying price)
204+ # Fetch underlying price from Alpaca (latest bar)
205+ from alpaca .data .historical import StockHistoricalDataClient
206+ from alpaca .data .requests import StockLatestBarRequest
207+ stock_client = StockHistoricalDataClient (API_KEY , API_SECRET )
208+ bar_resp = stock_client .get_stock_latest_bar (StockLatestBarRequest (symbol_or_symbols = symbol ))
209+ if not bar_resp or symbol .upper () not in bar_resp :
210+ print (f"No price data for { symbol } " )
211+ return None , None , None
212+ underlying_price = bar_resp [symbol .upper ()].close
213+ strikes = list (option_chain [expiry_short .strftime ('%Y-%m-%d' )].keys ())
214+ strike = min (strikes , key = lambda x : abs (x - underlying_price ))
215+ return expiry_short .strftime ('%Y-%m-%d' ), expiry_long .strftime ('%Y-%m-%d' ), strike
216+ except Exception as e :
217+ print (f"Error selecting expiries/strike from Alpaca: { e } " )
218+ return None , None , None
219+
220+
221+ def get_option_spread_mid_price (symbol , expiry_short , expiry_long , strike , callput = 'C' ):
222+ """
223+ Fetch the latest quotes for both legs and return the mid price for the calendar spread (long_mid - short_mid).
224+ Returns float or None if unavailable.
225+ """
226+ def make_option_symbol (symbol , expiry , strike , callput ):
227+ expiry_fmt = expiry .replace ('-' , '' )[2 :]
228+ strike_fmt = f"{ int (float (strike ) * 1000 ):08d} "
229+ return f"{ symbol .upper ()} { expiry_fmt } { callput .upper ()} { strike_fmt } "
230+ try :
231+ options_client = OptionHistoricalDataClient (
232+ api_key = os .environ .get ("APCA_API_KEY_ID" ),
233+ secret_key = os .environ .get ("APCA_API_SECRET_KEY" )
234+ )
235+ call_symbol_short = make_option_symbol (symbol , expiry_short , strike , 'C' )
236+ call_symbol_long = make_option_symbol (symbol , expiry_long , strike , 'C' )
237+ req = OptionLatestQuoteRequest (symbol_or_symbols = [call_symbol_short , call_symbol_long ])
238+ quote_resp = options_client .get_option_latest_quote (req )
239+ quote_short = quote_resp .get (call_symbol_short )
240+ quote_long = quote_resp .get (call_symbol_long )
241+ if not quote_short or not quote_long :
242+ return None
243+ short_bid = quote_short .bid_price
244+ short_ask = quote_short .ask_price
245+ long_bid = quote_long .bid_price
246+ long_ask = quote_long .ask_price
247+ if None in (short_bid , short_ask , long_bid , long_ask ):
248+ return None
249+ short_mid = (short_bid + short_ask ) / 2
250+ long_mid = (long_bid + long_ask ) / 2
251+ return float (long_mid - short_mid )
252+ except Exception as e :
253+ print (f"Error fetching Alpaca spread mid price: { e } " )
139254 return None
0 commit comments