Skip to main content

nakagai

The deterministic, LLM-free core for rule-driven trading agents: a point-in-time bar cache, a statistically honest portfolio replay (one shared cash account, look-ahead prevention, T+1 cash settlement), the RuleSpec strategy DSL, and a screener compiler.

What is here

  • data/: BarCache/MemoryBars over local parquet, the DataProvider contract and its Alpaca implementation (single-symbol and batched multi-symbol), and a sync routine that keeps the cache current.
  • engine/: the portfolio replay itself. run_portfolio(request, bars, registry, schedule) is the only entry point: it replays one account across every selected play and symbol in one causal chronology, and returns one canonical result with the trades, the structured rejections, the account equity curve, an independently calculated benchmark, per play-symbol attribution, and the metrics. It is deterministic and side-effect free, so it reads no cache, writes no file, mints no identifier, and consults no installed calendar.
  • strategies/: rule-based (rules/), boolean-composed (composite/), and ICT-flavored (ict/) strategies, plus catalog_definitions, which turns a directory of JSON specs into frozen StrategyDefinition values a registry bundle takes.
  • screen/: a conditions-only screener over the same RuleSpec grammar. Evaluation is deterministic and LLM-free; an optional English-to-spec compiler shares the nlbuilder extra with nlbuilder/, which installs anthropic.
  • nlbuilder/: English-to-RuleSpec compilation via the Claude API, behind the optional nlbuilder extra (installs anthropic).
  • stats.py: poolable return moments and the deflated-Sharpe family (PSR, DSR, minimum track record length, effective trial count), which is how a candidate is priced for how many candidates were tried.
  • filelock.py: cross-process advisory file locking for concurrent read-modify-write on shared result files.

Quickstart

This builds one schedule, one frame of bars, one frozen registry, and one request, then replays them through the single public entry point and prints the result's metrics. No network, no credentials, no optional extras, and it prints the same line every time: the whole contract is that a replay reads its four arguments and nothing else.

It is longer than a one-liner on purpose. Core does not discover strategies, resolve a cache, or decide what a trading session is; a caller states all of it, which is what makes two runs of the same request byte-identical wherever they run.

import dataclasses
from datetime import date, timedelta

import numpy as np
import pandas as pd

from nakagai.engine import (
    ARITHMETIC_VERSION,
    AccountPolicy,
    BenchmarkSpec,
    ExchangeScheduleIdentity,
    ExecutionPolicy,
    FeeSpec,
    FrozenStrategyRegistry,
    PlayRequest,
    PortfolioBars,
    PortfolioReplayRequest,
    ReplaySchedule,
    ReplayWindow,
    ScheduledBaseInterval,
    SlippageSpec,
    definition_digest,
    expected_candidate_id,
    expected_replay_id,
    rules_definition,
    run_portfolio,
    schedule_digest,
    spec_base_digest,
)

SESSIONS, PER_SESSION = 8, 26          # eight regular sessions of 15-minute bars
WARMUP = 2 * PER_SESSION               # the first two are warmup, the rest is tested

# 1. The schedule IS the clock. Core never consults an installed calendar: it
#    replays exactly the intervals it is handed, so early closes and holidays
#    enter as data. These eight weekdays open at 14:30Z.
intervals, day = [], date(2026, 1, 5)
while len({row.session_date for row in intervals}) < SESSIONS or not intervals:
    if day.weekday() < 5:
        opens = pd.Timestamp(f"{day}T14:30:00Z")
        intervals += [
            ScheduledBaseInterval(
                session_date=day, interval_ordinal=n,
                open_ts=opens + pd.Timedelta(minutes=15 * n),
                close_ts=opens + pd.Timedelta(minutes=15 * (n + 1)))
            for n in range(PER_SESSION)]
    day += timedelta(days=1)
draft = ReplaySchedule(
    identity=ExchangeScheduleIdentity(
        calendar_id="XNYS", calendar_version="exchange_calendars:4.5.6:nakagai-rth-v1",
        schedule_digest="0" * 64, timezone="America/New_York", base_timeframe="15m"),
    base_intervals=tuple(intervals), context_bars=())
schedule = dataclasses.replace(draft, identity=dataclasses.replace(
    draft.identity, schedule_digest=schedule_digest(draft)))

# 2. One frame per (symbol, timeframe), labeled at the scheduled opens. Swap
#    this block for real bars once you have them; nothing below changes.
rng = np.random.default_rng(0)
index = pd.DatetimeIndex([row.open_ts for row in schedule.base_intervals], name="ts")
close = pd.Series(400 * np.exp(np.cumsum(rng.normal(0, 0.004, len(index)))), index=index)
prev = close.shift(1).bfill()
bars = PortfolioBars({("SPY", "15m"): pd.DataFrame({
    "open": prev, "high": np.maximum(close, prev) * 1.002,
    "low": np.minimum(close, prev) * 0.998, "close": close, "volume": 1_000_000.0,
}, index=index)})

# 3. A registry is a frozen bundle of definitions. `rules_definition` builds one
#    over a RuleSpec; the base digest covers the spec and the grammar it is read
#    under, because one spec under two grammars is two strategies.
spec = {
    "version": 2, "name": "sma_cross", "timeframe": "15m",
    "long": {"all": [{"lhs": {"ind": "sma", "n": 10}, "op": "crosses_above",
                      "rhs": {"ind": "sma", "n": 30}}]},
    "risk": {"stop": {"kind": "atr", "n": 14, "mult": 2.0},
             "target": {"kind": "rr", "rr": 2.0}},
}
base_digest = spec_base_digest(spec)
registry = FrozenStrategyRegistry.from_definitions(
    (rules_definition("sma_cross", base_digest, spec=spec),))

# 4. The request names the plays, the symbols, and the whole visible policy.
#    Every play carries the digest binding its definition to its own params, and
#    core refuses the replay if it does not recompute.
params: dict = {}
window = ReplayWindow(
    train_start=intervals[0].open_ts, train_end=intervals[WARMUP].open_ts,
    test_start=intervals[WARMUP].open_ts, test_end=intervals[-1].close_ts)
draft = PortfolioReplayRequest(
    request_version=1,
    replay_id="replay:" + "0" * 64, candidate_id="candidate:" + "0" * 64,
    batch_id="0198b1c2-3d4e-7f80-8123-456789abcdef",
    registry_digest=registry.registry_digest,
    plays=(PlayRequest(play_id="play-1", strategy="sma_cross",
                       definition_digest=definition_digest(base_digest, params),
                       params=params, priority=100),),
    symbols=("SPY",), window=window, schedule_identity=schedule.identity,
    ic_horizons=(1, 5, 20), ic_tail_end=window.test_end,
    account=AccountPolicy(starting_equity=10_000.0, risk_pct=0.01,
                          max_open_positions=5, max_positions_per_play_symbol=1,
                          settlement_model="cash_t1"),
    execution=ExecutionPolicy(
        arithmetic_version=ARITHMETIC_VERSION, fill_mode="pessimistic",
        slippage=SlippageSpec(bps=1.0, min_per_share=0.01),
        fees=FeeSpec(per_fill=0.0, per_share=0.0),
        funding_order="play_priority_symbol_signal", missing_bar_policy="strict"),
    benchmark=BenchmarkSpec(kind="equal_weight_request_symbols", symbol=None,
                            weighting="equal", rebalance="never"))
named = dataclasses.replace(draft, candidate_id=expected_candidate_id(draft))
request = dataclasses.replace(named, replay_id=expected_replay_id(named))

# 5. One call, one canonical result.
result = run_portfolio(request, bars, registry, schedule)
metrics = result.metrics
print(f"trades: {metrics.all_trades.n_trades}, "
      f"rejections: {metrics.n_rejections}, "
      f"total_return: {metrics.total_return:.2%}, "
      f"benchmark: {metrics.benchmark_return:.2%}")
print(f"digest: {result.result_digest}")

Because the series is seeded and the arithmetic is canonical, this prints the same line on every machine, which makes it a usable smoke test as well as an example:

trades: 3, rejections: 0, total_return: -2.89%, benchmark: -2.18%

A trend follower run on a random walk is not supposed to make money, and it doesn't. That is the example working, not failing: the replay's job is to tell you that honestly. Point step 2 at real bars to see something worth judging.

Two details matter if you change it. Position size is risk_pct of frozen equity divided by the protective distance, floored to whole shares, so a series with a low price-to-volatility ratio asks for more shares than the account can buy and every entry is refused for cash, which shows up as structured rejections rather than as a silent zero-trade run. And every declared frame must carry exactly the labels the schedule declares, with no gaps and nothing past the boundary: a missing scheduled bar refuses the whole replay rather than shrinking it.

The RuleSpec DSL

A RuleSpec is plain JSON: an entry condition tree for long and short, and a risk block for the stop and target. Conditions compare an indicator or price source against another indicator or a constant, with operators like crosses_above and crosses_below; all/any groups combine them into arbitrarily nested boolean trees. nakagai.strategies.rules.validate_spec is the single source of truth for the grammar, so a spec that loads has already been checked. Here is the shipped sma_cross.json example, abridged to the DSL fields (catalog card metadata like category and tags omitted):

{
  "title": "Moving average crossover",
  "description": "The classic trend follower: long when the fast SMA crosses above the slow SMA on the 1h chart, short on the cross down. ATR-sized stop, fixed reward:risk target.",
  "spec": {
    "version": 2,
    "name": "sma_cross",
    "timeframe": "1h",
    "long": {"all": [
      {"lhs": {"ind": "sma", "n": 20}, "op": "crosses_above", "rhs": {"ind": "sma", "n": 50}}
    ]},
    "short": {"all": [
      {"lhs": {"ind": "sma", "n": 20}, "op": "crosses_below", "rhs": {"ind": "sma", "n": 50}}
    ]},
    "risk": {"stop": {"kind": "atr", "n": 14, "mult": 2.0}, "target": {"kind": "rr", "rr": 2.0}}
  }
}

Two more examples ship in nakagai/strategies/catalog/specs/: rsi_reversion.json (mean reversion) and macd_trend.json (momentum). catalog_definitions(specs_dir, core_vocabulary) turns every JSON file in a directory like this one into a frozen StrategyDefinition ready to enter a registry.

What is NOT here

This repo does not include the curated Playbook content (the hand-authored strategy specs), the evidence store and proving pipeline, the intraday scanner, or the hosted platform: API, web UI, and the mandate and approvals judgment layer. The hosted product at nakag.ai is built on top of this core.

Release notes

0.5.0

An intentional pre-1.0 breaking release. Every replay entry point 0.4.x offered is gone, the strategy contract is strict, the result is one canonical value, and the arithmetic is stamped 2. A consumer migrates before pinning this version: there is no release in which both contracts work, and the hosted platform's cutover is a migration rather than a version bump.

Breaking: one public replay, and the singleton engine is gone. run_portfolio(request, bars, registry, schedule) replaces Engine, run_one, and run_grid. It replays ONE cash account across every selected play and symbol in one causal chronology, so two candidates that were each affordable alone now contend for the same settled cash and the same position capacity, and the result carries a real account equity curve instead of per-symbol rows combined after the fact.

Removed with them: nakagai.engine.engine, nakagai.engine.runner, nakagai.engine.provenance, nakagai.engine.costs, nakagai.icir, BacktestResult, the singleton Trade, summarize, buy_and_hold_return, process-grid expansion, core-owned result parquet, FeeModel, SlippageModel, PreloadedBars, FrozenStrategyRegistry.definitions, and CompositeStrategy.bound. There is no adapter and no alias: a caller migrates to the new contract or stays on 0.4.x.

FeeSpec and SlippageSpec now price a fill themselves, so the request's own policy is the model. Composite membership arrives one way, as member factories passed to CompositeStrategy(...). nakagai.engine exports the complete contract and nothing else.

Breaking: FeeSpec.per_fill replaces FeeModel.per_trade, and charge(qty) prices exactly one fill. The old model returned 2 * (per_trade + per_share * qty) from a single call, on the assumption that a fee is priced once per round trip. The portfolio replay charges the entry and the exit separately, as each one happens, so the field is named for what it prices and the method returns per_fill + per_share * qty. This changes fee arithmetic for any caller that carried a non-zero per_trade: the same number passed as per_fill leaves the round-trip total unchanged, while reading the old round-trip total as one fill halves it. Zero stays zero, which is what the broker this core was built against charges.

Breaking: a strategy proposes values and never touches engine state. Signal moves out of nakagai.strategies.base into nakagai.engine.portfolio_types, which owns the whole canonical contract, and nakagai.engine exports it. It loses entry and gains entry_ref, the deciding raw close its protective levels were bracketed against; a signal whose reference is not that close is refused, so a play can no longer name a price the replay did not decide on. Strategy.on_bar returns a Sequence[Signal] and every element of it is a proposal: the singleton engine took signals[0] and dropped the rest silently, while the order now carries into replay-wide signal ordinals. Strategy.manage returns an immutable ManagementDecision in place of the removed PositionAction enum, and the position it is handed is a frozen PositionView, so a ratcheted stop or a replaced target travels back as a value and assigning to the position raises. Every return is checked at the boundary against a closed error taxonomy, and none of those errors becomes an empty signal list: a strategy that refused, a strategy that returned something invalid, and a strategy that saw nothing are three different observations, and a replay that cannot tell them apart reports contention it never had.

Canonical transport is core's, and only core's. canonical_replay_bytes is the one hashing encoding. Object keys sort lexically, a finite binary64 value travels as its exact float.hex() inside a tagged object, a date travels tagged, and a timestamp has one UTC spelling, so identical inputs produce identical bytes no matter what order a mapping was built in or how a runtime renders a decimal. Local, remote, and hosted agree byte for byte or they disagree loudly. result_digest is taken over those bytes, and every identifier formula lives beside it: expected_candidate_id, expected_replay_id, schedule_digest, definition_digest, spec_base_digest, trade_id, rejection_id. A caller recomputes rather than reimplementing, and core refuses a request whose declared identifiers do not recompute. encode_replay_* and decode_replay_* are a separate ordinary-JSON wire form for an API, a worker envelope, or a database column; a receiver recomputes the canonical bytes from the decoded values rather than trusting the transport text.

Arithmetic version 2, and one result carries the whole reading. ARITHMETIC_VERSION is "2", and the chronology, the cost model, and the metric formulas are one arithmetic under it. A request declaring another version is refused rather than reported under a label that does not describe how its numbers were reached, so nothing stamped 1 is comparable to anything stamped 2. PortfolioReplayResult carries the trades, the structured rejections, the account equity curve, an independently calculated benchmark, the portfolio metrics, and one PortfolioSlice per play symbol holding that pair's own counts, gross and net sums, fees, win rate, expectancy, and an IcEstimate at each of the three horizons. The IC estimate is an in-sample diagnostic over the window that was replayed, not a forecast, and observations is its load-bearing field: a lens that never ran and a lens that ran and found nothing both report a null coefficient, and only the count separates them.

Breaking: one catalog door, and a grammar is a value. load_catalog is removed, with RuleStrategy.bound and the RuleStrategy.VOCABULARY_FACTORY class attribute it was the only caller of. catalog_definitions(specs_dir, vocabulary_factory) replaces it: a definition carries the name, binds the immutable spec, records the grammar it is read under, and builds a plain RuleStrategy fresh per candidate, so a catalog entry can no longer exist as two minted classes in one process. load_entries is unchanged.

Two digests, and the names say which is which. spec_base_digest(spec, vocabulary_factory) covers a strategy body and the grammar it is read under, because one spec under two grammars is two strategies. definition_digest(base_digest, params) binds that base to one play's own params. They sit one keystroke apart and mean different things: a registry freezes bases, a request declares definitions, and core refuses a play whose declared definition digest does not recompute. Both names are new in this release; 0.4.x had no digest of either kind.

A definition's grammar now reaches the replay. StrategyDefinition carries vocabulary_factory, and the context a runtime decides through is built from it. Before this, entries were always evaluated under the core grammar while the IC lens graded the definition's own, so a play using an added term aborted and a play using a redefined one was graded on a factor that did not produce its trades. Nothing announced it: vocabulary_digest covers what a term declares, not what it computes. Replays under the core grammar are byte-identical.

Breaking: pandas>=3 is the declared floor, raised from >=2.2. The floor is load-bearing rather than tidy. Copy-on-write is opt-in in pandas 2.x and unconditional from 3.0, and build_scheduled_context hands a strategy zero-copy prefixes of engine-owned frames. Under 2.x without copy-on-write, a strategy writing into ctx.bars[tf] would write through into the replay's own prices, and nothing would report it. Lowering this floor reintroduces that.

New: a packaged per-term causality gate. nakagai.strategies.rules.verify adds verify_term(term, bars) and verify_vocabulary(vocabulary, bars), which ask of one term whether it reads only rows at or before the row it answers for, by comparing a whole-frame computation against the prefix computation at probe rows across the term's own mandated argument sets. The answer is a TermVerdict carrying CHECKED, FAILED, EXEMPT or VACUOUS, plus a machine-readable cause on a failure, rather than a bare boolean: an end-anchored term returns a scalar with no whole-frame series to index, and a condition-taking term cannot be called without an evaluator, so under a boolean both would be indistinguishable from a genuine pass. tests/test_whole_frame_equivalence.py pins causality for the grammar from a hand-maintained list; this pins it for one term from the term's own schema, which is what lets a term nobody wrote by hand be admitted or refused. reference_bars() ships the frame the gate is meant to run on inside the wheel rather than in tests/, because the consumer reaching this gate lives in another repository and cannot get a test fixture. It merged to main after 0.4.2 without a version bump, so 0.5.0 is the first release to carry it.

0.4.2

  • Stamp every new replay run with arithmetic version 1 and fill mode pessimistic. These durable identities distinguish result semantics from package releases and source revisions. Existing replay arithmetic and trade output are unchanged.

0.4.1

  • Canonicalize daily cache rows to midnight New York by UTC session date, so mixed provider labels cannot retain duplicate rows for one market session.

0.4.0

Breaking: nakagai.stats.pf_from_trades and PF_CLAMP are removed. They computed a pooled profit factor over a trade ledger. The lab was their only caller, and with the lab gone in 0.3.0 nothing reached them: not core, not the hosted platform, which derives profit factor from its own gross sums. The module no longer imports pandas.

Breaking: run_one loses its icir keyword. It opted a caller out of the ICIR lens, and it had exactly two callers, the permutation harness and the frontier open-window snapshots. Both were retired, so the flag has been dead in production for some time and only a test still set it. The lens itself is untouched and still runs for rule specs, still abstains to empty fields for everything else, and still degrades to empty rather than killing a run row.

Breaking: Engine.slippage_for is removed. A one-line accessor over SlippageModel.per_share, added so callers could ask the engine what it would charge without reaching into the model. No caller ever did. Its only reference was a test asserting the method exists, which is a test that cannot fail for any reason worth catching, so it went too.

This release is also the first to carry everything merged since 0.3.0, which shipped without a version bump: the deflated-Sharpe family, the injected vocabulary reaching composites, and the session-open fix.

0.3.0

Breaking: nakagai.lab is removed. The module searched strategy space and scored the winner against a best-of-N permutation null. It shipped in 0.2.0 with one consumer, the hosted platform's study subsystem, and that subsystem was retired; nothing has imported the lab since. Gone with it: Site, Trial, composite_trials, literal_trials, mutable_sites, spec_hash, best_of_n_null, study_verdict, StudyResult, StudySpec, TrialResult, run_study, trial_pf, and the Calibration workflow that gated them.

Note that spec_hash also exists, unrelated and unaffected, at nakagai.strategies.rules.canon.spec_hash. Only the lab's is gone.

Breaking: the bar-permutation Monte Carlo null is removed with it. Gone: nakagai.engine.permutation entirely (permute_bars, permutation_seed) and nakagai.stats.permutation_pvalue. These were the two halves of one feature, generating null price series and scoring an observation against them, and the lab was the only thing that ever called either. Permutation testing is no longer part of what this core does.

nakagai.stats keeps pf_from_trades and PF_CLAMP, and its module docstring no longer describes it as permutation-test math.

The question the lab answered, "is this survivor real or did I just search hard enough to find noise", is not being abandoned; it is moving to the deflated-Sharpe family, which prices the same overfitting risk from the trial count directly rather than by replaying the search on permuted bars.

0.2.0

Behavior change: every session-scoped term is anchored on the 09:30 bell and scoped to regular hours. Backtest output moves for any play reading opening_range_high, opening_range_low, minutes_into_session, prev_session_high, prev_session_low, prev_session_close, gap_pct or vwap; re-run anything that depends on them. The bar caches are not regular-hours-only, and these all grouped a New York calendar date and treated its first row as the session's start, which is ordinarily an 08:00 pre-market print. So the opening range was a thin band nobody trades, minutes_into_session ran an hour and a half fast, the previous session's high and low were off-hours extremes and its "close" was the last post-market print, a gap was measured from 19:45 to 08:00, and session VWAP was set by pre-market volume. A session now runs [09:30, 16:00) on the exchange wall clock, from nakagai/data/schema.py, and a bar before the bell reads NaN rather than a value a condition would act on. A daily frame is unaffected: one row is its own whole session.

Behavior change: day_of_week reads the weekday off the FRAME, not off a label's clock. Backtest output moves for any play using day_of_week on an intraday frame; re-run anything that depends on it. The old predicate decided which clock to read by looking for a midnight-UTC label, and the bar caches are not regular-hours-only, so a 19:00 New York post-market bar carries exactly the label a resampled daily bar carries and was read as the next day: Tuesday, for a Monday evening. It answered wrong on one bar of a session and right on all the others, which is the shape of divergence a spec author never catches. The weekday is now the frame's to decide, per strategies/rules/primitives.py.

New: a Pine v6 compiler for RuleSpec v2. compile_pine(spec, vocabulary) returns an indicator and a strategy, rendered from one lowering so the pair cannot disagree about which bar decided; lower_pine returns the target-neutral program underneath. Both are exported from nakagai.strategies.rules, alongside PineBundle and PineCompileError. Every export charts the engine's 15-minute driving cadence and requests a play's own timeframe rather than charting it, so the script refuses any other chart at runtime, and it requires extended trading hours for the same reason the engine's own frames carry pre-market bars.

Breaking: the catalog loaders require a vocabulary factory. load_catalog(specs_dir) becomes load_catalog(specs_dir, core_vocabulary), and the same for load_entries. Both are cached on their whole argument tuple, so a defaulted call and an explicit one built two different strategy classes over the same spec files, with isinstance quietly disagreeing and nothing raising.

Development

uv sync --all-extras
uv run pytest

uv sync --all-extras pulls in anthropic so the nlbuilder tests run too; the rest of the package works fine without it.

License

MIT

Release files for nakagai 0.5.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 nakagai 0.5.0
File Size Uploaded
nakagai-0.5.0.tar.gz 536.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nakagai 0.5.0
File Interpreter ABI Platform
nakagai-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 796.2 kB

Release files / nakagai-0.5.0.tar.gz

Download URL nakagai-0.5.0.tar.gz
Size 536.2 kB
Tags Source
SHA-256 checksum
How to use checksums
bbe5390f30f7219be19819e16c0fe1d5ee24129a092ab7eb6232bdbe2cb4c538
BLAKE2b-256 checksum
How to use checksums
25e71521173f94df371a759821dea3a0de35d234932fd1a05890857b9e45ccf2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 11, 2026.

Transparency log

Release files / nakagai-0.5.0-py3-none-any.whl

Download URL nakagai-0.5.0-py3-none-any.whl
Size 260.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f98b6c4976800964f1bb4c7aec5ffdd2579111469c621fd397d4680f9c768fe6
BLAKE2b-256 checksum
How to use checksums
85122d19aee4dcde76775fd7205dc8365756f95be1aa7d10f3c182a27dfa6432
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 11, 2026.

Transparency log

Release history Release notifications | RSS feed

0.10.6

2 release files

0.10.5

2 release files

0.10.4

2 release files

0.10.3

2 release files

0.10.2

2 release files

0.10.1

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

This release

0.5.0 This release

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

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