Upgrading and reproducing results
This guide covers 2.0.19 → 2.2.5. Compare identical input data, signals, positions, trades and reports before switching production. Pinning FinLab alone does not pin pandas behavior or subsequent data corrections. See the changelog for complete entries and linked changes.
Cumulative impact
| Effective version | Condition and impact | Migration |
|---|---|---|
| 2.0.21 | Quarterly Boolean masks expanded to disclosure dates change selection (PR #160) | Rebuild masks; fillna(False) before astype(bool)/np.where |
| 2.0.22, retained in 2.1.0 | Quarterly reindex uses filing dates; stock Series broadcast across columns (PR #166, #168) | Make dates explicit; mul(s, axis=0/1) for ambiguous Series |
| 2.0.23, retained in 2.1.0 | Same-day/late filings use newest disclosed quarter with a value (PR #169) | Recompute signals |
| 2.0.22, retained in 2.1.0 | Rank IC becomes Spearman; quantiles become 1–5 (PR #166) | Recompute IC/bucket analysis; replace old labels |
| 2.1.0 | Date Boolean selection retains flat dates (PR #191) | position & timing for flat periods |
| 2.1.0 | NaN positions become zero, fixing missed transitions (PR #194) | add(fill_value=0) to combine sleeves; rebuild trades |
| 2.1.0 | Quarterly rank/cs/sector/neutralize use disclosed peers only (PR #184, #188) | Recompute rankings; date rows are not shared quarters |
| 2.1.0 | Delisted holdings exit; 20 trailing missing bars can infer exit (PR #183) | Check warnings and exits; inference does not confirm delisting |
| 2.1.0 | Normalize Boolean/overweight inputs before drawdown/volatility scaling (PR #180) | Recompute exposure; sim still caps at full investment |
| 2.1.0 | Future signals cannot extend sparse historical simulations (PR #196) | Compare date ranges; specify end_date |
| 2.1.0 | Naive timestamps use market time; Taiwan non-session mornings retain current-day signals (PR #185) | Save execution instant/timezone; check latest targets separately |
| 2.1.0 | Empty set_universe resets; unmatched filters raise (PR #192, #181) | exclude_sector=[] for catalog-only scope; check spelling |
| 2.1.0 → 2.2.1 | Sparse exits are events in 2.1.0–2.2.0; 2.2.1 restores default state mode (PR #177, Issue #224) | Explicit exit_mode='state'/'event'; keep original event dates |
| 2.2.0 | Missing quarterly dates no longer leak values backward (PR #208) | Recompute; do not invent availability dates |
| 2.2.0 | Revenue deadlines and factor dates retain make-up Saturday sessions (PR #210) | Recompute affected signals, IC and bucket returns |
| 2.2.1 | Quarterly category means use filing dates (PR #220) | industry_mean.loc[:'2025-05-15'].tail(1); quarter loc is no longer Q1 accounts |
| 2.2.1 | Industry calculations/caps, custom reports and duplicate columns are deterministic across processes (Issue #225) | Save new comparison outputs; small differences can cross thresholds |
| 2.2.2 | Unknown TA-Lib parameters raise (Issue #274) | timeperiod, not timperiod; adjust_price=True or adj=True |
| 2.2.3 | Stop-loss live targets match the engine; both bool/float can change (PR #319, #314) | Inspect next_weights before ordering; backtest P&L/trades are unchanged |
| 2.2.5 | After a mid-rebalance stop in a strategy holding shorts, report.weights/Position.from_report() no longer enlarge the remaining shorts; live short sizes shrink to match the backtest (PR #338) |
Inspect targets before ordering; long-only strategies, backtest P&L and trades are unchanged |
| 2.2.5 | On pandas 3, the first return after a price gap is restored; position.weight.inverse_volatility() and related weights, event_study() and leveraged Position.from_weight() can change (PR #331) |
Recompute weights and event returns on pandas 3; pandas 2 results are unchanged |
2.0.22 and 2.0.23 are yanked. Their other fixes remain in later releases, but pandas-left alignment was reverted in 2.1.0. Exact reproduction of old bugs requires the old wheel, identical dependencies and identical inputs; most fixes have no compatibility switch.
Executable before/after cases
Download the cases. Inputs, filing dates, session calendar and clock are synthetic fixtures. Calculations use installed APIs and the actual engine; backtest cases require normal FinLab login. These outputs locate differences and do not represent real strategy performance.
Runs used macOS, CPython 3.13.3, pandas 2.2.3, numpy 2.2.6 and released PyPI wheels; TA-Lib 0.8.1 for indicators, UTC for the clock, and PYTHONHASHSEED=0 for duplicate columns. Decimals in the table are rounded. Run the downloaded file outside source checkouts:
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
Replace rank_ic with a case below. Old exceptions are actual outputs.
| Case | 2.0.19 | 2.2.4 | Output |
|---|---|---|---|
quarter_bool |
TypeError: bad operand type for unary ~: 'float' |
[True, False] |
Inverted quarterly mask |
latest_quarter |
[5.0, 9.0] |
[7.0, 3.0] |
Latest same-day filing |
quarter_reindex |
[NaN, NaN] |
[5.0, NaN] |
Disclosure-aware reindex |
quarter_rank |
0.5 |
1.0 |
Early reporter rank |
quarter_group |
['2023-Q1', 7.0] |
['2023-05-01 00:00:00', 5.0] |
Category index and first mean |
missing_date |
5.0 |
NaN |
Missing-date value |
series |
[NaN, NaN] |
[200.0, 300.0] |
Stock Series broadcasting |
timing |
[True, True] |
[True, False, True] |
Flat dates retained |
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] |
Ten stock buckets |
drawdown |
[2.0, 0.6536433173] |
[1.0, 0.4806677708] |
Gross exposure, rows 0 and 5 |
volatility |
[2.0, 0.0609895989] |
[1.0, 0.0609895989] |
Warmup normalization; row 5 unchanged |
nan_position |
['2022-01-04'] |
['2022-01-04','2022-01-07'] |
Entry dates around NaN |
short_position |
IndexError |
'2022-04-22' |
Two-row position end date |
future_position |
'2022-04-22' |
'2022-01-17' |
Sparse position end date |
delisted |
NaN |
0.0 |
Trailing missing prices |
monthly_saturday |
'2016-09-12' |
'2016-09-10' |
Revenue business-day alignment |
factor_saturday |
'2016-09-12' |
'2016-09-10' |
Factor make-up session |
duplicate_columns |
['c','a','b'] |
['a','b','c'] |
Duplicate column order |
universe_reset |
['2330','2317'] |
['2330','2317','0050O'] |
Catalog-external identifier |
universe_empty |
['2330','2317','0050O'] |
ValueError |
Unmatched sector |
from_weight |
AssertionError: Unexpectedly insufficient funds. |
2330/2317: 5 lots each, weight 0.5 | Overweight allocation |
clock |
{'2330': 1.0} |
{'2317': 1.0} |
Taiwan Saturday 00:30 target |
indicator_typo |
NaN (default 30-day SMA) |
ValueError, lists timeperiod |
Misspelled TA-Lib parameter |
stop_target |
2317/1301: 0.2 each | 2317/1301: 0.5 each | Pending target; P&L unchanged |
stop_reentry |
2330/2317/2454: 1/3 each | 2317/2454: 0.5 each | Executed target; P&L unchanged |
pandas_left |
DataFrame, 2×2 |
DataFrame, 2×2 |
Pandas-left endpoints agree |
Intermediate runs with identical dependencies: hold_default is [True,False,False] on 2.0.19, [True,False,True] on 2.2.0, and [True,False,False] on 2.2.1/2.2.4. pandas_left is pandas DataFrame 2×2 on 2.0.21, FinlabDataFrame 1×0 on yanked 2.0.23, and pandas DataFrame 2×2 on 2.1.0/2.2.4. Install yanked versions only for investigation in isolated environments. exit_mode exists from 2.2.1.
Defaults, output and exceptions
From 2.1.0, Studio research uses upload=True to upload explicitly; forced cloud strategy names still upload. Compare with upload=False. From 2.0.22, trades.stock_id is a bare identifier; use name or symbol for names. Statistics aliases preserve old values/keys; get_stats(resample=...) never changed frequency. win_ratio and get_metrics winRate cover different trades. Specify end_date for a fixed comparison window.
ic()/calc_metric() no longer require index name date. All-NaN factors remain NaN. Quarterly conversion no longer renames source columns: set df.columns.name='symbol' before stacking if needed. Unknown keys raise DatasetNotFoundError (RuntimeError subclass); download failures use DataError, with status in error.status. Pyodide should catch DataError. For pyarrow < 16 cache errors, upgrade FinLab and restart the kernel, without deleting caches.
From 2.2.2, insufficient rebuilding funds stop the entire portfolio update; verify total_balance/cash before saving or syncing. TA-Lib typo validation is default; optional strict=True in 2.2.4 also refuses input selectors and does not support pandas_ta. The additional cache correction check in 2.2.3 is reverted in 2.2.4. From 2.2.5, corrected data arrives within about 5 minutes without an extra request on every call; on 2.2.4, wait for the next scheduled update or refresh related inputs with force_download=True. Returned frames remain snapshots. For cash retention after stops, specify float weights before sim, excluding missing-price assets first; a Boolean basket redistributes equally.
Pin environment, data and settings
Keep production and comparison environments separate. Record python -VV, save python -m pip freeze > requirements.lock.txt, and archive compatible wheels with python -m pip download --only-binary=:all: -r requirements.lock.txt -d wheels. Rebuild on the same Python/OS/CPU with python -m pip install --no-index --find-links=wheels -r requirements.lock.txt. Pin actual production FinLab, pandas, numpy, pyarrow, scipy, indicator/model packages; save ML models, seeds and feature ordering. Restart kernels after upgrading.
Actual pct_change outputs for [100.0,None,110.0] are [nan,0.0,0.10000000000000009] on pandas 2.2.3 and [nan,nan,nan] on 3.0.6. Explicitly choose pct_change(fill_method=None) or ffill().pct_change(fill_method=None). From 2.2.5, FinLab's built-in position.weight methods, event_study() and Position.from_weight() carry prices forward before computing returns, so pandas 2 and 3 agree; your own pct_change() calls still need an explicit choice. See pandas 2.2 and pandas 3.
From 2.2.0, paid members can list data.versions(dataset), select a generation, then call data.get(dataset, version=str(row['generation'])). Record generation/hash/publication and replacement times and save actual inputs. Do not attach metadata fetched afterward to a latest-data read. Pin prices, filing dates, calendar, membership and industry classifications too. Each generation pins one table, not an atomic snapshot across tables.
Retention is what versions() currently lists, not a guaranteed three weeks. The 2.2.4 interface describes deletion after 30 days since replacement and 10 newer versions, effective 2026-09-26; it cannot recreate versions never retained. Expired generations cannot be recovered from their ids/hashes; save inputs for long-term reproduction. Hash verifies the server data object, not a backtest. Keep saved data within your usage rights and load only trusted pickles.
Naive as_of uses Asia/Taipei for every market; a bare date means midnight. as_of/version are mutually exclusive and cannot combine with start/end, intraday or local-only mode. Permanent financial vintages use financial_statement_versions. The API is experimental.
Save strategy source, execution instant/timezone, dependencies, data manifest, prices, positions and every sim parameter: market, trade price, rebalance/offset, fees/taxes, stops, touched exits, position cap, retained cost, next-period stop policy, dates and upload. Preserve custom market/benchmark code and inputs. Report pickle/HTML do not archive all inputs and are not actual executed trades. Compare data/dates, signals, positions, trades and return curves before summary metrics. Validate latest targets separately. The Chinese guide includes manifest and settings examples.
Public release feeds
JSON and Atom are generated with docs from the same changelog and reviewed annotations, covering published releases from 2.0.19. Unreleased is excluded. Each item has results_may_change, behavior_change, experimental, functions, trigger, references and full description. Result flags cover calculation/selection/backtest changes; live-target-only changes use behavior flags. False does not imply immutable data.
import requests
version = lambda text: tuple(map(int, text.split('.')))
start, end = version('2.0.19'), version('2.2.5')
response = requests.get('https://finlab.finance/docs/releases.json', timeout=30)
response.raise_for_status()
for release in response.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'])
The range excludes the start and includes the target; compare numeric versions. Yanked releases remain for historical readers. Review later reversions, including hold_until in 2.2.1 and cache validation in 2.2.4. Docs and feeds are deployed and verified before PyPI upload.