| Overall Statistics |
|
Total Orders 666 Average Win 1.27% Average Loss -0.37% Compounding Annual Return 7.830% Drawdown 26.600% Expectancy 0.951 Start Equity 100000 End Equity 105378.19 Net Profit 5.378% Sharpe Ratio 0.117 Sortino Ratio 0.105 Probabilistic Sharpe Ratio 25.767% Loss Rate 56% Win Rate 44% Profit-Loss Ratio 3.47 Alpha -0.076 Beta 0.915 Annual Standard Deviation 0.23 Annual Variance 0.053 Information Ratio -0.412 Tracking Error 0.208 Treynor Ratio 0.029 Total Fees $511.97 Estimated Strategy Capacity $0 Lowest Capacity Asset NB YL7PGIXPX6CM|NB R735QTJ8XC9X Portfolio Turnover 8.73% |
# region imports
from AlgorithmImports import *
from itertools import groupby
# endregion
class MuscularVioletFox(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2024, 1, 1)
self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))
date_rules = self.date_rules.every(DayOfWeek.MONDAY)
self.universe_settings.schedule.on(date_rules)
self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW
self._universe = self.add_universe(self._fundamental_selection_function)
# Schedule a filtering on Monday morning every week
spy = Symbol.create("SPY", SecurityType.EQUITY, Market.USA)
self.schedule.on(date_rules, self.time_rules.after_market_open(spy, 30), self.filter_and_trade)
def _fundamental_selection_function(self, fundamental: List[Fundamental]) -> List[Symbol]:
# Filter for securities with (price > $10), has fundamental data, and has a price_to_earnings ratio.
filtered = [f for f in fundamental if f.price > 10 and f.has_fundamental_data and not np.isnan(f.valuation_ratios.pe_ratio)]
# Sort filtered securities by dollar volume in decending order, keep the top 100 (securities with the most dollar volume).
sorted_by_dollar_volume = sorted(filtered, key=lambda f: f.dollar_volume, reverse=True)[:100]
# Sort the previously sorted 100 by P/E ratio, keep the securities with the lowest 10 P/E ratios.
sorted_by_pe_ratio = sorted(sorted_by_dollar_volume, key=lambda f: f.valuation_ratios.pe_ratio, reverse=False)[:10]
# Return the final selected securities
return [f.symbol for f in sorted_by_pe_ratio]
def on_securities_changed(self, changes):
for security in [x for x in changes.added_securities if x.type==SecurityType.EQUITY]:
security.dividend_yield_provider = DividendYieldProvider(security.symbol)
security.contracts = []
# When we remove the under
for security in [x for x in changes.removed_securities if x.type==SecurityType.EQUITY]:
self.liquidate(security.symbol)
for contract in security.contracts:
self.remove_option_contract(contract)
def filter_and_trade(self) -> None:
keyfunc = lambda x: x.id.strike_price
option_model = OptionPricingModelType.FORWARD_TREE
for symbol in self._universe.selected:
# Get all option contracts trading
contract_symbols = self.option_chain_provider.get_option_contract_list(symbol, self.time)
if not contract_symbols:
message = f'No options for {symbol} on {self.time}'
#self.log(message)
continue
# Get the first expiry that just over 7 days later and within 1 year
expiry = min(x.id.date for x in contract_symbols if x.id.date > self.time + timedelta(7))
# Get contracts expires on the selected expiry
contract_symbols = [x for x in contract_symbols if x.id.date == expiry]
dividend_yield_model = self.securities[symbol].dividend_yield_provider
def get_delta(contracts):
# Mirror option contract pairs
call, put = contracts
if call.id.option_right == OptionRight.PUT:
call, put = put, call
delta = Delta(call, self.risk_free_interest_rate_model, dividend_yield_model, put, option_model)
self.indicator_history(delta, [call, put, call.Underlying], 1)
return call, delta.current.value
# Use strike price to group option contracts and get the delta
delta_by_call = dict([get_delta(group) for strike, group in groupby(sorted(contract_symbols, key=keyfunc), keyfunc)])
# Get the strike closest to delta of 0.75
call, delta = sorted(delta_by_call.items(), key=lambda x: abs(x[1]-0.75))[0]
# Let's not buy more calls
option = self.securities.get(call)
if option and option.invested:
continue
# Subscribe and buy one contract
self.add_option_contract(call)
self.market_order(call, 1, tag=f"delta={delta}")
# Save the contracts to the underlying to liquidate and remove the subscrition when the underlying is removed
self.securities[symbol].contracts.append(call)