问题描述
解决方案
这是一个非常经典的量化策略需求。在 PTrade 中实现“持仓满 N 天自动卖出”的策略,核心难点在于如何跨交易日记录持仓天数。
由于策略在实盘或模拟盘中可能会因为服务器重启而中断,内存中的变量(如 g.count)可能会丢失。因此,我们需要利用 Python 的 pickle 模块将持仓天数保存到文件中,实现数据的持久化。
以下是完整的策略代码及详细说明。
策略逻辑说明
- 初始化 (
initialize): 设定要操作的股票和持仓天数阈值 $N$。尝试读取本地保存的持仓天数文件,如果文件不存在,则初始化一个空字典。 - 盘前处理 (
before_trading_start): 每天开盘前,将所有在持仓字典中的股票天数加 1。 - 盘中处理 (
handle_data):- 买入逻辑: 如果当前没有持仓,则买入股票,并将该股票记入持仓字典,天数设为 0(或 1,视具体需求而定)。
- 卖出逻辑: 检查持仓字典,如果某只股票的持仓天数达到 $N$ 天,则执行卖出操作,并从字典中移除该记录。
- 数据保存: 每次操作后或每日结束时,将最新的持仓字典保存到文件中,防止数据丢失。
PTrade 策略代码实现
import pickle
from collections import defaultdict
def initialize(context):
"""
初始化函数,设置股票池、全局变量和加载持久化数据
"""
# 1. 设置要操作的股票
g.security = '600570.SS'
set_universe(g.security)
# 2. 设置持仓天数阈值 (例如:持仓满5天卖出)
g.hold_days_limit = 5
# 3. 获取研究路径,用于存放持久化文件
# 注意:不要使用 import os,直接使用 PTrade 提供的 get_research_path()
g.file_path = get_research_path() + 'holding_days_record.pkl'
# 4. 尝试加载之前的持仓天数记录
try:
with open(g.file_path, 'rb') as f:
g.hold_days_dict = pickle.load(f)
log.info("成功加载持仓记录: %s" % g.hold_days_dict)
except:
# 如果文件不存在或读取失败,初始化一个新的字典
# key为股票代码,value为持仓天数
g.hold_days_dict = defaultdict(int)
log.info("未找到持仓记录,初始化为空")
def before_trading_start(context, data):
"""
盘前处理:每天开盘前,将记录中的持仓天数 +1
"""
# 遍历字典中的所有股票,天数加1
# 注意:这里需要判断一下实际持仓,防止字典里有记录但实际仓位已为0的情况(比如手工卖出)
current_positions = context.portfolio.positions.keys()
# 使用 list() 转换 keys,避免在遍历时修改字典导致报错
for stock in list(g.hold_days_dict.keys()):
if stock in current_positions:
g.hold_days_dict[stock] += 1
log.info("股票 %s 持仓天数增加至: %d" % (stock, g.hold_days_dict[stock]))
else:
# 如果账户里已经没有该股票(可能被手动卖出),则清除记录
del g.hold_days_dict[stock]
log.info("股票 %s 已无持仓,清除记录" % stock)
def handle_data(context, data):
"""
盘中处理:执行买入和基于时间的卖出逻辑
"""
security = g.security
# --- 卖出逻辑 ---
# 检查是否满足卖出条件(持仓天数 >= N)
if security in g.hold_days_dict:
current_hold_days = g.hold_days_dict[security]
if current_hold_days >= g.hold_days_limit:
# 卖出该股票所有持仓
order_target(security, 0)
log.info("股票 %s 持仓已满 %d 天,执行卖出" % (security, current_hold_days))
# 卖出后,从记录字典中删除
del g.hold_days_dict[security]
# 保存数据到文件
save_data()
return # 卖出后结束本次 handle_data,避免同日买入
# --- 买入逻辑 (示例) ---
# 如果当前没有持仓,且不在持仓记录中,则买入
# 这里仅为示例,实际策略可替换为你的买入信号(如均线金叉等)
curr_position = get_position(security)
if curr_position.amount == 0 and security not in g.hold_days_dict:
# 买入 1000 股
order(security, 1000)
# 记录初始持仓天数,通常设为 0 或 1
# 设为 0 表示买入当天不算持有天数,T+1 为第 1 天
g.hold_days_dict[security] = 0
log.info("买入股票 %s,初始化持仓天数为 0" % security)
# 保存数据到文件
save_data()
def after_trading_end(context, data):
"""
盘后处理:确保每日收盘后数据被保存
"""
save_data()
log.info("盘后数据已保存")
def save_data():
"""
自定义函数:将 g.hold_days_dict 保存到 pickle 文件
"""
try:
with open(g.file_path, 'wb') as f:
pickle.dump(g.hold_days_dict, f)
except Exception as e:
log.error("保存持仓记录失败: %s" % e)
代码关键点解析
-
pickle模块的使用:- PTrade 的策略在服务器重启或隔日运行时,
initialize会重新执行。如果不将变量保存到硬盘文件,g.hold_days_dict会被重置,导致持仓天数归零。 - 使用
pickle.dump保存数据,pickle.load读取数据。
- PTrade 的策略在服务器重启或隔日运行时,
-
get_research_path():- 这是 PTrade 专用的 API,用于获取策略允许读写文件的目录。严禁使用绝对路径或尝试访问系统其他目录。
-
持仓天数的计算逻辑:
- 在
handle_data买入时,将天数设为0。 - 在
before_trading_start(每日开盘前)将天数+1。 - 这意味着:
- T日买入(Day 0)。
- T+1日开盘前变为 Day 1。
- ...
- T+5日开盘前变为 Day 5 -> 触发
handle_data中的卖出逻辑。
- 在
-
异常处理与数据清洗:
- 在
before_trading_start中增加了一个判断:if stock in current_positions。这是为了防止你在交易终端手动卖出了股票,但策略代码里的字典还记录着它,导致逻辑错乱。
- 在
Q&A 常见问题解答
Q: 为什么不能直接用 g.count 累加,而要用文件保存?
A: PTrade 的实盘/模拟盘环境在每天收盘后或服务器维护时可能会重启进程。重启后,内存中的全局变量 g 会被重置为 initialize 中的初始状态。为了保证策略逻辑跨日连续(即“持久化”),必须将关键数据写入文件。
Q: before_trading_start 和 handle_data 里的逻辑顺序有影响吗?
A: 有影响。通常建议在 before_trading_start 中增加天数。因为该函数在每天 9:00-9:30 之间运行一次,非常适合做“过夜”后的状态更新。如果在 handle_data 中增加天数,需要非常小心控制频率,因为 handle_data 可能会每分钟甚至每 3 秒运行一次。
Q: 如果我想按分钟级别回测,这个策略适用吗?
A: 适用。代码中的 before_trading_start 依然是按天运行的(每个交易日早晨一次)。无论你的回测频率是分钟还是日线,持仓天数的逻辑都是以“自然交易日”为单位增加的,不会受分钟级频率影响。
Q: 如何修改为“持仓满 N 个交易日”而不是自然日?
A: 上述代码实现的正是交易日逻辑。因为 before_trading_start 只在交易日运行。如果中间隔了周末或节假日,函数不会执行,计数器不会增加,完全符合股市的 T+N 交易日概念。