├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── README.md ├── README_zh.md └── static ├── images └── awesome-systematic-trading.jpeg └── strategies ├── 12-month-cycle-in-cross-section-of-stocks-returns.py ├── 52-weeks-high-effect-in-stocks.py ├── accrual-anomaly.py ├── asset-class-momentum-rotational-system.py ├── asset-class-trend-following.py ├── asset-growth-effect.py ├── betting-against-beta-factor-in-country-equity-indexes.py ├── betting-against-beta-factor-in-stocks.py ├── combining-fundamental-fscore-and-equity-short-term-reversals.py ├── combining-smart-factors-momentum-and-market-portfolio.py ├── consistent-momentum-strategy.py ├── crude-oil-predicts-equity-returns.py ├── currency-momentum-factor.py ├── currency-value-factor-ppp-strategy.py ├── dispersion-trading.py ├── dollar-carry-trade.py ├── earnings-announcement-premium.py ├── earnings-announcements-combined-with-stock-repurchases.py ├── earnings-quality-factor.py ├── esg-factor-momentum-strategy.py ├── fed-model.py ├── fx-carry-trade.py ├── how-to-use-lexical-density-of-company-filings.py ├── intraday-seasonality-in-bitcoin.py ├── january-barometer.py ├── low-volatility-factor-effect-in-stocks.py ├── market-sentiment-and-an-overnight-anomaly.py ├── momentum-and-reversal-combined-with-volatility-effect-in-stocks.py ├── momentum-effect-in-commodities.py ├── momentum-factor-and-style-rotation-effect.py ├── momentum-factor-combined-with-asset-growth-effect.py ├── momentum-factor-effect-in-stocks.py ├── momentum-in-mutual-fund-returns.py ├── option-expiration-week-effect.py ├── paired-switching.py ├── pairs-trading-with-country-etfs.py ├── pairs-trading-with-stocks ├── payday-anomaly.py ├── rd-expenditures-and-stock-returns.py ├── rebalancing-premium-in-cryptocurrencies.py ├── residual-momentum-factor.py ├── return-asymmetry-effect-in-commodity-futures.py ├── reversal-during-earnings-announcements.py ├── roa-effect-within-stocks.py ├── sector-momentum-rotational-system.py ├── short-interest-effect-long-short-version.py ├── short-term-reversal-in-stocks.py ├── short-term-reversal-with-futures.py ├── skewness-effect-in-commodities.py ├── small-capitalization-stocks-premium-anomaly.py ├── soccer-clubs-stocks-arbitrage.py ├── synthetic-lending-rates-predict-subsequent-market-return.py ├── term-structure-effect-in-commodities.py ├── time-series-momentum-effect.py ├── trading-wti-brent-spread.py ├── trend-following-effect-in-stocks.py ├── turn-of-the-month-in-equity-indexes.py ├── value-and-momentum-factors-across-asset-classes.py ├── value-book-to-market-factor.py ├── value-factor-effect-within-countries.py └── volatility-risk-premium-effect.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Current file", 6 | "type": "debugpy", 7 | "request": "launch", 8 | "program": "${file}", 9 | "args": [], 10 | "console": "integratedTerminal", 11 | "python": "${env:HOME}/miniconda3/bin/python", 12 | "justMyCode": false 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "python.defaultInterpreterPath": "${env:HOME}/miniconda3/bin/python", 4 | } -------------------------------------------------------------------------------- /static/images/awesome-systematic-trading.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paperswithbacktest/awesome-systematic-trading/1ce1eb2b7752aae1a7357afa47885c8bb8291404/static/images/awesome-systematic-trading.jpeg -------------------------------------------------------------------------------- /static/strategies/12-month-cycle-in-cross-section-of-stocks-returns.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/12-month-cycle-in-cross-section-of-stocks-returns/ 2 | # 3 | # The top 30% of firms based on their market cap from NYSE and AMEX are part of the investment universe. Every month, stocks are grouped 4 | # into ten portfolios (with an equal number of stocks in each portfolio) according to their performance in one month one year ago. Investors 5 | # go long in stocks from the winner decile and shorts stocks from the loser decile. The portfolio is equally weighted and rebalanced every month. 6 | # 7 | # QC implementation changes: 8 | # - Universe consists of top 3000 US stock by market cap from NYSE, AMEX and NASDAQ. 9 | # - Portfolio is value weighted. 10 | 11 | from AlgorithmImports import * 12 | 13 | class Month12CycleinCrossSectionofStocksReturns(QCAlgorithm): 14 | 15 | def Initialize(self): 16 | self.SetStartDate(2000, 1, 1) 17 | self.SetCash(100000) 18 | 19 | self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol 20 | 21 | self.coarse_count = 500 22 | 23 | # Monthly close data. 24 | self.data = {} 25 | self.period = 13 26 | 27 | self.weight = {} 28 | 29 | self.selection_flag = False 30 | self.UniverseSettings.Resolution = Resolution.Daily 31 | self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) 32 | 33 | self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.BeforeMarketClose(self.symbol), self.Selection) 34 | 35 | def OnSecuritiesChanged(self, changes): 36 | for security in changes.AddedSecurities: 37 | security.SetFeeModel(CustomFeeModel()) 38 | security.SetLeverage(10) 39 | 40 | def CoarseSelectionFunction(self, coarse): 41 | if not self.selection_flag: 42 | return Universe.Unchanged 43 | 44 | # Update the rolling window every month. 45 | for stock in coarse: 46 | symbol = stock.Symbol 47 | 48 | # Store monthly price. 49 | if symbol in self.data: 50 | self.data[symbol].update(stock.AdjustedPrice) 51 | 52 | # selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa'] 53 | selected = [x.Symbol 54 | for x in sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa'], 55 | key = lambda x: x.DollarVolume, reverse = True)[:self.coarse_count]] 56 | 57 | # Warmup price rolling windows. 58 | for symbol in selected: 59 | if symbol in self.data: 60 | continue 61 | 62 | self.data[symbol] = SymbolData(symbol, self.period) 63 | history = self.History(symbol, self.period*30, Resolution.Daily) 64 | if history.empty: 65 | self.Log(f"Not enough data for {symbol} yet.") 66 | continue 67 | closes = history.loc[symbol].close 68 | 69 | closes_len = len(closes.keys()) 70 | # Find monthly closes. 71 | for index, time_close in enumerate(closes.iteritems()): 72 | # index out of bounds check. 73 | if index + 1 < closes_len: 74 | date_month = time_close[0].date().month 75 | next_date_month = closes.keys()[index + 1].month 76 | 77 | # Found last day of month. 78 | if date_month != next_date_month: 79 | self.data[symbol].update(time_close[1]) 80 | 81 | return [x for x in selected if self.data[x].is_ready()] 82 | 83 | def FineSelectionFunction(self, fine): 84 | fine = [x for x in fine if x.MarketCap != 0 and x.CompanyReference.IsREIT != 1 and \ 85 | ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE"))] 86 | 87 | if len(fine) > self.coarse_count: 88 | sorted_by_market_cap = sorted(fine, key = lambda x: x.MarketCap, reverse=True) 89 | top_by_market_cap = sorted_by_market_cap[:self.coarse_count] 90 | else: 91 | top_by_market_cap = fine 92 | 93 | # Performance sorting. One month performance, one year ago with market cap data. 94 | performance_market_cap = { x.Symbol : (self.data[x.Symbol].performance(), x.MarketCap) for x in top_by_market_cap if x.Symbol in self.data and self.data[x.Symbol].is_ready()} 95 | 96 | long = [] 97 | short = [] 98 | if len(performance_market_cap) >= 10: 99 | sorted_by_perf = sorted(performance_market_cap.items(), key = lambda x:x[1][0], reverse = True) 100 | decile = int(len(sorted_by_perf) / 10) 101 | long = [x for x in sorted_by_perf[:decile]] 102 | short = [x for x in sorted_by_perf[-decile:]] 103 | 104 | total_market_cap_long = sum([x[1][1] for x in long]) 105 | for symbol, perf_market_cap in long: 106 | self.weight[symbol] = perf_market_cap[1] / total_market_cap_long 107 | 108 | total_market_cap_short = sum([x[1][1] for x in short]) 109 | for symbol, perf_market_cap in short: 110 | self.weight[symbol] = perf_market_cap[1] / total_market_cap_short 111 | 112 | return [x[0] for x in self.weight.items()] 113 | 114 | def OnData(self, data): 115 | if not self.selection_flag: 116 | return 117 | self.selection_flag = False 118 | 119 | # Trade execution. 120 | stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested] 121 | for symbol in stocks_invested: 122 | if symbol not in self.weight: 123 | self.Liquidate(symbol) 124 | 125 | for symbol, w in self.weight.items(): 126 | self.SetHoldings(symbol, w) 127 | 128 | self.weight.clear() 129 | 130 | def Selection(self): 131 | self.selection_flag = True 132 | 133 | class SymbolData(): 134 | def __init__(self, symbol, period): 135 | self.Symbol = symbol 136 | self.Window = RollingWindow[float](period) 137 | 138 | def update(self, value): 139 | self.Window.Add(value) 140 | 141 | def is_ready(self): 142 | return self.Window.IsReady 143 | 144 | # One month performance, one year ago. 145 | def performance(self): 146 | values = [x for x in self.Window] 147 | return (values[-2] / values[-1] - 1) 148 | 149 | # Custom fee model. 150 | class CustomFeeModel(FeeModel): 151 | def GetOrderFee(self, parameters): 152 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 153 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/accrual-anomaly.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/accrual-anomaly/ 2 | # 3 | # The investment universe consists of all stocks on NYSE, AMEX, and NASDAQ. Balance sheet based accruals (the non-cash component of 4 | # earnings) are calculated as: BS_ACC = ( ∆CA – ∆Cash) – ( ∆CL – ∆STD – ∆ITP) – Dep 5 | # Where: 6 | # ∆CA = annual change in current assets 7 | # ∆Cash = change in cash and cash equivalents 8 | # ∆CL = change in current liabilities 9 | # ∆STD = change in debt included in current liabilities 10 | # ∆ITP = change in income taxes payable 11 | # Dep = annual depreciation and amortization expense 12 | # Stocks are then sorted into deciles and investor goes long stocks with the lowest accruals and short stocks with the highest accruals. 13 | # The portfolio is rebalanced yearly during May (after all companies publish their earnings). 14 | 15 | from AlgorithmImports import * 16 | 17 | class AccrualAnomaly(QCAlgorithm): 18 | 19 | def Initialize(self): 20 | self.SetStartDate(2006, 1, 1) 21 | self.SetCash(100000) 22 | 23 | self.symbol = self.AddEquity("SPY", Resolution.Daily).Symbol 24 | 25 | self.coarse_count = 1000 26 | 27 | self.long = [] 28 | self.short = [] 29 | 30 | # Latest accruals data. 31 | self.accrual_data = {} 32 | 33 | self.selection_flag = False 34 | self.UniverseSettings.Resolution = Resolution.Daily 35 | self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) 36 | self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection) 37 | 38 | def OnSecuritiesChanged(self, changes): 39 | for security in changes.AddedSecurities: 40 | security.SetFeeModel(CustomFeeModel()) 41 | security.SetLeverage(5) 42 | 43 | for security in changes.RemovedSecurities: 44 | symbol = security.Symbol 45 | if symbol in self.accrual_data: 46 | del self.accrual_data[symbol] 47 | 48 | def CoarseSelectionFunction(self, coarse): 49 | if not self.selection_flag: 50 | return Universe.Unchanged 51 | 52 | # selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa'] 53 | selected = [x.Symbol 54 | for x in sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa'], 55 | key = lambda x: x.DollarVolume, reverse = True)[:self.coarse_count]] 56 | 57 | return selected 58 | 59 | def FineSelectionFunction(self, fine): 60 | fine = [x for x in fine if (float(x.FinancialStatements.BalanceSheet.CurrentAssets.TwelveMonths) != 0) 61 | and (float(x.FinancialStatements.BalanceSheet.CashAndCashEquivalents.TwelveMonths) != 0) 62 | and (float(x.FinancialStatements.BalanceSheet.CurrentLiabilities.TwelveMonths) != 0) 63 | and (float(x.FinancialStatements.BalanceSheet.CurrentDebt.TwelveMonths) != 0) 64 | and (float(x.FinancialStatements.BalanceSheet.IncomeTaxPayable.TwelveMonths) != 0) 65 | and (float(x.FinancialStatements.IncomeStatement.DepreciationAndAmortization.TwelveMonths) != 0)] 66 | 67 | if len(fine) > self.coarse_count: 68 | sorted_by_market_cap = sorted(fine, key = lambda x: x.MarketCap, reverse=True) 69 | top_by_market_cap = sorted_by_market_cap[:self.coarse_count] 70 | else: 71 | top_by_market_cap = fine 72 | 73 | accruals = {} 74 | for stock in top_by_market_cap: 75 | symbol = stock.Symbol 76 | 77 | if symbol not in self.accrual_data: 78 | self.accrual_data[symbol] = None 79 | 80 | # Accrual calc. 81 | current_accruals_data = AccrualsData(stock.FinancialStatements.BalanceSheet.CurrentAssets.TwelveMonths, stock.FinancialStatements.BalanceSheet.CashAndCashEquivalents.TwelveMonths, 82 | stock.FinancialStatements.BalanceSheet.CurrentLiabilities.TwelveMonths, stock.FinancialStatements.BalanceSheet.CurrentDebt.TwelveMonths, stock.FinancialStatements.BalanceSheet.IncomeTaxPayable.TwelveMonths, 83 | stock.FinancialStatements.IncomeStatement.DepreciationAndAmortization.TwelveMonths, stock.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths) 84 | 85 | # There is not previous accrual data. 86 | if not self.accrual_data[symbol]: 87 | self.accrual_data[symbol] = current_accruals_data 88 | continue 89 | 90 | # Accruals and market cap calc. 91 | acc = self.CalculateAccruals(current_accruals_data, self.accrual_data[symbol]) 92 | accruals[symbol] = acc 93 | 94 | # Update accruals data. 95 | self.accrual_data[symbol] = current_accruals_data 96 | 97 | # Accruals sorting. 98 | sorted_by_accruals = sorted(accruals.items(), key = lambda x: x[1], reverse = True) 99 | decile = int(len(sorted_by_accruals) / 10) 100 | self.long = [x[0] for x in sorted_by_accruals[-decile:]] 101 | self.short = [x[0] for x in sorted_by_accruals[:decile]] 102 | 103 | return self.long + self.short 104 | 105 | def OnData(self, data): 106 | if not self.selection_flag: 107 | return 108 | self.selection_flag = False 109 | 110 | # Trade execution. 111 | stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested] 112 | for symbol in stocks_invested: 113 | if symbol not in self.long: 114 | self.Liquidate(symbol) 115 | 116 | for symbol in self.long: 117 | self.SetHoldings(symbol, 1 / len(self.long)) 118 | for symbol in self.short: 119 | self.SetHoldings(symbol, -1 / len(self.short)) 120 | 121 | self.long.clear() 122 | self.short.clear() 123 | 124 | def Selection(self): 125 | if self.Time.month == 4: 126 | self.selection_flag = True 127 | 128 | def CalculateAccruals(self, current_accrual_data, prev_accrual_data): 129 | delta_assets = current_accrual_data.CurrentAssets - prev_accrual_data.CurrentAssets 130 | delta_cash = current_accrual_data.CashAndCashEquivalents - prev_accrual_data.CashAndCashEquivalents 131 | delta_liabilities = current_accrual_data.CurrentLiabilities - prev_accrual_data.CurrentLiabilities 132 | delta_debt = current_accrual_data.CurrentDebt - prev_accrual_data.CurrentDebt 133 | delta_tax = current_accrual_data.IncomeTaxPayable - prev_accrual_data.IncomeTaxPayable 134 | dep = current_accrual_data.DepreciationAndAmortization 135 | avg_total = (current_accrual_data.TotalAssets + prev_accrual_data.TotalAssets) / 2 136 | 137 | bs_acc = ((delta_assets - delta_cash) - (delta_liabilities - delta_debt - delta_tax) - dep) / avg_total 138 | return bs_acc 139 | 140 | class AccrualsData(): 141 | def __init__(self, current_assets, cash_and_cash_equivalents, current_liabilities, current_debt, income_tax_payable, depreciation_and_amortization, total_assets): 142 | self.CurrentAssets = current_assets 143 | self.CashAndCashEquivalents = cash_and_cash_equivalents 144 | self.CurrentLiabilities = current_liabilities 145 | self.CurrentDebt = current_debt 146 | self.IncomeTaxPayable = income_tax_payable 147 | self.DepreciationAndAmortization = depreciation_and_amortization 148 | self.TotalAssets = total_assets 149 | 150 | # Custom fee model. 151 | class CustomFeeModel(FeeModel): 152 | def GetOrderFee(self, parameters): 153 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 154 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/asset-class-momentum-rotational-system.py: -------------------------------------------------------------------------------- 1 | # region imports 2 | from AlgorithmImports import * 3 | 4 | # endregion 5 | # https://quantpedia.com/strategies/asset-class-momentum-rotational-system/ 6 | # 7 | # Use 5 ETFs (SPY - US stocks, EFA - foreign stocks, IEF - bonds, VNQ - REITs, GSG - commodities). 8 | # Pick 3 ETFs with strongest 12 month momentum into your portfolio and weight them equally. 9 | # Hold for 1 month and then rebalance. 10 | 11 | 12 | class MomentumAssetAllocationStrategy(QCAlgorithm): 13 | def Initialize(self): 14 | self.SetStartDate(2000, 1, 1) 15 | self.SetCash(100000) 16 | 17 | self.data = {} 18 | period = 12 * 21 19 | self.SetWarmUp(period) 20 | self.symbols = ["SPY", "EFA", "IEF", "VNQ", "GSG"] 21 | 22 | for symbol in self.symbols: 23 | self.AddEquity(symbol, Resolution.Daily) 24 | self.data[symbol] = self.ROC(symbol, period, Resolution.Daily) 25 | 26 | self.recent_month = -1 27 | 28 | def OnData(self, data): 29 | if self.IsWarmingUp: 30 | return 31 | 32 | # monthly rebalance 33 | if self.Time.month == self.recent_month: 34 | return 35 | self.recent_month = self.Time.month 36 | 37 | sorted_by_momentum = sorted( 38 | [ 39 | x 40 | for x in self.data.items() 41 | if x[1].IsReady and x[0] in data and data[x[0]] 42 | ], 43 | key=lambda x: x[1].Current.Value, 44 | reverse=True, 45 | ) 46 | count = 3 47 | long = [x[0] for x in sorted_by_momentum][:count] 48 | 49 | invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested] 50 | for symbol in invested: 51 | if symbol not in long: 52 | self.Liquidate(symbol) 53 | 54 | for symbol in long: 55 | self.SetHoldings(symbol, 1 / len(long)) 56 | -------------------------------------------------------------------------------- /static/strategies/asset-class-trend-following.py: -------------------------------------------------------------------------------- 1 | # region imports 2 | from AlgorithmImports import * 3 | 4 | # endregion 5 | # https://quantpedia.com/strategies/asset-class-trend-following/ 6 | # 7 | # Use 5 ETFs (SPY - US stocks, EFA - foreign stocks, IEF - bonds, VNQ - REITs, 8 | # GSG - commodities), equal weight the portfolio. Hold asset class ETF only when 9 | # it is over its 10 month Simple Moving Average, otherwise stay in cash. 10 | # 11 | # QC implementation: 12 | # - SMA with period of 210 days is used. 13 | 14 | 15 | class AssetClassTrendFollowing(QCAlgorithm): 16 | def Initialize(self): 17 | self.SetStartDate(2000, 1, 1) 18 | self.SetCash(100000) 19 | 20 | self.sma = {} 21 | period = 10 * 21 22 | self.SetWarmUp(period, Resolution.Daily) 23 | 24 | self.symbols = ["SPY", "EFA", "IEF", "VNQ", "GSG"] 25 | self.rebalance_flag = False 26 | 27 | self.tracked_symbol = None 28 | for symbol in self.symbols: 29 | self.AddEquity(symbol, Resolution.Minute) 30 | self.sma[symbol] = self.SMA(symbol, period, Resolution.Daily) 31 | 32 | self.recent_month = -1 33 | 34 | def OnData(self, data): 35 | # rebalance once a month 36 | if self.Time.month == self.recent_month: 37 | return 38 | if self.Time.hour != 9 and self.Time.minute != 31: 39 | return 40 | self.recent_month = self.Time.month 41 | 42 | long = [ 43 | symbol 44 | for symbol in self.symbols 45 | if symbol in data 46 | and data[symbol] 47 | and self.sma[symbol].IsReady 48 | and data[symbol].Value > self.sma[symbol].Current.Value 49 | ] 50 | 51 | # trade execution 52 | invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested] 53 | for symbol in invested: 54 | if symbol not in long: 55 | self.Liquidate(symbol) 56 | 57 | for symbol in long: 58 | self.SetHoldings(symbol, 1 / len(long)) 59 | -------------------------------------------------------------------------------- /static/strategies/asset-growth-effect.py: -------------------------------------------------------------------------------- 1 | #region imports 2 | from AlgorithmImports import * 3 | #endregion 4 | # https://quantpedia.com/strategies/asset-growth-effect/ 5 | # 6 | # The investment universe consists of all non-financial U.S. stocks listed on NYSE, AMEX, and NASDAQ. Stocks are then sorted each year at the end 7 | # of June into ten equal groups based on the percentage change in total assets for the previous year. The investor goes long decile with low asset 8 | # growth firms and short decile with high asset growth firms. The portfolio is weighted equally and rebalanced every year. 9 | # 10 | # QC implementation changes: 11 | # - Top 3000 stocks by market cap are selected from QC stock universe. 12 | 13 | class AssetGrowthEffect(QCAlgorithm): 14 | 15 | def Initialize(self): 16 | self.SetStartDate(2000, 1, 1) 17 | self.SetCash(100000) 18 | 19 | self.symbol:Symbol = self.AddEquity("SPY", Resolution.Daily).Symbol 20 | 21 | self.long:list[Symbol] = [] 22 | self.short:list[Symbol] = [] 23 | 24 | self.coarse_count:int = 3000 25 | self.quantile:int = 10 26 | 27 | # Latest assets data. 28 | self.total_assets:dict[Symbol, float] = {} 29 | 30 | self.selection_flag:bool = False 31 | self.UniverseSettings.Resolution = Resolution.Daily 32 | self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) 33 | self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection) 34 | 35 | def OnSecuritiesChanged(self, changes): 36 | for security in changes.AddedSecurities: 37 | security.SetFeeModel(CustomFeeModel()) 38 | security.SetLeverage(5) 39 | 40 | def CoarseSelectionFunction(self, coarse): 41 | if not self.selection_flag: 42 | return Universe.Unchanged 43 | 44 | # Select all stocks in universe. 45 | return [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa'] 46 | 47 | def FineSelectionFunction(self, fine): 48 | fine = [x for x in fine if x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths > 0 and 49 | ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE"))] 50 | 51 | if len(fine) > self.coarse_count: 52 | sorted_by_market_cap = sorted(fine, key = lambda x: x.MarketCap, reverse=True) 53 | fine = sorted_by_market_cap[:self.coarse_count] 54 | 55 | assets_growth:dict[Symbol, float] = {} 56 | for stock in fine: 57 | symbol = stock.Symbol 58 | 59 | if symbol not in self.total_assets: 60 | self.total_assets[symbol] = None 61 | 62 | current_assets = stock.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths 63 | 64 | # There is not previous assets data. 65 | if not self.total_assets[symbol]: 66 | self.total_assets[symbol] = current_assets 67 | continue 68 | 69 | # Assets growth calc. 70 | assets_growth[symbol] = (current_assets - self.total_assets[symbol]) / self.total_assets[symbol] 71 | 72 | # Update data. 73 | self.total_assets[symbol] = current_assets 74 | 75 | # Asset growth sorting. 76 | if len(assets_growth) >= self.quantile: 77 | sorted_by_assets_growth = sorted(assets_growth.items(), key = lambda x: x[1], reverse = True) 78 | decile = int(len(sorted_by_assets_growth) / self.quantile) 79 | self.long = [x[0] for x in sorted_by_assets_growth[-decile:]] 80 | self.short = [x[0] for x in sorted_by_assets_growth[:decile]] 81 | 82 | return self.long + self.short 83 | 84 | def OnData(self, data): 85 | if not self.selection_flag: 86 | return 87 | self.selection_flag = False 88 | 89 | # Trade execution. 90 | stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested] 91 | for symbol in stocks_invested: 92 | if symbol not in self.long: 93 | self.Liquidate(symbol) 94 | 95 | for symbol in self.long: 96 | if symbol in data and data[symbol]: 97 | self.SetHoldings(symbol, 1 / len(self.long)) 98 | 99 | for symbol in self.short: 100 | if symbol in data and data[symbol]: 101 | self.SetHoldings(symbol, -1 / len(self.short)) 102 | 103 | self.long.clear() 104 | self.short.clear() 105 | 106 | def Selection(self): 107 | if self.Time.month == 6: 108 | self.selection_flag = True 109 | 110 | # Custom fee model. 111 | class CustomFeeModel(FeeModel): 112 | def GetOrderFee(self, parameters): 113 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 114 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/betting-against-beta-factor-in-country-equity-indexes.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/betting-against-beta-factor-in-country-equity-indexes/ 2 | # 3 | # The investment universe consists of all country ETFs. The beta for each country is calculated with respect to the MSCI US 4 | # Equity Index using a 1-year rolling window. ETFs are then ranked in ascending order based on their estimated beta. The ranked 5 | # ETFs are assigned to one of two portfolios: low beta and high beta. Securities are weighted by the ranked betas, and the portfolios 6 | # are rebalanced every calendar month. Both portfolios are rescaled to have a beta of one at portfolio formation. The “Betting-Against-Beta” 7 | # is the zero-cost zero-beta portfolio that is long on the low-beta portfolio and that shorts the high-beta portfolio. There are a lot of 8 | # simple modifications (like going long on the bottom beta decile and short on the top beta decile), which could probably improve the strategy’s performance. 9 | 10 | import numpy as np 11 | from AlgorithmImports import * 12 | from collections import deque 13 | 14 | class BettingAgainstBetaFactorinInternationalEquities(QCAlgorithm): 15 | 16 | def Initialize(self): 17 | self.SetStartDate(2002, 2, 1) 18 | self.SetCash(100000) 19 | 20 | self.countries = [ 21 | "EWA", # iShares MSCI Australia Index ETF 22 | "EWO", # iShares MSCI Austria Investable Mkt Index ETF 23 | "EWK", # iShares MSCI Belgium Investable Market Index ETF 24 | "EWZ", # iShares MSCI Brazil Index ETF 25 | "EWC", # iShares MSCI Canada Index ETF 26 | "FXI", # iShares China Large-Cap ETF 27 | "EWQ", # iShares MSCI France Index ETF 28 | "EWG", # iShares MSCI Germany ETF 29 | "EWH", # iShares MSCI Hong Kong Index ETF 30 | "EWI", # iShares MSCI Italy Index ETF 31 | "EWJ", # iShares MSCI Japan Index ETF 32 | "EWM", # iShares MSCI Malaysia Index ETF 33 | "EWW", # iShares MSCI Mexico Inv. Mt. Idx 34 | "EWN", # iShares MSCI Netherlands Index ETF 35 | "EWS", # iShares MSCI Singapore Index ETF 36 | "EZA", # iShares MSCI South Africe Index ETF 37 | "EWY", # iShares MSCI South Korea ETF 38 | "EWP", # iShares MSCI Spain Index ETF 39 | "EWD", # iShares MSCI Sweden Index ETF 40 | "EWL", # iShares MSCI Switzerland Index ETF 41 | "EWT", # iShares MSCI Taiwan Index ETF 42 | "THD", # iShares MSCI Thailand Index ETF 43 | "EWU", # iShares MSCI United Kingdom Index ETF 44 | ] 45 | 46 | self.leverage_cap = 5 47 | 48 | # Daily price data. 49 | self.data = {} 50 | self.period = 12 * 21 51 | 52 | self.symbol = 'SPY' 53 | 54 | for symbol in self.countries + [self.symbol]: 55 | data = self.AddEquity(symbol, Resolution.Daily) 56 | data.SetFeeModel(CustomFeeModel()) 57 | data.SetLeverage(15) 58 | 59 | self.data[symbol] = RollingWindow[float](self.period) 60 | 61 | self.recent_month = -1 62 | 63 | def OnData(self, data): 64 | for symbol in self.data: 65 | symbol_obj = self.Symbol(symbol) 66 | if symbol_obj in data.Keys: 67 | if data[symbol_obj]: 68 | price = data[symbol_obj].Value 69 | if price != 0: 70 | self.data[symbol].Add(price) 71 | 72 | if self.recent_month == self.Time.month: 73 | return 74 | self.recent_month = self.Time.month 75 | 76 | beta = {} 77 | for symbol in self.countries: 78 | # Data is ready. 79 | if self.data[self.symbol].IsReady and self.data[symbol].IsReady and self.symbol in data and symbol in data: 80 | market_closes = np.array([x for x in self.data[self.symbol]]) 81 | asset_closes = np.array([x for x in self.data[symbol]]) 82 | 83 | market_returns = (market_closes[1:] - market_closes[:-1]) / market_closes[:-1] 84 | asset_returns = (asset_closes[1:] - asset_closes[:-1]) / asset_closes[:-1] 85 | 86 | cov = np.cov(asset_returns, market_returns)[0][1] 87 | market_variance = np.var(market_returns) 88 | beta[symbol] = cov / market_variance 89 | 90 | weight = {} 91 | if len(beta) != 0: 92 | # Beta diff calc. 93 | beta_median = np.median([x[1] for x in beta.items()]) 94 | 95 | long_diff = [(x[0], abs(beta_median - x[1])) for x in beta.items() if x[1] < beta_median] 96 | short_diff = [(x[0], abs(beta_median - x[1])) for x in beta.items() if x[1] > beta_median] 97 | 98 | # Beta rescale. 99 | long_portfolio_beta = np.mean([beta[x[0]] for x in long_diff]) 100 | long_leverage = 1 / long_portfolio_beta 101 | 102 | short_portfolio_beta = np.mean([beta[x[0]] for x in short_diff]) 103 | short_leverage = 1 / short_portfolio_beta 104 | 105 | # Cap long and short leverage. 106 | long_leverage = min(self.leverage_cap, long_leverage) 107 | long_leverage = max(-self.leverage_cap, long_leverage) 108 | short_leverage = min(self.leverage_cap, short_leverage) 109 | short_leverage = max(-self.leverage_cap, short_leverage) 110 | 111 | # self.Log(f"long: {long_leverage}; short: {short_leverage}") 112 | 113 | total_long_diff = sum([x[1] for x in long_diff]) 114 | total_short_diff = sum([x[1] for x in short_diff]) 115 | 116 | # Beta diff weighting. 117 | weight = {} 118 | for symbol, diff in long_diff: 119 | weight[symbol] = (diff / total_long_diff) * long_leverage 120 | for symbol, diff in short_diff: 121 | weight[symbol] = - (diff / total_short_diff) * short_leverage 122 | 123 | # Trade execution. 124 | invested = [x.Key for x in self.Portfolio if x.Value.Invested] 125 | for symbol in invested: 126 | if symbol not in weight: 127 | self.Liquidate(symbol) 128 | 129 | for symbol, w in weight.items(): 130 | self.SetHoldings(symbol, w) 131 | 132 | # Custom fee model. 133 | class CustomFeeModel(FeeModel): 134 | def GetOrderFee(self, parameters): 135 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 136 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/betting-against-beta-factor-in-stocks.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/betting-against-beta-factor-in-stocks/ 2 | # 3 | # The investment universe consists of all stocks from the CRSP database. The beta for each stock is calculated with respect to the MSCI US Equity Index using a 1-year 4 | # rolling window. Stocks are then ranked in ascending order on the basis of their estimated beta. The ranked stocks are assigned to one of two portfolios: low beta and 5 | # high beta. Securities are weighted by the ranked betas, and portfolios are rebalanced every calendar month. Both portfolios are rescaled to have a beta of one at portfolio 6 | # formation. The “Betting-Against-Beta” is the zero-cost zero-beta portfolio that is long on the low-beta portfolio and short on the high-beta portfolio. There are a lot of 7 | # simple modifications (like going long on the bottom beta decile and short on the top beta decile), which could probably improve the strategy’s performance. 8 | # 9 | # QC implementation changes: 10 | # - The investment universe consists of 1000 most liquid US stocks with price > 5$. 11 | 12 | from scipy import stats 13 | from AlgorithmImports import * 14 | import numpy as np 15 | 16 | class BettingAgainstBetaFactorinStocks(QCAlgorithm): 17 | 18 | def Initialize(self): 19 | self.SetStartDate(2000, 1, 1) 20 | self.SetCash(100000) 21 | 22 | # Daily price data. 23 | self.data = {} 24 | self.period = 12 * 21 25 | 26 | self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol 27 | self.data[self.symbol] = RollingWindow[float](self.period) 28 | 29 | self.weight = {} 30 | self.long = [] 31 | self.short = [] 32 | self.long_lvg = 1 # leverage for long portfolio calculated from average beta 33 | self.short_lvg = 1 # leverage for short portfolio calculated from average beta 34 | self.leverage_cap = 2 35 | 36 | self.coarse_count = 1000 37 | 38 | self.selection_flag = False 39 | self.UniverseSettings.Resolution = Resolution.Daily 40 | self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) 41 | self.Schedule.On(self.DateRules.MonthStart(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection) 42 | 43 | def OnSecuritiesChanged(self, changes): 44 | for security in changes.AddedSecurities: 45 | security.SetFeeModel(CustomFeeModel()) 46 | security.SetLeverage(self.leverage_cap*3) 47 | 48 | def CoarseSelectionFunction(self, coarse): 49 | # Update the rolling window every day. 50 | for stock in coarse: 51 | symbol = stock.Symbol 52 | 53 | if symbol in self.data: 54 | # Store daily price. 55 | self.data[symbol].Add(stock.AdjustedPrice) 56 | 57 | # Selection once a month. 58 | if not self.selection_flag: 59 | return Universe.Unchanged 60 | 61 | # selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa' and x.Price > 5] 62 | selected = [x.Symbol 63 | for x in sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa' and x.Price > 5], 64 | key = lambda x: x.DollarVolume, reverse = True)[:self.coarse_count]] 65 | 66 | # Warmup price rolling windows. 67 | for symbol in selected: 68 | if symbol in self.data: 69 | continue 70 | 71 | self.data[symbol] = RollingWindow[float](self.period) 72 | history = self.History(symbol, self.period, Resolution.Daily) 73 | if history.empty: 74 | self.Log(f"Not enough data for {symbol} yet") 75 | continue 76 | closes = history.loc[symbol].close 77 | for time, close in closes.iteritems(): 78 | self.data[symbol].Add(close) 79 | 80 | return [x for x in selected if self.data[x].IsReady] 81 | 82 | def FineSelectionFunction(self, fine): 83 | fine = [x for x in fine if x.MarketCap != 0] 84 | 85 | # if len(fine) > self.coarse_count: 86 | # sorted_by_market_cap = sorted(fine, key = lambda x: x.MarketCap, reverse=True) 87 | # top_by_market_cap = sorted_by_market_cap[:self.coarse_count] 88 | # else: 89 | # top_by_market_cap = fine 90 | 91 | beta = {} 92 | 93 | if not self.data[self.symbol].IsReady: return [] 94 | 95 | for stock in fine: 96 | symbol = stock.Symbol 97 | market_closes = np.array([x for x in self.data[self.symbol]]) 98 | stock_closes = np.array([x for x in self.data[symbol]]) 99 | 100 | market_returns = (market_closes[:-1] - market_closes[1:]) / market_closes[1:] 101 | stock_returns = (stock_closes[:-1] - stock_closes[1:]) / stock_closes[1:] 102 | 103 | cov = np.cov(stock_returns[::-1], market_returns[::-1])[0][1] 104 | market_variance = np.var(market_returns) 105 | beta[symbol] = cov / market_variance 106 | 107 | # beta_, intercept, r_value, p_value, std_err = stats.linregress(market_returns[::-1], stock_returns[::-1]) 108 | # beta[symbol] = beta_ 109 | 110 | if len(beta) >= 10: 111 | # sort by beta 112 | sorted_by_beta = sorted(beta.items(), key = lambda x:x[1], reverse=True) 113 | decile = int(len(sorted_by_beta) / 10) 114 | self.long = [x for x in sorted_by_beta[-decile:]] 115 | self.short = [x for x in sorted_by_beta[:decile]] 116 | 117 | # create zero-beta portfolio 118 | long_mean_beta = np.mean([x[1] for x in self.long]) 119 | short_mean_beta = np.mean([x[1] for x in self.short]) 120 | 121 | self.long = [x[0] for x in self.long] 122 | self.short = [x[0] for x in self.short] 123 | 124 | self.long_lvg = 1/long_mean_beta 125 | self.short_lvg = 1/short_mean_beta 126 | 127 | # cap leverage 128 | if self.long_lvg <= 0: 129 | self.long_lvg = self.leverage_cap 130 | else: 131 | self.long_lvg = min(self.leverage_cap, self.long_lvg) 132 | 133 | if self.short_lvg <= 0: 134 | self.short_lvg = self.leverage_cap 135 | else: 136 | self.short_lvg = min(self.leverage_cap, self.short_lvg) 137 | 138 | return self.long + self.short 139 | 140 | def OnData(self, data): 141 | if not self.selection_flag: 142 | return 143 | self.selection_flag = False 144 | 145 | # Trade execution. 146 | stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested] 147 | for symbol in stocks_invested: 148 | if symbol not in self.long + self.short: 149 | self.Liquidate(symbol) 150 | 151 | long_len = len(self.long) 152 | short_len = len(self.short) 153 | 154 | for symbol in self.long: 155 | self.SetHoldings(symbol, (1/long_len)*self.long_lvg) 156 | for symbol in self.short: 157 | self.SetHoldings(symbol, -(1/short_len)*self.short_lvg) 158 | 159 | self.long.clear() 160 | self.short.clear() 161 | self.long_lvg = 1 162 | self.short_lvg = 1 163 | 164 | def Selection(self): 165 | self.selection_flag = True 166 | 167 | # Custom fee model. 168 | class CustomFeeModel(FeeModel): 169 | def GetOrderFee(self, parameters): 170 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 171 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/consistent-momentum-strategy.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/consistent-momentum-strategy/ 2 | # 3 | # The investment universe consists of stocks listed at NYSE, AMEX, and NASDAQ, whose price data (at least for the past 7 months) are available 4 | # at the CRSP database. The investor creates a zero-investment portfolio at the end of the month t, longing stocks that are in the top decile 5 | # in terms of returns both in the period from t-7 to t-1 and from t-6 to t, while shorting stocks in the bottom decile in both periods (i.e. 6 | # longing consistent winners and shorting consistent losers). The stocks in the portfolio are weighted equally. The holding period is six months, 7 | # with no rebalancing during the period. There is a one-month skip between the formation and holding period. 8 | # 9 | # QC implementation changes: 10 | # - Universe consists of 500 most liquid stocks traded on NYSE, AMEX, or NASDAQ. 11 | 12 | from AlgorithmImports import * 13 | 14 | class ConsistentMomentumStrategy(QCAlgorithm): 15 | 16 | def Initialize(self): 17 | self.SetStartDate(2000, 1, 1) 18 | self.SetCash(100000) 19 | 20 | self.coarse_count = 500 21 | 22 | self.long = [] 23 | self.short = [] 24 | 25 | self.data = {} 26 | 27 | self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol 28 | 29 | self.period = 7 * 21 30 | 31 | self.months = 0 32 | self.selection_flag = False 33 | self.UniverseSettings.Resolution = Resolution.Daily 34 | self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) 35 | 36 | self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Rebalance) 37 | 38 | def OnSecuritiesChanged(self, changes): 39 | for security in changes.AddedSecurities: 40 | symbol = security.Symbol 41 | 42 | security.SetFeeModel(CustomFeeModel()) 43 | security.SetLeverage(10) 44 | 45 | def CoarseSelectionFunction(self, coarse): 46 | # Update the rolling window every day. 47 | for stock in coarse: 48 | symbol = stock.Symbol 49 | 50 | # Store monthly price. 51 | if symbol in self.data: 52 | self.data[symbol].update(stock.AdjustedPrice) 53 | 54 | if not self.selection_flag: 55 | return Universe.Unchanged 56 | 57 | # selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa'] 58 | selected = [x.Symbol 59 | for x in sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa'], 60 | key = lambda x: x.DollarVolume, reverse = True)[:self.coarse_count]] 61 | 62 | # Warmup price rolling windows. 63 | for symbol in selected: 64 | if symbol in self.data: 65 | continue 66 | 67 | self.data[symbol] = SymbolData(symbol, self.period) 68 | history = self.History(symbol, self.period, Resolution.Daily) 69 | if history.empty: 70 | self.Log(f"Not enough data for {symbol} yet") 71 | continue 72 | closes = history.loc[symbol].close 73 | for time, close in closes.iteritems(): 74 | self.data[symbol].update(close) 75 | 76 | return [x for x in selected if self.data[x].is_ready()] 77 | 78 | def FineSelectionFunction(self, fine): 79 | fine = [x for x in fine if x.MarketCap != 0 and x.CompanyReference.IsREIT != 1 and \ 80 | ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE"))] 81 | 82 | # if len(fine) > self.coarse_count: 83 | # sorted_by_market_cap = sorted(fine, key = lambda x: x.MarketCap, reverse=True) 84 | # top_by_market_cap = [x.Symbol for x in sorted_by_market_cap[:self.coarse_count]] 85 | # else: 86 | # top_by_market_cap = [x.Symbol for x in fine] 87 | top_by_market_cap = [x.Symbol for x in fine] 88 | 89 | momentum_t71_t60 = { x : (self.data[x].performance_t7t1(), self.data[x].performance_t6t0()) for x in top_by_market_cap} 90 | 91 | # Momentum t-7 to t-1 sorting 92 | sorted_by_perf_t71 = sorted(momentum_t71_t60.items(), key = lambda x: x[1][0], reverse = True) 93 | decile = int(len(sorted_by_perf_t71) / 10) 94 | high_by_perf_t71 = [x[0] for x in sorted_by_perf_t71[:decile]] 95 | low_by_perf_t71 = [x[0] for x in sorted_by_perf_t71[-decile:]] 96 | 97 | # Momentum t-6 to t sorting 98 | sorted_by_perf_t60 = sorted(momentum_t71_t60.items(), key = lambda x: x[1][1], reverse = True) 99 | decile = int(len(sorted_by_perf_t60) / 10) 100 | high_by_perf_t60 = [x[0] for x in sorted_by_perf_t60[:decile]] 101 | low_by_perf_t60 = [x[0] for x in sorted_by_perf_t60[-decile:]] 102 | 103 | self.long = [x for x in high_by_perf_t71 if x in high_by_perf_t60] 104 | self.short = [x for x in low_by_perf_t71 if x in low_by_perf_t60] 105 | 106 | self.selection_flag = False 107 | 108 | return self.long + self.short 109 | 110 | def Rebalance(self): 111 | if self.months == 0: 112 | self.selection_flag = True 113 | self.months += 1 114 | return 115 | 116 | if self.months == 1: 117 | # Trade execution and liquidation. 118 | invested = [x.Key for x in self.Portfolio if x.Value.Invested] 119 | for symbol in invested: 120 | if symbol not in self.long + self.short: 121 | self.Liquidate(symbol) 122 | 123 | long_count = len(self.long) 124 | short_count = len(self.short) 125 | 126 | for symbol in self.long: 127 | self.SetHoldings(symbol, 1/long_count) 128 | for symbol in self.short: 129 | self.SetHoldings(symbol, -1/short_count) 130 | 131 | self.months += 1 132 | 133 | if self.months == 6: 134 | self.months = 0 135 | 136 | class SymbolData(): 137 | def __init__(self, symbol, period): 138 | self.Symbol = symbol 139 | self.Price = RollingWindow[float](period) 140 | 141 | def update(self, value): 142 | self.Price.Add(value) 143 | 144 | def is_ready(self): 145 | return self.Price.IsReady 146 | 147 | def performance_t7t1(self): 148 | closes = [x for x in self.Price][21:] 149 | return (closes[0] / closes[-1] - 1) 150 | 151 | def performance_t6t0(self): 152 | closes = [x for x in self.Price][:-21] 153 | return (closes[0] / closes[-1] - 1) 154 | 155 | # Custom fee model. 156 | class CustomFeeModel(FeeModel): 157 | def GetOrderFee(self, parameters): 158 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 159 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/currency-momentum-factor.py: -------------------------------------------------------------------------------- 1 | # region imports 2 | from AlgorithmImports import * 3 | 4 | # endregion 5 | # Custom fee model 6 | class CustomFeeModel(FeeModel): 7 | def GetOrderFee(self, parameters): 8 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 9 | return OrderFee(CashAmount(fee, "USD")) 10 | 11 | 12 | # Quandl "value" data 13 | class QuandlValue(PythonQuandl): 14 | def __init__(self): 15 | self.ValueColumnName = "Value" 16 | 17 | 18 | # Quantpedia data. 19 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 20 | class QuantpediaFutures(PythonData): 21 | def GetSource(self, config, date, isLiveMode): 22 | return SubscriptionDataSource( 23 | "data.quantpedia.com/backtesting_data/futures/{0}.csv".format( 24 | config.Symbol.Value 25 | ), 26 | SubscriptionTransportMedium.RemoteFile, 27 | FileFormat.Csv, 28 | ) 29 | 30 | def Reader(self, config, line, date, isLiveMode): 31 | data = QuantpediaFutures() 32 | data.Symbol = config.Symbol 33 | 34 | if not line[0].isdigit(): 35 | return None 36 | split = line.split(";") 37 | 38 | data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1) 39 | data["back_adjusted"] = float(split[1]) 40 | data["spliced"] = float(split[2]) 41 | data.Value = float(split[1]) 42 | 43 | return data 44 | 45 | 46 | # https://quantpedia.com/strategies/currency-momentum-factor/ 47 | # 48 | # Create an investment universe consisting of several currencies (10-20). Go long three currencies with the highest 12-month momentum against USD 49 | # and go short three currencies with the lowest 12-month momentum against USD. Cash not used as margin invest on overnight rates. Rebalance monthly. 50 | 51 | import data_tools 52 | from AlgorithmImports import * 53 | 54 | 55 | class CurrencyMomentumFactor(QCAlgorithm): 56 | def Initialize(self): 57 | self.SetStartDate(2000, 1, 1) 58 | self.SetCash(100000) 59 | 60 | self.data = {} 61 | self.period = 12 * 21 62 | self.SetWarmUp(self.period, Resolution.Daily) 63 | 64 | self.symbols = [ 65 | "CME_AD1", # Australian Dollar Futures, Continuous Contract #1 66 | "CME_BP1", # British Pound Futures, Continuous Contract #1 67 | "CME_CD1", # Canadian Dollar Futures, Continuous Contract #1 68 | "CME_EC1", # Euro FX Futures, Continuous Contract #1 69 | "CME_JY1", # Japanese Yen Futures, Continuous Contract #1 70 | "CME_MP1", # Mexican Peso Futures, Continuous Contract #1 71 | "CME_NE1", # New Zealand Dollar Futures, Continuous Contract #1 72 | "CME_SF1", # Swiss Franc Futures, Continuous Contract #1 73 | ] 74 | 75 | for symbol in self.symbols: 76 | data = self.AddData(data_tools.QuantpediaFutures, symbol, Resolution.Daily) 77 | data.SetFeeModel(data_tools.CustomFeeModel()) 78 | data.SetLeverage(5) 79 | self.data[symbol] = self.ROC(symbol, self.period, Resolution.Daily) 80 | 81 | self.recent_month = -1 82 | 83 | def OnData(self, data): 84 | if self.IsWarmingUp: 85 | return 86 | 87 | # rebalance monthly 88 | if self.Time.month == self.recent_month: 89 | return 90 | self.recent_month = self.Time.month 91 | 92 | perf = { 93 | x[0]: x[1].Current.Value 94 | for x in self.data.items() 95 | if self.data[x[0]].IsReady and x[0] in data and data[x[0]] 96 | } 97 | 98 | long = [] 99 | short = [] 100 | if len(perf) >= 6: 101 | sorted_by_performance = sorted( 102 | perf.items(), key=lambda x: x[1], reverse=True 103 | ) 104 | long = [x[0] for x in sorted_by_performance[:3]] 105 | short = [x[0] for x in sorted_by_performance[-3:]] 106 | 107 | # trade execution 108 | invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested] 109 | for symbol in invested: 110 | if symbol not in long + short: 111 | self.Liquidate(symbol) 112 | 113 | for symbol in long: 114 | self.SetHoldings(symbol, 1 / len(long)) 115 | for symbol in short: 116 | self.SetHoldings(symbol, -1 / len(short)) 117 | -------------------------------------------------------------------------------- /static/strategies/currency-value-factor-ppp-strategy.py: -------------------------------------------------------------------------------- 1 | # region imports 2 | from AlgorithmImports import * 3 | 4 | # endregion 5 | # Custom fee model 6 | class CustomFeeModel(FeeModel): 7 | def GetOrderFee(self, parameters): 8 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 9 | return OrderFee(CashAmount(fee, "USD")) 10 | 11 | 12 | # Quandl "value" data 13 | class QuandlValue(PythonQuandl): 14 | def __init__(self): 15 | self.ValueColumnName = "Value" 16 | 17 | 18 | # Quantpedia data. 19 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 20 | class QuantpediaFutures(PythonData): 21 | def GetSource(self, config, date, isLiveMode): 22 | return SubscriptionDataSource( 23 | "data.quantpedia.com/backtesting_data/futures/{0}.csv".format( 24 | config.Symbol.Value 25 | ), 26 | SubscriptionTransportMedium.RemoteFile, 27 | FileFormat.Csv, 28 | ) 29 | 30 | def Reader(self, config, line, date, isLiveMode): 31 | data = QuantpediaFutures() 32 | data.Symbol = config.Symbol 33 | 34 | if not line[0].isdigit(): 35 | return None 36 | split = line.split(";") 37 | 38 | data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1) 39 | data["back_adjusted"] = float(split[1]) 40 | data["spliced"] = float(split[2]) 41 | data.Value = float(split[1]) 42 | 43 | return data 44 | 45 | 46 | # https://quantpedia.com/strategies/currency-value-factor-ppp-strategy/ 47 | # 48 | # Create an investment universe consisting of several currencies (10-20). Use the latest OECD Purchasing Power Parity figure to assess 49 | # the fair value of each currency versus USD in the month of publishing and then use monthly CPI changes and exchange rate changes to 50 | # create fair PPP value for the month prior to the current month. Go long three currencies that are the most undervalued (lowest PPP 51 | # fair value figure) and go short three currencies that are the most overvalued (highest PPP fair value figure). Invest cash not used 52 | # as margin on overnight rates. Rebalance quarterly or monthly. 53 | # 54 | # QC implementation changes: 55 | # - Yearly rebalance instead of quarterly is performed. 56 | 57 | import data_tools 58 | from AlgorithmImports import * 59 | 60 | 61 | class CurrencyValueFactorPPPStrategy(QCAlgorithm): 62 | def Initialize(self): 63 | self.SetStartDate(2000, 1, 1) 64 | self.SetCash(100000) 65 | 66 | # currency future symbol and PPP yearly quandl symbol 67 | # PPP source: https://www.quandl.com/data/ODA-IMF-Cross-Country-Macroeconomic-Statistics?keyword=%20United%20States%20Implied%20PPP%20Conversion%20Rate 68 | self.symbols = { 69 | "CME_AD1": "ODA/AUS_PPPEX", # Australian Dollar Futures, Continuous Contract #1 70 | "CME_BP1": "ODA/GBR_PPPEX", # British Pound Futures, Continuous Contract #1 71 | "CME_CD1": "ODA/CAD_PPPEX", # Canadian Dollar Futures, Continuous Contract #1 72 | "CME_EC1": "ODA/DEU_PPPEX", # Euro FX Futures, Continuous Contract #1 73 | "CME_JY1": "ODA/JPN_PPPEX", # Japanese Yen Futures, Continuous Contract #1 74 | "CME_NE1": "ODA/NZL_PPPEX", # New Zealand Dollar Futures, Continuous Contract #1 75 | "CME_SF1": "ODA/CHE_PPPEX", # Swiss Franc Futures, Continuous Contract #1 76 | } 77 | 78 | for symbol in self.symbols: 79 | data = self.AddData(data_tools.QuantpediaFutures, symbol, Resolution.Daily) 80 | data.SetFeeModel(data_tools.CustomFeeModel()) 81 | data.SetLeverage(5) 82 | 83 | # PPP quandl data. 84 | ppp_symbol = self.symbols[symbol] 85 | self.AddData(data_tools.QuandlValue, ppp_symbol, Resolution.Daily) 86 | 87 | self.recent_month = -1 88 | 89 | def OnData(self, data): 90 | if self.recent_month == self.Time.month: 91 | return 92 | self.recent_month = self.Time.month 93 | 94 | # January rebalance 95 | if self.recent_month == 1: 96 | ppp = {} 97 | for symbol, ppp_symbol in self.symbols.items(): 98 | # if symbol in data and data[symbol]: 99 | if ( 100 | self.Securities[symbol].GetLastData() 101 | and ( 102 | self.Time.date() 103 | - self.Securities[symbol].GetLastData().Time.date() 104 | ).days 105 | < 3 106 | ): 107 | # new ppp data arrived 108 | if ppp_symbol in data and data[ppp_symbol]: 109 | ppp[symbol] = data[ppp_symbol].Value 110 | 111 | count = 3 112 | long = [] 113 | short = [] 114 | if len(ppp) >= count * 2: 115 | # ppp sorting 116 | sorted_by_ppp = sorted(ppp.items(), key=lambda x: x[1], reverse=True) 117 | long = [x[0] for x in sorted_by_ppp[-count:]] 118 | short = [x[0] for x in sorted_by_ppp[:count]] 119 | 120 | # trade execution 121 | invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested] 122 | for symbol in invested: 123 | if symbol not in long + short: 124 | self.Liquidate(symbol) 125 | 126 | for symbol in long: 127 | self.SetHoldings(symbol, 1 / len(long)) 128 | for symbol in short: 129 | self.SetHoldings(symbol, -1 / len(short)) 130 | -------------------------------------------------------------------------------- /static/strategies/dispersion-trading.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/dispersion-trading/ 2 | # 3 | # The investment universe consists of stocks from the S&P 100 index. 4 | # Trading vehicles are options on stocks from this index and also options on the index itself. 5 | # The investor uses analyst forecasts of earnings per share from the Institutional Brokers Estimate System (I/B/E/S) database and 6 | # computes for each firm the mean absolute difference scaled by an indicator of earnings uncertainty (see page 24 in the source academic paper for detailed methodology). 7 | # Each month, investor sorts stocks into quintiles based on the size of belief disagreement. 8 | # He buys puts of stocks with the highest belief disagreement and sells the index puts with Black-Scholes deltas ranging from -0.8 to -0.2. 9 | # 10 | # QC Implementation: 11 | # - Due to lack of data, strategy only buys puts of 100 liquid US stocks and sells the SPX index puts. 12 | 13 | #region imports 14 | from AlgorithmImports import * 15 | from numpy import floor 16 | #endregion 17 | 18 | class DispersionTrading(QCAlgorithm): 19 | 20 | def Initialize(self): 21 | self.SetStartDate(2010, 1, 1) 22 | self.SetCash(1000000) 23 | 24 | self.min_expiry = 20 25 | self.max_expiry = 60 26 | 27 | self.index_symbol = self.AddIndex('SPX').Symbol 28 | self.percentage_traded = 1.0 29 | 30 | self.spx_contract = None 31 | self.selected_symbols = [] 32 | self.subscribed_contracts = {} 33 | 34 | self.coarse_count = 100 35 | self.UniverseSettings.Resolution = Resolution.Minute 36 | self.AddUniverse(self.CoarseSelectionFunction) 37 | self.SetSecurityInitializer(lambda x: x.SetDataNormalizationMode(DataNormalizationMode.Raw)) 38 | self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw 39 | 40 | def OnSecuritiesChanged(self, changes): 41 | for security in changes.AddedSecurities: 42 | security.SetFeeModel(CustomFeeModel()) 43 | security.SetLeverage(5) 44 | 45 | def CoarseSelectionFunction(self, coarse): 46 | # rebalance on SPX contract expiration (should be on monthly basis) 47 | if len(self.selected_symbols) != 0: 48 | return Universe.Unchanged 49 | 50 | # select top n stocks by dollar volume 51 | selected = sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa' and x.Price > 5], 52 | key=lambda x: x.DollarVolume, reverse=True)[:self.coarse_count] 53 | 54 | self.selected_symbols = [x.Symbol for x in selected] 55 | 56 | return self.selected_symbols 57 | 58 | def OnData(self, data): 59 | # liquidate portfolio, when SPX contract is about to expire in 2 days 60 | if self.index_symbol in self.subscribed_contracts and self.subscribed_contracts[self.index_symbol].ID.Date.date() - timedelta(2) <= self.Time.date(): 61 | self.subscribed_contracts.clear() # perform new subscribtion 62 | self.selected_symbols.clear() # perform new selection 63 | self.Liquidate() 64 | 65 | if len(self.subscribed_contracts) == 0: 66 | if self.Portfolio.Invested: 67 | self.Liquidate() 68 | 69 | # NOTE order is important, index should come first 70 | for symbol in [self.index_symbol] + self.selected_symbols: 71 | # subscribe to contract 72 | contracts = self.OptionChainProvider.GetOptionContractList(symbol, self.Time) 73 | # get current price for stock 74 | underlying_price = self.Securities[symbol].Price 75 | 76 | # get strikes from stock contracts 77 | strikes = [i.ID.StrikePrice for i in contracts] 78 | 79 | # check if there is at least one strike 80 | if len(strikes) <= 0: 81 | continue 82 | 83 | # at the money 84 | atm_strike = min(strikes, key=lambda x: abs(x-underlying_price)) 85 | 86 | # filtred contracts based on option rights and strikes 87 | atm_puts = [i for i in contracts if i.ID.OptionRight == OptionRight.Put and 88 | i.ID.StrikePrice == atm_strike and 89 | self.min_expiry <= (i.ID.Date - self.Time).days <= self.max_expiry] 90 | 91 | # index contract is found 92 | if symbol == self.index_symbol and len(atm_puts) == 0: 93 | # cancel whole selection since index contract was not found 94 | return 95 | 96 | # make sure there are enough contracts 97 | if len(atm_puts) > 0: 98 | # sort by expiry 99 | atm_put = sorted(atm_puts, key = lambda item: item.ID.Date, reverse=True)[0] 100 | 101 | # add contract 102 | option = self.AddOptionContract(atm_put, Resolution.Minute) 103 | option.PriceModel = OptionPriceModels.CrankNicolsonFD() 104 | option.SetDataNormalizationMode(DataNormalizationMode.Raw) 105 | 106 | # store subscribed atm put contract 107 | self.subscribed_contracts[symbol] = atm_put 108 | 109 | # perform trade, when spx and stocks contracts are selected 110 | if not self.Portfolio.Invested and len(self.subscribed_contracts) != 0 and self.index_symbol in self.subscribed_contracts: 111 | index_option_contract = self.subscribed_contracts[self.index_symbol] 112 | # make sure subscribed SPX contract has data 113 | if self.Securities.ContainsKey(index_option_contract): 114 | if self.Securities[index_option_contract].Price != 0 and self.Securities[index_option_contract].IsTradable: 115 | # sell SPX ATM put contract 116 | self.Securities[index_option_contract].MarginModel = BuyingPowerModel(2) 117 | price = self.Securities[self.index_symbol].Price 118 | if price != 0: 119 | q = floor((self.Portfolio.TotalPortfolioValue * self.percentage_traded) / (price*100)) 120 | self.Sell(index_option_contract, q) 121 | 122 | # buy stock's ATM put contracts 123 | long_count = len(self.subscribed_contracts) - 1 # minus index symbol 124 | for stock_symbol, stock_option_contract in self.subscribed_contracts.items(): 125 | if stock_symbol == self.index_symbol: 126 | continue 127 | 128 | if self.Securities[stock_option_contract].Price != 0 and self.Securities[stock_option_contract].IsTradable: 129 | # buy contract 130 | self.Securities[stock_option_contract].MarginModel = BuyingPowerModel(2) 131 | if self.Securities.ContainsKey(stock_option_contract): 132 | price = self.Securities[stock_symbol].Price 133 | if price != 0: 134 | q = floor(((self.Portfolio.TotalPortfolioValue / long_count) * self.percentage_traded) / (price*100)) 135 | self.Buy(stock_option_contract, q) 136 | 137 | # Custom fee model 138 | class CustomFeeModel(FeeModel): 139 | def GetOrderFee(self, parameters): 140 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 141 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/dollar-carry-trade.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/dollar-carry-trade/ 2 | # 3 | # The investment universe consists of currencies from developed countries (the Euro area, Australia, Canada, Denmark, Japan, New Zealand, Norway, Sweden, 4 | # Switzerland, and the United Kingdom). The average forward discount (AFD) is calculated for this basket of currencies (each currency has an equal weight). 5 | # The average 3-month rate could be used instead of the AFD in the calculation. The AFD is then compared to the 3-month US Treasury rate. The investor 6 | # goes long on the US dollar and goes short on the basket of currencies if the 3-month US Treasury rate is higher than the AFD. The investor goes short 7 | # on the US dollar and long on the basket of currencies if the 3-month US Treasury rate is higher than the AFD. The portfolio is rebalanced monthly. 8 | 9 | import numpy as np 10 | from AlgorithmImports import * 11 | 12 | 13 | class DollarCarryTrade(QCAlgorithm): 14 | def Initialize(self): 15 | self.SetStartDate(2000, 1, 1) 16 | self.SetCash(100000) 17 | 18 | self.symbols = { 19 | "CME_AD1": "OECD/KEI_IR3TIB01_AUS_ST_M", # Australian Dollar Futures, Continuous Contract #1 20 | "CME_BP1": "OECD/KEI_IR3TIB01_GBR_ST_M", # British Pound Futures, Continuous Contract #1 21 | "CME_CD1": "OECD/KEI_IR3TIB01_CAN_ST_M", # Canadian Dollar Futures, Continuous Contract #1 22 | "CME_EC1": "OECD/KEI_IR3TIB01_EA19_ST_M", # Euro FX Futures, Continuous Contract #1 23 | "CME_JY1": "OECD/KEI_IR3TIB01_JPN_ST_M", # Japanese Yen Futures, Continuous Contract #1 24 | "CME_MP1": "OECD/KEI_IR3TIB01_MEX_ST_M", # Mexican Peso Futures, Continuous Contract #1 25 | "CME_NE1": "OECD/KEI_IR3TIB01_NZL_ST_M", # New Zealand Dollar Futures, Continuous Contract #1 26 | "CME_SF1": "SNB/ZIMOMA", # Swiss Franc Futures, Continuous Contract #1 27 | } 28 | 29 | for symbol in self.symbols: 30 | data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily) 31 | data.SetFeeModel(CustomFeeModel()) 32 | data.SetLeverage(5) 33 | 34 | # Interbank rate data. 35 | cash_rate_symbol = self.symbols[symbol] 36 | self.AddData(QuandlValue, cash_rate_symbol, Resolution.Daily) 37 | 38 | self.treasury_rate = self.AddData( 39 | QuandlValue, "FRED/DGS3MO", Resolution.Daily 40 | ).Symbol 41 | 42 | def OnData(self, data): 43 | fd = {} 44 | for future_symbol, cash_rate_symbol in self.symbols.items(): 45 | if cash_rate_symbol in data and data[cash_rate_symbol]: 46 | if ( 47 | self.Securities[future_symbol].GetLastData() 48 | and ( 49 | self.Time.date() 50 | - self.Securities[future_symbol].GetLastData().Time.date() 51 | ).days 52 | < 5 53 | ): 54 | cash_rate = data[cash_rate_symbol].Value 55 | # Update cash rate only once a month. 56 | fd[future_symbol] = cash_rate 57 | 58 | if len(fd) == 0: 59 | return 60 | 61 | afd = np.mean([x[1] for x in fd.items()]) 62 | 63 | if ( 64 | self.Securities[self.treasury_rate].GetLastData() 65 | and ( 66 | self.Time.date() 67 | - self.Securities[self.treasury_rate].GetLastData().Time.date() 68 | ).days 69 | < 5 70 | ): 71 | treasuries_3m_rate = self.Securities[self.treasury_rate].Price 72 | 73 | count = len(self.symbols) 74 | if treasuries_3m_rate > afd: 75 | # Long on the US dollar and goes short on the basket of currencies. 76 | for symbol in self.symbols: 77 | self.SetHoldings(symbol, -1 / count) 78 | else: 79 | # Short on the US dollar and long on the basket of currencies. 80 | for symbol in self.symbols: 81 | self.SetHoldings(symbol, 1 / count) 82 | 83 | 84 | # Quantpedia data. 85 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 86 | class QuantpediaFutures(PythonData): 87 | def GetSource(self, config, date, isLiveMode): 88 | return SubscriptionDataSource( 89 | "data.quantpedia.com/backtesting_data/futures/{0}.csv".format( 90 | config.Symbol.Value 91 | ), 92 | SubscriptionTransportMedium.RemoteFile, 93 | FileFormat.Csv, 94 | ) 95 | 96 | def Reader(self, config, line, date, isLiveMode): 97 | data = QuantpediaFutures() 98 | data.Symbol = config.Symbol 99 | 100 | if not line[0].isdigit(): 101 | return None 102 | split = line.split(";") 103 | 104 | data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1) 105 | data["back_adjusted"] = float(split[1]) 106 | data["spliced"] = float(split[2]) 107 | data.Value = float(split[1]) 108 | 109 | return data 110 | 111 | 112 | # Quandl "value" data 113 | class QuandlValue(PythonQuandl): 114 | def __init__(self): 115 | self.ValueColumnName = "Value" 116 | 117 | 118 | # Custom fee model. 119 | class CustomFeeModel(FeeModel): 120 | def GetOrderFee(self, parameters): 121 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 122 | return OrderFee(CashAmount(fee, "USD")) 123 | -------------------------------------------------------------------------------- /static/strategies/earnings-announcements-combined-with-stock-repurchases.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/earnings-announcements-combined-with-stock-repurchases/ 2 | # 3 | # The investment universe consists of stocks from NYSE/AMEX/Nasdaq (no ADRs, CEFs or REITs), bottom 25% of firms by market cap are dropped. 4 | # Each quarter, the investor looks for companies that announce a stock repurchase program (with announced buyback for at least 5% of outstanding stocks) 5 | # during days -30 to -15 before the earnings announcement date for each company. 6 | # Investor goes long stocks with announced buybacks during days -10 to +15 around an earnings announcement. 7 | # The portfolio is equally weighted and rebalanced daily. 8 | # 9 | # QC Implementation: 10 | # - Universe consists of tickers, which have earnings annoucement. 11 | 12 | #region imports 13 | from AlgorithmImports import * 14 | import numpy as np 15 | #endregion 16 | 17 | class EarningsAnnouncementsCombinedWithStockRepurchases(QCAlgorithm): 18 | 19 | def Initialize(self): 20 | self.SetStartDate(2011, 1, 1) # Buyback data strats at 2011 21 | self.SetCash(100000) 22 | 23 | self.fine = {} 24 | self.price = {} 25 | self.managed_symbols = [] 26 | self.earnings_universe = [] 27 | 28 | self.earnings = {} 29 | self.buybacks = {} 30 | 31 | self.max_traded_stocks = 40 # maximum number of trading stocks 32 | self.quantile = 4 33 | 34 | self.symbol = self.AddEquity("SPY", Resolution.Daily).Symbol 35 | 36 | # load earnings dates 37 | csv_data = self.Download('data.quantpedia.com/backtesting_data/economic/earning_dates.csv') 38 | lines = csv_data.split('\r\n') 39 | 40 | for line in lines: 41 | line_split = line.split(';') 42 | date = line_split[0] 43 | 44 | if date == '' : 45 | continue 46 | 47 | date = datetime.strptime(date, "%Y-%m-%d").date() 48 | self.earnings[date] = [] 49 | 50 | for ticker in line_split[1:]: # skip date in current line 51 | self.earnings[date].append(ticker) 52 | 53 | if ticker not in self.earnings_universe: 54 | self.earnings_universe.append(ticker) 55 | 56 | # load buyback dates 57 | csv_data = self.Download('data.quantpedia.com/backtesting_data/equity/BUY_BACKS.csv') 58 | lines = csv_data.split('\r\n') 59 | 60 | for line in lines[1:]: # skip header 61 | line_split = line.split(';') 62 | date = line_split[0] 63 | 64 | if date == '' : 65 | continue 66 | 67 | date = datetime.strptime(date, "%d.%m.%Y").date() 68 | self.buybacks[date] = [] 69 | 70 | for ticker in line_split[1:]: # skip date in current line 71 | self.buybacks[date].append(ticker) 72 | 73 | self.months_counter = 0 74 | self.selection_flag = False 75 | self.UniverseSettings.Resolution = Resolution.Daily 76 | self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) 77 | self.Schedule.On(self.DateRules.MonthStart(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection) 78 | 79 | def OnSecuritiesChanged(self, changes): 80 | for security in changes.AddedSecurities: 81 | security.SetFeeModel(CustomFeeModel()) 82 | security.SetLeverage(5) 83 | 84 | def CoarseSelectionFunction(self, coarse): 85 | # update stocks last prices 86 | for stock in coarse: 87 | ticker = stock.Symbol.Value 88 | 89 | if ticker in self.earnings_universe: 90 | # store stock's last price 91 | self.price[ticker] = stock.AdjustedPrice 92 | 93 | # rebalance quarterly 94 | if not self.selection_flag: 95 | return Universe.Unchanged 96 | self.selection_flag = False 97 | 98 | # select stocks, which had spin off 99 | selected = [x.Symbol for x in coarse if x.Symbol.Value in self.earnings_universe] 100 | 101 | return selected 102 | 103 | def FineSelectionFunction(self, fine): 104 | fine = [x for x in fine if x.MarketCap != 0 and 105 | ((x.SecurityReference.ExchangeId == "NYS") or 106 | (x.SecurityReference.ExchangeId == "NAS") or 107 | (x.SecurityReference.ExchangeId == "ASE"))] 108 | 109 | if len(fine) < self.quantile: 110 | return Universe.Unchanged 111 | 112 | # exclude 25% stocks with lowest market capitalization 113 | quantile = int(len(fine) / self.quantile) 114 | sorted_by_market_cap = sorted(fine, key = lambda x: x.MarketCap) 115 | selected = sorted_by_market_cap[quantile:] 116 | self.fine = {x.Symbol.Value : x.Symbol for x in selected} 117 | 118 | return list(self.fine.values()) 119 | 120 | def OnData(self, data:Slice) -> None: 121 | remove_managed_symbols = [] 122 | # maybe there should be BDay(15) 123 | liquidate_date = self.Time.date() - timedelta(15) 124 | 125 | # check if bought stocks have 15 days after earnings annoucemnet 126 | for managed_symbol in self.managed_symbols: 127 | if managed_symbol.earnings_date >= liquidate_date: 128 | remove_managed_symbols.append(managed_symbol) 129 | 130 | # liquidate stock by selling it's quantity 131 | self.MarketOrder(managed_symbol.symbol, -managed_symbol.quantity) 132 | 133 | # remove liquidated stocks from self.managed_symbols 134 | for managed_symbol in remove_managed_symbols: 135 | self.managed_symbols.remove(managed_symbol) 136 | 137 | # maybe there should be BDay(10) 138 | after_current = self.Time.date() + timedelta(10) 139 | 140 | if after_current in self.earnings: 141 | # this stocks has earnings annoucement after 10 days 142 | stocks_with_earnings = self.earnings[after_current] 143 | 144 | # 30 days before earnings annoucement 145 | buyback_start = self.Time.date() - timedelta(20) 146 | # 15 days before earnings annoucement 147 | buyback_end = self.Time.date() - timedelta(5) 148 | 149 | stocks_with_buyback = [] # storing stocks with buyback in period -30 to -15 days before earnings annoucement 150 | 151 | for buyback_date, tickers in self.buybacks.items(): 152 | # check if buyback date is in period before earnings annoucement 153 | if buyback_date >= buyback_start and buyback_date <= buyback_end: 154 | # iterate through each stock ticker for buyback date 155 | for ticker in tickers: 156 | # add stock ticker if it isn't already added, it has earnings annoucement after 10 days and was selected in fine 157 | if (ticker not in stocks_with_buyback) and (ticker in stocks_with_earnings) and (ticker in self.fine): 158 | stocks_with_buyback.append(self.fine[ticker]) 159 | 160 | # buying stocks buyback in period -30 to -15 days before earnings annoucement 161 | # and stocks, which have earnings date -10 days before current date 162 | for symbol in stocks_with_buyback: 163 | # check if there is a place in Portfolio for trading current stock 164 | if not len(self.managed_symbols) < self.max_traded_stocks: 165 | continue 166 | 167 | # calculate stock quantity 168 | weight = self.Portfolio.TotalPortfolioValue / self.max_traded_stocks 169 | quantity = np.floor(weight / self.price[symbol.Value]) 170 | 171 | # go long stock 172 | self.MarketOrder(symbol, quantity) 173 | 174 | # store stock's ticker, earnings date and traded quantity 175 | if symbol in data and data[symbol]: 176 | self.managed_symbols.append(ManagedSymbol(symbol, after_current, quantity)) 177 | 178 | def Selection(self): 179 | # quarterly selection 180 | if self.months_counter % 3 == 0: 181 | self.selection_flag = True 182 | self.months_counter += 1 183 | 184 | class ManagedSymbol(): 185 | def __init__(self, symbol, earnings_date, quantity): 186 | self.symbol = symbol 187 | self.earnings_date = earnings_date 188 | self.quantity = quantity 189 | 190 | # custom fee model 191 | class CustomFeeModel(FeeModel): 192 | def GetOrderFee(self, parameters): 193 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 194 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/fed-model.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/fed-model/ 2 | # 3 | # Each month, the investor conducts a one-month predictive regression (using all available data up to that date) predicting excess stock market 4 | # returns using the yield gap as an independent variable. The “Yield gap” is calculated as YG = EY − y, with earnings yield EY ≡ ln (1 ++ E/P) 5 | # and y = ln (1 ++ Y) is the log 10 year Treasury bond yield. Then, the strategy allocates 100% in the risky asset if the forecasted excess 6 | # returns are positive, and otherwise, it invests 100% in the risk-free rate. 7 | 8 | from collections import deque 9 | from AlgorithmImports import * 10 | import numpy as np 11 | from scipy import stats 12 | 13 | 14 | class FEDModel(QCAlgorithm): 15 | def Initialize(self): 16 | self.SetStartDate(2000, 1, 1) 17 | self.SetCash(100000) 18 | 19 | # monthly price data and yield gap data 20 | self.data = {} 21 | 22 | self.period = 12 * 21 23 | self.SetWarmUp(self.period) 24 | 25 | self.market = self.AddEquity("SPY", Resolution.Daily).Symbol 26 | self.market_data = deque() 27 | 28 | self.cash = self.AddEquity("SHY", Resolution.Daily).Symbol 29 | 30 | # risk free rate 31 | self.risk_free_rate = self.AddData( 32 | QuandlValue, "FRED/DGS3MO", Resolution.Daily 33 | ).Symbol 34 | 35 | # 10Y bond yield symbol 36 | self.bond_yield = self.AddData( 37 | QuantpediaBondYield, "US10YT", Resolution.Daily 38 | ).Symbol 39 | 40 | # SP500 earnings yield data 41 | self.sp_earnings_yield = self.AddData( 42 | QuandlValue, "MULTPL/SP500_EARNINGS_YIELD_MONTH", Resolution.Daily 43 | ).Symbol 44 | 45 | self.yield_gap = deque() 46 | 47 | self.recent_month = -1 48 | 49 | def OnData(self, data): 50 | rebalance_flag = False 51 | 52 | if self.sp_earnings_yield in data and data[self.sp_earnings_yield]: 53 | if self.Time.month != self.recent_month: 54 | self.recent_month = self.Time.month 55 | rebalance_flag = True 56 | 57 | if not rebalance_flag: 58 | # earnings yield data is no longer coming in 59 | if self.Securities[self.sp_earnings_yield].GetLastData(): 60 | if ( 61 | self.Time.date() 62 | - self.Securities[self.sp_earnings_yield].GetLastData().Time.date() 63 | ).days > 31: 64 | self.Liquidate() 65 | return 66 | 67 | # pdate market price data 68 | if ( 69 | self.market in data 70 | and self.risk_free_rate in data 71 | and self.bond_yield in data 72 | ): 73 | if ( 74 | data[self.market] 75 | and data[self.risk_free_rate] 76 | and data[self.bond_yield] 77 | ): 78 | market_price = data[self.market].Value 79 | rf_rate = data[self.risk_free_rate].Value 80 | bond_yield = data[self.bond_yield].Value 81 | sp_ey = data[self.sp_earnings_yield].Value 82 | if ( 83 | market_price != 0 84 | and rf_rate != 0 85 | and bond_yield != 0 86 | and sp_ey != 0 87 | ): 88 | self.market_data.append((market_price, rf_rate)) 89 | 90 | yield_gap = np.log(sp_ey) - np.log(bond_yield) 91 | self.yield_gap.append(yield_gap) 92 | rebalance_flag = True 93 | 94 | # ensure minimum data points to calculate regression 95 | min_count = 6 96 | if len(self.market_data) >= min_count: 97 | market_closes = np.array([x[0] for x in self.market_data]) 98 | market_returns = (market_closes[1:] - market_closes[:-1]) / market_closes[ 99 | :-1 100 | ] 101 | rf_rates = np.array([x[1] for x in self.market_data][1:]) 102 | excess_returns = market_returns - rf_rates 103 | 104 | yield_gaps = [x for x in self.yield_gap] 105 | 106 | # linear regression 107 | # Y = α + (β ∗ X) 108 | # intercept = alpha 109 | # slope = beta 110 | beta, alpha, r_value, p_value, std_err = stats.linregress( 111 | yield_gaps[1:-1], market_returns[1:] 112 | ) 113 | X = yield_gaps[-1] 114 | 115 | # predicted market return 116 | Y = alpha + (beta * X) 117 | 118 | # trade execution / rebalance 119 | if Y > 0: 120 | if self.Portfolio[self.cash].Invested: 121 | self.Liquidate(self.cash) 122 | self.SetHoldings(self.market, 1) 123 | else: 124 | if self.Portfolio[self.market].Invested: 125 | self.Liquidate(self.market) 126 | self.SetHoldings(self.cash, 1) 127 | 128 | 129 | # Quantpedia bond yield data. 130 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 131 | class QuantpediaBondYield(PythonData): 132 | def GetSource(self, config, date, isLiveMode): 133 | return SubscriptionDataSource( 134 | "data.quantpedia.com/backtesting_data/bond_yield/{0}.csv".format( 135 | config.Symbol.Value 136 | ), 137 | SubscriptionTransportMedium.RemoteFile, 138 | FileFormat.Csv, 139 | ) 140 | 141 | def Reader(self, config, line, date, isLiveMode): 142 | data = QuantpediaBondYield() 143 | data.Symbol = config.Symbol 144 | 145 | if not line[0].isdigit(): 146 | return None 147 | split = line.split(",") 148 | 149 | data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1) 150 | data["yield"] = float(split[1]) 151 | data.Value = float(split[1]) 152 | 153 | return data 154 | 155 | 156 | # Quandl "value" data 157 | class QuandlValue(PythonQuandl): 158 | def __init__(self): 159 | self.ValueColumnName = "Value" 160 | -------------------------------------------------------------------------------- /static/strategies/fx-carry-trade.py: -------------------------------------------------------------------------------- 1 | # region imports 2 | from AlgorithmImports import * 3 | 4 | # endregion 5 | # Quandl "value" data 6 | class QuandlValue(PythonQuandl): 7 | def __init__(self): 8 | self.ValueColumnName = "Value" 9 | 10 | 11 | # Quantpedia data. 12 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 13 | class QuantpediaFutures(PythonData): 14 | def GetSource(self, config, date, isLiveMode): 15 | return SubscriptionDataSource( 16 | "data.quantpedia.com/backtesting_data/futures/{0}.csv".format( 17 | config.Symbol.Value 18 | ), 19 | SubscriptionTransportMedium.RemoteFile, 20 | FileFormat.Csv, 21 | ) 22 | 23 | def Reader(self, config, line, date, isLiveMode): 24 | data = QuantpediaFutures() 25 | data.Symbol = config.Symbol 26 | 27 | if not line[0].isdigit(): 28 | return None 29 | split = line.split(";") 30 | 31 | data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1) 32 | data["back_adjusted"] = float(split[1]) 33 | data["spliced"] = float(split[2]) 34 | data.Value = float(split[1]) 35 | 36 | return data 37 | 38 | 39 | # Custom fee model. 40 | class CustomFeeModel: 41 | def GetOrderFee(self, parameters): 42 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 43 | return OrderFee(CashAmount(fee, "USD")) 44 | 45 | 46 | # region imports 47 | from AlgorithmImports import * 48 | 49 | # endregion 50 | # https://quantpedia.com/strategies/fx-carry-trade/ 51 | # 52 | # Create an investment universe consisting of several currencies (10-20). Go long three currencies with the highest central bank prime rates and 53 | # go short three currencies with the lowest central bank prime rates. The cash not used as the margin is invested in overnight rates. The strategy 54 | # is rebalanced monthly. 55 | 56 | import data_tools 57 | 58 | 59 | class ForexCarryTrade(QCAlgorithm): 60 | def Initialize(self): 61 | self.SetStartDate(2000, 1, 1) 62 | self.SetCash(100000) 63 | 64 | # Source: https://www.quandl.com/data/OECD-Organisation-for-Economic-Co-operation-and-Development 65 | self.symbols = { 66 | "CME_AD1": "OECD/KEI_IR3TIB01_AUS_ST_M", # Australian Dollar Futures, Continuous Contract #1 67 | "CME_BP1": "OECD/KEI_IR3TIB01_GBR_ST_M", # British Pound Futures, Continuous Contract #1 68 | "CME_CD1": "OECD/KEI_IR3TIB01_CAN_ST_M", # Canadian Dollar Futures, Continuous Contract #1 69 | "CME_EC1": "OECD/KEI_IR3TIB01_EA19_ST_M", # Euro FX Futures, Continuous Contract #1 70 | "CME_JY1": "OECD/KEI_IR3TIB01_JPN_ST_M", # Japanese Yen Futures, Continuous Contract #1 71 | "CME_MP1": "OECD/KEI_IR3TIB01_MEX_ST_M", # Mexican Peso Futures, Continuous Contract #1 72 | "CME_NE1": "OECD/KEI_IR3TIB01_NZL_ST_M", # New Zealand Dollar Futures, Continuous Contract #1 73 | "CME_SF1": "SNB/ZIMOMA", # Swiss Franc Futures, Continuous Contract #1 74 | } 75 | 76 | for symbol, rate_symbol in self.symbols.items(): 77 | self.AddData(Quandl, rate_symbol, Resolution.Daily) 78 | 79 | data = self.AddData(data_tools.QuantpediaFutures, symbol, Resolution.Daily) 80 | data.SetFeeModel(data_tools.CustomFeeModel()) 81 | data.SetLeverage(5) 82 | 83 | self.recent_month = -1 84 | 85 | def OnData(self, data): 86 | rebalance_flag: bool = False 87 | rate: dict[str, float] = {} 88 | 89 | for symbol, int_rate in self.symbols.items(): 90 | # futures data is present in the algorithm 91 | if symbol in data and data[symbol]: 92 | if self.recent_month != self.Time.month: 93 | rebalance_flag = True 94 | self.recent_month = self.Time.month 95 | 96 | # IR data is still coming in 97 | if ( 98 | self.Securities[int_rate].GetLastData() 99 | and ( 100 | self.Time.date() 101 | - self.Securities[int_rate].GetLastData().Time.date() 102 | ).days 103 | <= 31 104 | ): 105 | rate[symbol] = self.Securities[int_rate].Price 106 | 107 | if rebalance_flag: 108 | long = [] 109 | short = [] 110 | if len(rate) >= 3: 111 | # interbank rate sorting 112 | sorted_by_rate = sorted(rate.items(), key=lambda x: x[1], reverse=True) 113 | traded_count = 3 114 | long = [x[0] for x in sorted_by_rate[:traded_count]] 115 | short = [x[0] for x in sorted_by_rate[-traded_count:]] 116 | 117 | # trade execution 118 | invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested] 119 | for symbol in invested: 120 | if symbol not in long + short: 121 | self.Liquidate(symbol) 122 | 123 | for symbol in long: 124 | self.SetHoldings(symbol, 1 / len(long)) 125 | for symbol in short: 126 | self.SetHoldings(symbol, -1 / len(short)) 127 | -------------------------------------------------------------------------------- /static/strategies/intraday-seasonality-in-bitcoin.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/intraday-seasonality-in-bitcoin/ 2 | # 3 | # The investment universe consists of Bitcoin and the data are obtained from Gemini exchange. To exploit the seasonality, open a long position in the BTC at 22:00 (UTC +0) and hold it for two hours. The position is closed after the two hour holding period. 4 | # 5 | # QC implementation changes: 6 | # - BTC data are obtained from Bitfinex exchange. 7 | 8 | # region imports 9 | from AlgorithmImports import * 10 | # endregion 11 | 12 | class OvernightSeasonalityinBitcoin(QCAlgorithm): 13 | 14 | def Initialize(self): 15 | self.SetStartDate(2016, 1, 1) 16 | self.SetCash(100000) 17 | 18 | # NOTE Coinbase Pro, CoinAPI, and Bitfinex data is all set in UTC Time. This means that when accessing data from this brokerage, all data will be time stamped in UTC Time. 19 | self.crypto = self.AddCrypto("BTCUSD", Resolution.Minute, Market.Bitfinex) 20 | self.crypto.SetLeverage(10) 21 | self.crypto.SetFeeModel(CustomFeeModel()) 22 | self.crypto = self.crypto.Symbol 23 | 24 | self.open_trade_hour:int = 22 25 | self.close_trade_hour:int = 0 26 | 27 | def OnData(self, data): 28 | if self.crypto in data and data[self.crypto]: 29 | time:datetime.datetime = self.UtcTime 30 | 31 | # open long position 32 | if time.hour == self.open_trade_hour and time.minute == 0: 33 | self.SetHoldings(self.crypto, 1) 34 | 35 | # close position 36 | if time.hour == self.close_trade_hour and time.minute == 0: 37 | if self.Portfolio[self.crypto].Invested: 38 | self.Liquidate(self.crypto) 39 | 40 | class CustomFeeModel(FeeModel): 41 | def GetOrderFee(self, parameters): 42 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 43 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/january-barometer.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/january-barometer/ 2 | # 3 | # Invest in the equity market in each January. Stay invested in equity markets (via ETF, fund, or futures) only if January return is positive; otherwise, switch investments to T-Bills. 4 | 5 | from AlgorithmImports import * 6 | 7 | 8 | class JanuaryBarometer(QCAlgorithm): 9 | def Initialize(self): 10 | self.SetStartDate(2000, 1, 1) 11 | self.SetCash(100000) 12 | 13 | data = self.AddEquity("SPY", Resolution.Daily) 14 | data.SetLeverage(10) 15 | self.market = data.Symbol 16 | 17 | data = self.AddEquity("BIL", Resolution.Daily) 18 | data.SetLeverage(10) 19 | self.t_bills = data.Symbol 20 | 21 | self.start_price = None 22 | self.recent_month = -1 23 | 24 | def OnData(self, data): 25 | if self.recent_month == self.Time.month: 26 | return 27 | self.recent_month = self.Time.month 28 | 29 | if ( 30 | self.Securities[self.market].GetLastData() 31 | and self.Securities[self.t_bills].GetLastData() 32 | ): 33 | if ( 34 | self.Time.date() 35 | - self.Securities[self.market].GetLastData().Time.date() 36 | ).days < 5 and ( 37 | self.Time.date() 38 | - self.Securities[self.t_bills].GetLastData().Time.date() 39 | ).days < 5: 40 | if self.Time.month == 1: 41 | self.Liquidate(self.t_bills) 42 | self.SetHoldings(self.market, 1) 43 | 44 | self.start_price = self.Securities[self.market].Price 45 | 46 | if self.Time.month == 2 and self.start_price: 47 | returns = ( 48 | self.Securities[self.market].Price - self.start_price 49 | ) / self.start_price 50 | if returns > 0: 51 | self.SetHoldings(self.market, 1) 52 | else: 53 | self.start_price = None 54 | self.Liquidate(self.market) 55 | self.SetHoldings(self.t_bills, 1) 56 | else: 57 | self.Liquidate() 58 | else: 59 | self.Liquidate() 60 | -------------------------------------------------------------------------------- /static/strategies/low-volatility-factor-effect-in-stocks.py: -------------------------------------------------------------------------------- 1 | #region imports 2 | from AlgorithmImports import * 3 | #endregion 4 | # https://quantpedia.com/strategies/low-volatility-factor-effect-in-stocks-long-only-version/ 5 | # 6 | # The investment universe consists of global large-cap stocks (or US large-cap stocks). At the end of each month, the investor constructs 7 | # equally weighted decile portfolios by ranking the stocks on the past three-year volatility of weekly returns. The investor goes long 8 | # stocks in the top decile (stocks with the lowest volatility). 9 | # 10 | # QC implementation changes: 11 | # - Top quartile (stocks with the lowest volatility) is selected instead of decile. 12 | 13 | import numpy as np 14 | 15 | class LowVolatilityFactorEffectStocks(QCAlgorithm): 16 | 17 | def Initialize(self): 18 | self.SetStartDate(2000, 1, 1) 19 | self.SetCash(100000) 20 | 21 | self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol 22 | 23 | self.period = 12*21 24 | 25 | self.coarse_count = 3000 26 | self.last_coarse = [] 27 | self.data = {} 28 | 29 | self.long = [] 30 | 31 | self.selection_flag = True 32 | self.UniverseSettings.Resolution = Resolution.Daily 33 | self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) 34 | self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection) 35 | 36 | def OnSecuritiesChanged(self, changes): 37 | for security in changes.AddedSecurities: 38 | security.SetFeeModel(CustomFeeModel()) 39 | security.SetLeverage(10) 40 | 41 | def CoarseSelectionFunction(self, coarse): 42 | # Update the rolling window every day. 43 | for stock in coarse: 44 | symbol = stock.Symbol 45 | 46 | # Store daily price. 47 | if symbol in self.data: 48 | self.data[symbol].update(stock.AdjustedPrice) 49 | 50 | if not self.selection_flag: 51 | return Universe.Unchanged 52 | 53 | selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa'] 54 | # selected = [x.Symbol 55 | # for x in sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa'], 56 | # key = lambda x: x.DollarVolume, reverse = True)[:self.coarse_count]] 57 | 58 | # Warmup price rolling windows. 59 | for symbol in selected: 60 | if symbol in self.data: 61 | continue 62 | 63 | self.data[symbol] = SymbolData(self.period) 64 | history = self.History(symbol, self.period, Resolution.Daily) 65 | if history.empty: 66 | self.Log(f"Not enough data for {symbol} yet.") 67 | continue 68 | closes = history.loc[symbol].close 69 | for time, close in closes.iteritems(): 70 | self.data[symbol].update(close) 71 | 72 | return [x for x in selected if self.data[x].is_ready()] 73 | 74 | def FineSelectionFunction(self, fine): 75 | fine = [x for x in fine if x.MarketCap != 0] 76 | 77 | # market cap sorting 78 | if len(fine) > self.coarse_count: 79 | sorted_by_market_cap = sorted(fine, key = lambda x: x.MarketCap, reverse=True) 80 | fine = sorted_by_market_cap[:self.coarse_count] 81 | 82 | weekly_vol = {x.Symbol : self.data[x.Symbol].volatility() for x in fine} 83 | 84 | # volatility sorting 85 | sorted_by_vol = sorted(weekly_vol.items(), key = lambda x: x[1], reverse = True) 86 | quartile = int(len(sorted_by_vol) / 4) 87 | self.long = [x[0] for x in sorted_by_vol[-quartile:]] 88 | 89 | return self.long 90 | 91 | def OnData(self, data): 92 | if not self.selection_flag: 93 | return 94 | self.selection_flag = False 95 | 96 | # Trade execution. 97 | stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested] 98 | for symbol in stocks_invested: 99 | if symbol not in self.long: 100 | self.Liquidate(symbol) 101 | 102 | for symbol in self.long: 103 | if symbol in data and data[symbol]: 104 | self.SetHoldings(symbol, 1 / len(self.long)) 105 | 106 | self.long.clear() 107 | 108 | def Selection(self): 109 | self.selection_flag = True 110 | 111 | class SymbolData(): 112 | def __init__(self, period): 113 | self.price = RollingWindow[float](period) 114 | 115 | def update(self, value): 116 | self.price.Add(value) 117 | 118 | def is_ready(self) -> bool: 119 | return self.price.IsReady 120 | 121 | def volatility(self) -> float: 122 | closes = [x for x in self.price] 123 | 124 | # Weekly volatility calc. 125 | separete_weeks = [closes[x:x+5] for x in range(0, len(closes), 5)] 126 | weekly_returns = [(x[0] - x[-1]) / x[-1] for x in separete_weeks] 127 | return np.std(weekly_returns) 128 | 129 | # Custom fee model. 130 | class CustomFeeModel(FeeModel): 131 | def GetOrderFee(self, parameters): 132 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 133 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/market-sentiment-and-an-overnight-anomaly.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/market-sentiment-and-an-overnight-anomaly/ 2 | # 3 | # The investment universe consists of SPY ETF, and the price of SPY, price of VIX and Brain Market Sentiment (BMS) indicator 4 | # are used to identify the market sentiment. The investor buys SPY ETF and holds it overnight; when the price of SPY is above its 20-day moving average, 5 | # the price of VIX is below its moving average, and the value of the BMS indicator is greater than its 20-day moving average. 6 | # Note that the authors suggest using this strategy as an overlay when deciding whether to make a trade rather than using this system on its own. 7 | # 8 | # QC Implementation: 9 | 10 | # region imports 11 | from AlgorithmImports import * 12 | 13 | # endregion 14 | 15 | 16 | class MarketSentimentAndAnOvernightAnomaly(QCAlgorithm): 17 | def Initialize(self): 18 | self.SetStartDate(2000, 1, 1) 19 | self.SetCash(100000) 20 | 21 | self.period: int = 20 # sma period 22 | 23 | self.weight: float = 0 24 | self.price_data: dict = {} 25 | 26 | self.spy_symbol: Symbol = self.AddEquity("SPY", Resolution.Minute).Symbol 27 | self.vix_symbol: Symbol = self.AddData( 28 | QuandlVix, "CBOE/VIX", Resolution.Daily 29 | ).Symbol # starts in 2004 30 | self.bms_symbol: Symbol = self.AddData( 31 | QuantpediaBMS, "BMS_GLOBAL", Resolution.Daily 32 | ).Symbol # starts in 2018 33 | 34 | for symbol in [self.spy_symbol, self.vix_symbol, self.bms_symbol]: 35 | self.price_data[symbol] = RollingWindow[float](self.period) 36 | 37 | def OnData(self, data: Slice): 38 | # calculate signal from SPY 16 minutes before close 39 | if ( 40 | self.spy_symbol in data 41 | and data[self.spy_symbol] 42 | and self.Time.hour == 15 43 | and self.Time.minute == 44 44 | ): 45 | weight: float = 0.0 46 | 47 | for symbol in [self.spy_symbol, self.vix_symbol, self.bms_symbol]: 48 | # trade only sub-strategies with underlying data available 49 | if ( 50 | self.Securities[symbol].GetLastData() 51 | and ( 52 | self.Time.date() 53 | - self.Securities[symbol].GetLastData().Time.date() 54 | ).days 55 | <= 3 56 | ): 57 | price: float = self.Securities[symbol].GetLastData().Price 58 | rolling_window: RollingWindow = self.price_data[symbol] 59 | if rolling_window.IsReady and self.GetSignal( 60 | price, 61 | rolling_window, 62 | True if symbol != self.vix_symbol else False, 63 | ): 64 | weight += 1 / 3 65 | 66 | rolling_window.Add(price) 67 | 68 | q: int = int( 69 | (self.Portfolio.TotalPortfolioValue * weight) 70 | / data[self.spy_symbol].Value 71 | ) 72 | if q != 0: 73 | self.MarketOnCloseOrder(self.spy_symbol, q) 74 | self.MarketOnOpenOrder(self.spy_symbol, -q) 75 | 76 | def GetSignal( 77 | self, curr_value: float, rolling_window: RollingWindow, signal_above_sma: bool 78 | ) -> bool: 79 | prices: list[float] = [x for x in rolling_window] 80 | moving_average: float = sum(prices) / len(prices) 81 | 82 | result: bool = False 83 | if signal_above_sma and (curr_value > moving_average): 84 | result = True 85 | elif not signal_above_sma and (curr_value < moving_average): 86 | result = True 87 | 88 | return result 89 | 90 | 91 | # Quantpedia data. 92 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 93 | class QuantpediaBMS(PythonData): 94 | def GetSource(self, config, date, isLiveMode): 95 | return SubscriptionDataSource( 96 | "data.quantpedia.com/backtesting_data/index/{0}.csv".format( 97 | config.Symbol.Value 98 | ), 99 | SubscriptionTransportMedium.RemoteFile, 100 | FileFormat.Csv, 101 | ) 102 | 103 | def Reader(self, config, line, date, isLiveMode): 104 | data: QuantpediaBMS = QuantpediaBMS() 105 | data.Symbol = config.Symbol 106 | 107 | if not line[0].isdigit(): 108 | return None 109 | 110 | split: list = line.split(",") 111 | 112 | data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1) 113 | data.Value = float(split[2]) 114 | 115 | return data 116 | 117 | 118 | class QuandlVix(PythonQuandl): 119 | def __init__(self): 120 | self.ValueColumnName = "VIX Close" 121 | -------------------------------------------------------------------------------- /static/strategies/momentum-effect-in-commodities.py: -------------------------------------------------------------------------------- 1 | #region imports 2 | from AlgorithmImports import * 3 | #endregion 4 | # https://quantpedia.com/strategies/1-month-momentum-in-commodities/ 5 | # 6 | # Create a universe of tradable commodity futures. Rank futures performance for each commodity for the last 12 months and divide them into quintiles. 7 | # Go long on the quintile with the highest momentum and go short on the quintile with the lowest momentum. Rebalance each month. 8 | 9 | class MomentumEffectCommodities(QCAlgorithm): 10 | 11 | def Initialize(self): 12 | self.SetStartDate(2000, 1, 1) 13 | self.SetCash(100000) 14 | 15 | self.symbols = [ 16 | "CME_S1", # Soybean Futures, Continuous Contract 17 | "CME_W1", # Wheat Futures, Continuous Contract 18 | "CME_SM1", # Soybean Meal Futures, Continuous Contract 19 | "CME_BO1", # Soybean Oil Futures, Continuous Contract 20 | "CME_C1", # Corn Futures, Continuous Contract 21 | "CME_O1", # Oats Futures, Continuous Contract 22 | "CME_LC1", # Live Cattle Futures, Continuous Contract 23 | "CME_FC1", # Feeder Cattle Futures, Continuous Contract 24 | "CME_LN1", # Lean Hog Futures, Continuous Contract 25 | "CME_GC1", # Gold Futures, Continuous Contract 26 | "CME_SI1", # Silver Futures, Continuous Contract 27 | "CME_PL1", # Platinum Futures, Continuous Contract 28 | "CME_CL1", # Crude Oil Futures, Continuous Contract 29 | "CME_HG1", # Copper Futures, Continuous Contract 30 | "CME_LB1", # Random Length Lumber Futures, Continuous Contract 31 | "CME_NG1", # Natural Gas (Henry Hub) Physical Futures, Continuous Contract 32 | "CME_PA1", # Palladium Futures, Continuous Contract 33 | "CME_RR1", # Rough Rice Futures, Continuous Contract 34 | "CME_DA1", # Class III Milk Futures 35 | "ICE_RS1", # Canola Futures, Continuous Contract 36 | "ICE_GO1", # Gas Oil Futures, Continuous Contract 37 | "CME_RB2", # Gasoline Futures, Continuous Contract 38 | "CME_KW2", # Wheat Kansas, Continuous Contract 39 | "ICE_WT1", # WTI Crude Futures, Continuous Contract 40 | 41 | "ICE_CC1", # Cocoa Futures, Continuous Contract 42 | "ICE_CT1", # Cotton No. 2 Futures, Continuous Contract 43 | "ICE_KC1", # Coffee C Futures, Continuous Contract 44 | "ICE_O1", # Heating Oil Futures, Continuous Contract 45 | "ICE_OJ1", # Orange Juice Futures, Continuous Contract 46 | "ICE_SB1", # Sugar No. 11 Futures, Continuous Contract 47 | ] 48 | 49 | self.period = 12 * 21 50 | self.SetWarmUp(self.period, Resolution.Daily) 51 | self.data = {} 52 | 53 | for symbol in self.symbols: 54 | data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily) 55 | data.SetFeeModel(CustomFeeModel()) 56 | data.SetLeverage(5) 57 | self.data[symbol] = self.ROC(symbol, self.period, Resolution.Daily) 58 | 59 | self.recent_month = -1 60 | 61 | def OnData(self, data): 62 | if self.IsWarmingUp: 63 | return 64 | 65 | # rebalance once a month 66 | if self.recent_month == self.Time.month: 67 | return 68 | self.recent_month = self.Time.month 69 | 70 | perf = { x[0] : x[1].Current.Value for x in self.data.items() if self.data[x[0]].IsReady and x[0] in data and data[x[0]] } 71 | 72 | long = [] 73 | short = [] 74 | if len(perf) >= 5: 75 | sorted_by_performance = sorted(perf.items(), key = lambda x:x[1], reverse=True) 76 | quintile = int(len(sorted_by_performance) / 5) 77 | long = [x[0] for x in sorted_by_performance[:quintile]] 78 | short = [x[0] for x in sorted_by_performance[-quintile:]] 79 | 80 | # trade execution 81 | invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested] 82 | for symbol in invested: 83 | if symbol not in long + short: 84 | self.Liquidate(symbol) 85 | 86 | for symbol in long: 87 | self.SetHoldings(symbol, 1 / len(long)) 88 | for symbol in short: 89 | self.SetHoldings(symbol, -1 / len(short)) 90 | 91 | # Quantpedia data. 92 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 93 | class QuantpediaFutures(PythonData): 94 | def GetSource(self, config, date, isLiveMode): 95 | return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv) 96 | 97 | def Reader(self, config, line, date, isLiveMode): 98 | data = QuantpediaFutures() 99 | data.Symbol = config.Symbol 100 | 101 | if not line[0].isdigit(): return None 102 | split = line.split(';') 103 | 104 | data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1) 105 | data['back_adjusted'] = float(split[1]) 106 | data['spliced'] = float(split[2]) 107 | data.Value = float(split[1]) 108 | 109 | return data 110 | 111 | # Custom fee model. 112 | class CustomFeeModel(): 113 | def GetOrderFee(self, parameters): 114 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 115 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/momentum-factor-and-style-rotation-effect.py: -------------------------------------------------------------------------------- 1 | #region imports 2 | from AlgorithmImports import * 3 | #endregion 4 | # https://quantpedia.com/strategies/momentum-factor-and-style-rotation-effect/ 5 | # 6 | # Russell’s ETFs for six equity styles are used 7 | # (small-cap value, mid-cap value, large-cap value, small-cap growth, mid-cap growth, large-cap growth). 8 | # Each month, the investor calculates 12-month momentum for each style and goes long on the winner and short on the loser. 9 | # The portfolio is rebalanced each month. 10 | # 11 | # QC Implementation: 12 | # - Trading IVW in 10/2020 is skipped due to data error. 13 | 14 | class MomentumFactorAndStyleRotationEffect(QCAlgorithm): 15 | 16 | def Initialize(self): 17 | self.SetStartDate(2000, 1, 1) 18 | self.SetCash(100000) 19 | 20 | self.tickers = [ 21 | 'IWS', # iShares Russell Midcap Value ETF 22 | 'IWP', # iShares Russell Midcap Growth ETF 23 | 'IWN', # iShares Russell 2000 Value ETF 24 | 'IWO', # iShares Russell 2000 Growth ETF 25 | 'IVE', # iShares S&P 500 Value ETF 26 | 'IVW' # iShares S&P 500 Growth ETF 27 | ] 28 | 29 | self.mom = {} 30 | 31 | self.period = 12 * 21 32 | self.SetWarmUp(self.period) 33 | 34 | for ticker in self.tickers: 35 | security = self.AddEquity(ticker, Resolution.Daily) 36 | security.SetFeeModel(CustomFeeModel()) 37 | security.SetLeverage(10) 38 | 39 | self.mom[security.Symbol] = self.MOM(security.Symbol, self.period) 40 | 41 | self.recent_month = -1 42 | 43 | def OnData(self, data): 44 | if self.recent_month == self.Time.month: 45 | return 46 | self.recent_month = self.Time.month 47 | 48 | mom_ready = [ s for s in self.mom if self.mom[s].IsReady and s in data] 49 | if mom_ready: 50 | sorted_mom = sorted(mom_ready, key = lambda x: self.mom[x].Current.Value, reverse=True) 51 | 52 | for symbol in sorted_mom[1:-1]: 53 | if self.Portfolio[symbol].Invested: 54 | self.Liquidate(symbol) 55 | 56 | winner = sorted_mom[0] 57 | loser = sorted_mom[-1] 58 | 59 | if self.Securities[winner].Price != 0 and self.Securities[winner].IsTradable: 60 | if (self.Time.month == 10 and self.Time.year == 2020) and winner.Value == 'IVW': # prevent data error 61 | self.Liquidate(winner) 62 | else: 63 | self.SetHoldings(winner, 1) 64 | 65 | if self.Securities[loser].Price != 0 and self.Securities[loser].IsTradable: 66 | if (self.Time.month == 10 and self.Time.year == 2020) and loser.Value == 'IVW': # prevent data error 67 | self.Liquidate(loser) 68 | else: 69 | self.SetHoldings(loser, -1) 70 | 71 | # Custom fee model. 72 | class CustomFeeModel(FeeModel): 73 | def GetOrderFee(self, parameters): 74 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 75 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/momentum-factor-effect-in-stocks.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/momentum-factor-effect-in-stocks/ 2 | # 3 | # The investment universe consists of NYSE, AMEX, and NASDAQ stocks. We define momentum as the past 12-month return, skipping the most 4 | # recent month’s return (to avoid microstructure and liquidity biases). To capture “momentum”, UMD portfolio goes long stocks that have 5 | # high relative past one-year returns and short stocks that have low relative past one-year returns. 6 | # 7 | # QC implementation changes: 8 | # - Instead of all listed stock, we select top 500 stocks by market cap from QC stock universe. 9 | 10 | from AlgorithmImports import * 11 | 12 | class MomentumFactorEffectinStocks(QCAlgorithm): 13 | 14 | def Initialize(self): 15 | self.SetStartDate(2000, 1, 1) 16 | self.SetCash(100000) 17 | 18 | symbol = self.AddEquity('SPY', Resolution.Daily).Symbol 19 | 20 | self.weight = {} 21 | self.data = {} 22 | self.period = 12 * 21 23 | self.quantile = 5 24 | 25 | self.coarse_count = 500 26 | self.selection_flag = False 27 | self.UniverseSettings.Resolution = Resolution.Daily 28 | self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) 29 | self.Schedule.On(self.DateRules.MonthStart(symbol), self.TimeRules.AfterMarketOpen(symbol), self.Selection) 30 | 31 | def OnSecuritiesChanged(self, changes): 32 | for security in changes.AddedSecurities: 33 | security.SetFeeModel(CustomFeeModel()) 34 | security.SetLeverage(10) 35 | 36 | def CoarseSelectionFunction(self, coarse): 37 | # Update the rolling window every day. 38 | for stock in coarse: 39 | symbol = stock.Symbol 40 | 41 | # Store monthly price. 42 | if symbol in self.data: 43 | self.data[symbol].Add(stock.AdjustedPrice) 44 | 45 | if not self.selection_flag: 46 | return Universe.Unchanged 47 | 48 | # selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa' and x.Price > 5] 49 | selected = [x.Symbol 50 | for x in sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa'], 51 | key = lambda x: x.DollarVolume, reverse = True)[:self.coarse_count]] 52 | 53 | # Warmup price rolling windows. 54 | for symbol in selected: 55 | if symbol in self.data: 56 | continue 57 | 58 | self.data[symbol] = RollingWindow[float](self.period) 59 | history = self.History(symbol, self.period, Resolution.Daily) 60 | if history.empty: 61 | self.Log(f"Not enough data for {symbol} yet") 62 | continue 63 | closes = history.loc[symbol].close 64 | for time, close in closes.iteritems(): 65 | self.data[symbol].Add(close) 66 | 67 | return [x for x in selected if self.data[x].IsReady] 68 | 69 | def FineSelectionFunction(self, fine): 70 | fine = [x for x in fine if x.MarketCap != 0 and \ 71 | ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE"))] 72 | 73 | # if len(fine) > self.coarse_count: 74 | # sorted_by_market_cap = sorted(fine, key = lambda x:x.MarketCap, reverse=True) 75 | # top_by_market_cap = [x for x in sorted_by_market_cap[:self.coarse_count]] 76 | # else: 77 | # top_by_market_cap = fine 78 | 79 | perf = {x.Symbol : self.data[x.Symbol][0] / self.data[x.Symbol][self.period-1] - 1 for x in fine} 80 | 81 | if len(perf) >= self.quantile: 82 | sorted_by_perf = sorted(perf.items(), key = lambda x:x[1], reverse=True) 83 | quantile = int(len(sorted_by_perf) / self.quantile) 84 | long = [x[0] for x in sorted_by_perf[:quantile]] 85 | short = [x[0] for x in sorted_by_perf[-quantile:]] 86 | 87 | long_count = len(long) 88 | short_count = len(short) 89 | 90 | for symbol in long: 91 | self.weight[symbol] = 1 / long_count 92 | for symbol in short: 93 | self.weight[symbol] = -1 / short_count 94 | 95 | return list(self.weight.keys()) 96 | 97 | def OnData(self, data): 98 | if not self.selection_flag: 99 | return 100 | self.selection_flag = False 101 | 102 | # Trade execution. 103 | stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested] 104 | for symbol in stocks_invested: 105 | if symbol not in self.weight: 106 | self.Liquidate(symbol) 107 | 108 | for symbol, w in self.weight.items(): 109 | if symbol in data and data[symbol]: 110 | self.SetHoldings(symbol, w) 111 | 112 | self.weight.clear() 113 | 114 | def Selection(self): 115 | self.selection_flag = True 116 | 117 | # Custom fee model. 118 | class CustomFeeModel(FeeModel): 119 | def GetOrderFee(self, parameters): 120 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 121 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/momentum-in-mutual-fund-returns.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/momentum-in-mutual-fund-returns/ 2 | # 3 | # The investment universe consists of equity funds from the CRSP Mutual Fund database. 4 | # This universe is then shrunk to no-load funds (to remove entrance fees). 5 | # Investors then sort mutual funds based on their past 6-month return and divide them into deciles. 6 | # The top decile of mutual funds is then picked into an investment portfolio (equally weighted), and funds are held for three months. 7 | # Other measures of momentum could also be used in sorting (fund’s closeness to 1 year high in NAV and momentum factor loading), 8 | # and it is highly probable that the combined predictor would have even better results than only the simple 6-month momentum. 9 | # 10 | # QC Implementation: 11 | # - Universe consist of approximately 850 mutual funds. 12 | 13 | #region imports 14 | from AlgorithmImports import * 15 | #endregion 16 | 17 | class MomentuminMutualFundReturns(QCAlgorithm): 18 | 19 | def Initialize(self): 20 | # NOTE: most of the data start from 2014 and until 2015 there wasn't any trade 21 | self.SetStartDate(2014, 1, 1) 22 | self.SetCash(100000) 23 | 24 | self.data = {} 25 | self.symbols = [] 26 | 27 | self.period = 21 * 6 # Storing 6 months of daily prices 28 | self.quantile = 10 29 | 30 | self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol 31 | 32 | # Load csv file with etf symbols and split line with semi-colon 33 | etf_symbols_csv = self.Download("data.quantpedia.com/backtesting_data/equity/mutual_funds/symbols.csv") 34 | splitted_csv = etf_symbols_csv.split(';') 35 | 36 | for symbol in splitted_csv: 37 | self.symbols.append(symbol) 38 | 39 | # Subscribe for QuantpediaETF by etf symbol, then set fee model and leverage 40 | data = self.AddData(QuantpediaETF, symbol, Resolution.Daily) 41 | data.SetFeeModel(CustomFeeModel()) 42 | data.SetLeverage(5) 43 | 44 | self.data[symbol] = RollingWindow[float](self.period) 45 | 46 | self.recent_month = -1 47 | 48 | def OnData(self, data): 49 | # Update daily prices of etfs 50 | for symbol in self.symbols: 51 | if symbol in data and data[symbol]: 52 | price = data[symbol].Value 53 | self.data[symbol].Add(price) 54 | 55 | if self.recent_month == self.Time.month: 56 | return 57 | self.recent_month = self.Time.month 58 | 59 | # Rebalance quarterly 60 | if self.recent_month % 3 != 0: 61 | return 62 | 63 | performance = {} 64 | 65 | for symbol in self.symbols: 66 | # If data for etf are ready calculate it's 6 month performance 67 | if self.data[symbol].IsReady: 68 | if self.Securities[symbol].GetLastData() and (self.Time.date() - self.Securities[symbol].GetLastData().Time.date()).days <= 3: 69 | prices = [x for x in self.data[symbol]] 70 | performance[symbol] = (prices[0] - prices[-1]) / prices[-1] 71 | 72 | if len(performance) < self.quantile: 73 | self.Liquidate() 74 | return 75 | 76 | decile = int(len(performance) / self.quantile) 77 | # sort dictionary by performance and based on it create sorted list 78 | sorted_by_perf = [x[0] for x in sorted(performance.items(), key=lambda item: item[1], reverse=True)] 79 | # select top decile etfs for investment based on performance 80 | long = sorted_by_perf[:decile] 81 | 82 | # Trade execution 83 | invested_etfs = [x.Key for x in self.Portfolio if x.Value.Invested] 84 | for symbol in invested_etfs: 85 | if symbol not in long: 86 | self.Liquidate(symbol) 87 | 88 | long_length = len(long) 89 | 90 | for symbol in long: 91 | self.SetHoldings(symbol, 1 / long_length) 92 | 93 | # Quantpedia data 94 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 95 | class QuantpediaETF(PythonData): 96 | def GetSource(self, config, date, isLiveMode): 97 | return SubscriptionDataSource("data.quantpedia.com/backtesting_data/equity/mutual_funds/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv) 98 | 99 | def Reader(self, config, line, date, isLiveMode): 100 | data = QuantpediaETF() 101 | data.Symbol = config.Symbol 102 | 103 | if not line[0].isdigit(): return None 104 | split = line.split(';') 105 | 106 | data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1) 107 | data['settle'] = float(split[1]) 108 | data.Value = float(split[1]) 109 | 110 | return data 111 | 112 | # Custom fee model 113 | class CustomFeeModel(FeeModel): 114 | def GetOrderFee(self, parameters): 115 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 116 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/option-expiration-week-effect.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/option-expiration-week-effect/ 2 | # 3 | # Investors choose stocks from the S&P 100 index as his/her investment universe (stocks could be easily tracked via ETF or index fund). 4 | # He/she then goes long S&P 100 stocks during the option-expiration week and stays in cash during other days. 5 | 6 | from AlgorithmImports import * 7 | 8 | class OptionExpirationWeekEffect(QCAlgorithm): 9 | 10 | def Initialize(self): 11 | self.SetStartDate(2010, 1, 1) 12 | self.SetCash(10000) 13 | 14 | self.symbol = self.AddEquity("OEF", Resolution.Minute).Symbol 15 | 16 | option = self.AddOption("OEF") 17 | option.SetFilter(-3, 3, timedelta(0), timedelta(days = 60)) 18 | 19 | self.SetBenchmark("OEF") 20 | self.near_expiry = datetime.min 21 | 22 | self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Monday), self.TimeRules.AfterMarketOpen(self.symbol, 1), self.Rebalance) 23 | 24 | def OnData(self, slice): 25 | if self.Time.date() == self.near_expiry.date(): 26 | self.Liquidate() 27 | 28 | def Rebalance(self): 29 | calendar = self.TradingCalendar.GetDaysByType(TradingDayType.OptionExpiration, self.Time, self.EndDate) 30 | expiries = [i.Date for i in calendar] 31 | if len(expiries) == 0: return 32 | 33 | self.near_expiry = expiries[0] 34 | 35 | if (self.near_expiry - self.Time).days <= 5: 36 | self.SetHoldings(self.symbol, 1) -------------------------------------------------------------------------------- /static/strategies/paired-switching.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/paired-switching/ 2 | # 3 | # This strategy is very flexible. Investors could use stocks, funds, or ETFs as an investment vehicle. We show simple trading rules for a sample strategy 4 | # from the source research paper. The investor uses two Vanguard funds as his investment vehicles – one equity fund (VFINX) and one government bond 5 | from AlgorithmImports import * 6 | 7 | # fund (VUSTX). These two funds have a negative correlation as they are proxies for two negatively correlated asset classes. The investor looks at the 8 | # performance of the two funds over the prior quarter and buys the fund that has a higher return during the ranking period. The position is held for one 9 | # quarter (the investment period). At the end of the investment period, the cycle is repeated. 10 | 11 | 12 | class PairedSwitching(QCAlgorithm): 13 | def Initialize(self): 14 | self.SetStartDate(2004, 1, 1) 15 | self.SetCash(100000) 16 | 17 | self.first_symbol = self.AddEquity("SPY", Resolution.Daily).Symbol 18 | self.second_symbol = self.AddEquity("AGG", Resolution.Daily).Symbol 19 | self.recent_month = -1 20 | 21 | def OnData(self, data): 22 | if self.Time.month == self.recent_month: 23 | return 24 | self.recent_month = self.Time.month 25 | 26 | if self.recent_month % 3 == 0: 27 | if self.first_symbol in data and self.second_symbol in data: 28 | history_call = self.History( 29 | [self.first_symbol, self.second_symbol], timedelta(days=90) 30 | ) 31 | if not history_call.empty: 32 | first_bars = history_call.loc[self.first_symbol.Value] 33 | last_p1 = first_bars["close"].iloc[0] 34 | 35 | second_bars = history_call.loc[self.second_symbol.Value] 36 | last_p2 = second_bars["close"].iloc[0] 37 | 38 | # Calculates performance of funds over the prior quarter. 39 | first_performance = ( 40 | float(self.Securities[self.first_symbol].Price) - float(last_p1) 41 | ) / (float(self.Securities[self.first_symbol].Price)) 42 | second_performance = ( 43 | float(self.Securities[self.second_symbol].Price) 44 | - float(last_p2) 45 | ) / (float(self.Securities[self.second_symbol].Price)) 46 | 47 | # Buys the fund that has the higher return during the period. 48 | if first_performance > second_performance: 49 | if self.Securities[self.second_symbol].Invested: 50 | self.Liquidate(self.second_symbol) 51 | self.SetHoldings(self.first_symbol, 1) 52 | else: 53 | if self.Securities[self.first_symbol].Invested: 54 | self.Liquidate(self.first_symbol) 55 | self.SetHoldings(self.second_symbol, 1) 56 | -------------------------------------------------------------------------------- /static/strategies/payday-anomaly.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/payday-anomaly/ 2 | # 3 | # The investment universe consists of the S&P500 index. Simply, buy and hold the index during the 16th day in the month during each month of the year. 4 | 5 | from dateutil.relativedelta import relativedelta 6 | from AlgorithmImports import * 7 | 8 | class PayDayAnomaly(QCAlgorithm): 9 | 10 | def Initialize(self): 11 | self.SetStartDate(2000, 1, 1) 12 | self.SetCash(100000) 13 | 14 | self.symbol = self.AddEquity('SPY', Resolution.Minute).Symbol 15 | self.liquidate_next_day = False 16 | 17 | self.Schedule.On(self.DateRules.EveryDay(self.symbol), self.TimeRules.BeforeMarketClose(self.symbol, 1), self.Purchase) 18 | 19 | def Purchase(self): 20 | alg_time = self.Time 21 | paydate = self.PaydayDate(alg_time) 22 | 23 | if alg_time.date() == paydate: 24 | self.SetHoldings(self.symbol, 1) 25 | self.liquidate_next_day = True 26 | # self.algorithm.EmitInsights(Insight.Price(self.symbol, timedelta(days=1), InsightDirection.Up, None, None, None, self.weight)) 27 | 28 | if self.liquidate_next_day: 29 | self.liquidate_next_day = False 30 | return 31 | 32 | if self.Portfolio[self.symbol].IsLong: 33 | self.Liquidate(self.symbol) 34 | 35 | def PaydayDate(self, date_time): 36 | payday = date(date_time.year, date_time.month, 1) + relativedelta(day=15) 37 | 38 | if payday.weekday() == 5: # Is saturday. 39 | payday = payday - timedelta(days=1) 40 | elif payday.weekday() == 6: # Is sunday. 41 | payday = payday - timedelta(days=2) 42 | 43 | return payday -------------------------------------------------------------------------------- /static/strategies/rd-expenditures-and-stock-returns.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/rd-expenditures-and-stock-returns/ 2 | # 3 | # The investment universe consists of stocks that are listed on NYSE NASDAQ or AMEX. At the end of April, for each stock in the universe, calculate a measure of total R&D expenditures in the past 5 years scaled by the firm’s Market cap (defined on page 7, eq. 1). 4 | # Go long (short) on the quintile of firms with the highest (lowest) R&D expenditures relative to their Market Cap. Weight the portfolio equally and rebalance next year. The backtested performance of the paper is substituted by our more recent backtest in Quantconnect. 5 | 6 | # region imports 7 | from AlgorithmImports import * 8 | from numpy import log, average 9 | from scipy import stats 10 | import numpy as np 11 | 12 | # endregion 13 | 14 | 15 | class RDExpendituresandStockReturns(QCAlgorithm): 16 | def Initialize(self): 17 | self.SetStartDate(1998, 1, 1) 18 | self.SetCash(100000) 19 | 20 | self.weight = {} 21 | self.coarse_count = 3000 22 | 23 | # R&D history. 24 | self.RD = {} 25 | self.rd_period = 5 26 | 27 | self.long = [] 28 | self.short = [] 29 | 30 | data = self.AddEquity("XLK", Resolution.Daily) 31 | data.SetLeverage(10) 32 | self.technology_sector = data.Symbol 33 | 34 | self.symbol = self.AddEquity("SPY", Resolution.Daily).Symbol 35 | 36 | self.selection_flag = True 37 | self.UniverseSettings.Resolution = Resolution.Daily 38 | self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) 39 | self.Schedule.On( 40 | self.DateRules.MonthEnd(self.symbol), 41 | self.TimeRules.AfterMarketOpen(self.symbol), 42 | self.Selection, 43 | ) 44 | 45 | def OnSecuritiesChanged(self, changes): 46 | for security in changes.AddedSecurities: 47 | security.SetLeverage(10) 48 | security.SetFeeModel(CustomFeeModel()) 49 | 50 | def CoarseSelectionFunction(self, coarse): 51 | if not self.selection_flag: 52 | return Universe.Unchanged 53 | 54 | selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Price > 5] 55 | 56 | return selected 57 | 58 | def FineSelectionFunction(self, fine): 59 | fine = [ 60 | x 61 | for x in fine 62 | if ( 63 | x.FinancialStatements.IncomeStatement.ResearchAndDevelopment.TwelveMonths 64 | ) 65 | and (x.MarketCap != 0) 66 | and ( 67 | (x.SecurityReference.ExchangeId == "NYS") 68 | or (x.SecurityReference.ExchangeId == "NAS") 69 | or (x.SecurityReference.ExchangeId == "ASE") 70 | ) 71 | ] 72 | # and x.AssetClassification.MorningstarSectorCode == MorningstarSectorCode.Technology] 73 | 74 | top_by_market_cap = None 75 | if len(fine) > self.coarse_count: 76 | sorted_by_market_cap = sorted(fine, key=lambda x: x.MarketCap, reverse=True) 77 | top_by_market_cap = sorted_by_market_cap[: self.coarse_count] 78 | else: 79 | top_by_market_cap = fine 80 | 81 | fine_symbols = [x.Symbol for x in top_by_market_cap] 82 | ability = {} 83 | 84 | updated_flag = [] # updated this year already 85 | 86 | for stock in top_by_market_cap: 87 | symbol = stock.Symbol 88 | 89 | # prevent storing duplicated value for the same stock in one year 90 | if symbol not in updated_flag: 91 | 92 | # Update RD. 93 | if symbol not in self.RD: 94 | self.RD[symbol] = RollingWindow[float](self.rd_period) 95 | # rd = stock.FinancialStatements.IncomeStatement.ResearchAndDevelopment.TwelveMonths 96 | # self.RD[symbol].Add(rd) 97 | 98 | if self.RD[symbol].IsReady: 99 | coefs = np.array([1, 0.8, 0.6, 0.4, 0.2]) 100 | rds = np.array([x for x in self.RD[symbol]]) 101 | 102 | rdc = sum(coefs * rds) 103 | ability[stock] = rdc / stock.MarketCap 104 | 105 | rd = ( 106 | stock.FinancialStatements.IncomeStatement.ResearchAndDevelopment.TwelveMonths 107 | ) 108 | self.RD[symbol].Add(rd) 109 | 110 | # prevent storing duplicated value for the same stock in one year 111 | if fine_symbols.count(symbol) > 1: 112 | updated_flag.append(symbol) 113 | 114 | # Ability market cap weighting. 115 | # total_market_cap = sum([x.MarketCap for x in ability]) 116 | # for stock, rdc in ability.items(): 117 | # ability[stock] = rdc * (stock.MarketCap / total_market_cap) 118 | 119 | # Remove not updated symbols 120 | symbols_to_delete = [] 121 | for symbol in self.RD.keys(): 122 | if symbol not in fine_symbols: 123 | symbols_to_delete.append(symbol) 124 | for symbol in symbols_to_delete: 125 | if symbol in self.RD: 126 | del self.RD[symbol] 127 | 128 | # starts trading after data storing period 129 | if len(ability) != 0: 130 | # Ability sorting. 131 | sorted_by_ability = sorted( 132 | ability.items(), key=lambda x: x[1], reverse=True 133 | ) 134 | decile = int(len(sorted_by_ability) / 5) 135 | high_by_ability = [x[0].Symbol for x in sorted_by_ability[:decile]] 136 | low_by_ability = [x[0].Symbol for x in sorted_by_ability[-decile:]] 137 | 138 | self.long = high_by_ability 139 | self.short = low_by_ability 140 | # self.short = [self.technology_sector] 141 | 142 | return self.long + self.short 143 | 144 | def Selection(self): 145 | if self.Time.month == 4: 146 | self.selection_flag = True 147 | 148 | def OnData(self, data): 149 | if not self.selection_flag: 150 | return 151 | self.selection_flag = False 152 | 153 | # Trade execution. 154 | long_count = len(self.long) 155 | short_count = len(self.short) 156 | 157 | stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested] 158 | for symbol in stocks_invested: 159 | if symbol not in self.long + self.short: 160 | self.Liquidate(symbol) 161 | 162 | for symbol in self.long: 163 | if ( 164 | self.Securities[symbol].Price != 0 165 | and self.Securities[symbol].IsTradable 166 | ): 167 | self.SetHoldings(symbol, 1 / long_count) 168 | 169 | for symbol in self.short: 170 | if ( 171 | self.Securities[symbol].Price != 0 172 | and self.Securities[symbol].IsTradable 173 | ): 174 | self.SetHoldings(symbol, -1 / short_count) 175 | 176 | self.long.clear() 177 | self.short.clear() 178 | 179 | 180 | class SymbolData: 181 | def __init__(self, tested_growth, period): 182 | self.TestedGrowth = tested_growth 183 | self.RD = RollingWindow[float](period) 184 | 185 | def update(self, window_value): 186 | self.RD.Add(window_value) 187 | 188 | def is_ready(self): 189 | return self.RD.IsReady 190 | 191 | 192 | class CustomFeeModel(FeeModel): 193 | def GetOrderFee(self, parameters): 194 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 195 | return OrderFee(CashAmount(fee, "USD")) 196 | -------------------------------------------------------------------------------- /static/strategies/rebalancing-premium-in-cryptocurrencies.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/rebalancing-premium-in-cryptocurrencies/ 2 | # 3 | # The investment universe consists of 27 cryptocurrencies: BAT (Basic Attention Token), BTC (Bitcoin), BTG (Bitcoin Gold), 4 | # DAI (Dai), DATA (Data Coin), DGB (DigiByte), EOS (EIS.io), ETH (Ethereum), FUN (FUN Token), IOTA (Iota), LRC (Loopring token), 5 | # LTC (Litecoin), MANA (Mana coin), NEO (Neo), OMG (OMG, Formally known as OmiseGo), REQ (Request), SAN (Santiment Network Token), 6 | # SNT (Status), TRX (Tron), WAX (Wax), XLM (Stellar), XMR (Monero), XRP (Ripple), XVG (Verge), ZEC (Zcash), ZIL (Zilliqa) and ZRX (0x). 7 | # Two portfolios are created. The first portfolio is the daily rebalanced portfolio of all 27 cryptos to ensure that the assets have equal weights. 8 | # The second portfolio is not rebalanced at all: an investor buys the equally-weighted crypto portfolio and lets the weights drift. 9 | # Then the investor goes long the first portfolio and shorts the second portfolio with 70% weight. 10 | # 11 | # QC Implementation: 12 | # - BTGUSD is not traded due to data error. 13 | 14 | from AlgorithmImports import * 15 | 16 | class RebalancingPremiumInCryptocurrencies(QCAlgorithm): 17 | 18 | def Initialize(self): 19 | self.SetStartDate(2015, 1, 1) 20 | self.SetCash(100000000) 21 | 22 | self.cryptos = [ 23 | "BTCUSD", 24 | "BATUSD", 25 | # "BTGUSD", 26 | "DAIUSD", 27 | "DGBUSD", "EOSUSD", 28 | "ETHUSD", "FUNUSD", 29 | "LTCUSD", "NEOUSD", 30 | "OMGUSD", "SNTUSD", 31 | "TRXUSD", "XLMUSD", 32 | "XMRUSD", "XRPUSD", 33 | "XVGUSD", "ZECUSD", 34 | "ZRXUSD", "LRCUSD", 35 | "REQUSD", "SANUSD", 36 | "WAXUSD", "ZILUSD", 37 | "IOTAUSD", 38 | "MANAUSD", 39 | "DATAUSD" 40 | ] 41 | 42 | self.short_side_percentage = 0.7 43 | self.data = {} 44 | self.SetBrokerageModel(BrokerageName.Bitfinex) 45 | 46 | for crypto in self.cryptos: 47 | # GDAX is coinmarket, but it doesn't support this many cryptos, so we choose Bitfinex 48 | data = self.AddCrypto(crypto, Resolution.Minute, Market.Bitfinex) 49 | data.SetFeeModel(CustomFeeModel()) 50 | data.SetLeverage(10) 51 | 52 | self.data[crypto] = SymbolData() 53 | 54 | self.was_traded_already = False # wait for the price data to come only once 55 | self.prev_short_portfolio_equity = 0 # short leg equity tracking 56 | 57 | def OnData(self, data): 58 | if not (self.Time.hour == 9 and self.Time.minute == 30): 59 | return 60 | 61 | all_cryptos_are_ready = True # data warmup flag 62 | 63 | # check if all cryptos has ready data 64 | for crypto in self.cryptos: 65 | if crypto in data and data[crypto]: 66 | # update crypto price for weight calculation 67 | self.data[crypto].last_price = data[crypto].Value 68 | # if there is at least one crypto, which doesn't have data, then don't trade and break cycle 69 | else: 70 | all_cryptos_are_ready = False 71 | break 72 | 73 | if all_cryptos_are_ready or self.was_traded_already: 74 | self.was_traded_already = True 75 | 76 | # long strategy equity calculation 77 | long_portfolio_equity = self.Portfolio.TotalPortfolioValue 78 | long_equity_to_trade = long_portfolio_equity / len(self.cryptos) 79 | 80 | # short strategy equity calculation 81 | short_portfolio_equity = self.Portfolio.TotalPortfolioValue * self.short_side_percentage 82 | short_equity_to_trade = short_portfolio_equity / len(self.cryptos) 83 | 84 | # trading/rebalance 85 | for crypto, symbol_obj in self.data.items(): 86 | if crypto in data and data[crypto]: 87 | # short strategy 88 | if not self.Portfolio[crypto].Invested: 89 | short_q = np.floor(short_equity_to_trade / symbol_obj.last_price) 90 | if abs(short_q) >= self.Securities[crypto].SymbolProperties.MinimumOrderSize: 91 | self.MarketOrder(crypto, -short_q) 92 | 93 | # long strategy 94 | long_q = np.floor(long_equity_to_trade / symbol_obj.last_price) 95 | # currency was traded before 96 | if symbol_obj.quantity is not None: 97 | # calculate quantity difference 98 | diff_q = long_q - symbol_obj.quantity 99 | 100 | # rebalance position 101 | if abs(diff_q) >= self.Securities[crypto].SymbolProperties.MinimumOrderSize: 102 | self.MarketOrder(crypto, diff_q) 103 | 104 | # change new quantity 105 | symbol_obj.quantity += diff_q 106 | else: 107 | # rebalance position 108 | if abs(long_q) >= self.Securities[crypto].SymbolProperties.MinimumOrderSize: 109 | self.MarketOrder(crypto, long_q) 110 | 111 | # change new quantity 112 | symbol_obj.quantity = long_q 113 | 114 | class SymbolData(): 115 | def __init__(self): 116 | self.last_price = None 117 | self.quantity = None 118 | 119 | # Custom fee model. 120 | class CustomFeeModel(FeeModel): 121 | def GetOrderFee(self, parameters): 122 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 123 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/return-asymmetry-effect-in-commodity-futures.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/return-asymmetry-effect-in-commodity-futures/ 2 | # 3 | # The investment universe consists of 22 commodity futures, namely: 4 | # soybean oil, corn, cocoa, cotton, feeder cattle, gold, copper, heating oil, coffee, live cattle, lean hogs, 5 | # natural gas, oats, orange juice, palladium, platinum, soybean, sugar, silver, soybean meal, wheat, and crude oil. 6 | # Firstly, at the beginning of each month, construct the asymmetry measure (IE) for each commodity based on the latest 260 daily returns using the following formula 7 | # (the formula originally consists of theoretical density and integrals, however the solution is simple when empirical distribution is utilized): 8 | # IE = (number of trading days when the daily return is greater than the average plus two standard deviations) – 9 | # (number of trading days when the daily return is smaller than the average minus two standard deviations). 10 | # Then rank the commodities according to their IE. 11 | # Buy the bottom seven commodities with the lowest IE in the previous month and sell the top seven commodities with the highest IE in the previous month. 12 | # Weigh the portfolio equally and rebalance monthly. 13 | # 14 | # QC Implementation: 15 | # - Universe consists of Quantpedia comodity futures. 16 | # - Buying bottom 7 commodities and selling top 7 commodities according to IE. 17 | 18 | from AlgorithmImports import * 19 | 20 | class ReturnAsymmetryEffectInCommodityFutures(QCAlgorithm): 21 | 22 | def Initialize(self): 23 | self.SetStartDate(2000, 1, 1) 24 | self.SetCash(100000) 25 | 26 | self.tickers = [ 27 | "CME_S1", # Soybean Futures, Continuous Contract 28 | "CME_W1", # Wheat Futures, Continuous Contract 29 | "CME_SM1", # Soybean Meal Futures, Continuous Contract 30 | "CME_BO1", # Soybean Oil Futures, Continuous Contract 31 | "CME_C1", # Corn Futures, Continuous Contract 32 | "CME_O1", # Oats Futures, Continuous Contract 33 | "CME_LC1", # Live Cattle Futures, Continuous Contract 34 | "CME_FC1", # Feeder Cattle Futures, Continuous Contract 35 | "CME_LN1", # Lean Hog Futures, Continuous Contract 36 | "CME_GC1", # Gold Futures, Continuous Contract 37 | "CME_SI1", # Silver Futures, Continuous Contract 38 | "CME_PL1", # Platinum Futures, Continuous Contract 39 | "CME_CL1", # Crude Oil Futures, Continuous Contract 40 | "CME_HG1", # Copper Futures, Continuous Contract 41 | "CME_LB1", # Random Length Lumber Futures, Continuous Contract 42 | # "CME_NG1", # Natural Gas (Henry Hub) Physical Futures, Continuous Contract 43 | "CME_PA1", # Palladium Futures, Continuous Contract 44 | "CME_RR1", # Rough Rice Futures, Continuous Contract 45 | "CME_RB2", # Gasoline Futures, Continuous Contract 46 | "CME_KW2", # Wheat Kansas, Continuous Contract 47 | 48 | "ICE_CC1", # Cocoa Futures, Continuous Contract 49 | "ICE_CT1", # Cotton No. 2 Futures, Continuous Contract 50 | "ICE_KC1", # Coffee C Futures, Continuous Contract 51 | "ICE_O1", # Heating Oil Futures, Continuous Contract 52 | "ICE_OJ1", # Orange Juice Futures, Continuous Contract 53 | "ICE_SB1" # Sugar No. 11 Futures, Continuous Contract 54 | "ICE_RS1", # Canola Futures, Continuous Contract 55 | "ICE_GO1", # Gas Oil Futures, Continuous Contract 56 | "ICE_WT1", # WTI Crude Futures, Continuous Contract 57 | ] 58 | 59 | self.data = {} # storing objects of SymbolData class keyed by comodity symbols 60 | 61 | self.period = 261 # need 261 daily prices, to calculate 260 daily returns 62 | self.buy_count = 7 # buy n comodities on each rebalance 63 | self.sell_count = 7 # sell n comodities on each rebalance 64 | 65 | self.symbol = self.AddEquity("SPY", Resolution.Daily).Symbol 66 | 67 | # subscribe to futures contracts 68 | for ticker in self.tickers: 69 | security = self.AddData(QuantpediaFutures, ticker, Resolution.Daily) 70 | security.SetFeeModel(CustomFeeModel()) 71 | security.SetLeverage(5) 72 | 73 | self.data[security.Symbol] = SymbolData(self.period) 74 | 75 | self.rebalance_flag = False 76 | self.Schedule.On(self.DateRules.MonthStart(self.symbol), self.TimeRules.BeforeMarketClose(self.symbol, 0), self.Rebalance) 77 | 78 | def OnData(self, data): 79 | # update daily closes 80 | for symbol in self.data: 81 | if symbol in data and data[symbol]: 82 | close = data[symbol].Value 83 | self.data[symbol].update_closes(close) 84 | 85 | # rebalance monthly 86 | if not self.rebalance_flag: 87 | return 88 | self.rebalance_flag = False 89 | 90 | IE = {} 91 | 92 | for symbol, symbol_obj in self.data.items(): 93 | # check if comodity has ready prices 94 | if not symbol_obj.is_ready(): 95 | continue 96 | 97 | # calculate IE 98 | IE_value = symbol_obj.calculate_IE() 99 | 100 | # store IE value under comodity symbol 101 | IE[symbol] = IE_value 102 | 103 | # make sure, there are enough comodities for rebalance 104 | if len(IE) < (self.buy_count + self.sell_count): 105 | return 106 | 107 | # sort commodities based on IE values 108 | sorted_by_IE = [x[0] for x in sorted(IE.items(), key=lambda item: item[1])] 109 | 110 | # select long and short parts 111 | long = sorted_by_IE[:self.buy_count] 112 | short = sorted_by_IE[-self.sell_count:] 113 | 114 | # trade execution 115 | invested = [x.Key for x in self.Portfolio if x.Value.Invested] 116 | for symbol in invested: 117 | if symbol not in long + short: 118 | self.Liquidate(symbol) 119 | 120 | for symbol in long: 121 | self.SetHoldings(symbol, 1 / self.buy_count) 122 | 123 | for symbol in short: 124 | self.SetHoldings(symbol, -1 / self.sell_count) 125 | 126 | def Rebalance(self): 127 | self.rebalance_flag = True 128 | 129 | class SymbolData(): 130 | def __init__(self, period): 131 | self.closes = RollingWindow[float](period) 132 | 133 | def update_closes(self, close): 134 | self.closes.Add(close) 135 | 136 | def is_ready(self): 137 | return self.closes.IsReady 138 | 139 | def calculate_IE(self): 140 | closes = np.array([x for x in self.closes]) 141 | daily_returns = (closes[:-1] - closes[1:]) / closes[1:] 142 | 143 | average_daily_returns = np.average(daily_returns) 144 | two_daily_returns_std = 2 * np.std(daily_returns) 145 | 146 | avg_plus_two_std = average_daily_returns + two_daily_returns_std 147 | avg_minus_two_std = average_daily_returns - two_daily_returns_std 148 | 149 | over_avg_plus_two_std = 0 # counting number of daily returns, which were over avg_plus_two_std 150 | under_avg_minus_two_std = 0 # counting number of daily returns, which were under avg_minus_two_std 151 | 152 | for daily_return in daily_returns: 153 | if daily_return > avg_plus_two_std: 154 | over_avg_plus_two_std += 1 155 | elif daily_return < avg_minus_two_std: 156 | under_avg_minus_two_std += 1 157 | 158 | IE_value = over_avg_plus_two_std - under_avg_minus_two_std 159 | 160 | return IE_value 161 | 162 | # Quantpedia data. 163 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 164 | class QuantpediaFutures(PythonData): 165 | def GetSource(self, config, date, isLiveMode): 166 | return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv) 167 | 168 | def Reader(self, config, line, date, isLiveMode): 169 | data = QuantpediaFutures() 170 | data.Symbol = config.Symbol 171 | 172 | if not line[0].isdigit(): return None 173 | split = line.split(';') 174 | 175 | data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1) 176 | data['back_adjusted'] = float(split[1]) 177 | data['spliced'] = float(split[2]) 178 | data.Value = float(split[1]) 179 | 180 | return data 181 | 182 | # Custom fee model. 183 | class CustomFeeModel(FeeModel): 184 | def GetOrderFee(self, parameters): 185 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 186 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/roa-effect-within-stocks.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/roa-effect-within-stocks/ 2 | # 3 | # The investment universe contains all stocks on NYSE and AMEX and Nasdaq with Sales greater than 10 million USD. Stocks are then sorted into 4 | # two halves based on market capitalization. Each half is then divided into deciles based on Return on assets (ROA) calculated as quarterly 5 | # earnings (Compustat quarterly item IBQ – income before extraordinary items) divided by one-quarter-lagged assets (item ATQ – total assets). 6 | # The investor then goes long the top three deciles from each market capitalization group and goes short bottom three deciles. The strategy is 7 | # rebalanced monthly, and stocks are equally weighted. 8 | # 9 | # QC implementation changes: 10 | # - Instead of all listed stock, we select 500 most liquid stocks traded on NYSE, AMEX, or NASDAQ. 11 | 12 | from AlgorithmImports import * 13 | 14 | 15 | class ROAEffectWithinStocks(QCAlgorithm): 16 | def Initialize(self): 17 | self.SetStartDate(2000, 1, 1) 18 | self.SetCash(100000) 19 | 20 | self.symbol = self.AddEquity("SPY", Resolution.Daily).Symbol 21 | 22 | self.course_count = 500 23 | 24 | self.long = [] 25 | self.short = [] 26 | 27 | self.selection_flag = False 28 | self.UniverseSettings.Resolution = Resolution.Daily 29 | self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) 30 | self.Schedule.On( 31 | self.DateRules.MonthEnd(self.symbol), 32 | self.TimeRules.AfterMarketOpen(self.symbol), 33 | self.Selection, 34 | ) 35 | 36 | def OnSecuritiesChanged(self, changes): 37 | for security in changes.AddedSecurities: 38 | security.SetFeeModel(CustomFeeModel()) 39 | security.SetLeverage(5) 40 | 41 | def CoarseSelectionFunction(self, coarse): 42 | if not self.selection_flag: 43 | return Universe.Unchanged 44 | 45 | selected = sorted( 46 | [ 47 | x 48 | for x in coarse 49 | if x.HasFundamentalData and x.Market == "usa" and x.Price > 5 50 | ], 51 | key=lambda x: x.DollarVolume, 52 | reverse=True, 53 | ) 54 | 55 | return [x.Symbol for x in selected[: self.course_count]] 56 | 57 | def FineSelectionFunction(self, fine): 58 | fine = [ 59 | x 60 | for x in fine 61 | if x.MarketCap != 0 62 | and x.ValuationRatios.SalesPerShare 63 | * x.EarningReports.DilutedAverageShares.Value 64 | > 10000000 65 | and x.OperationRatios.ROA.ThreeMonths != 0 66 | and ( 67 | (x.SecurityReference.ExchangeId == "NYS") 68 | or (x.SecurityReference.ExchangeId == "NAS") 69 | or (x.SecurityReference.ExchangeId == "ASE") 70 | ) 71 | ] 72 | 73 | # Sorting by market cap. 74 | sorted_by_market_cap = sorted(fine, key=lambda x: x.MarketCap, reverse=True) 75 | half = int(len(sorted_by_market_cap) / 2) 76 | top_mc = [x for x in sorted_by_market_cap[:half]] 77 | bottom_mc = [x for x in sorted_by_market_cap[half:]] 78 | 79 | if len(top_mc) >= 10 and len(bottom_mc) >= 10: 80 | # Sorting by ROA. 81 | sorted_top_by_roa = sorted( 82 | top_mc, key=lambda x: (x.OperationRatios.ROA.Value), reverse=True 83 | ) 84 | decile = int(len(sorted_top_by_roa) / 10) 85 | long_top = [x.Symbol for x in sorted_top_by_roa[: decile * 3]] 86 | short_top = [x.Symbol for x in sorted_top_by_roa[-(decile * 3) :]] 87 | 88 | sorted_bottom_by_roa = sorted( 89 | bottom_mc, key=lambda x: (x.OperationRatios.ROA.Value), reverse=True 90 | ) 91 | decile = int(len(sorted_bottom_by_roa) / 10) 92 | long_bottom = [x.Symbol for x in sorted_bottom_by_roa[: decile * 3]] 93 | short_bottom = [x.Symbol for x in sorted_bottom_by_roa[-(decile * 3) :]] 94 | 95 | self.long = long_top + long_bottom 96 | self.short = short_top + short_bottom 97 | 98 | return self.long + self.short 99 | 100 | def OnData(self, data): 101 | if not self.selection_flag: 102 | return 103 | self.selection_flag = False 104 | 105 | # Trade execution. 106 | stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested] 107 | for symbol in stocks_invested: 108 | if symbol not in self.long + self.short: 109 | self.Liquidate(symbol) 110 | 111 | long_count = len(self.long) 112 | short_count = len(self.short) 113 | 114 | for symbol in self.long: 115 | self.SetHoldings(symbol, 1 / long_count) 116 | for symbol in self.short: 117 | self.SetHoldings(symbol, -1 / short_count) 118 | 119 | self.long.clear() 120 | self.short.clear() 121 | 122 | def Selection(self): 123 | self.selection_flag = True 124 | 125 | 126 | # Custom fee model. 127 | class CustomFeeModel(FeeModel): 128 | def GetOrderFee(self, parameters): 129 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 130 | return OrderFee(CashAmount(fee, "USD")) 131 | -------------------------------------------------------------------------------- /static/strategies/sector-momentum-rotational-system.py: -------------------------------------------------------------------------------- 1 | # region imports 2 | from AlgorithmImports import * 3 | 4 | # endregion 5 | # https://quantpedia.com/strategies/sector-momentum-rotational-system/ 6 | # 7 | # Use ten sector ETFs. Pick 3 ETFs with the strongest 12-month momentum into your portfolio and weight them equally. Hold them for one month and then rebalance. 8 | 9 | 10 | class SectorMomentumAlgorithm(QCAlgorithm): 11 | def Initialize(self): 12 | self.SetStartDate(2000, 1, 1) 13 | self.SetCash(100000) 14 | 15 | # Daily ROC data. 16 | self.data = {} 17 | 18 | self.period = 12 * 21 19 | self.SetWarmUp(self.period) 20 | 21 | self.symbols = [ 22 | "VNQ", # Vanguard Real Estate Index Fund 23 | "XLK", # Technology Select Sector SPDR Fund 24 | "XLE", # Energy Select Sector SPDR Fund 25 | "XLV", # Health Care Select Sector SPDR Fund 26 | "XLF", # Financial Select Sector SPDR Fund 27 | "XLI", # Industrials Select Sector SPDR Fund 28 | "XLB", # Materials Select Sector SPDR Fund 29 | "XLY", # Consumer Discretionary Select Sector SPDR Fund 30 | "XLP", # Consumer Staples Select Sector SPDR Fund 31 | "XLU", # Utilities Select Sector SPDR Fund 32 | ] 33 | 34 | for symbol in self.symbols: 35 | data = self.AddEquity(symbol, Resolution.Daily) 36 | data.SetFeeModel(CustomFeeModel()) 37 | data.SetLeverage(5) 38 | 39 | self.data[symbol] = self.ROC(symbol, self.period, Resolution.Daily) 40 | 41 | self.data[self.symbols[0]].Updated += self.OnROCUpdated 42 | self.recent_month = -1 43 | self.rebalance_flag = False 44 | 45 | def OnROCUpdated(self, sender, updated): 46 | # set rebalance flag 47 | if self.recent_month != self.Time.month: 48 | self.recent_month = self.Time.month 49 | self.rebalance_flag = True 50 | 51 | def OnData(self, data): 52 | if self.IsWarmingUp: 53 | return 54 | 55 | # rebalance once a month 56 | if self.rebalance_flag: 57 | self.rebalance_flag = False 58 | 59 | sorted_by_momentum = sorted( 60 | [ 61 | x 62 | for x in self.data.items() 63 | if x[1].IsReady and x[0] in data and data[x[0]] 64 | ], 65 | key=lambda x: x[1].Current.Value, 66 | reverse=True, 67 | ) 68 | long = [x[0] for x in sorted_by_momentum[:3]] 69 | 70 | # Trade execution. 71 | invested = [x.Key for x in self.Portfolio if x.Value.Invested] 72 | for symbol in invested: 73 | if symbol not in long: 74 | self.Liquidate(symbol) 75 | 76 | for symbol in long: 77 | self.SetHoldings(symbol, 1 / len(long)) 78 | 79 | 80 | # Custom fee model 81 | class CustomFeeModel(FeeModel): 82 | def GetOrderFee(self, parameters): 83 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 84 | return OrderFee(CashAmount(fee, "USD")) 85 | -------------------------------------------------------------------------------- /static/strategies/short-interest-effect-long-short-version.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/short-interest-effect-long-short-version/ 2 | # 3 | # All stocks from NYSE, AMEX, and NASDAQ are part of the investment universe. Stocks are then sorted each month into short-interest deciles based on 4 | # the ratio of short interest to shares outstanding. The investor then goes long on the decile with the lowest short ratio and short on the decile 5 | # with the highest short ratio. The portfolio is rebalanced monthly, and stocks in the portfolio are weighted equally. 6 | 7 | from AlgorithmImports import * 8 | 9 | 10 | class ShortInterestEffect(QCAlgorithm): 11 | def Initialize(self): 12 | self.SetStartDate(2010, 1, 1) 13 | self.SetCash(100000) 14 | 15 | # NOTE: We use only s&p 100 stocks so it's possible to fetch short interest data from quandl. 16 | self.symbols = [ 17 | "AAPL", 18 | "MSFT", 19 | "AMZN", 20 | "FB", 21 | "GOOGL", 22 | "GOOG", 23 | "JPM", 24 | "JNJ", 25 | "V", 26 | "PG", 27 | "XOM", 28 | "UNH", 29 | "BAC", 30 | "MA", 31 | "T", 32 | "DIS", 33 | "INTC", 34 | "HD", 35 | "VZ", 36 | "MRK", 37 | "PFE", 38 | "CVX", 39 | "KO", 40 | "CMCSA", 41 | "CSCO", 42 | "PEP", 43 | "WFC", 44 | "C", 45 | "BA", 46 | "ADBE", 47 | "WMT", 48 | "CRM", 49 | "MCD", 50 | "MDT", 51 | "BMY", 52 | "ABT", 53 | "NVDA", 54 | "NFLX", 55 | "AMGN", 56 | "PM", 57 | "PYPL", 58 | "TMO", 59 | "COST", 60 | "ABBV", 61 | "ACN", 62 | "HON", 63 | "NKE", 64 | "UNP", 65 | "UTX", 66 | "NEE", 67 | "IBM", 68 | "TXN", 69 | "AVGO", 70 | "LLY", 71 | "ORCL", 72 | "LIN", 73 | "SBUX", 74 | "AMT", 75 | "LMT", 76 | "GE", 77 | "MMM", 78 | "DHR", 79 | "QCOM", 80 | "CVS", 81 | "MO", 82 | "LOW", 83 | "FIS", 84 | "AXP", 85 | "BKNG", 86 | "UPS", 87 | "GILD", 88 | "CHTR", 89 | "CAT", 90 | "MDLZ", 91 | "GS", 92 | "USB", 93 | "CI", 94 | "ANTM", 95 | "BDX", 96 | "TJX", 97 | "ADP", 98 | "TFC", 99 | "CME", 100 | "SPGI", 101 | "COP", 102 | "INTU", 103 | "ISRG", 104 | "CB", 105 | "SO", 106 | "D", 107 | "FISV", 108 | "PNC", 109 | "DUK", 110 | "SYK", 111 | "ZTS", 112 | "MS", 113 | "RTN", 114 | "AGN", 115 | "BLK", 116 | ] 117 | 118 | for symbol in self.symbols: 119 | data = self.AddEquity(symbol, Resolution.Daily) 120 | data.SetFeeModel(CustomFeeModel()) 121 | data.SetLeverage(5) 122 | 123 | self.AddData( 124 | QuandlFINRA_ShortVolume, "FINRA/FNSQ_" + symbol, Resolution.Daily 125 | ) 126 | 127 | self.recent_month = -1 128 | 129 | def OnData(self, data): 130 | if self.recent_month == self.Time.month: 131 | return 132 | self.recent_month = self.Time.month 133 | 134 | short_interest = {} 135 | for symbol in self.symbols: 136 | sym = "FINRA/FNSQ_" + symbol 137 | if sym in data and data[sym] and symbol in data and data[symbol]: 138 | short_vol = data[sym].GetProperty("SHORTVOLUME") 139 | total_vol = data[sym].GetProperty("TOTALVOLUME") 140 | 141 | short_interest[symbol] = short_vol / total_vol 142 | 143 | long = [] 144 | short = [] 145 | if len(short_interest) >= 10: 146 | sorted_by_short_interest = sorted( 147 | short_interest.items(), key=lambda x: x[1], reverse=True 148 | ) 149 | decile = int(len(sorted_by_short_interest) / 10) 150 | long = [x[0] for x in sorted_by_short_interest[-decile:]] 151 | short = [x[0] for x in sorted_by_short_interest[:decile]] 152 | 153 | # trade execution 154 | stocks_invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested] 155 | for symbol in stocks_invested: 156 | if symbol not in long + short: 157 | self.Liquidate(symbol) 158 | 159 | for symbol in long: 160 | if symbol in data and data[symbol]: 161 | self.SetHoldings(symbol, 1 / len(long)) 162 | for symbol in short: 163 | if symbol in data and data[symbol]: 164 | self.SetHoldings(symbol, -1 / len(short)) 165 | 166 | 167 | class QuandlFINRA_ShortVolume(PythonQuandl): 168 | def __init__(self): 169 | self.ValueColumnName = "SHORTVOLUME" # also 'TOTALVOLUME' is accesible 170 | 171 | 172 | # Custom fee model. 173 | class CustomFeeModel(FeeModel): 174 | def GetOrderFee(self, parameters): 175 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 176 | return OrderFee(CashAmount(fee, "USD")) 177 | -------------------------------------------------------------------------------- /static/strategies/short-term-reversal-in-stocks.py: -------------------------------------------------------------------------------- 1 | # region imports 2 | from AlgorithmImports import * 3 | 4 | # endregion 5 | # https://quantpedia.com/strategies/short-term-reversal-in-stocks/ 6 | # 7 | # The investment universe consists of the 100 biggest companies by market capitalization. 8 | # The investor goes long on the ten stocks with the lowest performance in the previous week and 9 | # goes short on the ten stocks with the greatest performance of the prior month. The portfolio is rebalanced weekly. 10 | # 11 | # QC implementation changes: 12 | # - Instead of all listed stocks, we first select 500 most liquid stock from QC as a first filter due to time complexity issues tied to whole universe filtering. 13 | # - Then top 100 market cap stocks are used in momentum sorting. 14 | 15 | 16 | class ShortTermReversalEffectinStocks(QCAlgorithm): 17 | def Initialize(self): 18 | self.SetStartDate(2000, 1, 1) 19 | self.SetCash(100000) 20 | 21 | self.symbol = self.AddEquity("SPY", Resolution.Daily).Symbol 22 | 23 | self.coarse_count = 500 24 | self.stock_selection = 10 25 | self.top_by_market_cap_count = 100 26 | 27 | self.period = 21 28 | 29 | self.long = [] 30 | self.short = [] 31 | 32 | # Daily close data 33 | self.data = {} 34 | 35 | self.day = 1 36 | self.selection_flag = False 37 | self.UniverseSettings.Resolution = Resolution.Daily 38 | self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) 39 | self.Schedule.On( 40 | self.DateRules.EveryDay(self.symbol), 41 | self.TimeRules.AfterMarketOpen(self.symbol), 42 | self.Selection, 43 | ) 44 | 45 | def OnSecuritiesChanged(self, changes): 46 | for security in changes.AddedSecurities: 47 | security.SetFeeModel(CustomFeeModel()) 48 | security.SetLeverage(5) 49 | 50 | def CoarseSelectionFunction(self, coarse): 51 | # Update the rolling window every day. 52 | for stock in coarse: 53 | symbol = stock.Symbol 54 | 55 | # Store monthly price. 56 | if symbol in self.data: 57 | self.data[symbol].update(stock.AdjustedPrice) 58 | 59 | if not self.selection_flag: 60 | return Universe.Unchanged 61 | 62 | selected = sorted( 63 | [ 64 | x 65 | for x in coarse 66 | if x.HasFundamentalData and x.Market == "usa" and x.Price > 1 67 | ], 68 | key=lambda x: x.DollarVolume, 69 | reverse=True, 70 | ) 71 | selected = [x.Symbol for x in selected][: self.coarse_count] 72 | 73 | # Warmup price rolling windows. 74 | for symbol in selected: 75 | if symbol in self.data: 76 | continue 77 | 78 | self.data[symbol] = SymbolData(self.period) 79 | history = self.History(symbol, self.period, Resolution.Daily) 80 | if history.empty: 81 | self.Log(f"Not enough data for {symbol} yet") 82 | continue 83 | closes = history.loc[symbol].close 84 | for time, close in closes.iteritems(): 85 | self.data[symbol].update(close) 86 | 87 | return [x for x in selected if self.data[x].is_ready()] 88 | 89 | def FineSelectionFunction(self, fine): 90 | fine = [x for x in fine if x.MarketCap != 0] 91 | 92 | sorted_by_market_cap = sorted(fine, key=lambda x: x.MarketCap, reverse=True) 93 | top_by_market_cap = [ 94 | x.Symbol for x in sorted_by_market_cap[: self.top_by_market_cap_count] 95 | ] 96 | 97 | month_performances = { 98 | symbol: self.data[symbol].monthly_return() for symbol in top_by_market_cap 99 | } 100 | week_performances = { 101 | symbol: self.data[symbol].weekly_return() for symbol in top_by_market_cap 102 | } 103 | 104 | sorted_by_month_perf = [ 105 | x[0] 106 | for x in sorted( 107 | month_performances.items(), key=lambda item: item[1], reverse=True 108 | ) 109 | ] 110 | sorted_by_week_perf = [ 111 | x[0] for x in sorted(week_performances.items(), key=lambda item: item[1]) 112 | ] 113 | 114 | self.long = sorted_by_week_perf[: self.stock_selection] 115 | 116 | for symbol in sorted_by_month_perf: # Month performances are sorted descending 117 | if symbol not in self.long: 118 | self.short.append(symbol) 119 | 120 | if len(self.short) == 10: 121 | break 122 | 123 | return self.long + self.short 124 | 125 | def OnData(self, data): 126 | if not self.selection_flag: 127 | return 128 | self.selection_flag = False 129 | 130 | invested = [x.Key for x in self.Portfolio if x.Value.Invested] 131 | for symbol in invested: 132 | if symbol not in self.long + self.short: 133 | self.Liquidate(symbol) 134 | 135 | # Leveraged portfolio - 100% long, 100% short. 136 | for symbol in self.long: 137 | if ( 138 | self.Securities[symbol].Price != 0 139 | and self.Securities[symbol].IsTradable 140 | ): 141 | self.SetHoldings(symbol, 1 / len(self.long)) 142 | 143 | for symbol in self.short: 144 | if ( 145 | self.Securities[symbol].Price != 0 146 | and self.Securities[symbol].IsTradable 147 | ): 148 | self.SetHoldings(symbol, -1 / len(self.short)) 149 | 150 | self.long.clear() 151 | self.short.clear() 152 | 153 | def Selection(self): 154 | if self.day == 5: 155 | self.selection_flag = True 156 | 157 | self.day += 1 158 | if self.day > 5: 159 | self.day = 1 160 | 161 | 162 | class SymbolData: 163 | def __init__(self, period): 164 | self.closes = RollingWindow[float](period) 165 | self.period = period 166 | 167 | def update(self, close): 168 | self.closes.Add(close) 169 | 170 | def is_ready(self) -> bool: 171 | return self.closes.IsReady 172 | 173 | def weekly_return(self) -> float: 174 | return self.closes[0] / self.closes[5] - 1 175 | 176 | def monthly_return(self) -> float: 177 | return self.closes[0] / self.closes[self.period - 1] - 1 178 | 179 | 180 | # Custom fee model 181 | class CustomFeeModel(FeeModel): 182 | def GetOrderFee(self, parameters): 183 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 184 | return OrderFee(CashAmount(fee, "USD")) 185 | -------------------------------------------------------------------------------- /static/strategies/skewness-effect-in-commodities.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/skewness-effect-in-commodities/ 2 | # 3 | # The investment universe consists of 27 futures contracts on commodities. Each month, investor calculates skewness (3rd moment of returns) 4 | # from daily returns from data going 12 months into the past for all futures. Commodities are then sorted into quintiles and investor goes 5 | # long quintile containing the commodities with the 20% lowest total skewness and short quintile containing the commodities with the 20% highest 6 | # total skewness (over a ranking period of 12 months). The resultant portfolio is equally weighted and rebalanced each month. 7 | 8 | import numpy as np 9 | from AlgorithmImports import * 10 | from scipy.stats import skew 11 | 12 | 13 | class SkewnessEffect(QCAlgorithm): 14 | def Initialize(self): 15 | self.SetStartDate(2000, 1, 1) 16 | self.SetCash(100000) 17 | 18 | self.symbols = [ 19 | "CME_S1", # Soybean Futures, Continuous Contract 20 | "CME_W1", # Wheat Futures, Continuous Contract 21 | "CME_SM1", # Soybean Meal Futures, Continuous Contract 22 | "CME_BO1", # Soybean Oil Futures, Continuous Contract 23 | "CME_C1", # Corn Futures, Continuous Contract 24 | "CME_O1", # Oats Futures, Continuous Contract 25 | "CME_LC1", # Live Cattle Futures, Continuous Contract 26 | "CME_FC1", # Feeder Cattle Futures, Continuous Contract 27 | "CME_LN1", # Lean Hog Futures, Continuous Contract 28 | "CME_GC1", # Gold Futures, Continuous Contract 29 | "CME_SI1", # Silver Futures, Continuous Contract 30 | "CME_PL1", # Platinum Futures, Continuous Contract 31 | "CME_CL1", # Crude Oil Futures, Continuous Contract 32 | "CME_HG1", # Copper Futures, Continuous Contract 33 | "CME_LB1", # Random Length Lumber Futures, Continuous Contract 34 | # "CME_NG1", # Natural Gas (Henry Hub) Physical Futures, Continuous Contract 35 | "CME_PA1", # Palladium Futures, Continuous Contract 36 | "CME_RR1", # Rough Rice Futures, Continuous Contract 37 | "CME_DA1", # Class III Milk Futures 38 | "ICE_RS1", # Canola Futures, Continuous Contract 39 | "ICE_GO1", # Gas Oil Futures, Continuous Contract 40 | "CME_RB2", # Gasoline Futures, Continuous Contract 41 | "CME_KW2", # Wheat Kansas, Continuous Contract 42 | "ICE_WT1", # WTI Crude Futures, Continuous Contract 43 | "ICE_CC1", # Cocoa Futures, Continuous Contract 44 | "ICE_CT1", # Cotton No. 2 Futures, Continuous Contract 45 | "ICE_KC1", # Coffee C Futures, Continuous Contract 46 | "ICE_O1", # Heating Oil Futures, Continuous Contract 47 | "ICE_OJ1", # Orange Juice Futures, Continuous Contract 48 | "ICE_SB1", # Sugar No. 11 Futures, Continuous Contract 49 | ] 50 | 51 | self.period = 12 * 21 52 | self.quantile = 5 53 | self.SetWarmup(self.period) 54 | self.data = {} 55 | 56 | for symbol in self.symbols: 57 | data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily) 58 | data.SetFeeModel(CustomFeeModel()) 59 | data.SetLeverage(5) 60 | 61 | self.data[symbol] = RollingWindow[float](self.period) 62 | 63 | self.Schedule.On( 64 | self.DateRules.MonthStart(self.symbols[0]), 65 | self.TimeRules.At(0, 0), 66 | self.Rebalance, 67 | ) 68 | 69 | def OnData(self, data): 70 | for symbol in self.symbols: 71 | symbol_obj = self.Symbol(symbol) 72 | if symbol_obj in data.Keys: 73 | price = data[symbol_obj].Value 74 | if price != 0: 75 | self.data[symbol].Add(price) 76 | 77 | def Rebalance(self): 78 | if self.IsWarmingUp: 79 | return 80 | 81 | # Skewness calculation 82 | skewness_data = {} 83 | for symbol in self.symbols: 84 | if self.data[symbol].IsReady: 85 | if ( 86 | self.Securities[symbol].GetLastData() 87 | and ( 88 | self.Time.date() 89 | - self.Securities[symbol].GetLastData().Time.date() 90 | ).days 91 | < 5 92 | ): 93 | prices = np.array([x for x in self.data[symbol]]) 94 | returns = (prices[:-1] / prices[1:]) - 1 95 | if len(returns) == self.period - 1: 96 | # NOTE: Manual skewness calculation example 97 | # avg = np.average(returns) 98 | # std = np.std(returns) 99 | # skewness = (sum(np.power((x - avg), 3) for x in returns)) / ((self.return_history[symbol].maxlen-1) * np.power(std, 3)) 100 | skewness_data[symbol] = skew(returns) 101 | 102 | long = [] 103 | short = [] 104 | if len(skewness_data) >= self.quantile: 105 | # Skewness sorting 106 | sorted_by_skewness = sorted( 107 | skewness_data.items(), key=lambda x: x[1], reverse=True 108 | ) 109 | quintile = int(len(sorted_by_skewness) / self.quantile) 110 | long = [x[0] for x in sorted_by_skewness[-quintile:]] 111 | short = [x[0] for x in sorted_by_skewness[:quintile]] 112 | 113 | # Trade execution 114 | invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested] 115 | for symbol in invested: 116 | if symbol not in long + short: 117 | self.Liquidate(symbol) 118 | 119 | for symbol in long: 120 | self.SetHoldings(symbol, 1 / len(long)) 121 | for symbol in short: 122 | self.SetHoldings(symbol, -1 / len(short)) 123 | 124 | 125 | # Quantpedia data. 126 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 127 | class QuantpediaFutures(PythonData): 128 | def GetSource(self, config, date, isLiveMode): 129 | return SubscriptionDataSource( 130 | "data.quantpedia.com/backtesting_data/futures/{0}.csv".format( 131 | config.Symbol.Value 132 | ), 133 | SubscriptionTransportMedium.RemoteFile, 134 | FileFormat.Csv, 135 | ) 136 | 137 | def Reader(self, config, line, date, isLiveMode): 138 | data = QuantpediaFutures() 139 | data.Symbol = config.Symbol 140 | 141 | if not line[0].isdigit(): 142 | return None 143 | split = line.split(";") 144 | 145 | data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1) 146 | data["back_adjusted"] = float(split[1]) 147 | data["spliced"] = float(split[2]) 148 | data.Value = float(split[1]) 149 | 150 | return data 151 | 152 | 153 | # Custom fee model. 154 | class CustomFeeModel(FeeModel): 155 | def GetOrderFee(self, parameters): 156 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 157 | return OrderFee(CashAmount(fee, "USD")) 158 | -------------------------------------------------------------------------------- /static/strategies/small-capitalization-stocks-premium-anomaly.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/small-capitalization-stocks-premium-anomaly/ 2 | # 3 | # The investment universe contains all NYSE, AMEX, and NASDAQ stocks. Decile portfolios are formed based on the market capitalization 4 | # of stocks. To capture “size” effect, SMB portfolio goes long small stocks (lowest decile) and short big stocks (highest decile). 5 | # 6 | # QC implementation changes: 7 | # - Instead of all listed stock, we select top 3000 stocks by market cap from QC stock universe. 8 | 9 | from AlgorithmImports import * 10 | 11 | class ValueBooktoMarketFactor(QCAlgorithm): 12 | 13 | def Initialize(self): 14 | self.SetStartDate(2000, 1, 1) 15 | self.SetCash(100000) 16 | 17 | self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol 18 | 19 | self.coarse_count = 3000 20 | 21 | self.long = [] 22 | self.short = [] 23 | 24 | self.month = 12 25 | self.selection_flag = False 26 | self.UniverseSettings.Resolution = Resolution.Daily 27 | self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) 28 | 29 | self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection) 30 | 31 | def OnSecuritiesChanged(self, changes): 32 | for security in changes.AddedSecurities: 33 | security.SetFeeModel(CustomFeeModel()) 34 | security.SetLeverage(10) 35 | 36 | def CoarseSelectionFunction(self, coarse): 37 | if not self.selection_flag: 38 | return Universe.Unchanged 39 | 40 | selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa'] 41 | return selected 42 | 43 | def FineSelectionFunction(self, fine): 44 | sorted_by_market_cap = sorted([x for x in fine if x.MarketCap != 0 and \ 45 | ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE"))], \ 46 | key = lambda x:x.MarketCap, reverse=True) 47 | top_by_market_cap = [x for x in sorted_by_market_cap[:self.coarse_count]] 48 | 49 | quintile = int(len(top_by_market_cap) / 5) 50 | self.long = [i.Symbol for i in top_by_market_cap[-quintile:]] 51 | self.short = [i.Symbol for i in top_by_market_cap[:quintile]] 52 | 53 | return self.long + self.short 54 | 55 | def OnData(self, data): 56 | if not self.selection_flag: 57 | return 58 | self.selection_flag = False 59 | 60 | # Trade execution. 61 | long_count = len(self.long) 62 | short_count = len(self.short) 63 | 64 | stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested] 65 | for symbol in stocks_invested: 66 | if symbol not in self.long + self.short: 67 | self.Liquidate(symbol) 68 | 69 | # Leveraged portfolio - 100% long, 100% short. 70 | for symbol in self.long: 71 | self.SetHoldings(symbol, 1 / long_count) 72 | 73 | for symbol in self.short: 74 | self.SetHoldings(symbol, -1 / short_count) 75 | 76 | self.long.clear() 77 | self.short.clear() 78 | 79 | def Selection(self): 80 | if self.month == 12: 81 | self.selection_flag = True 82 | 83 | self.month += 1 84 | if self.month > 12: 85 | self.month = 1 86 | 87 | # Custom fee model. 88 | class CustomFeeModel(FeeModel): 89 | def GetOrderFee(self, parameters): 90 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 91 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/soccer-clubs-stocks-arbitrage.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/soccer-clubs-stocks-arbitrage/ 2 | # 3 | # The investment universe consists of liquid soccer clubs’ stocks that are publicly traded. 4 | # The investor then sells short stocks of clubs that play UEFA Championship matches (or other important matches) 5 | # at the end of the business day before the match. Stocks are held for one day, 6 | # and the portfolio of stocks is equally weighted (if there are multiple clubs with matches that day). 7 | # 8 | # QC Implementation: 9 | 10 | #region imports 11 | from AlgorithmImports import * 12 | #endregion 13 | 14 | class SoccerClubsStocksArbitrage(QCAlgorithm): 15 | 16 | def Initialize(self): 17 | self.SetStartDate(2000, 1, 1) 18 | self.SetCash(100000) 19 | 20 | self.tickers = [ 21 | 'FCPP', # Futebol Clube Do Porto 22 | 'SPSO', # Sporting Clube De Portugal 23 | 'SLBEN', # Benfica 24 | 'LAZI', # Lazio 25 | 'ASR', # AS Rome 26 | 'AJAX', # AJAX 27 | 'JUVE', # Juventus 28 | 'MANU', # Manchester United 29 | 'BVB', # Dortmund 30 | 'CCP', # Celtic 31 | # 'BOLA' # Bali Bintang Sejahtera Tbk PT 32 | ] 33 | 34 | self.match_dates = {} 35 | 36 | for ticker in self.tickers: 37 | security = self.AddData(QuantpediaSoccer, ticker, Resolution.Daily) 38 | security.SetFeeModel(CustomFeeModel()) 39 | security.SetLeverage(5) 40 | 41 | csv_string_file = self.Download('data.quantpedia.com/backtesting_data/equity/soccer/soccer_matches.csv') 42 | lines = csv_string_file.split('\r\n') 43 | for line in lines: 44 | line_split = line.split(';') 45 | date = datetime.strptime(line_split[0], "%d.%m.%Y").date() 46 | 47 | self.match_dates[date] = [] 48 | for i in range(1, len(line_split)): 49 | ticker = line_split[i] 50 | self.match_dates[date].append(ticker) 51 | 52 | def OnData(self, data): 53 | self.Liquidate() 54 | 55 | short = [] 56 | 57 | # Looking for todays date, because only daily closes are traded. 58 | today = (self.Time - timedelta(days=1)).date() 59 | 60 | if today in self.match_dates: 61 | for ticker in self.tickers: 62 | if ticker in self.match_dates[today] and ticker in data: 63 | short.append(ticker) 64 | 65 | for ticker in short: 66 | self.SetHoldings(ticker, -1 / len(short)) 67 | 68 | # Quantpedia data. 69 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 70 | class QuantpediaSoccer(PythonData): 71 | def GetSource(self, config, date, isLiveMode): 72 | return SubscriptionDataSource("data.quantpedia.com/backtesting_data/equity/soccer/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv) 73 | 74 | def Reader(self, config, line, date, isLiveMode): 75 | data = QuantpediaSoccer() 76 | data.Symbol = config.Symbol 77 | 78 | if not line[0].isdigit(): return None 79 | split = line.split(';') 80 | 81 | data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1) 82 | data['price'] = float(split[1]) 83 | data.Value = float(split[1]) 84 | 85 | return data 86 | 87 | # Custom fee model. 88 | class CustomFeeModel(FeeModel): 89 | def GetOrderFee(self, parameters): 90 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 91 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/synthetic-lending-rates-predict-subsequent-market-return.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/synthetic-lending-rates-predict-subsequent-market-return/ 2 | # 3 | # The investment universe consists of SPY ETF. Synthetic shorting costs data are obtained from Borrow Intensity Indicators by the CBOE (and includes 4877 stocks/ETFs). 4 | # The paper utilizes the constant maturities of 45 days. Intraday SPY data are obtained from FirstRate Data. The aggregate (mean) borrow intensity is calculated as equally weighted borrow intensity of each stock/ETF in the sample at day t. 5 | # The shorting costs data are estimated at a timestamp of 15:57. Calculate the change in the aggregate intensity at day t as the difference of aggregate borrowing intensity at day t and t-1. 6 | # Buy the SPY ETF at 15:59 if the difference is positive and short the SPY if the difference is negative. The positions are held for one day and are closed at 15:58 at next day. 7 | # 8 | # QC Implementation changes: 9 | # - Signal calculation and trade opening is done each day at 15:59. 10 | 11 | #region imports 12 | from AlgorithmImports import * 13 | #endregion 14 | 15 | class SyntheticLendingRatesPredictSubsequentMarketReturn(QCAlgorithm): 16 | def Initialize(self): 17 | self.SetStartDate(2016, 1, 1) 18 | self.SetCash(100000) 19 | 20 | self.spy_symbol:Symbol = self.AddEquity('SPY', Resolution.Minute).Symbol 21 | 22 | self.lending_data_symbol:Symbol = self.AddData( 23 | QuantpediaLendingRates, 24 | 'lending_rate', 25 | Resolution.Minute).Symbol 26 | 27 | self.last_lending_mean = None 28 | 29 | def OnData(self, data: Slice): 30 | curr_time:datetime.datetime = self.Time 31 | 32 | # liquidate on 15:58 33 | if curr_time.hour == 15 and curr_time.minute == 58: 34 | self.Liquidate(self.spy_symbol) 35 | 36 | # lending rate data came in 37 | if self.lending_data_symbol in data and data[self.lending_data_symbol]: 38 | curr_lending_mean:float = data[self.lending_data_symbol].Value 39 | 40 | if self.last_lending_mean: 41 | # calculate daily change in lending rate 42 | diff:float = curr_lending_mean - self.last_lending_mean 43 | 44 | if diff > 0: 45 | self.SetHoldings(self.spy_symbol, 1) 46 | else: 47 | self.SetHoldings(self.spy_symbol, -1) 48 | 49 | self.last_lending_mean = curr_lending_mean 50 | 51 | # Quantpedia data. 52 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 53 | class QuantpediaLendingRates(PythonData): 54 | def GetSource(self, config, date, isLiveMode): 55 | return SubscriptionDataSource("data.quantpedia.com/backtesting_data/options/lending_rates_day_close_matur_45_days.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv) 56 | 57 | def Reader(self, config, line, date, isLiveMode): 58 | data:QuantpediaLendingRates = QuantpediaLendingRates() 59 | data.Symbol = config.Symbol 60 | 61 | if not line[0].isdigit(): return None 62 | 63 | split:list = line.split(';') 64 | 65 | datetime_str:str = split[0] + ', 15:59' 66 | 67 | data.Time = datetime.strptime(datetime_str, "%Y-%m-%d, %H:%M") 68 | valid_values:list = list(filter(lambda value: value != '', split[1:])) 69 | valid_values:list = list(map(lambda str_value: float(str_value), valid_values)) 70 | data.Value = np.mean(valid_values) 71 | 72 | return data -------------------------------------------------------------------------------- /static/strategies/term-structure-effect-in-commodities.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/term-structure-effect-in-commodities/ 2 | # 3 | # This simple strategy buys each month the 20% of commodities with the highest roll-returns and shorts the 20% of commodities with the lowest 4 | # roll-returns and holds the long-short positions for one month. The contracts in each quintile are equally-weighted. 5 | # The investment universe is all commodity futures contracts. 6 | # 7 | # QC implementation: 8 | 9 | import numpy as np 10 | from datetime import time 11 | from AlgorithmImports import * 12 | 13 | class TermStructure(QCAlgorithm): 14 | 15 | def Initialize(self): 16 | self.SetStartDate(2009, 1, 1) 17 | self.SetCash(100000) 18 | 19 | symbols = { 20 | 'CME_S1': Futures.Grains.Soybeans, 21 | 'CME_W1' : Futures.Grains.Wheat, 22 | 'CME_SM1' : Futures.Grains.SoybeanMeal, 23 | 'CME_C1' : Futures.Grains.Corn, 24 | 'CME_O1' : Futures.Grains.Oats, 25 | 'CME_LC1' : Futures.Meats.LiveCattle, 26 | 'CME_FC1' : Futures.Meats.FeederCattle, 27 | 'CME_LN1' : Futures.Meats.LeanHogs, 28 | 'CME_GC1' : Futures.Metals.Gold, 29 | 'CME_SI1' : Futures.Metals.Silver, 30 | 'CME_PL1' : Futures.Metals.Platinum, 31 | 32 | 'CME_HG1' : Futures.Metals.Copper, 33 | 'CME_LB1' : Futures.Forestry.RandomLengthLumber, 34 | 'CME_NG1' : Futures.Energies.NaturalGas, 35 | 'CME_PA1' : Futures.Metals.Palladium, 36 | 'CME_DA1' : Futures.Dairy.ClassIIIMilk, 37 | 38 | 'CME_RB1' : Futures.Energies.Gasoline, 39 | 'ICE_WT1' : Futures.Energies.CrudeOilWTI, 40 | 'ICE_CC1' : Futures.Softs.Cocoa, 41 | 'ICE_O1' : Futures.Energies.HeatingOil, 42 | 'ICE_SB1' : Futures.Softs.Sugar11CME, 43 | } 44 | 45 | self.futures_info:dict = {} 46 | self.quantile:int = 5 47 | self.min_expiration_days:int = 2 48 | self.max_expiration_days:int = 360 49 | 50 | self.price_data:dict[Symbol, RollingWindow] = {} 51 | self.period:int = 60 52 | self.SetWarmup(self.period, Resolution.Daily) 53 | 54 | for qp_symbol, qc_future in symbols.items(): 55 | # QP futures 56 | data:Security = self.AddData(QuantpediaFutures, qp_symbol, Resolution.Daily) 57 | data.SetFeeModel(CustomFeeModel()) 58 | data.SetLeverage(5) 59 | self.price_data[data.Symbol] = RollingWindow[float](self.period) 60 | 61 | # QC futures 62 | future:Future = self.AddFuture(qc_future, Resolution.Daily, dataNormalizationMode=DataNormalizationMode.Raw) 63 | future.SetFilter(timedelta(days=self.min_expiration_days), timedelta(days=self.max_expiration_days)) 64 | self.futures_info[future.Symbol.Value] = FuturesInfo(data.Symbol) 65 | 66 | self.recent_month:int = -1 67 | 68 | def find_and_update_contracts(self, futures_chain, symbol) -> None: 69 | near_contract:FuturesContract = None 70 | dist_contract:FuturesContract = None 71 | 72 | if symbol in futures_chain: 73 | contracts:list = [contract for contract in futures_chain[symbol] if contract.Expiry.date() > self.Time.date()] 74 | 75 | if len(contracts) >= 2: 76 | contracts:list = sorted(contracts, key=lambda x: x.Expiry, reverse=False) 77 | near_contract = contracts[0] 78 | dist_contract = contracts[1] 79 | 80 | self.futures_info[symbol].update_contracts(near_contract, dist_contract) 81 | 82 | def OnData(self, data): 83 | if data.FutureChains.Count > 0: 84 | for symbol, futures_info in self.futures_info.items(): 85 | # check if near contract is expired or is not initialized 86 | if not futures_info.is_initialized() or \ 87 | (futures_info.is_initialized() and futures_info.near_contract.Expiry.date() == self.Time.date()): 88 | self.find_and_update_contracts(data.FutureChains, symbol) 89 | 90 | roll_return:dict[Symbol, float] = {} 91 | rebalance_flag:bool = False 92 | 93 | # roll return calculation 94 | for symbol, futures_info in self.futures_info.items(): 95 | # futures data is present in the algorithm 96 | if futures_info.quantpedia_future in data and data[futures_info.quantpedia_future]: 97 | # store daily data 98 | self.price_data[futures_info.quantpedia_future].Add(data[futures_info.quantpedia_future].Value) 99 | if not self.price_data[futures_info.quantpedia_future].IsReady: 100 | continue 101 | 102 | # new month rebalance 103 | if self.Time.month != self.recent_month and not self.IsWarmingUp: 104 | self.recent_month = self.Time.month 105 | rebalance_flag = True 106 | 107 | if rebalance_flag: 108 | if futures_info.is_initialized(): 109 | near_c:FuturesContract = futures_info.near_contract 110 | dist_c:FuturesContract = futures_info.distant_contract 111 | if self.Securities.ContainsKey(near_c.Symbol) and self.Securities.ContainsKey(dist_c.Symbol): 112 | raw_price1:float = self.Securities[near_c.Symbol].Close * self.Securities[symbol].SymbolProperties.PriceMagnifier 113 | raw_price2:float = self.Securities[dist_c.Symbol].Close * self.Securities[symbol].SymbolProperties.PriceMagnifier 114 | 115 | if raw_price1 != 0 and raw_price2 != 0: 116 | roll_return[futures_info.quantpedia_future] = raw_price1 / raw_price2 - 1 117 | 118 | if rebalance_flag: 119 | weights:dict[Symbol, float] = {} 120 | 121 | long:list[Symbol] = [] 122 | short:list[Symbol] = [] 123 | if len(roll_return) >= self.quantile: 124 | 125 | # roll return sorting 126 | sorted_by_roll:list = sorted(roll_return.items(), key = lambda x: x[1], reverse=True) 127 | quantile:int = int(len(sorted_by_roll) / self.quantile) 128 | long = [x[0] for x in sorted_by_roll[:quantile]] 129 | short = [x[0] for x in sorted_by_roll[-quantile:]] 130 | 131 | # trade execution 132 | invested:list[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested] 133 | for symbol in invested: 134 | if symbol not in long + short: 135 | self.Liquidate(symbol) 136 | 137 | for symbol in long: 138 | self.SetHoldings(symbol, 1 / len(long)) 139 | 140 | for symbol in short: 141 | self.SetHoldings(symbol, -1 / len(short)) 142 | 143 | class FuturesInfo(): 144 | def __init__(self, quantpedia_future:Symbol) -> None: 145 | self.quantpedia_future:Symbol = quantpedia_future 146 | self.near_contract:FuturesContract = None 147 | self.distant_contract:FuturesContract = None 148 | 149 | def update_contracts(self, near_contract:FuturesContract, distant_contract:FuturesContract) -> None: 150 | self.near_contract = near_contract 151 | self.distant_contract = distant_contract 152 | 153 | def is_initialized(self) -> bool: 154 | return self.near_contract is not None and self.distant_contract is not None 155 | 156 | # Custom fee model. 157 | class CustomFeeModel(): 158 | def GetOrderFee(self, parameters): 159 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 160 | return OrderFee(CashAmount(fee, "USD")) 161 | 162 | # Quantpedia data. 163 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 164 | class QuantpediaFutures(PythonData): 165 | def GetSource(self, config, date, isLiveMode): 166 | return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv) 167 | 168 | def Reader(self, config, line, date, isLiveMode): 169 | data = QuantpediaFutures() 170 | data.Symbol = config.Symbol 171 | 172 | if not line[0].isdigit(): return None 173 | split = line.split(';') 174 | 175 | data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1) 176 | data['back_adjusted'] = float(split[1]) 177 | data['spliced'] = float(split[2]) 178 | data.Value = float(split[1]) 179 | 180 | return data -------------------------------------------------------------------------------- /static/strategies/trading-wti-brent-spread.py: -------------------------------------------------------------------------------- 1 | #region imports 2 | from AlgorithmImports import * 3 | #endregion 4 | # https://quantpedia.com/strategies/trading-wti-brent-spread/ 5 | # 6 | # A 20-day moving average of WTI/Brent spread is calculated each day. If the current spread value is above SMA 20 then we enter a short position 7 | # in the spread on close (betting that the spread will decrease to the fair value represented by SMA 20). The trade is closed at the close of the 8 | # trading day when the spread crosses below fair value. If the current spread value is below SMA 20 then we enter a long position betting that 9 | # the spread will increase and the trade is closed at the close of the trading day when the spread crosses above fair value. 10 | 11 | class WTIBRENTSpread(QCAlgorithm): 12 | 13 | def Initialize(self): 14 | self.SetStartDate(2000, 1, 1) 15 | self.SetCash(100000) 16 | 17 | self.symbols = [ 18 | "ICE_WT1", # WTI Crude Futures, Continuous Contract 19 | "ICE_B1" # Brent Crude Oil Futures, Continuous Contract 20 | ] 21 | 22 | self.spread = RollingWindow[float](20) 23 | 24 | for symbol in self.symbols: 25 | data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily) 26 | data.SetLeverage(5) 27 | data.SetFeeModel(CustomFeeModel()) 28 | 29 | def OnData(self, data): 30 | symbol1 = self.Symbol(self.symbols[0]) 31 | symbol2 = self.Symbol(self.symbols[1]) 32 | 33 | if symbol1 in data.Keys and symbol2 in data.Keys and data[symbol1] and data[symbol2]: 34 | price1 = data[symbol1].Price 35 | price2 = data[symbol2].Price 36 | 37 | if price1 != 0 and price2 != 0: 38 | spread = price1 - price2 39 | self.spread.Add(spread) 40 | 41 | # MA calculation. 42 | if self.spread.IsReady: 43 | if (self.Time.date() - self.Securities[symbol1].GetLastData().Time.date()).days < 5 and (self.Time.date() - self.Securities[symbol2].GetLastData().Time.date()).days < 5: 44 | spreads = [x for x in self.spread] 45 | spread_ma20 = sum(spreads) / len(spreads) 46 | 47 | current_spread = spreads[0] 48 | 49 | if current_spread > spread_ma20: 50 | self.SetHoldings(symbol1, -1) 51 | self.SetHoldings(symbol2, 1) 52 | elif current_spread < spread_ma20: 53 | self.SetHoldings(symbol1, 1) 54 | self.SetHoldings(symbol2, -1) 55 | else: 56 | self.Liquidate() 57 | 58 | # Quantpedia data. 59 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 60 | class QuantpediaFutures(PythonData): 61 | def GetSource(self, config, date, isLiveMode): 62 | return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv) 63 | 64 | def Reader(self, config, line, date, isLiveMode): 65 | data = QuantpediaFutures() 66 | data.Symbol = config.Symbol 67 | 68 | if not line[0].isdigit(): return None 69 | split = line.split(';') 70 | 71 | data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1) 72 | data['back_adjusted'] = float(split[1]) 73 | data['spliced'] = float(split[2]) 74 | data.Value = float(split[1]) 75 | 76 | return data 77 | 78 | # Custom fee model. 79 | class CustomFeeModel(FeeModel): 80 | def GetOrderFee(self, parameters): 81 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 82 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/trend-following-effect-in-stocks.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/trend-following-effect-in-stocks/ 2 | # 3 | # The investment universe consists of US-listed companies. A minimum stock price filter is used to avoid penny stocks, and a minimum 4 | # daily liquidity filter is used to avoid stocks that are not liquid enough. The entry signal occurs if today’s close is greater than 5 | # or equal to the highest close during the stock’s entire history. A 10-period average true range trailing stop is used as an exit 6 | # signal. The investor holds all stocks which satisfy entry criterion and are not stopped out. The portfolio is equally weighted and 7 | # rebalanced daily. Transaction costs of 0.5% round-turn are deducted from each trade to account for estimated commission and slippage. 8 | # 9 | # QC implementation: 10 | # - Universe consists of top 100 liquid US stocks. 11 | 12 | import numpy as np 13 | from AlgorithmImports import * 14 | 15 | class TrendFollowingStocks(QCAlgorithm): 16 | 17 | def Initialize(self): 18 | self.SetStartDate(2010, 1, 1) 19 | self.SetCash(100000) 20 | 21 | self.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.GetLastKnownPrice(x))) 22 | 23 | self.course_count = 100 24 | self.long = [] 25 | 26 | self.max_close = {} 27 | self.atr = {} 28 | 29 | self.sl_order = {} 30 | self.sl_price = {} 31 | 32 | self.selection = [] 33 | self.period = 10*12*21 34 | 35 | self.UniverseSettings.Resolution = Resolution.Daily 36 | self.AddUniverse(self.CoarseSelectionFunction) 37 | 38 | def OnSecuritiesChanged(self, changes): 39 | for security in changes.AddedSecurities: 40 | security.SetFeeModel(CustomFeeModel()) 41 | 42 | symbol = security.Symbol 43 | if symbol not in self.atr: 44 | self.atr[symbol] = self.ATR(symbol, 10, Resolution.Daily) 45 | 46 | if symbol not in self.max_close: 47 | hist = self.History([self.Symbol(symbol)], self.period, Resolution.Daily) 48 | if 'close' in hist.columns: 49 | closes = hist['close'] 50 | self.max_close[symbol] = max(closes) 51 | 52 | def CoarseSelectionFunction(self, coarse): 53 | if self.IsWarmingUp: return 54 | 55 | selected = sorted([x for x in coarse if x.HasFundamentalData and x.Price > 5], 56 | key=lambda x: x.DollarVolume, reverse=True) 57 | 58 | self.selection = [x.Symbol for x in selected[:self.course_count]] 59 | 60 | return self.selection 61 | 62 | def OnData(self, data): 63 | if self.IsWarmingUp: 64 | return 65 | 66 | for symbol in self.selection: 67 | if symbol in data.Bars: 68 | price = data[symbol].Value 69 | 70 | if symbol not in self.max_close: continue 71 | 72 | if price >= self.max_close[symbol]: 73 | self.max_close[symbol] = price 74 | self.long.append(symbol) 75 | 76 | stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested] 77 | count = len(self.long) + len(stocks_invested) 78 | if count == 0: return 79 | 80 | # Update stoploss orders 81 | for symbol in stocks_invested: 82 | if not self.Securities[symbol].IsTradable: 83 | self.Liquidate(symbol) 84 | 85 | if self.atr[symbol].Current.Value == 0: continue 86 | 87 | # Move SL 88 | if symbol not in self.sl_price: continue 89 | 90 | self.SetHoldings(symbol, 1 / count) 91 | 92 | new_sl = self.Securities[symbol].Price - self.atr[symbol].Current.Value 93 | if new_sl > self.sl_price[symbol]: 94 | update_order_fields = UpdateOrderFields() 95 | update_order_fields.StopPrice = new_sl # Update SL price 96 | 97 | quantity = self.CalculateOrderQuantity(symbol, (1 / count)) 98 | update_order_fields.Quantity = quantity # Update SL quantity 99 | 100 | self.sl_price[symbol] = new_sl 101 | self.sl_order[symbol].Update(update_order_fields) 102 | # self.Log('SL MOVED on ' + str(symbol) + ' to: ' + str(new_sl)) 103 | 104 | # Open new trades 105 | for symbol in self.long: 106 | if not self.Portfolio[symbol].Invested and self.atr[symbol].Current.Value != 0: 107 | price = data[symbol].Value 108 | if self.Securities[symbol].IsTradable: 109 | unit_size = self.CalculateOrderQuantity(symbol, (1 / count)) 110 | 111 | self.MarketOrder(symbol, unit_size) 112 | 113 | sl_price = price - self.atr[symbol].Current.Value 114 | self.sl_price[symbol] = sl_price 115 | if unit_size != 0: 116 | self.sl_order[symbol] = self.StopMarketOrder(symbol, -unit_size, sl_price, 'SL') 117 | # self.Log('SL SET on ' + str(symbol) + ' to: ' + str(sl_price)) 118 | 119 | self.long.clear() 120 | 121 | # Custom fee model. 122 | class CustomFeeModel(FeeModel): 123 | def GetOrderFee(self, parameters): 124 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 125 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/turn-of-the-month-in-equity-indexes.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/turn-of-the-month-in-equity-indexes/ 2 | # 3 | # Buy SPY ETF 1 day (some papers say 4 days) before the end of the month and sell the 3rd trading day of the new month at the close. 4 | 5 | from AlgorithmImports import * 6 | 7 | class TurnoftheMonthinEquityIndexes(QCAlgorithm): 8 | 9 | def Initialize(self): 10 | self.SetStartDate(1998, 1, 1) 11 | self.SetCash(100000) 12 | 13 | self.symbol = self.AddEquity("SPY", Resolution.Daily).Symbol 14 | 15 | self.sell_flag = False 16 | self.days = 0 17 | 18 | self.Schedule.On(self.DateRules.MonthStart(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Rebalance) 19 | self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Purchase) 20 | 21 | def Purchase(self): 22 | self.SetHoldings(self.symbol, 1) 23 | 24 | def Rebalance(self): 25 | self.sell_flag = True 26 | 27 | def OnData(self, data): 28 | if self.sell_flag: 29 | self.days += 1 30 | if self.days == 3: 31 | self.Liquidate(self.symbol) 32 | self.sell_flag = False 33 | self.days = 0 -------------------------------------------------------------------------------- /static/strategies/value-book-to-market-factor.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/value-book-to-market-factor/ 2 | # 3 | # The investment universe contains all NYSE, AMEX, and NASDAQ stocks. To represent “value” investing, HML portfolio goes long high book-to-price stocks and short, 4 | # low book-to-price stocks. In this strategy, we show the results for regular HML which is simply the average of the portfolio returns of HML small (which goes long 5 | # cheap and short expensive only among small stocks) and HML large (which goes long cheap and short expensive only among large caps). The portfolio is equal-weighted 6 | # and rebalanced monthly. 7 | # 8 | # QC implementation changes: 9 | # - Instead of all listed stock, we select top 3000 stocks by market cap from QC stock universe. 10 | 11 | from AlgorithmImports import * 12 | 13 | class Value(QCAlgorithm): 14 | 15 | def Initialize(self): 16 | self.SetStartDate(2000, 1, 1) 17 | self.SetCash(100000) 18 | 19 | self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol 20 | 21 | self.coarse_count = 3000 22 | 23 | self.long = [] 24 | self.short = [] 25 | 26 | self.month = 12 27 | self.selection_flag = False 28 | self.UniverseSettings.Resolution = Resolution.Daily 29 | self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) 30 | 31 | self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection) 32 | 33 | def OnSecuritiesChanged(self, changes): 34 | for security in changes.AddedSecurities: 35 | security.SetFeeModel(CustomFeeModel()) 36 | security.SetLeverage(5) 37 | 38 | def CoarseSelectionFunction(self, coarse): 39 | if not self.selection_flag: 40 | return Universe.Unchanged 41 | 42 | selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa'] 43 | return selected 44 | 45 | def FineSelectionFunction(self, fine): 46 | sorted_by_market_cap = sorted([x for x in fine if x.ValuationRatios.PBRatio != 0 and \ 47 | ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE"))], 48 | key = lambda x:x.MarketCap, reverse=True) 49 | 50 | top_by_market_cap = [x for x in sorted_by_market_cap[:self.coarse_count]] 51 | 52 | sorted_by_pb = sorted(top_by_market_cap, key = lambda x:(x.ValuationRatios.PBRatio), reverse=False) 53 | quintile = int(len(sorted_by_pb) / 5) 54 | self.long = [i.Symbol for i in sorted_by_pb[:quintile]] 55 | self.short = [i.Symbol for i in sorted_by_pb[-quintile:]] 56 | 57 | return self.long + self.short 58 | 59 | def OnData(self, data): 60 | if not self.selection_flag: 61 | return 62 | self.selection_flag = False 63 | 64 | # Trade execution. 65 | stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested] 66 | for symbol in stocks_invested: 67 | if symbol not in self.long + self.short: 68 | self.Liquidate(symbol) 69 | 70 | # Leveraged portfolio - 100% long, 100% short. 71 | for symbol in self.long: 72 | self.SetHoldings(symbol, 1 / len(self.long)) 73 | 74 | for symbol in self.short: 75 | self.SetHoldings(symbol, -1 / len(self.short)) 76 | 77 | self.long.clear() 78 | self.short.clear() 79 | 80 | def Selection(self): 81 | if self.month == 12: 82 | self.selection_flag = True 83 | 84 | self.month += 1 85 | if self.month > 12: 86 | self.month = 1 87 | 88 | # Custom fee model. 89 | class CustomFeeModel(FeeModel): 90 | def GetOrderFee(self, parameters): 91 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 92 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/value-factor-effect-within-countries.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/value-factor-effect-within-countries/ 2 | # 3 | # The investment universe consists of 32 countries with easily accessible equity markets (via ETFs, for example). At the end of every year, 4 | # the investor calculates Shiller’s “CAPE” Cyclically Adjusted PE) ratio, for each country in his investment universe. CAPE is the ratio of 5 | # the real price of the equity market (adjusted for inflation) to the 10-year average of the country’s equity index (again adjusted for inflation). 6 | # The whole methodology is explained well on Shiller’s home page (http://www.econ.yale.edu/~shiller/data.htm) or 7 | # http://turnkeyanalyst.com/2011/10/the-shiller-pe-ratio/). The investor then invests in the cheapest 33% of countries from his sample if those 8 | # countries have a CAPE below 15. The portfolio is equally weighted (the investor holds 0% cash instead of countries with a CAPE higher than 15) 9 | # and rebalanced yearly. 10 | 11 | #region imports 12 | from AlgorithmImports import * 13 | #endregion 14 | 15 | class ValueFactorCAPEEffectwithinCountries(QCAlgorithm): 16 | 17 | def Initialize(self): 18 | self.SetStartDate(2008, 1, 1) 19 | self.SetCash(100000) 20 | 21 | self.symbols = { 22 | "Australia" : "EWA", # iShares MSCI Australia Index ETF 23 | "Brazil" : "EWZ", # iShares MSCI Brazil Index ETF 24 | "Canada" : "EWC", # iShares MSCI Canada Index ETF 25 | "Switzerland" : "EWL", # iShares MSCI Switzerland Index ETF 26 | "China" : "FXI", # iShares China Large-Cap ETF 27 | "France" : "EWQ", # iShares MSCI France Index ETF 28 | "Germany" : "EWG", # iShares MSCI Germany ETF 29 | "Hong Kong" : "EWH", # iShares MSCI Hong Kong Index ETF 30 | "Italy" : "EWI", # iShares MSCI Italy Index ETF 31 | "Japan" : "EWJ", # iShares MSCI Japan Index ETF 32 | "Korea" : "EWY", # iShares MSCI South Korea ETF 33 | "Mexico" : "EWW", # iShares MSCI Mexico Inv. Mt. Idx 34 | "Netherlands" : "EWN", # iShares MSCI Netherlands Index ETF 35 | "South Africa" : "EZA", # iShares MSCI South Africe Index ETF 36 | "Singapore" : "EWS", # iShares MSCI Singapore Index ETF 37 | "Spain" : "EWP", # iShares MSCI Spain Index ETF 38 | "Sweden" : "EWD", # iShares MSCI Sweden Index ETF 39 | "Taiwan" : "EWT", # iShares MSCI Taiwan Index ETF 40 | "UK" : "EWU", # iShares MSCI United Kingdom Index ETF 41 | "USA" : "SPY", # SPDR S&P 500 ETF 42 | 43 | "Russia" : "ERUS", # iShares MSCI Russia ETF 44 | "Israel" : "EIS", # iShares MSCI Israel ETF 45 | "India" : "INDA", # iShares MSCI India ETF 46 | "Poland" : "EPOL", # iShares MSCI Poland ETF 47 | "Turkey" : "TUR" # iShares MSCI Turkey ETF 48 | } 49 | 50 | for country, etf_symbol in self.symbols.items(): 51 | data = self.AddEquity(etf_symbol, Resolution.Daily) 52 | data.SetFeeModel(CustomFeeModel()) 53 | 54 | self.quantile:int = 3 55 | self.max_missing_days:int = 31 56 | 57 | # CAPE data import. 58 | self.cape_data = self.AddData(CAPE, 'CAPE', Resolution.Daily).Symbol 59 | 60 | self.recent_month:int = -1 61 | 62 | def OnData(self, data:Slice) -> None: 63 | if self.Time.month == self.recent_month: 64 | return 65 | self.recent_month = self.Time.month 66 | 67 | if self.recent_month != 12: 68 | return 69 | 70 | price = {} 71 | for country, etf_symbol in self.symbols.items(): 72 | if etf_symbol in data and data[etf_symbol]: 73 | # cape data is still coming in 74 | if self.Securities[self.cape_data].GetLastData() and (self.Time.date() - self.Securities[self.cape_data].GetLastData().Time.date()).days <= self.max_missing_days: 75 | country_cape = self.Securities['CAPE'].GetLastData().GetProperty(country) 76 | if country_cape < 15: 77 | price[etf_symbol] = data[etf_symbol].Value 78 | 79 | long = [] 80 | 81 | # Cape and price sorting. 82 | if len(price) >= self.quantile: 83 | sorted_by_price = sorted(price.items(), key = lambda x: x[1], reverse = True) 84 | tercile = int(len(sorted_by_price) / self.quantile) 85 | long = [x[0] for x in sorted_by_price[-tercile:]] 86 | 87 | # Trade execution. 88 | invested = [x.Key for x in self.Portfolio if x.Value.Invested] 89 | for symbol in invested: 90 | if symbol not in long: 91 | self.Liquidate(symbol) 92 | 93 | for symbol in long: 94 | if self.Securities[etf_symbol].Price != 0 and self.Securities[etf_symbol].IsTradable: 95 | self.SetHoldings(symbol, 1 / len(long)) 96 | 97 | # NOTE: IMPORTANT: Data order must be ascending (datewise) 98 | # Data source: https://indices.barclays/IM/21/en/indices/static/historic-cape.app 99 | class CAPE(PythonData): 100 | def GetSource(self, config, date, isLiveMode): 101 | return SubscriptionDataSource("data.quantpedia.com/backtesting_data/economic/cape_by_country.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv) 102 | 103 | def Reader(self, config, line, date, isLiveMode): 104 | data = CAPE() 105 | data.Symbol = config.Symbol 106 | 107 | if not line[0].isdigit(): return None 108 | split = line.split(';') 109 | 110 | data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1) 111 | 112 | data['Australia'] = float(split[1]) 113 | data['Brazil'] = float(split[2]) 114 | data['Canada'] = float(split[3]) 115 | data['Switzerland'] = float(split[4]) 116 | data['China'] = float(split[5]) 117 | data['France'] = float(split[6]) 118 | data['Germany'] = float(split[7]) 119 | data['Hong Kong'] = float(split[8]) 120 | data['India'] = float(split[9]) 121 | data['Israel'] = float(split[10]) 122 | data['Italy'] = float(split[11]) 123 | data['Japan'] = float(split[12]) 124 | data['Korea'] = float(split[13]) 125 | data['Mexico'] = float(split[14]) 126 | data['Netherlands'] = float(split[15]) 127 | data['Poland'] = float(split[16]) 128 | data['Russia'] = float(split[17]) 129 | data['South Africa'] = float(split[18]) 130 | data['Singapore'] = float(split[19]) 131 | data['Spain'] = float(split[20]) 132 | data['Sweden'] = float(split[21]) 133 | data['Taiwan'] = float(split[22]) 134 | data['Turkey'] = float(split[23]) 135 | data['UK'] = float(split[24]) 136 | data['USA'] = float(split[25]) 137 | 138 | data.Value = float(split[1]) 139 | 140 | return data 141 | 142 | # Custom fee model. 143 | class CustomFeeModel(FeeModel): 144 | def GetOrderFee(self, parameters): 145 | fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 146 | return OrderFee(CashAmount(fee, "USD")) -------------------------------------------------------------------------------- /static/strategies/volatility-risk-premium-effect.py: -------------------------------------------------------------------------------- 1 | # https://quantpedia.com/strategies/volatility-risk-premium-effect/ 2 | # 3 | # Each month, at-the-money straddle, with one month until maturity, is sold at the bid price with a 5% option premium, and an offsetting 15% 4 | # out-of-the-money puts are bought (at the ask price) as insurance against a market crash. The remaining cash and received option premium are 5 | # invested in the index. The strategy is rebalanced monthly. 6 | 7 | from AlgorithmImports import * 8 | 9 | 10 | class VolatilityRiskPremiumEffect(QCAlgorithm): 11 | def Initialize(self): 12 | self.SetStartDate(2010, 1, 1) 13 | self.SetCash(100000) 14 | 15 | data = self.AddEquity("SPY", Resolution.Minute) 16 | data.SetLeverage(5) 17 | self.symbol = data.Symbol 18 | 19 | option = self.AddOption("SPY", Resolution.Minute) 20 | option.SetFilter(-20, 20, 25, 35) 21 | 22 | self.last_day = -1 23 | 24 | def OnData(self, slice): 25 | # Check once a day. 26 | if self.Time.day == self.last_day: 27 | return 28 | self.last_day = self.Time.day 29 | 30 | for i in slice.OptionChains: 31 | chains = i.Value 32 | 33 | if not self.Portfolio.Invested: 34 | # divide option chains into call and put options 35 | calls = list(filter(lambda x: x.Right == OptionRight.Call, chains)) 36 | puts = list(filter(lambda x: x.Right == OptionRight.Put, chains)) 37 | 38 | # if lists are empty return 39 | if not calls or not puts: 40 | return 41 | 42 | underlying_price = self.Securities[self.symbol].Price 43 | expiries = [i.Expiry for i in puts] 44 | 45 | # determine expiration date nearly one month 46 | expiry = min( 47 | expiries, key=lambda x: abs((x.date() - self.Time.date()).days - 30) 48 | ) 49 | strikes = [i.Strike for i in puts] 50 | 51 | # determine at-the-money strike 52 | strike = min(strikes, key=lambda x: abs(x - underlying_price)) 53 | 54 | # determine 15% out-of-the-money strike 55 | otm_strike = min( 56 | strikes, key=lambda x: abs(x - float(0.85) * underlying_price) 57 | ) 58 | 59 | atm_call = [ 60 | i for i in calls if i.Expiry == expiry and i.Strike == strike 61 | ] 62 | atm_put = [i for i in puts if i.Expiry == expiry and i.Strike == strike] 63 | otm_put = [ 64 | i for i in puts if i.Expiry == expiry and i.Strike == otm_strike 65 | ] 66 | 67 | if atm_call and atm_put and otm_put: 68 | options_q = int( 69 | self.Portfolio.MarginRemaining / (underlying_price * 100) 70 | ) 71 | 72 | # Set max leverage. 73 | self.Securities[atm_call[0].Symbol].MarginModel = BuyingPowerModel( 74 | 5 75 | ) 76 | self.Securities[atm_put[0].Symbol].MarginModel = BuyingPowerModel(5) 77 | self.Securities[otm_put[0].Symbol].MarginModel = BuyingPowerModel(5) 78 | 79 | # sell at-the-money straddle 80 | self.Sell(atm_call[0].Symbol, options_q) 81 | self.Sell(atm_put[0].Symbol, options_q) 82 | 83 | # buy 15% out-of-the-money put 84 | self.Buy(otm_put[0].Symbol, options_q) 85 | 86 | # buy index. 87 | self.SetHoldings(self.symbol, 1) 88 | 89 | invested = [x.Key for x in self.Portfolio if x.Value.Invested] 90 | if len(invested) == 1: 91 | self.Liquidate(self.symbol) 92 | --------------------------------------------------------------------------------