跳转至

动量排序选股

每月末按过去 N 日收益率排序,选前几名买入。最经典的量化策略之一。


"""每月末按动量排序,选前 20% 的股票买入"""
from qka import Data, Strategy, Broker, Backtest


class Momentum(Strategy):
    def __init__(self, cash=1_000_000):
        super().__init__(cash=cash)
        self.last_month = None

    def on_bar(self, date):
        # 月末附近调仓
        if date.day < 28:
            return

        # 本月已调仓,跳过
        month_key = (date.year, date.month)
        if self.last_month == month_key:
            return
        self.last_month = month_key

        close = self.get('close')
        if close is None or close.empty:
            return

        # 过去 60 个交易日的动量
        hist = self.history('close', 60)
        if hist is None or hist.empty:
            return

        # 计算每只股票的阶段收益率
        ret = {}
        for sym in hist.columns:
            prices = hist[sym].dropna()
            if len(prices) >= 40:  # 至少 40 个有效数据
                ret[sym] = prices.iloc[-1] / prices.iloc[0] - 1

        if not ret:
            return

        # 排序,选前 20%
        sorted_syms = sorted(ret, key=ret.get, reverse=True)
        top_n = max(1, len(sorted_syms) // 5)
        buy_list = sorted_syms[:top_n]

        # 卖出不在列表里的持仓
        for sym in list(self.broker.positions.keys()):
            if sym not in buy_list:
                pos = self.broker.positions[sym]
                price = float(close.get(sym, 0))
                if price > 0:
                    self.broker.sell(sym, price, pos['size'])

        # 买入列表中的新标的
        cash_per_sym = self.broker.cash / len(buy_list)
        for sym in buy_list:
            if sym in self.broker.positions:
                continue
            price = float(close.get(sym, 0))
            if price > 0:
                size = self.sizing.fixed_amount(cash_per_sym, price)
                if size > 0:
                    self.broker.buy(sym, price, size)


if __name__ == '__main__':
    data = Data(
        symbols=['000001.SZ', '600000.SH', '000002.SZ',
                 '600036.SH', '601166.SH'],
    )
    bt = Backtest(data, Momentum())
    bt.run(benchmark='000300.SH')
    bt.summary()
    bt.report(title='动量排序选股')