3. 使用 HistoryPanel 操作和分析历史数据

在完成前两章「入门」和「获取并管理金融数据」之后,我们已经可以:

  • 在本地配置好数据源并下载所需的历史数据;

  • 使用 get_history_data()get_history_panel() 获取价格、成交量等基础行情。

本章将在此基础上,通过一个完整的小例子,演示如何使用 HistoryPanel

  • 统一管理多标的、多指标的历史数据;

  • 计算收益率、波动率等简单因子;

  • 叠加 K 线技术指标;

  • 识别部分蜡烛形态;

  • 使用 where 生成与数据同形的 bool 掩码,为后续带 mask= 的研究接口做准备;

  • 使用 列属性(如 hp.close)、比较运算(返回 numpy 布尔数组,可与 where 衔接)以及 hp.loc(仅沿时间轴筛选,等价 hp[:, :, key])。

更完整的 API 说明可参考 HistoryPanel API 参考(含「列属性访问、比较与 loc」与 where 专节)。


3.1. 1. 准备数据:获取一个 HistoryPanel

假设我们已经下载了沪深 300 指数(000300.SH)近一年的日线数据,可以直接通过 qt.get_kline()qt.get_history_data() 获取一个 HistoryPanel

import qteasy as qt

# 获取近一年日线 OHLCV,并直接返回 HistoryPanel
hp = qt.get_kline(
    shares='000300.SH',
    start='20230101',
    end='20231231',
    freq='D',
    as_panel=True,              # 关键:返回 HistoryPanel
)

print('HistoryPanel 结构:', hp.shape)
print('shares:', hp.shares)
print('htypes:', hp.htypes)

此时:

  • hp.shape 是一个 (L, R, C) 的三元组,分别对应 股票数 × 时间长度 × 数据类型数

  • hp.shares 通常是 ['000300.SH']

  • hp.htypes 默认包含 ['open', 'high', 'low', 'close', 'vol'] 等。


3.2. 2. 计算收益率与波动率因子

HistoryPanel 内置了针对价格序列的收益与波动率计算接口:

# 2.1 计算简单收益率(simple return)
ret = hp.returns(
    price_htype='close',
    method='simple',    # 或 'log'
    periods=1,
    as_panel=False,     # 返回 DataFrame: index=时间, columns=shares
)
print('日收益率:\n', ret.tail())

# 2.2 在收益率基础上计算 20 日滚动波动率
vol = hp.volatility(
    window=20,
    price_htype='close',
    method='simple',
    annualize=True,     # 年化波动率
    as_panel=False,     # 同样返回 DataFrame
)
print('20 日年化波动率:\n', vol.tail())

典型用法是:

  • retvol 当作因子值矩阵(时间 × 股票);

  • 直接送入回测引擎,或用于选股、打分等。


3.3. 3. 叠加 K 线技术指标

HistoryPanel 中包含 OHLC 数据时,可以通过 hp.kline 访问一批常用技术指标:

# 基于 close 价格生成 20 日简单移动平均线
hp_ma = hp.kline.sma(window=20, price_htype='close')
print('新增 htypes:', hp_ma.htypes)

# 再叠加 MACD 指标
hp_ma_macd = hp_ma.kline.macd(price_htype='close')
print('叠加 MACD 后的 htypes:', hp_ma_macd.htypes)

# 将单只股票切成 DataFrame,方便在 pandas / sklearn 中继续处理
df_300 = hp_ma_macd.to_share_frame('000300.SH')
print(df_300.tail())

这里:

  • hp.kline.sma(...) 会在 htypes 中追加一列,例如 sma_20

  • hp.kline.macd(...) 会追加 macd_xxxmacd_signal_xxxmacd_hist_xxx 三列;

  • 最终通过 to_share_frame(share) 得到标准的二维 DataFrame,可直接与其他数据源联立分析。

3.1 inplace:原地扩列(减少拷贝)

默认情况下,kline.* 返回 HistoryPanel(不污染原对象)。当你希望在同一个面板上连续追加多列,并减少中间对象时,可以使用 inplace=True

# 在原面板上原地追加两列,并返回原面板(支持链式)
hp2 = hp.kline.sma(window=20, inplace=True).kline.macd(inplace=True)
assert hp2 is hp
print('原地扩列后的 htypes:', hp.htypes)

提示:当新列名与已有列名冲突时,kline.* 会抛出英文 ValueError,避免静默覆盖。

3.2 第一入口:research_preset(一行拼列 + 直接出图)

如果你希望“拿到数据后立刻得到一张像样的图”,推荐直接使用 research_preset 生成官方预设的常用列集合(预设只做显式追加,不在 plot() 内隐藏计算):

# 生成 OHLCV + MACD + SMA20(预设名以 API 文档为准)
hp_preset = hp.research_preset('ohlcv_macd_ma', inplace=False)
print('preset htypes tail:', hp_preset.htypes[-8:])

# 多标的时可分页展示(不再静默只画前 5 只)
fig = hp_preset.plot(shares=hp_preset.shares, max_shares_per_figure=3, page=1)

当输入面板缺少预设所需的输入列时,会抛出英文 ValueError,信息包含缺失列列表与补齐建议。


3.4. 4. 使用 assign 一次派生多列

在实际研究中,经常需要基于已有列派生出多个新因子,例如价差、比例、缩放后的归一化值等。HistoryPanel.assign() 提供了一次性派生多列的 DSL:

# 假设 hp 已含 'close' 和 'open' 列
hp2 = hp.assign(
    spread=lambda p: p['close'].values[:, :, 0] - p['open'].values[:, :, 0],
    up_ratio=lambda p: p['close'].values[:, :, 0] / np.maximum(p['open'].values[:, :, 0], 1e-6),
)
print('新列:', hp2.htypes[-2:])
  • assign 的关键字参数名称即新列名(htype),值可以是接收当前 HistoryPanel 的可调用对象,也可以是可广播到 (M,L) 的数组或标量;

  • 同一次调用内,后面的列可以依赖前面刚新增的列,例如:

hp3 = hp.assign(
    a=lambda p: p['close'].values[:, :, 0] + 1.0,
    b=lambda p: p['a'].values[:, :, 0] * 2.0,
)

默认 inplace=False,返回新面板,不修改原对象;当你希望在原面板上原地扩列时,可以使用:

hp.assign(inplace=True, factor=lambda p: p['close'].values[:, :, 0] / 100.0)

提示:与 hp['col'] = ... 一样,assign 对已有列名采用“覆盖”语义;空面板上调用 assign 会抛出英文 ValueError


3.5. 5. 横截面 rank 与 zscore(cs/ts)

多标的研究中常见的两类标准化需求是:

  • 截面(cross-sectional):固定某个交易日,对所有股票的因子值做标准化或排名;

  • 时序(time-series rolling):固定某只股票,对它自己的历史序列用滚动窗口做标准化。

HistoryPanel.rank()HistoryPanel.zscore() 提供了这两类最基础变换(仅做变换,不做回归推断):

# 5.1 截面排名:逐日对 share 维做排名
hp_rank = hp.rank(by='close')                  # 追加一列 'rank_close'

# 5.2 截面 zscore:逐日横截面标准化(method='cs')
hp_cs = hp.zscore(by='close', method='cs')     # 追加一列 'cs_z_close'

# 5.3 时序 zscore:逐股滚动标准化(method='ts',需要 window)
hp_ts = hp.zscore(by='close', method='ts', window=20)   # 追加一列 'ts_z_close_20'

提示:zscore(method='cs')zscore(method='ts') 语义完全不同,务必显式指定 method(并在 ts 场景提供 window),避免把“截面标准化”误当成“时序标准化”。

5.5 对齐与重采样(align_to / resample):避免 silent 错行

当你手里有两块 HistoryPanel,想做逐元素运算(比如 hp_factor / hp_price、或两面板相减)时,需要特别注意:

  • HistoryPanel 不会在运算符里自动对齐两块面板;

  • shares / hdates 不一致,直接拿 .values 做 NumPy 运算很容易出现“看起来能算、但其实错行”的结果。

因此建议遵循固定流程:先对齐,再计算

# hp_a 与 hp_b 的 shares/hdates 不一定一致
a1, b1 = hp_a.align_to(hp_b, join='inner')   # 交集对齐:只保留共同 shares 与共同日期
ratio = a1.values / b1.values                # 此时逐元素运算不会错行
print('aligned shape:', a1.shape, b1.shape)

=当你需要把日线变成周线/月线等更低频数据时,可用 resample(),并显式指定每一列的聚合规则(避免语义不明):

# 典型 OHLCV 周线:open=first, high=max, low=min, close=last, vol=sum
hp_w = hp.resample('W', agg={
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'vol': 'sum',
})
print('weekly dates:', hp_w.hdates[:3], '...')

提示:resample() 要求 agg 覆盖全部 htypes。对于非 OHLCV 的自定义因子列,你需要自己决定 'last' / 'mean' 等聚合方式并写进 agg

5.6 用 (M,L) mask 做 plot 高亮(事件日/信号日)

在因子研究里,你经常会得到一个二维布尔矩阵 mask_ml,表示“每只股票在哪些日期满足条件”(例如事件日、信号日、可交易池)。

HistoryPanel.plot()highlight={'condition': mask_ml} 支持直接把它映射到图形中高亮点:

  • mask_ml.shape == (M_plot, L),其中 M_plot 为当前 plot(shares=...) 选中的股票数,则按绘图 share 顺序对应;

  • mask_ml.shape == (M_all, L),其中 M_all == len(hp.shares),则按 share 名抽取当前绘图子集(避免 silent 错行);

  • layout='overlay' 时默认 primary-only 显示高亮(与既有 Plotly highlight 语义一致)。

import numpy as np

M, L, _ = hp.shape
mask_ml = np.zeros((M, L), dtype=bool)
mask_ml[:, -1] = True                      # 举例:每只股票的最后一天高亮

fig = hp.plot(
    shares=hp.shares,
    layout='stack',
    highlight={'condition': mask_ml},
)

5.4 Newey–West / Fama–MacBeth(背景:本节不实现)

在计量实证中,你可能会看到 Newey–West(HAC)稳健标准误Fama–MacBeth 两步回归。它们主要用于回归推断(标准误修正、风险溢价显著性检验),不属于 HistoryPanel.zscore() 这类“基础变换”。 若需要发表级实证,建议将 HistoryPanel 导出为 DataFrame 后使用 statsmodels 等工具完成推断流程。


3.6. 6. 使用 apply_ta 统一调用技术指标

如果你已经熟悉 qteasy.tafuncs 或 ta-lib,可以通过 HistoryPanel.apply_ta() 做统一封装:

# 在所有股票上应用 'sma' 指标,对 close 做 10 日均线
hp_sma10 = hp.apply_ta(
    func_name='sma',
    htype='close',
    timeperiod=10,
    as_panel=True,      # 返回新的 HistoryPanel,追加一列 'sma'
)
print('htypes with sma:', hp_sma10.htypes)

这样可以避免在循环中手动调用 tafuncs.smaHistoryPanel 会自动对所有选定股票广播计算。


3.7. 7. 识别蜡烛形态

对于 OHLC 数据,还可以通过 HistoryPanel.candle_pattern() 封装 ta-lib 的蜡烛形态识别函数,例如:

# 识别锤头线形态(CDLHAMMER),返回时间 × 股票的整数信号矩阵
hammer = hp.candle_pattern(
    name='cdlhammer',
    as_panel=False,          # 返回 DataFrame
)
print('锤头线信号(非 0 代表出现形态):\n', hammer[hammer != 0].dropna(how='all'))

信号含义通常是:

  • 0:未识别到该形态;

  • 正数 / 负数:不同方向或强度的形态信号(取决于具体 ta-lib 函数的定义)。


3.8. 8. 生成研究用 bool 掩码(where

面板 values 的轴顺序为 (标的数 M, 时间长度 L, 指标列数 N),与 hp.shape 一致。hp.where(condition) 返回 同形numpy.ndarraydtype=bool,且不修改 hpcum_returnnormalizeportfolio 已支持 mask=hp.where(...)(或与 where 同广播规则的 bool 数组)。研究向语义与边界(路径断开、基准日无效、组合与基准关系等)以 API docstring 为准。

与整表数值比较(与 hp.values > 阈值 等价):

import numpy as np

mask = hp.where(hp.values > 0)   # 或先写 cond = hp.values > 0 再 hp.where(cond)
assert mask.shape == hp.shape
print('True 格点数量:', int(mask.sum()))

按时间维打标(例如某一列索引为「事件日」,沿所有 htype 复制;以下为示意,需保证 cond_ml 形状为 (M, L)):

M, L, N = hp.shape
cond_ml = np.zeros((M, L), dtype=bool)
cond_ml[:, -1] = True   # 示例:最后一个交易日标记为 True
mask_event = hp.where(cond_ml)
assert mask_event[:, -1, :].all() and not mask_event[:, 0, :].any()

按收盘价列构造条件callable 接收当前 HistoryPanel):

# 假设存在 'close' 列;多列面板可用 p.values[:, :, idx] 或子面板
mask_close = hp.where(lambda p: p['close'].values[:, :, 0] > 0)

复合条件与缺失值

mask_band = hp.where(lambda p: (p.values > -1e9) & (p.values < 1e9))
mask_nan = hp.where(np.isnan(hp.values))   # 标记 NaN 所在格,供排除缺失

说明:形状恰好为 (M, L) 的二维条件表示「每个 (标的, 日) 对所有指标列共用同一布尔值」,库内会先扩展为 (M, L, 1) 再广播到 (M, L, N)。一维 (M,)(M, 1) 表示仅随标的变化,会展开为 (M, 1, 1) 再广播。更完整的参数与异常说明见 HistoryPanel API 参考where 一节。

8.1 累计收益与归一化(cum_return / normalize

返回 HistoryPanel(不修改原面板),默认对解析后的 close 列计算;输出列名为 cumret_<列名>norm_<列名>(与复权列 close|b 并存时,仍可传 htypes=None,列名仍为 cumret_close)。可与掩码联用:

m = hp.where(hp.close > 0)           # 或与 hp.values 比较得到 (M,L,N)
cr = hp.cum_return(mask=m, method='simple')
nm = hp.normalize(base_index=0, mask=m)

8.2 组合聚合(portfolio

研究向:沿 标的维 对指定列做等权或加权平均,返回 HistoryPanelhdates 不变)。mask=where 同广播规则。无 groups 且指定 benchmark 时,基准标的不参与组合计算,仅用于 benchmark_output='tag_along'(多一行基准曲线)或 'excess_only'(输出 excess_<列名>)。非账户回测、无交易成本。详见 API docstring。

ew = hp.portfolio(mode='equal', benchmark_output='none')
m = hp.where(hp.close > 0)
ew_m = hp.portfolio(mode='equal', mask=m, benchmark_output='none')
# 含基准指数代码 'IDX' 时:
# tagged = hp.portfolio(mode='equal', benchmark='IDX', benchmark_output='tag_along', new_share_name='EW')

8.3 列属性、比较与 loc(与 pandas 的差异简述)

  • 列属性(只读):列名为合法 Python 标识符时可用 hp.close,等价 hp['close'];含 | 的复权列名等须用方括号。赋值请统一 hp['col'] = ...

  • 比较hp.close > hp.openhp > 0 等得到 numpy.ndarray(bool),不是子 HistoryPanel;全形掩码可写 hp.where(hp.close > 15)。两侧均为面板时须 shareshdates 一致;两列比较时多为两个 单列 子面板。

  • lochp.loc[s] 等价 hp[:, :, s],只沿 时间(hdates) 轴筛选;不要where 得到的 (M, L, N) 掩码传给 loc,格点条件仍用 where + 后续 mask=

细节与边界情况以 HistoryPanel API 参考 为准(含 wherecum_returnnormalizeportfolio)。

# 示意(在已含 open/close 列的 hp 上)
# m = hp.where(hp.close > hp.open)   # 比较 → bool ndarray → where 广播到 (M,L,N)
# tail = hp.loc[-5:]                 # 仅时间轴,等价 hp[:, :, -5:]

3.9. 9. 从 HistoryPanel 研究到 Strategy / Operator:迁移路径

HistoryPanel 上的 cum_returnportfolioplot 等是研究向工具:便于肉眼筛因子、看粗组合线,不包含交割周期、手续费、滑点、信号类型(PT/PS/VS)解析等与正式回测一致的账户语义。结论若用于实盘或严肃评价,应把同一经济含义的规则写进策略的 realize(),交给 Operator + Backtester。

下面用同一思想——20 期简单动量串起两阶段(需本地已配置数据源并下载相应字段;股票代码仅为示意)。

9.1 研究阶段:在多标的 HistoryPanel 上算因子并粗看截面

import numpy as np
import qteasy as qt

# 多标的日线收盘价(htype 以你本地数据源为准,可为 close 或复权列如 close|b)
hp = qt.get_history_data(
    htypes='close',
    shares=['000001.SZ', '000002.SZ', '600000.SH'],  # 示例股票池
    start='20200101',
    end='20231231',
    freq='d',
)

# 20 期简单收益 r_t = P_t / P_{t-20} - 1,直接得到带标签的子面板
hp_mom = hp.returns(price_htype='close', periods=20, method='simple', as_panel=True)

# 截面排名:逐日看多标的谁强谁弱(研究向)
hp_rank = hp_mom.rank(by='ret_close')

# 可选:等权「组合」在收益列上的逐日平均,仅用于目视,非账户回测
hp_ew = hp_mom.portfolio(htypes='ret_close', mode='equal', benchmark_output='none')

hp_mom.plot(shares=hp_mom.shares, layout='stack')   # 结合前几节 mask 高亮更佳

这里你在 三维面板 上完成了因子列生产、截面变换与粗聚合;若结果满意,进入 9.2 把因子定义搬到策略里。

9.2 正式回测阶段:FactorSorter + Operator

选股类策略常用 FactorSorterrealize() 返回每个标的一个标量因子(长度 = 股票数),框架再按 max_sel_countsort_ascending 等做排序与权重。下面动量与 9.1 中 returns(..., periods=20) 的经济含义一致,但数据来自策略窗口注入(与回测步对齐、受 use_latest_data_cycle 等约束)。

import numpy as np
import qteasy as qt
from qteasy import StgData


class Mom20Factor(qt.FactorSorter):
    """20 期收盘动量:close[-1]/close[0]-1,与 HP 上 returns(periods=20) 语义对齐。"""

    def __init__(self) -> None:
        super().__init__(
            name='Mom20',
            description='20-bar simple momentum on daily close',
            data_types=[StgData('close', freq='d', asset_type='E')],
            window_length=21,
            max_sel_count=3,
            sort_ascending=False,
            condition='any',
            use_latest_data_cycle=True,
        )

    def realize(self):
        close_w = self.get_data('close_E_d')   # 形状约为 (window_length, 股票数)
        out = close_w[-1] / close_w[0] - 1.0
        out = np.where(np.isfinite(out), out, np.nan)
        return out


# 以下为骨架:具体 run 参数(股票池、起止日、费率、signal_type 等)见「回测」与 Operator 文档
op = qt.Operator()
op.add_strategy(Mom20Factor(), run_freq='d', run_timing='close')
# res = qt.run(op, mode=1, ...)  # 按项目文档填写 shares、起止日期与回测配置

注意

  • Operator 默认组内可使用 PT 类目标仓位信号;FactorSorter 与 PT 的搭配以你项目中的策略文档为准。

  • HP 里用全样本算的 rank/zscore,与回测中「每步仅见历史窗口」的因子可能略有数值差异,属预期现象;应以策略内 get_data 为准复核防前视。

完整可运行脚本见项目根目录 examples/historypanel_research_to_strategy.py


3.10. 10. 多源数据拼成 HistoryPanel(价格 + 基本面示例)

HistoryPanel 可以承载任意 htypes 列;多表/多 DataType 的常见做法是:(1) 一次 get_history_data 拉多列;(2) 或分别取两块面板再 align_to

10.1 单次拉取多列(频率与资产类型需一致)

若本地已有日线 closepb(市净率)等字段,可直接:

import numpy as np
import qteasy as qt

hp = qt.get_history_data(
    htypes='close, pb',
    shares=['000001.SZ', '000002.SZ'],
    start='20200101',
    end='20231231',
    freq='d',
)

# 估值倒数近似账面市值比(仅作演示,非投资建议)
hp2 = hp.assign(bp=lambda p: 1.0 / np.maximum(p['pb'].values[:, :, 0], 1e-8))

# 横截面 zscore:逐日可比
hp_z = hp2.zscore(by='bp', method='cs')

缺列或表未下载时,get_history_data 会报错;请先通过数据源教程补齐表数据。

10.2 两块面板对齐后再派生(不同来源/不同拉取)

align_to 要求两侧 htypes 名称与顺序完全一致。若你只分别拉取了 closepb 两列,需先扩成同构列(缺失侧用 NaN 占位),对齐后再用 assign 把有效数值写回同一套列名:

import numpy as np
import qteasy as qt

hp_px = qt.get_history_data(htypes='close', shares=pool, start='20200101', end='20231231', freq='d')
hp_pb = qt.get_history_data(htypes='pb', shares=pool, start='20200101', end='20231231', freq='d')

c = hp_px['close'].values[:, :, 0]
p = hp_pb['pb'].values[:, :, 0]
nan_c = np.full_like(c, np.nan)
nan_p = np.full_like(p, np.nan)

hp_a = qt.HistoryPanel(
    np.stack([c, nan_p], axis=2),
    levels=hp_px.shares,
    rows=hp_px.hdates,
    columns=['close', 'pb'],
)
hp_b = qt.HistoryPanel(
    np.stack([nan_c, p], axis=2),
    levels=hp_pb.shares,
    rows=hp_pb.hdates,
    columns=['close', 'pb'],
)

a, b = hp_a.align_to(hp_b, join='inner')
hp_joined = a.assign(
    close=lambda x: a['close'].values[:, :, 0],
    pb=lambda x: b['pb'].values[:, :, 0],
)

更多字段(行业、市值等)同理;若可一次 get_history_data(htypes='close, pb', ...),优先用 §10.1,少一步手工同构。核心原则仍是 先对齐再运算,避免 NumPy 静默错行。

可运行合成数据演示见项目根目录 examples/historypanel_multisource_research.py


3.11. 11. 导出到 pandas / statsmodels(宽表、长表与截面回归)

HistoryPanel 不内置 Newey–West、Fama–MacBeth 等推断流程;需要发表级回归时,应导出为二维表后用 statsmodels 等库。

11.1 按标的切成多个 DataFrame(宽表)

by_share = hp.to_df_dict(by='share')
df_000001 = by_share['000001.SZ']   # index 为时间,columns 为 htypes

11.2 手工堆成长表(便于按日截面分组)

import pandas as pd

records = []
M, L, N = hp.shape
ci = hp.htypes.index('close')
fi = hp.htypes.index('factor')   # 假设已存在名为 factor 的列
for mi, sh in enumerate(hp.shares):
    for li, dt in enumerate(hp.hdates):
        records.append({
            'date': dt,
            'share': sh,
            'close': hp.values[mi, li, ci],
            'factor': hp.values[mi, li, fi],
        })
df_long = pd.DataFrame(records)

11.3 单日截面 OLS 示例(需已安装 statsmodels)

# pip install statsmodels
import statsmodels.api as sm

sub = df_long[df_long['date'] == df_long['date'].iloc[-100]].dropna()
y = sub['close'] / sub['close'].mean() - 1.0   # 示例因变量,实际常用前瞻收益
X = sm.add_constant(sub['factor'])
model = sm.OLS(y, X, missing='drop').fit()
print(model.summary())

上述因变量仅为演示;实务中应将 因子与前瞻收益对齐 并处理停牌、幸存者偏差等——均在计量层完成,而非 HistoryPanel 核心职责。

合成数据示例见项目根目录 examples/historypanel_statsmodels_export.py


3.12. 12. 小结与下一步

通过本章示例,你可以看到:

  • HistoryPanel 提供了统一的三维历史数据容器,适合多标的 / 多指标联合研究;

  • 在其之上,可以直接调用统计方法(describe/mean/std/min/max)、滚动窗口(rolling)、收益/波动率(returns/volatility)、横截面排名与标准化(rank/zscore累计收益与归一化(cum_return/normalize组合聚合(portfolio、K 线指标(kline.*)、技术指标桥接(apply_ta)、蜡烛形态识别(candle_pattern)、研究掩码(where 以及 列属性 / 比较 / loc 等;

  • 任意一步都可以方便地切回 DataFrame,与 pandas / sklearn / statsmodels 等协同工作;

  • 正式回测请通过 Strategy.realize + Operator 完成;研究向 HP 与回测引擎的边界与迁移路径见 §9

下一步建议:

  • 在 Notebook 中基于你自己关注的股票池构造 HistoryPanel,尝试叠加多个因子(均线、波动率、估值等),并画出与价格对齐的图表;

  • 在阅读 HistoryPanel API 参考 的基础上,为你的研究题目设计一套「数据 → 因子(HP)→ 规则固化(Strategy)→ 回测(Operator)」的工作流;

  • 若考虑是否增加独立「因子评价」API 封装,可阅读设计说明 HistoryPanel 与可选 FactorResearch 层