| Overall Statistics |
|
Total Orders 225 Average Win 2.30% Average Loss -1.73% Compounding Annual Return 11.489% Drawdown 35.900% Expectancy 0.350 Start Equity 100000 End Equity 172179.65 Net Profit 72.180% Sharpe Ratio 0.294 Sortino Ratio 0.366 Probabilistic Sharpe Ratio 7.109% Loss Rate 42% Win Rate 58% Profit-Loss Ratio 1.33 Alpha 0.009 Beta 0.739 Annual Standard Deviation 0.205 Annual Variance 0.042 Information Ratio -0.052 Tracking Error 0.18 Treynor Ratio 0.081 Total Fees $1290.80 Estimated Strategy Capacity $220000.00 Lowest Capacity Asset VSTA XGMI1RX3ULPH Portfolio Turnover 0.53% Drawdown Recovery 430 |
# region imports
from AlgorithmImports import *
# endregion
class VerticalTachyonRegulators(QCAlgorithm):
def initialize(self):
self.set_start_date(self.end_date - timedelta(5*365))
self.set_cash(100_000)
self.settings.seed_initial_prices = True
# Add a universe of US Equities.
self._universe_size = 25
self._date_rule = self.date_rules.year_start("SPY")
self.universe_settings.resolution = Resolution.DAILY
self.universe_settings.schedule.on(self._date_rule)
self.add_universe(self._select)
# Define the factors and their weights.
self._factor_by_weights = {'quality': 0, 'value': 2/3, 'size': 1/3}
self._specs = {
'quality': [
# True means higher value should translate to higher portfolio weight.
(lambda x: x.operation_ratios.quick_ratio.value, True, 1)
],
'value': [
(lambda x: x.valuation_ratios.total_yield, True, 1),
(lambda x: x.valuation_ratios.earning_yield, True, 1),
(lambda x: x.operation_ratios.total_assets_growth.one_year, False, 1),
(lambda x: x.valuation_ratios.ev_to_ebit, False, 1),
(lambda x: x.valuation_ratios.book_value_yield, True, 0.5),
],
'size': [
(lambda x: x.market_cap, False, 1)
]
}
# Add warmup so the algorithm trades on deployment.
self.set_warmup(timedelta(400))
def _select(self, fundamentals: List[Fundamental]) -> List[Symbol]:
# Filter to top 50k most liquid stocks with fundamental data.
top_liquid = sorted(
[f for f in fundamentals if f.has_fundamental_data],
key=lambda f: f.dollar_volume
)[-50_000:]
# Select small-cap value stocks with positive valuation metrics.
small_cap_value = [
f for f in top_liquid
if (80_000_000 < f.market_cap < 1_000_000_000 and
f.valuation_ratios.book_value_yield > 0 and
f.valuation_ratios.total_yield > 0 and
f.valuation_ratios.earning_yield > 0 and
f.valuation_ratios.ev_to_ebit > 0)
]
if not small_cap_value:
return []
# Rank all the assets by their factor values.
factor_scores = {
name: self._rank_by_factors(small_cap_value, fs)
for name, fs in self._specs.items()
}
score_by_symbol = {
f.symbol: sum(
factor_scores[name][f.symbol] * self._factor_by_weights[name]
for name in self._specs
)
for f in small_cap_value
}
# Select the assets with the best scores.
self._universe = sorted(score_by_symbol, key=lambda symbol: score_by_symbol[symbol], reverse=True)[:self._universe_size]
return self._universe
def _rank_by_factors(self, securities: List[Fundamental], factors: List[tuple]) -> dict:
# Normalize factor weights.
weights = [f[2] for f in factors]
total = sum(weights)
weights = [w / total for w in weights]
# Compute rank dictionaries for each factor.
rank_dicts = []
for accessor, higher_is_better, _ in factors:
ranked = sorted(securities, key=accessor, reverse=not higher_is_better)
rank_dicts.append({s.symbol: i for i, s in enumerate(ranked)})
# Combine ranks across factors with weighted average.
return {
s.symbol: sum(
rank_dicts[j].get(s.symbol) * weights[j]
for j in range(len(factors))
)
for s in securities
}
def on_warmup_finished(self):
# Add a Scheduled event to rebalance the portfolio yearly.
time_rule = self.time_rules.at(8, 0)
self.schedule.on(self._date_rule, time_rule, self._rebalance)
# Rebalance the portfolio today too.
self._rebalance()
def _rebalance(self):
# Apply quadratic weighting (highest rank gets highest weight).
weights = {
symbol: float(len(self._universe) - i) ** 2
for i, symbol in enumerate(self._universe)
}
# Make weights sum to 1.
total_weight = sum(weights.values())
weights = {symbol: w / total_weight for symbol, w in weights.items()}
# Place orders to rebalance the portfolio.
targets = [PortfolioTarget(symbol, weight) for symbol, weight in weights.items()]
self.set_holdings(targets, True)