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())
典型用法是:
將
ret或vol當作因子值矩陣(時間 × 股票);直接送入回測引擎,或用於選股、打分等。
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_xxx、macd_signal_xxx、macd_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.sma,HistoryPanel 會自動對所有選定股票廣播計算。
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.ndarray,dtype=bool,且不修改 hp。cum_return、normalize、portfolio 已支持 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)
研究向:沿 標的維 對指定列做等權或加權平均,返回 新 HistoryPanel(hdates 不變)。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.open、hp > 0等得到numpy.ndarray(bool),不是子HistoryPanel;全形掩碼可寫hp.where(hp.close > 15)。兩側均爲面板時須shares、hdates一致;兩列比較時多爲兩個 單列 子面板。loc:hp.loc[s]等價hp[:, :, s],只沿 時間(hdates) 軸篩選;不要把where得到的(M, L, N)掩碼傳給loc,格點條件仍用where+ 後續mask=。
細節與邊界情況以 HistoryPanel API 參考 爲準(含 where、cum_return、normalize、portfolio)。
# 示意(在已含 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_return、portfolio、plot 等是研究向工具:便於肉眼篩因子、看粗組合線,不包含交割週期、手續費、滑點、信號類型(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
選股類策略常用 FactorSorter:realize() 返回每個標的一個標量因子(長度 = 股票數),框架再按 max_sel_count、sort_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 單次拉取多列(頻率與資產類型需一致)
若本地已有日線 close 與 pb(市淨率)等字段,可直接:
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 名稱與順序完全一致。若你只分別拉取了 close 與 pb 兩列,需先擴成同構列(缺失側用 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 層。