Skip to content

Changelog

2.2.13 (2026-10-16)

Fixed

  • FinlabDataFrame.corrwith(axis=1) no longer returns incorrect zeros for nonconstant cross-sectional inputs or self-correlation. Missing values, constants, mixed DataFrame inputs and ordinary FinLab date-aware arithmetic retain their expected behavior. Upgrade and rerun affected factor correlation calculations. (Issue #436, PR #437)

2.2.12 (2026-10-16)

Fixed

  • Report snapshots, HTML export and uploads reuse the data readiness saved by the backtest, avoiding repeated metadata requests and full-table hashing. Historical backtests without pending sources no longer show a suggested trading time. Performance and holdings are unchanged. (Issue #431, PR #432)

  • HTML holdings reports show the execution date beside the entry price and list the signal date separately. Expired rebalance dates are hidden when no rebalance is pending, while valid legacy and other-market schedules remain visible. The analysis layout is also updated; backtest calculations are unchanged. (PR #430)

  • sim() now rejects positions with no rows or stock columns before backtesting and explains how to align stock columns or use a date Series for a market filter. Explicit all-zero cash positions remain supported. (Issue #423, PR #424)

  • groupby_category().groups, indices and ngroups retain pandas grouping metadata. Unequal industry sizes no longer fail, and equal sizes no longer convert group mappings into a DataFrame. Aggregation and disclosure-date alignment are unchanged. (Issue #420, PR #421)

  • Korean, Japanese, Hong Kong and UK stock and fund backtests now use the existing adjusted-price tables, fixing missing-dataset errors. Fees, taxes and historical data are unchanged. (Issue #418, PR #419)

  • Backtests starting at a non-trading monthly, weekly or custom rebalance date now retain the first holding period that could previously be skipped. Affected trades and performance may change; later rebalances and next-session execution rules are unchanged. (Issue #416, PR #417)

  • data.get() stops waiting sooner when authorization, downloads or version checks accept a connection but do not respond, retaining the existing retry policy. Download timeouts limit inactivity per read, allowing continuously streaming large files to finish. Data and cache rules are unchanged. (Issue #414, PR #415)

  • Ordinary and date-range data.get(..., save_to_storage=False) calls now fail before fetching and explain how to select data.CacheStorage() and omit the old argument, instead of warning and still writing to disk. Intraday and retained-version reads continue to support the argument. (Issue #411, PR #412)

  • Cloud readiness checks reuse verified publication hashes supplied by read-only storage instead of rehashing entire DataFrames. Mismatches remain unconfirmed, while writable caches and legacy data retain their existing verification. (Issue #433, PR #434)

2.2.11 (2026-10-13)

Changed

  • The backtest report dashboard now uses the updated Studio analysis and holdings pages, including the performance timeline, annual bands, and plain-language strategy checks. Backtest calculations and data values are unchanged.

  • Quarterly boolean conditions now use dense arrays for alignment with daily data, & / | / ^, and boolean-mask filtering, reducing per-column overhead for full-market US stock selection. Results, unknown values before disclosure, and public output dtypes are unchanged. (Issue #404, PR #405)

Fixed

  • In read-only environments, python -m finlab ai and data.get() now continue using the refreshed in-memory credentials when token renewal succeeds but the disk cache cannot be written. Calls in the same process reuse the new credentials; a new process refreshes them again. Initial login and environment-variable login are unchanged. (Issue #408, PR #409)

  • On Python 3.10, finlab[qlib] uses a TA-Lib version that still provides compatible wheels, avoiding source builds that fail without the system library after newer TA-Lib releases dropped Python 3.10 wheels. Dependency selection on Python 3.11 and later is unchanged.

Documentation

  • The industry_rank() documentation now identifies Taiwan industry classifications as the TPEx industry value-chain platform, explains the default broad categories and averaging across categories, and clarifies that the current classification snapshot applies to all dates rather than historical point-in-time classifications. rank() examples explicitly rank across columns. Function and data behavior are unchanged. (Issue #406, PR #407)

2.2.10 (2026-10-10)

Added

  • When data.get() downloads a new version rewritten outside scheduled updates, it shows the dataset, correction time, support-record link and an as_of= example. Reading again or requesting a date range after the cache update does not repeat the notice. It uses the existing revision index without adding a network request to every call; manually appended dates can also trigger it. This feature does not modify data values; historical retention and access restrictions still apply. (Issue #400, PR #402)

Fixed

  • finlab.ml.feature.combine() now applies resample to MultiIndex features, using the existing resampling dates both on their own and when mixed with wide features. Weekly/monthly settings are no longer ignored and feature columns are no longer emptied by date mismatch. String W retains Sunday dates; pass explicit date anchors for the last trading day of each week and use the same resample for labels. Without resample, label period still means trading days. (Issue #397, PR #399)

    Behavior change Results may change

2.2.9 (2026-10-10)

Changed

  • sim() no longer emits DelistedHoldingWarning for delisted holdings, and the finlab.backtest.delisting.DelistedHoldingWarning class is removed. How delisted holdings are exited and kept out of later rebalances is unchanged. Remove any code that imports the class or filters warnings by it. (Issue #390, PR #395)

Added

  • python -m finlab cloud deploy and cloud schedule set add --frequency (daily, weekly, monthly, custom), --days-of-week, --day-of-month and --cron, so cloud schedules can run weekly, monthly or on a custom cron. Times are always Asia/Taipei; dates missing from a month are skipped, and market holidays are not skipped automatically. Existing commands behave as before, and cloud schedule list/get show the saved frequency. (PR #386)

Fixed

  • In markets that define no prefetch datasets, such as USMarket and USFundMarket, sim() skipped the data version check when reading prices, index and company data, so after a historical data correction it kept using the old local cache and could mix it with the newer data you read through data.get(). Backtests in every market now check data versions the same way data.get() does; within one sim()/sim_batch() run, a cache already confirmed is reused without another check or download. Taiwan-market backtests, and results when no data was corrected, are unchanged. (Issue #391, PR #392)

  • When the sub-strategies of finlab.portfolio.Portfolio start or rebalance on different dates, position zeroed every date on which only some sub-strategies had positions. Each sub-strategy's positions are now aligned to all rebalance dates, carrying its latest target forward and holding cash before it starts, while a cleared holding is not carried forward; creturn still covers only the common period. (Issue #384, PR #385)

    Behavior change Results may change

  • On Apple Silicon Macs, DNNModel, TabnetModel and SFMModel in finlab.ml.qlib crashed during training or construction because sklearn and PyTorch each load their own OpenMP runtime, and importing torch first made LGBModel crash instead. macOS arm64 now trains with one thread by default, so all eight models train and predict, more slowly; other platforms are unchanged. get_models() now includes SFMModel and skips only a model whose construction fails; installed PyTorch is no longer reported as missing; and TabnetModel validation scores include the final partial batch weighted by sample count, so a validation set smaller than one batch no longer fails. (Issue #387, PR #388)

    Behavior change Results may change

2.2.8 (2026-10-09)

Fixed

  • verify_strategy() now raises a TypeError naming the returned type and showing how to return a sim(..., upload=False) Report when a strategy returns a position DataFrame or another non-Report value, instead of a missing-trades error. Valid Report-returning strategies and their verification results are unchanged. (Issue #381, PR #382)

2.2.7 (2026-10-09)

Changed

  • Data downloaded by free accounts is now kept in a free subfolder of the data cache folder, shared by Python programs and Jupyter kernels on the same computer, instead of being downloaded again by every run. When the cloud data has not changed, the local cache is read directly, with no download and no daily download quota used; a newer cloud file is downloaded once. Paid accounts never read the free subfolder, and when the folder cannot be written, data stays in memory as before. Data contents are unchanged. (PR #376)

  • When a large full table (a dataset name without a colon) is republished and the cloud provides a change file for it, data.get() downloads only the changed dates and merges them into the local cache. The merge is used only when it matches the published table's hash; otherwise the whole table is downloaded as before. Without a change file, or with a free account, behavior is unchanged. Data contents are unchanged. (PR #378)

  • data.indicator() always calculates with market prices, so custom price inputs such as high=, low=, close=, open=, volume= or data= still raise an error; the message now explains this and shows how to calculate one stock at a time with talib.abstract. The message for misspelled parameter names is unchanged. (Issue #350, PR #356)

Added

  • The stop_loss and take_profit parameters of sim() and hold_until() accept, besides numbers, a DataFrame indexed by date with stocks as columns, so each stock can use a different stop threshold each day, such as an ATR-multiple stop. Each bar uses the latest threshold dated on or before it, and intraday touched_exit=True exits use only thresholds dated up to the previous bar, never the same day's value; each cell behaves like the same number, and NaN, dates before the first row, and stocks missing from the DataFrame have no stop that day. With a DataFrame, report.stop_loss/report.take_profit are None, and stop exits are still recorded in report.actions. trail_stop and trail_stop_activation still accept numbers only; passing a Series, array or other unsupported type to a stop parameter raises a TypeError before the backtest that names the parameter and the alternative, instead of a pandas truth-value error. Backtests with numeric thresholds are unchanged. (Issue #358, PR #359, #361)

    atr_ratio = (high - low).rolling(14).mean() / close
    report = sim(position, stop_loss=atr_ratio * 2, take_profit=atr_ratio * 4)
    position = entries.hold_until(exits, stop_loss=atr_ratio * 2)
    
  • Added finlab.backtest.metrics_from_nav(): pass a daily NAV Series produced by another backtesting framework or your own ledger to compute curve metrics such as annual return, Sharpe ratio and maximum drawdown the same way as sim(). The NAV must already be net of trading costs and exclude deposits and withdrawals; the function creates no Report, trades, holdings or orders. (PR #367)

    import pandas as pd
    from finlab.backtest import metrics_from_nav
    
    nav = pd.Series([100, 102, 101], index=pd.date_range("2026-01-01", periods=3))
    metrics = metrics_from_nav(nav)
    print(metrics.annual_return(), metrics.sharpe_ratio(), metrics.max_drawdown())
    

Fixed

  • When sim() received a DataFrame, Series, DatetimeIndex or list of dates as resample (for example, rebalancing on monthly revenue release dates), resample_offset was silently ignored. Each rebalance date is now shifted by the offset, and the report's next rebalance date uses the same shift. Strategies that use both parameters will get different rebalance dates and backtest results; string resample values are unchanged. (Issue #362, PR #363)

    Cloud schedules receive this change with this runtime upgrade; no manual package upgrade is needed. If you run strategies yourself, upgrade the package, restart Python/Jupyter, and recheck strategies that combine custom rebalance dates with resample_offset.

    Behavior change Results may change

  • When the backtest authorization connection of sim() is interrupted, it is retried once after 3 seconds, and the request now has a 30-second timeout, so a single dropped connection no longer fails the whole backtest and an unresponsive server no longer blocks it indefinitely. If both attempts fail, the error asks you to check your network and rerun sim(). (Issue #368, PR #369)

  • When the local data cache is in a read-only folder (for example, a read-only mount), data.get() no longer raises LocalDataReadError because it cannot create or open the .lock file; writing the cache still requires a writable folder. (Issue #372, PR #373)

  • With pandas 2, boolean positions containing missing values (for example, a boolean DataFrame after .shift()) no longer make sim() print a FutureWarning about Downcasting object dtype arrays on .fillna; missing values still mean no holding, and backtest results are unchanged. (Issue #364, PR #365)

  • show_alerting_stocks() for order execution now prints a disposition/warning stock alert heading and notes, and prints a confirmation when no pending order is on the disposition or credit warning lists, instead of printing nothing. (PR #357)

2.2.6 (2026-10-05)

Fixed

  • Fix data.get() failing in cloud schedules when it cannot update the revision timestamp of a read-only cache; the existing cached contents are now used as-is. Data values and backtest calculations are unchanged. (Issue #344, PR #346)

  • Use the system temporary directory for the default data cache on Google Cloud Functions, Cloud Run services/jobs, and AWS Lambda. Other environments also fall back to temporary storage when the home cache cannot be written, avoiding failures during imports or data.get(). Writable local homes, Colab, and explicit FileStorage(path) locations are unchanged. (Issue #344, PR #347)

2.2.5 (2026-10-05)

Changed

  • When a strategy holding short positions has a holding stopped out between rebalances, report.weights enlarged the remaining short weights, so live positions built with Position.from_report() were oversized. The remaining shorts now keep their original weights, matching what the backtest actually holds: with five shorts at -0.2 each and one stopped out, the other four return from -0.25 to -0.2. Long-only strategies are unaffected, and backtest returns and trade records are unchanged. (Issue #229, PR #338)

    Cloud schedules receive this change with this runtime upgrade; no manual package upgrade is needed. If you run strategies yourself, upgrade the package, restart Python/Jupyter, and check live targets before placing orders.

    Behavior change

  • After a data correction, default data.get()/data.gets() calls receive the corrected data within about 5 minutes, without waiting for the next scheduled update or using force_download=True. An up-to-date local cache needs no online check. After the scheduled update time but before new data arrives, FinLab checks at most once every 5 minutes instead of adding an online request to every call. A DataFrame already returned remains a snapshot; call again to use the corrected data. import finlab no longer queries the data update time in the background. (Issue #304, PR #336)

Added

  • Added data.describe() to look up a dataset or field's description, columns, market, category and update schedule without downloading data. It shares the catalog cache with data.search(). Unknown names raise KeyError. (Issue #88, PR #313)

    from finlab import data
    
    data.describe('price:收盤價')
    data.describe('monthly_revenue')  # columns and description of the whole table
    data.describe('us_price:close', market='us')
    
  • Added the FINLAB_MAX_DOWNLOAD_WORKERS environment variable to set how many downloads data.get()/data.gets() run at once: 8 by default, and 1 downloads one at a time for environments that limit threads. Also added data.free_memory() to release memory held by loaded data; the next read loads from the local cache. DataFrames you still hold are not released, so del variables you no longer need first. (Issue #119, PR #337)

    import os
    from finlab import data
    
    os.environ['FINLAB_MAX_DOWNLOAD_WORKERS'] = '1'
    close = data.get('price:收盤價')
    
    del close
    data.free_memory()
    
  • finlab ai adds --history [N] to list recent questions (10 by default), --show <question ID> to read a question with all its replies (including later progress updates), and --json output for your own scripts or AI agents. (Issue #328, PR #329)

    python -m finlab ai --history
    python -m finlab ai --show <question ID> --json
    

Fixed

  • With pandas 3, the first return after a gap in a stock's prices was dropped. This affected weights from position.weight.inverse_volatility(), risk_parity(), correlation(), target_volatility() and drawdown_control(), stock and benchmark returns in event_study(), and margin allocation in leveraged Position.from_weight(). Prices are now carried forward across the gap before returns are computed, matching pandas 2. Users on pandas 2 are unaffected. (Issue #330, PR #331)

    Results may change

  • python -m finlab cloud deploy --help and the CLI documentation listed the small tier (s) as 2 GiB memory with a 120-second timeout. They now show 1 CPU, 4 GiB and 300 seconds. The actual runtime specification is unchanged. (Issue #332, PR #333)

Documentation

  • Added Upgrading and reproducing results, covering conditions since 2.0.19 that change stock selection, backtests or live targets, with migration code, downloadable before/after cases, and how to pin environments, data versions and backtest settings. JSON and Atom release-impact feeds let you filter changes that affect your strategy programmatically. (Issue #334, PR #335)

2.2.4 (2026-10-04)

Changed

  • Reverted the off-schedule historical-correction cache checks introduced in 2.2.3 to avoid an extra online request on every default data.get()/data.gets() call. Other 2.2.3 changes are retained. After a data correction, the local cache expires at the next scheduled update; fetch the data again then to receive the new values. US data updates at 05:30 Taipei time. To obtain the corrected data immediately, download the related inputs with force_download=True. The existing cache behavior is retained, and no additional fix is planned for this behavior. (Issue #304, PR #326)

Added

  • Added finlab.portfolio.TradeReview to rebuild daily holdings, cash, NAV and reports from actual fills, dividends, stock splits and cash flows, and compare actual positions with targets. from_account() queries history from supported broker integrations. Fees, cash flows and events unavailable from a broker may be estimated, use a holdings-based calculation, or be listed in limitations; review those limits before use. (Issue #82, PR #322)
  • Added opt-in data.indicator(..., strict=True): TA-Lib indicators accept only numeric parameter names, and TA-Lib price-input selectors are rejected. pandas_ta indicators raise NotImplementedError in strict mode. Only keyword names are checked; TA-Lib still validates the values. The default mode is unchanged. (Issue #134, PR #318)

Fixed

  • In finlab ai chat, multi-line pastes stay in a single question even when the last line has no trailing newline, instead of being split into two questions and charged twice. (PR #310)

2.2.3 (2026-10-04)

Changed

  • After stop-loss/take-profit exits, report.next_weights and Position.from_report() now follow the backtest engine's allocation to the remaining holdings at the next rebalance. For example, with five equally weighted stocks and three stopped out, the remaining two targets change from 20% each to 50% each when no position cap applies. This fix does not change backtest returns or trade records; the next live rebalance may create larger positions. position_limit still caps each stock's weight. (Issue #207, PR #319)

    Cloud schedules receive this change with this runtime upgrade; no manual package upgrade is needed. If you run strategies yourself, upgrade the package and restart Python/Jupyter. To retain cash after stops, convert boolean positions to floating-point weights before backtesting: five stocks at 0.2 each keep the remaining stocks at 0.2 after three stops. The example only scales rows whose total absolute weight exceeds 1. Exclude stocks with missing prices before computing the weights.

    position = position.astype(float)
    position = position.div(position.abs().sum(axis=1).clip(lower=1), axis=0)
    report = sim(position, resample="M", stop_loss=0.1)
    

    Behavior change

  • With resample=None, between a rebalance and the next position change, report.next_weights and Position.from_report() no longer add back stocks excluded at that rebalance by stop-loss/take-profit, so live trading no longer buys them back immediately, matching the backtest. Live targets change for both bool and float positions: for example, a float position of 3 stocks at 1/3 each with one stopped stock excluded at the rebalance now targets 2 stocks at 1/3 each with the remaining 1/3 in cash, instead of 3 stocks at 1/3 each; a bool position now targets 2 stocks at 1/2 each instead of 3 at 1/3. Backtest returns and trade records are unchanged. After upgrading, check live targets before placing orders. (Issue #228, PR #314)

    Behavior change

  • Online data.get()/data.gets() now check for historical corrections outside the usual refresh schedule, even when the cache has not expired. Previously returned DataFrames remain snapshots; fetch the relevant inputs again before recalculating. This adds online queries and cache validation work. (Issue #304, PR #305)

Added

  • Added finlab ai to ask the FinLab AI assistant from the terminal. (PR #308)
  • Added the first stage of stock futures backtesting: TWStockFuturesMarket computes integer-lot P&L, fees, taxes and margin usage in NTD. Prices use adjusted underlying stock prices, and users supply margin ratios. Basis, contract rolling, margin calls/liquidation, actual futures prices and live orders are not yet included. (Issue #219, PR #292)
  • Added download and search support for 1-minute futures data across contract months, including night sessions spanning weekends. (PR #309)
  • data.universe() supports explicit stock lists; Taiwan OHLCV datasets support English aliases. (PR #316, #320)
  • data.search(details=True) exposes dataset permissions and the free-data cutoff, and default searches flag VIP datasets. Free-account warnings and docs state the Taiwan historical cutoff of 2023-12-31 and trial-expiry restrictions. (Issue #162, PR #306)

Fixed

  • Fixed parsing historical revision timestamps containing both whole and fractional seconds. (PR #312)
  • Shares-outstanding data.readiness() now checks the latest officially available month for TWSE, TPEx and emerging stocks. Missing, unknown or stale periods return not ready; per-market diagnostics are available in sources[0].periods. Other monthly and quarterly datasets retain the existing source-schedule diagnostics. (Issue #133, PR #321)

Documentation

  • Explained that next_weights is a rebalance preview, demonstrated report.is_rebalance_due(), and distinguished a due rebalance, complete data and actual execution. (Issue #163, PR #317)
  • Redirected /docs/getting-start/ to /docs/getting-started/.

2.2.2 (2026-10-03)

Changed

  • data.indicator() with TA-Lib now reports misspelled or unsupported parameters and lists the valid ones, instead of silently using defaults. For adjusted prices, use adjust_price=True (adj=True also works); adjusted=True and adjust=True are invalid. pandas_ta indicators are unaffected. (Issue #274)

    # Use timeperiod, not the typo timperiod
    sma = data.indicator('SMA', timeperiod=10, adjust_price=True)
    

    Behavior change

  • When rebalancing with PortfolioSyncManager.update(), if strategies keeping their holdings have already used up the total budget, the update now raises an error and preserves existing holdings to avoid accidental liquidation. The entire update stops, including other strategies' stop-loss and take-profit checks. Increase total_balance as the error suggests, allow enough for purchases and spare cash, then rerun update(). Save or sync only after it succeeds. Updates that only handle stops or remove strategies, without rebuilding holdings, are unaffected. (Issue #164)

    The amount in the error is only a lower bound: check that the remaining budget can buy the target stocks. A positive balance too small to buy one share is still tracked in Issue #296.

    Behavior change

Fixed

  • sim() and lazy rebalance schedules: Fixed newer pandas rejecting legacy frequency names with day shifts such as resample='M+2' and 'Q+1'. Frequency names are converted for the installed pandas version before parsing the shift. 'M+2' and 'ME+2' produce identical rebalance dates; unshifted 'M' and 'W-FRI+1' keep their behavior, and pandas 2.1 still uses the legacy names. (Issue #269)

  • When IPython is unavailable, report.display() now gives environment-specific guidance: in Studio/Pyodide, put report by itself on the last line of the cell; in a Python script, save with report.to_html("report.html") and open the file in a browser. Text summaries and display behavior are unchanged. (Issue #283)

Documentation

  • Restore the Chinese and English API reference overview, including the destination of https://doc.finlab.tw/reference/, with links to each module. (Issue #282)
  • US index universe documentation and examples explain that us_universe(index='SP500'/'NDX') filters by the latest constituents. Historical backtests must apply constituent records by date; the existing API still uses the latest list, and production data is unchanged. (Issue #239)
  • Backfill data access, quarterly date conversion, host timezone, and factor analysis behavior changes from 2.0.19–2.2.0 under the versions where they took effect. (Issues #223, #274)

2.2.1 (2026-09-30)

Changed

  • hold_until() adds exit_mode. The default, 'state', keeps the previous exit condition active, restoring the exit behavior from before 2.1.0. Use 'event' to keep the 2.1.0–2.2.0 behavior of exiting only on signal dates. (Issue #224)

    # Revenue or financial conditions: stay active until the next update
    position = entries.hold_until(exits)
    
    # One-off events: exit only on dates listed in exits
    position = entries.hold_until(exits, exit_mode='event')
    

    For example, Monday's exit signal is True. Tuesday has an entry signal but no new exit data. 'state' blocks the entry; 'event' allows re-entry on Tuesday. Both modes agree when exit data is available every day.

    For 'event', keep the exit condition's original dates instead of mixing it with daily conditions first; in lazy strategies, call .collect() on the exit condition separately. For 'state', sort dates from oldest to newest; a final True blocks entries until a False appears. Other backtest fixes remain, so results may not exactly match an older release.

    Results may change

Fixed

  • data.get(): Fix repeated failures to fetch data after the cache expires when using finlab 2.0.19–2.2.0 with pyarrow < 16. If an error says RecordBatch has no cast or select attribute, upgrade and restart Python or the Jupyter kernel. There is no need to delete the cache. (PR #241, Issue #232)

    pip install -U finlab
    

    If you cannot upgrade finlab yet, use pip install -U "pyarrow>=16" and restart instead.

  • groupby_category() on quarterly data now computes industry statistics from reports already public on each date, excluding peers' undisclosed values. The result index changes from quarters to disclosure dates. Daily and monthly inputs are unchanged. (PR #220, Issue #212)

    This change requires updating .loc only when querying quarterly results after groupby_category(). .loc['2025-Q1'] now selects disclosures from January–March, not first-quarter reports, and may return data without an error. Query the latest data available by a date instead:

    industry_mean = quarterly_factor.groupby_category().mean()
    
    # Industry mean as of 2025-05-15; an empty table if no data is available
    latest = industry_mean.loc[:'2025-05-15'].tail(1)
    

    Companies that have not disclosed the new quarter still contribute their previous-quarter values.

    Results may change

  • If you call stack().reset_index() on the original data after quarterly date conversion or ranking and read a symbol column, set df.columns.name = "symbol" first. These operations no longer name the original data's columns automatically.

  • data.get() download errors now explain the problem and what to do, without exposing a URL that grants access to the data when you share the error for help. (PR #258, Issue #247)

    from finlab.exceptions import DataError
    
    try:
        close = data.get('price:收盤價')
    except DataError as error:
        print(error)
    

    Existing except requests.RequestException handlers still work for download errors; in the browser (Pyodide), use except DataError. For custom handlers, read error.status for the HTTP status and error.kind for the error category. error.request and response URLs, headers and bodies are no longer available; error.response is None for non-HTTP errors.

  • When running several Jupyter kernels or parallel backtests, data.get() could keep returning old data after a historical correction. It now downloads the data that needs updating on the next read, without requiring you to clear the cache. (PR #260, Issue #255)

  • Fix results or ordering changing after a Python restart with identical inputs in industry_rank(), weight.cap_industry(), create_custom_report() and drop_duplicate_cols(). Results are now consistent across runs. Tiny numerical differences after upgrading may still change selections at a cutoff. If you saved results for comparison, save a new baseline after upgrading. (Issue #225) Results may change

2.2.0 (2026-09-28)

Added

  • data.get(dataset, as_of=...) / data.get(dataset, version=...) downloads historical versions; data.versions(dataset) lists what is available. as_of uses Taipei time if no timezone is supplied, and a date alone means 00:00. Use version for a specific, reproducible snapshot. Paid members only; downloads count toward the daily quota, cached reads do not. Retention is about two weeks, with plans for 30+ days. For financial statements, use data.get('financial_statement_versions'). Choose either as_of or version; neither works with start/end, tick/minute data or use_local_data_only. The API may still change. (PR #211) Experimental

  • data.get supports index conversion and quarterly masking for TW *_revised (corrected) tables. When a backtest is truncated, revised_at dates later than the truncation date are capped at it, and value items emit a warning that corrected values cannot be rolled back to what was known then. These tables are not yet available. (PR #209)

Changed

  • finlab.tools.event_study.create_factor_data() now includes make-up trading Saturdays instead of moving them to Monday, so IC and quantile returns may change. Non-trading dates still move to the next trading day. This does not affect index_str_to_date(); .deadline() also keeps make-up trading dates (see PR #210 below for revenue data). (PR #210; added 2026-09-30, Issue #223) Behavior change Results may change

Fixed

  • Fix quarterly values with missing disclosure dates appearing too early in backtests. This applies to index_str_to_date() (including the default in sim()) and .deadline(). In a monthly “top 20 by ROA (.deadline())” strategy, untradable stock 7947 no longer takes a slot: total return changes from 128.2% to 139.5%, and trades from 2465 to 2575. The 2026-09-27 TW audit found missing dates only for .deadline(), not disclosure dates for index_str_to_date(); other markets were not checked. (PR #208; corrected 2026-09-30: this entry previously said index_str_to_date() was unaffected, Issue #274)

    Audit details

    A 2026-09-27 audit of all 211 TW quarterly fields, including Emerging Stock Board stocks, covered 2013-Q1 through 2026-Q2 and 2,870 stock codes. Of 15,178,900 non-null values, 136 lacked the cutoff date used by deadline() (0.000896%). All belonged to stock 7947 (補丁科), 2026-Q2: 92 financial statement fields and 44 fundamental indicators. Each record means one stock × quarter × field, not a separate company. That quarter has a disclosure date of 2026-09-17; the audit found no non-null values missing the disclosure date used by index_str_to_date(). The percentage measures source records before date conversion, not the historical cells affected after forward filling.

    Results may change

  • A monthly revenue deadline on a make-up trading Saturday (2009-01-10, 2016-09-10) previously moved incorrectly to Monday; it now takes effect that Saturday. Deadlines on non-trading holidays still move to the next trading day. In example strategies: revenue momentum total return 471.8% → 459.7%, PEG 1421.9% → 1427.7%; the other 7 price and quarterly example strategies are unchanged. (PR #210) Results may change

2.1.1 (2026-09-24)

Fixed

  • Fix OutOfBoundsDatetime when pandas 3 calculates TW market close times for distant dates, such as year 2999. Normal dates and timezone behavior are unchanged.

2.1.0 (2026-09-24)

2.0.22 and 2.0.23 were yanked from PyPI

These versions changed how plain pandas DataFrames combine with FinlabDataFrame, leaving positions in common templates entirely NaN and causing cloud strategies to fail. Upgrade from 2.0.21 or either yanked version to 2.1.0. Other improvements, including sim(end_date=...), search aliases and quarterly date alignment, remain in 2.1.0.

Rewrite (cond * score).is_largest(N) as score[cond].is_largest(N)

(cond1 & cond2) * score gives stocks that fail the conditions a score of 0. When fewer than N stocks qualify, is_largest(N) fills the remaining slots with those 0-score stocks in column order (1101, 1102, 1103, ...), so the holdings include stocks that fail the conditions. Use score[cond].is_largest(N) (or score.where(cond).is_largest(N)) instead: stocks that fail become NaN and are not ranked, and when fewer than N qualify only those are held. 2.1.0 emits TopNPaddingWarning when this padding happens; the selection is unchanged. The official 小資族資優生 template is being updated the same way, so its holdings can drop below 10 stocks when fewer than 10 qualify.

# Before
position = ((cond1 & cond2) * score).is_largest(10)
# After
position = score[cond1 & cond2].is_largest(10)

If a position is aligned with reindex(<monthly revenue dates>) without a fill value, add fill_value=False so the position does not start with NaN rows (see PR #194 below).

If you already write score[cond] and still see the warning, qualifying stocks are tied at the N-th place (for example all scoring 0) and are picked in column order; add a secondary score if you need a stable choice.

Keeping the 2.0.21 behavior

The old form of each behavior change is below. The bug fixes (NaN handling in sim(), look-ahead in quarterly cross-sections, drawdown_control / target_volatility, the from_weight weight sum) have no old form; reproducing old results exactly requires pip install finlab==2.0.21, which is not recommended because those results contain the bugs.

Change How to keep the old behavior
position[timing] now goes to cash (PR #191) position.loc[timing.reindex(position.index, fill_value=False).to_numpy()]: drops the False dates, so sim() carries the previous holdings
set_universe() without filters now removes the filter (PR #192) data.set_universe(exclude_sector=[]): restricts to every security in the current market's catalog
Studio does not upload by default (PR #187) sim(..., upload=True)
hold_until exits are events (PR #177) position = entries.hold_until(exit.reindex(entries.index, method='ffill'))
Universe filters that match nothing raise (PR #181) No old form; they used to fall back to the whole market silently, so fix the filter

Added

  • Reports returned by sim() now expose upload_status: skipped, pending, success, or error. StrategyUploadWarning surfaces upload failures and environment variables overriding an explicitly chosen report name. Native Python uploads remain asynchronous; the status updates when the upload finishes. (PR #178)

  • TopNPaddingWarning: when is_largest(N) / is_smallest(N) fills N places with tied stocks, the warning lists affected dates, counts and how to rewrite the selection. Only ties at the selection boundary trigger it; results are unchanged. (PR #200)

  • PandasLeftOperandWarning: emitted when a plain pandas DataFrame on the left and a FinlabDataFrame on the right are combined with + - * / // % ** & | ^ and their dates or stocks differ. pandas alignment is used in that case (no forward fill of dates, union of stocks, plain DataFrame result), which differs from putting the FinlabDataFrame on the left. Results are unchanged; filterable by class. (PR #193)

  • ZeroShareAllocationWarning: for board-lot orders, Position.from_weight() / from_report() now lists stocks allocated 0 shares because the budget cannot buy one lot. Use odd_lot=True or increase fund. Order quantities are unchanged. (PR #190, order_executor PR #23)

  • data.reset_universe(): removes the universe filter, so later get() / gets() calls (including cached reads) return complete data. Exiting a nested universe() still restores its previous selection. (PR #186)

  • data.get_calendar() and data.readiness(): check the TW trading calendar and whether your strategy’s data has updated. For TW daily data, data.is_tradable() returns True only after the sources update; other markets or unavailable calendars keep the cache-expiry check. Reports show the planned rebalance date, holding progress, pending data and when to rerun. (PR #182, #195, #198)

  • The backtest dashboard adds a support panel: a report shown with display() in a notebook carries the cached FinLab credential (valid for at most 1 hour), so users can contact support without logging in again. Files exported with to_html() do not contain it. Keep this in mind before sharing an .ipynb with outputs. (PR #197)

Changed

  • sim()'s default upload=None uploads the report only in cloud-scheduled runs (FINLAB_FORCED_STRATEGY_NAME is set). FinLab Studio uploads only with an explicit upload=True, so repeated AI experiments no longer overwrite the online report on every backtest. FINLAB_STRATEGY_NAME still sets the strategy name used when uploading. (PR #187) Behavior change

  • data.set_universe() with no filters (or with every filter set to "ALL") now equals data.reset_universe() and removes the filter; so do with data.universe():, set_us_universe() and with data.us_universe():. It used to restrict reads to every security in the current market's catalog, silently dropping identifiers outside it (for example non-standard TDCC codes and historical ids). (PR #192) Behavior change

  • universe filters that match no securities (for example a misspelled sector) raise ValueError and leave the current universe unchanged; they used to fall back silently to the whole market. When an active universe has no overlap with a dataset, a UserWarning is emitted and the data is returned unfiltered. Stock ids are stripped of surrounding whitespace before matching. (PR #181) Behavior change

  • Python 3.10 is now the minimum (requires-python >= 3.10). No Python 3.9 wheels have been published since 2.0.3; the package metadata now says so. Behavior change

  • Remove MissingDisclosureWarning, added in 2.0.23. Users cannot act on a quarter that lacks an alignment date; the data pipeline now fills those dates instead of warning users.

  • When a data.get() / data.gets() download returns HTTP 404 or 412, the download URL is requested again and the download is retried; it used to raise immediately. There is one retry only, and when the retry also fails, the error raised is the one from the retry. Other HTTP errors (for example 403, 500) are not retried and raise immediately. Downloads that succeed the first time are unaffected. (PR #185; added 2026-09-30, Issue #223)

  • (exception type only): when sim()'s resample is a list of dates (DatetimeIndex, DataFrame, Series or list) and none of them falls within the backtest period, sim() raises ValueError: resample has no dates within the simulation range; it used to raise IndexError: list index out of range. When resample is a string (for example 'M') and the backtest period contains no rebalance date, it is still IndexError. Code that must handle both cases should use except (IndexError, ValueError). (PR #185; added 2026-09-30, Issue #223) Behavior change

  • sim() now uses market time to set the data cutoff for positions without a timezone (Taipei for TW, UTC−5 for US). This fixes the latest signal and holdings lagging by a day on UTC hosts such as Colab when running at 00:00–08:00 Taipei time. The same instant now gives the same result across host timezones. Only the latest period is affected; UTC users upgrading from 2.0.23 may see a two-day advance when combined with the non-trading-day fix before 15:00. Custom markets with tzinfo() returning None still use host time. (PR #196; added 2026-09-30, Issue #274) Behavior change Holdings may change

Fixed

  • TW market close timestamps preserve their input resolution, avoiding OutOfBoundsDatetime when the calendar fallback sees signal dates outside pandas' nanosecond range (for example year 2999). Normal dates and timezone behavior are unchanged.

  • sim() exits delisted assets and excludes them from later rebalances. Taiwan stocks use exchange delisting notices: exit on the first available price bar after the announcement. If the announcement comes after the last price and the delisting is already within the data period, exit at the last available price. Without a notice, at least 20 trailing price bars with no price trigger the same last-price exit; this is an inference from missing prices, not confirmation of delisting. With resample=None, the portfolio rebalances on exit; scheduled rebalancing holds the proceeds as cash until the next rebalance. DelistedHoldingWarning lists affected assets and exit dates. Previously these holdings could remain at a frozen price, be bought again, and appear with NaN trade returns. This changes historical returns, drawdowns, trades, and current/next holdings. (PR #183) Results may change Holdings may change

  • Live rebalancing(PortfolioSyncManager, Position.from_report()): for TW daily strategies the rebalance window opens at 15:00 of the last trading session on or before a weekend or holiday signal date, and the target switches to the new period at the same time. Evening-only schedules used to rebuild on Friday evening with the previous period's target while recording the rebalance as done, so weekly strategies traded one week behind the backtest every week, and monthly strategies whose month-end fell on a weekend kept the old holdings for the whole month, with no warning. sl_enter / tp_enter temporary exits end at the same time. (PR #185, order_executor PR #24, #25)

  • Live rebalancing:when PortfolioSyncManager.update() finds holdings still built for a superseded target (the schedule missed the rebalance window, for example because the host was down or cron did not fire), it logs a warning and catches up to the report's current target. It used to keep the old holdings silently until the next rebalance. Position records gain applied_signal_at, the signal date of the target actually built; a weekend or holiday signal that executes on the same session as the previous weekday's signal is not treated as missed. An unparseable older record only skips this check and no longer stops other strategies from updating. Run update() on every schedule, not only when is_rebalance_due() is True. (PR #185)

  • TW daily live rebalancing: is_rebalance_due() stays True until the next signal replaces it, so schedules running after the open or close can still rebalance. If the trading calendar is unavailable, the signal date’s close determines timing. The dashboard stops showing “Rebalancing” after the execution day closes. (PR #185, new_website PR #103)

  • sim(): when a TW daily strategy runs before 15:00 Taipei time on a non-trading day (weekend or holiday), the data cutoff is no longer moved back to the previous day, so a signal dated that day is included, and next_weights and the current/next holdings can differ from 2.0.23 and earlier. Any run before 15:00 used to move the cutoff back, even when the market was closed that day; on trading days the cutoff still moves back before 15:00. Without a trading calendar the original rule applies. What is affected is the latest period at run time. (PR #185; added 2026-09-30, Issue #223) Holdings may change

  • Historical backtests (sim(end_date=...)) no longer query the trading calendar or dataset metadata, and report.is_tradable() is always False for them. (PR #185)

  • The trading calendar, the dashboard's stock data and symbol logos, and the reports embedded in the docs are served from the production domain r2.finlab.finance instead of the rate-limited r2.dev URL. The trading calendar is refreshed every three hours; a calendar past its expiry stays in use while it lists future sessions, and a failed download keeps the calendar already fetched. (PR #185, new_website PR #103)

  • position[timing] (with timing a date-indexed boolean Series) keeps every date and holds cash on dates where timing is False (False for boolean positions, 0 for weights), the same as position & timing. It used to drop those dates, so sim() forward-filled the previous holdings and a timing filter stayed fully invested through its risk-off periods. Stock-indexed boolean Series (including object dtype) still select columns. DateMaskSelectionWarning, which only existed in unreleased versions, is removed. (PR #191, #173) Behavior change Results may change

  • sim() now treats NaN in a position as not held, once, at its input (the same as fillna(0) / fillna(False)). Object columns produced by merging two sleeves (pos_a + pos_b, combine_first, pd.concat) become bool or float. With resample=None, entries and exits through NaN used to be skipped, a position whose first rows were all NaN raised IndexError, and avgNStock counted NaN as held. With resample=None, a position that starts with NaN rows can now start its backtest earlier. In a replay of 1,089 cloud-scheduled strategies, 275 had NaN in their positions, 0 changed their latest target weights (next_weights), and 79 changed their metrics, mostly because the first rebalance used to be skipped. (PR #194) Results may change

  • quarterly cross-sectional operations align values to each company's disclosure dates first and then compute with the values public on each date: rank(axis=1), is_largest() / is_smallest() (including lazy), every df.cs and df.sector method, and neutralize() on all-quarterly inputs. Early filers are no longer compared with same-quarter values that peers have not disclosed yet, removing lookahead bias; cs.rank() no longer returns all NaN for quarterly data. Tie handling and missing undisclosed values are preserved. (PR #184, #188) Results may change

  • hold_until(exit, ...) treats exits as events, like entries, effective only on their own dates. When exit had sparser dates than the entries (event dates, weekly/monthly/quarterly), the last exit used to be forward-filled and the stock could never be entered again. To treat a low-frequency exit condition as a persistent state, write exit.reindex(entries.index, method='ffill'). (PR #177) Results may change

  • weight.drawdown_control() / target_volatility() first convert boolean selections or weights with gross exposure above 1 into the weights used by sim(), then apply risk scaling. This prevents simulation normalization from undoing exposure reductions. Scaled exposure above full investment remains subject to the simulation cap. (PR #180) Results may change

  • With resample=None, future position rows are kept in the report as the planned rebalance date and never enter executable holdings. Future rows also no longer change the backtest period of sparse positions or bypass the daily signal cutoff. (PR #196)

  • Position.from_weight() scales weights whose total absolute value exceeds 1 down to full investment, the same as sim(), and logs a warning; it used to raise AssertionError: Unexpectedly insufficient funds. (PR #174, order_executor PR #21)

  • finlab.tools.factor_metrics.ic() / calc_metric() now group correctly by date even when the date index is unnamed or has a name other than date. (PR #176)

  • Convertible-bond reports use 債券簡稱 from cb_published_info, with compatibility for legacy name and identifier schemas, selecting the latest nonempty name instead of displaying only the bond code. (PR #159)

  • SinopacAccount.update_order() raises OddLotRepriceError if replacing a cancelled odd-lot order fails or returns no order ID, instead of merely logging the failure and appearing to succeed. (order_executor PR #18)

  • OrderExecutor.show_alerting_stocks(): fix pandas 3 compatibility and consistently use strings for alerting-stock codes. (order_executor PR #20)

  • Restore the 2.0.21 behavior: arithmetic and logical operations with a plain pandas DataFrame on the left and a FinlabDataFrame on the right (pd_df * fdf, pd_df + fdf, -, /, //, %, **, &, |, ^) again use the pandas alignment from 2.0.21: union of dates and columns, no forward fill, and a plain DataFrame result. Version 2.0.22 used FinlabDataFrame alignment instead (intersection of columns, forward-filled dates). Templates such as 「小資族資優生」 multiply empty_df = pd.DataFrame(1, index=..., columns=...) by par-value-change ratios (par_value_change_*) to calculate market capitalization. That change left only stocks with par-value-change events, then zero stocks after multiplying the TWSE and TPEx tables, producing all-NaN positions and a ValueError from sim(). Comparisons (<, >, ==, etc.) already used FinlabDataFrame alignment in 2.0.21 and remain unchanged. (PR #171) Behavior change

  • Plain DataFrames with quarterly string indexes can again be combined with date-indexed data, including through FinlabDataFrame.align(), without the TypeError introduced in 2.0.22, restoring the 2.0.21 results. Quarterly FinlabDataFrame inputs still convert to disclosure dates automatically when aligned to dates with reindex() / reindex_like() / align(). (PR #171)

  • Backtest reports (report.display() / report.to_html()): fix incorrect font sizes and spacing. Monthly returns scroll horizontally on phones so numbers no longer overlap.

  • Backtest report: with the stock panel open (⌘K stock search), the report reflows beside the panel on wide screens instead of being covered. The "Strategy" legend chip on the ratio page was invisible in dark mode and now uses the bar color.

  • plot_tw_stock_radar(): in the default line_polar mode the radial axis ends at cut_bins (10 by default); it used to be multiplied by the number of stocks (30 for three), squeezing the plot into the center. The stacked bar_polar mode is unchanged.

  • plot_tw_stock_candles(): the title is centered so it no longer overlaps the range selector.

Docs

  • The PyPI description is rewritten in English, with the actual output of every example and screenshots of each report page.

  • US-market examples now import from finlab.markets.us import USMarket (from finlab.market import USMarket raised an error).

  • The order docs add "Error 5: Signal but No Orders", explaining whole-lot flooring and ZeroShareAllocationWarning. (PR #190)

  • The is_largest() / is_smallest() docstrings and the DataFrame reference show score[cond].is_largest(N). (PR #200)

Engineering

  • Tests now reproduce the template with offline sim() and verify that operations with pandas on the left match 2.0.21. (PR #171)

2.0.23 (2026-09-23)

Fixed

  • Quarterly date conversion (index_str_to_date() and automatic alignment with daily data) now uses the latest disclosed quarter with a value. Older reports filed late no longer overwrite newer quarters; when several quarters are filed on one day, the newest is used. The date index stays the same. A comparison with production data found corrected EPS values across about 200 stocks and 13,000 trading days. (PR #169) Behavior change

Added

  • index_str_to_date() now warns with MissingDisclosureWarning when quarterly values lack an alignment date and are skipped, listing counts and examples. Previously it silently kept the earlier quarter’s value. Output is unchanged. (PR #170)

2.0.22 (2026-09-23)

Added

  • data.search() now searches dataset names, descriptions, aliases and columns. For example, data.search('市值') finds etl:market_value, and data.search('集保') finds inventory and etl:inventory:*. Matching tolerates case, character width and common wording differences; the return type is unchanged. (PR #165)

  • data.get(): a missing dataset now raises finlab.exceptions.DatasetNotFoundError (a RuntimeError subclass, so existing except RuntimeError handlers still work) listing the closest valid keys and any measurement-difference note, without exposing internal storage paths. Registered equivalent legacy keys (for example margin_transactions:融資餘額, financial_statement:營業活動之淨現金流入(流出)) emit a UserWarning and fetch the correct key instead; this also applies to data.gets() and start/end reads. (PR #165)

  • sim(end_date=...): set an inclusive backtest end date; open positions use prices available by that date only. Also supported by sim_batch() and lazy positions. Slicing monthly positions could still extend returns to today; use sim(position, end_date="2025-12-31") to set the cutoff explicitly. Calls without it are unchanged. (PR #166)

  • report.get_stats() / get_benchmark_stats(): Add the camelCase names used by get_metrics() (annualReturn, sharpeRatio, sortinoRatio, calmarRatio, volatility, maxDrawdown, avgDrawdown, avgDrawdownDays, m12WinRate) as aliases with the same values as the ffn keys. Existing keys are kept. win_ratio has no alias because it counts closed trades only, while get_metrics()'s winRate also counts open trades. (PR #166)

  • finlab.tools.factor_metrics.ic(method=...): Choose 'pearson' (default, unchanged results) or 'spearman' (rank IC). (PR #166)

Changed

  • report.trades['stock_id'] now contains the code only (e.g. 2330), so it can join directly with price data. Read the new name column for the name (e.g. 台積電, empty if unknown), or symbol for “code name”. Also applies to CloudReport.trades; website, Studio and HTML report displays are unchanged. (PR #166) Behavior change

  • Quarterly string indexes (e.g. 2013-Q1) now align with dates automatically. FinlabDataFrame.reindex() / reindex_like() / align() onto a date index first convert the quarters to each company's disclosure dates with index_str_to_date(). Previously reindex(close.index) returned all NaN and reindex(..., method='ffill') raised TypeError. Operations with a plain pandas DataFrame on the left (pd_df + fdf, pd_df & fdf, ...) now use FinlabDataFrame alignment (forward-filled union of dates, intersection of columns) and return a FinlabDataFrame instead of pandas' NaN-padded result. A plain (unwrapped) quarterly DataFrame combined with dated data now raises TypeError that points to FinlabDataFrame(df) or index_str_to_date(), instead of silently producing NaN. (PR #166) Behavior change

  • finlab.tools.event_study.create_factor_data() now labels <name>_factor_quantile groups 1–5, matching df.cs.bucket(5). Ten stocks are split into groups of two, fixing the previous 1/2/3/1/3 split with labels 0–4. (PR #166) Behavior change

  • report.get_stats(resample=...): resample never had an effect. Passing it now emits a DeprecationWarning and results are unchanged; stats are always computed on daily returns. The stats sent through the legacy upload API gains the alias keys above. (PR #166)

  • Operations between a FinlabDataFrame and a pd.Series now align on the axis the Series index labels. A Series indexed by stock ids (e.g. pd.Series({'2330': 1.2}), close.iloc[-1]) applies to every date; stocks missing from it are False in &/| and NaN in arithmetic. Previously the stock ids were aligned against the dates, giving all NaN or all False. A Series indexed by dates (or quarter/month strings) behaves as before. When the index matches both the dates and the stocks, or neither, a ValueError now suggests df.mul(s, axis=0) (align on dates) or df.mul(s, axis=1) (align on stocks). df[stock boolean Series] now selects columns. (PR #168) Behavior change

  • df[date boolean Series] (e.g. market timing via position[timing]) still drops the False dates, which sim() fills with the previous holdings instead of cash; it now emits a FutureWarning recommending position & timing. The next minor release will keep those dates as False. Row filters on long tables without a date index (e.g. df[df['stock_id'] == '2330'] on broker_transactions) are unaffected. (#112; PR #168)

Fixed

  • finlab.login(): when browser login failed, was rejected or timed out, it used to return silently as if logged in; it now raises finlab.exceptions.LoginError (a RuntimeError subclass) explaining how to retry and how to use python -m finlab token --env in headless environments. (PR #165)

  • finlab.tools.factor_metrics.calc_ic(rank=True) ranked the factors but not the returns, so the result was neither Pearson nor Spearman. It now ranks both on each date and computes true Spearman on pairwise-valid values, matching scipy.stats.spearmanr. Values change. (PR #166) Behavior change

  • calc_ic(rank=False) raised IndexError when any factor column was all NaN. It now starts at the first date where every factor that ever has an IC has one; all-NaN columns stay as NaN. (PR #166)

  • sim() raised IndexError for positions with fewer than 3 rows. They are now held to the last price date. (PR #166)

  • report.display() raised ImportError where IPython is not installed (e.g. Pyodide without dependencies, plain Python scripts). It now prints the report.to_text() summary and suggests pip install ipython or report.to_html(). save_report_path is still written first. (PR #166)

  • data.indicator(): When neither TA-Lib nor pandas_ta is installed, the error lists the exact pip install TA-Lib / pip install pandas-ta commands and each package's naming convention. In the browser (Pyodide), it says FinLab Studio preloads TA-Lib (reload the page) and that pandas_ta is not preloaded in Studio. When TA-Lib lacks an indicator, pandas_ta is tried next (previously, installing TA-Lib hid every pandas_ta indicator). When neither package has the indicator, a ValueError is raised with a case hint such as 'rsi' → 'RSI'. (PR #166)

Docs

  • Login docs now describe the current flow: python -m finlab login on your own machine; for servers and schedulers run python -m finlab token --env locally and set FINLAB_REFRESH_TOKEN, FINLAB_SESSION_ID, FINLAB_API_KEY; finlab.login() in Colab; FINLAB_API_TOKEN is deprecated. The authentication order is documented. (PR #165)

  • Document a 2.0.0 behavior change that was never written down: sim()'s upload defaults to None and uploads the report only when FINLAB_STRATEGY_NAME or FINLAB_FORCED_STRATEGY_NAME is set (FinLab Studio, cloud-scheduled runs). Local, Jupyter and Colab runs do not upload by default; pass upload=True or call report.upload() to upload. (PR #165)

Engineering

  • tests: Add an offline sim() harness (synthetic market and licence stub) so unit tests run the real backtest engine and report assembly without credentials or data downloads. (PR #166)

2.0.21 (2026-09-21)

Fixed

  • Quarterly boolean conditions now work correctly with daily data: expressions such as ~mask and A & ~B no longer select the wrong stocks. Recreate condition tables after upgrading. Values before the first disclosure remain unknown and are treated as not held in backtests. Use fillna(False) before .astype(bool) or np.where. (PR #160) Results may change

  • Report.to_pickle(): fix RuntimeError: dictionary changed size during iteration when saving a report while its background upload is still running. (PR #161)

  • Pyodide after-market tick/minute data: browser downloads that hit an HTTP error keep the server's error detail (for example an unavailable trading day or missing permission) instead of a generic HTTP error.

2.0.20 (2026-09-18)

Added

  • data.get("tw_tick:2330", start=..., end=...) and data.get("tw_minute:2330", start=..., end=...): download Taiwan after-market ticks and regular-session one-minute OHLCV. Both dates are required, up to 31 calendar days per call; incomplete trading days raise an error. Timestamps use Taipei time and simultaneous trades are preserved. Minute bars omit empty minutes and keep the 13:30 auction separate; 14:30 fixed-price trades appear only in ticks. Available to Taiwan data subscribers, with a free 2330 sample for 2026-06-01. Historical coverage is being added. See intraday data.

Changed

  • data.gets() does not support the intraday tick and minute datasets: a list containing tw_tick:* or tw_minute:* raises ValueError; use data.get() with start/end instead. data.gets() is unchanged for other datasets. (added 2026-09-30, Issue #223)

2.0.19 (2026-09-16)

Added

  • data.universe.membership(): Add daily boolean listing-eligibility masks for TSE, OTC, and TSE_OTC, for filtering before cross-sectional ranking with expressions such as score[eligible].is_largest(20). Reconstruct effective intervals from currently available listing and termination records, including listing dates and excluding termination dates, with support for exchange transfers and repeated listing periods. Unverified periods remain False; warnings and attrs['membership_missing_history'] identify incomplete histories. This does not provide historical version snapshots or a complete ETF universe. (#140)
  • data.get(dataset, start=..., end=...): regular datasets accept the keyword arguments start / end to read a date range; either may be omitted. The filter compares against the date column stored in the table, not the converted index: for a table indexed by quarter (for example 2024-Q1), the date stored for that row is compared, not the quarter itself. A bare date for start / end means 00:00 that day, and rows exactly equal to start or end are included; for a table whose date column carries a time, rows after 00:00 on the end day are not returned. A dataset without a date column, or start later than end, raises ValueError. Calls without start / end return the same data as before. (added 2026-09-30, Issue #223)

Fixed

  • CloudReport: Fill missing execution defaults when reading legacy holdings, including market, trailing-stop and weight settings, matching canonical reports while preserving explicit settings and holding actions. (#155; PR #156)
  • Position.from_weight() / from_report(): Honor the supplied allocation callable instead of always using greedy allocation. Default allocation and lot/odd-lot conversion remain unchanged. (#114; PR #151, order_executor#17)
  • sim(): Reject empty or all-NaN positions early with a ValueError explaining add(fill_value=0) for combining different assets and explicit zeros for cash. Normal zero and sparse positions still pass shape validation. (#65; PR #149)
  • Backtest engine: Reject zero, negative or infinite trading prices for positions still tracked by the engine before division, with an asset/row-specific ValueError, preventing division errors or NaN equity curves. Never-held assets and existing NaN handling are unchanged; upstream prices are not modified. (#45; PR #150)
  • Report: Export custom Series/DataFrame trading prices as a custom holdings-config marker, fixing JSON/HTML export failures and displaying Custom/自訂 in the viewer. The live Report retains its price matrix; execution metadata cannot reconstruct it and falls back to close on read-back. (#76; PR #152, new_website#80)
  • Pyodide downloads: Convert ArrayBuffers in chunks of at most 8 MiB before assembling Python bytes, avoiding JavaScript single-string length limits for large datasets. All byte values are preserved; overall browser memory limits still apply. (PR #153)
  • Report.upload(): Fix cloud runs using the legacy upload API reporting success before checking the server response. Non-200 HTTP responses, unparseable JSON, and server-reported errors now return an error result. When FINLAB_FORCED_STRATEGY_NAME overrides a different explicit report name, emit a warning identifying the deployment strategy that receives the upload. (#141)
  • CloudReport.from_cloud(): Load the complete website report when its metrics.backtest.updateDate is newer than the canonical report, avoiding stale performance and holdings. Equal, older, missing, or nonnumeric dates retain canonical loading; this comparison does not distinguish reruns covering the same data date. (#137)
  • ml.feature.TalibIndicatorFactory.generate_feature_names(): Preserve first-sampled order when deduplicating feature names, preventing Python hash randomization from changing feature column order across processes with the same random seeds. (#135)
  • data.universe(): Include table rows whose symbol / stock_id matches the active universe only after trimming leading or trailing whitespace, and emit a UserWarning instead of silently excluding those rows. Returned identifiers retain their original values; the source data still needs whitespace cleanup. (#132)
  • ml.DEnsembleModel: Fix feature selection deduping with set(...), which let Python hash randomization reorder selected feature columns across processes and could change model outputs even with the same data and seeds. Deduping now preserves the sampled order.
  • ml.DEnsembleModel: Fix pandas 3 Copy-on-Write compatibility when filling NaN feature-importance values; the chained inplace=True replacement is now an explicit assignment.
  • Behavior change; backtest results may change: data.indicator(..., adj=True) now really calculates with adjusted prices. Through 2.0.18, adj was passed unchanged to TA-Lib, which silently ignores keywords it does not know, so the indicator used unadjusted prices without an error; adjust_price=True used adjusted prices in both versions. finlab.ml.feature with adj=True likewise now calculates indicators on adjusted prices: through 2.0.18 the indicators used unadjusted prices, yet the normalized ones (moving averages, MACD, MOM, and so on) were divided by the adjusted close. Code written with adj=True on 2.0.18 or earlier may get different indicator values, signals, and backtest results on 2.0.19 and later. Unsupported adj_open / adj_high / adj_low / adj_close / adj_volume kwargs now raise a clear error instead of silently accepting adjusted-price intent that would not take effect. (Behavior change documented on 2026-09-30, Issue #274)
  • MaeMfeAnalysis.display(): Remove the dependency on Plotly figure_factory.create_distplot and build the distribution traces with graph_objects, restoring compatibility with Plotly 7.
  • ffn_core: Use compatible month-end/year-end frequencies for monthly conversion and freq="m" / freq="y" plots, fixing pandas 3 ValueError while preserving existing calls and sampling dates on pandas 2.1 and later. (#70; PR #144)
  • Report.position_info2(): Fix assigning adjusted-price fallbacks into an immutable Index when convertible bonds lack raw closing prices. Assets with raw prices continue to use them. (#64; PR #143)
  • Report numbers may change report.metrics.sharpe_ratio(riskfree_rate=...), sortino_ratio(riskfree_rate=...) and report.get_metrics(riskfree_rate=...): every call is computed with the riskfree_rate passed in. A report used to keep the riskfree_rate of its first calculation and return the same numbers whatever was passed afterwards: once computed at the default (0.02), a different value passed later had no effect; when the first call passed another value, later calls at the default returned the numbers computed with that value too. The affected metrics are those that use the risk-free rate, such as Sharpe and Sortino; annual return, max drawdown, volatility and Calmar do not change with riskfree_rate. (#127; added 2026-09-30, Issue #223)
  • PortfolioSyncManager.update(): Fix the stock-list drift warning to name force_override_difference=True instead of the nonexistent force_override=True; update decisions are unchanged. (#75; PR #142)

Documentation

  • Multi-strategy portfolios: Clarify that every update() call needs the chosen odd_lot / margin_trading options, which apply only to strategies rebuilt on that call. (#62; PR #145)
  • Trailing stops: Clarify the entry-cost denominator for ordinary long positions and distinguish exit signals from fills. Entry 100, peak 150 and trail_stop=0.2 give a threshold of 130. (#69; PR #146)
  • Custom benchmarks: Use a Market.get_benchmark() override before backtesting. Changing the benchmark requires rerunning sim(); assigning report.benchmark afterward does not update all charts and statistics. (#77, #80; PR #147)
  • Market timing: Use FinlabDataFrame multiplication, position *= timing, instead of deleting cash-day rows. Follow native alignment rules without unnecessary reindex or casts. (#112; PR #148)

Engineering

  • Pyodide wheels: Build only CPython 3.13 for the current website runtime, avoiding incompatible Python 3.14/3.15 wheels selected by newer cibuildwheel releases. (PR #157)
  • tests: Mark all five CloudReport freshness regression cases as unit so CI runs them with pytest -m unit; fix Ruff lint and formatting issues in the feature-name ordering tests. (#137, #135)

2.0.18 (2026-08-19)

Fixed

  • data.get() / data.gets(): Improve Pyodide browser compatibility. Runtimes with WebAssembly JSPI retain parallel pyfetch downloads; runtimes without stack switching automatically fall back to sequential synchronous XHR inside the Web Worker. If the metadata JSON POST fails CORS preflight, FinLab retries each item with a CORS-safelisted form POST instead of aborting the strategy on non-JSPI Safari / WebKit runtimes.
  • FinlabDataFrame.rank(axis=1): Fix tie handling. The cross-sectional numpy fast path assigned ordinal ranks (equivalent to method='first'), disagreeing with pandas' default method='average', so tied cells on low-cardinality / discrete factors received distinct percentiles (a tied group had non-zero cross-sectional std). Tie handling is now delegated to pandas while keeping the pre-index_str_to_date performance win (quarterly data is no longer expanded to daily before ranking). Behaviour change: strategies that threshold on rank(pct=True) over factors with many tied values may select different stocks than before.
  • sim(..., upload=True): Fix uploaded reports wiping the tier set by cloud deploy and the schedule set by cloud schedule set. The Firestore write omitted updateMask, which means whole-document replacement, so tier and schedule fields absent from the report payload were deleted; the write now touches only the report's own fields. This only bit when the report name matched the deployment SID — the previously recommended workaround of using different names is no longer needed from this release on.

2.0.17 (2026-08-02)

Added

  • finlab status / finlab auth status: Add a concise account and data-status summary showing the current plan, core Taiwan-price data freshness, consumed/total daily download quota, and the next daily quota reset (08:00 UTC+8), while retaining the existing active-session details.
  • Standalone strategy reports: free-plan users now see a VIP upgrade prompt, including the offer deadline when available. The upgrade link carries report-source attribution and opens the payment page in a new tab, making full backtests, current data, and cloud scheduling discoverable from the report.
  • report.get_benchmark_stats(): Add a benchmark performance-stats API. Returns the same ffn stats fields as get_stats (cagr, daily_sharpe, max_drawdown, ...) computed on the backtest-period benchmark (report.daily_benchmark), so strategy-vs-market comparisons can be done programmatically without recomputing from market.get_benchmark().

Fixed

  • finlab/core: Fix standalone strategy reports mixing Traditional Chinese and English during the initial render and across analysis sub-tabs. Locale selection now completes before the first render and consistently covers profitability, risk, risk/reward, win-rate, liquidity, trade-detail, and holdings labels, dates, pagination, empty states, and chart tooltips.
  • data.get() / data.gets(): Fix free users continuing to receive the truncated, end-of-2018 free cache after upgrading to VIP in the same Python interpreter. Normal free-cache hits now perform a lightweight entitlement metadata recheck; when a paid role is detected, the session's old in-memory free cache is cleared, the insufficient .recent merge path is skipped, and paid full data is downloaded directly. Existing fast paths for paid caches, prefer_local_if_exists, and use_local_data_only are unchanged.
  • cli: Much faster CLI startup. import finlab no longer imports pandas/numpy at load time (FinlabDataFrame became a lazy attribute), cutting finlab --help startup from ~1.3s to ~0.2s; from finlab import FinlabDataFrame continues to work unchanged.
  • data: Remove pyarrow.feather deprecation warnings on pyarrow 24+. Data-cache reads/writes now use pyarrow.ipc (Feather V2 is the Arrow IPC format, so files are unchanged and fully interoperable across versions); backtests and data.get() no longer emit pyarrow.feather.read_table is deprecated FutureWarnings.

2.0.16 (2026-07-19)

Added

  • cli: Restore the finlab shell command (console script). After pip install finlab, finlab --help works directly without typing python -m finlab; also add finlab auth login / finlab auth logout / finlab auth status subcommands for compatibility with the old CLI interface.
  • cli: finlab logout is now idempotent. With no prior login it prints "Not logged in. Nothing to do." and exits 0; when credentials come from environment variables it instead explains which of FINLAB_REFRESH_TOKEN / FINLAB_SESSION_ID / FINLAB_API_KEY to unset.

Fixed

  • auth: Fix a stale-credential bug where a process could keep using the in-memory id_token for up to an hour after logout (clear_session() now also invalidates the in-memory token cache).
  • auth: Token refresh now retries transient failures (network errors and HTTP 429/5xx, up to 3 attempts with exponential backoff and shorter retry timeouts) and distinguishes a rejected refresh token (400/401/403 — prompts rerunning finlab login) from transient server/network errors; error logs no longer echo response bodies.
  • cli: Harden the legacy Google/Firebase login flow (finlab.cli.login): optional dependencies (flask, google-auth-oauthlib, pywebview) are imported lazily, so importing the module without the finlab[cli] extra no longer fails and missing deps raise an actionable pip install 'finlab[cli]' message; the local callback uses a dynamically assigned port instead of hardcoded 5000; the login wait has a 180s timeout with guaranteed webview subprocess cleanup (no more indefinite hangs); missing pywebview falls back to the system browser; stdout redirection is restored via try/finally; a corrupted user.txt store recovers automatically and credential files are written with 0600 permissions.

Engineering

  • CI: unit tests now run on a Python 3.10–3.13 matrix; add a packaging job (build sdist/wheel, twine check, then a clean-venv install running finlab --help and finlab auth logout smoke tests).
  • tests: add unit tests for CLI commands, login-flow hardening, auth retry behavior, and packaging smoke tests that block optional dependencies via an import hook to simulate a minimal environment.

2.0.15 (2026-07-18)

Added

  • df.sector(by=...): The sector accessor is now callable with a custom classification. by accepts a dict (symbol → sector name), a pd.Series, or a time-varying pd.DataFrame (index=dates, columns=symbols); symbols not listed are excluded (NaN). Works with every sector.* method (rank, zscore, mean, ...), e.g. roe.sector({"2330": "AI", ...}).rank().
  • df.sector.map(): Add sector-value broadcasting. Broadcasts sector-level scalars (such as sector weights) into a DataFrame with the same shape as the original data, for composing with factor scores, e.g. roe.sector.rank() * roe.sector.map({"半導體": 1.2}).
  • df.weight.by_group(): Add group (sector) weight allocation. Each day, holdings within each group are normalized so the group total equals the specified share, e.g. selected.weight.by_group({"半導體": 0.5, "金融保險": 0.3}). Accepts the same custom-classification by argument as sector(by=...) and a default share for unlisted groups. Shares summing below 1 leave the remainder in cash, as does a group with no holdings that day; shares summing above 1 raise ValueError.
  • backtest.sim_batch(): Add a formal native batch screening API. Pass a mapping of compatible positions and each is backtested with the same formal Cython engine as sim, sharing the package/data freshness check, market dataset prefetch, and prepared market price inputs across the batch. A failing position does not cancel its siblings; each result is a versioned, JSON-safe item with provenance (schema finlab.agent-research.v1) and structured progress_callback events are supported. Metrics for all completed items use a common evaluation window (the overlap of the strategies' input periods on the shared market-price index, exposed as comparison_period_start / comparison_period_end) so entry-timing differences cannot skew comparisons; a batch whose valid positions share no overlapping price period raises ValueError.
  • sim(..., metrics_only=True): Add a lightweight metrics-only path. It skips building Report, trades, MAE/MFE, HTML, and upload payloads, but runs the same validation, market preparation, rebalance calendar, and backtest engine, returning the same Metrics type exposed by report.metrics.
  • data.get() / data.gets(): Add progress='display' | 'event' | 'silent' and progress_callback for structured download-progress events ({phase, dataset?, completed?, total?, cache_hit?, download_bytes?, elapsed_ms, timestamp}), so agents and workers outside Jupyter can avoid HTML display objects.
  • dataframe.neutralize_industry_batch() / factor_alignment_profile(): Add batch industry neutralization and factor-alignment profiling. Factors with identical dates and symbols share category mapping, date conversion, and vectorized industry passes, and the output preserves the input container shape. Default categories use a cache tied to the active market and data lifecycle, with data.clear_category_cache() to invalidate it and data.category_cache_stats() to inspect hits.

Changed

  • data.search(): Return type changed from list[str] to pd.Series (object dtype, name "dataset"), enabling vectorized filtering such as .str.contains(); iteration and list() conversion behave as before.

Removed

  • sim() / sim_batch(): Remove fast_mode. Sparse rebalance-day returns cannot produce reliable Sharpe ratios or maximum drawdowns, so existing sim(..., fast_mode=True) and sim_batch(..., fast_mode=True) calls now raise TypeError; use the formal metrics_only=True paths for lightweight screening.

Fixed

  • online: Fix Sinopac broker compatibility with shioaji 1.7.0, which removed the fetch_contract parameter from login() in favor of on-demand contract loading. Replace the buggy _resolve_exchange mechanism (which manually guessed TSE vs OTC by trying both exchanges via snapshots) with _get_contract that delegates exchange resolution to shioaji's own contract lookup (api.contracts.get() on 1.7+, api.Contracts.Stocks[] on <1.7). Fixes OTC stocks (e.g. 8042) silently disappearing from get_stocks() results. Compatible with shioaji 1.3–1.5 and 1.7+.
  • hold_until(): Fix handling of NaN exit values. The exit frame previously went through an extra .ffill(), so once a stock's exit turned True and later values became NaN (e.g. missing data), the True was forward-filled indefinitely and the stock could never re-enter. NaN exit values are now treated as "no exit signal" (matching exit_when()); index gaps still carry the last observed exit state forward, so sparse-frequency alignment behavior is unchanged.
  • data: Revert 2.0.14's full-table dataset bucket routing. No-colon full-table datasets such as security_categories and tw_business_indicators actually live in each market's item bucket, so routing them to table buckets made downloads read the wrong GCS bucket; they now resolve to item buckets again.

2.0.14 (2026-07-03)

Added

  • cli: Add python -m finlab blog commands for reading FinLab blog/tutorial content through top, search, and show; show --paid reuses the Firebase auth session from finlab.login() for VIP content without printing or asking users to paste tokens.

Fixed

  • data: Fix bucket routing for no-colon full-table datasets. Full-table datasets such as tw_business_indicators and us_fundamentals now use each market's table bucket, while item datasets such as price:收盤價 and us_price:close continue using item buckets, avoiding wrong GCS bucket reads during batch table downloads.
  • finlab/core: Update the strategy holdings dashboard price displayed next to position P&L. Open Taiwan-market holdings now display the realtime raw market price c instead of a stale report-snapshot currentPrice; P&L / cumulative return calculations still use adjusted price a with entryAdjPrice, preserving the split/dividend-adjusted return basis.
  • online: Fix Masterlink odd-lot price updates so intraday odd-lot, after-hours odd-lot, and emerging-market odd-lot orders cancel the remaining quantity and recreate an odd-lot order at the new price instead of directly modifying the odd-lot order price through the broker API.
  • report.position_info2() / CloudReport.from_cloud() / cloud_functions/report_projection / finlab/core: Fix dashboard holdings display when canonical holdings and legacy position documents are temporarily out of sync, which could show a profitable exited position as stopped out. The dashboard now prefers uploaded report holdings, including the canonical cloud read path, and avoids labeling positive regular exits as stop-loss rows when no trailing stop is configured.

Engineering

  • ci: Limit wheel build workflows to stable pushes or manual workflow_dispatch, so main commits no longer trigger cross-platform wheel builds. The regular wheel workflow also drops cp38-* from CIBW_BUILD, matching cibuildwheel 4.x's removal of Python 3.8 support.
  • process: Add repo-level agent instructions requiring every future commit to update both docs/change-log.md / docs/change-log.en.md and the pyproject.toml package version.

2.0.13 (2026-06-05)

Fixed

  • data/storage.py: When legacy .pickle and current .feather cache files coexist for the same dataset, FileStorage now chooses the newest file by mtime and removes the stale sibling, preventing refreshed data from being shadowed by an older cache file.

2.0.12 (2026-06-01)

Added

  • sim() / hold_until(): Add the trail_stop_activation parameter, allowing trailing stops to activate only after a position first reaches a configured profit threshold. The default None / 0 preserves existing behavior; sim() supports both long and short positions, while hold_until() covers the existing long-only rotation flow.
  • finlab/core: Add a unified settings modal to the strategy analytics dashboard top bar, grouping language, light / dark / system appearance, and rise/fall color preferences (default, East red-up/green-down, West green-up/red-down).
  • finlab/core: Standalone HTML reports embedded in an iframe now support parent-page postMessage sync for language, light / dark theme, and rise/fall color preferences, including querying the report's current preferences.
  • finlab/core: Standalone HTML reports still open the built-in right-side stock panel by default inside an iframe; when loaded with ?stockPanel=parent (or stockPanelTarget=parent, parentStockPanel=1), stock clicks send finlab:openStockPanel to the parent page so the outer app can open its own stock panel.
  • report.to_html(): Standalone HTML reports now set the browser tab title and FinLab favicon; use report.to_html(path, title="Strategy Name") to customize the tab title.

2.0.11 (2026-05-31)

Fixed

  • report.display() / report.to_html(): Fix the holdings dashboard dropping current-period positions that have already exited via stop_loss / take_profit and now have zero weight when monthly rebalancing uses resample_offset. Current-period exited positions now remain visible in the holdings table and pie/bar charts, with the table action column marking them as stopped out or profit-taken.
  • report.display() / report.to_html(): Stopped-out / profit-taken positions now display realized report return and exit price instead of recalculating cumulative return from the latest price, preventing a stopped-out row from turning positive after a post-exit rebound.
  • report.display() / report.to_html(): Pie and bar charts still include current-period exited positions, but chart labels no longer append stop/take-profit status text to avoid overflowing labels. The top Analysis / Holdings active tab style also removes the underline while keeping the outer border visible.

2.0.10 (2026-05-28)

Fixed

  • report.display(): Fix the dashboard current holdings panel dropping short positions with negative weights. Chart position sizing now uses absolute exposure, and canonical report projections preserve short holdings.

2.0.9 (2026-05-27)

Changed

  • data.set_market("rotc"): ROTC is now a first-class market code. It synchronizes ROTCMarket and resolves generic Taiwan dataset names to rotc_ tables, so data.get("price:收盤價") and data.get("monthly_revenue:去年同月增減(%)") work directly in the ROTC context with the same strategy style as listed/OTC and other markets.
  • data.search(market="rotc"): ROTC market search now returns only rotc_ catalog entries, avoiding listed/OTC financial-statement and other Taiwan-market tables in ROTC discovery results.
  • ROTCMarket.sim_datasets(): Add ROTC backtest prefetch datasets keyed by trade_at_price and touched_exit, loading the required ROTC OHLCV tables so sim() works with the default market after data.set_market("rotc").

Tests

  • Add unit coverage for ROTC market context, catalog mapping/search, and ROTCMarket.sim_datasets(), plus live-data validation that ROTC price, monthly revenue, and overlapping Taiwan financial-statement columns can build positions and run backtests.

2.0.8 (2026-05-18)

Changed

  • finlab/core: Rebuild the chart / strategy analytics web component bundle (everything.js, style.css) so the 2.0.8 wheel ships the latest dashboard and chart frontend assets.
  • report / CloudReport: Canonical report payloads now include the original holdings / position_info2 data. The legacy projection prefers canonical holdings when building position2, preventing dashboard fields such as currentPrice, profit, and entryAdjPrice from being lost when only execution / trades are used for reconstruction, and removes the unused rsv20 placeholder from synthetic legacy positions.

Fix

  • data.transform.process_data(): Fix the close-price dataset key used when aligning PMI / NMI data to trading dates. The lookup now uses canonical price:收盤價, avoiding escaped-unicode keys being passed into data retrieval and tests.
  • cloud_functions/report_projection: Preserve website strategy display names. When a website-triggered cloud run uploads with the strategy UUID as canonical sid / name, the legacy dashboard keeps the existing user-facing strategies.<sid>.name instead of overwriting it with the UUID.
  • report.position_info2(): Fix the position dashboard cumulative return / P&L basis when trade_at_price="open" or another non-close trading price is used. entryAdjPrice now follows the actual trading price basis instead of always using the entry-date adjusted close, preventing cases such as 2492 華新科 where the table displayed a 5/13 open entry price while cumulative return was calculated from the 5/13 close, showing +3.00% instead of the return implied by the visible entry price.

Engineering

  • build: Backfill the stable-branch wheel build configuration to main. Add cibuildwheel GitHub Actions for Windows, macOS universal2, Linux x86_64 / aarch64, and Pyodide wheels; pyproject.toml now also defines the Pyodide wheel repair command so wasm wheels strip Cython source files.

2.0.7 (2026-05-12)

Changed

  • dataframe: Keep raising AttributeError when external code assigns directly to FinlabDataFrame.index, and expand the error message to explain that the guard prevents financial statement / monthly revenue data from being used before it was available, avoiding lookahead bias. The message now lists safe migration paths: use .deadline(), use .index_str_to_date(), or convert to pd.DataFrame(df) first when users intentionally want to manage pandas timing semantics themselves. FAQ entries now also document the recommended AssetGrowth-style factor pattern and external-data integration path.
  • pandas: Remove Pandas 4 compatibility warnings. _safe_ffill() no longer reads the deprecated future.no_silent_downcasting option on pandas 3+, while pandas 2.x checks that the option exists before using it; Report, Metrics, report projection, and market get_freq() implementations now use pandas' recommended uppercase daily frequency "1D" to avoid 'd' is deprecated warnings during resample / offset parsing.
  • finlab/core: Rebuild dashboard and strategy analytics web component assets so the 2.0.7 wheel ships the latest report frontend bundle.

Fix

  • auth: FINLAB_ID_TOKEN is now accepted as a canonical id_token auth source, allowing Pyodide / browser runtimes to pass the new auth token through the environment; legacy finlab.login(api_token) no longer writes FINLAB_ID_TOKEN, preventing an API token from being misrouted as a Firebase id token.
  • report.upload(): Preserve hyphens in strategy names so website strategy UUIDs / Firestore document ids are not converted to underscores during report upload.
  • data/storage.py: FileStorage.save_expiry() now records the actual expiry.pkl file mtime in _expiry_loaded_time, preventing another process's expiry update from being skipped when wall-clock time and filesystem mtime differ.

Engineering

  • backtest/sim.py: Remove the duplicate _validate_sim_inputs() call inside _prepare_simulation_inputs(), keeping validation at the sim() entrypoint.
  • portfolio: Stabilize the rebalance + stop trigger test by forcing the second report's next rebalance into the future, avoiding sensitivity to live data lag.

2.0.5 (2026-05-02)

Fix

  • finlab/core: Rebuild standalone report assets so clicking a position opens the stock panel correctly outside the embedded dashboard context. Standalone HTML reports now dispatch the stock-panel open event in the top-level page, include the close control for the panel overlay, and allow normal vertical scrolling instead of locking the report body height.

2.0.4 (2026-05-01)

Fix

  • backtest/sim.py: Moved _prefetch_market_datasets() out of _prepare_simulation_inputs and now call it before sim() enters _backtest_context(). Previously prefetch ran inside the prefer_local_if_exists=True context, causing data.gets() to skip expiry checks and .recent merges. If a sim_dataset's local pickle (e.g. etl:adj_close) had not yet been refreshed at session start, sim would silently use the stale cache and report.creturn.index[-1] would be cut off earlier than user code's data.get('price:收盤價').index[-1]. Hoisting prefetch out of the context lets every dataset listed by sim_datasets() run through the normal expiry / recent merge path before backtest cache lockdown.

2.0.3 (2026-04-28)

Performance

  • data.gets(): Parallelize the stale-cache .recent merge loop with ThreadPoolExecutor (up to 8 workers). For a 4-dataset all-stale fetch at ~200ms RTT, wall-clock collapses from ~1030ms to ~420ms (2.4× speedup).
  • data/auth.py, data/get.py: Unify the download path to a single GET → pre-allocated bytearray(content_length) + memoryview slice writes → feather.read_feather(pa.py_buffer(raw)) zero-copy parse → set_dataframe_bytes verbatim persist. Drops the old stream-to-disk per-chunk callback, feather metadata validation read, and pandas re-encoding round-trip. Chunk size raised from 64 KiB to 1 MiB, cutting progress callbacks for a 100 MB download from ~1600 to ~100. Local micro-benchmarks show consistent 1.21-1.28× speedup across small / medium / parallel scenarios.

Refactor

  • data/get.py: Remove _download_persist_file_storage_misses, _download_file_storage_misses_to_storage, _download_persist_one_file_storage_miss, _validate_feather_file, and the isinstance(target_storage, FileStorage) branch in _materialize_full_misses. FileStorage and CacheStorage now share one _download_full_miss_frames → _persist_full_miss_frames path.
  • data/get.py: Extract _run_parallel_tasks(keys, worker) generator, shared by _resolve_recent_candidates and _download_full_miss_frames_threaded (same min(8, n) ThreadPoolExecutor + as_completed pattern). Single-task case falls back to in-thread execution.
  • data/auth.py: Delete _download_ipc_file, add _download_feather_bytes (streaming with optional progress lives here), and have _download_dataframe return (df, raw_bytes).
  • data/storage.py: Add FileStorage.set_dataframe_bytes(name, payload, expiry) — writes raw feather bytes verbatim, skipping df.to_feather re-encoding. Remove set_feather_file, set_feather_bytes, and the unused validate_fn parameter from _atomic_file_replace. set_dataframe and set_dataframe_bytes share a _persist_feather helper.
  • Net -213 lines, 162 related tests still green.

2.0.2 (2026-04-28)

Changed

  • markets/tw.py: TWMarket.sim_datasets() prefetch list now reincludes the five always-loaded TW tables (benchmark_return:發行量加權股價報酬指數, security_categories, price:成交股數, etl:is_flagged_stock, etl:market_value) so they ride a single batched request instead of trickling in via on-demand fetches during sim. This partially reverts 2.0.1's on-demand split — peak RSS rises slightly in exchange for a steadier batched-quota profile.
  • finlab/core: Rebuild everything.js and style.css web component dashboard assets.

Fix

  • finlab/core: Throttle dashboard iframe height updates with requestAnimationFrame and compare getBoundingClientRect() height to prevent ResizeObserver/scrollHeight feedback loops in certain layouts.

2.0.1 (2026-04-22)

Changed

  • finlab/core: Update web component dashboard assets.
  • finlab/core: Bundle the Cython core and dashboard assets directly in the main repo instead of cloning the finlab_core submodule.

2.0.0 (2026-04-04)

View the FinLab 2.0.0 feature introduction

Engineering

  • exceptions: Add finlab.exceptions module with structured exception hierarchy (FinlabError, DataError, BacktestError, BrokerError, AuthError, PortfolioError, ConfigError)
  • backtest: Refactor 500-line sim() into 5 independently testable stage functions (_validate_sim_inputs, _prepare_price_data, _normalize_position, _execute_simulation, _build_sim_report), passing data between stages via _NormalizedPosition and _SimulationResult dataclasses
  • data: Add DataContext class encapsulating 13 module-level globals into a single object, with override() context manager for safely overriding state temporarily, laying groundwork for parallel strategy execution
  • optimize: Remove eval() call in combinations.py, replace with AST-based safe condition parser to eliminate arbitrary code execution risk
  • exceptions: Narrow 47 except Exception clauses across 20 files to specific exception types (requests.RequestException, OSError, KeyError, etc.), improving error observability in the trading system
  • ci: Add pytest-cov coverage threshold (fail_under=50), enforced in CI, excluding vendored/submodule code
  • ruff: Re-enable BLE001 (blind except) and C901 (mccabe complexity) lint rules, with per-file-ignores for functions pending refactor

Features

  • data: Add pip-style real-time download progress to data.gets(), supporting multi-row concurrent updates during parallel downloads. Auto-detects environment: terminal shows ANSI colored progress bars (with speed and size), Jupyter Notebook shows HTML progress bars. Correct CJK character width alignment, zero extra dependencies
  • polars_frame: Add .cs accessor to PolarsFrame for cross-sectional data transforms (aligned with FinlabDataFrame.cs):
  • rank() — percentile rank
  • zscore() — standardization
  • demean() — de-mean
  • winsorize(lower, upper) — percentile winsorization
  • bucket(n) — equal-quantile bucketing
  • polars_frame: Add .sector accessor to PolarsFrame for within-industry data transforms (aligned with FinlabDataFrame.sector):
  • rank(), mean(), std(), median(), sum(), min(), max(), count()
  • zscore(), demean(), winsorize(), bucket()
  • polars_frame: Add 5 statistical methods to PolarsFrame.rolling(n):
  • std() — rolling standard deviation
  • var() — rolling variance
  • skew() — rolling skewness
  • kurt() — rolling kurtosis
  • median() — rolling median
  • dataframe: Add 7 within-industry aggregation methods to df.sector accessor:
  • mean() — industry mean
  • std(ddof) — industry standard deviation
  • median() — industry median
  • sum() — industry sum
  • min() — industry minimum
  • max() — industry maximum
  • count() — industry valid count
  • dataframe: Add df.weight accessor for portfolio weight construction:
  • cap_industry(max_weight) — cap max weight per industry, redistribute excess proportionally
  • clip_by_volume(total_fund, max_participation_ratio) — clip individual stock weights by average daily volume
  • inverse_volatility(window) — inverse-volatility weighting, lower-volatility stocks get higher weight
  • risk_parity(window) — risk parity weighting, equalizing risk contribution per holding
  • correlation(diversify) — adjust weights by correlation, diversify or concentrate
  • target_volatility(target, window) — scale weights to achieve target annualized volatility
  • limit_turnover(max_turnover) — cap two-way turnover between rebalances
  • drawdown_control(max_drawdown) — automatically reduce exposure when drawdown exceeds threshold
  • dataframe: Add df.cs accessor for cross-sectional data transforms:
  • rank() — percentile rank
  • winsorize(lower, upper) — percentile winsorization
  • bucket(n) — equal-quantile bucketing
  • zscore() — standardization
  • demean() — de-mean
  • dataframe: Add df.sector accessor for within-industry data transforms (same 5 methods as df.cs, computed within industry)
  • online: Add PositionStreamMixin for real-time position streaming:
  • Hybrid mode: initialize via get_position(), update in real-time via Fill events, periodic reconciliation polling (default 30s)
  • Add PositionUpdate dataclass with snapshot_key() deduplication
  • RealtimeProvider auto-inherits subscribe_positions() / on_position() interface

Refactor

  • dataframe: Extract _fetch_refined_categories() as shared helper, eliminating duplicate industry name cleaning logic between FinlabDataFrame._get_column_categories and PolarsFrame sector accessor
  • polars_frame: Extract _build_grouped_op() and _build_sector_agg() helpers, unifying expression construction logic in _CrossSection and _SectorOp
  • dataframe: Refactor FinlabDataFrame into a package (finlab/dataframe/), containing core.py, weight.py, cs.py, sector.py, existing import paths unaffected
  • dataframe: Add from finlab import FinlabDataFrame top-level export
  • dataframe: Extract _get_column_categories() as shared helper, eliminating duplicate logic between groupby_category and industry weight methods

Fix

  • analysis: Fix calc_shapley_values and calc_factor_return always raising ValueError due to using Python not on pandas boolean Series instead of element-wise ~ operator (#71)

Docs

  • market: Fix non-existent dataset name etl:us_adj_close in docs, corrected to us_price:adj_close (#68)

Chore

  • Add polars as optional dependency (pip install finlab[polars])

Tests

  • Add 34 tests covering rolling extensions, cs accessor, sector AST nodes and expression construction
  • Add 68 tests covering all new weight / cs / sector methods
  • Add unit tests for calc_factor_return and calc_shapley_values, verifying mask operations and input validation

1.5.13 (2026-03-22)

Features

  • data: Add index parameter to universe(), us_universe(), set_universe(), set_us_universe() for filtering US stocks by market index (S&P 500, NASDAQ 100). Combinable with sector, industry, exchange (#55, #57)
  • market: Add Taiwan convertible bond market (TW CB Market) with to_html support in finlab_core (#64, 399e789)

Fix

  • storage: Fix _check_and_apply_reset_time being redundantly checked across multiple Python processes. Use a marker file to record applied reset timestamp and expiry.pkl mtime, shared across processes. Also fix bug where save_expiry() failure still wrote the marker, causing subsequent resets to be permanently skipped (34c5055)
  • analysis: Fix MaeMfeAnalysis.calc_edge_ratio() ZeroDivisionError when entry_price is zero in US market backtests, and deduplicate edge ratio computation in display() (#45, #66)
  • compat: Replace deprecated pandas resample aliases ('M', 'Y', 'A') with 'ME'/'YE' (pandas ≥ 2.2), centralize to finlab.compat.MONTHLY_END / YEARLY_END constants, and fix string-based version comparison bug (#63)
  • portfolio: Fix PortfolioSyncManager.from_cloud() / to_cloud() using wrong token parameter name (hardcoded api_token instead of token_type), and fix notebook.py passing tuple instead of string (#52)
  • hold_until: Fix ValueError in hold_until() under pandas 3.0 Copy-on-Write mode (#67)
  • report: Fix position2 not populated with trade data and incorrect entryAdjPrice calculation (d89f782)
  • report: Fix "hold" actions not normalized in cloud report round-trip, and auto-rebuild stale Cython modules (c476cba)
  • report: Fix updated_at not propagated to positionConfig.created in CloudReport.__init__ (26eb765)
  • online: Update submodule — fix token_type not routed correctly in dashboard API calls (87ecf68)
  • data: Fix trailing delimiter in IndicatorName.decode for parameterless indicators (87e3c80)
  • data: Fix universe not reset after big_investments(), causing column mismatch in subsequent strategies (0c84806)
  • compat: Fix _pd_version parser failing on pandas prerelease version strings (5399d97)

Refactor

  • deprecation: Improve deprecation warnings to show once, extract deadline constant, deduplicate auth imports (b7f6072)

Docs

  • backtest: Update sim() docstrings and fix batch order example — progress applies to remaining delta (#56)
  • portfolio: Remove non-existent create_report() and get_returns() from Portfolio examples (#51)

Tests

  • Fix 14 test failures across auth, projection, and E2E suites (a194ea1)

Chore

  • Bump Docker base image to Python 3.12 and update dependencies (66a44df)
  • Add twine to dev dependencies (28bb8eb)

1.5.12 (2026-03-17)

Fix

  • data: Fix data.get() returning stale data in long-running sessions (e.g. Google Colab) — _loaded_datasets was completely skipping expiry checks. Now checks local expiry time and triggers merge/download when expired
  • online: Fix Sinopac broker hardcoding all stock contracts to TSE exchange, causing OTC stock real-time quote subscriptions and tick backfill to fail. Now dynamically detects TSE/OTC via snapshot API and caches results (e21f7fc)

1.5.11 (2026-03-11)

Features

  • report: Migrate report payload to canonical Firestore flow (users/{uid}/reports/{sid}) with cloud function projection for legacy docs (4719592)
  • data: Add data.get_role() and data.is_vip() for querying user role (e282344)

Fix

  • backtest: Fix sim() upload using daemon thread, causing upload to be killed on script exit (9b0b4c5)
  • data: is_tradable() now returns False when no expiry data available (conservative: can't verify freshness → don't trade) (9b0b4c5)
  • data: is_tradable and next_future_expiry now only check session-loaded datasets (b16eda4)
  • report: Fix report projection legacy parity (daily_mean, return_table, drawdown_details, trail_stop) (83a4e33, dea8afa)
  • report: Strip time-series arrays from Firestore legacy docs to avoid 40K index limit (9af94d9)
  • report: Fix market-close timestamp timezone normalization (91ba4dc)
  • report: Keep cloud report schedulers and execution in sync (11ef166)
  • report: Tighten cloud report migration validation (297de5e)
  • storage: Make file storage writes atomic across processes (8436ab8)
  • analysis: Fix LiquidityAnalysis mutating report.trades causing intermittent KeyError (a14d9fb)
  • docs: Fix i18n alternate links and redirect Location headers (784e4a1, e58b085)

Tests

  • Add E2E parity test verifying report projection legacy docs match old write_database output (5d893a3)
  • Tighten E2E parity test: compare stats, trades, all position fields and strategy summary (f2e5929, d702a84)
  • Add firebase admin report migration test coverage (7358a45)
  • Add market close timestamp normalization tests (d747ccc)
  • Fix test_data_freshness tests to align with scoped expiry behavior (9b0b4c5)

Chore

  • online: Update submodule — add Stock.pct_change across all brokers (94032d5)

1.5.9

Refactor

  • analysis: Remove Report subclass (~270 lines); to_text() and to_terminal() are now provided directly by the Report class in finlab_core. from finlab.analysis import Report still works (#43)

Features

  • data: Add an 80% quota-usage warning so data.get() notifies users before the quota is exhausted (#40)
  • schemas: Added finlab.schemas with formal dataclass/TypedDict contracts (PositionEntry, OrderEntry, PortfolioData, etc.)
  • online: OrderExecutor.generate_orders() now supports as_entries and quantity_type, with a typed convenience API generate_order_entries()
  • portfolio: PortfolioSyncManager now provides typed data access APIs: get_data() / set_data() and get_data_typed() / set_data_typed()

Fix

  • dataframe: Fix path-matching failure in FinlabDataFrame.__setattr__ on Windows that caused internal set_index calls to be incorrectly blocked
  • backtest: sim() uses market-specific default fee_ratio and tax_ratio instead of fixed defaults (#38)
  • market: get_market_by_name() supports market short-code fallback (MARKET_CODE_TO_CLASS) (#37)
  • analysis: Fix the candlestick chart spinning indefinitely when a Firestore API request fails; show an error message with a retry button instead (#23)

Docs

  • Remove the git-clone install guide for the not-yet-open-source repository (#27)
  • Improve Market.get_price() docs: add full parameter descriptions, required return values, and implementation examples (#26)
  • Added a typed data migration guide and symbol / stock_id deprecation policy notes (docs/details/typed_data_interfaces.md)

Chore

  • Add Claude Code GitHub Actions workflow (#25)

1.5.8

Features

  • verify: Add verify_strategy(), an automatic detector for lookahead bias
  • dataframe: Forbid assigning directly to FinlabDataFrame.index to prevent lookahead bias
  • dataframe: rank() gains a valid parameter for filtering on valid values
  • data: Add next_future_expiry() to automatically skip expired data
  • data: Add a data-freshness check to make sure data is up to date before trading
  • terminal: Add report.to_terminal(), a terminal chart tool for viewing backtest results without Jupyter
  • auth: Switch to Firebase browser login, with automatic session naming, env-var authentication, and FINLAB_API_TOKEN migration prompts
  • compat: Full stock_id → symbol migration; stock_id remains available for backward compatibility
  • pyodide: report.upload() now runs synchronously in Pyodide environments

Performance

  • Overall strategy execution speedup of 3.4x (12.9s → 3.8s, covering data loading, signal computation, and backtest)
Step Before After Speedup
data.get (4 calls) 0.40s 0.00s ∞
vol.average(20) 0.69s 0.62s 1.1x
pb.rank() 1.57s 1.04s 1.5x
rev.rank() 1.49s 0.50s 3.0x
is_smallest(10) 5.25s 0.40s 13.3x
sim() 3.30s 1.08s 3.1x
  • dataframe: is_largest / is_smallest switched to numpy argsort(kind='stable') instead of pandas apply(nlargest/nsmallest), preserving consistent column-order tie-breaking (13.3x)
  • dataframe: rank(axis=1) uses a numpy argsort fast path and ranks before index_str_to_date() expansion, avoiding ranking after quarterly data (51 rows) is expanded to daily (2110 rows) (3.0x)
  • dataframe: reshape() adds a fast path that skips reindex when two DataFrames already share the same index/columns
  • backtest: sim() uses prefer_local_if_exists internally to avoid redundant cloud expiry checks
  • backtest: Position normalization uses numpy array operations instead of pandas .div().fillna()
  • backtest: upload runs asynchronously on a daemon thread so it does not block backtest result return
  • backtest: Cache TWMarket._categories_cache to avoid repeated data.get('security_categories') calls
  • data: Add _finalized_cache to cache post-processed results from data.get(), skipping process_data conversion on repeat calls
  • data: Check expiry before reading the DataFrame to avoid unnecessary disk I/O
  • data: FileStorage.get_dataframe() skips the os.path.getmtime syscall on memory-cache hits

Fix

  • backtest: Fix a bug where touched_exit gap-open take-profit/stop-loss triggers multiplied position value by cr_old, inflating creturn. When the open price already exceeded the take-profit (or stop-loss) level, the touch_open path formula pos *= open_r / r diverged from touch_high/touch_low; corrected to pos = entry_pos * open_r
  • backtest: Fix floating-point drift in pdays (profitable days) calculation
  • analysis: Fix missing display() method on AlphaBetaAnalysis that caused run_analysis('AlphaBetaAnalysis') to produce no output
  • dataframe: Fix hold_until compatibility under Pandas 2.0+ Copy-on-Write
  • dataframe: Fix conversion error in is_largest/is_smallest when encountering pd.NA
  • dataframe: Replace eval() with ast.literal_eval() in industry_rank to fix a security issue
  • data: Fix truncate_end quarterly-financial truncation logic by using a disclosure-date mask, preventing accidental removal of already disclosed data
  • data: Fix _finalized_cache not including universe and truncate in its cache key
  • data: Fix _get_expiry_dict not filtering already-loaded datasets
  • storage: Fix missing mtime cache validation when sharing across multiple processes
  • portfolio: Fix Portfolio inconsistency when a strategy weight is zero
  • portfolio: Fix close-strategy incorrectly selling positions during open sync when historical data is empty
  • auth: Fix unnecessary browser login on failed token refresh
  • auth: Fix login() not checking the cached token first
  • auth: Switch to a stable machine ID instead of the unreliable uuid.getnode()
  • auth: Support id_token for backtest uploads and cloud data requests
  • sim: Fix pandas 3.0 compatibility when trade_at_price is a DataFrame

Docs

  • Add tutorials for neutralize / neutralize_industry
  • Add complete API reference and strengthen documentation structure
  • Add an introduction to the FinLab AI Skill (finlab.finance)
  • Fix incorrect take-profit / trailing-stop parameter names in the docs
  • Fix the erroneous description that trade_at_price='close' is a future-function
  • Fix ML strategy workflow docs that referenced a non-existent API

1.5.7 (2026-01-28)

Features

  • data: Improve data.search() — uses the Firestore API and simplifies output to a list of strings directly usable in data.get()
  • data: Support US stock data search (US catalog)

Refactor

  • data: Improve data-fetch and storage modules; extract shared helpers for readability

Fix

  • backtest: Fix pandas 3.0 ArrowStringArray compatibility — ensure DataFrame columns convert to a numpy array correctly
  • data: Narrow the exception scope in indicator() to avoid misleading TA-Lib install messages for invalid indicator names
  • data: Fix duplicate entries in the data catalog
  • tests: Fix randomness in the calc_regression_stats test by adding a fixed seed for stable results

Chore

  • Drop Python 3.8 support; minimum required version is now Python 3.9

1.5.4 (2025-11-21)

Fix

  • core: Fix Report error in is_rebalance_due when resample is a string
  • data: Fix FileStorage.get_time_expired failing to auto-update data when shared across multiple processes

Features

  • doc: Streamlined example documentation

1.5.4 (2025-11-14)

Refactor

  • data.universe: Refactor the universe module

Fix

  • core: Fix backward-compatibility issue in Report when is_rebalance_due is called

1.5.3 (2025-10-03)

Features

  • dataframe: Implement a truncation-window feature that accelerates data fetching based on finlab.data configuration

Refactor

  • data: Restructure the storage architecture and split modules (introduce storage.py, reshape __init__.py and universe.py) and integrate the truncation flow
  • portfolio: Streamline the trade-update flow; asset-value calculation now uses market-specific prices for greater accuracy

Fix

  • data.indicator: Correct adjust_price type validation to require booleans and improve error messages
  • label: excess_over_median/mean uses subtract for more consistent and readable arithmetic
  • backtest: Improve arguments resampling logic, unifying how resample_dates and position index types are handled

Docs

  • factor_analysis: Fix variable naming and add clearer examples; update navigation and tutorial content
  • docs: Remove navigation entries that no longer exist; update example docs and mkdocs settings

1.5.0 (2025-08-01)

Features

  • factor_analysis: Add factor-trend analysis along with short example documentation
  • plot: Add a unified plotly style with multiple color ranges and marker support; adds configurable color bands for line charts
  • plot: Enhance plot_line with advanced customization options and comprehensive examples; add plot_line_example and plot_line_financial_example
  • tests: Add support for serializing backtest results to pickle

Refactor

  • factor_analysis: Rename functions and parameters for clarity; update docs to reflect the changes
  • docs: Update the changelog and adjust module structure / naming

Fix

  • data: Add use_local_data_only and force_cloud_download options
  • feature: Improve market-configuration logic to correctly restore the original market setting in multi-core environments
  • liquidityAnalysis: Ensure trade-price and volume shapes stay aligned and convert them to DataFrame format
  • backtest: Fix position-filter logic so only valid market data is retained and converted to floats
  • doc: Fix plotly rendering issue (switch to static png)

Docs

  • Add the factor-crowding analysis page to navigation
  • Add simplified tutorials and refresh the practical-example tutorial for clarity
  • Update the practical-example documentation
  • Add KaTeX support for math rendering and remove outdated factor-crowding analysis
  • Add Yuanta Securities setup instructions to the order-API docs and remove the outdated PE-PB chart image
  • Fix Yuanta Securities references in the order-API docs

1.4.0 (2024-05-29)

Features

  • portfolio: Add get_total_position to return current holdings as a DataFrame
  • pyproject: Add shioaji support to the dev group

Refactor

  • backtest: Replace Exception with ValueError and TypeError for better error handling
  • market: Convert the Market class to an abstract base class and update the tw market

Fix

  • qlib: Improve error handling for a missing finlab[qlib] module with a more detailed message
  • data: Add use_local_data_only and force_cloud_download features
  • data: Update the get return type to FinlabDataFrame
  • data: Include additional exports in __all__
  • pytorch_nn: Remove a broken import and add ICLoss for information-coefficient loss
  • portfolio: Improve the remove-position logic and tighten update conditions
  • docker: Pin the finlab version to 1.4.0

1.3.0 (2024-05-24)

Features

  • online: Add pocket-account integration
  • fubon: Add the Yuanta Securities order-entry system
  • data: Add entry names for financial-data indicators
  • dataframe: Add a lookahead warning to the rank method
  • setup: Add classifiers for Python 3.7 and 3.8
  • requirements: Add Jinja2 and ipython
  • test: Add pytest as a dev dependency with a slow marker
  • Migrate to uv for dependency management

Refactor

  • alphalens: Remove the alphalens module and related features
  • portfolio: Improve position-removal logs in PortfolioSyncManager
  • backtest: Comment out unused global-report setup code
  • plot: Remove obsolete plot tests from plot_test.py
  • ml: Improve factory initialization and type hints in the combine function

Fix

  • setup: Enable find_namespace_packages and improve path handling
  • liquidityAnalysis: Fix disposition-stock detection logic and the date baseline for limit-up/down evaluation
  • docker: Update pandas and ta-lib versions
  • backtest: Update the backtest-upload message
  • ml: Fix variable reference in the combine function
  • analysis: Fix the end date used by drawdown
  • parallel: Fix ParallelExt compatibility with newer joblib versions
  • pyproject: Pin the Cython version for compatibility

Docs

  • docs: Rename getting-start.md and update mkdocs.yml
  • order_api: Update certificate acquisition and package installation instructions

Type Safety

  • core: Add type hints for FinlabDataFrame and the backtest module

1.2.27 (2025-05-15)

Features

  • Add the Yuanta Securities order-entry system
  • finlab.ml.feature.combine supports a dictionary of functions for feature generation
  • finlab.dataframe.FinlabDataFrame: Refactor rank to automatically handle cross-sectional comparison on financial reports, preventing potential lookahead bias
  • finlab.dataframe.FinlabDataFrame: Refactor rank to warn users when lookahead is possible

Fix

  • Fix qlib model reference

1.2.24 (2025-05-02)

Features

  • Integrate the full qlib feature set into finlab.ml; installing finlab is now enough to use qlib features (pip install finlab[qlib])

Refactor

  • report: Support report.to_html(path) for exporting the report to an HTML file
  • report: Support report._repr_html_() for direct display inside Jupyter notebooks
  • Move dashboard out of the report module into a standalone finlab.core.dashboard module

Fix

  • Fix minor data errors in report.trades related to warning stocks, disposition stocks, and full-delivery stocks
  • Fix report.weights.name and report.next_weights.name possibly being None

1.2.23 (2025-03-25)

Fix

  • Fix report.upload raising when resample=pd.DatetimeIndex

1.2.22 (2025-03-25)

Features

  • PositionScheduler supports multiple strategies and rebalance logic
  • backtest.sim automatically computes the next trading day when resample=None

Fixes

  • Update the macOS build architecture setting to keep only universal2
  • Fix resample parameter handling so it can be used directly
  • Adjust PositionScheduler.from_report to match the new resample logic
  • Update and fix associated unit tests

1.2.21 (2025-03-12)

Features

  • report.is_stop_triggered() detects whether stop-loss / take-profit has fired
  • report.is_rebalance_due() detects whether a rebalance is due
  • report.to_pickle('PATH') saves the report as a pickle file
  • report.from_pickle('PATH') loads the report from a pickle file
  • backtest.sim backtest-speed optimization

Fixes

  • PortfolioSyncManager handles the weight = 0 case
  • data.get retries automatically on download errors
  • backtest.sim skips symbols that throw exceptions automatically
  • backtest.sim fixes incorrect short-return display

1.2.20 (2024-12-09)

Refactor

  • MarketInfo is moved into the finlab.market module
  • TWMarketInfo is moved into finlab.markets.tw and renamed to TWMarket
  • fix(market_info): rename loop variable for clarity in shared memory creation
  • Use finlab.config.set_market for setting the global market info
  • Remove ml.get_market and ml.set_market; use finlab.config.set_market instead
  • Remove data.indicator(...market) in favor of finlab.config.set_market
  • Remove df.hold_until(...market) in favor of finlab.config.set_market

1.2.17 (2024-10-19)

Features

  • Rename report.weight.name to the backtest's last date for easier inspection
  • Rename report.next_weight.name to the rebalance date for easier inspection
  • Support numpy>=2.0; note that talib does not yet support it, so talib users must wait
  • Position supports optional weight storage and calculation for quick comparison between lots and weights
  • FugleAccount switches to websocket for order connection and updates
  • OrderExecutor gains progress, which interpolates between "current positions" and "target positions" for smoother rebalancing

Fixes

  • finlab.online.sinopac no longer errors when a stock has no trades for the day

1.2.16 (2024-10-02)

Fixed

  • data.get reduces unnecessary network calls
  • cli: Fix an unnecessary webview import error
  • finlab.online.sinopac: Fix get_trades converting timestamps to UTC when computing times
  • finlab.portfolio: Fix PortfolioSyncManager.to_cloud and to_json possibly producing incompatible formats

1.2.15 (2024-09-08)

Fixed

  • position.from_report: Fix weight.index being non-string
  • finlab.portfolio: Fix error raised when holding untradeable stocks
  • finlab.online: Fix error raised when attempting to trade untradeable stocks (now raises a warning instead)
  • finlab.data: Fix data.search returning empty results
  • finlab.portfolio: Fix stop-loss / take-profit being skipped at certain moments

1.2.12 (2024-8-27)

Features

  • dashboard: Add "market correlation" panel with a selectable time range
  • portfolio: Add PortfolioSyncManager.get_position(at='open') for planning the day's open positions
  • online.position: Improve position display
  • online.position: Add the ability to iterate over position info with a for-loop
  • tool.factor_analysis: Add factor-analysis features — in addition to IC, supports get_metric for computing various factor metrics
  • portfolio: PortfolioSyncManager auto-detects when smooth_transition should kick in — no smoothing on init, smoothing afterwards
  • doc: Improve the portfolio order-placement tutorial
  • sinopac: Sinopac Securities supports get_trades for reconciling account trade records

Fixed

  • data: Fix a circular import triggered by data.get on some operating systems
  • cli: Fix the finlab nb command triggering import webview

1.2.11 (2024-8-19)

Fixed

  • backtest: Fix stop-loss / take-profit occasionally using future data
  • test: Use historical data when testing the backtest system
  • report: Fix incorrect industry display in the backtest report
  • online: Fix too many decimal places being shown when placing orders
  • schedule: Fix strategy execution using the wrong token
  • portfolio.cloud_report: Support int-typed returns (used in rare cases)
  • backtest: Hide out-sample data when backtesting in-sample
  • data: Auto-abort downloads that exceed 5 minutes with an error message

1.2.9 (2024-8-3)

Feature

  • finlab.portfolio: PortfolioSyncManager.update accepts custom_position to set per-stock lot counts
  • finlab.portfolio: PortfolioSyncManager.update accepts excluded_stock_ids to ignore specified stocks

1.2.8 (2024-8-1)

Fix

  • Fix backtest errors in certain conditions

1.2.7 (2024-8-1)

Fix

  • Fix return calculation error when stocks settle at a non-close price
  • finlab.portfolio: Suppress unnecessary warnings from PortfolioSyncManager.update

1.2.5 (2024-7-29)

Feature

  • Stock dashboard: Add an option to hide stock names

Fix

  • Fix the stock dashboard so open_close_avg is labeled "average price"
  • Fix return calculation: inter-period transitions no longer accrue trading fees
  • Fix NaN handling in the dashboard

1.2.4 (2024-7-28)

Feature

  • finlab.portfolio: PortfolioSyncManager gains to_file and from_file for loading and saving data

Fix

  • finlab.portfolio: Fix PortfolioSyncManager error when handling odd lots
  • finlab.portfolio: Fix PortfolioSyncManager odd-lot values displayed to four decimal places

1.2.2 (2024-7-27)

Fix

  • finlab.cli: Fix the nb command erroring when not yet initialized
  • finlab.portfolio: Raise a more detailed error when the cloud strategy download cannot find the strategy
  • finlab.portfolio: Fix error raised when live_trading_performance is None
  • finlab.portfolio: Fix create_multi_asset_report path and return-value issues
  • finlab.portfolio: Fix errors in sync manager during stop-loss / take-profit
  • finlab.backtest: When resample=None, compute returns using holding-period returns
  • finlab.backtest: Fix NaN introduced by close-price settlement during intraday simulation
  • finlab.report: Fix exit-reason display when per-period returns are split

1.2.0 (2024-7-27)

Features

  • finlab.backtest: Add intraday dashboard and cumulative-return dashboard
  • finlab.backtest: Regardless of trade_at_price, all settlements now occur after market close
  • finlab.backtest: Open-price buy/sell with stop-loss / take-profit evaluated at the close
  • finlab.report: Add industry category to report.trades
  • finlab.portfolio: Add a new module for applying strategies to real portfolios
  • finlab.portfolio: Add tutorial documentation for using the module in real portfolios
  • finlab.cli: Add a CLI module for command-line operations and order placement
  • finlab.cli: Add tutorial documentation for using the CLI
  • Add docs describing how to use regex for industry selection
  • finlab.online: Enable automatic Fugle API login without manual password entry

Fix

  • finlab.sinopac: Fix account-balance calculation settling too early in the early hours of Sunday

1.1.6 (2024-7-11)

Fix

  • Fix offline-version data-cache reads during backtests
  • Fix Windows datetime → int32 conversion not being supported in backtests

1.1.4 (2024-7-9)

Features

  • Add strategy expiration time to remind users when data is stale and must be rerun
  • Backtest panel: Show entry and current prices together
  • Backtest panel: Reverse the default return display (most recent first)
  • Backtest panel: Improve holdings-price display

Fix

  • Fix finlab.tools.factor_analysis error caused by misaligned factor dates
  • Fix warning emitted under Pandas 2.2.0 when bridging to Pandas 3.0.0
  • ffn_core: Fix Tabular construction error
  • ffn_core: Fix Pandas 3.0.0 compatibility
  • Fix incorrect entry-price display on the backtest page
  • Backtest page: Fix stock history records
  • Backtest page: Account for recent maximum drawdown
  • Fix plot "annual average return" to handle the no-backtest case

1.1.3 (2024-6-4)

Features

  • Fix finlab.tools.factor_analysis and add multi-factor support
  • LINE Notify: Include the strategy name in notifications

Fix

  • Fix show_alerting_stocks not working

1.1.2 (2024-5-23)

Feature

  • Add the finlab.tools.factor_analysis module for factor analysis

Fix

  • Fix handling of NaN in prices
  • Fix multi-core processing logic in finlab.ml.feature.ta that raised on single-core machines
  • Fix the reference timing for the next rebalance: use the current close-price date instead of the position-change date to avoid referencing the wrong price on the next rebalance
  • Fix finlab.ml.qlib.get_models models producing inf or NaN labels during training

Refactor

  • Refactor bank-balance and settlement messaging

1.1.1 (2024-4-25)

Refactor

  • Add "Required installation" and "Model library (optional)" sections to machine_learning.md to clearly separate install commands
  • Standardize and correct market_close_at_timestamp and tzinfo on the MarketInfo and TWMarketInfo classes in market_info.py for readability and consistency
  • Convert some methods from dynamic to static implementation to improve efficiency and reduce dependencies
  • qlib.py: Handle the segments parameter in WrapperModel.fit so that custom data segments are processed correctly

Fix

  • Fix logical-condition error in market_info.py, correctly distinguishing the conditional branches
  • Adjust NaN handling in data.py so that invalid data is excluded from indicator computations
  • Adjust the multi-core processing logic in feature.py's ta function based on core count to work correctly inside Docker
  • Fix label.py errors when calling maximum_adverse_excursion to ensure correct function behavior
  • Fix label.py errors when calling maximum_favorable_excursion to ensure correct function behavior

1.1.0

Features

  • Add support for Microsoft Qlib by adding the pyqlib dependency to the Dockerfile, letting users run Qlib directly inside the Docker container.

Fix

  • Fix inconsistent version numbers across multiple files, unifying the version to 1.0.10.
  • Refactor code in finlab/ml/qlib.py, removing outdated comments and unused code to improve clarity and maintainability.
  • Update the finlab/online submodule reference to pick up the latest features and fixes.
  • Simplify run_online_test.sh and run_all_test.sh, removing unnecessary comments and code for a leaner, more efficient test process.

1.0.6

  • fix(report): Fix importlib.resources not working on python<=3.6, python==3.8,3.7, python >=3.9

1.0.5

  • fix(report): Fix importlib.resources not working on python<=3.8

1.0.4

  • feat(backtest): Add tests related to backtests and instruments
  • fix(backtest): Fix the issue of removing next_weight force to 1
  • fix(us_market): Fix support for market_close_at_timestamp supporting None
  • chore(pandas 3.0): Support the new Pandas >= 3.0.0 new resample string format
  • feat(backtest): Add sl_enter and tp_enter to mark immediate stop loss / take profit held until the next rebalance

1.0.3

Fix

  • Support running backtests from the web frontend
  • Fix report.current_trades so that when a stock name is duplicated, the latest position takes precedence
  • Fix the dashboard's monthly revenue display: revert from 1st-to-31st back to the previous behavior (previous month 31st to this month 31st)
  • Fix trail_stop exiting too early when buying the same stock again
  • When entering a position the next day while simultaneously hitting stop loss / take profit, fix the report.action and report.next_weights display for stop_trading_next_period, and add unit tests.

1.0.2

Refactor

  • Remove the tailwind default css and switch to the style.css output by vite
  • Remove the web server and switch to an iframe docsrc (verified to work on all platforms)

Fix

  • Fix the web server port being unusable when displayed on docker

1.0.1

Features

  • Display trailing stop loss
  • Control light/dark mode from the top-right of the dashboard
  • Save the dashboard to a specified path

Fix

  • Fix the issue caused by the default read/write encoding of report being cp950 on Windows.
  • Fix stop loss not being displayed
  • Adjust CSS fonts and layout on Colab
  • Make chart axis colors responsive to light/dark mode
  • Fix chart memory not being released, which caused delayed tab switching

1.0.0

Features

  • Add unit tests for Position.from_report
  • Test report.next_weights under a variety of conditions
  • Launch the brand new dashboard interface
  • Support Python 3.12

Fix

  • Fix normalization of report.next_weights when stop loss or take profit is enabled
  • Fix categorical columns not being indexed correctly by Pandas's reindex function
  • Fix plotting errors for categorical columns when adding operators

0.5.13

Features

  • Integrate Pytest (commit e7a487c): Add Pytest to make testing more efficient.
  • finlab.ml intraday return calculation (commit 8804fb1): Add a label for computing intraday returns.
  • finlab.ml.feature.ta multiprocess feature computation (commit 1ab0463): Introduce multiprocess support in the feature computation module for better performance.

Fix

  • Optimize the get_location function (commit 253e98c): Improve get_location in data.py to handle failure cases better.
  • Timezone support in market info (commits aa2756b & 79b3671): Add timezone handling in market_info.py.
  • Trailing stop loss fix (commits 6522bf7 & e7fc1f3): Adjust trade calculations in backtest.py and fix trailing stop loss behavior.
  • Dataframe import fix (commit 626ada1): Fix the import issue in dataframe.py.
  • Feature initialization fix (commit 772d659): Fix the initialization issue in ml/feature.py.
  • Force cloud download (commit 7ec28a3): Fix the force_cloud_download implementation in data.py.

Other updates

  • Refactor (commit 26d1518): Refactor the online module for a better code structure.
  • Documentation updates (commits 44a93d4, 9f5683f, cd4a894): Update the documentation for backtest.py, data.py, and ml/feature.py.
  • Version update (commit d361d7f): Bump the version to 0.5.13.
  • Increase test coverage (commit 350dfbc): Increase test coverage in the Docker configuration.
  • Test system robustness (commit 1647479): Improve the robustness of the test system.
  • Online test Position.to_list (commit f3d086f): Update online_test.py to add list-conversion coverage.

0.5.12

  • Fix online test price update in run_online_test.sh to include FUGLE-configured environment variables.
  • Adjust test_update_price in online_test.py to compare floats correctly.
  • Modify event_study.py to work with non-trading days by reindexing against business days.

0.5.11

  • Fix an error being thrown when position is zero

0.5.10

  • Fix Sinopac API orders larger than 1000 NTD failing to place
  • Fix the FinlabDataframe memory leak error
  • Fix the login UI style to avoid errors when logging in from Colab
  • Fix data.search

0.5.9

  • Fix the reshape issue in dataframe operator arithmetic
  • Fix df[bool_selector] not working

0.5.8

  • Improve the speed of data.get
  • Improve Sinopac API order-placement efficiency (though this introduced some issues, which have been fixed)

0.5.7 (2023-11-4)

Fix

  • FinlabDataFrame: Fix the __hash__ method to correctly use a UUID when none is set.
  • hold_until method: Fix the reindex handling of self, exit, and rank.
  • market_info.get_market_info: Correct the reindex logic to ensure the updated index is used.
  • fetch_data: Fix the daily-average calculation in the report section.
  • data.py: Add a User-Agent header to HTTP requests to prevent fetch errors.
  • data.py: Fix saving an empty DataFrame.
  • ffn_core.py: Fix the divide-by-zero bug in calc_sharpe.

Features

  • tools: Add finlab.tools.event_study for event-trading research and related features.
  • backtest.py: Introduce fast_mode for fast backtests that skip stop loss / take profit.
  • dataframe.py: Cache the index_str_to_date method for better performance.
  • report.add_trade_info: Use Cython to speed up the computation.
  • setup.py: Enable O3 optimization when compiling the Cython extension.
  • backtest.py: Add documentation for the sim function covering the new features and parameters in detail.
  • dataframe.py: Implement index optimization, converting string indices into dates and caching the result.
  • Adjust setup.py to change the Python requirement from >=3.7 to >=3.6.
  • Update submodules to their latest commits.

Tests

  • Add tests for the report_test.py module to guarantee correctness of the query functionality.
  • Add multiple test cases covering different data types in report_test.py.
  • Update the run_test_docker.sh script to include the new test cases.

0.5.2 (2023-09-28)

Fix

  • Fix a recursive-import issue that appeared on a few operating systems
  • Fix liquidity analysis where NaN values were being silently cast to float by Pandas
  • trade_at_price fix
  • Fix excessive memory consumption when attempting to merge

0.5.0 (2023-09-14)

Features

  • Add a trailing stop loss feature via sim(..., trail_stop=0.1) to configure the trailing stop.
  • data.universe supports market='etf', and also categories such as domestic_etf, foreign_etf, leveraged_etf, vanilla_futures_etf, leveraged_futures_etf.
  • data.get supports updating data from both US and Taiwan servers, automatically choosing the closer server.
  • data.get automatically detects and links Google Drive to store downloaded data.
  • data.get now fully supports local-side data updates: instead of re-downloading everything, patches are fetched and applied locally, and a full reset only runs when necessary (enabled by default).
  • sim(..., trade_at_price) lets you simulate trades at other prices: 'close', 'open', 'open_close_avg', 'high_low_avg', or 'price_avg'.
  • Add underlying features to ensure compatibility with the new website.

Fixes

  • Fix stop loss / take profit for short positions
  • FinlabDataFrame.hold_until now returns bool (previously int)
  • FinlabDataFrame has broader support for auto-reshape and fill arithmetic, e.g. &= and += are now supported
  • Remove the force-configured logger
  • finlab.online: Fix the timezone issue when fetching total balance.
  • data.FileStorage: Fix file-path errors on Windows.
  • ffn: Remove soon-to-be-unsupported syntax and update to pandas 2.1 compatible syntax.
  • Auto-detect and convert position.index to datetime inside backtest(position)
  • Fix upload failures when report.trades is too long
  • Fix a backtest bug where an instrument that exited last period was not re-entered this period

0.4.6 (2023-08-11)

Fixes

  • Fix aggressive-pricing orders (extra_bid_pct) failing when the order price exceeds the daily price limit; the order price is now capped at the upper/lower limit.
  • Fix the order-placement API mis-judging stop loss / take profit positions in Position.from_report.
  • Fix the Sinopac API's failure to modify the price of odd-lot orders by auto-cancelling and re-placing the order.
  • Change the monthly-revenue index to datetime format, fixing the issue where data.set_storage mode forced a re-download of monthly revenue data.
  • Prevent NaN values from appearing in finlab.dataframe.industry rank().

Features

  • Add strategy-return statistics plotting via finlab.plot.StrategyReturnStats.
  • Fugle Market Data API v1.0 is now compatible with finlab.online; v0.3 is not supported. Users are reminded to upgrade the Market Data API from v0.3 to the latest v1.0, as Fugle will deprecate the old version. Note that v1.0 uses a different authentication scheme, so v0.3 API keys cannot be used with v1.0 HTTP and WebSocket APIs. You will need to re-apply for an API key to use the latest API; please visit the key application page.
  • Add aggressive-pricing (extra_bid_pct) support to the order-placement API's "update order price" flow: buy at x% above the last trade price, sell at x% below the last trade price.

0.4.5 (2023-07-05)

Fixes

  • Fix hold_until ranking breaking when the rank parameter is inf.
  • Fix the alphalens incompatibility with pandas 2.0.
  • Fix the Sinopac API failing to place odd-lot market orders; these are now placed at the daily price limit instead.
  • Fix the title display offset on the candle plot.
  • Fix report.position being reused in sim to produce consistent results.
  • Fix the edge-ratio time-series sorting; results can now be sorted in chronological order.

Features

  • Add us_universe to select US sub-industries.
  • Add aggressive-pricing support to the order-placement API: buy at x% above the last trade price, sell at x% below.

Doc

  • Add a US-market strategy example for users to reference and learn from.
  • Add documentation for the market parameter in data.indicator when using US data, making it easier to understand how to use it.

0.4.3 (2023-05-11)

Warnings: Backtesting result and performance will change.

Features

  • Improved RAM usage by removing sorting and swaplevel for machine learning features (e081c924).
  • Optimized backtest report (8babbd7a).
  • Implemented cross-validation for machine learning (e55828c0).
  • support infomation for stop loss and take profit assets (5f9272f3).
  • Hold_until functionality made available for US data (8437d62).
  • Added indicators control in plot_stats (7364d58).

Fixes

  • Fixed deadline considering business date for DataFrame (312f2fe1).
  • Fixed the issue where a float object had no attribute 'month' (d51a5901).
  • Fixed run_all_test (6203f56f).
  • Prevented deletion during map iteration in backtest_core (2d1d9b05).
  • Fixed login info (d177ed47).
  • Fixed alpha annualize estimation in analysis (26a8becf).
  • Fixed issue with short selling in online module (919521b9).
  • Synced win_ratio in maeMfeAnalysis (a9a31189).
  • Added rotc deadline in DataFrame (ac10597).

Refactoring

  • Refactored f_disclosure_date to_business_date, removed date too far, and improved speed (7dcb203b).
  • Refactored market selection according to DataFrame columns (039cbc96).
  • Fixed bugs and future warnings in the analysis module (ec93088f).
  • Refactored environment name capitalization in login module (635caed4).
  • Refactored login to use FINLAB_API_TOKEN (a5c9064d).
  • Informed free users that data is limited (bbabad14).

Tests

  • Added test_hold_until (ccc4ba89).

Doc

  • Updated daily_sortino documentation (b7d1d91d).
  • Updated get_data documentation (276862c2).

0.4.2 (2023-04-27)

New features

  • Add label trade-price support in machine learning
  • Add usage documentation for report.display() in reports
  • Add US market information to market info

Fixes

  • Fix display-string issues in the online module
  • Revert the backtest feature to a stable version
  • Adjust how the backtest feature updates holdings based on entry/exit prices
  • Fix detection of qlib.init
  • Fix benchmark-index timezone in reports
  • Handle pandas 2.0.0 compatibility issues
  • Handle the talib dependency issue
  • Fix entry/exit ratios becoming NaN in the backtest feature
  • Fix the fugle Account numpy reference in the documentation

Other

  • Add related docs in the machine-learning module
  • Add related docs in the market-info module

0.4.1 (2023-04-14)

  • (fix): fugle np not found

0.3.19 (2023-04-04)

  • (improve): memory usage in machine learning feature creation
  • (improve): machine learning feature for different freqnecy index
  • (fix): data.indicator support almost all talib functions
  • (fix): 2023/4/10 monthly revenue reveal change to 2023/4/12
  • (fix): sinopac calculate account balance

0.3.18 (2023-03-28)

  • (Breaking): update shioaji >= 1.0.0. Please refer to finlab.inline module
  • (feat): finlab.ml (document are not ready yet)
  • (feat): support qlib data
  • (feat): report add yearly and monthly return table
  • (fix): backtest entry exit signal both activate conflict

0.3.12 (2022-11-29)

  • (Breaking): report.get_stats() has new format
  • (feat): data.get now read expire date from server and update data dynamically
  • (feat): add a bunch of analysis for backtesting report
  • (feat): getpass password to hide api_token
  • (fix): report.weight and report.next_weights remove considering of price changed
  • (fix): backtest(..., resample_offset='10d') fix backtesting display date
  • (fix): finlab.online.order_executor show_alerting_stocks fix price
  • (fix): fugle order status not change even when it is filled
  • (fix): support update price of fraction lot size

0.3.11 (2022-11-10)

  • (Fix) finlab backtest asset shorting fee miner adjust
  • (Fix) finlab.report.weights and finlab.report.next_weights not defined
  • (Fix) fugle-trade remove flag accoding to 0.4.0
  • (Fix) fugle-trade fetch other source when stock.close is nan
  • (Fix) fugle-trade FugleAccount.get_position not latest position
  • (Feat) finlab.data.universe('STOCK_FUTURE') subset of TWII stocks
  • (Feat) add yearly return when upload report
  • (Feat) add fugle_trade version not compatable warning

0.3.10 (2022-10-13)

0.3.9 (2022-10-13)

  • (Fix) finlab.online.sinopac_account order_lot position zero
  • (Fix) finlab.online.fugle_account order_lot position duplicated
  • (Fix) finlab.backtest weight display remove normalized
  • (Fix) finlab.data windows FileStorage fix
  • (Fix) finlab.data indicator exception error

0.3.8 (2022-9-27)

  • (Fix) finlab.online.order_executor price +-10% limit order restricted
  • (Fix) finlab.online.order_executor Position.from_report handle price=0, weight=0, and duplicated stock id
  • (Fix) finlab.plot.StrategySunburst compatibility
  • (Fix) finlab.report.display display multiple chart

0.3.8.dev1 (2022-9-27)

  • (Fix) talib abstract module incompatible
  • (Fix) backtest result: alpha and beta
  • (Fix) finlab.online.panel
  • (Fix) finlab.online.fugle_account stock not traded error
  • (Fix) add cert custom password for sinopac
  • (Fix) remove duplicated columns from unnumeric_dtype.
  • (Fix) KY company disclosure deadline
  • (Chore) Candlestick data format generalized
  • (Chore) raise error when data.get not found
  • (Chore) logger.info to logger.warning in finlab.data
  • (Chore) fig return for report.display
  • (Feat) add retain_cost_when_rebalance flag

0.3.7.dev1 (2022-8-8)

  • More granular backtesting for touched_exit:
  • Previously, a stock that touched the target and bounced back would not be marked as a touched sell; now it is simulated as a touched sell.
  • Previously, a gap-down open meant the stock could not be sold at 0.07; now it will also be sold at the lower level.

0.3.5.dev (2022-7-18)

  • Add finlab.optimize for enumerating parameter combinations in backtests
  • Add the mae_mfe chart
  • mae_mfe short-side analysis
  • Rename report.get_trades()['position'] to weighting
  • backtest touched_exit fix
  • Fix bugs in finlab.analysis

0.3.3.dev (2022-7-1)

Bug fixes

  • Fix the weight display for short positions
  • TreeMap plotting fix
  • Fix historical financial-report publication times

Analysis — analyze backtest results

Add an Analysis framework that can fully analyze backtest results. Future analyses will inherit from Analysis and render charts, making reports richer.

liquidityAnalysis — liquidity test

A strategy simulates buying and selling at the close or open price, but in reality a stock may not trade successfully — for instance a disposition stock, a fully-delivered stock, or a prolonged limit-down. liquidityAnalysis checks whether any such instruments were bought during the backtest and computes their proportion, so you can ensure the strategy's liquidity matches your capital requirements. See the tutorial article for more detail.

report.run_analysis('LiquidityAnalysis')

inequalityAnalysis — inequality test

If you already have a strategy and want to add additional technical, chip, or profitability indicators, inequalityAnalysis lets you inspect whether they improve stock-selection performance. For example, to look at how the price-to-book ratio further filters stocks, use:

report.run_analysis('InequalityAnalysis', name='price_earning_ratio:股價淨值比')

Refer to the tutorial article for how to read the results.

0.3.2.dev (2022-6-23)

SunBurst — inspect strategy positions

Display the industries represented in the current holdings and which industry is weighted most heavily. See details!

Short selling and per-stock weights

The FinLab Package now supports short selling. If you already have a good strategy, try shorting 0050 — returns may drop slightly, but risk shrinks substantially, which is great for allocating capital across multiple stocks.

Also worth noting: the new version allows position to use weights in addition to the previous boolean signals (True/False). So in the code below, 2330 is longed at 50% weight and 1101 is shorted at 50%.

This weighting scheme is fully backward-compatible with the previous True/False position style, because True is treated as 1 and False as 0 in computation. When all weights are 1, the program allocates capital evenly across assets as before.

from finlab import data
from finlab import backtest

close = data.get('price:收盤價')

position = close < 0
position['2330'] = 0.5
position['1101'] = -0.5

r = backtest.sim(position)
r.display()

Pandas_ta support for technical indicators

Pandas_ta has emerged over the last couple of years as a strong alternative for computing technical indicators, largely because TaLib can be quite hard to install and ships with a fixed set of roughly 100 indicators that rarely get updated. In contrast, Pandas_ta computes indicators in pure Pandas, which plays well across operating systems. It also covers many of the newer indicators, such as the supertrend indicator, along with less-common ones like "Even Better Sinewave", which sounds pretty interesting. You can find all of them in the Pandas_ta docs.

from finlab import data

values = data.indicator('supertrend')