| Overall Statistics |
|
Total Orders 552 Average Win 2.84% Average Loss -1.74% Compounding Annual Return 7.075% Drawdown 1.300% Expectancy -0.238 Start Equity 10000000 End Equity 12487012.79 Net Profit 24.870% Sharpe Ratio -0.198 Sortino Ratio -0.249 Probabilistic Sharpe Ratio 98.614% Loss Rate 71% Win Rate 29% Profit-Loss Ratio 1.63 Alpha -0.008 Beta 0.027 Annual Standard Deviation 0.027 Annual Variance 0.001 Information Ratio -0.714 Tracking Error 0.126 Treynor Ratio -0.195 Total Fees $206225.48 Estimated Strategy Capacity $2100000.00 Lowest Capacity Asset NVDA Z0J3SWDEYSVA|NVDA RHM8UTD8DT2D Portfolio Turnover 22.84% Drawdown Recovery 28 |
'''
Dynamic Version of the Arbitrage stock model
Includes contract swapping when onhand AR + cost to exit + optimization < market AR
Strategy Design by Thomas Deng
Programmed by Thomas Deng & Eli Webster
'''
from AlgorithmImports import *
import math
from collections import defaultdict
class DynamicAR(QCAlgorithm):
def Initialize(self):
# Simulation date, currently 2023 - 2026 End of Q1
self.set_start_date(2023, 1, 1)
self.set_end_date(2026, 3, 31)
self.set_cash(10000000)
# Disable fees and margin call (Auto Liquidate)
self.portfolio.margin_call_model = MarginCallModel.NULL
#self.set_security_initializer(lambda s: s.SetFeeModel(ConstantFeeModel(0)))
# Money Market Variables
self.shv = self.add_equity("SHV").Symbol
self.in_money_market = False
# Mag 7 stocks only
self.tickers = ["AAPL", "MSFT", "NVDA", "AMZN", "META", "GOOGL", "TSLA"]
self.underlyings = {}
self.option_symbols = {}
self.current_position = None
# Minimum AR threshold to buy contract
self.min_ar = 5
# Optimization hurdle: minimum AR improvement required to justify a swap
self.swap_optimization = 3.0
self.trade_history = []
# Go through selected tickers and get contracts according to filters
for ticker in self.tickers:
equity = self.add_equity(ticker)
self.underlyings[ticker] = equity.Symbol
option = self.add_option(ticker)
option.SetFilter(lambda u: u.include_weeklys().Expiration(7, 36))
self.option_symbols[ticker] = option.Symbol
# Morning entry window
times = [(9, 30), (9, 35), (9, 40), (9, 45), (9, 50), (9, 55), (10, 0)]
for h, m in times:
self.schedule.on(
self.date_rules.every_day(),
self.time_rules.at(h, m),
self.CheckForEntry
)
# Expiry-day liquidation at 3:58pm (2 min before close)
# Then market orders have time to fill before 4pm
self.schedule.on(
self.date_rules.every_day(),
self.time_rules.before_market_close("AAPL", 2),
self.CheckExpiry
)
# Check if contract expiring
def CheckExpiry(self):
if self.current_position is None:
return
today = self.time.date()
expiry = self.current_position["expiry"]
if today == expiry:
#self.debug(f"Expiry day: liquidating at 3:58pm before close")
self.liquidate()
self.current_position = None
def CheckForEntry(self):
# Find the best available market opportunity
best_trade = self.FindBestTrade()
# If holding a position, evaluate whether to swap or hold
if self.current_position is not None:
# Auto-liquidate if expired
if self.time.date() >= self.current_position["expiry"]:
self.liquidate()
#self.debug("Position expired: liquidated all holdings")
self.current_position = None
return
ticker = self.current_position["trade_record"]["ticker"]
underlying = self.underlyings[ticker]
# Detect early assignment: stock position gone but expiry not reached
stock_qty = self.portfolio[underlying].quantity
if stock_qty == 0 and self.time.date() < self.current_position["expiry"]:
#self.debug(f"Early assignment detected on {ticker}: liquidating remaining legs")
self.liquidate()
self.current_position = None
return
# Compute hand AR (AR of the position we currently hold)
hand_ar = self.GetHandAR()
# Compute exit cost (bid-ask spread cost of unwinding, annualized)
exit_cost = self.GetExitCost()
market_ar = 0.0
if best_trade:
market_ar = best_trade["ar"]
total_current = hand_ar + exit_cost + self.swap_optimization
# self.debug(
# f"Hand AR: {hand_ar:.2f}% | Exit Cost: {exit_cost:.2f}% | "
# f"Optimization Hurdle: {self.swap_optimization:.2f}% | "
# f"Total Required: {total_current:.2f}% | Market AR: {market_ar:.2f}%"
# )
# Check if best contract beats optimized total
if best_trade is not None and market_ar > total_current:
improvement = market_ar - total_current
#self.debug(f"SWAP - Improvement: {improvement:.2f}%")
self.SwapPosition(best_trade)
#else:
#self.debug("HOLD - current position is still optimal")
return
# No contracts
if best_trade is None:
return
# Buy money market if cant reach threshold
if best_trade["ar"] < self.min_ar:
if not self.in_money_market:
cash = self.portfolio.margin_remaining
shv_price = self.securities[self.shv].AskPrice
if shv_price <= 0:
shv_price = self.securities[self.shv].Price
shares = int(cash / shv_price)
if shares > 0:
self.market_order(self.shv, shares)
self.in_money_market = True
#self.debug(f"No qualifying AR: parked {shares} shares in SHV @ ${shv_price:.2f}")
return
else:
# Beating threshold, sell money market
if self.in_money_market:
self.liquidate(self.shv)
self.in_money_market = False
#self.debug("AR threshold met: exited SHV")
self.EnterPosition(best_trade)
# Scan all tickers and return the best (call, put, ar) combo dict
def FindBestTrade(self):
best_trade = None
# Scan all contract chains in selected tickers, finding best contract
for ticker in self.tickers:
chain = self.current_slice.option_chains.get(self.option_symbols[ticker])
if not chain:
continue
result = self.GetBestContract(chain, ticker)
if not result:
continue
call, put, ar = result
# Save best contract
if best_trade is None or ar > best_trade["ar"]:
best_trade = {
"ticker": ticker,
"call": call,
"put": put,
"ar": ar
}
return best_trade
# Execute entry into a new position
def EnterPosition(self, trade):
ticker = trade["ticker"]
call = trade["call"]
put = trade["put"]
underlying = self.underlyings[ticker]
# A = Ask Price of Stock
underlying_price = self.securities[underlying].AskPrice
if underlying_price <= 0:
underlying_price = self.securities[underlying].Price
# C = Bid Price of Call
call_bid = call.BidPrice
# P = Ask Price of Put
put_ask = put.AskPrice
cash = self.portfolio.margin_remaining
# Cost per combo: Buy stock at Ask + Buy put at Ask - Sell call at Bid
combo_price = (underlying_price * 100) + (put_ask * 100) - (call_bid * 100)
combos = int(cash * .95 / combo_price)
if combos < 1:
self.debug("Insufficient cash to enter position")
return
legs = [
Leg.create(underlying, 100),
Leg.create(put.Symbol, 1),
Leg.create(call.Symbol, -1)
]
self.combo_market_order(legs, combos)
self.debug(
f"POST-FILL | Stock: {self.portfolio[underlying].quantity} shares | "
f"Call qty: {self.portfolio[call.Symbol].quantity} | "
f"Put qty: {self.portfolio[put.Symbol].quantity}"
)
trade_record = {
"entry_time": self.time,
"ticker": ticker,
"call_symbol": call.Symbol,
"put_symbol": put.Symbol,
"strike": call.Strike,
"expiry": call.Expiry.date(),
"ar": trade["ar"],
"combos": combos,
"entry_stock_price": underlying_price,
"entry_call_bid": call_bid,
"entry_put_ask": put_ask,
"combo_cost": combo_price
}
self.trade_history.append(trade_record)
self.current_position = {
"expiry": call.Expiry.date(),
"trade_record": trade_record
}
self.debug(
f"Trade: {self.time} | {ticker} | "
f"Strike: {call.Strike} | Expiry: {call.Expiry.date()} | "
f"AR: {trade['ar']:.2f}% | Combos: {combos}"
)
self.debug(
f" -> Call: {call.Symbol} @ ${call_bid:.2f} | "
f"Put: {put.Symbol} @ ${put_ask:.2f} | "
f"Stock Ask: ${underlying_price:.2f}"
)
# Liquidate current position and enter a new one
def SwapPosition(self, new_trade):
old_ticker = self.current_position["trade_record"]["ticker"]
new_ticker = new_trade["ticker"]
#self.debug(f"Swapping from {old_ticker} to {new_ticker}")
self.liquidate()
self.current_position = None
self.EnterPosition(new_trade)
# Mark latest trade history entry as a swap
if self.trade_history:
self.trade_history[-1]["action"] = "SWAP"
# AR of the position currently held (using current market prices)
# Formula mirrors GetBestContract: ((C - P) + (S - A)) * 100 / (A * T)
def GetHandAR(self):
if self.current_position is None:
return 0.0
record = self.current_position["trade_record"]
ticker = record["ticker"]
call = record["call_symbol"]
put = record["put_symbol"]
underlying = self.underlyings[ticker]
if (call not in self.securities or
put not in self.securities or
underlying not in self.securities):
return 0.0
# C = Bid Price of Call (what we can sell our held call for)
C = self.securities[call].BidPrice
# P = Ask Price of Put (what it costs to close our short put)
P = self.securities[put].AskPrice
# A = Ask Price of Stock
A = self.securities[underlying].AskPrice
if A <= 0:
A = self.securities[underlying].Price
S = record["strike"]
if C <= 0 or P <= 0 or A <= 0:
return 0.0
expiry = self.current_position["expiry"]
days_to_expiry = (expiry - self.time.date()).days
T = max(days_to_expiry / 365.25, 1 / 365)
hand_ar = ((C - P) + (S - A)) * 100 / (A * T)
return hand_ar
# Annualized cost of exiting the current position (bid-ask spread)
# Formula: (put_spread + call_spread) * 100 / (T * A_bid)
def GetExitCost(self):
if self.current_position is None:
return 0.0
record = self.current_position["trade_record"]
ticker = record["ticker"]
call_sym = record["call_symbol"]
put_sym = record["put_symbol"]
underlying = self.underlyings[ticker]
if (call_sym not in self.securities or
put_sym not in self.securities or
underlying not in self.securities):
return 0.0
call = self.securities[call_sym]
put = self.securities[put_sym]
stock = self.securities[underlying]
call_ask = call.AskPrice
call_bid = call.BidPrice
put_ask = put.AskPrice
put_bid = put.BidPrice
stock_bid = stock.BidPrice
if call_ask <= 0 or call_bid <= 0 or put_ask <= 0 or put_bid <= 0 or stock_bid <= 0:
return 0.0
expiry = self.current_position["expiry"]
days_to_expiry = (expiry - self.time.date()).days
T = max(days_to_expiry / 365.25, 1 / 365)
put_spread = put_ask - put_bid
call_spread = call_ask - call_bid
exit_cost = (put_spread + call_spread) * 100 / (T * stock_bid)
return exit_cost
# For a given option chain, return (call, put, ar) with highest AR
def GetBestContract(self, chain, ticker):
pairs = defaultdict(dict)
today = self.time.date()
underlying = self.underlyings[ticker]
# A = Ask Price of Stock (use Price if Ask not available)
underlying_price = self.securities[underlying].AskPrice
if underlying_price <= 0:
underlying_price = self.securities[underlying].Price
for contract in chain:
key = (contract.Strike, contract.Expiry.date())
pairs[key][contract.Right] = contract
# Initalize starting variables
best = None
best_ar = -math.inf
for (strike, expiry), legs in pairs.items():
# Skip contracts expiring today
if expiry == today:
continue
call = legs.get(OptionRight.CALL)
put = legs.get(OptionRight.PUT)
if not call or not put:
continue
# S = Strike Price
# C = Bid Price of Call
# P = Ask Price of Put
S = strike
C = call.BidPrice
P = put.AskPrice
# Net premium must be positive
net_premium = C - P
if net_premium < 0:
continue
# Exclude P = 0.00 and C = 0.00
if C <= 0 or P <= 0:
continue
# Filter strikes within +-10% of underlying
if S < 0.90 * underlying_price or S > 1.10 * underlying_price:
continue
# T = Time Till Expiry (Years)
days_to_expiry = (expiry - today).days
T = max(days_to_expiry / 365.25, 1 / 365)
# AR = Annualized Return (%)
A = underlying_price
ar = ((C - P) + (S - A)) * 100 / (A * T)
if ar > best_ar:
best_ar = ar
best = (call, put, ar)
return best
# Log trades, summary, and add to txt file
def OnEndOfAlgorithm(self):
lines = []
lines.append("=" * 80)
lines.append("TRADE HISTORY")
lines.append("=" * 80)
for i, trade in enumerate(self.trade_history, 1):
action = trade.get("action", "ENTER")
lines.append(
f"Trade {i} [{action}]: {trade['entry_time']} | {trade['ticker']} | "
f"Strike: {trade['strike']} | Expiry: {trade['expiry']} | "
f"AR: {trade['ar']:.2f}% | Combos: {trade['combos']}"
)
lines.append(
f" -> Call: {trade['call_symbol']} @ ${trade['entry_call_bid']:.2f} | "
f"Put: {trade['put_symbol']} @ ${trade['entry_put_ask']:.2f} | "
f"Stock Ask: ${trade['entry_stock_price']:.2f}"
)
lines.append("=" * 80)
lines.append(f"Total Trades: {len(self.trade_history)}")
if self.trade_history:
avg_ar = sum(t["ar"] for t in self.trade_history) / len(self.trade_history)
max_ar = max(t["ar"] for t in self.trade_history)
min_ar = min(t["ar"] for t in self.trade_history)
lines.append(f"Average AR: {avg_ar:.2f}%")
lines.append(f"Max AR: {max_ar:.2f}%")
lines.append(f"Min AR: {min_ar:.2f}%")
lines.append(f"Final Portfolio Value: ${self.portfolio.total_portfolio_value:,.2f}")
lines.append(f"Total Return: {((self.portfolio.total_portfolio_value - 10000000) / 10000000 * 100):.2f}%")
lines.append(f"Total Fees: ${self.portfolio.total_fees:.2f}")
lines.append("=" * 80)
content = "\n".join(lines)
self.object_store.save("trade_history.txt", content)
self.debug("Trade history saved to object store: trade_history.txt")
for line in lines:
self.debug(line)