Zipfolio
Backtest tearsheets that read the book, not just the returns.
Zipfolio turns a backtest into one self-contained HTML report about how the strategy behaves position by position: what it held and how concentrated it was, what each instrument and sector earned, which factors it was really exposed to, where its risk sat, how its orders were filled -- and how far those fills can be believed -- and what trading costs took. It is part of the ZipLime ecosystem, where it is built for every backtest next to the QuantStats returns tearsheet.
from zipfolio import build_tearsheet
build_tearsheet("report.html", returns, benchmark,
position_records=positions, cash=ending_cash, transactions=fills,
orders=orders, fill_bars=fill_bars, decision_prices=decisions,
market_data=daily_bars, cost_settings=settings)
Why a backtester-aware report
A returns tearsheet -- QuantStats, or pyfolio fed only a return series -- sees one number per day. That is enough for Sharpe ratios and drawdowns, and nothing else: it cannot tell you whether a fill was possible, what slippage cost, or which position carried the risk, because none of that is in the returns.
A backtester knows all of it while it runs. Every order has a type, a creation time, a limit or a stop; every fill belongs to an order and happens on a bar with an open, high, low, close and volume; every cost comes from a model with known settings. Most of this is thrown away once the simulation ends. Zipfolio is built to be fed by the backtester itself, while that detail still exists, and that is what makes its metrics possible:
| Question the report answers | What it needs | Where a ziplime run has it |
|---|---|---|
| Was each fill priced inside the bar it happened on? | the fill's own bar (OHLC) | the market-data bundle, joined as of the fill time |
| Did a fill take more of the bar's volume than could trade? | the fill's bar volume | the same bundle |
| Did the strategy trade at a price it had only just seen? | execution timing settings | same_bar_execution, price_used_in_order_execution |
| What did a fill cost over the price the strategy decided at? | the close at the order's creation | the order's created time, joined to the bundle |
| How long did orders wait; were any left unfilled, and why? | order lifecycle | orders: created, dt, filled, status, reason |
| Which order types did the strategy use? | execution style | MarketOrder / LimitOrder / StopOrder / StopLimitOrder |
| What did commission cost? | commission per order | order.commission (ziplime charges it to the order, not the fill) |
| What did slippage cost? | the slippage model | FixedBasisPointsSlippage: reconstructed exactly from the fill prices |
| What did each instrument earn? | positions and fills per day | position snapshots, fills with prices and quantities |
| How much capital could the strategy take? | daily volume per traded name | the bundle, folded into sessions |
| How is the cost model set, in words? | the settings themselves | PerShare / PerDollar, minimum per order, slippage bps, volume limit |
The result reads like an execution and risk desk's review of the backtest, not a restatement of its equity curve.
What is in a report
Charts on the left and narrow tables on the right in a 960px column, in the
QuantStats Pro look (white ground, light grid, the same palette: strategy
#6B9E00, benchmark #5C5C5C), so the two reports read as one product.
Every chart is SVG; the page has no external requests. The header and footer
name the Zipfolio version that built the page and link back to this repository.
The default title is "Zipfolio Tearsheet: Portfolio, Execution & Cost Analysis"
(title= changes it).
| Section | Shown when | Contents |
|---|---|---|
| Performance | always | key metrics, cumulative returns, underwater curve -- deliberately short; returns are QuantStats' job |
| Positions | the strategy traded | exposure, top positions, long/short concentration, holdings, gross leverage |
| Instruments | traded | each name's PnL, contribution, weight, days held, trade stats; contribution by year; daily and round-trip return distributions per name; cumulative PnL by name |
| Sector Analysis | a held name has a sector | allocation over time, average and peak, profit attribution by GICS sector |
| Factor Analysis | a factor model is set | Fama-French 5 + momentum betas with Newey-West t-stats, alpha, fit, attribution (overall and by year), rolling betas, return decomposition |
| Portfolio Risk | always (more when traded) | VaR/CVaR/EVaR, drawdown-at-risk/CDaR/EDaR, Ulcer index, Gini mean difference; rolling tail risk; risk contribution per holding; clustered correlations; the same names under four allocation rules, walk-forward |
| Transactions | traded | trading frequency, fill sizes, buys and sells, turnover, volume, fill times on the market's own clock |
| Execution | orders are given | how the backtest fills orders, realism checks, orders by type, fill statistics, fill price within the bar and against its typical price, share of bar volume, implementation shortfall, waits |
| Trading Costs | traded | the cost model in plain words, cost summary and per symbol, costs over time, returns before and after costs, sensitivity to extra slippage |
| Round Trips | 5+ round trips | summary, PnL, duration and return statistics, per symbol, lifetimes, distributions |
| Capacity | daily bars are given | days to liquidate, fills over 5% of daily volume, Sharpe across capital bases |
A section that cannot be computed becomes a short note, a chart that fails is left out, and both are logged; the page fails only if nothing at all can be reported.
Zipfolio and pyfolio
Zipfolio stands on pyfolio-reloaded and owes it most of its analytics. What it takes, what it changes, and what it adds:
Uses pyfolio for the computation. Performance statistics, drawdown tables,
exposures and concentration, top positions, turnover, round-trip extraction and
statistics, profit attribution, sector exposure, liquidity and the capacity
sweep, slippage sensitivity -- all are pyfolio's functions (timeseries,
pos, txn, round_trips, capacity, plotting.plot_*).
Replaces the presentation. pyfolio is written for a notebook: tables go to
IPython's display, and each tear sheet is one tall seaborn figure of a dozen
panels. Zipfolio does not call the create_*_tear_sheet functions. It calls the
individual plotting.plot_* functions, one chart per figure on an axis it
owns, restyles each (palette, grid, percent axes, legends) and renders it to
SVG; it catches every table at utils.print_table as a frame and renders it
itself, with formatting pyfolio applies one-size-fits-all repaired (counts as
146, money as -$5,067.13, three-decimal percentages as two).
Fixes, locally. pyfolio-reloaded 0.9.9 is used as released; where it misbehaves, Zipfolio works around it for the one call that needs it, never by patching pyfolio globally:
- the capacity analysis fails whenever two names trade on the same day
(
daily_txns_with_bar_datacallsasfreqon duplicate dates) -- a repaired copy is swapped in for that call; - the transaction-time chart is fixed to New York's 09:30-16:00 -- Zipfolio draws its own, on the market's clock and over the hours fills actually fall in;
- the exposure panel disappears when pandas draws a regular-frequency index in period ordinals -- charts are drawn with matplotlib dates;
- labels and heatmap numbers that collide on dense axes are thinned and fitted after drawing.
Leaves out what QuantStats already shows. Rolling statistics, return distributions, the monthly heatmap and stress events are pyfolio's too, but a ZipLime backtest already has them in its QuantStats tearsheet.
Adds what pyfolio does not have. The Instruments, Execution, Trading Costs and Factor Analysis sections, the Portfolio Risk section (computed with skfolio), sector analysis from a GICS table you supply, and the cost model described in words.
Zipfolio and ziplime
Zipfolio is the reporting layer of ZipLime: it is designed around what a
ziplime 1.20 run produces, and the zipfolio.ziplime module reads the parts a
return series loses.
What it relies on from ziplime:
- Orders as objects --
execution_style(market, limit, stop, stop-limit),createdanddt,amountandfilled,limitandstop,statusandreason, andcommission, which ziplime charges to the order rather than the fill. - Fills with their order -- each transaction carries its
order_id,amount,priceand simulation time. - The market-data bundle -- a polars frame of OHLCV bars per instrument,
daily or intraday, that the simulation read;
zipfolio.ziplime.execution_barsjoins every fill and every order to it as of the simulation time, anddaily_barsfolds it into sessions. - The cost and execution models --
PerShare/PerDollarcommission with a per-order minimum,FixedBasisPointsSlippagewith a volume limit,same_bar_executionandprice_used_in_order_execution. Zipfolio describes them in words and checks every fill against them. - Positions and cash per bar -- position snapshots taken during
run_simulation_iter(ziplime mutates position objects later, so they are copied as they are emitted) andending_cashfrom the performance frame.
from zipfolio.ziplime import daily_bars, execution_bars, order_type
# while the run's bundle is still loaded:
bars = daily_bars(bundle) # symbol, day, price, volume
found = execution_bars(bundle, fills, orders) # fills: id, symbol, dt; orders: id, symbol, created
fill_bars, decisions = found["fill_bars"], found["decision_prices"]
kind = order_type(order) # "market", "limit", "stop", "stop_limit"
The ZipLime backtester calls exactly this after every simulation and passes the
frames to build_tearsheet. Nothing in the report requires ziplime itself:
the inputs are plain pandas frames, and any backtester that can produce them
gets the same report.
Installation
pip install zipfolio
With the optional parts:
pip install "zipfolio[all]" # everything
pip install "zipfolio[risk]" # + skfolio, for the Portfolio Risk section
pip install "zipfolio[ziplime]" # + polars, for zipfolio.ziplime
Without skfolio the Portfolio Risk section is left out; nothing else changes. Requires Python 3.10+.
Until the first release reaches PyPI, or for the latest unreleased code, install from GitHub:
pip install "zipfolio[all] @ git+https://github.com/Limex-com/zipfolio"
Releases
Zipfolio is published on GitHub first and then on PyPI, under the same
version: a version is tagged here (v0.1.0), and the wheel built from that tag
is uploaded to PyPI as zipfolio. Every report says which version built it --
in the header ("Generated by ZipLime Zipfolio v0.1.0") and the footer, both
linking back to this repository -- so a report can always be traced to the code
that made it. Changes are listed in CHANGELOG.md.
To publish a release:
poetry version 0.1.1 # or: patch / minor / major
git commit -am "Zipfolio 0.1.1" && git tag v0.1.1 && git push --tags
poetry build && poetry publish # PyPI token configured with `poetry config pypi-token.pypi ...`
Inputs
All frames are plain pandas. Only returns is required; each input switches
on the sections that need it.
| Argument | Shape | Used by |
|---|---|---|
returns |
per-period strategy returns, datetime index | everything |
benchmark |
benchmark returns, same index, or None |
Performance, Portfolio Risk |
position_records |
rows of date, symbol, quantity, last_sale_price per open position per day |
Positions, Instruments, Sectors, Risk, Round Trips |
cash |
cash at the end of each period | (same) |
transactions |
rows of id, order_id, dt, symbol, quantity, price |
Instruments, Transactions, Execution, Costs, Round Trips |
orders |
rows of id, created, dt, type, symbol, quantity, filled, commission, limit, stop, status, reason |
Execution, Trading Costs, Instruments |
fill_bars |
open, high, low, close, volume indexed by fill id |
Execution |
decision_prices |
decision_price indexed by order id |
Execution (shortfall) |
market_data |
rows of symbol, day, price, volume |
Capacity, Portfolio Risk |
cost_settings |
commission_model, commission_per_share, commission_rate, commission_min_trade_cost, slippage_basis_points, slippage_volume_limit, price_used_in_order_execution, same_bar_execution, capital |
Trading Costs, Execution |
sector_mappings |
{symbol: sector}; us_sector_mappings(["AAPL@XNGS", ...], table=...) gives GICS sectors for US listings from your table |
Sector Analysis |
factor_model |
"ff5_mom" (default), "ff5", "ff3", "ff3_mom" or None |
Factor Analysis |
factor_data |
your own factor returns (decimals, with RF), instead of the download |
Factor Analysis |
title |
the page title; defaults to zipfolio.DEFAULT_TITLE |
header |
market_timezone |
e.g. "Europe/Moscow"; defaults to ZIPFOLIO_MARKET_TIMEZONE, then New York |
fill-time chart |
build_tearsheet returns the titles of the sections that made it into the page.
Data the package fetches or ships
- Fama-French factors are downloaded from Kenneth French's
data library
-- the same files
pandas_datareader.famafrenchreads, without the dependency -- and cached for a week under~/.cache/zipfolio(ZIPFOLIO_CACHE_DIR). A failed download falls back to a stale cache, then to a note in the report; the timeout is 8 seconds. The library publishes monthly with a lag, so the last weeks of a fresh backtest may fall outside it. The factors describe US stocks: passfactor_model=Nonefor other markets. - GICS sectors are not shipped. The classification is licensed by MSCI
and S&P, so the codes are yours to bring: a CSV of
symbol,gics_sector(two-digit sector code) passed asus_sector_mappings(listings, table=...)or found throughZIPFOLIO_GICS_TABLE.scripts/build_us_sector_map.pybuilds one from exports that carry GICS codes, one row per ticker from its most recent classification.us_sector_mappingslooks up only listings on US venues, so a MoscowTis not filed as AT&T; a reused ticker gets its current holder's sector. Or skip the table and passsector_mappingsyourself -- any{symbol: sector}works, GICS or not.
Limitations
- Futures exposure is understated: position values are quantity x price, and the records carry no contract multiplier.
- Slippage is reconstructed for the fixed-basis-point model; other slippage models would need their own reconstruction.
- Instrument contribution adds daily PnL over the start-of-day portfolio value, so dollars and percent can disagree in sign for a name that earned early and gave back later -- the report says so where it shows them.
- Factor analysis needs at least 60 sessions inside the factor data.
Development
pip install -e ".[all]" pytest # or: poetry install --all-extras
pytest
Tests never touch the network: factor downloads are replaced by synthetic factors. Release notes are in CHANGELOG.md.
License
Apache License 2.0 -- see LICENSE. Zipfolio builds on pyfolio-reloaded (Apache-2.0) and skfolio (BSD-3-Clause). No market or classification data is distributed with it.
Metadata
Release files for zipfolio 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| zipfolio-0.1.0.tar.gz | 70.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| zipfolio-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 137.0 kB
Release files / zipfolio-0.1.0.tar.gz
| Download URL | zipfolio-0.1.0.tar.gz |
|---|---|
| Size | 70.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
daf2c1a1218ccd088fedeea9378da14f90d3689ce273622784bd2376684af798
|
|
BLAKE2b-256 checksum How to use checksums |
1ebdb2e9e20f53303e198383fc6c0f25aabfcac83886d8740b0318958c2281e0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|
Release files / zipfolio-0.1.0-py3-none-any.whl
| Download URL | zipfolio-0.1.0-py3-none-any.whl |
|---|---|
| Size | 66.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
36df2a954259e66fb60342629a870498403c6b3a69fa7d60027799e8789e867e
|
|
BLAKE2b-256 checksum How to use checksums |
39444e20e1f93eebcdbeeb43dc2765de853fc8465fce571a23bf9ad2d76d7e36
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|