问题描述
解决方案
这是一个非常经典的时间止损策略。在 QMT 中实现这个逻辑,我们需要利用 get_trade_detail_data 获取持仓信息(包括建仓日期),然后结合 get_trading_dates 计算持仓天数,最后判断盈亏状态进行卖出。
以下是完整的策略代码及详细说明。
策略逻辑说明
- 获取持仓:读取当前账户的所有持仓。
- 计算天数:获取每只股票的建仓日期(
m_strOpenDate),计算从建仓日到当前日期的交易日数量。 - 判断条件:
- 条件 A:持仓交易日数量 > 3天。
- 条件 B:浮动盈亏 $\le$ 0(即没有上涨,或者亏损)。
- 执行卖出:如果同时满足 A 和 B,则以最新价市价卖出全部持仓。
QMT 策略代码
# -*- coding: gbk -*-
import time
def init(ContextInfo):
# -------------------------------------------------------
# 1. 设置资金账号 (请务必修改为您自己的资金账号)
# -------------------------------------------------------
ContextInfo.accID = '600000248'
ContextInfo.accountType = 'STOCK' # 账号类型:'STOCK'股票, 'FUTURE'期货
# 绑定账号,用于接收回报和获取持仓
ContextInfo.set_account(ContextInfo.accID)
print("策略初始化完成,监控持仓中...")
def handlebar(ContextInfo):
# -------------------------------------------------------
# 2. 获取当前时间和持仓信息
# -------------------------------------------------------
# 获取当前K线的时间戳,并转为 YYYYMMDD 格式字符串
index = ContextInfo.barpos
timetag = ContextInfo.get_bar_timetag(index)
current_date_str = timetag_to_datetime(timetag, '%Y%m%d')
# 获取当前账户的持仓列表
# 参数:账号ID, 账号类型, 数据类型('POSITION'为持仓)
positions = get_trade_detail_data(ContextInfo.accID, ContextInfo.accountType, 'POSITION')
# 如果没有持仓,直接返回
if not positions:
return
# -------------------------------------------------------
# 3. 遍历持仓进行判断
# -------------------------------------------------------
for pos in positions:
stock_code = pos.m_strInstrumentID # 证券代码
open_date = pos.m_strOpenDate # 建仓日期 (字符串格式 YYYYMMDD)
volume = pos.m_nVolume # 持仓数量
can_use_volume = pos.m_nCanUseVolume # 可用数量 (防止卖出当日买入的股票)
# 如果可用持仓为0,说明是今日新买的,跳过
if can_use_volume == 0:
continue
# --- 计算持仓天数 ---
# 获取从建仓日(open_date)到当前日期(current_date_str)之间的交易日列表
# 注意:需要确保本地有下载对应的日线数据,否则 get_trading_dates 可能返回空
trading_days_list = ContextInfo.get_trading_dates(stock_code, open_date, current_date_str, -1, '1d')
if not trading_days_list:
# 如果获取不到交易日数据,无法计算天数,跳过
continue
days_held = len(trading_days_list)
# --- 判断是否上涨 ---
# 使用 m_dFloatProfit (浮动盈亏) 来判断
# 如果浮动盈亏 <= 0,视为"不涨" (包含保本和亏损)
is_not_rising = (pos.m_dFloatProfit <= 0)
# 也可以使用价格判断:现价 <= 成本价
# is_not_rising = (pos.m_dLastPrice <= pos.m_dOpenPrice)
# -------------------------------------------------------
# 4. 执行卖出逻辑
# -------------------------------------------------------
# 逻辑:持仓超过3个交易日 且 不涨
if days_held > 3 and is_not_rising:
print(f"触发止损: {stock_code}, 建仓日: {open_date}, 持仓天数: {days_held}, 浮动盈亏: {pos.m_dFloatProfit}")
# 执行下单:卖出
# opType=24 (卖出), orderType=1101 (单股单账号普通下单)
# priceType=5 (最新价), price=-1 (最新价模式下价格填-1即可)
passorder(24, 1101, ContextInfo.accID, stock_code, 5, -1, can_use_volume, ContextInfo)
代码关键点解析
-
get_trade_detail_data:- 这是获取账户真实持仓的核心函数。
- 返回的对象
pos中,m_strOpenDate属性记录了该笔持仓的建仓日期,这是计算持仓时间的关键。
-
ContextInfo.get_trading_dates:- 我们不能简单地用
当前日期 - 建仓日期,因为中间可能包含周末和节假日。 - 使用此函数获取两个日期之间的实际交易日列表,列表的长度 (
len) 就是真实的持仓交易天数。
- 我们不能简单地用
-
pos.m_dFloatProfit:- 这是持仓对象的“浮动盈亏”属性。
- 代码中
pos.m_dFloatProfit <= 0意味着当前是亏损或者持平状态,符合你“不涨”的定义。
-
passorder参数:24: 代表股票卖出。1101: 代表普通交易模式。5: 代表报价类型为“最新价”。can_use_volume: 使用可用持仓量,避免T+1规则下卖出当日买入的股票导致报错。
使用前注意事项
- 修改账号:请务必将
init函数中的ContextInfo.accID修改为你实际的资金账号。 - 数据下载:
get_trading_dates依赖本地的基础日线数据。请在 QMT 客户端顶部菜单点击 “数据管理” -> “下载历史数据”,勾选日线数据进行补充,否则计算天数可能会出错。 - 运行模式:此代码适用于实盘或模拟盘模式(因为用到了
get_trade_detail_data读取账户信息)。如果是纯历史回测,逻辑需要稍作调整(使用虚拟持仓变量)。
Q&A
Q: 这里的“3天”是指自然日还是交易日?
A: 代码中使用的是 get_trading_dates,所以计算的是交易日。如果你周五买入,周六周日不算时间,周一算第2天。如果你想用自然日,可以直接解析日期字符串相减。
Q: 如果我想改成“盈利少于 1% 也卖出”怎么办?
A: 可以修改判断条件。
将 is_not_rising = (pos.m_dFloatProfit <= 0)
改为 is_not_rising = (pos.m_dFloatProfit / pos.m_dMarketValue < 0.01) (即盈亏比例小于1%)。
Q: 为什么使用 can_use_volume 而不是 volume?
A: A股是 T+1 制度,当天买入的股票当天不能卖出。m_nVolume 是总持仓,m_nCanUseVolume 是可用持仓(昨仓)。使用可用持仓可以防止程序试图卖出今仓而报错。