Quant Buffet放轻松,别过度思虑

抄底“坠刀”股票策略

登录后收藏

回测表现

年化收益23.09%
贝塔0.978
索提诺比率0.33
胜率55%

完整 Python 代码

from AlgorithmImports import *
from typing import List, Dict
import numpy as np
from numpy import isnan
class CatchingFallingKnifeStocks(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.fundamental_sorting_key = lambda x: x.MarketCap
self.fundamental_count:int = 3000
self.period:int = 500

self.long:List[Symbol] = []
# Daily data
self.data:Dict[Symbol, SymbolData] = {}
self.min_share_price:int = 5
self.leverage:int = 3

self.symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
if self.symbol not in self.data:
    self.data[self.symbol] = SymbolData(self.symbol, self.period)
    history:DataFrame = self.History(self.symbol, self.period, Resolution.Daily)
    if history.empty:
        self.Log(f"Note enough data for {symbol} yet")
    else:
        closes:Series = history.loc[self.symbol].close[:-1]
        for time, close in closes.items():
            self.data[self.symbol].update(close)

self.last_month:int = -1
self.selection_flag:bool = True
self.settings.daily_precise_end_time = False
self.settings.minimum_order_margin_portfolio_percentage = 0.
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.schedule.on(self.date_rules.month_start(self.market),
                self.time_rules.after_market_open(self.market),
                self.selection)

def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
    security.SetFeeModel(CustomFeeModel())
    security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
for stock in fundamental:
    symbol:Symbol = stock.Symbol
    
    # Store daily price.
    if symbol in self.data:
        self.data[symbol].update(stock.AdjustedPrice)
if not self.selection_flag:
    return Universe.Unchanged
selected:List[Fundamental] = [
    x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.Price > self.min_share_price and x.MarketCap != 0 \
    and not isnan(x.OperationRatios.TotalDebtEquityRatio.ThreeMonths) and x.OperationRatios.TotalDebtEquityRatio.ThreeMonths > 0 \
    and not isnan(x.AssetClassification.MorningstarIndustryGroupCode) and x.AssetClassification.MorningstarIndustryGroupCode != 0
]

if len(selected) > self.fundamental_count:
    selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
group:Dict[str, List[Symbol]] = {}
# Warmup price rolling windows.
for stock in selected:
    symbol:Symbol = stock.Symbol
    if symbol not in self.data:
        self.data[symbol] = SymbolData(symbol, self.period)
        history = self.History(symbol, self.period, Resolution.Daily)
        if history.empty:
            self.Log(f"Not enough data for {symbol} yet")
            continue
        closes = history.loc[symbol].close
        for time, close in closes.items():
            self.data[symbol].update(close)
    if not self.data[symbol].is_ready():
        continue
    industry_group_code = stock.AssetClassification.MorningstarIndustryGroupCode
    # Debt to equity ratio.
    debt_to_equity = stock.OperationRatios.TotalDebtEquityRatio.ThreeMonths
    self.data[symbol]._debt_to_equity = debt_to_equity
    
    # Adding stocks in groups
    if not industry_group_code in group:
        group[industry_group_code] = []
    
    group[industry_group_code].append(self.data[symbol])

if self.symbol in self.data and self.data[self.symbol].is_ready():
    spy_ret:float = self.data[self.symbol].performance()
            
    for industry_code in group:
        industry_debt_to_equity_10th_percentile:float = np.percentile([symbol_data._debt_to_equity for symbol_data in group[industry_code]], 10)
            
        # Stocks that suffered losses of 50 percent or more than s&p
        # and
        # stocks that have a Debt/Equity ratio within at least 10% of the lowest in the industry
        long:List[Symbol] = [symbol_data._symbol for symbol_data in group[industry_code] if symbol_data.performance() <= (spy_ret - 0.5) \
                            and symbol_data._debt_to_equity <= industry_debt_to_equity_10th_percentile]
        
        for symbol in long:
            self.long.append(symbol)
            
return self.long
def OnData(self, data: Slice) -> None:
if not self.selection_flag: 
    return
self.selection_flag = False

# Trade execution.        
targets:List[PortfolioTarget] = []
for symbol in self.long:
    if symbol in data and data[symbol]:
        targets.append(PortfolioTarget(symbol, 1 / len(self.long)))

self.SetHoldings(targets, True)
self.long.clear()
def selection(self) -> None:
self.selection_flag = True
class SymbolData():
def __init__(self, symbol:Symbol, period:int) -> None:
self._symbol:Symbol = symbol
self._price:RollingWindow = RollingWindow[float](period)
self._debt_to_equity:float = 0.

def is_ready(self) -> bool:
return self._price.IsReady

def update(self, close:float) -> None:
self._price.Add(close)

def performance(self) -> float:
return (self._price[0] / self._price[self._price.Count - 1] - 1)
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))