Overall Statistics |
Total Orders 210 Average Win 0.61% Average Loss -0.48% Compounding Annual Return 5.022% Drawdown 16.400% Expectancy -0.155 Start Equity 1000000 End Equity 1050465.18 Net Profit 5.047% Sharpe Ratio -0.013 Sortino Ratio -0.014 Probabilistic Sharpe Ratio 18.481% Loss Rate 63% Win Rate 37% Profit-Loss Ratio 1.27 Alpha 0 Beta 0 Annual Standard Deviation 0.198 Annual Variance 0.039 Information Ratio 0.257 Tracking Error 0.198 Treynor Ratio 0 Total Fees $1077.30 Estimated Strategy Capacity $8400000000.00 Lowest Capacity Asset ES YGT6HGVF2SQP Portfolio Turnover 16.68% |
#region imports from AlgorithmImports import * from datetime import timedelta from utils import get_position_size from futures import categories #endregion import pandas as pd class FastTrendFollowingLongAndShortWithTrendStrenthAlphaModel(AlphaModel): _futures = [] _BUSINESS_DAYS_IN_YEAR = 256 _FORECAST_SCALAR_BY_SPAN = {64: 1.91, 32: 2.79, 16: 4.1, 8: 5.95, 4: 8.53, 2: 12.1} # Given by author on https://gitfront.io/r/user-4000052/iTvUZwEUN2Ta/AFTS-CODE/blob/chapter7.py def __init__(self, algorithm, slow_ema_span, abs_forecast_cap, sigma_span, target_risk, blend_years): self._algorithm = algorithm self._slow_ema_span = slow_ema_span self._fast_ema_span = int(self._slow_ema_span / 4) # "Any ratio between the two moving average lengths of two and six gives statistically indistinguishable results." (p.165) self._annulaization_factor = self._BUSINESS_DAYS_IN_YEAR ** 0.5 self._abs_forecast_cap = abs_forecast_cap self._sigma_span = sigma_span self._target_risk = target_risk self._blend_years = blend_years self._idm = 1.5 # Instrument Diversification Multiplier. Hardcoded in https://gitfront.io/r/user-4000052/iTvUZwEUN2Ta/AFTS-CODE/blob/chapter8.py self._forecast_scalar = self._FORECAST_SCALAR_BY_SPAN[self._fast_ema_span] self._categories = categories self._total_lookback = timedelta(365*self._blend_years+self._slow_ema_span) self._day = -1 def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]: # Record the new contract in the continuous series if data.quote_bars.count: for future in self._futures: future.latest_mapped = future.mapped # If warming up and still > 7 days before start date, don't do anything # We use a 7-day buffer so that the algorithm has active insights when warm-up ends if algorithm.start_date - algorithm.time > timedelta(7): return [] if self._day == data.time.day or data.bars.count == 0: return [] # Estimate the standard deviation of % daily returns for each future sigma_pct_by_future = {} for future in self._futures: # Estimate the standard deviation of % daily returns sigma_pct = self._estimate_std_of_pct_returns(future.raw_history, future.adjusted_history) if sigma_pct is None: continue sigma_pct_by_future[future] = sigma_pct # Create insights insights = [] weight_by_symbol = get_position_size({future.symbol: self._categories[future.symbol] for future in sigma_pct_by_future.keys()}) for symbol, instrument_weight in weight_by_symbol.items(): future = algorithm.securities[symbol] current_contract = algorithm.securities[future.mapped] daily_risk_price_terms = sigma_pct_by_future[future] / (self._annulaization_factor) * current_contract.price # "The price should be for the expiry date we currently hold (not the back-adjusted price)" (p.55) # Calculate target position position = (algorithm.portfolio.total_portfolio_value * self._idm * instrument_weight * self._target_risk) /(future.symbol_properties.contract_multiplier * daily_risk_price_terms * (self._annulaization_factor)) # Adjust target position based on forecast risk_adjusted_ewmac = future.ewmac.current.value / daily_risk_price_terms scaled_forecast_for_ewmac = risk_adjusted_ewmac * self._forecast_scalar forecast = max(min(scaled_forecast_for_ewmac, self._abs_forecast_cap), -self._abs_forecast_cap) if forecast * position == 0: continue # Save some data for the PCM current_contract.forecast = forecast current_contract.position = position # Create the insights local_time = Extensions.convert_to(algorithm.time, algorithm.time_zone, future.exchange.time_zone) expiry = future.exchange.hours.get_next_market_open(local_time, False) - timedelta(seconds=1) insights.append(Insight.price(future.mapped, expiry, InsightDirection.UP if forecast * position > 0 else InsightDirection.DOWN)) if insights: self._day = data.time.day return insights def _estimate_std_of_pct_returns(self, raw_history, adjusted_history): # Align history of raw and adjusted prices idx = sorted(list(set(adjusted_history.index).intersection(set(raw_history.index)))) adjusted_history_aligned = adjusted_history.loc[idx] raw_history_aligned = raw_history.loc[idx] # Calculate exponentially weighted standard deviation of returns returns = adjusted_history_aligned.diff().dropna() / raw_history_aligned.shift(1).dropna() rolling_ewmstd_pct_returns = returns.ewm(span=self._sigma_span, min_periods=self._sigma_span).std().dropna() if rolling_ewmstd_pct_returns.empty: # Not enough history return None # Annualize sigma estimate annulized_rolling_ewmstd_pct_returns = rolling_ewmstd_pct_returns * (self._annulaization_factor) # Blend the sigma estimate (p.80) blended_estimate = 0.3*annulized_rolling_ewmstd_pct_returns.mean() + 0.7*annulized_rolling_ewmstd_pct_returns.iloc[-1] return blended_estimate def _consolidation_handler(self, sender: object, consolidated_bar: TradeBar) -> None: security = self._algorithm.securities[consolidated_bar.symbol] end_date = consolidated_bar.end_time.date() if security.symbol.is_canonical(): # Update adjusted history security.adjusted_history.loc[end_date] = consolidated_bar.close security.adjusted_history = security.adjusted_history[security.adjusted_history.index >= end_date - self._total_lookback] else: # Update raw history continuous_contract = self._algorithm.securities[security.symbol.canonical] if consolidated_bar.symbol == continuous_contract.latest_mapped: continuous_contract.raw_history.loc[end_date] = consolidated_bar.close continuous_contract.raw_history = continuous_contract.raw_history[continuous_contract.raw_history.index >= end_date - self._total_lookback] def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None: for security in changes.added_securities: symbol = security.symbol # Create a consolidator to update the history security.consolidator = TradeBarConsolidator(timedelta(1)) security.consolidator.data_consolidated += self._consolidation_handler algorithm.subscription_manager.add_consolidator(symbol, security.consolidator) if security.symbol.is_canonical(): # Add some members to track price history security.adjusted_history = pd.Series() security.raw_history = pd.Series() # Create indicators for the continuous contract security.fast_ema = algorithm.EMA(security.symbol, self._fast_ema_span, Resolution.DAILY) security.slow_ema = algorithm.EMA(security.symbol, self._slow_ema_span, Resolution.DAILY) security.ewmac = IndicatorExtensions.minus(security.fast_ema, security.slow_ema) security.automatic_indicators = [security.fast_ema, security.slow_ema] self._futures.append(security) for security in changes.removed_securities: # Remove consolidator + indicators algorithm.subscription_manager.remove_consolidator(security.symbol, security.consolidator) if security.symbol.is_canonical(): for indicator in security.automatic_indicators: algorithm.deregister_indicator(indicator)
# region imports from AlgorithmImports import * # endregion categories = { Symbol.create(Futures.Financials.Y_10_TREASURY_NOTE, SecurityType.FUTURE, Market.CBOT): ("Fixed Income", "Bonds"), Symbol.create(Futures.Indices.SP_500_E_MINI, SecurityType.FUTURE, Market.CME): ("Equity", "US") }
# region imports from datetime import timedelta from AlgorithmImports import * #from futures import future_datas from universe import AdvancedFuturesUniverseSelectionModel from alpha import FastTrendFollowingLongAndShortWithTrendStrenthAlphaModel from portfolio import BufferedPortfolioConstructionModel # endregion class FuturesFastTrendFollowingLongAndShortWithTrendStrenthAlgorithm(QCAlgorithm): _undesired_symbols_from_previous_deployment = [] _checked_symbols_from_previous_deployment = False def initialize(self): self.set_start_date(2023, 3, 1) self.set_end_date(2024, 3, 1) self.set_cash(1_000_000) self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN) self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices))) self.settings.minimum_order_margin_portfolio_percentage = 0 self.universe_settings.data_normalization_mode = DataNormalizationMode.BACKWARDS_PANAMA_CANAL self.universe_settings.data_mapping_mode = DataMappingMode.OPEN_INTEREST self.add_universe_selection(AdvancedFuturesUniverseSelectionModel()) slow_ema_span = 2 ** self.get_parameter("slow_ema_span_exponent", 6) # Should be >= 5. "It's convenient to stick to a series of parameter values that are powers of two" (p.131) blend_years = self.get_parameter("blend_years", 3) # Number of years to use when blending sigma estimates self.add_alpha(FastTrendFollowingLongAndShortWithTrendStrenthAlphaModel( self, slow_ema_span, self.get_parameter("abs_forecast_cap", 20), # Hardcoded on p.173 self.get_parameter("sigma_span", 32), # Hardcoded to 32 on p.604 self.get_parameter("target_risk", 0.2), # Recommend value is 0.2 on p.75 blend_years )) self.settings.rebalance_portfolio_on_security_changes = False self.settings.rebalance_portfolio_on_insight_changes = False self.total_count = 0 self._day = -1 self.set_portfolio_construction(BufferedPortfolioConstructionModel( self._rebalance_func, self.get_parameter("buffer_scaler", 0.1) # Hardcoded on p.167 & p.173 )) self.add_risk_management(NullRiskManagementModel()) self.set_execution(ImmediateExecutionModel()) self.set_warm_up(timedelta(365*blend_years + slow_ema_span + 7)) def _rebalance_func(self, time): if (self.total_count != self.insights.total_count or self._day != self.time.day) and not self.is_warming_up and self.current_slice.quote_bars.count > 0: self.total_count = self.insights.total_count self._day = self.time.day return time return None def on_data(self, data): # Exit positions that aren't backed by existing insights. # If you don't want this behavior, delete this method definition. if not self.is_warming_up and not self._checked_symbols_from_previous_deployment: for security_holding in self.portfolio.values(): if not security_holding.invested: continue symbol = security_holding.symbol if not self.insights.has_active_insights(symbol, self.utc_time): self._undesired_symbols_from_previous_deployment.append(symbol) self._checked_symbols_from_previous_deployment = True for symbol in self._undesired_symbols_from_previous_deployment[:]: if self.is_market_open(symbol): self.liquidate(symbol, tag="Holding from previous deployment that's no longer desired") self._undesired_symbols_from_previous_deployment.remove(symbol)
#region imports from AlgorithmImports import * #endregion class BufferedPortfolioConstructionModel(EqualWeightingPortfolioConstructionModel): def __init__(self, rebalance, buffer_scaler): super().__init__(rebalance) self._buffer_scaler = buffer_scaler def create_targets(self, algorithm: QCAlgorithm, insights: List[Insight]) -> List[PortfolioTarget]: targets = super().create_targets(algorithm, insights) adj_targets = [] for insight in insights: future_contract = algorithm.securities[insight.symbol] optimal_position = future_contract.forecast * future_contract.position / 10 ## Create buffer zone to reduce churn buffer_width = self._buffer_scaler * abs(future_contract.position) upper_buffer = round(optimal_position + buffer_width) lower_buffer = round(optimal_position - buffer_width) # Determine quantity to put holdings into buffer zone current_holdings = future_contract.holdings.quantity if lower_buffer <= current_holdings <= upper_buffer: continue quantity = lower_buffer if current_holdings < lower_buffer else upper_buffer # Place trades adj_targets.append(PortfolioTarget(insight.symbol, quantity)) # Liquidate contracts that have an expired insight for target in targets: if target.quantity == 0: adj_targets.append(target) return adj_targets
#region imports from AlgorithmImports import * #endregion # 08/29/2023: -Adjusted insight expiry so all insights end at the same time each day # https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_e1c8af207b1a4da945a4696f7db3ef9a.html # # 08/31/2023: -Adjusted universe filter to ensure the Mapped contract is always in the universe # -Updated the Alpha model to rely on warm-up rather than history requests # -Reduced the `blend_years` parameter to 3 to avoid any data issues from far in the past # https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_ecb85ecf7a6ea332088f4b369017fa09.html # # 04/15/2024: -Updated to PEP8 style # https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_f8e01739e5624ee03aa3a6e2ac5c5108.html
# region imports from AlgorithmImports import * from pandas import Timedelta as timedelta from datetime import datetime from Selection.FutureUniverseSelectionModel import FutureUniverseSelectionModel from futures import categories # endregion class AdvancedFuturesUniverseSelectionModel(FutureUniverseSelectionModel): def __init__(self) -> None: super().__init__(timedelta(1), self.select_future_chain_symbols) self.symbols = list(categories.keys()) def select_future_chain_symbols(self, utc_time: datetime) -> List[Symbol]: return self.symbols def filter(self, filter: FutureFilterUniverse) -> FutureFilterUniverse: return filter.expiration(0, 365)
#region imports from AlgorithmImports import * #endregion def get_position_size(group): subcategories = {} for category, subcategory in group.values(): if category not in subcategories: subcategories[category] = {subcategory: 0} elif subcategory not in subcategories[category]: subcategories[category][subcategory] = 0 subcategories[category][subcategory] += 1 category_count = len(subcategories.keys()) subcategory_count = {category: len(subcategory.keys()) for category, subcategory in subcategories.items()} weights = {} for symbol in group: category, subcategory = group[symbol] weight = 1 / category_count / subcategory_count[category] / subcategories[category][subcategory] weights[symbol] = weight return weights