Skip to content

Commit b92d2af

Browse files
Enhance Alpaca integration with option chain fetching and calendar spread selection.
1 parent 255ef31 commit b92d2af

File tree

4 files changed

+360
-108
lines changed

4 files changed

+360
-108
lines changed

alpaca_integration.py

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
from alpaca.trading.client import TradingClient
33
from alpaca.trading.models import Position
44
from 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

610
load_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

Comments
 (0)