Last active
August 27, 2026 00:10
-
-
Save jweinst1/e86e1ce67507834ba0832b1b9a2bc6ac to your computer and use it in GitHub Desktop.
bull put spreads script for alpaca py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from alpaca.data.historical import StockHistoricalDataClient | |
| from alpaca.data.requests import StockSnapshotRequest | |
| from alpaca.data.enums import DataFeed | |
| from alpaca.data.historical import OptionHistoricalDataClient | |
| from alpaca.data.requests import OptionChainRequest, StockBarsRequest, OptionBarsRequest, OptionSnapshotRequest, OptionLatestQuoteRequest | |
| from alpaca.data.enums import OptionsFeed | |
| from alpaca.trading.enums import ContractType, AssetClass | |
| from alpaca.trading.enums import QueryOrderStatus, OrderSide, OrderClass, TimeInForce, OrderStatus, OrderType, PositionIntent | |
| from alpaca.trading.client import TradingClient | |
| from alpaca.trading.requests import GetOrdersRequest, LimitOrderRequest, TakeProfitRequest, StopLimitOrderRequest, GetOptionContractsRequest, OptionLegRequest, ReplaceOrderRequest | |
| from alpaca.data.timeframe import TimeFrame | |
| from itertools import combinations | |
| from collections import defaultdict | |
| import argparse | |
| import os | |
| from pathlib import Path | |
| import statistics | |
| from datetime import datetime, timezone, timedelta | |
| import time | |
| import json | |
| import re | |
| import pandas as pd | |
| import numpy as np | |
| from dataclasses import dataclass | |
| from typing import List, Dict, Tuple | |
| # Get the path of ~ | |
| # home_path = Path.home() | |
| # todo json credentials load | |
| def get_json_creds(): | |
| home_path = Path.home() | |
| creds_path = home_path / "spread_creds.json" | |
| with open(creds_path, "r", encoding="utf-8") as file: | |
| data = json.load(file) | |
| return data | |
| account_credentials = get_json_creds() | |
| @dataclass | |
| class OptionPosition: | |
| symbol: str | |
| underlying: str | |
| expiration: str | |
| option_type: str | |
| strike: float | |
| qty: int | |
| cost_basis: float | |
| available:bool | |
| def find_spread_orders(client, symbols, qty): | |
| current_until = datetime.now(timezone.utc).isoformat() | |
| found_orders = [] | |
| found_qty = 0 | |
| while found_qty < qty: | |
| orders_request = GetOrdersRequest( | |
| status=QueryOrderStatus.OPEN, | |
| nested=True, | |
| limit=500, | |
| symbols=symbols, | |
| until=current_until, | |
| direction="desc" | |
| ) | |
| chunk = client.get_orders(filter=orders_request) | |
| if not chunk: | |
| break | |
| for order in chunk: | |
| if order.order_class == OrderClass.MLEG and len(order.legs) == 2: | |
| ordset = {str(order.legs[0].symbol), str(order.legs[1].symbol)} | |
| if symbols[0] in ordset and symbols[1] in ordset: | |
| found_qty += float(order.qty) | |
| found_orders.append(order) | |
| current_until = chunk[-1].submitted_at.isoformat() | |
| return found_orders | |
| @dataclass | |
| class BullPutSpread: | |
| underlying: str | |
| expiration: str | |
| short_leg: OptionPosition | |
| long_leg: OptionPosition | |
| qty: int | |
| available:bool | |
| def matches_lo(self, key): | |
| return key in self.long_leg.symbol | |
| def matches_sh(self, key): | |
| return key in self.short_leg.symbol | |
| def find_existing_orders(self, trade_client:TradingClient): | |
| if self.available: | |
| return [] | |
| return find_spread_orders(trade_client, [self.short_leg.symbol, self.long_leg.symbol], self.qty) | |
| def cancel_existing_orders(self, trade_client, found_orders): | |
| for forder in found_orders: | |
| if forder.status == OrderStatus.PENDING_CANCEL: | |
| continue | |
| print(f"Canceling order by ID={forder.id}") | |
| trade_client.cancel_order_by_id(forder.id) | |
| def adjust_existing_orders(self, trade_client, found_orders, base_percent_debt): | |
| net_credit_received = abs(self.short_leg.cost_basis) - abs(self.long_leg.cost_basis) | |
| debt_lim = (net_credit_received/ 100) * base_percent_debt | |
| debt_lim = round(debt_lim, 2) | |
| assert debt_lim > 0.0 | |
| replace_request = ReplaceOrderRequest(limit_price=debt_lim) | |
| for forder in found_orders: | |
| if forder.status == OrderStatus.PENDING_REPLACE: | |
| print(f"Skipping order ID={forder.id} due to pending replace") | |
| continue | |
| elif forder.status == OrderStatus.ACCEPTED: | |
| print(f"Skipping order ID={forder.id} due to already accepted") | |
| continue | |
| elif forder.limit_price == debt_lim: | |
| print(f"Skipping order ID={forder.id} due to already same price") | |
| continue | |
| print(f"Adjusting order by ID={forder.id} for Lim={debt_lim}") | |
| trade_client.replace_order_by_id(forder.id, replace_request) | |
| def calculate_pl_and_liq( | |
| self, optdata_client: OptionHistoricalDataClient | |
| ) -> Tuple[float, float, float, float]: | |
| """ | |
| Fetches live bid/ask quotes to calculate: | |
| - Mid Liquidation Debit per share | |
| - Total Net Liquidation Cost ($) | |
| - Net Credit Received ($) | |
| - True Unrealized P/L ($) | |
| """ | |
| request = OptionLatestQuoteRequest( | |
| symbol_or_symbols=[self.short_leg.symbol, self.long_leg.symbol] | |
| ) | |
| quotes = optdata_client.get_option_latest_quote(request) | |
| q_short = quotes[self.short_leg.symbol] | |
| q_long = quotes[self.long_leg.symbol] | |
| # Calculate Mid prices for both legs | |
| short_mid = (q_short.ask_price + q_short.bid_price) / 2.0 | |
| long_mid = (q_long.ask_price + q_long.bid_price) / 2.0 | |
| # Mid debit to buy back short and sell long leg | |
| mid_debit_per_share = short_mid - long_mid | |
| total_liq_cost = mid_debit_per_share * 100.0 * self.qty | |
| # Net initial credit received based on original position cost basis | |
| net_credit_received = abs(self.short_leg.cost_basis) - abs(self.long_leg.cost_basis) | |
| # True Unrealized P/L = Net Credit Received - Current Liquidation Cost | |
| true_unrealized_pl = net_credit_received - total_liq_cost | |
| return mid_debit_per_share, net_credit_received, total_liq_cost, true_unrealized_pl | |
| def target_closing_debit( | |
| self, net_credit_received: float, profit_target_pct: float = 0.50 | |
| ) -> float: | |
| """Calculates target closing limit debit per share based on net initial credit received.""" | |
| net_credit_per_share = net_credit_received / (100.0 * self.qty) | |
| return round(net_credit_per_share * (1.0 - profit_target_pct), 2) | |
| def close_spread(self, client:TradingClient, base_percent_debt:float): | |
| my_ords = self.find_existing_orders(client) | |
| if my_ords: | |
| self.adjust_existing_orders(client, my_ords, base_percent_debt) | |
| else: | |
| self.place_sell_order(client, base_percent_debt) | |
| def place_sell_order(self, client:TradingClient, base_percent_debt:float): | |
| net_credit_received = abs(self.short_leg.cost_basis) - abs(self.long_leg.cost_basis) | |
| debt_lim = (net_credit_received/ 100) * base_percent_debt | |
| debt_lim = round(debt_lim, 2) | |
| assert debt_lim > 0.0 | |
| leg_buy_to_close = OptionLegRequest( | |
| symbol=self.short_leg.symbol, | |
| ratio_qty=1, | |
| side=OrderSide.BUY, | |
| position_intent=PositionIntent.BUY_TO_CLOSE) | |
| leg_sell_to_close = OptionLegRequest( | |
| symbol=self.long_leg.symbol, | |
| ratio_qty=1, | |
| side=OrderSide.SELL, | |
| position_intent=PositionIntent.SELL_TO_CLOSE) | |
| # Construct Multi-Leg Limit Order (Net Debit) | |
| mleg_order_req = LimitOrderRequest( | |
| qty=self.qty, | |
| time_in_force=TimeInForce.GTC, | |
| order_class=OrderClass.MLEG, | |
| legs=[leg_buy_to_close, leg_sell_to_close], | |
| limit_price=debt_lim) | |
| try: | |
| print("===============================================================================\n") | |
| print(f"Submitting limit close for lim={debt_lim} order={self.underlying} exp={self.expiration}") | |
| order_resp = client.submit_order(mleg_order_req) | |
| print("✅ [CLOSE ORDER SUCCESSFULLY PLACED]") | |
| print(f" Order ID: {order_resp.id}") | |
| print(f" Status: {order_resp.status}") | |
| print(f" Submitted At: {order_resp.submitted_at}") | |
| print("===============================================================================\n") | |
| except Exception as err: | |
| print(f"❌ Close order submission failed at Alpaca: {err}") | |
| print("===============================================================================\n") | |
| def display(self, opclient, trdclient): | |
| formatted_exp = f"20{self.expiration[:2]}-{self.expiration[2:4]}-{self.expiration[4:]}" | |
| # Fetch live quote data & calculate true P/L | |
| ( | |
| mid_debit_per_share, | |
| net_credit_received, | |
| total_liq_cost, | |
| true_unrealized_pl, | |
| ) = self.calculate_pl_and_liq(opclient) | |
| target_debit = self.target_closing_debit(net_credit_received, 50.0) | |
| pl_pct = (true_unrealized_pl / net_credit_received * 100) if net_credit_received > 0 else 0.0 | |
| pl_sign = "+" if true_unrealized_pl >= 0 else "" | |
| my_ords = self.find_existing_orders(trdclient) | |
| mycurord = my_ords[0].limit_price if my_ords else "NA" | |
| print( | |
| f"| {self.underlying} Bull Put Spread ({self.qty} Contract{'s' if self.qty > 1 else ''})" | |
| ) | |
| print(f" Expiration: {formatted_exp}") | |
| print( | |
| f" Short Leg: {self.short_leg.symbol} Sell {self.qty}x ${self.short_leg.strike:.2f} Put" | |
| ) | |
| print( | |
| f" Long Leg: {self.long_leg.symbol} Buy {self.qty}x ${self.long_leg.strike:.2f} Put" | |
| ) | |
| print( | |
| f" Net Credit: ${net_credit_received / (100 * self.qty):.2f}/sh (${net_credit_received:.2f} total)" | |
| ) | |
| print( | |
| f" Net Liquidity: ${mid_debit_per_share:.2f}/sh (${total_liq_cost:.2f} total cost to BTC @ Mid)" | |
| ) | |
| print( | |
| f" Real P/L: {pl_sign}${true_unrealized_pl:.2f} ({pl_sign}{pl_pct:.1f}% of max credit)" | |
| ) | |
| print( | |
| f" Available:{self.available} CurrentClose: {mycurord}" | |
| ) | |
| print("=" * 80) | |
| def parse_occ_symbol_data(symbol: str) -> Tuple[str, str, str, float]: | |
| """ | |
| Parses OCC option symbols from right to left to support variable-length tickers. | |
| Format: [Ticker][YYMMDD][C/P][8-digit Strike] | |
| """ | |
| strike_raw = symbol[-8:] | |
| option_type = symbol[-9] | |
| exp_raw = symbol[-15:-9] | |
| underlying = symbol[:-15] | |
| strike = int(strike_raw) / 1000.0 | |
| return underlying, exp_raw, option_type, strike | |
| def reconstruct_bull_put_spreads(positions) -> List[BullPutSpread]: | |
| grouped_positions: Dict[Tuple[str, str], List[OptionPosition]] = {} | |
| for pos in positions: | |
| if len(pos.symbol) < 15: | |
| continue | |
| underlying, exp, opt_type, strike = parse_occ_symbol_data(pos.symbol) | |
| if opt_type != "P": | |
| continue | |
| opt_pos = OptionPosition( | |
| symbol=pos.symbol, | |
| underlying=underlying, | |
| expiration=exp, | |
| option_type=opt_type, | |
| strike=strike, | |
| qty=int(float(pos.qty)), | |
| cost_basis=float(pos.cost_basis), | |
| available=pos.qty_available != '0' | |
| ) | |
| key = (underlying, exp) | |
| grouped_positions.setdefault(key, []).append(opt_pos) | |
| spreads: List[BullPutSpread] = [] | |
| for (underlying, exp), pos_list in grouped_positions.items(): | |
| shorts = sorted( | |
| [p for p in pos_list if p.qty < 0], key=lambda x: x.strike, reverse=True | |
| ) | |
| longs = sorted( | |
| [p for p in pos_list if p.qty > 0], key=lambda x: x.strike, reverse=True | |
| ) | |
| for short_put in shorts: | |
| available_short_qty = abs(short_put.qty) | |
| if available_short_qty == 0: | |
| continue | |
| for long_put in longs: | |
| available_long_qty = long_put.qty | |
| if available_long_qty == 0 or long_put.strike >= short_put.strike: | |
| continue | |
| matched_qty = min(available_short_qty, available_long_qty) | |
| # Calculate proportional cost basis for exact contract matching | |
| short_ratio = matched_qty / abs(short_put.qty) | |
| long_ratio = matched_qty / long_put.qty | |
| short_leg_matched = OptionPosition( | |
| symbol=short_put.symbol, | |
| underlying=short_put.underlying, | |
| expiration=short_put.expiration, | |
| option_type=short_put.option_type, | |
| strike=short_put.strike, | |
| qty=-matched_qty, | |
| cost_basis=short_put.cost_basis * short_ratio, | |
| available=short_put.available | |
| ) | |
| long_leg_matched = OptionPosition( | |
| symbol=long_put.symbol, | |
| underlying=long_put.underlying, | |
| expiration=long_put.expiration, | |
| option_type=long_put.option_type, | |
| strike=long_put.strike, | |
| qty=matched_qty, | |
| cost_basis=long_put.cost_basis * long_ratio, | |
| available=long_put.available | |
| ) | |
| spreads.append( | |
| BullPutSpread( | |
| underlying=underlying, | |
| expiration=exp, | |
| short_leg=short_leg_matched, | |
| long_leg=long_leg_matched, | |
| qty=matched_qty, | |
| available=short_put.available and long_put.available | |
| ) | |
| ) | |
| short_put.qty += matched_qty # short_put.qty is negative | |
| long_put.qty -= matched_qty | |
| break | |
| return spreads | |
| def is_occ_symbol(symbol): | |
| # Ticker (1-6 letters) + YYMMDD + C/P + 8-digit Strike | |
| pattern = r"^[A-Z]{1,6}\d{6}[CP]\d{8}$" | |
| return bool(re.fullmatch(pattern, symbol)) | |
| def format_occ_symbol(ticker: str, exp_date_str: str, option_type: str, strike: float) -> str: | |
| """Helper to format standardized 21-character OCC Option Symbols.""" | |
| d = datetime.strptime(exp_date_str, "%Y-%m-%d") | |
| yy = d.strftime("%y") | |
| mm = d.strftime("%m") | |
| dd = d.strftime("%d") | |
| strike_int = int(round(float(strike) * 1000)) | |
| return f"{ticker.upper()}{yy}{mm}{dd}{option_type.upper()}{strike_int:08d}" | |
| def parse_occ_symbol(occ_symbol: str): | |
| """Parses standard OCC option ticker into (ticker, expiration_date, option_type, strike_price).""" | |
| pattern = r"^([A-Z]{1,6})(\d{2})(\d{2})(\d{2})([CP])(\d{8})$" | |
| match = re.match(pattern, occ_symbol.strip().upper()) | |
| if not match: | |
| return None | |
| ticker, yy, mm, dd, otype, strike_raw = match.groups() | |
| exp_date = f"20{yy}-{mm}-{dd}" | |
| strike = float(strike_raw) / 1000.0 | |
| return { | |
| "occ":occ_symbol, | |
| "ticker": ticker, | |
| "exp_date": exp_date, | |
| "type": otype, | |
| "strike": strike | |
| } | |
| def retrieve_orders(client, symbols): | |
| current_until = datetime.now(timezone.utc).isoformat() | |
| all_orders = [] | |
| while True: | |
| orders_request = GetOrdersRequest( | |
| status=QueryOrderStatus.OPEN, | |
| nested=True, | |
| limit=500, | |
| symbols=symbols, | |
| until=current_until, | |
| direction="desc" | |
| ) | |
| chunk = client.get_orders(filter=orders_request) | |
| if not chunk: | |
| break | |
| all_orders.extend(chunk) | |
| current_until = chunk[-1].submitted_at.isoformat() | |
| return all_orders | |
| def get_snapshot_opt(client, symbol): | |
| return client.get_option_snapshot(OptionSnapshotRequest(symbol_or_symbols=symbol))[symbol] | |
| def parse_arguments(): | |
| parser = argparse.ArgumentParser(description="Alpaca Stock CLI") | |
| parser.add_argument('--acc', type=str, help='Account identifier or name', required=True) | |
| subparsers = parser.add_subparsers( | |
| dest='command', | |
| required=True, # Makes a subcommand mandatory | |
| title='Commands', | |
| metavar='COMMAND' | |
| ) | |
| power = subparsers.add_parser('power', help='Check buying power', description='Check buying power') | |
| orders = subparsers.add_parser('orders', help='current open orders', description='current open orders') | |
| orders.add_argument('--sym', type=str, help='symbol to check', required=True) | |
| owned = subparsers.add_parser('owned', help='current positions', description='current positions') | |
| owned.add_argument('--long-str', type=str, help='string match against ticker', default="") | |
| owned.add_argument('--short-str', type=str, help='string match against ticker', default="") | |
| owned.add_argument('--place-close', type=float, help='percent based close') | |
| owned.add_argument('--available', action='store_true', help='show only available pos') | |
| searcher = subparsers.add_parser('search', help='find spreads to invest in', description='list puts') | |
| searcher.add_argument('--tick', type=str, help='Stock Symbol', required=True) | |
| searcher.add_argument("--type", type=str, default="PUT", choices=["PUT"], help="Option type: PUT (default: PUT)") | |
| searcher.add_argument('--dte-max', type=int, help='day range', default=8) | |
| searcher.add_argument('--dte-min', type=int, help='day begin', default=1) | |
| searcher.add_argument('--str-max', type=float, help='strike max', default=0.98) | |
| searcher.add_argument('--str-min', type=float, help='strike min', default=0.70) | |
| searcher.add_argument('--str-width', type=int, default=1) | |
| searcher.add_argument('--cred-nat', type=float, help='strike nat min', default=0.0) | |
| searcher.add_argument('--cred-mid', type=float, help='strike mid min', default=0.0) | |
| searcher.add_argument('--lim', type=int, help='max number to show', default=0) | |
| searcher.add_argument('--submit-orders', type=str, default="NONE", choices=["NONE", "NAT", "MID", "HALF", "MID34"], help='submit buy orders for these spreads') | |
| # todo make this on argment about mid or nat and a diff of a float from them | |
| args = parser.parse_args() | |
| # check command via arg.command | |
| return args | |
| class CreditSpread(object): | |
| def __init__(self, sell_sym, buy_sym, cred_nat, cred_mid, theta): | |
| self.sell_sym = sell_sym | |
| self.buy_sym = buy_sym | |
| self.cred_nat = cred_nat | |
| self.cred_mid = cred_mid | |
| self.theta = theta | |
| def __repr__(self): | |
| return f" short={self.sell_sym} long={self.buy_sym} mid={self.cred_mid} nat={self.cred_nat} theta={self.theta}" | |
| def place_limit_order(self, client, credit_amount, qty = 1): | |
| credit_amount = round(credit_amount, 2) | |
| if credit_amount >= 0: | |
| credit_amount = -credit_amount | |
| leg_sold = OptionLegRequest( | |
| symbol=self.sell_sym, | |
| ratio_qty=1, | |
| side=OrderSide.SELL, | |
| position_intent=PositionIntent.SELL_TO_OPEN | |
| ) | |
| # Leg 2: Buy Long Put to Open | |
| leg_bought = OptionLegRequest( | |
| symbol=self.buy_sym, | |
| ratio_qty=1, | |
| side=OrderSide.BUY, | |
| position_intent=PositionIntent.BUY_TO_OPEN | |
| ) | |
| assert credit_amount < 0 | |
| mleg_order_req = LimitOrderRequest( | |
| qty=qty, | |
| time_in_force=TimeInForce.DAY, | |
| order_class=OrderClass.MLEG, | |
| legs=[leg_sold, leg_bought], | |
| limit_price=credit_amount | |
| ) | |
| try: | |
| print(f"Submitting Order at cred={credit_amount} " + str(self)) | |
| order_resp = client.submit_order(mleg_order_req) | |
| print("✅ [ORDER SUCCESSFULLY PLACED]") | |
| print(f" Order ID: {order_resp.id}") | |
| print(f" Status: {order_resp.status}") | |
| print(f" Submitted At: {order_resp.submitted_at}") | |
| print("=========================================================================\n") | |
| except Exception as err: | |
| print(f"❌ Order submission failed at Alpaca: {err}") | |
| print("=========================================================================\n") | |
| def nat_limit_order(self, client, qty = 1): | |
| self.place_limit_order(client, self.cred_nat, qty) | |
| def mid_limit_order(self, client, qty = 1): | |
| self.place_limit_order(client, self.cred_mid, qty) | |
| def half_limit_order(self, client, qty = 1): | |
| self.place_limit_order(client, (self.cred_mid + self.cred_nat) / 2, qty) | |
| def three4_limit_order(self, client, qty = 1): | |
| self.place_limit_order(client, self.cred_mid * 0.75, qty) | |
| class OwnedCreditSpread(object): | |
| def __init__(self, sh_sym, lo_sym, pl, basis, qty, delta_pc, theta_pc, delta_total, theta_total, cur_ord = None): | |
| self.sh_sym = sh_sym | |
| self.lo_sym = lo_sym | |
| self.pl = pl | |
| self.basis = basis | |
| self.qty = qty | |
| self.delta_pc = delta_pc | |
| self.theta_pc = theta_pc | |
| self.delta_total = delta_total | |
| self.theta_total = theta_total | |
| self.cur_ord = cur_ord | |
| def active_sell_id(self): | |
| return self.cur_ord.id if self.cur_ord is not None else None | |
| def matches_sh(self, term): | |
| return term in self.sh_sym | |
| def matches_lo(self, term): | |
| return term in self.lo_sym | |
| def __repr__(self): | |
| return ( | |
| f" short={self.sh_sym} long={self.lo_sym} qty={self.qty} " | |
| f"pl={self.pl} cost={self.basis} liq={self.basis + self.pl} " | |
| f"net_delta={self.delta_pc} net_theta={self.theta_pc} " | |
| f"(Total Delta: {self.delta_total} shares, Total Theta: ${self.theta_total}/day) " | |
| f"CurOrder={self.cur_ord.limit_price if self.cur_ord is not None else 'N'}" | |
| ) | |
| def place_sell_order(self, client, base_percent_debt): | |
| if self.cur_ord is not None: | |
| id_to_delete = self.active_sell_id | |
| if self.cur_ord.status != OrderStatus.PENDING_CANCEL: | |
| client.cancel_order_by_id(id_to_delete) | |
| debt_lim = -1 * (self.basis / 100) * base_percent_debt | |
| debt_lim = round(debt_lim, 2) | |
| assert debt_lim > 0.0 | |
| leg_buy_to_close = OptionLegRequest( | |
| symbol=self.sh_sym, | |
| ratio_qty=1, | |
| side=OrderSide.BUY, | |
| position_intent=PositionIntent.BUY_TO_CLOSE) | |
| leg_sell_to_close = OptionLegRequest( | |
| symbol=self.lo_sym, | |
| ratio_qty=1, | |
| side=OrderSide.SELL, | |
| position_intent=PositionIntent.SELL_TO_CLOSE) | |
| # Construct Multi-Leg Limit Order (Net Debit) | |
| mleg_order_req = LimitOrderRequest( | |
| qty=self.qty, | |
| time_in_force=TimeInForce.GTC, | |
| order_class=OrderClass.MLEG, | |
| legs=[leg_buy_to_close, leg_sell_to_close], | |
| limit_price=debt_lim) | |
| try: | |
| print("===============================================================================\n") | |
| print(f"Submitting limit close for lim={debt_lim} order={str(self)}") | |
| order_resp = client.submit_order(mleg_order_req) | |
| print("✅ [CLOSE ORDER SUCCESSFULLY PLACED]") | |
| print(f" Order ID: {order_resp.id}") | |
| print(f" Status: {order_resp.status}") | |
| print(f" Submitted At: {order_resp.submitted_at}") | |
| print("===============================================================================\n") | |
| except Exception as err: | |
| print(f"❌ Close order submission failed at Alpaca: {err}") | |
| print("===============================================================================\n") | |
| def handle_orders(argobj): | |
| trade_client = TradingClient(api_key=account_credentials[argobj.acc]["API"], secret_key=account_credentials[argobj.acc]["SECRET"], paper=argobj.acc == "paper") | |
| got_ords = retrieve_orders(trade_client, [argobj.sym]) | |
| for order in got_ords: | |
| if order.order_class == OrderClass.MLEG and len(order.legs) == 2: | |
| print(f"price={order.limit_price} {order.legs[0].position_intent}={order.legs[0].symbol} {order.legs[1].position_intent}={order.legs[1].symbol} status={order.status}") | |
| def handle_owned(argobj): | |
| trade_client = TradingClient(api_key=account_credentials[argobj.acc]["API"], secret_key=account_credentials[argobj.acc]["SECRET"], paper=argobj.acc == "paper") | |
| optclient = OptionHistoricalDataClient(account_credentials[argobj.acc]["API"], account_credentials[argobj.acc]["SECRET"]) | |
| all_positions = [ pos for pos in trade_client.get_all_positions() if pos.asset_class == AssetClass.US_OPTION] | |
| mybps = reconstruct_bull_put_spreads(all_positions) | |
| if argobj.available: | |
| mybps = [bp for bp in mybps if bp.available] | |
| for bp in mybps: | |
| if bp.matches_lo(argobj.long_str) and bp.matches_sh(argobj.short_str): | |
| if argobj.place_close is not None: | |
| bp.close_spread(trade_client, argobj.place_close) | |
| else: | |
| bp.display(optclient, trade_client) | |
| def handle_power(argobj): | |
| trade_client = TradingClient(api_key=account_credentials[argobj.acc]["API"], secret_key=account_credentials[argobj.acc]["SECRET"], paper=argobj.acc == "paper") | |
| account = trade_client.get_account() | |
| print(f"POWER with_margin={account.buying_power} non_margin={account.non_marginable_buying_power} overnight={account.regt_buying_power} fees={account.accrued_fees} maint={account.maintenance_margin} equity={account.equity}") | |
| def handle_search(argobj): | |
| trade_client = TradingClient(api_key=account_credentials[argobj.acc]["API"], secret_key=account_credentials[argobj.acc]["SECRET"], paper=argobj.acc == "paper") | |
| client = OptionHistoricalDataClient(account_credentials[argobj.acc]["API"], account_credentials[argobj.acc]["SECRET"]) | |
| current_date = datetime.now() + timedelta(days=argobj.dte_min) | |
| end_date = datetime.now() + timedelta(days=argobj.dte_max) | |
| data_client = StockHistoricalDataClient(account_credentials[argobj.acc]["API"], account_credentials[argobj.acc]["SECRET"]) | |
| request_params = StockSnapshotRequest( | |
| symbol_or_symbols=argobj.tick, | |
| feed=DataFeed.SIP | |
| ) | |
| snapshot = data_client.get_stock_snapshot(request_params) | |
| stock_data = snapshot[argobj.tick] | |
| # latest_close_price = stock_data.minute_bar.close | |
| latest_ask_price = stock_data.latest_quote.ask_price | |
| latest_bid_price = stock_data.latest_quote.bid_price | |
| mid_price = round((latest_ask_price + latest_bid_price) / 2, 2) | |
| max_strike = mid_price * argobj.str_max | |
| min_strike = mid_price * argobj.str_min | |
| print(f"----[{argobj.tick}]----") | |
| print(f"mid_price={mid_price} min={min_strike} max={max_strike} tick={argobj.tick}") | |
| req = OptionChainRequest(underlying_symbol=argobj.tick, feed=OptionsFeed.OPRA, | |
| expiration_date_gte=current_date.date().isoformat(), expiration_date_lte=end_date.date().isoformat(), | |
| type=ContractType.PUT, strike_price_lte=max_strike, strike_price_gte=min_strike) | |
| resp = client.get_option_chain(req) | |
| chains = list(resp.values()) | |
| chains.sort(key=lambda x: x.symbol) | |
| chains.reverse() | |
| crspreads = [] | |
| print(len(chains)) | |
| for i in range(len(chains) - argobj.str_width): | |
| short_opt = chains[i] | |
| long_opt = chains[i + argobj.str_width] | |
| short_key = short_opt.symbol[:short_opt.symbol.index('P')] | |
| long_key = long_opt.symbol[:long_opt.symbol.index('P')] | |
| if short_key != long_key: | |
| print(f"passing due to key {short_key} {long_key}") | |
| continue | |
| nat_credit = round(float(short_opt.latest_quote.bid_price) - float(long_opt.latest_quote.ask_price), 2) | |
| short_mid = (float(short_opt.latest_quote.bid_price) + float(short_opt.latest_quote.ask_price)) / 2 | |
| long_mid = (float(long_opt.latest_quote.bid_price) + float(long_opt.latest_quote.ask_price)) / 2 | |
| mid_credit = round(short_mid - long_mid, 2) | |
| if mid_credit < argobj.cred_mid: | |
| continue | |
| if argobj.cred_nat != 0.0 and nat_credit < argobj.cred_nat: | |
| continue | |
| short_theta = short_opt.greeks.theta if short_opt.greeks else 0.0 | |
| long_theta = long_opt.greeks.theta if long_opt.greeks else 0.0 | |
| net_theta = round((-short_theta + long_theta) * 100, 4) | |
| crspreads.append(CreditSpread(short_opt.symbol, long_opt.symbol, nat_credit, mid_credit, net_theta)) | |
| proc_lim = argobj.lim if argobj.lim > 0 and argobj.lim <= len(crspreads) else len(crspreads) | |
| for i in range(proc_lim): | |
| print(crspreads[i]) | |
| if argobj.submit_orders == "NAT": | |
| crspreads[i].nat_limit_order(trade_client) | |
| elif argobj.submit_orders == "MID": | |
| crspreads[i].mid_limit_order(trade_client) | |
| elif argobj.submit_orders == "HALF": | |
| crspreads[i].half_limit_order(trade_client) | |
| elif argobj.submit_orders == "MID34": | |
| crspreads[i].three4_limit_order(trade_client) | |
| handler_functions = { | |
| "owned":handle_owned, | |
| "search":handle_search, | |
| "orders":handle_orders, | |
| "power":handle_power | |
| } | |
| if __name__ == '__main__': | |
| args = parse_arguments() | |
| my_func = handler_functions[args.command] | |
| my_func(args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment