Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions scripts/place_option_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
Place a single-leg option order (buy-to-open or sell-to-close).
Account: 490706777

Usage:
# Dry run
python scripts/place_option_order.py XLY 2026-09-18 120 put 1 4.50 buy
# Live
python scripts/place_option_order.py XLY 2026-09-18 120 put 1 4.50 buy --live
python scripts/place_option_order.py XLY 2026-09-18 120 put 1 6.75 sell --live
"""

import os
import sys

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from utils.safe_cash_bot import SafeCashBot # noqa: E402


USAGE = """\
Usage:
python scripts/place_option_order.py SYMBOL EXPIRATION STRIKE TYPE QTY PRICE SIDE [--live]

SYMBOL Underlying ticker (e.g. XLY)
EXPIRATION YYYY-MM-DD (e.g. 2026-09-18)
STRIKE Strike price (e.g. 120)
TYPE put | call
QTY Number of contracts (1 = 100 shares)
PRICE Limit price per contract share (e.g. 4.50)
SIDE buy -> buy-to-open
sell -> sell-to-close (must already hold the contract)
--live Send to broker. Default is DRY RUN.

Examples:
Dry-run open: python scripts/place_option_order.py XLY 2026-09-18 120 put 1 4.50 buy
Live open: python scripts/place_option_order.py XLY 2026-09-18 120 put 1 4.50 buy --live
Live take-profit: python scripts/place_option_order.py XLY 2026-09-18 120 put 1 6.75 sell --live
"""


def main():
if len(sys.argv) < 8:
print(USAGE)
sys.exit(1)
try:
symbol = sys.argv[1].upper()
expiration = sys.argv[2]
strike = float(sys.argv[3])
option_type = sys.argv[4].lower()
quantity = int(sys.argv[5])
price = float(sys.argv[6])
side = sys.argv[7].lower()
dry_run = '--live' not in sys.argv
except (ValueError, IndexError) as e:
print(f"[ERR] Could not parse args: {e}\n")
print(USAGE)
sys.exit(1)

if option_type not in ('put', 'call'):
print(f"[ERR] TYPE must be 'put' or 'call', got {option_type!r}")
sys.exit(1)
if side not in ('buy', 'sell'):
print(f"[ERR] SIDE must be 'buy' or 'sell', got {side!r}")
sys.exit(1)
if quantity <= 0:
print(f"[ERR] QTY must be positive")
sys.exit(1)
if price <= 0:
print(f"[ERR] PRICE must be positive")
sys.exit(1)

bot = SafeCashBot()
try:
if side == 'buy':
order = bot.place_option_buy_limit_order(
symbol=symbol,
expiration=expiration,
strike=strike,
option_type=option_type,
quantity=quantity,
price=price,
dry_run=dry_run,
)
else:
order = bot.place_option_sell_limit_order(
symbol=symbol,
expiration=expiration,
strike=strike,
option_type=option_type,
quantity=quantity,
price=price,
dry_run=dry_run,
)

if order and isinstance(order, dict) and order.get('id'):
print(f"\n[OK] Order ID: {order['id']} state={order.get('state', 'N/A')}\n")
elif dry_run:
print("\n[OK] Dry run completed - no real order placed\n")
else:
print("\n[ERR] Order failed - see error above\n")
finally:
bot.auth.logout()


if __name__ == "__main__":
main()
185 changes: 185 additions & 0 deletions utils/safe_cash_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -1737,6 +1737,191 @@ def get_quote(self, symbol):
print(f"[ERR] Error fetching quote: {e}")
return None

def place_option_buy_limit_order(self, symbol, expiration, strike, option_type,
quantity, price, dry_run=True, time_in_force='gtc'):
"""
Place a single-leg limit BUY (open) order on an equity option.

Args:
symbol: Underlying ticker (e.g. 'XLY')
expiration: 'YYYY-MM-DD' (e.g. '2026-09-18')
strike: Strike price as float (e.g. 120.0)
option_type: 'put' or 'call'
quantity: Number of contracts (1 contract = 100 shares)
price: Limit price per contract share (debit, e.g. 4.50)
dry_run: If True, simulates without execution
time_in_force: 'gtc' or 'gfd'
"""
option_type = option_type.lower()
if option_type not in ('put', 'call'):
print(f"[ERR] option_type must be 'put' or 'call', got {option_type!r}")
return None

notional = quantity * price * 100
print(f"\n{'='*70}")
print(f"OPTION BUY ORDER - {'DRY RUN' if dry_run else 'LIVE'}")
print(f"{'='*70}")
print(f" Account: {self.account_number}")
print(f" Contract: {symbol} {expiration} ${strike:.2f} {option_type.upper()}")
print(f" Quantity: {quantity} contract(s) ({quantity * 100} shares)")
print(f" Limit Price: ${price:.2f} per share")
print(f" Total Debit: ${notional:.2f}")
print(f" TIF: {time_in_force.upper()}")

try:
account = r.profiles.load_account_profile(account_number=self.account_number)
cash = float(account.get('buying_power', 0))
print(f" Buying Power: ${cash:.2f}")
if cash < notional:
print(f"\n[ERR] Insufficient buying power: ${cash:.2f} < ${notional:.2f}")
print(f"{'='*70}\n")
return None
except Exception as e:
print(f" [WARN] Buying-power check failed (proceeding): {e}")

if dry_run:
print("\n[WARN] DRY RUN MODE - Order not executed")
print(" To execute real orders, call with dry_run=False")
print(f"{'='*70}\n")
return None

try:
print("\nExecuting option BUY order...")
order = r.orders.order_buy_option_limit(
positionEffect='open',
creditOrDebit='debit',
price=price,
symbol=symbol,
quantity=quantity,
expirationDate=expiration,
strike=strike,
optionType=option_type,
timeInForce=time_in_force,
account_number=self.account_number,
)
order_id = order.get('id') if isinstance(order, dict) else None
order_state = order.get('state') if isinstance(order, dict) else None
if order_id:
print("[OK] Option buy order placed!")
print(f" Order ID: {order_id}")
print(f" State: {order_state or 'N/A'}")
print(f"{'='*70}\n")
return order
detail = None
if isinstance(order, dict):
detail = order.get('detail') or order.get('non_field_errors') or order.get('message')
print(f"[ERR] Option buy order failed!")
print(f" Reason: {detail or order}")
print(f"{'='*70}\n")
return order
except Exception as e:
print(f"[ERR] Option buy order failed: {e}")
print(f"{'='*70}\n")
return None

def place_option_sell_limit_order(self, symbol, expiration, strike, option_type,
quantity, price, dry_run=True, time_in_force='gtc'):
"""
Place a single-leg limit SELL (close) order on an equity option.

Use this to set a take-profit limit on an existing long option position.

Args:
symbol: Underlying ticker (e.g. 'XLY')
expiration: 'YYYY-MM-DD'
strike: Strike price as float
option_type: 'put' or 'call'
quantity: Number of contracts to sell
price: Limit price per contract share (credit)
dry_run: If True, simulates without execution
time_in_force: 'gtc' or 'gfd'
"""
option_type = option_type.lower()
if option_type not in ('put', 'call'):
print(f"[ERR] option_type must be 'put' or 'call', got {option_type!r}")
return None

notional = quantity * price * 100
print(f"\n{'='*70}")
print(f"OPTION SELL (CLOSE) ORDER - {'DRY RUN' if dry_run else 'LIVE'}")
print(f"{'='*70}")
print(f" Account: {self.account_number}")
print(f" Contract: {symbol} {expiration} ${strike:.2f} {option_type.upper()}")
print(f" Quantity: {quantity} contract(s)")
print(f" Limit Price: ${price:.2f} per share")
print(f" Total Credit: ${notional:.2f}")
print(f" TIF: {time_in_force.upper()}")

try:
positions = r.options.get_open_option_positions(account_number=self.account_number)
held_qty = 0
for pos in positions or []:
inst = pos.get('option') or pos.get('option_id')
if not inst:
continue
try:
if isinstance(inst, str) and inst.startswith('http'):
details = r.helper.request_get(inst)
else:
details = r.options.get_option_instrument_data_by_id(inst)
except Exception:
details = None
if not details:
continue
if (details.get('chain_symbol') == symbol
and details.get('expiration_date') == expiration
and float(details.get('strike_price', 0)) == float(strike)
and details.get('type') == option_type):
held_qty = int(float(pos.get('quantity', 0)))
break
print(f" Held: {held_qty} contract(s)")
if held_qty < quantity:
print(f"\n[ERR] Insufficient open contracts to close: held {held_qty} < sell {quantity}")
print(f"{'='*70}\n")
return None
except Exception as e:
print(f" [WARN] Position check failed (proceeding): {e}")

if dry_run:
print("\n[WARN] DRY RUN MODE - Order not executed")
print(" To execute real orders, call with dry_run=False")
print(f"{'='*70}\n")
return None

try:
print("\nExecuting option SELL (close) order...")
order = r.orders.order_sell_option_limit(
positionEffect='close',
creditOrDebit='credit',
price=price,
symbol=symbol,
quantity=quantity,
expirationDate=expiration,
strike=strike,
optionType=option_type,
timeInForce=time_in_force,
account_number=self.account_number,
)
order_id = order.get('id') if isinstance(order, dict) else None
order_state = order.get('state') if isinstance(order, dict) else None
if order_id:
print("[OK] Option sell order placed!")
print(f" Order ID: {order_id}")
print(f" State: {order_state or 'N/A'}")
print(f"{'='*70}\n")
return order
detail = None
if isinstance(order, dict):
detail = order.get('detail') or order.get('non_field_errors') or order.get('message')
print(f"[ERR] Option sell order failed!")
print(f" Reason: {detail or order}")
print(f"{'='*70}\n")
return order
except Exception as e:
print(f"[ERR] Option sell order failed: {e}")
print(f"{'='*70}\n")
return None

def run_example(self):
"""Example usage of the bot"""
try:
Expand Down
Loading