github JerBouma/FinanceToolkit v2.2.0
FinanceToolkit v2.2.0

3 hours ago

This is the largest release to date. The headline addition is a brand new Econometrics module (toolkit.econometrics) — 48 methods covering regression, panel data, causal inference, unit root/cointegration testing, Granger causality, diagnostics, and forecasting, all funneled through statsmodels/linearmodels instead of hand-built math. Alongside it, Risk, Performance, Models and Economics roughly double in depth with systemic-risk, market-timing, bankruptcy-scoring and crisis-dating metrics, and Technicals, Ratios, Fixed Income and Options each pick up a batch of previously-missing metrics.

The second large addition is a rebuilt caching layer. Three separate, incompatible caching mechanisms have been replaced by one incremental, range-aware SQLite cache that knows what it already holds per ticker and per date range — widening a period now retrieves only the missing years, and adding a ticker retrieves only that ticker. It also removes a genuine correctness bug in the old one, which silently overwrote the tickers and dates you asked for with whatever an earlier cached run had used. See Caching below.

Just as importantly, this release includes an extensive formula-correctness pass across nearly every module, carried out over two separate audit rounds — the second explicitly re-checking the first round's own fixes rather than trusting them. Every accepted finding was verified against live FMP/FRED/OECD/ECB/NY Fed data, published figures from BLS/BEA/the US Treasury/Ken French/the source papers, a reference library (TA-Lib/arch/statsmodels/linearmodels/py_vollib) where one exists, or analytic ground truth via Monte Carlo and finite differences. Combined, this fixed over 200 defects. Several of these are not edge-case bugs — Sharpe Ratio, Sortino Ratio, M2 Ratio, RSI, ADX, ATR, EWMA Volatility, the Hurst Exponent, Macaulay's Duration, bond DV01, Portfolio Volatility, US "headline" unemployment and every price-to-fundamental ratio on the LSE/JSE/TASE were all silently returning the wrong number in prior releases. See Bug Fixes below for the full list with what changed and why.

Minimum requirements changed: pandas>=3.0 (was >=2.2) and Python 3.11+ (was 3.10+, since pandas 3.0 itself drops 3.10 support).

Every method below hangs off the matching Toolkit sub-module (e.g. toolkit.econometrics.get_ols()) or its standalone class (Discovery(...), Economics(...), FixedIncome(...)). Full runnable examples with real output live in the documentation — each section below links to its module's page.

New Features

Econometrics (new module)

toolkit.econometrics is a new, optional module for testing statistical properties of price/return series and estimating causal effects — installed via the financetoolkit[econometrics] extra (statsmodels/linearmodels). Every result is a plain dict (matching the rest of the codebase's convention of returning pd.Series/pd.DataFrame rather than opaque result objects), with a matching *_summary_table/*_summary companion wherever a .summary() would otherwise be expected. Full examples: docs/econometrics.

  • Regression: get_ols, get_wls, get_gls (nonrobust/HC0-3/cluster/HAC-Newey-West standard errors), get_logistic_regression, get_probit_regression, get_quantile_regression.
  • Panel data: get_fixed_effects, get_random_effects, get_hausman_test (which specification to prefer).
  • Causal inference: get_difference_in_differences, get_iv_2sls, get_propensity_score_matching, get_regression_discontinuity, get_synthetic_control (Abadie/Diamond/Hainmueller 2010, with placebo-based inference).
  • Time series forecasting: get_arima_forecast, get_var_forecast (plus get_impulse_response_function and get_variance_decomposition), get_vecm_forecast.
  • Cross-sectional asset pricing: get_fama_macbeth_regression (Fama-MacBeth two-pass regression).
  • Event studies: get_event_study (MacKinlay 1997 market-model cumulative abnormal return analysis).
  • Unit root & cointegration: get_augmented_dickey_fuller, get_kpss_test, get_phillips_perron_test, get_zivot_andrews_test; get_engle_granger_cointegration, get_johansen_cointegration.
  • Granger causality: get_granger_causality.
  • Diagnostics: get_arch_lm_test, get_jarque_bera_test, get_ljung_box_test, get_cusum_test, get_variance_ratio_test.
  • Specification & hypothesis tests: get_breusch_pagan_test, get_white_test, get_durbin_watson_test, get_vif, get_ramsey_reset_test, get_chow_test, get_f_test, get_likelihood_ratio_test, get_wald_test, get_hausman_wu_test, get_two_sample_t_test.
  • Forecast evaluation: get_rmse, get_mae, get_diebold_mariano_test, get_out_of_sample_validation.

Risk

Call these via toolkit.risk.<method>(). Full examples: docs/risk.

  • get_covar (CoVaR / Delta-CoVaR) — how much returns' own tail risk worsens when conditioning_returns (e.g. the financial system, or a specific counterparty) is itself in distress, via quantile regression. Reference: Adrian, T. & Brunnermeier, M.K. (2016), American Economic Review.
  • get_marginal_value_at_risk / get_component_value_at_risk — Euler risk decomposition of portfolio VaR: Marginal VaR is each asset's Beta to the portfolio times portfolio VaR; Component VaR is that scaled by the asset's weight, and the components sum exactly to total portfolio VaR (Garman 1997, Litterman 1996).
  • get_amihud_illiquidity (Amihud 2002) and get_roll_spread (Roll 1984) — market-microstructure liquidity measures derived purely from price/volume, without needing order-book data.
  • get_volatility(method=...) — four realized-volatility estimators ("parkinson", "garman_klass", "rogers_satchell", "yang_zhang") built from daily OHLC(V) prices rather than just closes, each using more of the day's price action (and, for Yang-Zhang, overnight gaps) to estimate volatility more efficiently than a close-to-close standard deviation. The default method="close_to_close" is the existing behaviour.
  • get_har_rv_forecast (Corsi 2009 HAR-RV) — forecasts future realized variance from a simple linear combination of daily, weekly and monthly lagged realized variance, a cheap and surprisingly effective alternative to a full GARCH fit.
  • get_hill_estimator — a nonparametric estimate of the tail index of the return distribution, i.e. how fat the tails actually are.
  • get_gjr_garch / get_egarch (plus _forecast/_parameters variants, and get_garch_parameters for the plain model) — asymmetric GARCH models where negative and positive shocks affect future volatility differently (the leverage effect), unlike plain GARCH. References: Glosten, Jagannathan & Runkle (1993); Nelson (1991).
  • get_var_backtest — builds a rolling, out-of-sample VaR path and backtests it with the Kupiec (1995) proportion-of-failures test and the Christoffersen (1998) independence test, to check whether a VaR model's breach rate and breach clustering match what the confidence level implies.
  • get_acerbi_szekely_test — the counterpart for Expected Shortfall, backtesting a CVaR model against realized returns via the Acerbi-Szekely (2014) Z2 statistic, since the breach-counting tests above only validate VaR.
  • get_tail_dependence_coefficient, get_copula_parameters / get_copula_simulation / get_best_fitting_copula — Gaussian, Student-t, Clayton, Gumbel and Frank copulas with MLE fitting, simulation, and AIC-based family comparison, for modelling joint tail behaviour between two return series beyond what linear correlation captures. Default to running across every unique ticker pair on the Toolkit instance when no explicit pair is given.

Performance

Call these via toolkit.performance.<method>(). Full examples: docs/performance.

  • get_sharpe_ratio(method=...) — three corrections to the plain Sharpe Ratio: "adjusted" for skewness/kurtosis, "probabilistic" for estimation uncertainty given the sample size, and "deflated" for multiple-testing bias when many strategies were tried before picking the best one. The default method="standard" is the existing behaviour.
  • get_appraisal_ratio — Jensen's Alpha divided by the idiosyncratic (residual) standard deviation left over from the CAPM regression that produced it: how much stock-picking skill an active manager is generating per unit of the risk that skill itself introduces.
  • get_fama_decomposition (Fama 1972) — splits total excess return into Selectivity and Diversification, decomposing Jensen's Alpha further than the Alpha figure alone can.
  • get_henriksson_merton_model and get_treynor_mazuy_model — market-timing regressions that test whether a manager's returns show evidence of successfully timing the market, not just picking good securities.
  • get_rachev_ratio (R-Ratio) and get_starr_ratio — tail-focused alternatives to Sharpe/Sortino that measure reward per unit of tail risk (CVaR-based) rather than per unit of standard deviation or downside deviation.
  • get_carhart_four_factor_model (Carhart 1997) — adds a momentum factor to the existing Fama-French regression machinery.

Models

Call these via toolkit.models.<method>(). Full examples: docs/models.

  • get_fulmer_h_score, get_grover_score, get_ohlson_o_score, get_springate_score, get_zmijewski_score — five more bankruptcy/distress prediction models alongside the existing Altman Z-Score and Piotroski F-Score, each estimated on a different sample or via a different technique (multiple discriminant analysis, logistic regression), so comparing several together is more robust than relying on any single one.
  • get_free_cash_flow_to_equity / get_free_cash_flow_to_firm (FCFE / FCFF) — the levered and unlevered cash flow bases used in DCF valuation, discounted at cost of equity and WACC respectively.
  • get_market_value_added (MVA) — the market-priced counterpart to Economic Value Added: the gap between a company's current market value and the total capital historically invested in it.
  • get_residual_income — the equity-side counterpart to EVA: profit in excess of what equity holders require, netting only the cost of equity (not debt) off of Net Income.
  • get_tobins_q_ratio — market value of a company relative to the replacement cost of its assets; a high Q suggests management has an incentive to invest further.
  • get_two_stage_dividend_discount_model — extends the (single-stage) Gordon Growth Model to companies not expected to grow at a constant rate forever, explicitly projecting a high-growth phase before switching to a terminal constant-growth phase.

Technical Indicators

Call these via toolkit.technicals.<method>(). Full examples: docs/technicals.

  • get_awesome_oscillator, get_know_sure_thing, get_rate_of_change — momentum oscillators built from moving averages / smoothed rate-of-change components.
  • get_choppiness_index — how much a market is trending versus chopping sideways, independent of direction.
  • get_ease_of_movement, get_vortex_indicator — price/volume and directional-movement indicators for gauging trend strength.
  • get_chaikin_money_flow, get_elder_ray_index, get_negative_volume_index / get_positive_volume_index — volume-based buying/selling pressure indicators.
  • get_kaufman_adaptive_moving_average (KAMA) — a moving average that automatically speeds up in trending markets and slows down in choppy ones, based on an efficiency ratio of net movement to total movement.
  • get_supertrend — a volatility-adjusted (ATR-based) trend-following stop-and-reverse line, in the same family as Parabolic SAR.
  • get_fibonacci_retracement_levels — the standard retracement grid (0%, 23.6%, 38.2%, 50%, 61.8%, 78.6%, 100%) between a swing high and low, for either trend direction, with the ratios overridable. It sits in the same category as Pivot Points, the Ichimoku Cloud and Support/Resistance Levels: a price-level reading convention rather than a formula with empirical support. The docstring says so explicitly, including that the 50% level is not a Fibonacci ratio at all and that 78.6% is the square root of 61.8% rather than a ratio from the sequence.

Ratios

Call these via toolkit.ratios.<method>(). Full examples: docs/ratios.

  • get_asset_coverage_ratio — how well tangible assets, after covering current liabilities, can cover total debt.
  • get_cash_return_on_assets — operating cash flow generated per dollar of assets, a cash-basis alternative to accrual-based ROA.
  • get_defensive_interval_ratio — how many days a company could keep covering operating expenses using only its most liquid ("defensive") assets, with no further revenue.
  • get_ebitda_margin, get_free_cash_flow_margin — EBITDA and free cash flow, each as a share of revenue.
  • get_gross_debt_to_ebitda_ratio — total (gross) debt relative to EBITDA, a common leverage covenant metric.
  • get_price_to_sales_ratio — market cap relative to revenue, useful for unprofitable or early-stage companies where P/E doesn't apply.
  • get_working_capital_turnover_ratio — how effectively working capital is converted into revenue.

Economics

Call these via Economics(...).get_*(), or toolkit.economics.<method>() on a Toolkit instance. Full examples: docs/economics.

  • Crisis dating: get_banking_crisis, get_currency_crisis, get_sovereign_debt_crisis — Reinhart & Rogoff style binary (0/1) crisis dummies per country and year from the Global Macro Database, rather than the continuous series the rest of the module returns, for marking historical stress episodes on a chart or conditioning a model on them.
  • Cross-country real activity: get_real_gross_domestic_product_usd and get_real_gross_domestic_product_per_capita (GMDB, inflation-adjusted and directly comparable across countries), get_trade_balance, get_real_effective_exchange_rate (trade-weighted and inflation-adjusted), and the OECD's get_output_gap (actual versus potential GDP).
  • Composite and derived indicators: get_misery_index (unemployment plus inflation), get_real_interest_rate (nominal rate net of inflation) and get_yield_curve_slope (10-year government bond yield minus the 3-month money market rate).
  • 8 new FRED-backed, US-only indicators with no existing GMD/OECD alternative: get_nonfarm_payrolls, get_initial_jobless_claims, get_retail_sales, get_industrial_production_index, get_housing_starts, get_recession_indicator (NBER-based), get_real_personal_income, get_mortgage_rate_30_year.
  • TIPS/inflation-market data: get_real_yield_curve (FRED TIPS real yields, 5/7/10/20/30Y) and get_breakeven_inflation_expectations (market-implied inflation, including the 5Y5Y forward rate).
  • get_commercial_real_estate_prices (FRED's commercial, not residential, property price index) and get_commodity_forward_curve (dated CME futures contracts from Yahoo Finance, since FRED/OECD/FMP only expose a single spot price per commodity).
  • 3 new OECD-backed methods: get_producer_price_index, get_household_savings_rate, get_household_debt_to_income_ratio, plus an OECD-sourced CPI option; startPeriod/endPeriod support added across all OECD-backed functions.
  • On the Toolkit itself (not ticker-scoped): get_market_risk_premium and get_commitment_of_traders.
  • The FRED API key is now actually threaded through to Economics/FixedIncome when using the MCP server — it was silently never passed before.

Fixed Income

Call this via FixedIncome().get_*(), or toolkit.fixedincome.<method>(). Full examples: docs/fixedincome.

  • get_par_yield, get_forward_rate — deriving par and forward rates from a zero-coupon spot curve.
  • get_breakeven_inflation_rate — the inflation rate implied by a nominal yield versus its inflation-protected counterpart.
  • get_key_rate_duration — a bond's sensitivity to a single maturity point on the yield curve moving in isolation, rather than the whole curve shifting in parallel.
  • get_yield_curve_spread, get_z_spread — spread between two curve points, and the constant spread that reprices a bond exactly against a benchmark curve.
  • get_bond_equivalent_yield — converts a money-market discount yield (e.g. quoted for T-bills) into a directly comparable bond-equivalent yield.
  • get_taylor_price_change — estimates a bond's price change for a given yield move using a second-order Taylor expansion (duration + convexity) instead of duration alone.
  • get_treasury_rates — the official U.S. Treasury par yield curve, every maturity from 1 Month to 30 Year in one dataset, paginated in 90-day windows to respect the endpoint's per-request cap.

Options

Call these via toolkit.options.<method>(). Full examples: docs/options.

  • get_asian_option — geometric-average Asian option pricing (Kemna & Vorst 1990 closed form).
  • get_barrier_option — single-barrier knock-in/knock-out option pricing (Reiner & Rubinstein 1991).
  • get_binary_option — cash-or-nothing digital option pricing under Black-Scholes.
  • get_bjerksund_stensland — a closed-form analytical approximation for American option prices (Bjerksund & Stensland 1993), much faster than a binomial tree.
  • get_garman_kohlhagen — Black-Scholes adapted for FX options, using both the domestic and foreign risk-free rate.
  • get_monte_carlo_option_price — European option pricing via Monte Carlo simulation of Geometric Brownian Motion price paths.
  • get_put_call_parity — checks or derives the no-arbitrage put-call parity relationship for European options.
  • get_strategy_payoff — net expiration P&L of a multi-leg option (and, optionally, stock) strategy.
  • get_volatility_surface — fits a raw SVI smile (Gatheral 2004) per expiry across multiple maturities and flags (rather than silently ignoring) calendar-spread arbitrage between them.
  • get_risk_neutral_density — the market-implied risk-neutral probability density of the underlying at expiration via the Breeden-Litzenberger (1978) theorem, applied to a calibrated SVI smile rather than one flat assumed volatility (see Bug Fixes).

Caching (rebuilt)

use_cached_data=True still turns caching on, but what happens underneath is entirely new. Where the old cache stored one pickle per dataset and could only ever answer "same request as last time, or nothing", the new one records which tickers and which date ranges it actually holds, and asks the API only for what is missing.

  • Incremental and range-aware. Repeating a request retrieves nothing at all. Widening start_date/end_date retrieves only the years that were not already there. Adding a ticker retrieves only that ticker. Previously any change to any parameter refetched everything.
  • Every external source is covered — financial statements, historical and intraday prices, FRED series, OECD queries, treasury rates, futures contracts, company profiles, quotes, ratings, analyst estimates, earnings and dividend calendars, ESG scores, segmentations, Commitment of Traders, option chains and expiries, GMD, ECB, Fed, Ken French factor datasets, the market risk premium and the discovery endpoints. Anything the Toolkit computes itself is never cached — it is always derived from that data on demand.
  • One shared database. It lives in your platform's user configuration directory (overridable with FINANCE_TOOLKIT_CACHE_DB, or by passing a folder to use_cached_data), which is the same file the MCP server uses, so a local server and the library warm each other's cache instead of each keeping their own.
  • Two freshness knobs per dataset. A time-to-live decides whether to refresh at all, and a revision window decides how much of the recent tail is re-requested when it does — so a rerun inside the TTL makes zero external calls, while a stale daily price series re-requests only its last few days rather than its whole history.
  • toolkit.get_cache_contents() reports what is stored, grouped by source and dataset with entity counts and write times.
  • toolkit.clear_cache(source=, dataset=, ticker=, confirm=) is the only way anything is ever removed. Nothing expires or is evicted on its own, not even when the cache's own internal structure changes between versions — a version mismatch is reported and left alone. An unscoped wipe requires confirm=True. It is deliberately not exposed as an MCP tool.
  • The source names are the same ones enforce_source uses ("FinancialModelingPrep", "YahooFinance", "OECD", "FRED", …), so they mean the same thing in both places.
  • A cache is an optimization and never breaks the caller: a corrupt database, a read-only directory or an unreadable entry disables caching with a warning instead of raising, and every read treats a storage failure as a miss.
  • store() originally spanned three separate connections, so two concurrent writers could both record coverage while the second discarded the first's rows — now a single atomic BEGIN IMMEDIATE transaction under WAL, verified across 8 concurrent processes.

Your FMP subscription plan is part of the cache key for the five endpoints whose response length depends on it, so a Free-plan response can never be served to a Premium caller on a shared database. Cached price history is also only ever spliced together when the same provider served both halves, since a ticker that fell back to the other provider carries different split and dividend adjustments.

Existing cached/*.pickle directories are ignored rather than migrated — the new cache simply rebuilds on first use, and the old folder can be deleted by hand.

Breaking Changes

  • Minimum Python is now 3.11 (was 3.10) and minimum pandas is now 3.0 (was 2.2) — pandas 3.0 itself drops Python 3.10 support, so both floors moved together.
  • get_relative_vigor_index no longer accepts a volumes argument. Its old "volume-weighted" formula algebraically reduced to plain up_sum / down_sum regardless of volume (volume canceled out of both sides of the ratio), so the parameter never actually did anything; it's been replaced with a correct, bounded 0-1 ratio and the dead parameter removed.
  • get_short_term_coverage_ratio now takes short_term_debt instead of accounts_receivable/inventory/accounts_payable — see Bug Fixes.
  • use_cached_data=True no longer restores your previous parameters. It used to reload tickers, start_date, end_date and quarterly from a configurations.pickle, which meant a cached run could not be given a different ticker list or period without first clearing the cache. Those arguments are now always taken from what you pass. This was a correctness bug rather than a feature (see Bug Fixes), but it is listed here because the old behaviour of initializing Toolkit(use_cached_data=True) with no arguments at all and getting your previous session back no longer works.
  • Caching is now off by default on a hosted MCP server (MCP_TRANSPORT=sse or streamable-http); a local stdio server is unaffected. Set FINANCE_TOOLKIT_CACHE_ENABLED=true to opt back in.
  • financetoolkit.utilities.cache_model has been removed, along with the MCP server's own SQLiteCache. Both are replaced by financetoolkit.cache. These were internal modules, so this only matters if you imported load_cached_data/save_cached_data directly.
  • Rates and ratios are decimals everywhere now. get_treasury_rates, the FRED rate methods, the whole Global Macro Database family, the output gap and get_commercial_real_estate_prices previously returned percentage points. 4.25% is now 0.0425, not 4.25.
  • Quarterly statement labels shift for companies whose fiscal quarters end in January/April/July/October — both frequencies now apply the same month-rule relabelling the annual path always had, so a company's quarters actually sum to its annual figures (they previously didn't for these companies). Calendar quarter-enders (the majority) are unaffected.
  • handle_errors raises AttributeError and TypeError where it previously swallowed them into an empty Series. FINANCETOOLKIT_STRICT_ERRORS=1 raises everything.
  • Unreported statement line items are NaN, not 0 — both whole missing rows and, as of this cycle, individual missing cells within an otherwise-reported row on the Yahoo path, which is common for the oldest year of a multi-year statement.
  • Yahoo-sourced non-USD tickers are now currency-converted, where before they silently weren't (Yahoo published no statistics_yf.csv, so Reported Currency was never available for that path).
  • Unadjusted OHLC from Yahoo changes, since auto_adjust is now off — this is the fix that makes Yahoo and FMP agree on Close.
  • smooth_widow is renamed to smooth_window (old spelling deprecated but still accepted).
  • pyyaml moved to the base dependencies — the package root imports it at module scope for Portfolio, so a plain pip install financetoolkit couldn't previously even import the package; it was only pulled in by the mcp extra.

Bug Fixes

Core data plumbing

  • Quarterly statements never had the fiscal-year relabelling the annual path applies (see Breaking Changes above); a company's calendar-year quarters now sum exactly to its annual revenue, an identity that didn't hold before.
  • Four normalized column labels pointed at source keys that no longer exist in either provider's schema and were zero-filled phantoms for every ticker; four labels the ratios/models code reads were entirely missing from the Yahoo vocabulary. All 56 labels the package reads are now available from both providers (40/40 spot-checked against actual 10-K filings).
  • filter_columns only searched the first level of a MultiIndex and silently returned the unfiltered frame on a miss. rounding=0 fell through to the default in 31 places; rounding=None raised outright.
  • FMP caps a response at a fixed row count, not a span, so a capped response runs to the requested end but is short only at the start — this bypassed the existing span-ratio truncation check entirely (AAPL from 2005 came back starting 2006-02-16, 94% of the requested span). Now detected and falls back to Yahoo unless a source is forced.
  • Raw Yahoo statement cells the provider genuinely never reported (common for the oldest year of a partially-reported multi-year statement) were zero-filled instead of left NaN, reintroducing the "unreported = 0" bug for individual cells even after it was fixed for whole missing rows.
  • yfinance's repair=True split-repair heuristic — genuinely useful for real equities, where it catches actual $/cents unit mixups and bad splits — misfires on synthetic instruments that have no real splits: it inflated ^IRX (13-week Treasury) by exactly 100x for every date before its March 2020 crash to near-zero, and blew CL=F (WTI crude) up by 10,000x on the day after oil settled negative. Now scoped off for anything ^-prefixed or containing = (indices, FX, futures) and left on for everything else.
  • get_historical_data()'s cache guard only checked whether the cached frame was empty, not whether enforce_source (or return_column/include_dividends/fill_nan/rounding) changed since the call that populated it — the first call's source won for the Toolkit's whole lifetime, and every later call with different arguments silently got the same cached frame back unless overwrite=True (which then re-fetched everything rather than reacting to what actually changed). Now tracks the params used and auto-invalidates on a mismatch.

Currencies

  • LSE (pence), JSE (cents) and TASE (agorot) quote in a fractional unit, but the FX pair resolved to the major unit. JSE now routes through FMP's native minor-unit ticker; TASE has no equivalent on either provider and now produces an honest warning instead of a silent NaN (a working ticker for a different currency in the same batch was previously suppressing that warning entirely).

Ratios & Models

  • Piotroski F-Score's ΔROA criterion computed the growth rate of ROA growth (comparing this period's YoY ROA growth to last period's), instead of simply comparing current ROA to the prior period as Piotroski's (2000) original F_dROA signal does.
  • get_interest_burden_ratio was computing the interest coverage ratio, not the burden ratio (AAPL FY23: 0.9951 against FMP's own figure, was 29.06). get_free_cash_flow_yield summed share prices instead of averaging them. Three days-based ratios silently dropped their days argument in the trailing branch.
  • Piotroski's F_ACCRUAL and F_ΔLEVER were both mis-specified against Piotroski (2000); an unreported fiscal year scored 0/9 rather than NaN, since every criterion is a boolean and NaN > x evaluates to False. That NaN-vs-zero mask has since been extended twice more this cycle: first to cover all nine criteria's underlying fields (not just the three feeding ROA/CFO/accruals), then to also require the previous period be reported before trusting the five criteria that compare against it — a NaN in period t-1 was still silently scoring period t's change-based criteria as 0 instead of propagating.
  • Fulmer's H-Score returned inf on zero interest expense; Market Value Added averaged the market value of debt, which cancels debt out of the measure entirely. Every published coefficient set (Altman, Ohlson, Zmijewski, Springate, Grover, Fulmer, Beneish) was re-derived from raw statements and matches to ≤5e-11.
  • Three collect_* ratio methods (efficiency, profitability, valuation) silently dropped the caller's trailing argument on one method each, while every sibling in the same table correctly forwarded it. get_price_to_cash_flow_ratio's denominator was rolled up to the trailing window but its numerator wasn't, a numerator/denominator period mismatch.
  • get_net_current_asset_value computed Working Capital (Current Assets − Current Liabilities), not Graham's actual NCAV (Current Assets − Total Liabilities) — a $162bn, sign-flipping error on AAPL 2021. get_asset_coverage_ratio double-subtracted short-term debt, once inside Current Liabilities and again inside Total Debt.
  • The Gordon Growth Model's period-tracking froze one period early, so the true final historical period was valued off a stale projected dividend instead of its own actual reported one — contradicting the method's own documented behaviour.
  • Intrinsic value and the two-stage dividend discount model summed their discounted cash flows with the builtin sum, whose accumulation order (and therefore last-digit result) depends on the Python version. Both now use math.fsum, which is correctly rounded and gives the same answer everywhere.

Performance & Risk

  • Sharpe Ratio was returning the wrong thing entirely: excess_returns / excess_returns.std() divided the whole return series by its own standard deviation instead of dividing the mean by the standard deviation — every "Sharpe Ratio" was actually a series of standardized returns, not a single risk-adjusted number.
  • Sortino Ratio's (and Kappa Ratio's) downside deviation used the wrong denominator — .std() of only the negative returns instead of the root-mean-square of the shortfall below the minimum acceptable return over all observations (Sortino & Price, 1994) — understating downside risk whenever there are a few large losses. M2 Ratio never used the benchmark's standard deviation, the entire point of the M2 rescaling, and was silently computing the plain Sharpe Ratio under the "M2 Ratio" name; it now requires a benchmark_ticker.
  • Fama-French single-factor regression had its arguments swapped (linregress(excess_returns, factor) instead of linregress(factor, excess_returns)), returning the wrong slope, intercept and residuals. The multi-factor variant crashed whenever the factor dataset had missing values, calling .bfill(axis=1) on a pd.Series that doesn't support axis=1. CAPM and Alpha silently returned None on bad input instead of raising TypeError.
  • EWMA (RiskMetrics) Volatility used pandas.Series.ewm().std(), which demeans the series and uses the contemporaneous return, where the RiskMetrics recursion assumes a zero mean and the lagged return — rebuilt directly from that recursion. The Hurst Exponent was systematically doubled (the regression slope already is H for this estimator).
  • Delta-CoVaR re-fit a separate median-quantile regression instead of evaluating the same distress-quantile regression at the conditioning variable's median (Adrian & Brunnermeier 2016, Section II.B). Student-T VaR/CVaR passed loc=1 instead of scale=1 to stats.t.ppf, understating tail risk by roughly 2x; EVaR's formula used std instead of alpha inside the exponential term, ignoring the confidence level entirely.
  • GARCH's log-likelihood paired each return with the wrong period's variance (an off-by-one), which could blow up to ~1e199 and crash the optimizer on zero-return windows; the forecast formula separately double-squared the already-computed long-run variance. GARCH/GJR/EGARCH forecasts were later found to still seed from the last in-sample variance rather than the full sample, so the reported "one-step-ahead" forecast wasn't; the optimizer bound on omega was also six orders of magnitude off the daily optimum.
  • CAGR divided by the observation count rather than compounding intervals (S&P 2015-24: 11.15% → 12.46%, the correct figure). Both market-timing models returned an empty Series for every real Toolkit instance. Four factor-model loops iterated per row, re-running each regression roughly 252 times.
  • Five copula and CoVaR methods defaulted to period="yearly", so a decade of data was ten observations — several of these silently worked around their own broken defaults in the docstring examples rather than raising.
  • get_starr_ratio and the non-rolling branch of get_appraisal_ratio divided a period-scale numerator (excess return, Jensen's Alpha) by a daily-scale denominator (CVaR, CAPM-residual std) with no rescaling, inflating both by roughly √252 (16-30x). GARCH/GJR/EGARCH fits never checked whether the optimizer actually moved off its starting guess before returning it as converged; both now retry once and fall back to NaN on a genuinely stuck fit.
  • Portfolio Volatility was a weighted average of each holding's own volatility, which ignores correlation between holdings and always overstates true portfolio risk unless every pair is perfectly correlated. Now uses the full covariance matrix (w^T Σ w, Markowitz 1952), falling back to the old weighted-average approximation only when a return series isn't available.

Technicals & Options

  • RSI, ATR and ADX used a plain rolling mean instead of Wilder's own smoothing method (a slower recursive average with a 1/window constant) for indicators Wilder specifically defined using that smoothing — one of the most common mistakes when re-implementing these. Added a shared get_wilder_moving_average helper and switched all three (plus DX→ADX smoothing) to use it.
  • Ultimate Oscillator was off by roughly two orders of magnitude — its per-timeframe averages used a rolling mean instead of a rolling sum, and the final weighted combination was missing its ×100 scaling entirely. Ichimoku Cloud's Leading Span A/B were shifted forward by conversion_window instead of base_window. Detrended Price Oscillator's displacement was off by one period and shifted the moving average as well as the close price.
  • Triangular Moving Average's formula was simply wrong (summed prices over the window and divided by (window+1)/2, rather than computing a genuine double-smoothed SMA), and its sub-window length was later found to only be correct for odd windows — the function's own default is even. Aroon Indicator's "periods since extreme" count had an off-by-one error.
  • Force Index used a rolling sum of volume multiplied by a single day's price change instead of Elder's EMA-smoothed raw Force Index. On-Balance Volume and the single-ticker Advancers/Decliners breadth signal divided by abs(price_diff) to derive a sign, which is undefined on any day the price didn't move at all — switched to np.sign().
  • Bollinger Bands used the sample standard deviation where Bollinger's own definition (and TA-Lib) use the population one, inflating the band half-width by 2.6%. Donchian Channels included the current bar in their own high/low window, making a breakout impossible by construction (0 breakouts detected on a series that should have had 129).
  • Parabolic SAR was missing TA-Lib's reversal clamp, letting the stop sit on the wrong side of price for several days right after a trend flip (up to 1.40 off, on 12/500 sampled days). Removed look-ahead bias from Support/Resistance Levels (354/701 values previously revised retroactively as new bars arrived; now covered by a regression test spanning all 55 indicators).
  • Binomial tree option pricing discounted by T / (steps − 1) instead of T / steps (an off-by-one in the per-step discount factor), and had the up/down payoff branches swapped in the backward-induction step. Black-Scholes put Theta was missing a set of parentheses, so the leading negative sign applied only to the formula's first term. Gamma didn't forward dividend_yield into its own internal d1 calculation.
  • get_partial_derivative's docstring claimed to compute the Breeden-Litzenberger risk-neutral density, but used one flat assumed volatility at every strike, which can only ever recover a lognormal density; the new get_risk_neutral_density does it properly by fitting an SVI smile to real market-implied vols first. veta carried the opposite sign to the rest of the Greek surface, and SVI calibration ran a single optimizer start and could silently converge to a degenerate straight line through a real smile while still reporting success.
  • Eleven methods declared a tuple return type but actually return a DataFrame. Several Greeks and Black-Scholes docstrings drifted from their real signatures (get_d2 documented get_d1's stale parameter list; get_dual_gamma/get_vanna documented a put_option parameter that doesn't exist; get_delta/get_lambda/get_vega omitted real dividend_yield/put_option parameters; several used time_to_expiry where the real parameter is time_to_expiration).

Econometrics

  • Event studies didn't validate that pre_event_days <= gap_days, so a badly chosen gap_days could let the estimation window overlap the event window, contaminating the market model's "normal return" estimate. get_out_of_sample_validation duck-typed a .forecast attribute off the raw forecast object rather than reading the documented key, silently returning the entire result dict instead of the forecast itself.
  • A nested closure with no docstring broke the website's regex-based documentation parser, silently dropping the next method in the same file from the generated docs — inside the Diebold-Mariano test (dropping get_ols) and inside get_implied_volatility (dropping get_volatility_surface). Both were moved into proper, documented model functions, and the parser itself has been rebuilt on ast rather than regex — this was still dropping get_gross_domestic_product, collect_bond_statistics and four Risk methods from the published docs.
  • get_vif called add_constant(has_constant="skip"), so a pre-existing constant column shifted every regressor one position left of the index used to look its VIF back up — every value was mislabelled, and the last one raised IndexError. The Ramsey RESET test silently discarded the caller's robust covariance type.

Economics & Fixed Income

  • Macaulay's Duration discounted each coupon by (1 + y/freq)^(t/freq) instead of (1 + y/freq)^t. Bond DV01 had a spurious extra × 0.01 factor on top of the ±1bp yield shift already baked into the price comparison, so every reported DV01 was 100x too small (and sign-flipped); rebuilt on top of get_bond_price.
  • get_house_prices(inflation_adjusted=False) had a copy-paste bug — both branches of the if/else queried the same OECD "real" series, so requesting nominal house prices silently returned real ones. get_short_term_interest_rate silently ignored a class-level gmdb_source=True, always falling through to the OECD path regardless of the source set on the instance.
  • All 22 OECD SDMX queries left their dimensions unpinned. US labour productivity requested a year-over-year growth rate while its own docstring documented a level.
  • get_effective_duration repriced only in the upward direction, inheriting the curvature of the price-yield relationship rather than cancelling it — understated by 2.7% on a 5-year bond and 11.8% on a 30-year. get_bond_equivalent_yield applied the short-bill formula at every maturity, overstating a 52-week bill's yield by roughly 7bp. Black-76 and Bachelier shared a single volatility argument despite needing lognormal versus normal vol respectively — a 20% lognormal vol fed into Bachelier priced it 30.77x too high (exactly 1/forward).

Portfolio & Discovery

  • Passing a DataFrame directly to Portfolio(...) raised, since the guard tested the frame for truthiness — the documented constructor path was completely broken. Weekend and holiday trades were dropped from every position metric. The P&L overview concatenated grouped results positionally, misplacing figures whenever two trades shared a date. The benchmark was replicated in equal share counts rather than matched cash flows, so alpha was never genuinely like-for-like.
  • Five discovery methods pointed at retired FMP endpoints and raised on every call. Portfolio Invested could go negative after a profitable partial exit.
  • The transaction sort used the default non-stable quicksort, so same-date transactions could land in a different relative order than the source file — the FIFO/LIFO cost-basis walk depends on that order to compute realized PnL correctly; now a stable sort.

MCP Server

  • MCP router tools baked the first-inspected indicator's parameter defaults into the schema shared by the whole group (98 of 2,325 parameter entries were wrong, now 0). The MCP response cache key dropped list-valued kwargs, so lag=[1,2] and lag=[2,3] collided. The Ken French cache policy was registered under its pre-rename key, causing a daily re-download of the archive instead of weekly.
  • financetoolkit-mcp --help previously ignored the flag entirely and started the server instead of printing help, since the entry point never parsed its command-line arguments. Both it and the inspector now have a real argument parser; transport/host/port can be passed as flags and still fall back to MCP_TRANSPORT/MCP_HOST/MCP_PORT, so existing client configs are unaffected.

Caching (the previous, now-replaced system)

  • The cache silently overwrote the parameters you asked for. Toolkit(use_cached_data=True) reloaded tickers, start_date, end_date and quarterly from a configurations.pickle and applied them over the arguments you had just passed, so a second run with a different ticker or period quietly analysed the first run's companies and dates instead.
  • The cache never updated once written — save_cached_data returned early if the target file already existed, so a dataset written once was frozen and refetched from scratch on any parameter change. The OECD had its own separate file cache with module-global state that no other module shared or could clear.
  • All three of these are moot with the rebuild described under Caching above, which replaces the whole mechanism rather than patching it.

Documentation

A correctness pass over the docstrings themselves, since they are the reference material on the website and the source of every MCP tool description, across both audit rounds. Roughly 115 defects were found and fixed combined; the ones that could actually mislead:

  • Stated formulas that contradicted the code. The Ulcer Index formula (SQRT(SUM[(Pn / Highest High)^2] / n)) evaluates to ~1 for any series — the code correctly uses (Pn - Highest High) / Highest High. Dollar Duration omitted the /100 the code applies. Modified Duration's formula was only correct at frequency=1, and was falsely equated with DV01. Phillips-Perron's Z_t had the wrong denominator. A simply-wrong extended-DuPont formula, an inverted EQ_OFFER interpretation, an inverted Enterprise Value interpretation, Ohlson's cutoff documented 10x off (0.38 vs the real 0.038), several options Greek formulas with N/N' swapped, and an inverted Theta interpretation.
  • Methods documented as doing something they don't. get_yield_to_maturity claimed Newton-Raphson (it's the secant method); get_arima_forecast claimed Conditional Sum of Squares "as a deliberate limitation" (it's exact MLE via the Kalman filter); get_cusum_test described recursive residuals under a Brownian motion null citing Brown-Durbin-Evans (it's OLS residuals under a Brownian Bridge, Ploberger-Kramer). get_ice_bofa_effective_yield described current yield, and the ICE BofA total-return and yield-to-worst links pointed at entirely different FRED series than the ones fetched.
  • Whole docstrings left over from copy-paste: get_carbon_footprint described environmental taxes, get_kurtosis described CVaR throughout, get_rent_prices described the price-to-rent ratio rather than the rent index it returns.
  • Args: blocks that didn't match the signature — roughly 15 documented parameters that don't exist (alpha, trailing, t, dataset, per_capita), omitted real ones, or stated the wrong default.
  • period defaults across Risk, Performance and Econometrics. 114 methods documented a hard default ("yearly" in Risk, "quarterly" in Econometrics) while the code resolves period to the Toolkit's own quarterly/yearly setting. Around 50 of those also omitted monthly from their list of accepted values despite validating against it. Technicals is unaffected — its period genuinely defaults to "daily".
  • Several stale "Which returns" example tables that had never been regenerated after earlier fixes landed (still showing the pre-fix numbers for ROIC, capex coverage, capex-dividend coverage and the Hurst Exponent) have been re-run against current code.

The website documentation also gains a dedicated Econometrics page, which the README already linked to, plus the matching Econometrics notebook. The headline figure is now 500+ methods, applied consistently across the README, server.json, the MCPB manifest, the MCP tool metadata, the class docstrings, every example notebook and the website — the long-standing "200+" understated it by more than half, given the toolkit documents 522 methods and the MCP server reaches 476 of them. The wording changed with it: these are methods, not "financial ratios, indicators and performance measurements", since ratios are one module out of twelve. The MCP page's tool tables were also still listing the old get_-prefixed tool names (get_valuation_ratios rather than valuation).

Test Coverage

New test suites for the Econometrics module (14 files covering every estimator category), the five new Models bankruptcy scores (Fulmer, Grover, Ohlson, Springate, Zmijewski), Fixed Income's bond and yield curve models, and Risk's new backtesting, copula, CoVaR, market-liquidity and realized-volatility models. Expanded coverage across Technicals (momentum, overlap, volatility, controller) and Ratios/Performance/Risk for the formula fixes described above, so the corrected values are locked in going forward.

A new tests/cache/ suite covers the caching rebuild: the interval algebra, per-ticker splitting, scoped removal, the Toolkit's own cache methods, and one file per family of data source verifying that a warm request makes no external call and a widened one requests only the gap. It also covers the failure modes specifically — a corrupt database, a read-only directory, a corrupted payload, twelve concurrent writers, two processes sharing one file, and a schema-version bump — all of which have to degrade to "no caching" rather than raise.

The stored-output tests now compare CSV records numerically instead of as exact strings: a value is equal if it is within one unit of the last recorded decimal, or within 1e-5 relative. Shape, headers and text still have to match exactly. Iterative estimators (ARIMA forecasts, the GARCH family) walk to slightly different optima on different platforms, so the previous exact-string comparison made the suite pass or fail depending on which machine had recorded the expected values.

The suite grew from 1,353 to 1,475 passing tests across the two audit rounds. tests/economics/ previously asserted only "does not raise" on all 68 of its tests, which is exactly why the OECD unemployment mislabelling survived as long as it did — those tests now assert real values reconciled against BLS/FRED.

MCP Server

  • New econometrics tool group registered in config.yaml, alongside the existing per-module tool groups. The financetoolkit[mcp] extra now pulls in financetoolkit[econometrics] as well, so statsmodels/linearmodels ship with every MCP install and the econometrics tools work out of the box — no client config change needed.
  • The macroeconomics tool group's description and method list updated to cover the new FRED-backed indicators (retail sales, industrial production, housing starts, real personal income, recession indicator) and OECD household savings/debt-to-income methods.
  • FRED API key resolution (header/query/JWT, mirroring the existing FMP key path) wired through to Economics and FixedIncome over MCP — previously the key was never passed through this path at all.
  • The server's own SQLiteCache is gone, replaced by the same financetoolkit.cache the library uses. Tool responses are still cached whole under their own source, but the data underneath them now benefits from — and contributes to — the shared per-ticker cache, so a tool call is warmed by anything a co-located library session already retrieved. Eviction of tool responses is scoped to that source so it cannot take the accumulated price history down with it.
  • Caching defaults to off when the server is hosted. cache.enabled: auto in config.yaml resolves to on for stdio (a local server is one user on one machine) and off for MCP_TRANSPORT=sse/streamable-http. A hosted server serves every user from one process against one database, where a shared entry would answer one user's request with another subscriber's paid data, and retrieved source data has no eviction policy that would keep the disk bounded. FINANCE_TOOLKIT_CACHE_ENABLED=true|false overrides it either way, and an explicit boolean in config.yaml beats the transport heuristic. When it is off, no database file is opened at all — the Toolkit and Discovery instances the server builds are opted out too, not just the tool-response layer.
  • Six metrics that the toolkit exposes but the server could not reach are now registered, taking the reachable total to 476. get_producer_price_index was missing from the macroeconomics list; get_interest_burden_ratio, get_tax_burden_ratio, get_reinvestment_rate, get_support_resistance_levels and get_volatility_cone were invisible because their tool group discovers its methods by scanning a collect_* helper that doesn't call them. Those groups now list their methods explicitly. collect_bond_statistics was likewise neither exposed nor skipped, and now joins the other collect_* helpers in skip_methods.
  • MCP router tools baked the first-inspected indicator's parameter defaults into the schema shared by the whole group (98 of 2,325 parameter entries were wrong, now 0) financetoolkit-mcp --help previously ignored the flag entirely and started the server instead of printing help, since the entry point never parsed its command-line arguments; both it and the inspector now have a real argument parser.

Method note

Findings were only accepted where they could be demonstrated, not just argued for — against a reference library (TA-Lib, arch, statsmodels/linearmodels, py_vollib) where one exists, against live data from the source (FMP, Yahoo, FRED, OECD, ECB, NY Fed) reconciled to published BLS/BEA/Treasury/Ken French figures, or by hand: Monte Carlo against analytic ground truth, central finite differences of a pricer plus put-call/in-out parity, re-derivation of published coefficients from raw statements, and hand-computed FIFO walkthroughs. Several proposed findings were investigated and rejected rather than fixed: TA-Lib's own ADX disagrees with the toolkit by 18.6, but a line-by-line port of ta_ADX.c and the ta library both agree with the toolkit, so TA-Lib was the outlier there — and the same pattern repeated with CMO, where TA-Lib's Wilder-smoothed version diverges from Chande's own 1994 formula, which is what the toolkit (correctly) implements. The KAMA seeding "fix" proposed during the first audit round was reverted once TA-Lib was found to back the existing code.

Full comparison: v2.1.4...v2.2.0

Don't miss a new FinanceToolkit release

NewReleases is sending notifications on new releases.