Skip to content

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.