Michal Kaszubski

Bugs I found in libraries that quants use, and the fixes their authors merged.

Each entry names the library, what was wrong, how I found it and what the fix does, and links to the pull request so the claim can be checked. Most were found by testing a library against itself: Greeks against finite differences, put-call and in-out parities, closed forms against Monte Carlo, one engine against another. Only merged pull requests are described as merged.

As of 9 November 2026. Commits are under my own name; the GitHub profile is github.com/Cashubski (opens in a new tab).

FinancePy (opens in a new tab) a derivatives-pricing library

twenty-four pull requests merged, September to November 2026

  1. Fixed the Bjerksund and Stensland American option pricer, which divided by zero at a zero dividend yield and fell below the European price near it; the fix returns the European value when early exercise is never optimal and floors the approximation at the European bound.

    When the cost of carry equals the riskless rate, the Bjerksund and Stensland exercise boundary is undefined: beta is exactly one and the trigger price divides by zero. The library raised. Just below that point the approximation also fell under the European price, which an American option cannot do. The fix returns the European value where early exercise is never optimal and floors the approximation at the European bound, since both are lower bounds on the American price. Found by checking every American pricer against a 2,000-step binomial tree and against the no-dividend identity.

  2. Made date-schedule generation repeatable: a second call stepped back from the adjusted maturity date and shifted every coupon date whenever the maturity fell on a weekend or holiday, which reached bonds, cross-currency swaps and cliquet options.

    The schedule generator produced the right coupon dates on its first call and different ones on every call after, because the first call replaced the maturity date with its weekend-adjusted version and later calls stepped backwards from that. Most products call the generator twice, so bonds with floating coupons, cross-currency swaps and cliquet options received shifted dates and a spurious one-day stub whenever the maturity fell on a weekend or holiday. One existing test had silently encoded the bug: its stored value included a one-day option that should not exist.

  3. Rewrote the forward-start option, which could not be constructed, and verified it against the Rubinstein closed form and the library’s cliquet option.

    The forward-start option class referenced calendar settings it never set, so it could not be constructed, and its valuation loop priced an extra option before the start date. Rewritten as the sequence of at-the-money forward-start options between reset dates, and tested against the Rubinstein 1991 closed form for one period and against the library’s own cliquet option for several.

  4. Corrected two FX option error paths that raised a Python type error instead of the library’s own error.

    Two error messages concatenated an enum to a string, so an invalid option type raised a Python TypeError from the message itself instead of the library’s FinError.

  5. Fixed four of the six Monte Carlo pricing methods, which returned NaN for every input because they passed the option type where the model expects the interest rate.

    Four of the six Monte Carlo pricing methods returned NaN for every input: they passed the option type in the position the model reserves for the interest rate. A test now prices the same call with all six variants and pins each to the closed form.

  6. Fixed the variance-swap fair strike, which was too high by twice the dividend yield and crashed on large strike grids, and verified it against the Demeterfi, Derman, Kamal and Zhou replication.

    Under a flat volatility the fair variance strike of a variance swap must equal the squared volatility, whatever the rate and dividend yield. It came out too high by twice the dividend yield, because the log-contract drift used the rate alone and the option portfolio was compounded at the wrong rate, and it crashed on large strike grids. Fixed against the Demeterfi, Derman, Kamal and Zhou replication and verified to within 0.2 percent across rate and yield combinations.

  7. Made the FX variance swap price at all, fixed the same dividend defects as the equity version, and corrected realised variance in both classes to divide by the number of returns rather than the number of observations.

    The FX variance swap class could not run at all. Once it did, it carried the same two dividend defects as the equity version fixed earlier, with the foreign rate in place of the dividend yield. Both classes also divided realised variance by the number of observations instead of the number of returns, which understates it by one part in N.

  8. Fixed the FX double digital option, which discounted a domestic payout at the foreign rate and valued a foreign payout with the wrong probability term, and tested it against the difference of two single digital options.

    The double digital still had the payout-currency errors that had already been corrected in the single digital. A domestic cash payout was discounted at the foreign rate, and a payout of one unit of foreign currency, which is an asset-or-nothing claim, used the cash-or-nothing probability term. A range digital is a digital call at the lower strike less one at the upper strike, so the tests pin it to that difference in both currencies.

  9. Made the FX rainbow option price at all, corrected its correlation, which ignored the second rate, and repaired FX option rho, which raised an error for every product that inherits it.

    The rainbow option read an attribute it never set, so it could not be priced. In its one-factor model the correlation of the two rates is the product of their loadings, but the code squared the first and ignored the second, and the Monte Carlo passed the loadings where a correlation matrix was expected. After the fix it matches the equity rainbow option (Stulz 1982) for all four payoffs. FX option rho called a curve method that does not exist.

  10. Fixed the SABR volatility formula, which overwrote one minus beta with another term, so smiles were exact at the money but wrong in the wings by up to two volatility points; tested against the exact Bachelier limit.

    SABR is the market-standard smile model for interest-rate options. In the library the variable holding one minus beta was overwritten by the correlation term before the denominator of the Hagan formula was formed, so the smile was right at the money and wrong away from it. With beta zero and no volatility of volatility, SABR is the Bachelier model and its Black volatility is known exactly: on a 3 percent forward the library was 215 basis points of volatility too high at a 1 percent strike, against 3 for the textbook formula. The fix renames one variable in three places.

  11. Repaired the swaption volatility surface, whose calibration report and printed summary always raised an error and whose fixed-beta SABR fits started from a degenerate point.

    The swaption volatility surface compared a numpy grid with an empty list, so its calibration check always raised, and its printed summary had been copied from the FX surface and referred to attributes it never sets. Its fixed-beta SABR fits started from zero parameters, which divided by zero for beta one and left beta one half three volatility points off a smooth smile.

  12. Fixed the credit default swap premium leg, which accrued the first coupon over the following period rather than the current one and, in the final coupon period, read past the end of its accrual factors and returned a negative risky PV01.

    The premium leg of a credit default swap pays the running spread on each coupon date if the reference entity survives, plus the accrued portion on default. The first coupon was accrued over the length of the next period instead of the current one, a leftover from an older layout of the accrual factors that had a leading zero. The difference is a few days of accrual, about 0.06 percent of the risky PV01 on a five-year contract. In the final coupon period there is no next period, so the compiled kernel read past the end of the array and returned a negative clean PV01 and a par spread of minus 248 basis points. The fix is a one-index change; fifteen pinned regression values across the CDS, index, option and tranche tests moved by 0.05 to 0.5 percent and were updated with an instrumented run.

  13. Made the CDS index spread adjustment do something: its calibration loop never ran, so the issuer curves came back unchanged, and once entered it called a method that no longer existed.

    Adjusting single-name curves so that the portfolio reprices a traded index is standard in index and tranche pricing. The spread-based version started its iteration counter at the value its loop condition tests for convergence, so the loop never ran and the input curves were returned unchanged. Once the loop did run it called a method renamed a month earlier. With both fixed, ten random issuer curves with a 176 basis-point intrinsic spread reprice to 100 and 150 basis-point index coupons, and non-zero upfronts match to one part in a million of notional.

  14. Corrected the sign of a pay-fixed forward rate agreement, which lost value when the forward rate rose, the opposite of a pay-fixed swap on the same curve.

    The party paying the fixed rate on a forward rate agreement receives the difference between the realised and contract rates, so the contract gains when the forward rises. The library flipped that sign, with comments in the code questioning it, so a pay-fixed FRA struck at 3 percent was worth minus 4,811 on a million notional with the forward at 4.08 percent while a pay-fixed swap on the same curve was positive. Curve bootstraps were unaffected because they solve for a zero value.

  15. Fixed floating-rate note pricing from a discount margin, which crashed unless accrued interest had been computed first and otherwise reused a stale coupon period.

    Pricing a floating-rate note from its discount margin read the previous and next coupon dates from attributes that were only set as a side effect of the accrued-interest method, so calling the pricer first raised an error and calling it later for another settlement date silently reused the old coupon period. The lookup now lives in one helper both methods call.

  16. Aligned the US Treasury yield convention in a bond’s final coupon period with the Treasury formula, which uses simple interest over the fractional period; the library compounded there, so the convention switched once a bond entered its last period.

    Under the US Treasury convention (31 CFR 356, Appendix B) a bond price is the sum of discounted coupons and principal, with the fractional first period discounted at simple interest. The library did this while full coupon periods remained but compounded over the fraction once the bond was in its final period, so the convention changed on the penultimate coupon date. Setting the number of remaining full periods to zero in the regulation’s general formula gives simple interest, which is also what the street convention does in that period. On a 4 percent semi-annual bond three months from maturity at a 5 percent yield the price moved by 0.8 cents per 100; mid-period prices and the existing Treasury pins are unchanged.

  17. Fixed the Gatheral and Weber Heston pricers, which returned negative call prices at long maturities when the variance is mean-fleeing under the share measure; the formulas were sound and the quadrature was missing a boundary layer near zero frequency, so both now integrate in the logarithm of the frequency.

    The Heston model prices a call from two probabilities, each a Fourier integral of a characteristic function over frequency. When the mean-reversion speed is smaller than the product of the correlation and the volatility of variance, the variance process is mean-fleeing under the share measure, and the first characteristic function only reaches its limit of one inside a layer around zero frequency whose width shrinks exponentially with maturity. At thirty years the layer is about 1e-20 wide and the integrand carries a 1/u tail across twenty decades, so the adaptive quadrature never saw the layer and returned a call price of minus 12.36 where the Lewis methods gave 24.68. The closed-form exponents were checked against a numerical solution of the Riccati equations and were correct, so the fix is to the integration: both methods now integrate in the logarithm of the frequency, which makes the tail bounded and puts the layer at a finite, resolvable point. Over 400 random parameter sets the largest difference from the Lewis price fell from 48.6 to 5.7e-5.

  18. Fixed the forward risky PV01 in the Anderson CDS index option pricer, which took the previous coupon date from after option expiry and dropped the first coupon; the two errors nearly cancelled at the forward spread but gave the wrong spread dependence inside the lognormal integral.

    The Anderson pricer for an option on a CDS index integrates the payoff over a lognormal forward spread, and at each spread it needs the risky PV01 of the index contract that starts at expiry. The previous coupon date was found by scanning for the first payment date after the valuation date, but the forward contract starts at expiry, so the scan landed on the first coupon after expiry and the accrual from it was negative. Separately, the coupon loop started from the second payment. At the forward spread the two errors nearly cancelled, which is why prices looked plausible, but the dropped coupon should have been survival weighted and discounted while the added constant was neither, so the PV01 had the wrong dependence on the spread. The previous coupon is now the forward contract’s first accrual start, floored at zero when a business day adjustment pushes it past expiry, and the loop includes the first coupon. On a ten-name index at 150 basis points the forward PV01 moved from 3.6234 to 3.6172, and payer values by up to 0.17 percent at high volatility.

  19. Kept the value-date node in the survival curve built by the one-factor Gaussian basket pricer, which had overwritten it with the first payment date and so placed a default mass at the value date; a one-name basket now reprices its own CDS exactly.

    The one-factor Gaussian pricer for an n-th to default basket builds a basket survival curve on the payment dates and hands it to the CDS contract. The code set the first node to time zero with survival probability one and then overwrote it, because the loop over payment dates started at index zero, so the curve began at the first payment date with a probability below one and the flat-forward interpolation turned that into a default mass at the value date. A first-to-default basket on a single credit is that credit’s CDS, so its breakeven spread must equal the CDS par spread; it was 1.4 basis points high per 100 basis points of spread, independent of correlation. The maintainer changed the basket interface while the fix was under review, so the final version builds the curve as a list on the new interface, and the tests compare a one-name basket with the legs of its own CDS contract and a two-name independent basket with the product of the two survival curves.

  20. Fixed FX double one-touch and double no-touch options when the spot is on or beyond a barrier: a knocked-out no-touch paid the full amount and a one-touch sitting on its barrier paid nothing.

    A double no-touch pays if the spot stays between two barriers to expiry; a double one-touch pays if either barrier is hit. Two edge cases were wrong. With the spot already beyond a barrier the valuation returned the discounted payment for both types, although a knocked-out no-touch is worth nothing. With the spot exactly on a barrier the fast pricer returned the payment when asked for a one-touch, but its caller treats the result as the no-touch price and subtracts it from the payment, so a one-touch that had just touched was priced at zero. The fast pricer now always returns the no-touch value, as its docstring and caller assume, and the valuation treats a spot on or beyond either barrier as touched, matching the Brownian-bridge Monte Carlo. Found by a numerical audit that compares each pricer with Monte Carlo and with its own limits; the regression test only used spots inside the barriers.

  21. Fixed perpetual bond pricing off a sloped discount curve, which valued the tail at the spot rate and was 12 per 100 high for monthly coupons, and let annuities use the ACT/ACT ICMA day count.

    Pricing a perpetual bond off a discount curve summed two hundred coupons and then valued the remaining perpetuity at the spot zero rate to the last summed date. On an upward-sloping curve that rate is well below the forward rates that discount the tail, so a 5 percent monthly perpetual priced at 129.88 against a reference of 117.44, and an annual perpetual could not be priced at all because two hundred annual dates ran past the supported range. The method now takes a horizon in years, keeps every date inside the range, and values the tail as a geometric sum continuing the forward rate over the final year of summed coupons, measured over a whole year so that month lengths do not bias it. All four frequencies agree with a brute-force reference to 0.001 per 100. The same pull request let annuities use the ACT/ACT ICMA day count, which raised because the accrued-interest and payment calculations passed a float where a frequency type was expected and omitted the period end.

  22. Fixed the large homogeneous portfolio tail probability, which applied the normal density where the formula needs the inverse distribution function and then returned the loss distribution instead of its complement; at a 5 percent loss level it gave 0.46 for a probability of 0.086.

    The large homogeneous portfolio model of Vasicek gives the distribution of the loss on a portfolio of many identical credits under a one-factor Gaussian copula. The public function for the probability that the loss exceeds a level had two defects: it called the normal density where the formula needs the inverse normal distribution function, as every other function in the module does, and it returned one minus the distribution function, which is the distribution function of the loss itself rather than its complement. For 100 credits with a 3 percent default probability, 40 percent recovery and a correlation of 0.5, the probability of a loss above 5 percent came out at 0.46 against a correct 0.086. The function is not used elsewhere in the library, so no product prices changed. The test checks the identity with the loss distribution function on a grid of 25 levels and five correlations to 1e-12, the limits at zero loss, maximum loss and zero default probability, and monotonicity in the level.

  23. Made the composite discount curve return a float for a scalar time, as every other curve does, instead of a one-element array.

    A composite discount curve multiplies the discount factors of its child curves, which is how a parallel shock or a basis is layered on a base curve. Its time interface returned a one-element array for a scalar time, while every other discount curve returns a float for a scalar and an array for an array, so code that formats the result, compares it with a float or stores it in a scalar field behaved differently on a composite curve. The method now converts the input to an array of times, multiplies as before, and returns a float when the input was a scalar. The child curves are always called with an array, which is the form they all accept. The date interface was already correct and is unchanged.

  24. Made all six classes in the rates swaps package constructible and valuable: three could not be built, the two float-float basis swaps priced both legs from a schedule the constructor never created, and the fixed-leg classes crashed on a fresh object or a seasoned date; both kinds now reprice the library’s own basis and vanilla swaps to 1e-10 from forward start to maturity.

    The rates swaps package holds six basis and cross-currency swap classes that the maintainer had flagged as work in progress ahead of a December review, with an invitation to fix what could be fixed. Three could not be constructed: two built their calendar from the enum rather than the calendar class, and one read a schedule attribute under a name that is never set. The two float-float basis swaps had a valuation copied from a fixed-float swap, so both legs were priced from a schedule and day count the constructor never created and the basis spread was never applied. The leg valuation now takes each leg’s own dates, day count and spread and follows the conventions of the library’s floating leg: the spread is added to the received rate, a first fixing can be supplied, the index curve is only read once a rate has to be implied so that a seasoned swap values on curves anchored at the value date, a payment on the value date counts as paid, and a matured swap is worth zero. In the four fixed-leg classes the value-date discount factor was stored under one name and divided by another, so PV01 and the swap rate raised on a fresh object; the first floating forward was implied from the effective date while the accrual ran from the previous payment date; a supplied fixing dropped the spread; and two print methods used a date format the date class rejects. The float-float classes reproduce the library’s basis swap to 1e-10 and the fixed-leg classes its vanilla swap, at the effective date without fixings and with fixings at a forward start, a seasoned date, a payment date, maturity and after maturity; 45 of the 54 new tests fail on the previous code. The design questions the package still carries, such as whether a payment on the value date is included and which of the duplicated classes to keep, are listed in the pull request for the December review rather than decided unilaterally.

vectorbt (opens in a new tab) a backtesting library

two pull requests merged, September 2026

  1. Fixed the deflated Sharpe ratio, which used excess kurtosis where the Bailey and López de Prado formula requires raw kurtosis, and which counted missing returns as zero-return periods.

    The deflated Sharpe ratio of Bailey and López de Prado uses the raw kurtosis, which is 3 for Gaussian returns and recovers the classic Lo standard error. The implementation passed excess kurtosis, so the variance term was off by three quarters of the squared Sharpe ratio and could turn negative. It also zero-filled missing returns before computing skewness. Both fixed, with tests against the paper’s formula.

  2. Made the Numba and Rust engines agree on expanding windows shorter than the minimum period, matching pandas.

    Found with a harness that runs every function through both the Numba and Rust engines on clean, gappy, constant and short inputs. The one discrepancy: expanding windows shorter than the minimum period raised an error in Numba and returned NaN in Rust and pandas.

skfolio (opens in a new tab) a scikit-learn-compatible portfolio-optimisation library

four pull requests merged, two under review, September and October 2026

  1. Under review: a parametric portfolio policy estimator after Brandt, Santa-Clara and Valkanov (2009): weights as a linear function of characteristics, fitted by maximising realised CRRA utility, with long-only and transaction-cost variants.

    Weights are the benchmark plus a linear tilt in cross-sectionally demeaned characteristics, and the few coefficients maximise the realised CRRA utility of the return path, with no expected-return or covariance estimate. The smooth case is concave and solved with a trust-region method using the analytic gradient and Hessian; long-only and transaction-cost variants use a quasi-Newton method. Reviewed in discussion, including a look-ahead bias the reviewer caught and I fixed.

  2. Merged: online learning for the Schur complementary allocation, so it can be updated in a stream. The maintainer extended it with spectral seriation, released it in skfolio 1.8.4 and credits me in the source file.

    Adds streaming updates to the Schur complementary allocation and to the covariance-based distance estimator, so both can be fed new observations without refitting on history. Chunked updates reproduce the batch fit exactly.

  3. Merged: a bias fix in the regime-adjusted covariance estimator, whose short-term volatility multiplier was normalised by the square root of the asset count rather than by the expected chi statistic, understating variance by a third for a single asset; the maintainer confirmed the argument and measured better next-day variance forecasts on 2011 to 2022 stock data.

    The regime-adjusted exponentially weighted covariance scales its forecast by a short-term volatility multiplier built from the norm of a standardised return vector. For the diagonal target the norm was divided by the square root of the number of assets, but the expected norm of a standard Gaussian vector is the mean of a chi distribution, which is smaller: dividing by the square root of n biases the multiplier below one even when the risk scale is right, by 35 percent of variance for one asset and 9 percent for five. The fix divides by the expected chi statistic, as the Mahalanobis target already did. The maintainer agreed with the derivation, noted it also holds for Gaussian scale mixtures such as multivariate Student-t returns, and found that the change improved the QLIKE loss of next-day variance forecasts on the library’s stock dataset over 2011 to 2022.

  4. Merged: information criteria for the univariate marginals now count only estimated parameters, so a distribution with a fixed location or scale is no longer penalised for parameters it did not fit when the library selects a marginal.

    Akaike and Schwarz information criteria penalise the number of estimated parameters. The univariate marginals counted every parameter of the underlying scipy distribution, including a location or scale the user had fixed, so a zero-mean Gaussian paid for two parameters when it fitted one, and a Student-t with fixed location and scale paid for three when it fitted one. The library ranks candidate marginals by these criteria before building a vine copula, so fixed-parameter candidates, a natural choice for daily returns, were unfairly ranked. The count now subtracts the fixed attributes. Tested over the four distributions with none, one and two parameters fixed.

  5. Merged: walk-forward splits with a negative calendar offset dropped the last rebalancing date, because the anchor range was extended on the start side but not the end; the fix mirrors the start-side adjustment.

    Calendar-based walk-forward splitting generates rebalancing anchors on a frequency and shifts them by an offset, for example three days before each Monday. The anchor range had been widened on the start side to allow for the shift but not on the end side, so with a negative offset the final anchor, which falls just after the data but maps to a date inside it, was never generated. On a four-week daily sample the last test window was silently merged into the previous one, or dropped. The fix widens the end of the range by the same offset; anchors beyond the data are still filtered out, so positive offsets are unchanged.

  6. Reported and prototyped a fix for the Student-t copula distribution function, which was computed by Monte Carlo and so varied between calls and lost accuracy in the tails; the maintainer thanked me for the finding and shipped a deterministic integration, together with a joint survival method, in his own pull request.

    The bivariate Student-t copula has no closed-form distribution function, and the library estimated it by Monte Carlo, so the same input gave different values on each call and the deep tails were inaccurate. I proposed a deterministic one-dimensional quadrature of the conditional t distribution with tests against a double integral of the bivariate density. The maintainer found the conditional form could miss mass under strong correlation in a batched evaluation and a separate precision loss in the shared tail-concentration code, and addressed both in his own pull request with a finite-interval integration and a joint survival probability method, released in skfolio 1.10.

  7. Under review: a rolling-window multi-factor exposure estimator with gap-tolerant lags.

    Turns one panel field into a library of trailing statistics and lags as a single multi-factor exposure, with a policy for listing gaps: either invalidate the lookback or measure it in active observations.

The same work appears in brief on the CV.