版本升級與結果重現
這頁整理 2.0.19 → 2.2.5 的累積影響。先看下表哪些條件符合你的策略,再用同一份資料比較升級前後的訊號、部位、交易與報表。單純鎖住 finlab 版本,無法固定 pandas 的計算方式或之後更正的資料。
2.0.22、2.0.23 已從 PyPI 撤回(yanked)。它們的其他修正保留於後續版本,但「一般 pandas 放在左側」的對齊變更已在 2.1.0 還原。請以 更新日誌 核對每版完整項目。
哪些策略需要調整
| 生效版本 | 觸發條件與影響 | 升級時怎麼處理 |
|---|---|---|
| 2.0.21 | 季度布林遮罩轉日期後取反、與日頻條件合併,可能選到不同股票(PR #160) | 重新產生遮罩;需要 astype(bool) 或 np.where 時先 fillna(False) |
| 2.0.22,保留於 2.1.0 | 季度 reindex() 改按各公司公布日對齊,不再全為 NaN;股票 Series 改按股票欄位廣播(PR #166、PR #168) |
明確指定季度轉日期;Series 意義不明時用 mul(s, axis=0/1) |
| 2.0.23,保留於 2.1.0 | 同日公布多季、或舊季晚交,改用已公布且有值的最新季度(PR #169) | 重新取得輸入、計算訊號;沒有保留錯誤值的開關 |
| 2.0.22,保留於 2.1.0 | calc_ic(rank=True) 改為真正的 Spearman;create_factor_data() 的組別改為 1~5、分組較均勻(PR #166) |
重算 IC 與分組分析;不要沿用舊 0~4 組別標籤 |
| 2.1.0 | position[timing] 保留 False 日期並空手,過去刪除日期後回測會沿用前期持股(PR #191) |
確認擇時目的;要空手可明寫 position & timing |
| 2.1.0 | NaN 部位視為未持有;resample=None 不再漏掉 NaN 前後的進出場(PR #194) |
合併不同策略用 add(fill_value=0);新建日期補 fill_value=False,重算交易 |
| 2.1.0 | 季度橫斷面先轉公布日,再做排名、cs、sector 與季度 neutralize();避免比較尚未公開的同季財報(PR #184、PR #188) |
重新排名;日頻結果不能再用季度 .loc 當作同季財報 |
| 2.1.0 | 下市或價格尾端至少 20 個交易日缺價的持股出場,之後不再買回;缺價推定不等於確認下市(PR #183) | 檢查 DelistedHoldingWarning、出場日期與交易;歷史績效可能改變 |
| 2.1.0 | 布林或總絕對權重大於 1 的部位,先正規化再做 drawdown_control()/target_volatility()(PR #180) |
重算曝險;回測仍限制最高滿倉,不支援用這些方法加槓桿 |
| 2.1.0 | resample=None 的未來部位列不再延長歷史回測期間(PR #196) |
比較回測起訖日;需要固定結束日請傳 end_date |
| 2.1.0 | 無時區的部位改採市場時間;非交易日 15:00 前不再把台股訊號退回前一天,最新持股可能改變(PR #185) | 保存執行時刻、主機時區;歷史比較固定 end_date,最新一期另查換股狀態 |
| 2.1.0 | set_universe() 不帶條件取消篩選,保留市場目錄外的代號;篩選結果為空改報錯(PR #192、PR #181) |
想限制在市場目錄用 set_universe(exclude_sector=[]);檢查條件拼字 |
| 2.1.0 → 2.2.1 | 2.1.0~2.2.0 的稀疏出場訊號是單次事件;2.2.1 起預設 exit_mode='state',恢復 2.0.19 的持續條件(PR #177、Issue #224) |
月/季條件用 state;單次事件明寫 event,保留出場原日期。其他修正仍可能改結果 |
| 2.2.0 | 缺少公布日或截止日的季報數值不再提前填入(PR #208) | 重新計算;缺日期維持未知,不自行補出可交易日 |
| 2.2.0 | 月營收截止日與因子日期遇補班交易週六,保留當天,不再延至週一(PR #210) | 重算包含這些日期的訊號、IC 與分組報酬 |
| 2.2.1 | 季度 groupby_category() 改按公布日計算同業統計(PR #220) |
改成 industry_mean.loc[:'2025-05-15'].tail(1);.loc['2025-Q1'] 不再表示第一季財報 |
| 2.2.1 | 產業計算、產業權重、custom report、重複欄位去重不再受 Python hash 順序影響(Issue #225) | 重存比對結果;門檻附近的小數尾數仍可能影響選股 |
| 2.2.2 | TA-Lib 不支援的參數名稱改報錯,過去可能被忽略而用預設值計算(Issue #274) | 把 timperiod 改為 timeperiod;還原價用 adjust_price=True 或 adj=True |
| 2.2.3 | 停損後的 next_weights、Position.from_report() 與回測的換股分配一致;實單目標可能變大、或移除已排除股票(PR #319、PR #314) |
下單前確認目標;bool 與 float 都可能受影響。回測損益、交易紀錄不變 |
| 2.2.5 | 持有空單的策略在換股之間有持股停損後,report.weights、Position.from_report() 不再放大其餘空單;實單放空部位會變小,與回測持有的部位一致(PR #338) |
下單前確認目標。只做多的策略不受影響;回測損益、交易紀錄不變 |
| 2.2.5 | pandas 3 下,價格中間有缺值的股票補回缺值後第一天的報酬;position.weight.inverse_volatility() 等權重方法、event_study() 與槓桿 Position.from_weight() 的結果可能改變(PR #331) |
使用 pandas 3 的策略重算權重與事件報酬;pandas 2 結果不變 |
如果要完全重現舊套件的錯誤計算,需要舊 wheel、同一組相依版本與同一份資料。修正項目通常沒有「沿用舊錯誤」的選項。
最小案例與兩版實跑輸出
下載完整案例。每個案例只呼叫受影響的計算,使用固定價格、財報與公布日期。季度、交易日曆與時鐘以 fixture 提供,不依賴日後更正的線上資料;sim() 使用真正的回測引擎。執行回測案例需要正常 FinLab 登入,見 登入說明。這些合成輸出用來定位版本差異,不能代表真實策略績效。
以下實跑環境為 macOS、CPython 3.13.3、pandas 2.2.3、numpy 2.2.6、PyPI 已發布的 FinLab wheel;TA-Lib 案例用 TA-Lib 0.8.1。日期案例採 UTC 主機、固定時鐘。重複欄位案例指定 PYTHONHASHSEED=0。小數在表內四捨五入;完整程式會印出原值。先將下載的檔案放到原始碼目錄以外,再分別執行:
python3.13 -m venv before
python3.13 -m venv after
before/bin/python -m pip install "finlab==2.0.19" "pandas==2.2.3" "numpy==2.2.6" "TA-Lib==0.8.1" matplotlib
after/bin/python -m pip install "finlab==2.2.4" "pandas==2.2.3" "numpy==2.2.6" "TA-Lib==0.8.1" matplotlib
before/bin/python upgrade_results.py rank_ic
after/bin/python upgrade_results.py rank_ic
把最後的 rank_ic 換成下表案例名稱,即可用同一段程式比較兩版。NaN 表示未知或缺值,舊版錯誤也是實跑結果。
| 案例 | 2.0.19 輸出 | 2.2.4 輸出 | 比較的是什麼 |
|---|---|---|---|
quarter_bool |
TypeError: bad operand type for unary ~: 'float' |
[True, False] |
公布日展開後的遮罩反相 |
latest_quarter |
[5.0, 9.0] |
[7.0, 3.0] |
同日公布兩季所採用的值 |
quarter_reindex |
[NaN, NaN] |
[5.0, NaN] |
一家公司已公布、另一家尚未公布 |
quarter_rank |
0.5 |
1.0 |
第一家公司公布時的橫斷面百分位 |
quarter_group |
['2023-Q1', 7.0] |
['2023-05-01 00:00:00', 5.0] |
產業平均的索引與第一筆值 |
missing_date |
5.0 |
NaN |
缺少日期的財報不再提前填入 |
series |
[NaN, NaN] |
[200.0, 300.0] |
股票權重 Series 的廣播方向 |
timing |
[True, True] |
[True, False, True] |
保留空手日期 |
rank_ic |
0.7850264209630101 |
1.0 |
Spearman IC |
quantiles |
[0,1,1,2,2,2,3,4,4,4] |
[1,1,2,2,3,3,4,4,5,5] |
十檔股票的分位組別 |
drawdown |
[2.0, 0.6536433173] |
[1.0, 0.4806677708] |
第 0、5 列的總絕對權重 |
volatility |
[2.0, 0.0609895989] |
[1.0, 0.0609895989] |
暖機列先正規化,並非每一列都不同 |
nan_position |
['2022-01-04'] |
['2022-01-04','2022-01-07'] |
NaN 前後實際進場的日期 |
short_position |
IndexError |
'2022-04-22' |
兩列部位的回測結束日 |
future_position |
'2022-04-22' |
'2022-01-17' |
未來訊號不再延長稀疏部位的回測 |
delisted |
NaN |
0.0 |
價格尾端長期缺價後的 B 部位 |
monthly_saturday |
'2016-09-12' |
'2016-09-10' |
月營收使用的交易日對齊路徑 |
factor_saturday |
'2016-09-12' |
'2016-09-10' |
因子資料納入的補班交易日 |
duplicate_columns |
['c','a','b'] |
['a','b','c'] |
同一 seed 的欄位排列 |
universe_reset |
['2330','2317'] |
['2330','2317','0050O'] |
不帶條件是否仍排除目錄外代號 |
universe_empty |
['2330','2317','0050O'] |
ValueError |
拼錯產業不再默默使用全市場 |
from_weight |
AssertionError: Unexpectedly insufficient funds. |
2330、2317 各 5 張、權重各 0.5 | 權重總和 1.6 先縮為滿倉 |
clock |
{'2330': 1.0} |
{'2317': 1.0} |
台北週六 00:30 的最新訊號,主機為 UTC |
indicator_typo |
NaN(仍用預設 30 日均線) |
ValueError,列出 timeperiod |
timperiod=3 的錯字不再被忽略 |
stop_target |
2317、1301 各 0.2 | 2317、1301 各 0.5 | 停損三檔後的下一次換股目標;回測計算不變 |
stop_reentry |
2330、2317、2454 各 1/3 | 2317、2454 各 0.5 | 不再買回這次換股已排除的 2330;回測計算不變 |
pandas_left |
DataFrame、2×2 |
DataFrame、2×2 |
端點版本的 pandas 左側運算結果相同 |
中間版本也會影響升級路徑。同一案例以相同 pandas/numpy 實跑:
| 案例 | 修正前 | 中間版本 | 修正後 |
|---|---|---|---|
hold_default |
2.0.19:[True,False,False] |
2.2.0:[True,False,True] |
2.2.1/2.2.4:[True,False,False] |
pandas_left |
2.0.21:DataFrame、2×2 |
2.0.23:FinlabDataFrame、1×0 |
2.1.0/2.2.4:DataFrame、2×2 |
2.0.23 是已撤回版本,只有追查歷史行為時才在隔離環境安裝。hold_until(exit_mode='event') 可在 2.2.1 起明確選擇事件模式;舊版不接受 exit_mode。
預設值、輸出與例外
| 變更 | 需要更新的寫法 |
|---|---|
Studio 的 sim(upload=None) 自 2.1.0 不再自動上傳,雲端強制策略名稱仍會上傳 |
需要上傳時傳 upload=True;研究比較用 upload=False |
report.trades['stock_id'] 自 2.0.22 改為純代號 |
名稱用 name,代號加名稱用 symbol;舊欄位不再包含名稱 |
sim() 支援 end_date;未指定時仍會依訊號列距決定回測期間 |
固定比較窗口用 sim(position, end_date='2025-12-31', upload=False) |
get_stats() 增加 camelCase 別名,舊欄位保留;resample 原本無效,現在警告 |
不依賴 resample 改統計頻率;win_ratio 與 get_metrics()['winRate'] 的交易口徑不同 |
因子 ic()/calc_metric() 自 2.1.0 不要求索引名稱 date |
仍建議使用明確的日期/股票 MultiIndex;全 NaN 因子保留 NaN |
data.get() 找不到 key 改為 DatasetNotFoundError;下載失敗改為 DataError |
RuntimeError 舊捕捉仍支援找不到 key;Pyodide 用 except DataError,HTTP 狀態用 error.status |
| 2.2.1 季報轉日期不再改寫原表欄位名稱 | 若 stack().reset_index() 需要 symbol,先設 df.columns.name='symbol' |
| 2.2.1 修正 pyarrow < 16 的過期快取錯誤 | 升級後重啟 kernel;暫時不能升級可用 pyarrow>=16,不用刪快取 |
2.2.2 的 PortfolioSyncManager.update() 遇總資金被不換股策略占滿會整次停止 |
確認 total_balance 與購股現金;成功後才儲存/同步 |
2.2.4 的 data.indicator(strict=True) 是選用功能 |
預設仍檢查 TA-Lib 參數錯字;嚴格模式拒絕價格 selector,pandas_ta 尚不支援 |
| 2.2.3 增加快取修正檢查,2.2.4 撤回;2.2.5 起資料修正後最慢約 5 分鐘取得,不再每次呼叫都多一次查詢 | 2.2.5 起重新呼叫即可;停在 2.2.4 則等下次排程更新,或對相關輸入用 force_download=True。已回傳的表格仍是當時快照 |
若停損後希望保留現金,請在回測前明確使用浮點權重;五檔各 0.2 不等同布林部位自動等權重重分配。先排除缺少價格的股票,再計算權重,並確認 position_limit。
鎖住執行環境
在正式策略與新版本比較環境分開安裝,先保存現有環境:
python -VV > python-version.txt
python -m pip freeze > requirements.lock.txt
python -m pip download --only-binary=:all: -r requirements.lock.txt -d wheels
重建時使用相同 Python 版本、作業系統與 CPU 架構:
不要把驗證環境的版本號直接套到既有正式環境;先鎖住正式策略實際使用的 finlab、pandas、numpy、pyarrow、scipy、TA-Lib/pandas_ta 等完整相依版本。ML 策略另保存模型、亂數 seed、特徵欄位順序與模型套件版本。升級後重啟 Python/Jupyter kernel,避免記憶體裡仍載入舊套件。
pandas 2.2 的 pct_change() 預設仍會先往前填缺值,pandas 3 改為 fill_method=None。策略應明確選擇缺值處理,參見 pandas 2.2 文件 與 pandas 3 文件。
import pandas as pd
s = pd.Series([100.0, None, 110.0])
print(s.pct_change().tolist())
# pandas 2.2.3:[nan, 0.0, 0.10000000000000009]
# pandas 3.0.6:[nan, nan, nan]
print(s.pct_change(fill_method=None).tolist()) # 明確保留缺值
print(s.ffill().pct_change(fill_method=None).tolist()) # 明確先填值
2.2.5 起,FinLab 內建的 position.weight.inverse_volatility() 等權重方法、event_study() 與 Position.from_weight() 會先沿用缺值前的價格再計算報酬,pandas 2 與 3 結果相同;策略裡自己呼叫的 pct_change() 仍需依上例明確選擇。
記錄並保存資料版本
2.2.0 起,付費資料會員可用 data.versions() 列出仍保留的 generation、公布/取代時間、大小與 hash,再下載指定 generation。先選 generation,再讀取;不要先抓最新值、再用另一個請求的最新 metadata 當作它的版本。
import json
from pathlib import Path
from finlab import data
datasets = ['price:收盤價', 'etl:adj_close'] # 列出策略的所有輸入
manifest = {}
for dataset in datasets:
versions = data.versions(dataset)
if versions.empty:
raise RuntimeError(f'{dataset} 沒有可用版本')
row = versions.iloc[-1]
generation = str(row['generation'])
snapshot = data.get(dataset, version=generation)
# 保存實際輸入;檔名與資料集名稱對照也留在 manifest
filename = f'input-{len(manifest)}.pkl'
snapshot.to_pickle(filename)
manifest[dataset] = {
'generation': generation,
'hash': str(row['hash']),
'time_created': str(row['time_created']),
'file': filename,
}
Path('data-versions.json').write_text(
json.dumps(manifest, ensure_ascii=False, indent=2)
)
generation 固定單張表,不代表跨資料表的原子快照。還原價、公布日期、交易日曆、股票母體與產業分類也是輸入,請一併保存。自行保存資料限於你的使用權限;pickle 只讀取自己保存、可信任的檔案。
保留期以 data.versions() 當次列出的內容為準,不能把「約三週」當成長期保證。2.2.4 的版本介面說明為:版本被取代滿 30 天且已有 10 個較新版本後可刪除;這項規則自 2026-09-26 生效,不會補出先前未保留的版本。指定 generation 過期後,僅靠 generation/hash 無法重新下載,因此長期重現需自行保存實際輸入。hash 是伺服器資料物件的驗證值,不是回測結果的 hash。
as_of 未帶時區時採台北時間,只有日期代表當天 00:00,不代表當日收盤。version 與 as_of 只能擇一,不能搭配 start/end、逐筆/分鐘資料或 use_local_data_only。財報的永久歷史版本改查 financial_statement_versions。這個 API 仍屬實驗性。
保存策略與回測設定
保存策略原始碼、實際執行時刻與時區、環境 lock、資料 manifest、經處理的輸入價格及 position,並把所有 sim() 參數明寫:市場、成交價、換股頻率/偏移、費率、稅率、停損停利、移動停利、觸價模式、持股上限、retain_cost_when_rebalance、stop_trading_next_period、開始/結束範圍與 upload。自訂 Market、價格矩陣與 benchmark 也需保存程式及輸入,不能只保存名稱。
import json
from pathlib import Path
from finlab.backtest import sim
settings = dict(
resample='M',
trade_at_price='close',
market='TW_STOCK',
fee_ratio=0.001425,
tax_ratio=0.003,
position_limit=1,
stop_loss=None,
take_profit=None,
trail_stop=None,
touched_exit=False,
retain_cost_when_rebalance=False,
stop_trading_next_period=True,
end_date='2025-12-31',
upload=False,
)
position.to_pickle('position.pkl') # 保存你實際傳入的 position
Path('sim-settings.json').write_text(json.dumps(settings, indent=2))
report = sim(position, **settings)
report.to_pickle('report.pkl')
report.to_html('report.html')
report.pkl、HTML 或雲端報告可保存結果,但不會自動封存全部來源資料、相依環境、原始碼與自訂市場。回測報告也不是實際成交紀錄。驗證重現時,依序比較資料值與日期、訊號、position、交易日期與數量、報酬曲線,再比較 Sharpe 等摘要;先固定 end_date,再另外驗證最新一期的換股與實單目標。
以程式追蹤版本影響
公開 feed:JSON、Atom。兩者在文件建置時,從同一份更新日誌與逐項審閱的影響分類產生,涵蓋 2.0.19 起的已發布版本;Unreleased 不會當作已發布。每個項目包含三個布林標記、functions、trigger、references 與完整說明。results_may_change 指計算/選股/回測數字;只改實單目標則用 behavior_change,仍應閱讀說明。false 不代表資料從此不會更正。
import requests
version = lambda text: tuple(map(int, text.split('.')))
start, end = version('2.0.19'), version('2.2.5')
feed = requests.get('https://finlab.finance/docs/releases.json', timeout=30)
feed.raise_for_status()
for release in feed.json()['releases']:
if start < version(release['version']) <= end:
for item in release['items']:
if item['results_may_change'] or item['behavior_change']:
print(release['version'], item['functions'], item['trigger'])
區間排除起始版、包含目標版;不要用字串比較版本。兩個 yanked 版本仍列在 feed,避免漏掉曾升級至該版的歷史。分類保留每版當時的變更,所以需注意後續的還原:例如 2.2.1 的 hold_until 與 2.2.4 的快取檢查。文件與 feed 會先上線並驗證,再上傳新版至 PyPI。