问题描述
解决方案
在 PTrade 量化交易平台中,before_trading_start 是一个非常关键的事件函数。它在每个交易日的开盘前运行(回测模式下通常为 08:30,实盘模式下通常为 09:10 或策略启动时)。
利用这个时间窗口进行准备工作,可以显著提高策略的运行效率和逻辑的严谨性。以下是在 before_trading_start 中最合适做的几类准备工作:
1. 更新和过滤股票池 (最核心用途)
这是该函数最常见的用途。由于指数成分股会调整,或者股票状态(如停牌、ST)会发生变化,因此需要在每日开盘前刷新目标股票池。
- 获取成分股:调用
get_index_stocks获取最新的指数成分股(如沪深300)。 - 剔除异常状态:使用
filter_stock_by_status或get_stock_status剔除 ST、停牌、退市整理期的股票,防止买入无法交易或高风险的标的。 - 设置 Universe:调用
set_universe将清洗后的列表设置为当天的股票池。
2. 预计算技术指标 (提升效率)
如果你的策略依赖于日线级别的技术指标(如日均线、ATR、MACD 等),不要在盘中每分钟(handle_data)都去重复计算。
- 获取历史数据:调用
get_history获取过去 N 天的收盘价。 - 计算指标:计算出昨天的 MA5、MA10 等指标,并存储在全局变量
g中。盘中只需直接调用这些计算好的值,能大幅减少计算量,避免回测变慢或实盘延迟。
3. 重置每日计数器 (逻辑控制)
很多策略有“每日限制”逻辑,例如“每天最多买入 5 只股票”或“单只股票每天只交易一次”。
- 清空列表/字典:在
before_trading_start中将这些计数变量(如g.bought_today = [])重置为空,确保新的一天逻辑重新开始。
4. 获取基本面/财务数据
财务数据(如市盈率、净利润增长率)通常不会在盘中变化。
- 查询财务数据:调用
get_fundamentals获取最新的财务因子。由于该函数涉及跨表查询,开销较大,强烈建议仅在盘前调用一次,而不是在盘中频繁调用。
5. 盘前风控检查
- 资金检查:检查账户可用资金是否充足。
- 持仓核对:在实盘中,可以核对本地记录的持仓与柜台返回的实际持仓是否一致。
综合代码示例
以下是一个标准的 before_trading_start 实现模板,涵盖了上述主要功能:
def initialize(context):
# 初始化全局变量
g.stock_pool = []
g.ma_data = {}
g.bought_today = [] # 记录当天已买入的股票
# 设定基准
set_benchmark('000300.SS')
def before_trading_start(context, data):
# 1. 【重置每日变量】
g.bought_today = []
log.info("每日盘前初始化:清空当日买入记录")
# 2. 【更新股票池】
# 获取沪深300成分股
index_stocks = get_index_stocks('000300.SS')
# 过滤掉 ST、停牌、退市的股票
# filter_stock_by_status 默认过滤 ST, HALT, DELISTING
valid_stocks = filter_stock_by_status(index_stocks)
# 进一步过滤:比如只保留上市超过90天的股票(需自定义逻辑,此处略)
g.stock_pool = valid_stocks
# 设置为当天的 Universe,供 handle_data 使用
set_universe(g.stock_pool)
log.info("今日股票池数量: %d" % len(g.stock_pool))
# 3. 【预计算技术指标】
# 批量获取股票池中所有股票过去 20 天的收盘价
# 注意:count=21 是为了计算 20日均线,需要包含今天之前的20个数据点
history_data = get_history(21, '1d', 'close', g.stock_pool, fq='pre', include=False)
# 计算每只股票的 MA20 并存入全局字典
# 注意:get_history 返回的数据结构处理
g.ma_data = {}
# 遍历股票池计算
for stock in g.stock_pool:
# 提取该股票的收盘价序列
# 注意:根据 PTrade 版本和 get_history 返回类型(DataFrame 或 Panel),取值方式可能略有不同
# 这里假设返回的是 DataFrame 且包含 code 列,或者直接通过 query 查询
try:
# 仅 Python 3.11+ 支持多股查询返回 DataFrame 包含 code 列
# 如果是旧版本或单股循环,逻辑如下:
closes = history_data[stock] # 假设返回的是 dict 或 panel 结构
if len(closes) >= 20:
ma20 = closes[-20:].mean()
g.ma_data[stock] = ma20
except Exception as e:
# 容错处理,防止某只股票数据缺失导致策略崩溃
continue
log.info("完成 %d 只股票的 MA20 预计算" % len(g.ma_data))
# 4. 【获取基本面数据】(可选)
# 获取市盈率小于 50 的股票列表,用于盘中进一步筛选
df_fund = get_fundamentals(g.stock_pool, 'valuation', 'pe_ttm', date=context.blotter.current_dt)
if df_fund is not None and not df_fund.empty:
# 筛选 PE < 50 的股票
g.low_pe_stocks = df_fund[df_fund['pe_ttm'] < 50].index.tolist()
else:
g.low_pe_stocks = []
def handle_data(context, data):
# 盘中只需直接使用 g.ma_data 和 g.low_pe_stocks
pass
总结
在 before_trading_start 中做准备工作的核心原则是:将所有不需要实时更新的计算任务、数据查询任务和状态重置任务都放在这里完成。 这样可以让盘中高频运行的 handle_data 函数保持轻量和快速。
Q&A
Q1: before_trading_start 里可以下单交易吗?
A: 通常不可以。在回测中,该函数运行时间(08:30)早于交易所开盘时间;在实盘中,虽然部分券商支持隔夜单或早盘委托,但 PTrade 的撮合机制主要在 handle_data 或 tick_data 中进行。建议将交易逻辑放在盘中函数,盘前仅做数据准备。
Q2: 为什么不把 get_index_stocks 放在 initialize 里?
A: initialize 只在策略启动的第一天运行一次。如果你在 initialize 里获取指数成分股,那么随着回测时间的推移,你的股票池永远是几年前的旧名单,无法捕捉到新纳入指数的优质股票或剔除被调出的股票。因此必须在 before_trading_start 中每日更新。
Q3: 在实盘模式下,before_trading_start 获取的 get_history 数据包含今天吗?
A: 不包含。before_trading_start 运行时,当天的行情尚未开始(或刚开始),get_history(..., include=False) 获取的是截止到昨天收盘的数据。这是计算日线级别指标(如昨日收盘价、昨日均线)的最佳时机。