Skip to main content

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_data calls asfreq on 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), created and dt, amount and filled, limit and stop, status and reason, and commission, which ziplime charges to the order rather than the fill.
  • Fills with their order -- each transaction carries its order_id, amount, price and simulation time.
  • The market-data bundle -- a polars frame of OHLCV bars per instrument, daily or intraday, that the simulation read; zipfolio.ziplime.execution_bars joins every fill and every order to it as of the simulation time, and daily_bars folds it into sessions.
  • The cost and execution models -- PerShare / PerDollar commission with a per-order minimum, FixedBasisPointsSlippage with a volume limit, same_bar_execution and price_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) and ending_cash from 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.famafrench reads, 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: pass factor_model=None for 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 as us_sector_mappings(listings, table=...) or found through ZIPFOLIO_GICS_TABLE. scripts/build_us_sector_map.py builds one from exports that carry GICS codes, one row per ticker from its most recent classification. us_sector_mappings looks up only listings on US venues, so a Moscow T is not filed as AT&T; a reused ticker gets its current holder's sector. Or skip the table and pass sector_mappings yourself -- 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)

Source distribution for zipfolio 0.1.0
File Size Uploaded
zipfolio-0.1.0.tar.gz 70.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for zipfolio 0.1.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

0.3.0

2 release files

0.2.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page