| Overall Statistics |
|
Total Orders 372 Average Win 4.80% Average Loss -4.51% Compounding Annual Return 87.544% Drawdown 53.700% Expectancy 0.436 Start Equity 1000000 End Equity 26582134.24 Net Profit 2558.213% Sharpe Ratio 1.437 Sortino Ratio 0.957 Probabilistic Sharpe Ratio 64.236% Loss Rate 30% Win Rate 70% Profit-Loss Ratio 1.07 Alpha 0.711 Beta -0.107 Annual Standard Deviation 0.489 Annual Variance 0.239 Information Ratio 1.194 Tracking Error 0.525 Treynor Ratio -6.564 Total Fees $215639.94 Estimated Strategy Capacity $2300000.00 Lowest Capacity Asset GIG XNAR4L6AIOV9 Portfolio Turnover 6.17% |
# region imports
from AlgorithmImports import *
from sklearn.cluster import KMeans
from sklearn.preprocessing import MinMaxScaler
# endregion
class MuscularFluorescentPinkPanda(QCAlgorithm):
def initialize(self):
self.set_start_date(2020, 1, 1)
self.set_cash(1_000_000)
self.universe_settings.extended_market_hours = True
self._universe = self.add_universe(self._select_assets)
self._spy = self.add_equity('SPY')
self.schedule.on(self.date_rules.every_day(self._spy.symbol), self.time_rules.before_market_open(self._spy.symbol, 30), self._rebalance)
self._liquidity_filter_size = self.get_parameter('liquidity_filter_size', 100)
self._roc_threshold = self.get_parameter('roc_threshold', 0.1)
self._clusters = self.get_parameter('clusters', 5)
self._training_data_period = timedelta(self.get_parameter('training_data_days', 365)) # Calendar days
self._hold_duration = self.get_parameter('hold_duration', 3) # Trading days
self._max_positions = 3
self._scaler = MinMaxScaler(feature_range=(0, 1))
self._kmeans = KMeans(n_clusters=self._clusters, random_state=42)
self._trades = []
self._columns = ['end_time', 'symbol', 'price_volatility', 'volume_volatility', 'pct_above_vwap', 'vwap_deviation', 'fraction_of_vol_below_vwap']
self._factor_history = pd.DataFrame(columns=self._columns).set_index(['end_time', 'symbol'])
self.set_warm_up(self._training_data_period)
def _select_assets(self, fundamentals):
# Select the most liquid assets.
symbols = [f.symbol for f in sorted(fundamentals, key=lambda f: f.dollar_volume)[-self._liquidity_filter_size:]]
# Select the subset of symbols that had >=10% growth yesterday from open to close.
history = self.history(symbols, 1, Resolution.DAILY)
symbols = [idx[0] for idx in history[(history.close / history.open - 1) >= self._roc_threshold].index]
# Get the data we'll need to calculate the factors.
vwap_by_symbol = {s: IntradayVwap('') for s in symbols}
df_by_symbol = {s: pd.DataFrame(columns=['close', 'vwap', 'volume']) for s in symbols}
for bars in self.history[TradeBar](symbols, self._spy.exchange.hours.get_previous_market_open(self.time, False), self.time, Resolution.MINUTE, extended_market_hours=False):
for symbol, bar in bars.items():
vwap = vwap_by_symbol[symbol]
if vwap.update(bar):
df_by_symbol[symbol].loc[bar.end_time] = [bar.close, vwap.current.value, bar.volume]
# Calculate factors for the selected assets.
# The clustering process will try to minimize these.
factors = pd.DataFrame(columns=self._columns)
for symbol, df in df_by_symbol.items():
if df.empty:
continue
factors.loc[len(factors)] = [
self.time,
symbol,
df.close.std() / df.close.mean(), # price_volatility
df.volume.std() / df.volume.mean(), # volume_volatility
(df.close > df.vwap).astype(int).sum() / len(df), # pct_above_vwap
np.mean(np.abs(df.close - df.vwap)) / df.vwap.mean(), # vwap_deviation
df[df.close < df.vwap].volume.sum() / df.volume.sum() # fraction_of_vol_below_vwap
]
if factors.empty:
return []
factors.set_index(['end_time', 'symbol'], inplace=True)
# Define the universe.
universe = []
if not self.is_warming_up:
# Fit the scaler and k-means model to the training data.
self._kmeans.fit(self._scaler.fit_transform(self._factor_history)) # Drop the `time` column
# Find the cluster that's closest to the origin.
closest_cluster_idx = np.argmin(np.linalg.norm(self._kmeans.cluster_centers_, axis=1))
# Predict the cluster of the current universe.
cluster_labels = self._kmeans.predict(self._scaler.transform(factors))
# Select the subset of asset in the universe that are in the cluster closest to the origin.
universe = [factors.index.levels[1][i] for i, label in enumerate(cluster_labels) if label == closest_cluster_idx]
# Append the latest factor samples to the history.
self._factor_history = pd.concat([self._factor_history, factors], axis=0).reset_index()
# Trim off samples that have fallen out of the lookback window (1 year).
self._factor_history = self._factor_history[self._factor_history.end_time >= self.time-self._training_data_period].set_index(['end_time', 'symbol'])
# Return the assets selected for the universe.
return universe
def _rebalance(self):
# Scan for exits.
closed_trades = []
for i, trade in enumerate(self._trades):
trade.scan(self)
if trade.closed:
closed_trades.append(i)
# Delete closed trades
for i in closed_trades[::-1]:
del self._trades[i]
# Scan for entries.
if self._universe.selected and len(self._trades) < self._max_positions:
for symbol in self._universe.selected:
self._trades.append(Trade(self, symbol, self._hold_duration, -1/self._max_positions))
self.plot('Trades', 'Open', len(self._trades))
class Trade:
def __init__(self, algorithm, symbol, hold_duration, weight):
self._symbol = symbol
# Determine position size
self.closed = True
price = algorithm.securities[symbol].price
if not price:
return
self._quantity = algorithm.calculate_order_quantity(symbol, weight)
if self._quantity == 0:
return
self.closed = False
# Enter trade
algorithm.market_order(symbol, self._quantity)
# Set variable for exit logic
self._hold_duration = hold_duration
def scan(self, algorithm):
if self.closed:
return
self._hold_duration -= 1
if self._hold_duration <= 0:
algorithm.market_order(self._symbol, -self._quantity)
self.closed = True