🚀 AI 一键生成 qmt 策略代码

如何在多个备选股票之间进行轮动交易,始终持有表现最强的股票?

qmt | 发布时间: 2025/12/19
以下内容由 EasyQuant 生成。

问题描述

我想写一个策略,每个月对股票池里的所有股票按过去20天的涨幅排序,然后卖掉排名后20%的,买入排名前20%的,该怎么实现?

解决方案

这是一个非常经典的动量策略(Momentum Strategy)

策略逻辑分析

  1. 选股范围:股票池(例如沪深300或自定义板块)。
  2. 调仓周期:按月调仓(通常在每个月的第一个交易日或最后一个交易日)。
  3. 因子计算:计算过去20个交易日的涨跌幅。
    • 公式:$(当前收盘价 - 20日前收盘价) / 20日前收盘价$
  4. 排序与筛选
    • 按涨幅从高到低排序。
    • 买入:排名前 20% 的股票。
    • 卖出:为了保持仓位纯粹性,通常的做法是清仓所有不在“排名前20%”名单中的股票(这自然包含了排名后20%的股票以及中间的股票),并将资金等权买入新的前20%股票。

代码实现

以下是基于 QMT Python API 的完整策略代码。

# -*- coding: gbk -*-
import pandas as pd
import time

def init(ContextInfo):
    # 1. 设置股票池,这里示例使用沪深300,您可以修改为 '我的自选' 等
    # 注意:实盘或回测前请确保已下载相关板块的历史数据
    ContextInfo.index_code = '000300.SH'
    ContextInfo.stock_list = ContextInfo.get_stock_list_in_sector('沪深300')
    
    # 2. 设置策略参数
    ContextInfo.lookback_days = 20       # 回溯天数:20天
    ContextInfo.select_ratio = 0.2       # 筛选比例:前20%
    ContextInfo.hold_percent = 0.98      # 目标总仓位(留一点现金防止手续费不够)
    
    # 3. 记录上一次调仓的月份,用于判断是否换月
    ContextInfo.last_month = None
    
    # 4. 设置资金账号 (请修改为您自己的资金账号)
    # ContextInfo.set_account('您的资金账号') 

def handlebar(ContextInfo):
    # 获取当前K线的时间
    current_timetag = ContextInfo.get_bar_timetag(ContextInfo.barpos)
    current_date_str = timetag_to_datetime(current_timetag, '%Y%m%d')
    current_month = current_date_str[0:6] # 提取年份和月份,如 '202305'
    
    # 判断是否换月:如果当前月份与上一次记录的月份不同,则触发调仓
    # 注意:这里是在每个月出现的第一个K线(即月初)进行调仓
    if current_month != ContextInfo.last_month:
        print(f'检测到月份变化,开始执行调仓逻辑。当前日期: {current_date_str}')
        rebalance(ContextInfo)
        ContextInfo.last_month = current_month

def rebalance(ContextInfo):
    # 1. 获取历史行情数据
    # 我们需要获取过去21天的数据(包含今天),以便计算 (今天收盘 - 20天前收盘) / 20天前收盘
    # dividend_type='front' 使用前复权数据,保证收益率计算准确
    count = ContextInfo.lookback_days + 1
    market_data = ContextInfo.get_market_data_ex(
        ['close'], 
        ContextInfo.stock_list, 
        period='1d', 
        count=count, 
        dividend_type='front'
    )
    
    # 2. 计算涨幅
    momentum_data = []
    for stock in ContextInfo.stock_list:
        if stock in market_data:
            df = market_data[stock]
            # 确保数据长度足够
            if len(df) >= count:
                price_now = df.iloc[-1]['close']      # 最新收盘价
                price_prev = df.iloc[0]['close']      # 20天前的收盘价
                
                # 避免除以0或停牌导致的数据异常
                if price_prev > 0 and price_now > 0:
                    pct_change = (price_now - price_prev) / price_prev
                    momentum_data.append({
                        'code': stock,
                        'pct_change': pct_change
                    })
    
    # 如果没有有效数据,直接返回
    if not momentum_data:
        print("未获取到有效行情数据,跳过本次调仓")
        return

    # 3. 排序与筛选
    # 按涨幅从大到小排序
    df_momentum = pd.DataFrame(momentum_data)
    df_momentum = df_momentum.sort_values(by='pct_change', ascending=False)
    
    # 计算买入数量
    target_count = int(len(df_momentum) * ContextInfo.select_ratio)
    if target_count == 0:
        target_count = 1
        
    # 获取目标买入列表(前20%)
    target_list = df_momentum.iloc[:target_count]['code'].tolist()
    
    print(f"本次选股数量: {len(target_list)}, 排名首位: {target_list[0]}")

    # 4. 执行交易
    # 获取当前持仓
    positions = ContextInfo.get_trade_detail_data(ContextInfo.accid, 'stock', 'position')
    current_holding_codes = [pos.m_strInstrumentID + '.' + pos.m_strExchangeID for pos in positions]
    
    # 4.1 卖出逻辑:不在目标列表中的股票全部卖出
    # 这涵盖了“卖出排名后20%”以及“卖出中间排名”的股票,只保留最强的
    for stock in current_holding_codes:
        if stock not in target_list:
            # 目标价值设为0,即清仓
            order_target_value(stock, 0, ContextInfo, ContextInfo.accid)
            print(f"卖出(不在前20%): {stock}")
            
    # 4.2 买入逻辑:对目标列表中的股票进行等权买入
    # 获取当前账户总资产
    account_info = ContextInfo.get_trade_detail_data(ContextInfo.accid, 'stock', 'account')
    if len(account_info) > 0:
        total_asset = account_info[0].m_dBalance
        
        # 计算每只股票的目标持仓市值
        # 留出少量资金防止手续费不足导致废单
        target_value_per_stock = (total_asset * ContextInfo.hold_percent) / len(target_list)
        
        for stock in target_list:
            order_target_value(stock, target_value_per_stock, ContextInfo, ContextInfo.accid)
            print(f"买入/调仓: {stock}, 目标市值: {target_value_per_stock:.2f}")

关键点说明

  1. get_market_data_ex

    • 这是 QMT 获取数据最高效的接口。
    • count=21:因为要计算过去20天的涨幅,我们需要第0天(20天前)和第20天(今天)的数据,所以总共需要取21根K线。
    • dividend_type='front'非常重要。计算收益率必须使用前复权数据,否则分红派息会导致价格跳空,从而计算出错误的暴跌。
  2. 调仓逻辑 (rebalance)

    • 策略的核心在于“持有最强的,卖出不够强的”。
    • 代码中的逻辑是:只要股票不在“前20%”的名单里,就卖出。这比单纯“卖出后20%”更符合动量策略的逻辑,因为它能确保你的资金始终集中在表现最好的股票上。
  3. 交易函数 (order_target_value)

    • 使用 order_target_value(目标价值下单)比指定股数下单更方便。它会自动计算需要买入或卖出多少股才能达到设定的金额。
    • 如果当前已经持有该股票且市值刚好,它不会重复下单;如果市值不够,它会补仓;如果市值多了,它会减仓。
  4. 运行频率

    • 在 QMT 界面运行或回测时,请将策略的运行周期设置为 “日线”
    • 代码中的 if current_month != ContextInfo.last_month: 逻辑保证了虽然每天运行,但只在每个月的第一天触发交易。

使用前准备

  1. 补充数据:在 QMT 客户端顶部菜单 -> 【数据管理】 -> 【历史行情数据】,下载“沪深300”或您所选板块的日线数据
  2. 设置账号:在代码 init 函数中,取消 ContextInfo.set_account 的注释,并填入您的真实资金账号。
  3. 设置股票池:代码默认使用沪深300成分股,您可以在 init 中修改 ContextInfo.stock_list 为自定义的代码列表。

Q&A

Q: 为什么不直接用 order_target_percent
A: order_target_percent 在回测中很好用,但在实盘中,如果账户中有非策略持仓(比如你自己手动买的股票),直接用百分比可能会干扰其他持仓。使用 order_target_value 基于总资产计算具体金额通常更稳健。

Q: 如何改成每周调仓?
A: 可以将 handlebar 中的换月判断逻辑修改为判断周。例如使用 timetag_to_datetime 获取当前是周几,如果是周一且上一次交易不是周一,则触发调仓。

Q: 为什么卖出逻辑是“不在目标列表中就卖出”?
A: 题目要求是“卖掉排名后20%,买入排名前20%”。如果只执行这个操作,那么中间60%的股票会一直滞留在账户里,导致资金被占用,无法最大化利用动量效应。标准的动量策略通常是轮动,即始终持有最强的那部分。我的代码实现的是标准的轮动逻辑。