股票与 ETF / 行情与历史数据

股票与 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 datakeys()items()values() 或直接遍历。要遍历行情,就遍历自己的证券列表,再逐个取值。

bar 用属性访问,写 bar.close,不写 bar['close']

二、Bar 字段

所有 Bar 都有以下通用字段:

字段 类型、单位 含义
symbol 字符串 完整证券代码
openhighlowclose 数值,元/股或元/份 当前 Bar 的开、高、低、收盘价
last 数值,元/股或元/份 当前采用的开盘或收盘参考价,不是独立的逐笔最新价
volume 数值,股或份 当前 Bar 成交量;回测撮合按它消耗可成交数量

股票与 ETF 的扩展字段命名不同,按标的类型取用:

含义 股票 ETF 日线
前收盘价 preclose,元/股 pre_close,元/份
成交金额 turnover,元 amount,元
涨跌停价 limit_uplimit_down0 表示未提供 limit_uplimit_down;使用前检查是否有有效值
交易状态 仅日线有 trade_status

分钟行情另有 date(整数 YYYYMMDD)、trade_date(字符串 YYYYMMDD)和 time(整数 HHMMSS)。通用的日期判断优先用 context.trade_datecontext.trade_time,不要在同时支持日线和分钟的策略里直接访问日线专属字段。

停牌、涨跌停和无成交量时,即使有 Bar 也不保证委托能成交。

三、校验价格

日线缺失可能返回 None,分钟缺失也可能返回价格为零的对象。下面的函数只接受有限的正价格,行情对象缺失或价格无效时返回 None,调用方跳过这次决策即可:

python
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 分开查询,再按需合并。

python
# 股票日线
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 symbolstart_dateend_datetype='stock'factorsindex_component 因子 DataFrame,使用前检查 symboldate 和所请求的因子列
panda_data.get_prev_trade_date dateexchange='SH'n=1 指定日期之前第 n 个交易日
panda_data.get_trade_cal start_dateend_dateexchange='SH'is_trading_dayfields 交易日历 DataFrame

参数说明:

参数 用法
symbol 完整证券代码或代码列表,建议明确传入研究范围
start_dateend_date 查询起止日期,YYYYMMDD 字符串
fields 返回列名列表,至少包含后续计算和时间筛选需要的列
exchange 基金日线的市场过滤条件,可省略
frequency 历史股票分钟支持 '1m''5m''15m''60m',ETF 分钟支持 '1m''5m''10m''60m'
time_zone 每日盘中时间段,如 ('10:00', '11:00'),包含两端、结束须晚于开始;不是时区。完整交易日保留 None
indicatorst 股票日线筛选参数;默认 indicator='' 不筛指数成分,st=True 表示包含 ST 股票
factors get_factor 必填;可用基础因子 close 构造自己的收益率信号
说明

数据查询的分钟频率写小写 1m,回测任务自身的分钟频率写大写 1M,是两个不同参数的取值,别写串。

五、使用返回的 DataFrame

常用列是 symboldateopenhighlowclosevolume,分钟表另有 minute。实际列由请求和数据接口决定:先检查 emptycolumns,再排序、筛选或计算;不要直接假设最后一行就是最新记录,也不能把 DataFrame 当成 data[symbol] 的 Bar 对象使用。

无数据时应保持现有持仓或跳过决策,不能把查询失败解释为「目标清仓」。查询股票池和因子时使用同一历史时点,避免用当前成分股替代历史成分股。

六、预加载历史行情

研究所需的历史行情,在初始化时把回测区间和预热区间一并准备好;盘前和行情回调只筛选已经准备好的表,不重复请求。单次查询范围有限制:

  • 基金日线、分钟和申赎清单接口单次不超过一年;
  • 股票日线单次不超过五年,股票分钟还受证券数量与区间大小约束。

较长的区间分段查询后合并。下面的片段分段准备股票与 ETF 的原始日收盘价:在 initialize 中配置完 context.symbols 后调用 load_daily_history(context, 预热开始日期)。它不自动生成交易信号;若计算跨分红、折算日期的收益或指标,应改查所需复权口径的行情。

python
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'])。它只查询上一交易日的因子表,不提交交易:

python
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()