股票与 ETF
行情与历史数据
在股票与 ETF 回测策略中读取当前行情:data 对象的正确用法、Bar 字段与单位、有效价格校验;用 panda_data 查询股票与 ETF 历史行情、因子与交易日历,以及分段预加载与避免未来信息。
策略里有两种数据:handle_data 收到的当前行情(Bar 对象),以及用 panda_data 查询的历史数据(DataFrame)。两者字段名不完全一样,不能混用。
一、读取当前行情
在 handle_data(context, data) 中用 bar = data[symbol] 或 data.get(symbol) 读取当前行情,股票和 ETF 写法相同,不需要先调用订阅函数。
data 不是普通字典:get 只接收一个参数、不支持默认值;不能用 symbol in data、keys()、items()、values() 或直接遍历。要遍历行情,就遍历自己的证券列表,再逐个取值。
bar 用属性访问,写 bar.close,不写 bar['close']。
二、Bar 字段
所有 Bar 都有以下通用字段:
| 字段 | 类型、单位 | 含义 |
|---|---|---|
symbol |
字符串 | 完整证券代码 |
open、high、low、close |
数值,元/股或元/份 | 当前 Bar 的开、高、低、收盘价 |
last |
数值,元/股或元/份 | 当前采用的开盘或收盘参考价,不是独立的逐笔最新价 |
volume |
数值,股或份 | 当前 Bar 成交量;回测撮合按它消耗可成交数量 |
股票与 ETF 的扩展字段命名不同,按标的类型取用:
| 含义 | 股票 | ETF 日线 |
|---|---|---|
| 前收盘价 | preclose,元/股 |
pre_close,元/份 |
| 成交金额 | turnover,元 |
amount,元 |
| 涨跌停价 | limit_up、limit_down;0 表示未提供 |
limit_up、limit_down;使用前检查是否有有效值 |
| 交易状态 | 仅日线有 trade_status |
— |
分钟行情另有 date(整数 YYYYMMDD)、trade_date(字符串 YYYYMMDD)和 time(整数 HHMMSS)。通用的日期判断优先用 context.trade_date、context.trade_time,不要在同时支持日线和分钟的策略里直接访问日线专属字段。
停牌、涨跌停和无成交量时,即使有 Bar 也不保证委托能成交。
三、校验价格
日线缺失可能返回 None,分钟缺失也可能返回价格为零的对象。下面的函数只接受有限的正价格,行情对象缺失或价格无效时返回 None,调用方跳过这次决策即可:
from math import isfinite
def current_close(data, symbol):
try:
bar = data[symbol]
except KeyError:
return None
if bar is None:
return None
value = bar.close
if value is None:
return None
try:
price = float(value)
except (TypeError, ValueError):
return None
return price if isfinite(price) and price > 0 else None
普通行情价格用于成交参考;复权价格用于选股、收益或指标计算,不作为限价委托的价格。
四、历史数据接口
先 import panda_data。查询返回 pandas.DataFrame(交易日查询返回日期字符串)。股票和 ETF 分开查询,再按需合并。
# 股票日线
panda_data.get_stock_daily(
symbol="", start_date="", end_date="",
fields=None, indicator="", st=True, **kwargs
)
# 股票分钟
panda_data.get_stock_min(
symbol="", start_date="", end_date="",
fields=None, time_zone=None, frequency="1m"
)
# ETF 日线:不复权、后复权、前复权
panda_data.get_fund_daily(
start_date, end_date, symbol=None, exchange=None, fields=None
)
panda_data.get_fund_daily_post(
start_date, end_date, symbol=None, exchange=None, fields=None
)
panda_data.get_fund_daily_pre(
start_date, end_date, symbol=None, exchange=None, fields=None
)
# ETF 分钟
panda_data.get_fund_min(
start_date="", end_date="", symbol=None,
fields=None, time_zone=None, frequency="1m", **kwargs
)
股票还常用下面三个接口:
| 函数 | 常用参数 | 返回 |
|---|---|---|
panda_data.get_factor |
symbol、start_date、end_date、type='stock'、factors、index_component |
因子 DataFrame,使用前检查 symbol、date 和所请求的因子列 |
panda_data.get_prev_trade_date |
date、exchange='SH'、n=1 |
指定日期之前第 n 个交易日 |
panda_data.get_trade_cal |
start_date、end_date、exchange='SH'、is_trading_day、fields |
交易日历 DataFrame |
参数说明:
| 参数 | 用法 |
|---|---|
symbol |
完整证券代码或代码列表,建议明确传入研究范围 |
start_date、end_date |
查询起止日期,YYYYMMDD 字符串 |
fields |
返回列名列表,至少包含后续计算和时间筛选需要的列 |
exchange |
基金日线的市场过滤条件,可省略 |
frequency |
历史股票分钟支持 '1m'、'5m'、'15m'、'60m',ETF 分钟支持 '1m'、'5m'、'10m'、'60m' |
time_zone |
每日盘中时间段,如 ('10:00', '11:00'),包含两端、结束须晚于开始;不是时区。完整交易日保留 None |
indicator、st |
股票日线筛选参数;默认 indicator='' 不筛指数成分,st=True 表示包含 ST 股票 |
factors |
get_factor 必填;可用基础因子 close 构造自己的收益率信号 |
数据查询的分钟频率写小写 1m,回测任务自身的分钟频率写大写 1M,是两个不同参数的取值,别写串。
五、使用返回的 DataFrame
常用列是 symbol、date、open、high、low、close、volume,分钟表另有 minute。实际列由请求和数据接口决定:先检查 empty、columns,再排序、筛选或计算;不要直接假设最后一行就是最新记录,也不能把 DataFrame 当成 data[symbol] 的 Bar 对象使用。
无数据时应保持现有持仓或跳过决策,不能把查询失败解释为「目标清仓」。查询股票池和因子时使用同一历史时点,避免用当前成分股替代历史成分股。
六、预加载历史行情
研究所需的历史行情,在初始化时把回测区间和预热区间一并准备好;盘前和行情回调只筛选已经准备好的表,不重复请求。单次查询范围有限制:
- 基金日线、分钟和申赎清单接口单次不超过一年;
- 股票日线单次不超过五年,股票分钟还受证券数量与区间大小约束。
较长的区间分段查询后合并。下面的片段分段准备股票与 ETF 的原始日收盘价:在 initialize 中配置完 context.symbols 后调用 load_daily_history(context, 预热开始日期)。它不自动生成交易信号;若计算跨分红、折算日期的收益或指标,应改查所需复权口径的行情。
import pandas as pd
import panda_data
from datetime import datetime, timedelta
def load_daily_history(context, warmup_start):
fields = ['symbol', 'date', 'close']
start = datetime.strptime(str(warmup_start), '%Y%m%d')
end = datetime.strptime(str(context.run_info.end_date), '%Y%m%d')
tables = []
available_markets = set()
while start <= end:
chunk_end = min(start + timedelta(days=179), end)
params = {'start_date': start.strftime('%Y%m%d'),
'end_date': chunk_end.strftime('%Y%m%d'), 'fields': fields}
stock = panda_data.get_stock_daily(symbol=STRATEGY_PARAMS['stock_symbols'], **params)
etf = panda_data.get_fund_daily(symbol=STRATEGY_PARAMS['etf_symbols'], **params)
for market, table in (('stock', stock), ('etf', etf)):
if table is None:
raise ValueError('研究行情为空或查询没有返回结果')
if table.empty:
continue
if not set(fields).issubset(table.columns):
raise ValueError('研究行情缺少 symbol、date 或 close 列')
tables.append(table)
available_markets.add(market)
start = chunk_end + timedelta(days=1)
if available_markets != {'stock', 'etf'}:
raise ValueError('研究行情为空,请检查证券和日期范围')
history = pd.concat(tables, ignore_index=True)
history['date'] = (
history['date'].astype(str)
.str.replace('-', '', regex=False)
.str.replace(r'\.0$', '', regex=True)
)
context.daily_history = history.sort_values(['symbol', 'date'])
def prior_daily_prices(context, symbol, count):
history = context.daily_history
visible = history[
(history['symbol'] == symbol)
& (history['date'] < str(context.trade_date))
]
prices = visible['close'].tail(count)
return prices if len(prices) == count else None
七、按时点取用,避免未来信息
盘前信号只用 date < 当日交易日 的完整日线。分钟信号先确认时间标签的含义,再筛选已经完成的分钟。窗口不足时就等待,不要用后续记录补齐;例如计算 20 个日收益,通常需要 21 个有效收盘价。
净值、申赎清单、现金差额等业务数据,还要按实际公开时点使用,不能仅凭记录日期就判断盘中已经可见。
下面的辅助函数可在 before_trading 中调用,例如 load_previous_close(context, ['600000.SH'])。它只查询上一交易日的因子表,不提交交易:
import panda_data
def load_previous_close(context, symbols):
date = panda_data.get_prev_trade_date(date=str(context.trade_date), exchange="SH", n=1)
if not date or date == "None":
raise ValueError("上一交易日不可用")
frame = panda_data.get_factor(symbol=symbols, start_date=date, end_date=date,
type="stock", factors=["close"])
if frame is None or frame.empty:
return frame
if not {"symbol", "date", "close"}.issubset(frame.columns):
raise ValueError("因子返回缺少必需字段")
return frame[frame["date"].astype(str) == date].copy()
