V0 Changelog#
0.67.0 (2026-09-26)#
Added#
One licence check for every KaxaNuk library. The engine validates its licence through
kaxanuk-license-client1.0, the package the Backtest Engine, Attribution Analysis and Portfolio Construction share, instead of a validator of its own. It is a new dependency,kaxanuk-license-client~=1.0, which takes the client’s fixes without a new engine release, and it comes from KaxaNuk’s private package index, the one the engine’s ownpip installcommand already names, which serves it to any valid licence.services.license_validatorstays as a thin wrapper with the names the engine and the KaxaNuk Research Lab use:validate_license(),_state(now the client’s own dict, changed in place and never replaced), the licence errors as attributes of the module, and a newreset_validation_state(), which forgets that the process validated so the next call checks again. It also holdsPRODUCT, what the engine tells the client about itself: the productbacktest-engine, the key inKNBE_API_KEY_KAXANUK, another server inKNBE_LICENSE_SERVER_URL, the unified licence server by default, and the logger it has always used,kaxanuk.backtest_engine.services.license_validator. The Research Lab’s worker, which sets the key and the device per job, clears the flag between jobs and catches the errors by name, keeps working as it is.``LicenseConfigurationError``, raised when
KNBE_LICENSE_SERVER_URLorKAXANUK_LICENSE_SERVER_URLis not an http or https URL a request can be sent to, before anything is sent. The engine’s licence errors,LicenseErrorand its four subclasses, now inherit fromBacktestErrorand from the client’s matching error at once, so bothexcept BacktestErrorandexcept kaxanuk.license_client.LicenseErrorcatch them.``KNBE_LICENSE_SERVER_URL`` points the engine, and only the engine, at another licence server.
KAXANUK_LICENSE_SERVER_URLstill works, for every KaxaNuk library, and is read after it.
Changed#
The licence is checked for this product. The request to the licence server names the product,
backtest-engine, and the engine’s version; a licence that does not include the Backtest Engine is refused with a message naming the products it does include.A backtest keeps running when the licence server cannot be reached, for 3 days. A successful check is saved, and inside 24 hours a run does not contact the server at all, as before. When the server cannot be reached, a check saved less than 3 days ago stands in, with a warning each run saying how much of the grace is left. “Cannot be reached” is now anything that is not a verdict on the licence: no connection, a timeout, a redirect, a 400, 408, 422, 425, 429 or 5xx, or a 200 that cannot be read. In 0.66 the grace was 7 days but covered only connection errors and timeouts, and any answer other than a 200, an outage on our side included, rejected the licence outright. The grace runs from the last *successful* check, never past the expiry the server reported, and ignores a check stamped more than an hour in the future, so neither repeated offline runs nor a rolled-back clock can stretch it.
A rejection is final. A 401, 402, 403 or 404, or a 200 whose
okis nottrue, raisesLicenseValidationErrorat once and deletes the saved check, so going offline cannot bring a revoked licence back. In 0.66 a rejection left the saved check in place, where it went on standing in (up to 24 hours from the last success online, 7 days offline), and a 200 whoseokwas"yes"or1was accepted.Where the saved checks live. One file per licence key, named after the key’s SHA-256 digest, in the folder
%LOCALAPPDATA%\KaxaNuk\licence-cache\backtest-engineon Windows and$XDG_CACHE_HOME/kaxanuk/licence-cache/backtest-engineelsewhere;KAXANUK_LICENSE_CACHE_DIRmoves them. Each holds the key’s digest, never the key, is signed, is tied to the device, and is written atomically, so a process that validates several users’ keys, like the Research Lab’s worker, never mixes them.~/.kaxanuk_cache, the single cache of 0.66 and earlier, is neither read nor removed: the first run after upgrading needs to reach the licence server, and the old file can be deleted by hand.``.kaxanuk_license`` is read as a ``.env`` file. The line
KNBE_API_KEY_KAXANUK=<key>is the engine’s, and a file holding a single key without a name serves every product; in 0.66 the first line counted, whatever product it named. AKNBE_API_KEY_KAXANUKholding only spaces now counts as unset instead of being sent as an empty key.validate_license()returns the client’sValidationResult: keyword-only, withok,messageand the licence’sexpires_at. Code that built the engine’s two-fieldValidationResultby position has to name its fields. Every licence message and log line is ASCII and never carries the licence key; the device fingerprint is unchanged, so no machine takes a new device seat.Development and CI.
kaxanuk-license-clientis the only package taken from KaxaNuk’s private index, and it is taken from nowhere else ([[tool.pdm.source]]withinclude_packagesandexclude_packages): installing the project with PDM needsKAXANUK_INDEX_KEYset to a licence key, which the two workflows read from the secret of the same name. The upload to Google Cloud Storage now deletes only the engine’s older wheels, never another package’s.The Docker image and the dev container take the private index’s key as a build secret. Installing the engine’s dependencies now needs
kaxanuk-license-client, which only KaxaNuk’s private index serves, so theDockerfilereadsKAXANUK_INDEX_KEYfrom a BuildKit secret (docker build --secret id=kaxanuk_index_key,env=KAXANUK_INDEX_KEY .) and.devcontainer/docker-compose.ymlpasses it the same way. The key is used by the install step only and is never stored in the image.Read the Docs no longer installs the engine. Installing it would need the licence client from the private index, so the docs build stopped updating; autodoc already imports the engine from
src/, andkaxanuk.license_clientis now mocked.
Removed#
The PyPI and ghcr.io publishing jobs.
.github/workflows/main.ymlcame from the Data Curator’s workflow, which publishes to PyPI and to GitHub’s container registry; a tag or a manual run would have made the private engine public. It now only lints and tests onmainanddev; the engine still ships only as a wheel in thekaxanuk-packagesbucket (build-and-upload.yml).
Fixed#
``main()`` returns a licence that does not validate, as its docstring promises. The licence was validated before any
try, so a missing, rejected or unverifiable licence escaped as an exception instead of coming back in theBacktestResultlike every otherBacktestError. It is now validated first inside the setup phase, before any data is read: it comes back withsuccess=Falseand the licence error, and, since the package logger is configured first, the licence check’s warnings and errors reach the caller’s log file.A log line with an accent no longer breaks the log.
services.config_loggeropened the log file withlogging.FileHandler(path, mode="a")and no encoding, so it was written in the machine’s codepage: an accented output folder left a file nothing could read back as UTF-8, and on a console codepage that lacks the character the write failed outright. The file is now UTF-8 whatever the machine is set to. Its console twin had the same weakness - a character the console cannot encode raisesUnicodeEncodeErrorinsidelogging, which prints--- Logging error ---and drops the record, warnings included - so the console stream is now moved to UTF-8 when it is not on it already, and a console that refuses to be reconfigured gets the record with the offending characters replaced rather than no record at all.
0.66.0 (2026-09-11)#
Added#
Slippage models can see what they are trading. New
SlippageContextvalue object (kaxanuk.backtest_engine.entities.slippage_context), followingCommissionContextandFinancingContext: it carries the ticker, the trading day, the pre-slippage price, the order size and its side, plus the day’s average daily volume, volatility and half spread when the market data provides them (Noneotherwise), and aparticipationproperty with the order’s share of the daily volume.SlippageModelInterfacegains a non-abstractapply_to(context)whose default forwards toapply(), so every model written against the three-argument interface keeps working untouched, user-injected ones included; a model that needs market data overridesapply_toinstead. The execution broker, the single place slippage is applied for both engines, now callsapply_toand threads the rebalancing date into_create_sell_order/_create_buy_order.PriceDatagains optionalaverage_daily_volume,volatilityandhalf_spread(allNoneby default), andexecute_rebalance_vectorizedgains the matching optional arrays aligned totarget_tickers, where a NaN element is read as “no observation” rather than as a zero. Nothing populates them yet; the data arrives in a later release.Execution cost is attributable.
TradeOrdergainsreference_price, the fill price *before* slippage (Noneby default, so orders built by older code report no fabricated cost), and aslippage_costproperty equal toabs(execution_price - reference_price) \* shares. The broker records the reference price on every order it creates, on both sides.Five optional market-data columns for execution costs.
volume_column,high_column,low_column,close_columnandspread_columnjoinborrow_fee_rate_columninStandardField.OPTIONAL_FIELDS, so the handlers,MarketDataTable,FieldMappingand the wide-table builder treat them exactly as they already treated the borrow fee: a file without the column loads fine, blank cells become nulls, and none of them is ever treated as a price by the integrity check.FieldMappingandConfigurationnow derive their optional attributes from the enum instead of naming each one, so a future column needs no further plumbing.Configurationgainsuser_column_volume,user_column_high,user_column_low,user_column_close,user_column_spread,execution_cost_window_days(default 20, range 2 to 252) andspread_estimator, all reachable throughwith_overrides(). They are not in the Excel sheet yet; that, and the batch wiring, come with the models that read them.Three derived metrics, computed only when their columns are mapped. New
kaxanuk.backtest_engine.market_data_pipeline.execution_cost_metrics:c_adv(average daily volume),c_volatility(sample standard deviation of daily returns) andc_half_spread(half the relative bid-ask spread, from a mappedspread_columnor estimated from daily bars). NewSpreadEstimatorenum with Corwin-Schultz (2012, highs and lows only, the default) and Abdi-Ranaldo (2017, also needs closes, more stable on daily data); a negative estimate is read as zero, the papers’ convention, and the half spread is capped at 5% so one broken bar cannot swamp a backtest. Two rules hold everywhere: every window ends on the trading day before the one it is stamped on, never including the bar being traded into, and an incomplete window yields a null, never a value from a shorter sample, because a null is what makes a model take its documented fallback.The windows are warmed up. The input handlers filter each file to
[start_date, end_date]as they read it, so a rolling window would have no history on the first day of a backtest however long the file is. When an execution-cost column is mapped, the pipeline now loads from earlier (about 1.6 calendar days per trading day of window, plus a cushion), computes the metrics, and trims the extra rows off again before anything else sees them — so the schedule, the integrity check, the delisting scan and both engines work on exactly the rows they worked on before. Shared by the single-backtest pipeline and the batch data pool (warmup_configuration(),trim_to_backtest_window()).Two slippage models for the standard cost decomposition.
HalfSpreadSlippagecharges half the bid-ask spread, the cost of crossing it: a buy lifts the ask and a sell hits the bid, and the cost is flat in order size.SpreadPlusImpactSlippageadds square-root market impact to it —half_spread + impact_coefficient \* volatility \* sqrt(participation)— which is the decomposition the external feedback asked for, minus the commission the engine already charges. Each input comes from the ticker’s own market data when it is there and from the configured constant otherwise.``SquareRootSlippage`` and ``VolumeBasedSlippage`` now read the context. They took a single average daily volume and volatility for the whole universe; they now use each ticker’s own, per day, and fall back to their constructor constants only where the data has none. The three inputs fall back independently, so a file with highs and lows but no volume still gets a real spread and only a constant impact. With a context carrying no market data at all,
apply_to()returns the very floatapply()returns, so a model injected before the context existed behaves exactly as it did.Slippage is configurable from the Excel sheet. The
Generalsheet gains five rows aftercash_interest_spread—slippage_model(a dropdown:none,basis_points,half_spread,square_root,spread_plus_impact;noneby default),slippage_basis_points(0),market_impact_coefficient(0.1),execution_cost_window_days(20) andspread_estimator(a dropdown:corwin_schultz,abdi_ranaldo) — and five column mappings afterborrow_fee_rate_column:volume_column,high_column,low_column,close_columnandspread_column, blank by default. Until now the six slippage models could only be injected through the Python API, so every production run had zero slippage whether or not that was intended. NewSlippageTermsvalue object (kaxanuk.backtest_engine.entities.slippage_terms), the single authority on the ranges, followingBasisPoints,ExposureLimitsandFinancingTerms;SlippageModelTypeandSpreadSourceenums;build_slippage_model()factory next to the commission and financing ones, which the engine now calls instead of hard-codingNoSlippage().An unworkable configuration is refused before any file is read. A spread-based model with no spread column, no highs and lows to estimate from, and
slippage_basis_pointsat 0 would have charged nothing while looking like it was working; a market-impact model with no volume column would have priced every ticker in the universe off one reference volume;abdi_ranaldowithout a close column has nothing to compute. All three now stop the run with a message naming the column to map or the number to set.Where the spread comes from is derived, not configured. Whichever columns you map already says it: a mapped
spread_columnbeats an estimate from daily bars, and an estimate beats one constant for the whole universe. There is no fourth row to get out of step with the other three, and no way to ask for an estimate while mapping a vendor column.The methodology page gains an “Execution Costs” section: the decomposition, both spread estimators written out as their papers define them, why every window ends the day before the one it prices, what happens when a ticker has no data of its own, and why an execution price a vendor has already penalised must not be combined with a slippage model.
Batch runs accept every new setting in
global_defaultsand in per-portfolio overrides, validated through the sameSlippageTerms;slippage_basis_pointsandmarket_impact_coefficientjoin the fields a variation sweep can vary, whileslippage_modeldoes not, matching howcommission_modelis treated.Execution cost is reported, not just charged. Results gain
Total_slippage_costs(rounded to cents, like commissions),Slippage_df(cost by date and ticker, the counterpart ofCommission_df) andExecution_costs_df(Commissions,SlippageandTotalper rebalancing date, so the two halves of what trading costs can be read side by side; several results on one date, as a forced liquidation on a rebalancing day produces, are summed).RebalanceResultgainstotal_slippage, accumulated by the broker in the same pass as the commissions, in the same order in both engines. “Total Slippage Costs” joins the summary statistics next to commissions and financing, a “Slippage Costs” card joins the dashboard, and the Excel report gainsslippageandexecution_costssheets — written only when slippage was actually charged, since under the default every cell would be zero andexecution_costswould just repeat the commissions sheet. NewSlippageRecord, exported next toCommissionRecord.HalfSpreadSlippageandSpreadPlusImpactSlippageare exported fromkaxanuk.backtest_engine.backtestand...backtest.cost_models, next to the models that were already there.No fallback is silent. Every model that reads market data logs, once per ticker, which input it could not find, what constant it used instead and that mapping a column would fix it. A backtest cannot quietly price a whole universe on defaults.
The metrics reach both engines.
MarketDataBundlePyarrowgainsmetric_tables, held apart fromprice_tablesbecause these are derived rather than mapped, and validated against them so a metric table is always on the same tickers and the same dates as the prices. The scalar engine filters them per day and fills the newPriceDatafields; the vectorized engine keeps one dense matrix each and slices them alongside the prices. A missing observation arrives asNone, never as a zero.
Changed#
``parameters_format_version`` bumped to ``0.66.0``. The
Generalsheet gained ten rows, so existing parameter files must be regenerated withkaxanuk.backtest_engine update excel. Every new row defaults to the behaviour of 0.65.0.Nothing in results with the default settings.
NoSlippageis still the default, no execution-cost column is mapped by default, and nothing is loaded early, computed or trimmed; the new result keys are present and zero rather than absent, so a user comparing two runs sees the cost go from zero to something rather than from missing to something. Both bundled portfolios are bit-identical to 0.65.0 in both engines. Slippage still never resizes an order either: shares are floored against the pre-slippage price, so share counts are the same with and without a model, which is what lets the two engines be compared share for share under any of them.The derived metrics are computed from the *raw* tables, before interior gaps are carried forward for valuation. A carried price is a real price to value a position at and a fake one to measure a return with: it would show a day of exactly zero return that never happened.
Mapping an execution-cost column widens the window each market-data file is read over, so a row before
start_datewith a null in a mapped price column is now rejected at load time where it previously went unread. This only affects runs that opt into the new columns.``SquareRootSlippage`` and ``VolumeBasedSlippage`` cap an order’s participation rate, at the whole of a day’s volume by default (
max_participation, configurable). Beyond that the cost stops growing and the engine says so once per ticker. This changes the result of a backtest that injected either model *and* traded more than a day’s volume in one order; the linear volume model is the one that produced a frankly absurd number there. Below the cap nothing changes. Neither model is reachable from the Excel sheet, so only code that constructs them directly is affected.
0.65.0 (2026-09-11)#
Added#
The financing benchmark can be a dated series.
financing_benchmark_ratenow accepts, besides a number, the name (without extension) of a rate file in the market data directory, in the market data format: a date column and one column of annual rates as decimals (a Fed Funds effective series, say). NewRateSeriesvalue object (kaxanuk.backtest_engine.entities.rate_series) with as-of lookup: on each trading day the benchmark in force is the last observation on or before it, never a later one, so calendar-day or monthly series align to the trading calendar without look-ahead. The pipeline loads and validates the file (data_processors.rate_series_loader;DataPipelineResult.get_benchmark_rate_series()), requires it to start on or before the first rebalancing date, and warns when it ends before the backtest does (the last value is carried forward). Percent values (4.33for 4.33%) are rejected with a message that says so.FinancingTermsgainsbenchmark_rate_series;BenchmarkSpreadFinancingtakes the series and a constant series reproduces the constant benchmark bit for bit; the backtester factories and constructor acceptbenchmark_rate_series.The borrow fee can vary by ticker. New optional
borrow_fee_rate_columnin the ExcelGeneralsheet (anduser_column_borrow_fee_rateinConfiguration, batchglobal_defaults/ overrides andwith_overrides()): a column of the market data files with each ticker’s own annual borrow fee as a decimal.StandardField.BORROW_FEE_RATEis the first optional field: a file without the column loads fine, blank cells become nulls, negative values are rejected, and the column is never treated as a price by the integrity check. When mapped, the market data bundle carries a fourth table and both engines hand the financing model each short with its own rate (FinancingContext.short_positions,ShortPosition); a ticker or a day without a value falls back toborrow_fee_rate, logged once per ticker. Values are checked against the borrow fee range once per run.FinancingTermsgainsborrow_fee_rate_column;FinancingModelInterfacegainsbenchmark_rate_on().Results gain
Risk_free_rate: the benchmark rate averaged over the trading days of the backtest (zero when no benchmark is configured).
Changed#
The Sharpe ratio subtracts the financing benchmark.
compute_portfolio_statisticswas always called withrisk_free_rate=0; the single-backtest, batch and variation paths now passRisk_free_rate, for the portfolio and for every benchmark alike. Anyone with a non-zerofinancing_benchmark_rate(constant or series) gets a lower Sharpe ratio than in 0.64.0; with the default zero benchmark nothing changes.``parameters_format_version`` bumped to ``0.65.0``. The Excel
Generalsheet gained theborrow_fee_rate_columnrow afterdate_column(blank by default) and the description offinancing_benchmark_ratenow mentions the rate file, so existing parameter files must be regenerated withkaxanuk.backtest_engine update excel.Per-ticker borrow fees change the borrow arithmetic even when every ticker carries the global rate (a sum of products instead of a product of the sum), so mapping the column can move results at the last decimal; without the mapping the arithmetic is unchanged.
orders_dfandShares_dfare alwaysfloat64. Share counts arrive as integers frommath.floor, and pandas is deprecating (to raise in pandas 4) filling an integer column with the0.0the pivot uses for dates on which a ticker did not trade; the column is cast before pivoting. Tables that had such gaps were alreadyfloat64; a table without gaps changes type only, not values.
Fixed#
Backtests are reproducible bit for bit between runs. The ticker columns of the wide market-data tables followed the iteration order of a
set, which depends on Python’s hash seed, and the vectorized engine sums positions across those columns, so two runs of the same backtest could differ by1e-12in aDaily_Weightscell on a rounding tie. Tickers are now loaded and laid out in sorted order (single-backtest pipeline and batch data pool alike). The scalar engine’s values are unchanged; the vectorized engine’s daily valuation is now the same on every run, within the documented A/B tolerance of the previous one.The Sphinx configuration read a hard-coded
release = '0.58.2'; it now takes the version from the package.
0.64.0 (2026-09-11)#
Added#
Daily financing: borrow fee, margin interest and cash interest. Both engines now accrue financing every trading day, for the calendar days until the next trading day (a Friday finances three days, ACT/360), and deduct it from cash before the daily snapshot. Four new parameters in the Excel
Generalsheet, all annual rates as decimals and all zero by default:financing_benchmark_rate(the reference rate, e.g. Fed Funds effective),borrow_fee_rate(charged on the market value of short positions; an absolute fee, not linked to the benchmark),margin_interest_spread(benchmark plus spread charged on a negative cash balance) andcash_interest_spread(benchmark plus spread paid on a positive cash balance, never below zero; since short-sale proceeds sit in cash, this leg is also the short rebate). They are wired throughConfiguration,with_overrides(), batchglobal_defaultsand overrides, andBacktestSuite.add_variation(). With every rate at zero the model isZeroFinancingand the daily loop skips the accrual entirely, so results are bit-identical to 0.63.0.New
FinancingTermsvalue object (kaxanuk.backtest_engine.entities.financing_terms), the single authority on the rate ranges, followingBasisPointsandExposureLimits;FinancingContext/FinancingChargeentities;FinancingModelInterfacenext to the commission and slippage interfaces;ZeroFinancingandBenchmarkSpreadFinancingmodels;build_financing_model()factory. Custom models can be injected through the backtester factories’ newfinancing_modelargument.Results gain
Total_financing_costs(net amount deducted from cash) andFinancing_df(a daily ledger withDays,Borrow_Fee,Margin_Interest,Cash_Interest,Financing_Cost);DailyRecordgainsfinancing_cost. “Total Financing Costs” appears next to “Total Commissions” in the summary statistics and as a card in the dashboard.The long-short warning now reports whether financing is modeled; when it is not, it says which parameters switch it on. The high-leverage warning is silent once financing rates are configured. The engine warns once if financing charges leave the portfolio with zero or negative equity (margin calls are still not modeled).
The packaged Excel template gained the new parameter rows, each with the formatting of its neighbours, and every dropdown now sits on its own row. A new test loads the template through
ExcelConfiguratorand checks keys, version and validations.Rebalancing dates that are not trading days can move to the next trading day. New
rebalance_date_handlingparameter in the ExcelGeneralsheet (and in batchglobal_defaults/ overrides andwith_overrides()):exact(the default, unchanged behaviour: such a date is an error before the backtest starts) ornext_trading_day(the rebalance moves to the next trading day in the market data and the log names both dates). Earlier trading days are never used, to avoid look-ahead; two dates landing on the same trading day are an error, as is a date with no trading day after it in the loaded data. With anautoend date the backtest still ends on the last rebalancing date, i.e. on the moved one: the pipeline loads ten calendar days past it and trims back once the schedule is aligned. The single-backtest pipeline and the batch data pool share the sameapply_rebalance_date_handling();DataPipelineResult.get_configuration()exposes the effective configuration (resolved dates, possibly moved end date) and the batch runner uses it.Delisted positions can be closed automatically. New
delisted_position_handlingparameter in the ExcelGeneralsheet (and in batchglobal_defaults/ overrides andwith_overrides()) for a position whose price series ends before the backtest does:fail(the default, unchanged: the integrity check rejects the portfolio naming the ticker and its last price date),sell_at_last_price(the whole position is sold on its last price date, ordelisting_exit_lead_daystrading days earlier, at that day’s execution price through the normal broker, so slippage and commission apply and the order lands inorders_dfandCommission_df; a short is bought back the same way; the proceeds stay in cash) ormark_to_zero(the position is written off on the first trading day without a price: no cash, no commission). From the exit date on, any non-zero weight the file still carries for that ticker is ignored and logged; the other weights are never renormalized. The exits are planned once in the pipeline (plan_delisting_exits(), shared by the single-backtest pipeline and the batch data pool) from the last date on which the ticker has a value in every price column, the integrity check takes them into account, and both engines execute them after the scheduled rebalance of the day; the vectorized engine’s loop now runs on rebalancing dates plus forced-exit dates. Results gainForced_liquidations_df(date, ticker, reason, last price date, signed shares, price, value, commission), written to aforced_liquidationssheet of the Excel report when it has rows;RebalanceResultgainsreason(scheduledorforced_liquidation). NewDelistingPolicyvalue object andDelistedPositionHandlingenum, followingExposureLimitsandRebalanceDateHandling;ForcedExitentity;DataPipelineResult.get_forced_exits(); the backtester factories and constructor acceptforced_exits. The bundled delisting fixture (AAPL with CELG, TWTR and PXD, real series) runs end to end under both policies in both engines.
Changed#
``parameters_format_version`` bumped to ``0.64.0``. The Excel
Generalsheet gained the four financing rows (aftermax_net_exposure, formatted as percentages, default 0), therebalance_date_handlingrow (after them, with a dropdown), thedelisted_position_handlingrow (with a dropdown, defaultfail) and thedelisting_exit_lead_daysrow (default 0), so existing parameter files must be regenerated withkaxanuk.backtest_engine update excel.Vectorized engine: when financing is active, cash follows a day-by-day sequential accrual inside each window (never a
cumsum), so the cash that sizes the next rebalance is bit-identical to the scalar engine’s.
Fixed#
The Excel template’s dropdowns were attached to the wrong rows since 0.62.0 inserted the exposure rows without shifting the validations:
commission_price_columncarried thelogger_levellist (so Excel rejected typing a column name there),max_net_exposurecarried the file-format list and an orphanVWAP_adjustedlist, and the date check covered the capital and commission rows. Each dropdown now sits on its own row (market_data_input_formatofferscsv,parquetandportfolio_input_formatofferscsv,excel), and the date check covers onlystart_dateandend_date.
0.63.0 (2026-09-09)#
Fixed#
A missing price is no longer read as a price of zero. The wide market-data tables use the union of every ticker’s dates, so a ticker with no row on a day other tickers traded (a halt, a vendor gap, a delisting) carried a null that both engines converted to
0.0. That silently valued the position at nothing for the day, producing a fake drawdown that contaminated returns, volatility, Sharpe and max drawdown, and it crashed with an uncaughtZeroDivisionErrorwhen the ticker had to trade that day. Now:The price getters of both engines raise the new
MissingPriceError(aValueError, matching the documented price-getter contract) on a null, and the execution broker refuses to size or fill an order at a non-positive price.On a rebalancing date, a ticker without an execution or commission price is skipped with a warning in both engines; the other orders are unchanged. A ticker with a zero target and nothing held needs no price at all and is skipped before pricing, so a delisted name with a zero weight no longer touches the market data on later dates.
The capital base and the solvency check value a held position at the execution price, falling back to the mark-to-market price only when the execution price is missing. On complete data this is exactly the previous behaviour.
A held position without any mark-to-market price is still valued at zero, but never silently: both engines log a warning once per ticker naming the first date without a price. This is only reachable when the engine is built directly from data that skipped the pipeline.
Interior gaps are carried forward for valuation. The data pipeline (single backtests and batch runs alike) now fills each ticker’s *interior* nulls in the mark-to-market table with the ticker’s last valid price, so a held position is valued at a stale-but-real price on a day it simply has no row. Rows before the first price (not yet listed) and after the last one (delisted) stay null on purpose; execution and commission tables are never filled, because a carried price cannot be traded at;
data_dictis left untouched. The log lists every ticker filled and warns about runs longer than 10 trading days, which usually mean a halt or a delisting. This changes the results of backtests whose data has interior gaps: the fake drawdowns disappear. The bundled dataset has none in its equities, so its results are bit-identical.The integrity check now covers everything the engine will price. It previously validated only tickers whose weight *changed* between rebalancing dates, using long-only
> 0comparisons, so a rebalancing date with unchanged weights that fell on a non-trading day was skipped silently (and the engine then dropped that rebalance, losing the drift correction), short positions were never validated, and a ticker held with a constant weight through its delisting reached the engine and crashed. The checker now walks the schedule tracking open positions and validates, on every rebalancing date and in every price column, each ticker with a non-zero target weight or an open position; a zero or negative price counts as missing. It also rejects any position held past the end of its price series before the backtest’s end date (capped at the last date in the data), reporting the ticker, its last price date and the backtest end, since the engine would value it at zero from the next day on.
Changed#
SimplePyArrowDataIntegrityCheckeraccepts a keyword-onlybacktest_end_date; its result gains aheld_past_series_endentry. The single-backtest pipeline andSharedDataPoolshare the newcheck_and_bundle_market_data()so both apply identical rules.New helpers
carry_forward_interior_gaps()andvalid_date_bounds()inmarket_data_helpers.
0.62.0 (2026-08-12)#
Added#
Long-short portfolios. A negative portfolio weight now opens a short position:
{AAPL: 0.5, MSFT: 0.2, TSLA: -0.3}is 50% long, 20% long and 30% short. Weights are interpreted as gross, so that book has 1.00 gross exposure and +0.40 net. Short selling is inferred from the weights rather than switched on by a flag — the engine derives what the portfolio *is* and only asks configuration for what it is *allowed* to do.Two new parameters, ``max_gross_exposure`` (default ``1.0``) and ``max_net_exposure`` (default blank/unconstrained), wired through the Excel
Generalsheet,Configuration, batchglobal_defaults, andBacktestSuite.add_variation().max_gross_exposurereplaces the implicit “weights must sum to at most 1.0” rule with “the sum of *absolute* weights must stay within the limit”; a 130/30 book needs1.6.max_net_exposurebounds|sum of weights|for market-neutral mandates. Above 2.0x gross the engine warns that financing costs are not modeled.New ``ExposureLimits`` value object (
kaxanuk.backtest_engine.entities.ExposureLimits), the single authority on the exposure ranges and on thenet <= grossinvariant, following the same pattern asBasisPoints.The engine logs which tickers are held short and warns explicitly that short financing costs are not modeled — no borrow fee, no short rebate, no margin requirement — so a long-short backtest’s return is optimistic by roughly the borrow rate times the average short exposure.
Fixed#
``benchmark_comparison`` put Alpha, Information Ratio and Tracking Error in the benchmark column instead of the portfolio column, contradicting the
summarysheet and asserting something false — a benchmark’s alpha against itself is zero by definition, so “SPY: Alpha 23.93%” is not a meaningful reading. The values were always correct, only misplaced; nothing is recomputed. They now sit in thePortfoliocolumn, matchingsummary. With several benchmarks the portfolio has a different value against each, so those rows are emitted once per benchmark with the benchmark named in the label ("Alpha vs QQQ"); a single-benchmark run keeps the bare label.Short-sale proceeds opened on the first rebalancing date were dropped. The first-date cash formula computed
capital_to_invest - total_buy_value, ignoringtotal_sale_value. This never mattered for long-only portfolios (the first rebalance has nothing to sell) but silently lost the entire proceeds of any short opened on day one. The fix inserts the sale term into the existing expression rather than restructuring it, which is an exact no-op for long-only:total_sale_valueis0.0there andx + 0.0 == xfor every finite float. (Restructuring to the algebraically equivalentinitial_capital + sale - buy - commissionswas measured to change ~19% of long-only rebalances by up to1.19e-07, becausecash_reserve + capital_to_investdoes not always round back toinitial_capitaland the operation order differs.)PortfolioEntitynow rejects non-finite weights with aPortfolioEntityError.Decimal(str(float('nan')))succeeds, so aNaNweight previously reached a comparison that raised a rawdecimal.InvalidOperationinstead.
Changed#
``parameters_format_version`` bumped to ``0.62.0``. The Excel
Generalsheet gained the two exposure rows, and every field in the schema is mandatory, so existing parameter files must be regenerated withkaxanuk.backtest_engine update excel.Portfolio valuation, weights, order routing and the zero-weight ticker filter are now generalized by sign (
shares != 0rather thanshares > 0). These are provable no-ops for long-only inputs — a long-only portfolio can never produce a negative share count, since a sell offloor(|diff| / P)shares is bounded by the existing position whenever the target weight is non-negative — and a new golden regression test pins long-only results to exact equality.Liquidating a position (target weight
0) now uses the exact position size on both sides and routes by position side, so closing a short emits a *buy* to cover. Flooring|diff| / pricewould give the same count in exact arithmetic but can strand a fraction of a share when float rounding computes the quotient as e.g.152.99999999999997.validate_cash_positionaccepts an optionalequityargument. With shorts open,cash >= 0stops being meaningful — short-sale proceeds inflate cash, so a book can hold cash worth 119% of its own value — and the check moves from liquidity to solvency (equity > 0). Long-only callers pass nothing and keep the original behaviour exactly.PortfolioEntity’s weight rules, previously duplicated between thefrom_dictand direct-table construction paths, are now a single shared validator, so the two cannot drift apart. A new per-position cap rejects any|weight| > 1.0, taking over the typo protection that the old “no negative weights” guard provided.
Known limitations#
Short financing is not modeled: no borrow fee, no short rebate, no margin requirement or margin call. Borrow fees accrue daily, and the engine only charges costs on rebalancing dates, so adding them requires a daily accrual hook that does not yet exist. *Resolved in 0.64.0*, which accrues borrow fee, margin interest and cash interest every trading day; margin requirements and margin calls remain unmodeled.
0.61.1 (2026-07-28)#
Fixed#
The summary sheet’s statistics table now shows one column per configured benchmark (labeled by ticker) instead of a single
Benchmarkcolumn, so a multi-benchmark run ("SPY,QQQ") compares the portfolio against every benchmark side by side — matching thebenchmark_comparisonsheet. Portfolio-relative metrics (Alpha, Information Ratio, Tracking Error) remainN/Ain the benchmark columns, and a single-benchmark run is unchanged except that the column is now labeled with the ticker.The stats table keeps exactly two empty columns of separation from the annual-returns table; the gap is now computed from the stats-table width so it stays correct as benchmark columns are added.
The portfolio-info block (Start Date / End Date / Years) is placed below the stats table instead of at a fixed row 18. It previously overlapped the last two metric rows (
Max Drawdown Date,Alpha), hiding them; both rows are now visible, and an extra benchmark column no longer collides with the info block.
0.61.0 (2026-07-28)#
Added#
Multiple benchmarks.
benchmark_file_namenow accepts a comma-separated list (e.g."SPY,QQQ,IWM"); the first entry is the primary (comparison) benchmark used for Alpha / Information Ratio / Tracking Error, and every listed benchmark is built and carried through the pipeline. NewConfiguration.get_benchmark_file_names()/get_primary_benchmark()(backed by the module-levelsplit_benchmark_file_names) andDataPipelineResult.get_benchmarks()expose the full set additively, while the singularbenchmarkremains the primary for backward compatibility. A single ticker behaves exactly as before.New performance metrics. PnL (
End Balance − Start Balance), Information Ratio (mean(active) / std(active) × √252) and Tracking Error (annualized sample std of the active return), computed against the primary benchmark and surfaced in both the Excel report and the dashboard.Multi-benchmark Excel report. A dedicated
benchmark_comparisonmatrix sheet (metric × [Portfolio, benchmarks…]); the performance/drawdown chart draws one value line and one drawdown fill per benchmark; theportfolio_bench_total_valuesheet gains a<ticker>_Value/<ticker>_Returnscolumn pair per benchmark; and the annual-returns table carries one column set per benchmark.Multi-benchmark dashboard. The performance chart, the statistics table (now a metric × [Portfolio, benchmarks…] matrix), the drawdown tab (one fill per benchmark) and the annual-returns tab (one column set per benchmark) all show every configured benchmark. A single benchmark renders exactly as before.
Multi-benchmark batch runs. A comma-separated
benchmark_file_nameinglobal_defaultsor a per-portfolio override no longer breaks a batch:SharedDataPoolloads and builds every benchmark, and the ticker-collection pass expands each one. The per-portfolio report labels use the primary benchmark.
Changed#
~44× faster ``run()`` on the bundled 110-ticker × 2836-day dataset (≈17 s → ≈0.4 s), verified bit-exact against the previous engine via the side-by-side A/B test (rebalancing outputs —
Commission_df,Shares_df,orders_df,Total_commissions— identical; daily valuation within float tolerancertol=1e-9, atol=1e-6):New numpy-backed
PyArrowBacktesterFastpre-converts the three price tables to densefloat64matrices once in__init__, turning each per-ticker/day price lookup into an O(1) array index instead of a PyArrow filter-and-extract (Opt#1).run()now loops over rebalances and computes each constant-state window’s daily snapshots with a single matrix operation, cutting the Python loop from ~2500 days to ~40 rebalances (Opt#2).ExecutionBroker.execute_rebalance_vectorizedcomputes every target ticker’s diff vectorized and only iterates the tickers that actually trade, reusing the existing commission/slippage models (bit-exact).main(),MultiPortfolioBacktesterandBacktestVariationRunnerinstantiate the fast engine in production; the originalPyArrowBacktesterand the A/B test are kept as the regression baseline.
Fixed#
Annual-returns table (Excel + dashboard): it showed a single, mislabeled
"SPY,QQQ_return"column set — the rawbenchmark_file_namewas used as the column prefix and only the primary benchmark was merged — and carried a junkyear_numbercolumn. It now shows one correctly-labeled column set per benchmark (per-year CAGR and alpha computed exactly as before) and dropsyear_number; single-benchmark output is bit-identical.The
benchmark_comparisonmatrix sheet now formats its values by metric (currency / percentage / ratio), matching the summary statistics table, instead of writing raw numbers. The generic data-sheet writer had derived each cell’s format from the column name, which on this sheet is a ticker that matches no metric.
0.60.0 (2026-07-27)#
Fixed#
Daily portfolio valuation now uses the configured
mark_to_market_price_columninstead of the execution price.PyArrowBacktester._record_daily_statuswas passing_get_execution_price_for_tickerto bothcalculate_portfolio_valueandcalculate_weights, so the mark-to-market price table was loaded and filtered on every trading day but never read — themark_to_market_price_columnsetting had no effect on any result. A new_get_mark_to_market_price_for_tickergetter is now used for the daily snapshot. Trade execution, commission sizing, and the available-capital calculation continue to use the execution price, soCommission_df,Shares_df,orders_df, andTotal_commissionsare bit-exact unchanged. This changes the numerical results of existing backtests —Total_Portfolio_Value,Portfolio_Value,Daily_Weights,Returns, and all derived metrics (Sharpe ratio, max drawdown, annualized return,final_total_portfolio_value) shift whenever the mark-to-market column differs from the execution column (e.g. a dividend-and-split-adjusted series vs a split-only-adjusted one).
Changed#
Performance and memory, with no change to results (verified bit-exact against the pre-change engine on the bundled dataset —
Register_df,Daily_Weights,Commission_df,Shares_df,orders_df, andTotal_commissionsall identical):Removed the per-date
_all_prices_cachefromPyArrowBacktester. The main loop visits each trading day exactly once, so the cache never registered a hit; it only accumulated one filtered copy of all three price tables per day, holding ~64 MB of PyArrow buffers alive for the entire run on a 110-ticker × 2836-day backtest (peak memory dropped ~22%)._get_all_prices_for_datenow builds the day’s tables fresh and lets them be freed once the day is processed.PortfolioStateManager.calculate_weightsaccepts an optional pre-computedtotal_value._record_daily_statusnow passes the value it already computed for the daily snapshot instead of havingcalculate_weightsrecompute the full portfolio valuation a second time per day.
0.59.0 (2026-06-03)#
Added#
Basis-point commission model for crypto and other notional-based fee schedules. New
commission_modelselector (per_share|basis_points) andcommission_basis_pointsparameters in the ExcelGeneralsheet (and in batchglobal_defaults/ template / portfolio overrides), validated by the newBasisPointsvalue object (range 0–1000 bps) andCommissionModelTypeenum. The basis-points model prices against the real trade notional via the trade price (1 bp = 0.01%, so 100 = 1%). The default remainsper_share, preserving existing behavior. Model selection is dispatched by the newbuild_commission_model()factory.
Changed#
Breaking:
CommissionModelInterface.calculate(shares)is nowcalculate(context), receiving a newCommissionContext(shares + price) value object. All built-in commission models were updated; the unusedPercentageCommission.calculate_with_price()method andnotional_per_shareparameter were removed. Custom external commission models must update theircalculatesignature accordingly.Excel
parameters_format_versionbumped to0.57.0. Configuration files using an older format version are rejected with an “outdated format” message (runkaxanuk.backtest_engine update excelto get the latest template).
Documentation#
Removed obsolete references to
execution_pricefrom the End User Manual (configuration.rst,excel_workflow.rst,running_from_python.rst). The field was removed in v0.57.0 and is now rejected byBatchConfigParsersince v0.58.5; users following the manual no longer see it as a valid parameter.use_cases.rstnow points to the publicKaxaNuk/Backtest-Engine-Use-Casesrepository as the single source of executable examples, mirroring the pattern used by the Data Curator project.end_user_manual/index.rstexpanded with an Excel-vs-Python “Choose your workflow” table that helps a new reader pick the right execution path.Main
index.rstgained a “What’s new in 0.58.x” section with a TL;DR of breaking changes and headline fixes.
0.58.6 (2026-05-19)#
Fixed#
generate_comprehensive_excel_reportnow restores thedateindex column on theportfolio_bench_total_valuesheet.pd.concat([register_df, benchmark[...]], axis=1)was silently droppingindex.namebecause the two source DataFrames carried different names (register_df.index.name == "date"vsbenchmark.index.name == "date_column"fromStandardField.DATE.value). Without a name,include_index = dataframe.index.name is not NoneevaluatedFalseand the writer skipped both the header and every date cell, leaving the sheet with only the value columns. The concat result is now explicitly renamed to"date", mirroring the other data sheets.
0.58.5 (2026-05-13)#
Changed#
BatchConfigParser.parse_dict()/parse_file()now rejectexecution_priceinglobal_defaultswith aBatchConfigErrorpointing touser_column_trade_execution_price. The key was silently ignored since v0.57.0 (whenexecution_pricewas removed from the Excel configuration and order execution was wired toStandardField.TRADE_EXECUTION_PRICEexclusively), which let users believe they were configuring the execution price via the batch dict/YAML when in fact onlyuser_column_trade_execution_pricewas being read. Surfacing the error closes that hidden mismatch and makes the batch path consistent with the Excel path.
0.58.4 (2026-05-13)#
Changed#
MultiPortfolioRunner.run()now logs the empty-batch failure vialogger.error(...)before raisingNoPortfoliosDiscoveredError. The error message appears as a single[ERROR]line in the configured logger format (matching the surrounding[INFO] Discovered 0 portfolio files ...line) instead of only surfacing as a raw Python traceback when callers invoke the runner directly without a top-leveltry/except BacktestErrorwrapper like the one inbacktest_engine.main().
0.58.3 (2026-05-13)#
Added#
NoPortfoliosDiscoveredErrorexception (subclass ofBatchConfigError→ConfigurationError) exported fromkaxanuk.backtest_engine.exceptions.
Fixed#
MultiPortfolioRunner.run()no longer crashes with an opaqueIndexError: list index out of rangeinside_build_reference_configurationwhenportfolio_directoryis configured but resolves to zero portfolios (empty directory, missing directory, or no files matching the expected format). It now raisesNoPortfoliosDiscoveredErrorwith a message that includes the scanned directory and the expected format.
0.58.2 (2026-05-12)#
Fixed#
MultiPortfolioBacktester.generate_individual_reportsnow sets the benchmarkDatetimeIndexfrom the canonicalStandardField.DATEcolumn (currently"date_column") instead of the literal string"date"that does not exist in the schema. The old lookup silently fell through and calledpd.to_datetimeon the integer RangeIndex (0, 1, 2, …), producing timestamps near 1970-01-01 that propagated to every annual-returns row, the Max Drawdown Date of the benchmark, and the embedded chart. Annual returns and Max Drawdown Date for the benchmark now align with the portfolio’s date range. Same fix applied to theregister_dflookup for symmetry.
0.58.1 (2026-05-12)#
Fixed#
portfolio_drawdown_viznow picks thedate_columnfrom the benchmark DataFrame when present, instead of relying on its index. The previous 0.58.0 fix coerced the index to datetime, but when the benchmark comes from apa.Table.to_pandas()the index is a plain RangeIndex (0, 1, 2, …) andpd.to_datetimeinterpreted those integers as nanoseconds-since-epoch, leaving every point near 1970-01-01 — the visible vertical line in the embedded Excel charts.
0.58.0 (2026-05-12)#
Breaking Changes#
Renamed environment variable
KNPC_API_KEY_KAXANUK→KNBE_API_KEY_KAXANUK. Update yourConfig/.envfile accordingly.
Added#
arrow_to_pandas_for_analysis()helper inkaxanuk.backtest_engine.modules.type_converters. Converts apyarrow.Tableto apandas.DataFramewith analysis-friendly dtypes:decimal128/decimal256columns are coerced tofloat64anddate32/timestampcolumns todatetime64[ns]. Use it when feeding a backtest’s PyArrow result into matplotlib, numpy or scikit-learn.
Fixed#
portfolio_drawdown_vizno longer renders a spurious vertical line at 1970-01-01 when the caller passes a benchmark DataFrame that originated from apa.Table.to_pandas()(object dtype withdatetime.dateindex anddecimal.Decimalvalues). The function now coerces the index and the plotted columns defensively before drawing.
Documentation#
Full Sphinx documentation overhaul: real runtime dependencies in
docs/requirements.txt(no moreMagicMockin signatures), newBacktest Componentssection coveringcost_models,enums,execution,interfacesandorchestratorssubpackages, enriched docstrings across thebacktestpackage and the data/portfolio/market pipelines, CLI page rebuilt with intro + summary table + common workflows, andSee Alsocross-references from load methods to the user guide. Build now finishes with 0 warnings.Fixed two malformed
.. deprecated::directives inportfolio_configuration_service.pyso the rendered notice reads “Deprecated since version 0.40.0: Use load_from_file() instead.” instead of the previous garbled output.load_portfolio_from_nameandload_portfolio_from_weightsare excluded from the Sphinx API reference (still callable from Python for backwards compatibility).
0.57.0 (2026-04-15)#
Breaking Changes#
Removed ``execution_price`` Excel configuration field. Order execution now always uses the column named in
trade_execution_price_column(internally mapped toStandardField.TRADE_EXECUTION_PRICE). To migrate, runkaxanuk.backtest_engine update exceland delete theexecution_pricerow from your existingConfig/backtest_engine_parameters.xlsxif you updated manually.Renamed environment variable
KAXANUK_LICENSE_KEY→KNPC_API_KEY_KAXANUK. Update yourConfig/.envfile accordingly.``MarketDataBundlePyarrow.get_execution_table()`` signature changed. The method no longer accepts an
execution_priceparameter; it returns the TRADE_EXECUTION_PRICE table directly.Excel template format version bumped from
0.54.0to0.56.0. The engine enforces this check at startup — outdated Excels must be regenerated viakaxanuk.backtest_engine update excel.Minimum rebalance dates relaxed from 2 → 1. Users relying on the previous validation for error detection should revisit their portfolio files.
Added#
__parameters_format_version__constant exported fromkaxanuk.backtest_enginefor tracking Excel schema version independently of the package version.Explicit version-compatibility check at Excel load time: raises
ConfigurationHandlerErrorwith a “run update excel” hint when the Excel format is outdated, and logs a warning when the Excel is newer than the engine supports.Support for buy-and-hold portfolios: a single rebalance date is now valid.
Validation preventing any price column (
commission_price_column,trade_execution_price_column,mark_to_market_price_column) from being set to the same value asuser_column_date.packagingadded as an explicit dependency for semantic version comparison.
Changed#
__version__bumped to0.57.0,__parameters_format_version__bumped to0.56.0.CLI module (
services/cli.py) aligned stylistically with sibling packages: added-> Nonereturn annotations, periods to docstrings, removed dead commented code.dependenciesinpyproject.tomlsorted alphabetically (case-insensitive).
Fixed#
Duplicate log output when
ExcelConfiguratorwas instantiated in a session that already had a root logger configured (logger.propagate = Falseand handler-deduplication guard added).
0.55.0 (2026-04-01)#
Added#
``load_config_env()`` helper that loads
Config/.envinto the process environment, eliminating the need to manuallyset KNPC_API_KEY_KAXANUK=…in the terminal before every runConfig/.envtemplate generated automatically byinit excelwith a placeholder for the license keypython-dotenvdependency for.envfile loadingload_config_envexported at the top-level package namespace (from kaxanuk.backtest_engine import load_config_env)
Changed#
Template
__main__.pynow callsload_config_env()before any license-validated codeLicense error message updated to recommend
Config/.envas the primary configuration methodREADME examples updated to include
load_config_env()call
0.54.0 (2026-03-31)#
Added#
Runtime license validation (phone-home) to protect against unauthorized use
license_validatorservice with device fingerprinting, local cache (24h TTL), and 7-day offline grace periodLicense validation at all entry points:
__main__.py,main(), andPyArrowBacktester.run()Custom exception hierarchy:
LicenseError,LicenseNotFoundError,LicenseValidationError,LicenseServerErrorHMAC-signed local cache file (
~/.kaxanuk_cache) to prevent tamperingIn-process validation flag to avoid redundant server calls within the same session
httpxdependency for license server communication
Changed#
.gitignoreupdated to exclude.kaxanuk_licenseand.kaxanuk_cachefiles
0.52.0 (2026-03-04)#
Added#
Batch backtesting capability for running multiple portfolios in a single execution
BatchRunnerorchestrator withSharedDataPoolfor efficient market data reuse across portfoliosBatchConfigParserfor dict/YAML-based batch configurationDirectory scanning mode to auto-discover portfolio files
Comparative Excel report ranking portfolios by configurable metric
Individual Excel reports per portfolio with:
Full performance metrics
Drawdown analysis
Annual returns breakdown
Benchmark statistics computation in batch processing
Alpha calculation in batch path
Changed#
PyArrow Decimal-to-float coercion at numpy math operation boundaries for compatibility
0.51.0 (2026-02-18)#
Added#
BacktestSuiteclass withadd_variation()method to define configuration overrides per variation“auto” date derivation from portfolio rebalancing dates
Comparative Excel reports with:
Summary metrics across all variations
Cumulative returns comparison
Drawdown comparison charts
Fixed#
Alpha metric calculation bug
CAGR (Compound Annual Growth Rate) metric calculation bug
0.50.0 (2026-01-16)#
Added#
New PyArrow-based backtesting engine (backtest_2) with component-oriented design:
Explicit PortfolioStateManager, ExecutionBroker, and ReportingEngine components
Pluggable commission and slippage models via interfaces
Immutable BacktestReport dataclass as structured result container
Clear separation between configuration (inputs) and runtime state
Ability to run multiple backtest cases (different parameters) with only one data pipeline
Fixed#
Summary card alpha now uses the same calculation as annual returns table, ensuring consistent alpha display across dashboards
0.49.0 (2025-11-25)#
Added#
StandardFieldenum for type-safe field names (VWAP, ADJUSTED_VWAP, ADJUSTED_CLOSE, DATE)Long-short portfolio support with
allow_shortparameter in PortfolioEntityPortfolioEntity.from_dict()class method for creating portfolios from dictionaries
Changed#
Portfolio validation now supports both long-only and long-short strategies
README documentation updated
Removed#
Unnecessary configuration entity fields
0.48.0 (2025-10-30)#
Added#
Annual returns table to Excel report
Annual returns analysis to dashboard
Alpha metric to annual returns table (both dashboard and Excel report)
Fixed#
Performance metrics formatting
Dashboard alpha metric format display
0.47.0 (2025-10-15)#
Added#
Helper functions module (
input_handlers_helpers) for common data processing operationsInteractive dropdown menu in portfolio weights evolution chart to filter tickers
Comprehensive docstrings to almost all codes
Changed#
ExcelPortfolioInputHandler now returns
pa.Tableinstead ofpd.DataFrameDashboard pie chart now displays Top 10 assets alphabetically with “Others” category for remaining positions
Benchmark builder now uses
ADJ_CLOSE_COLUMN_NAMEconstant instead of hardcoded stringsExcel portfolio validates first column must be ‘Ticker’
0.46.0 (2025-10-06)#
Added#
MissingBenchmarkexception and benchmark validation before processingBenchmark availability validation (
_validate_benchmark_availability())
Changed#
Backtest engine optimized (converted class parameters to Python float for better performance)
Portfolio transformer now includes all ticker weights (not just >0)
Increased decimal precision for financial calculations
Fixed#
Portfolio calculation bugs
_convert_df_to_python()now properly handles None/NaN values (converts to 0.0)
0.45.0 (2025-10-02)#
Added#
Execution price selection option (customizable execution price for backtesting)
safe_decimal()function for PyArrow Decimal conversions_convert_df_to_python()method for converting PyArrow scalars to Python values
Changed#
Migrated data handling from Pandas to PyArrow
Build system changed from
pdm-backendtohatchling
Fixed#
Input handler and Entities bugs
Removed#
Unnecessary project directories and files
0.44.0 (2025-08-27)#
Added#
Project templates for easier setup (
__main__.pytemplate for CLI init).gitkeepfiles for empty directories
Changed#
Rebalancing logic improvements
CLI interface updates and improvements
pdm.locknow excluded from version control
Fixed#
CLI bugs
0.43.0 (2025-08-12)#
Added#
Benchmark name displayed in plot legends
Sortino ratio calculation with robust downside deviation methodology
Changed#
Dashboard color scheme updated to company brand colors
Interactive pie chart now updates based on selected date
Fixed#
Functions for CVaR and VaR calculations with robust historical and parametric Gaussian methods (including Cornish-Fisher modification) were fixed
0.42.0 (2025-07-29)#
Added#
Broker interfaces implementation
Config factory as alternative to Excel configuration
PyArrow-based portfolio entity
Custom exceptions for better error handling
Mixed date format support
New ‘portfolio’ Excel report sheet added
0.41.0 (2025-06-17)#
Added#
Portfolio weights evolution plot
Custom market data column names support
Changed#
Market data builder updated to support custom column names
Fixed#
Annualized return calculation
CAGR calculation
Alpha metric calculation
0.40.1 (2025-06-04)#
Fixed#
OS Error on cli
init scriptbecause of entry script template missing from wheel data
0.40.0 (2025-06-04)#
Changed#
First public release, now on PyPI