Skip to content

AlphaLab: integrate research workflows and global derivative lifecycle - #18

Draft
VarunSingh022 wants to merge 41 commits into
mainfrom
work/v4.1.1-future-readiness
Draft

VarunSingh022 wants to merge 41 commits into
mainfrom
work/v4.1.1-future-readiness

Conversation

@VarunSingh022

Copy link
Copy Markdown
Owner

Summary

This PR delivers the v4.1.1 future-readiness program as one commit, dc508e9, on work/v4.1.1-future-readiness. It sits on v4.1.0 (33e61d3).

Recommended version: v4.2.0. The work adds public API, three persisted enum members (AssetType.BOND, AssetType.INDEX, ExecutionRouting.ALGORITHMIC) and two persisted schema steps with upgrades. The version is still 4.1.0 because changing it is the maintainer's decision (section 9 of docs/audit/FUTURE_READINESS_V4_1_1.md).

Draft, not for merge. Hosted CI has not run on this branch.

Milestones and engine changes

Milestone What changed in the engine
Scheduling (FUT-003, FUT-004, FUT-005) One RunSchedule and ScheduleCursor cover every timed input: rates, funding, cash flows, distributions, funding rates, splits, observations, timers, exercise, assignment, expiry and settlement. Each entry sits before or after the records at its instant, and ties break by kind. A paused replay keeps outstanding information durably. The run manifest names the schedule.
Multi-dataset (OFE-027, OFE-028) combine_datasets gives several datasets read by one run a single derived identity. observations work on backtest and replay. A replay merges observations through the same public functions as a backtest. Two datasets that price one instrument at the same instant are refused.
Macro and model composition (FUT-006, FUT-007, FUT-008, FUT-010, FUT-012) The macro engine's series arrive as point-in-time observations. Features and a fitted model run on the execution path (examples 72 and 73). Bonds are supported. Contract expiry is supported with cash settlement.
FUT-009: algorithmic execution See below.
FUT-013: derivative lifecycle See below.

FUT-009: algorithmic execution through backtest and replay

  • Routing. An ExecutionPlan given to BacktestEngine.run, ReplayBacktest.run or the facade routes the run ALGORITHMIC.
  • Parents. Each parent is accepted and held. Risk judges it whole and allocation reserves it. Stop, auction and immediate terms are refused before an order exists.
  • Children.
    • They are released at each of the asset's events and checked against a declared VenueCheck.
    • Each one is simulated by ExecutionPipeline.work_child under the run's fill policy, simulator and cost model, and settles on its parent through the canonical report path.
    • Under NEXT_EVENT a child trades only after its release instant, and never outside its slice or window.
    • The parent's reservation is released once.
  • Persistence and lineage. The AlgorithmBook persists in replay snapshot schema 2 and is checked against its plan. The manifest records execution_plan_id. AlgorithmicResult carries the book. Example 74 and benchmarks/benchmark_algorithmic_execution.py are new.
  • Restated criterion. Children match what route_child_order sends a PaperBroker, by id, quantity and instant. Fills are priced at each child's bar, because a PaperBroker sees no market data.

FUT-013: global derivative lifecycle

Terms. InstrumentRecord.terms holds OptionTerms or FutureTerms, outside the identity key (instrument schema 4). They declare:

  • call or put and the strike;
  • AMERICAN or EUROPEAN exercise, with its window;
  • CASH or PHYSICAL settlement against one Deliverable of any quantity per contract;
  • an AutomaticExercise threshold.

Operations. Exercise, Assignment and OptionExpiry are both schedule entries and RunEngine/ExecutionPipeline operations.

Calendars. session_close and settles_at turn a declared MarketCalendar and SettlementRule into lifecycle instants, for any market and timezone.

Global-market design requirement

AlphaLab ships no market's rules. It supports a market when that market's calendar and contracts are declared (matrix 2.27).

tests/integration/test_lifecycle_conventions.py runs the same lifecycle code under several declared markets, timezones and holidays, including Asia/Kolkata and Asia/Dubai. There is no per-market branch in the engine.

Derivative capabilities actually tested

The tests are 58 in test_option_lifecycle.py, 9 in test_lifecycle_conventions.py and 4 in test_lifecycle_example.py.

Exercise and assignment

  • American early exercise inside its window.
  • A European exercise before expiry is refused.
  • Exercise by long holders and assignment of short positions, before expiry (as an input) and at expiry (by the automatic-exercise threshold).
  • Contracts left unexercised expire worthless.

Settlement

  • Cash settlement pays max(S × units × m_deliverable / m − K, 0) against a supplied settlement value. This covers puts, calls and options on futures (deliverable multiplier).
  • Physical delivery closes the option at nothing and moves the deliverable at strike × multiplier through the portfolio's fill authority. The delivered position is re-marked at the market's price, checked at the delivery instant.
  • A physical future delivers at its final price after its last variation.
  • AssetType.INDEX serves as a cash-settlement reference and is refused for trading.

Multi-strategy attribution. A partial exercise is apportioned across strategies by largest remainder, ties going to the earlier strategy. A partial exercise across opposite sides is refused.

Refusals, before anything moves

  • An undeclared deliverable, or one in another currency.
  • A payment the cash cannot cover (no financing is modelled).
  • A delivery that would leave the book short when shorting is not allowed.
  • An exercise outside the window or beyond the position.
  • Orders still working in the contract.
  • A cash exercise with no settlement value, or a physical exercise given one.

Example 75 (a London calendar with a holiday) covers:

  • an early exercise at the close before an ex-dividend date;
  • the dividend reaching the delivered shares;
  • an index put settled in cash;
  • replay equality, and resume from pauses at records 10, 40 and 70 to the identical result_id.

Sentiment and macro acceptance scenario

examples/72_compose_a_multi_asset_strategy.py is held by test_multi_asset_composition.py (29 tests) and is run from every built artifact.

Inputs

  • ACME, a cash equity on XNYS.
  • IDXM4, an index future on XCME with multiplier 50 and variation margin, missing one session.
  • The two come from two vendor files read as one CombinedDataset.
  • 46 social-media posts with collected_at: a late-collected post, an undated post, a repeat, an unreadable row, all out of order.
  • US CPI vintages, including a revision and a release after the data ends.

What the tests establish

  • Consumption. Every eligible input is consumed in knowledge order: 87 records and 49 of 50 deliveries.
  • Point-in-time. Late information is used only once it is available. A control stamped at observation time moves decisions two weeks early, and the same check catches it.
  • Data quality. Out-of-order, duplicate, unreadable and missing data are each explicit.
  • Instruments. Whole contracts, the multiplier and the currency are respected. Equity and NAV reconcile.
  • Risk. Twelve risk refusals are surfaced.
  • Lineage. The lineage covers the combined dataset and the schedule.
  • Determinism. Reruns are deterministic across fresh interpreters and PYTHONHASHSEEDs.
  • Replay. A replay paused at every one of the 87 boundaries, restored from JSON, resumes to the same digest.

What it does not prove. It does not show that sentiment or inflation predicts returns. The prices are synthetic and the strategy loses money.

Validation

All figures are from local runs on the final tree.

Gate Result
ruff check, ruff format --check clean (1,310 files)
mypy . (strict, including examples and benchmarks) 1,208 files, no issues
Full suite, dev checkout 10,020 passed, 3 failed (listed below)
Fresh CPython 3.12.4 / 3.13.17 / 3.14.9 10,021 passed, 2 failed, on each
Examples 75 of 75 pass
Build, twine check --strict, release preflight passed: 609 modules, 2,667 exported names; examples 11 and 70–75 run from the wheel and the sdist
certify_release.py --check 11 checks passed, matching the committed certificate
Public API manifest regenerated identical to the committed manifest: 2,667 exports, 35 shared names each with a reason
historical_inventory_v4.py --check clean
git diff --check clean
Mutation checks 12 of 12 caught (FUT-013), 8 of 8 caught (FUT-009)
Benchmarks no regression: backtest 4.04x (was 4.03x), replay 1.03x, run layer +2.10% (was +2.33%). Algorithmic benchmark: 4.00x and 1.51x

Known failing tests

These are not hidden, and no test was weakened.

  1. tests/regression/test_api_changes_are_in_the_changelog.py::test_every_removed_or_changed_name_is_in_the_changelog is version-keyed. It needs the version bump.
  2. tests/regression/test_nothing_is_left_for_later.py::test_no_ledger_entry_is_assigned_beyond_this_release is version-keyed. It refuses v4.2.0 ledger entries while the package says 4.1.0.
  3. tests/unit/test_package_metadata.py::test_package_exposes_version fails in the dev venv only, because of stale editable-install metadata. It passes in fresh environments.

The first two fail by construction until the maintainer sets the version.

Hosted CI

Not run. No hosted result is claimed.

Outstanding gaps (accepted future work, on the ROADMAP)

  • FUT-002: a strategy configuration that reads back typed. Target v5.0, pending the maintainer's decision.
  • FUT-011: a queue-position and fill-probability model. Target v5.0.
  • FUT-014: a strategy that instructs an exercise from its own hooks. Target v4.3.
  • FUT-015: settlement-date accounting. Target v5.0.
  • FUT-016: richer contract terms (baskets, cash in lieu, quanto, Bermudan). Target v4.3.
  • FUT-017: margin, collateral and forced liquidation. Target v5.0; it depends on FUT-015.

A backtest also simulates a single venue, so routing between venues is evaluated only on the live and paper path. There is no live venue or vendor integration.

Not done

  • No version change.
  • No tag or release.
  • dist/ is untouched.
  • The v4.1.0 tag is untouched.

🤖 Generated with Claude Code

VarunSingh022 and others added 30 commits October 26, 2026 13:55
…(v4.2.0 candidate)

The v4.1.1 future-readiness program, unreleased; the version stays 4.1.0.

Schedule and inputs on every driver
- One RunSchedule for every timed input (FUT-004): rates, funding,
  cash flows, distributions, funding rates, splits, observations,
  timers, and now exercise, assignment, expiry and settlement. Entries
  sit before or after an instant's records, and ties break by kind.
- A paused replay keeps outstanding information durably (FUT-003). The
  schedule is recorded in the run manifest (FUT-005).
- Observations and several combined datasets on backtest and replay
  (combine_datasets, OFE-027/OFE-028). The macro engine's series arrive
  as observations (FUT-008).
- Features and a fitted model on the execution path (FUT-006/FUT-007,
  examples 72 and 73). Bonds (FUT-010) and contract expiry with cash
  settlement (FUT-012).

FUT-009: execution algorithms on the historical drivers
- An ExecutionPlan given to BacktestEngine.run, ReplayBacktest.run or
  the facade routes the run ALGORITHMIC. Each parent is accepted and
  held whole, and stop, auction and immediate terms are refused.
- Children are released per asset event and checked against a
  VenueCheck. Each one fills through ExecutionPipeline.work_child under
  the run's fill policy, simulator and costs, and reconciles to its
  parent.
- The AlgorithmBook persists in replay snapshot schema 2. The manifest
  records the plan id.
- Added AlgorithmicResult, example 74 and
  benchmark_algorithmic_execution.

FUT-013: a market-agnostic derivative lifecycle
- InstrumentRecord.terms declares OptionTerms or FutureTerms (instrument
  schema 4, outside the identity key):
  - call or put, strike, AMERICAN or EUROPEAN with a window;
  - CASH or PHYSICAL settlement with a Deliverable;
  - an automatic-exercise threshold.
- Exercise, Assignment and OptionExpiry run as schedule entries and as
  RunEngine/ExecutionPipeline operations.
  - Cash settlement pays the intrinsic value against a supplied
    settlement value.
  - Physical delivery moves the deliverable at strike x multiplier
    through the portfolio's fill authority. It is apportioned across
    strategies by largest remainder.
  - A physical future delivers at its final price.
- Refused before anything moves:
  - an undeclared or other-currency deliverable;
  - an unfunded payment;
  - a forbidden short;
  - out-of-window or over-position exercise;
  - working orders;
  - a missing or extraneous settlement value.
- AssetType.INDEX is a reference value that cannot be traded.
- Calendars gain session_close and settles_at, for any declared market
  and timezone. Added example 75.

Governance
- Ledger: FUT-009 and FUT-013 delivered; FUT-014..FUT-017 and TST-023
  recorded.
- Also updated: ROADMAP, CHANGELOG [Unreleased], ADR-0051, the
  capability matrix (2.27 global conventions), the gaps report,
  FUTURE_READINESS_V4_1_1, the public API manifest (2,667 exports) and
  the release certification (11 checks).

Validation (local; hosted CI not run)
- ruff, format and mypy (1,208 files) are clean.
- Checkout: 10,020 passed, 3 failed.
- Fresh CPython 3.12, 3.13 and 3.14: 10,021 passed, 2 failed each.
- 75/75 examples; build, twine --strict and preflight passed.
- Mutation checks: 12/12 (FUT-013) and 8/8 (FUT-009) caught.
- The failures are expected until the version is bumped:
  test_api_changes_are_in_the_changelog and
  test_no_ledger_entry_is_assigned_beyond_this_release are
  version-keyed; test_package_metadata fails on the stale editable
  metadata of the dev venv only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-011)

A backtest or replay given queue= fills a resting limit order only as far as
its queue model says has reached the front of its price level. Before this,
the order filled as soon as the market touched its limit, as if nothing had
been resting there ahead of it.

What it adds
- alphalab.execution.queue:
  - QueueModel, a protocol that depends only on core types;
  - QueueObservation, what one event showed: the quote, the print with its
    reported aggressor, the bar's range;
  - QueuePosition, an order's limit and the quantity ahead of it, or
    unknown;
  - QueueTurn, the new position and what of the order may fill;
  - QueueBook, the positions of the orders a model governs;
  - PriceTimeQueue, the one model shipped.
- PriceTimeQueue models FIFO price-time priority over displayed liquidity:
  - an order joins behind what is shown at the best price, at the front
    inside the spread, or with an unknown queue behind the best price;
  - prints at the limit from the other side consume the queue first;
  - a smaller displayed size bounds the queue, and a larger one never adds
    to it;
  - a best price beyond the level empties the queue;
  - the market trading through the limit clears it;
  - a touch fills only an order already at the front;
  - an unsigned print counts only with count_unsigned_trades.
- queue= on BacktestEngine.run, ReplayBacktest.run and alphalab.api
  backtest/replay. The result is a QueuedResult, whose
  execution_assumptions.queue_model names the model.
- ExecutionAssumptions.passive_fills states how passive fills were decided,
  with or without a model.
- ReproducibilityManifest.queue_model records the model. It enters the
  manifest identity only when present.
- A paused replay carries the queue book: replay schema 2 (unreleased) and
  QUEUE_BOOK_SCHEMA 1. restore(..., queue=) refuses a model that is missing,
  extra, or configured differently.
- The model is refused beside an execution plan (QueueModelError) and on any
  run that does not route SIMULATED (RuntimeValidationError).

How it fits the engine
- Only the passive fill is gated. The run's own fill policy decides within
  the queue's limit, the simulator prices the fill as a maker, and the OMS,
  portfolio, reservation and attribution settle it as they settle any fill.
- The QueueBook is driver state, passed into RunEngine.advance and the
  pipeline step as a parameter, the way the FX table is. No pipeline, run or
  config schema moves, and a run without a model keeps its exact record and
  result_id. The certificate re-certified with only the source digest and
  the API manifest's digest and export count moved.

What it does not model, stated on the ledger and in ADR-0051 decision 15
- Pro-rata books, hidden or iceberg liquidity, depth beyond the top of the
  book, keep-priority amends and self-trade prevention.
- A fill probability.
- An execution plan's children.

Docs and tests
- Docs: the ledger (FUT-011 delivered), ROADMAP, CHANGELOG [Unreleased],
  ADR-0051, the capability matrix, the gaps report, the readiness report,
  STATE_MODEL, COMPOSING_STRATEGIES, PUBLIC_API, INTEGRATION and the public
  API manifest (2,678 exports).
- Tests: tests/integration/test_queue_model.py (32) and a manifest test.
- Pins extended, each with its reason: the schema-constant lists, the replay
  field set, the frozen v4.1.0 replay payload's upgrade, the order of the
  result's fields, and the ADR deferral classifier.

Validation (local; this commit's hosted CI is not yet run)
- ruff and format are clean (1,313 files); mypy has no issues in 1,211
  source files.
- Dev checkout: 10,053 passed, 3 failed.
- Fresh CPython 3.12.4, 3.13.17 and 3.14.9: 10,054 passed, 2 failed each.
- All 75 examples pass.
- certify_release --check: 11 checks passed.
- Build, twine --strict and preflight passed (611 modules, 2,678 names).
- Benchmarks against v4.1.0: no regression. A queue model costs about 9% on
  a run resting about 150 orders at every one of 6,000 events.
- Mutation checks: 15 of 15 caught.
- The failures are the two version-keyed guards
  (test_api_changes_are_in_the_changelog and
  test_no_ledger_entry_is_assigned_beyond_this_release), plus the dev
  venv's stale editable metadata (test_package_metadata). The version is
  unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pe (FUT-002)

A strategy class that sets configuration_type gets three guarantees:
- It is configured only with a value of that type.
- After a restore, it reads the recorded value back as that type, equal
  and of the same types at every depth, so restore(capture(s)) == s.
- The payload is exactly what the encoder has always written. No schema,
  result_id, configuration_id or manifest moves, and every earlier payload
  reads as before.

What it adds
- alphalab.strategy.configuration, also exported by alphalab.strategy:
  - CONFIGURATION_ATTRIBUTE;
  - StrategyConfigurationError;
  - declared_configuration;
  - require_configuration;
  - decode_configuration.
- RuntimeSupervisor.configure checks a declaring strategy's value against
  its type, strictly. An int is not a float, a bool is not an int, a str
  subclass is not a str, and a non-finite float or Decimal is refused. Each
  refusal names the field.
- restore reads a declaring strategy's configuration back as its type.
  Anything that does not match is refused as a StateDecodeError naming the
  strategy and the field.

Declarable
- Scalars: None, bool, int, finite float, str, finite Decimal (its exact
  text, exponent kept) and UUID.
- Enums: StrEnum, IntEnum and plain Enum.
- Literal, T | None and Annotated.
- list[T], tuple[T, ...] and fixed tuples.
- dict[K, V] and Mapping[K, V], the latter read back as a read-only mapping.
  K may be str, int, bool, float, or a string or integer enum.
- Dataclasses, rebuilt through their own constructors. A field missing from
  an older payload reads back at its default, and __post_init__ constraints
  hold on the way back.
- Any, for JSON-native values.

Refused when the strategy is configured, never coerced
- A set or a datetime.
- A union of two non-None types.
- A mapping keyed by Decimal, UUID or a plain Enum.
- A class that is not a dataclass, or a dataclass with a non-init field.
- An enum that redefines how it prints.

Scope, stated on the ledger, the ROADMAP and ADR-0051 decision 16
- The typed read-back comes from the declaration.
- A strategy that declares nothing reads back as JSON reads it (LIM-004),
  unchanged.
- Reading an undeclared configuration typed would need type tags in the
  payload. That would move every run's identity, which this release does
  not do.

Docs and tests
- Docs: the ledger (FUT-002 delivered), ROADMAP (FUT-002 moved out of
  Future work), CHANGELOG [Unreleased], ADR-0051, STATE_MODEL, INTEGRATION,
  the capability matrix, the gaps report, the readiness report, PUBLIC_API
  and the public API manifest (2,683 exports). The certificate re-certified
  with only the source digest and the API manifest's digest and export count
  moved.
- Tests: tests/regression/test_typed_strategy_configuration.py (62). The
  LIM-004 contract suite is unchanged and passes; its docstring now names
  the typed path. The ADR-deferral classifier lists the new limitation.

Validation (local; this commit's hosted CI is not yet run)
- ruff and format are clean (1,315 files); mypy has no issues in 1,213
  source files.
- Dev checkout: 10,115 passed, 3 failed.
- Fresh CPython 3.12.4, 3.13.17 and 3.14.9: 10,116 passed, 2 failed each.
- All 75 examples pass.
- certify_release --check: 11 checks passed.
- Build, twine --strict and preflight passed (612 modules, 2,683 names).
- Mutation checks: 13 of 13 caught.
- The failures are the two version-keyed guards, plus the dev venv's stale
  editable metadata. The version is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The maintainer approved v4.2.0 as the development and release target. This
applies the readiness report's versioning checklist, except anything that
publishes: no tag is created or moved, no release or upload is made, `main` is
not touched, and the v4.1.0 tag and its artifacts are unchanged.

What changed
- alphalab/common/_version.py, the one declaration, is now 4.2.0, and
  test_package_metadata's pin follows it.
- CHANGELOG: [Unreleased] becomes [4.2.0], marked unreleased and not yet
  published or tagged. The changelog guard showed the section lacked three
  changed bindings, and the rows are added:
  - BrokerAssetClass, the AssetType enum, with BOND and INDEX;
  - InstrumentRegistryRecord, with last_trade_at and terms;
  - InstrumentRegistrySnapshot, whose schema_version default goes 3 -> 4.
- The version markers in README, docs/README, ARCHITECTURE and nowandfuture
  now say 4.2.0, and README states 4.2.0 is unpublished and the published tag
  is v4.1.0.
- docs/INTEGRATION.md's pins follow the release-prep convention, with a
  note that the 4.2.0 references resolve only once it is published. Its claim
  about which payloads were frozen stays v4.1.0's, which is what is true.
- docs/api/history/4.2.0.json is the release's own manifest, which the
  release preflight requires, with its README row. The public API manifest is
  regenerated (release 4.2.0, 2,683 exports) and the certificate re-certified:
  only the release label, the engine string and the source and manifest
  digests moved, and no result_id.
- .github/workflows/preflight.yml's push route and RELEASE_VERSION now name
  the 4.2.0 candidate branch, work/v4.2.0-release, and 4.2.0. This
  development branch was named before the number was chosen; its pull request
  can be preflighted by its label. The preflight tests' identity cases follow
  the same convention. actionlint (with shellcheck) is clean.
- ROADMAP and FUTURE_READINESS_V4_1_1 record the decision.

The stale-metadata failure (test_package_exposes_version) was the dev venv's
editable install, made at 4.0.0 and never refreshed. It was reinstalled
locally with --no-deps -e; nothing in the repository changed for it.

Validation: the full suite, dev venv, -W error, gives 10,118 passed, 0
failed, 0 skipped. ruff, format and git diff --check are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…FUT-014)

A strategy can now decide inside the run to exercise an option it holds.

How a strategy asks
- Any hook can return Intent(kind=IntentKind.EXERCISE, instrument=<option>,
  target=<whole contracts>). An intent is still the one thing a hook
  returns.
- It is not an order. The execution pipeline keeps it from allocation, risk
  and the OMS, and AllocationEngine.allocate refuses one handed to it
  directly, so it is never sized as a target.

What the pipeline does with it
- It carries the request out at the end of the step whose hook returned it,
  at that event's instant, in the order asked.
- The checks are the ones a scheduled Exercise passes: the contract's
  declared window, no working orders in it, and a delivery the book can
  take.
- It acts on the asking strategy's own long contracts only, and attributes
  the result to that strategy alone.
- A cash-settled contract settles against the last price the run observed
  for its underlying, never a value the strategy supplies. If the run has
  observed none, the request is refused.

Malformed versus refused
- A malformed instruction fails the strategy, as any malformed intent does:
  a fractional or non-positive count, a strength other than 1, or order
  terms.
- An instruction the run cannot carry out is not a strategy failure. It is
  recorded as an ExerciseRefused event in the strategy runtime's persisted
  log, with its reason. Nothing moves and the strategy keeps running.

Effective time
- The ledger's criterion said after the instant's records. The instruction
  instead takes effect at the end of the step that asked, at the same
  instant.
- Holding it until the instant's last record would need pending state kept
  across a pause between two records of one instant. That would be a new
  persisted field, and it would move every run's result_id.
- The end of the asking step needs no such state, uses nothing later than
  the event the strategy decided on, and is identical in backtest and replay
  and across a pause. The ledger and ADR-0051 decision 17 record this.

Records and identities are unchanged
- A run that asks for no exercise records exactly what it did. The new
  IntentKind member and the ExerciseRefused event tag appear only when used.
- The certificate re-certified with only the source digest, the manifest
  digest and the export count (2,684) moved.

Docs: the ledger (FUT-014 delivered), ROADMAP, CHANGELOG [4.2.0], ADR-0051
decision 17 with its limitation, the capability matrix and the gaps report.
The pins extended with reasons are the IntentKind members, the persisted
strategy-event tags and the ADR classifier.

Tests: tests/integration/test_strategy_directed_exercise.py (18) and an
allocation test. Eleven mutants of the rules are each caught.

Validation: the full suite, dev venv, -W error, gives 10,137 passed, 0
failed. All 75 examples pass. ruff, format, mypy (1,214 files) and
certify --check are clean.

Not pushed: the repository's protect ruleset currently refuses updates to
this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion, Bermudan exercise, adjusted terms (FUT-016)

A contract's terms now say what it delivers and from when. Every term is
declared on the contract; nothing is inferred from a market.

New terms on OptionTerms (all defaulted)
- basket: further Deliverables delivered with the main one.
- cash_per_contract: a cash component of each contract's delivery.
- consideration_currency: the currency the strike is paid in, converted at
  the exercise instant by the run's rates. A quanto contract adds
  consideration_rate, a conversion rate it fixes.
- fractions: CashInLieu, at a declared price or the last observed one, for a
  fraction of a unit the book cannot hold.
- ExerciseStyle.BERMUDAN, with ordered, non-overlapping exercise_windows
  that end at expiry.
- adjustments: TermsAdjustment entries, each effective from its own instant.
  OptionTerms.split_adjusted writes the adjustment a split implies, and a
  merger's adjustment is declared the same way.

How a package is delivered
- The delivery is planned before anything moves, then booked through the
  portfolio's fill authority.
- The side taking the package in pays the strike less the cash component,
  converted through PortfolioEngine.convert_cash when the consideration is
  in another currency.
- Any fraction of a unit is settled in cash in lieu.
- The rest is the whole components' cost, apportioned by their observed
  market values, so the fills move exactly the net cash. Each component is
  attributed to the strategies whose contracts delivered it.
- A cash-settled package, and a package's moneyness at expiry, are valued
  at each component's observed price. OptionExpiry.underlying_value is now
  optional, and a single value is refused for a package.

Splits of a deliverable
- A split of an instrument that a held or working contract delivers is
  refused unless the contract declares, effective at the split's instant,
  the adjustment the split implies: the split instrument's units times the
  ratio, with the strike and everything else unchanged. That adjustment is
  economically exact for any ratio.
- A declared adjustment whose economics differ is refused.
- A physically delivering future on the split instrument refuses the split.

Fixed
- A delivery could leave a whole-units book holding a fraction of a share.
  It is now refused unless the contract declares cash in lieu.
- A split left the contracts on its instrument unadjusted, delivering half
  their value. It is now refused unless an adjustment is declared.
- OptionContract refuses a Bermudan style, so no pricer prices it as
  American or European.

Not modelled, and stated on the ledger and in ADR-0051:
- components in two currencies within one package;
- a package with no whole unit;
- cost allocation by anything but observed market value;
- quanto conversion of the payoff (only the consideration is converted);
- adjustment of futures' terms.

Records and identities are unchanged
- Single-deliverable contracts are booked exactly as before.
- The new terms are written to the registry snapshot only when declared,
  and the registry is not part of a run's record, so no result_id moves.
- The certificate re-certified with only the source and manifest digests
  and the export count (2,686) moved.

Docs: the ledger (FUT-016 delivered), ROADMAP, CHANGELOG [4.2.0] (Added,
Fixed, and the API table), ADR-0051 decision 18 with its limitation, the
capability matrix's conventions table, the gaps report and the ADR
classifier.

Tests: tests/integration/test_advanced_contract_terms.py (24). Thirteen
mutants are each caught.

Validation: the full suite, dev venv, -W error, gives 10,160 passed with
the classifier entry fixed. All 75 examples pass. ruff, format, mypy (1,215
files) and certify --check are clean.

Not pushed: the repository's protect ruleset refuses updates to this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ying power (FUT-015)

An instrument can now declare its settlement cycle, and a run then treats a
sale's proceeds as unspendable until they settle.

How it works
- InstrumentRecord.settlement declares a rule: trade date, or T+n trading
  or calendar days. It sits outside the identity key.
- Each fill of such an instrument is booked with its settlement instant:
  simulated and venue fills, lifecycle deliveries and contract final
  settlements alike. The instant comes from the rule over the listing
  venue's declared calendar, starting from the trade's local session date,
  so holidays, weekends and the venue's time zone all count.
- The instant and the signed cash ride on the fill's own ledger Transaction
  as metadata (settles_at, settlement_cash). The ledger is already
  persisted, so what is unsettled follows from it exactly on every driver
  and across every restore.
- PortfolioState.unsettled is a derived index of PendingSettlement entries,
  kept by one rule at every fill and rebuilt from the ledger on restore.
- PortfolioEngine.cash_position reports, per currency, trade-date cash,
  unsettled credits and debits, settled cash and available cash.

What changes for a run
- Buying power is trade-date cash less proceeds not yet received, converted
  into the base currency. It is applied wherever the risk gate resyncs, so
  an order needing unsettled proceeds is refused, and is accepted once they
  settle.
- Settlement is a fact of the clock, never an applied event, so nothing
  settles twice.
- An order in an instrument whose venue declares no calendar for its rule
  is refused before it is placed, naming the fix.

Records and identities are unchanged
- This entry expected a portfolio schema step; none is needed. An
  instrument declaring no rule settles at its trade and writes the empty
  metadata it always did, so no run's record or result_id moves and every
  earlier payload reads as settled.
- The certificate re-certified with only the source and manifest digests
  and the export count (2,688) moved.

Not modelled, and stated on the ledger and in ADR-0051 decision 19:
- the books are trade-date, so units bought and not yet settled may be sold
  (there is no free-riding rule);
- interest on unsettled balances;
- a settlement fail;
- rules that vary by counterparty rather than by instrument.

Docs: the ledger (FUT-015 delivered), ROADMAP, CHANGELOG [4.2.0], ADR-0051
decision 19 with its limitation, the classifier, the capability matrix,
the gaps report, STATE_MODEL and COMPOSING_STRATEGIES. The pins extended
with reasons are the persisted enum names (SettlementBasis), the instrument
upgrade expectations (settlement: None) and the portfolio field coverage
(unsettled is derived).

Tests: tests/integration/test_settlement_date_accounting.py (11). Ten
mutants are each caught.

Validation: the full suite, dev venv, -W error, gives 10,172 passed, plus
the one roadmap wording fix after it. All 75 examples pass. ruff, format,
mypy (1,217 files) and certify --check are clean.

Not pushed: the repository's protect ruleset refuses updates to this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rms (FUT-017)

An instrument declares what holding it requires as collateral, and the account's
margin decides when it is called. Nothing is inferred from an asset class or a
venue, and a run declaring no margin records exactly what it did: no schema moves
and no earlier run's result_id changes.

What changed
- alphalab/instrument/margin.py: MarginTerms (a MarginBasis -- a rate of value or
  an amount per contract -- initial and maintenance requirements, a collateral
  haircut, a liquidation priority). InstrumentRecord.margin, kept by the registry
  snapshot; InstrumentRegistry.margined, a derived index, so a run declaring none
  reads nothing.
- alphalab.runtime.margin_position: the account's margin in the base currency.
  After every market step an account below maintenance is called (a MarginCall
  risk event) and liquidated: the least that restores the initial requirement,
  in whole lots or units, by priority then the largest requirement, as each
  holding strategy's own target through allocation, risk and the OMS, resting to
  its asset's next event and filling by the run's fill policy. A step still in
  breach calls again from the book as it is; a working liquidation is never
  placed twice; under an execution plan the liquidation is worked by the asset's
  algorithm.
- Before an order: while in breach only reductions trade; an order growing a
  margined position must leave equity at or above the initial requirement,
  working orders counted; a margined future spends its initial margin of buying
  power (RiskEngine.evaluate / project gain a keyword-only charge), not a
  notional no cash moves for.
- Financed deliveries: a delivery the cash cannot pay is booked as a debit
  (apply_fill / convert_cash gain keyword-only financed) only when every
  deliverable declares margin terms; an obligation is booked and called, an
  instructed exercise financed only within the initial requirement. A deliverable
  declaring none is refused as before.

Fixed
- Buying back a short for more than the cash raised InsufficientFundsError and
  stopped the run, with or without margin; it is now debited as the obligation
  it is.
- The persisted-enum scanner only recognised modules defining from_primitives,
  so ExerciseStyle, OptionType, SettlementStyle, SettlementModel,
  AlgorithmStatus, IncompletePolicy and MissingVolumePolicy were persisted and
  unpinned. It now follows every decoder by the architecture's names, the helpers
  it reaches and the tables it reads; they are pinned, with MarginBasis.

Not modelled, and stated: portfolio margin and offsets (FUT-019, v5.0); buying a
fully paid instrument on margin and a debit's interest (FUT-018, v4.3); a grace
period; checks between market events. Risk's MarginLimit and an allocation
budget stay configured notional caps.

Evidence: 35 margin and 13 financed-delivery integration tests, 4 tests where
FUT-017 meets FUT-009, FUT-015 and FUT-011, replay parity and a pause at every
boundary; 36 mutants each caught. Full suite under -W error: 10,240 passed, the
one failure the certificate's digest, re-certified (only the source digest,
manifest digest and export count moved, 2,688 -> 2,694). 75/75 examples. ruff,
format and mypy . clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ap audit (EXE-012)

The v4.2.0 global gap audit read the engine against its objective -- any
researcher-defined strategy, in any declared market -- rather than the FUT list,
and this commit holds what it fixed and what it recorded.

Fixed: a strategy could not take back an order it had working (EXE-012)
- In a historical run a resting limit or stop left only by filling or by its
  time in force. A strategy re-targeting beside one traded against it: a target
  of zero next to a resting buy for 100 sold 100 short, and the stale buy filled
  later. A passive or market-making strategy could not re-quote.
- IntentKind.CANCEL (target 0) withdraws every order working in an instrument
  that the asking strategy alone contributed to, at the asking event, after its
  fills and before the batch's orders are placed, so a cancel and a new limit in
  one return replace a quote. It takes the cancellation every withdrawn order
  takes: the OMS cancels it, its reservation and contributions are freed, a
  queue model holds nothing for it, and the strategy hears it through on_order.
- An order netted with another strategy's, a parent an execution plan works, or
  an order at a venue is left working and recorded as a CancelRefused.
  Allocation refuses a cancel handed to it directly; a malformed one fails the
  strategy as any malformed intent does.
- Additive, so no run's record moves: a new target still counts a resting order
  as held, as the strategy guide now states. Making a target supersede working
  orders would have moved every such run's result_id. Brackets, one-cancels-
  other and trailing stops compose from CANCEL over the hooks.

Recorded
- LIM-008, kept: a run whose NAV falls below zero -- now reachable through a
  financed delivery or a short covered past its cash -- is refused by analytics,
  correctly; such a run keeps its books with compile_analytics=False.
- FUT-020 (venue price limits and halts, v4.3) and FUT-021 (holding exchanges:
  mergers, spin-offs, rights, v4.3), accepted with completion criteria.
- FUT-018 widened to short borrow (availability and fees), with the declared
  ScheduledCashFlow named as the interim for a debit's interest.
- No region is hard-coded: the only US literals left are docstrings and the
  ISO 4217 minor-unit table.

Evidence: 12 cancel integration tests and an allocation test; 10 mutants each
caught. Full suite under -W error: 10,258 passed, 0 failed; 75/75 examples.
ruff, format and mypy . (1,222 files) clean. Re-certified: only the source digest,
manifest digest and export count moved (2,694 -> 2,695).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/audit/V4_2_0_READINESS.md states, from each command's own output, what the
v4.2.0 program delivered and what remains:

- each milestone's commit and evidence: FUT-011, FUT-002, the version, FUT-014,
  FUT-016, FUT-015, FUT-017 and the gap audit's EXE-012;
- the global gap audit's findings, each classified and disposed, and every
  material limitation;
- validation on the final tree: 10,258 passed under -W error in the dev venv and
  in fresh CPython 3.12.4, 3.13.18 and 3.14.10 venvs from a clean mirror; 75/75
  examples; the certificate's check; build, twine --strict and the
  installed-artifact preflight passing;
- performance against v4.1.0 and identity and compatibility evidence;
- remote and PR state, and the confirmations that main, the v4.1.0 tag and its
  artifacts are unchanged and nothing is tagged, published or merged.

Its verdict is that the engine is complete for what the ledger and ROADMAP
state, not globally: FUT-018 to FUT-021 and the stated limitations remain.
docs/README.md points to it; FUTURE_READINESS_V4_1_1.md is marked superseded for
v4.2.0. Documentation only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ared terms (FUT-018)

A broker's financing and a lender's borrow are terms, declared with their dates;
the run charges what they state on what the book holds and refuses what nothing
makes available. A run declaring none of it records exactly what it did: no
schema moves and no earlier run's result_id changes.

What changed
- Margin purchases: MarginTerms.purchase_on_margin (written only when declared)
  lets a fully paid instrument be bought on margin. It spends its initial
  requirement of buying power, the pre-trade initial-margin check holds it to
  equity, and the fill's shortfall is booked as a financed debit. A short sale,
  or a purchase in an instrument not declaring it, is charged its notional.
- Interest: ScheduledInterest charges a period's declared debit tiers
  (FinancingTier, each band at its own rate, the last unbounded) and optional
  credit rate on one currency's balance at its instant. The period's dates are
  counted by a conventions DayCount as an exact Decimal; the balance is
  trade-date or, by BalanceBasis.SETTLED, settled cash (FUT-015). It is booked
  as an INTEREST cash flow.
- Borrow: InstrumentRecord.borrow declares BorrowTerms, a dated series of
  BorrowAvailability (units available or a stated no-limit, and an annual fee),
  kept by the registry snapshot.
  - An order growing the book's committed short, every working sale counted,
    beyond what is available in effect is refused before it is placed, as is
    any short before the first entry. A cover never is.
  - ScheduledBorrowFee charges each short the fee in effect on its value at the
    last mark, multiplier included, refusing one with no entry in effect or no
    mark.
- Fixed: the deterministic encoder refused datetime.date, so no schedule could
  hold an accrual period. A date is now ISO 8601; a datetime is still refused.

Not modelled, and stated: accrual on the balance at each declared period's
instant rather than a daily average; borrow recall or buy-in; borrow per
strategy rather than for the book.

Evidence: 39 integration tests and an encoder test; 19 of 20 mutants caught,
the twentieth equivalent (a tier loop's early exit). The structural enum
scanner pinned DayCount, which borrow_from_primitives now reads. Full suite
under -W error: 10,299 passed, 0 failed; 75/75 examples. ruff, format and
mypy . (1,225 files) clean. Re-certified: only the source and manifest digests
and the export count moved (2,695 -> 2,701).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te (FUT-019)

Offsets between positions are a clearing house's or a broker's methodology, so
the engine applies one only as declared, and every reader of margin takes the
declared model's figure. A run declaring no model is FUT-017's exactly; the model
lives on the registry, outside every run's record.

What changed
- alphalab.instrument.margin: a MarginModel protocol reads every margined
  position at once (MarginExposure) and returns the book's MarginRequirement.
  - PerPositionMargin is FUT-017's sum, declared.
  - ScenarioMargin revalues the positions it covers under each declared
    MarginScenario at once and requires the worst loss, never below a declared
    floor rate of their gross value; maintenance is a declared ratio of it.
  - A scenario set that leaves a covered instrument out of any scenario is
    refused. A position the model does not cover is margined on its own terms
    and offsets nothing.
- declare_margin_model puts the model on the registry, kept by the registry
  snapshot; it refuses a model covering what the registry does not hold or
  margin.
- With a model declared:
  - the margin check after each market step reads its requirement;
  - a liquidation is sized by coordinate descent over the positions in their
    declared order (the deficit is convex in one position's size: ternary
    search for the best reduction, bisection for the least that restores),
    leaving a hedge leg alone when selling it would raise what is owed;
  - the pre-trade initial-margin check projects the book with its working
    orders and the order;
  - the risk gate's MarginLimit reads the model's requirement over margin equity
    (RiskEngine.evaluate / project gain keyword-only margin);
  - a future or margin purchase is gated by the model, not charged its own
    offset-blind initial margin of buying power.

Not modelled, and stated: a scenario moves each covered instrument by its
declared move alone (an option is not repriced from its underlying); each order
is judged against the book as it stands, so a large pair's first leg is judged
before its hedge offsets it.

Evidence: 24 integration tests; 19 of 20 mutants caught, the twentieth
equivalent. FUT-017's and FUT-018's suites unchanged. Full suite under -W error:
10,323 passed, 0 failed; 75/75 examples. ruff, format and mypy . (1,226 files)
clean. Re-certified: only the source and manifest digests and the export count
moved (2,701 -> 2,708).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d inputs (FUT-020)

Where and when an instrument trades is its venue's rule, declared with its dates
on the registry, applied at every order's turn, and nothing is assumed of any
market. A run declaring no controls, or controls that never bind, records exactly
what it did.

What changed
- alphalab/instrument/controls.py: TradingControls holds
  - PriceBands: for one instrument and an interval, a limit down and up and a
    BandAction for an order priced beyond them, REFUSE or CAP; two bands of one
    instrument are never in force at once;
  - TradingHalts: of one instrument or of every instrument a venue lists.

  declare_trading_controls puts them on the registry, kept by its snapshot, and
  refuses an instrument the registry does not hold or a venue nothing lists on.
- At every simulated order's turn (_work_order, and an algorithm's children)
  nothing trades in a halt or at a price outside the band in force:
  - the order rests, a market order to its first turn after;
  - an immediate-or-cancel or fill-or-kill order is cancelled;
  - a stop is not set off by a price in a halt;
  - a resting limit or a stop's gap price beyond the band does not fill there;
  - a child is withdrawn as an unfilled market child is while its parent's
    schedule goes on.
- At placement, a limit beyond a band is refused, naming the band, or capped to
  its edge, as the band declares.
- A missing quote, a halt, a band, a closed session and an absent fill each
  refuse, rest or fail for their own reason. A margin liquidation rests through
  a halt and is never placed twice.
- context.universe.halted and price_band (on UniverseProtocol, the view and the
  null universe) tell a strategy what is in force now, never when it ends.

Not modelled, and stated: a reopening auction; a venue that rejects rather than
holds a market order in a halt; bands computed from a reference price inside
the run.

Evidence: 27 integration tests; 18 of 18 mutants caught. The structural scanner
pinned BandAction. Full suite under -W error: 10,351 passed, 0 failed; 75/75
examples. ruff, format and mypy . (1,228 files) clean. Re-certified: only the
source and manifest digests and the export count moved (2,708 -> 2,713).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ms (FUT-021)

A holding exchanged by its issuer is booked by the issuer's declared terms, its
basis carried exactly, and one event says why the holding became another. A run
declaring no exchange records exactly what it did.

What changed
- alphalab.portfolio.HoldingExchange declares one transformation:
  - legs (ExchangeLeg: the instrument received, units per unit held, the share
    of the basis it carries, an optional in-lieu price for a fraction);
  - a cash part per unit, in a declared currency;
  - a retained basis share for a spin-off or a rights issue.

  The shares sum to one, so no basis is lost or invented. This covers a
  stock-for-stock, cash-and-stock or cash merger, a spin-off, and a rights
  issue, whose rights are a declared option exercised through the lifecycle.
- ExecutionPipeline.apply_exchange / RunEngine.apply_exchange (scheduled as
  ScheduledExchange, before the instant's records as a split is) plan it before
  anything moves and refuse it whole if anything fails:
  - every leg must be declared by the registry;
  - the book takes whole lots or units where it trades them, and a fraction is
    paid in lieu or refused;
  - the basis is apportioned and converted at the rate in force, refused
    without one;
  - the cash part realizes its share, received by a long and paid by a short,
    which receives short legs;
  - a contract on the instrument is refused unless it declares its adjustment;
  - working orders in it are cancelled, as at a split;
  - a leg held on the other side is refused rather than netted.
- PortfolioEngine.apply_exchange books the result: EXCHANGE ledger transactions
  and one PositionExchanged event (what was held, what it became, the cash, the
  P&L realized, the reference), persisted and pinned. Each strategy's share
  becomes its share of each leg.
- The roadmap guard's guard-on-the-guard now checks that the parser reads a
  delivered entry's status, rather than requiring some future work to exist;
  its exact match between ROADMAP and ledger is unchanged.

Not modelled, and stated: a holder's election between considerations; an
exchange settling after its instant; strategies on opposite sides of the holding.

Evidence: 26 integration tests; 17 of 17 mutants caught. Full suite under
-W error: 10,377 passed, 0 failed; 75/75 examples. ruff, format and mypy .
(1,229 files) clean. Re-certified: only the source and manifest digests and the
export count moved (2,713 -> 2,716).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four tests hold FUT-018 to FUT-021 against each other: a holding exchanged
into a stricter margined instrument is called on its new terms; a forced
liquidation rests while the market is beyond its band and fills once back
inside; an option adjusted for a merger delivers the new instrument; and one
book -- short on declared borrow, a margin purchase placed in a halt, a
period's interest and borrow fee, a merger -- replays identically from every
pause, each scheduled item applied once.

Example 76, a financed long-short book: a pair margined by declared
scenarios, the long bought on margin and its debit charged tiered interest,
the short sold against a lender's borrow and its fee, a bid placed in a halt
withdrawn and re-quoted inside a price band, the short carried through a
merger; replayed and resumed. It is built on public names only, held figure
for figure by tests/integration/test_financed_book_example.py, and run from
every built artifact by tests/installed_smoke.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to FUT-029

Read after FUT-018 to FUT-021 were delivered, against the engine objective
rather than the FUT list, each gap classed 1 to 6.

Fixed:
- A strategy could read a lender's later borrow entries through
  context.universe.instrument (class 1, information before its instant).
  The view now shows a record's borrow terms only as far as they are in
  effect, and context.universe.borrow gives the units available and the fee
  in effect now -- (0, 0) before the first entry, as the execution path
  reads it. Protocol, view and null object each gain the method.
- A declared tick grid was read by nothing that placed an order (EXE-013).
  InstrumentRecord.tick declares the venue's TickSchedule, kept by the
  registry snapshot (an absent key reads as none); a limit or stop between
  two ticks of its own band is refused at placement, a negative one too, a
  market order never; a band's cap lands on the grid inside the band.
- A day count's year was chosen by elimination (NUM-016): days_in_year
  states each basis's year, which year_fraction and day_fraction read, and
  refuses a basis whose year is not stated. Every figure is unchanged.

Accepted on the ROADMAP, each with interim behaviour, horizon, dependencies
and completion criteria: FUT-022 borrow recall and buy-in, FUT-023
daily-balance accrual, FUT-024 holder elections and announcement instants,
FUT-025 a corporate action settling after its instant, FUT-026 options
repriced in margin scenarios, FUT-027 a halt's reopening auction and auction
imbalance, FUT-028 bands from a reference price, FUT-029 a strategy's amend
in place.

29 new tests; 23 of 23 mutants caught. No run's record moves: the
re-certification moved only the source digest, the manifest's digest and
the export count (2,717).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The milestones table gains FUT-018 to FUT-021, the integration commit and
the phase 2 gap audit; the verdict, the audit's classified findings, the
limitations and the validation on 5014c79 are rewritten: 10,417 tests on a
fresh CPython 3.12, 3.13 and 3.14, 76 examples, both artifacts built,
twine-checked and smoke-tested alone, the v4.1.0 payload resuming to its
reference, the run benchmarks level with v4.1.0, and the cost of the
declared paths measured. README's test and example counts brought current.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(FUT-022)

A borrow entry declares a recall's buy_in_at deadline, at or after its
notice and only on an entry stating a limit; written only when declared,
so terms without a recall keep their form. The registry indexes the
instruments whose terms declare a recall (recallable, derived, appended
last), so a run declaring none costs one emptiness test per market step.

After each market step, before the margin check, a short beyond the entry
in effect's availability, once its deadline has passed, is bought in: the
excess rounded up to whole lots or units, each short strategy covering its
own share as a DELTA intent through allocation, risk and the OMS, a market
order resting to the asset's next event. It fills as the market allows --
resting through a halt, in part with thin liquidity, not at all with
nothing shown -- and what it leaves is bought in at the next step. A
working market buy defers it; a strategy's resting limit does not. Each
round is a BuyIn on the risk log, persisted and pinned.

20 integration tests; 14 of 14 mutants caught. Re-certified: only the
source digest, the manifest's digest and the export count (2,718) moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-015)

A run bounding its results trimmed the ledger regardless of settlement,
and a restore rebuilds the fills not yet settled from the ledger it
travels in (FUT-015), so a sale trimmed before it settled was lost on a
restore and the restored run's settled cash and buying power differed
from the uninterrupted run's. Found in phase 3 while designing FUT-023.

The ledger's bound now widens, while any fill is unsettled, to keep the
oldest one and everything after it -- at most a settlement cycle's fills
more. No payload's form moves; a run bounding nothing, or holding nothing
unsettled, trims exactly as before. A regression test pauses and restores
a T+2 book under RetentionPolicy(results=1) at every boundary; 3 of 3
mutants caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…once (FUT-023)

A DailyInterest or DailyBorrowFee wraps a period with a DailyAccrual -- an
IANA zone, resolved by the one zone site, and a naive cutoff time -- and,
for interest, optional RateChanges effective from their day. The schedule
expands each into one ACCRUAL entry per calendar day at its cutoff, before
any posting at the same instant, and its posting; every cutoff precedes
the posting or the declaration is refused.

At each cutoff the day accrues on the book as the day leaves it: interest
on the trade-date or settled balance at that day's rates, a borrow fee on
each short at the run's mark and the fee in effect then, each day weighted
by what its day count counts it as (30/360 counting a 31st as no day). The
exact amount is an ACCRUAL ledger transaction moving no cash; the period
closes it and posts the sum once, rounded once -- nothing when it rounds to
nothing, and nothing twice. The ledger travels with the portfolio, so a
restore needs no new state, and a results bound keeps every open accrual
(ACC-015). The encoder writes a naive time of day as ISO 8601.

ScheduledInterest and ScheduledBorrowFee are unchanged, so a schedule
declaring neither daily form keeps its digest. 20 integration tests and a
settled-basis test; 14 of 14 mutants caught. Re-certified: only the source
digest, the manifest's digest and the export count (2,722) moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…FUT-025)

A PaidExchange states both of the issuer's instants for a HoldingExchange.
At the effective instant the holding becomes its legs, its gain is
realized, and the cash part and any cash in lieu become an entitlement:
EXCHANGE ledger transactions carrying the settlement metadata a fill
carries, and pending settlements in the FUT-015 index -- in trade-date
cash, out of buying power and settled cash -- until payment_at, when the
cash is paid. A receivable funds no order before then; a short holder owes
the cash part from the effective instant. A restore rebuilds the
entitlement from the ledger (an exchange's entry moving no units -- the
replay test found the rebuild booking the cash as units), and a results
bound keeps it while unpaid.

ExecutionPipeline, RunEngine and PortfolioEngine.apply_exchange take the
payment instant keyword-only, None by default, and refuse one before the
effective instant. A ScheduledExchange, and a PaidExchange paying at once,
book exactly what they did.

8 integration tests; 9 of 9 mutants caught. Re-certified: only the source
digest, the manifest's digest and the export count (2,723) moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s (FUT-026)

A ScenarioMargin declares repriced options, each with an OptionPricing --
a declared volatility, rate and carry (dividend yield, foreign rate or a
futures underlying's none) and, for an American contract, its lattice --
and each MarginScenario may declare volatility shifts. declare_margin_model
refuses an option it cannot value: no option terms, a package or quanto, a
Bermudan contract, an American one without a lattice, or a deliverable no
scenario moves.

The execution path supplies each repriced option's facts (OptionRisk) --
its terms in effect now, its underlying's price now, units per contract,
its currency's rate into the base -- for the book's margin, a
liquidation's sizing and an order's pre-trade projection alike. Under each
scenario the option is valued with its underlying moved and its volatility
shifted (Black-Scholes, the lattice, or intrinsic once expired) and loses
its model value's change, so a hedged book's gamma and vega are margined,
not only its delta. Until its underlying is priced an option is margined
by its own terms, never at a guessed price. Written to the model's
primitives only when declared; CarryKind pinned.

Profiled first: the margin check reads only the margined index; a tighter
scenario loop gained under 1%, and caching the check between steps is not
safe, so none is made.

21 integration tests; 14 of 14 mutants caught. Re-certified: only the
source digest, the manifest's digest and the export count (2,725) moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An ElectiveExchange offers a holding's strategies alternatives -- each a
complete HoldingExchange: stock, cash, a mix -- with the issuer's default,
an announcement instant, an election deadline, an effective instant, a
reference, and optionally a payment instant for its cash (FUT-025).

A strategy elects with IntentKind.ELECT (the alternative's index, the
action's reference in its metadata), validated and never sized: allocation
refuses one handed to it, and the pipeline records each as an ElectionMade
in the strategy runtime's log, persisted and pinned. At the effective
instant each holder's latest election naming the action and an offered
alternative, between announcement and deadline, is read: the holding is
exchanged by their common election, or by the default when one did not
elect or they disagree -- one position, one application. The announcement
reaches a strategy as the caller's data delivers it.

A TermsAdjustment may declare announced_at: a strategy's view of its
contract shows the adjustment from then, never before, while the execution
path applies it at its effective instant as before.

22 integration tests and an allocation test; 12 of 12 mutants caught.
Re-certified: only the source digest, the manifest's digest and the export
count (2,727) moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TradingControls declare references -- each a venue's BandReference for one
instrument, dated as published, a correction a later entry -- and
reference_bands: each a ReferenceBand rule, rates of the reference or
absolute offsets either side, refuse or cap, an optional staleness bound.
Two rules of one instrument at once, or a rule beside a declared band, are
refused.

The band in force is a declared band or the rule's band around the
reference in effect -- never a later one -- rounded inward to the tick grid
when declared, and read wherever a band is: at placement, at every order's
turn and in a strategy's price_band. With a rule in force and no current
reference the venue's limits are unknown: every order rests, as in a halt,
until one is published, and TradingControls.unbanded says why. Written to
the controls' primitives only when declared. The class is BandReference,
not ReferencePrice, which is execution quality's enum of benchmarks.

20 integration tests; 10 of 11 mutants caught, the eleventh equivalent
(routing's unrounded band is rounded onto the grid by EXE-013's cap).
Re-certified: only the source digest, the manifest's digest and the export
count (2,729) moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…T-027)

A TradingHalt declares auction=True when its venue reopens by auction,
written only when declared. At the instrument's first event after the
halt -- read from the market history the run keeps -- the orders that
rested through it cross at that event's opening print (a bar's open, a
quote's or trade's price): market orders, and limits the price reaches,
fill at it as makers, by time and ahead of any queue model, as far as the
fill policy gives them; a market order's remainder is withdrawn, and a
limit the price does not reach, or a stop, trades on continuously.
Outside a band in force the auction does not cross. An order placed after
the halt has its continuous turn -- a short-history test found it denied
one, fixed before commit. Imbalance reaches strategies as the caller's
observations. Opening and closing auction orders are unchanged.

9 integration tests; 10 of 10 mutants caught. The registry-read guard
counts the two new indexed reads of the controls, with its reason.
Re-certified: only the source digest and the manifest's digest moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-029)

IntentKind.AMEND names the quantity to leave working and terms of the
order's own type and time in force; validated, never sized. It applies to
the one order the strategy alone has working in the instrument, under
simulated routing, wherever a cancel is carried out. A reduction at the
same prices is made in place -- the OMS replaces the quantity, the
contribution and the reservation shrink -- keeping the order's id and so
its queue priority, the price-time rule. Any other amend withdraws the
order and places its replacement through allocation, risk and the OMS,
judged anew and at the back of its queue. What cannot be amended -- no
single order of the strategy's own, a netted or venue-held or algorithm-
worked order, other terms, a market order -- is an AmendRefused in the
strategy runtime's log, persisted and pinned.

All eight phase 2 entries are now delivered; the ROADMAP's Future work
says none is open, and the guard no longer expects an open entry.

11 integration tests and an allocation pin; 10 of 10 mutants caught.
Re-certified: only the source digest, the manifest's digest and the export
count (2,730) moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… purchases refused (ACC-016, ACC-017)

Found by the phase 3 gap audit.

ACC-016: a margin call deferred to any working order reducing the
position, so a strategy's bid resting far from the market kept a squeezed
short from ever being liquidated, and a liquidation sized as a target
counted that bid as held. FUT-022's buy-in, sized as a delta, was read
through the run's sizing model. Only a working market order now defers a
call or a buy-in, and every forced trade is a target naming the position
and the strategy's own working orders, so allocation trades exactly the
reduction required.

ACC-017: in a book settling several currencies, buying power sums them
all, so a purchase in a currency the book held too little of passed the
gate and stopped the run at the fill with InsufficientFundsError. A fully
paid purchase is now refused when placed if its own currency's cash --
less reservations, unreceived proceeds (FUT-025 entitlements included) and
working buys -- cannot pay it; the strategy hears why and the run goes on.
Cash is still never converted to pay it (ADR-0035). The multi-currency
refusal test is restated for the refusal's new point.

5 tests; 4 of 4 mutants caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two tests hold phase 3's capabilities against each other: a recall bought
in through a halt that reopens by auction while the short accrues its
day's fee, and an election's cash paid later while the balance accrues
interest day by day on the settled basis -- each replayed and resumed from
every pause to one result.

Example 77, a short squeezed by its lender: a recall from its notice, the
buy-in after its deadline resting through a halt and crossing at the
venue's reopening auction, a resting bid amended in place keeping its
priority, the day's borrow fee accrued at a declared cutoff; built on
public names only, held figure for figure by
tests/integration/test_squeezed_short_example.py and run from every built
artifact by tests/installed_smoke.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read after FUT-022 to FUT-029, against the brief's areas and against each
other. Its correctness fixes were committed as they were found (ACC-015,
ACC-016, ACC-017); the capability matrix now lists holding exchanges. Seven
entries are accepted, each with interim behaviour, completion criteria,
dependencies and a proposed horizon for the maintainer to confirm:
FUT-030 elections split between holders and proration, FUT-031 an atomic
amend, FUT-032 a dated volatility surface and scenario horizons, FUT-033 an
auction uncross and a retention-proof first event, FUT-034 failed or
corrected payments and legs delivered later, FUT-035 a buy-in premium and
per-strategy borrow, FUT-036 a capital ceiling's reservation reduced in
place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sections 1 to 9 still describe the candidate validated at 350f3f7; a new
section 10 records phase 3 apart from it: the eight FUT-022 to FUT-029
commits and the three correctness fixes with their evidence, FUT-030 to
FUT-036 accepted, and the validation on b600ba7 -- 10,562 tests on a fresh
CPython 3.12, 3.13 and 3.14, 77 examples, both artifacts built,
twine-checked and smoke-tested alone, the v4.1.0 payload resuming to its
reference, the run benchmarks level with v4.1.0, the declared paths'
cost measured, 100 of 101 mutants caught -- and the recommendation that
the maintainer release the branch with phase 3 rather than 350f3f7.
README's test and example counts brought current.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VarunSingh022 and others added 11 commits November 1, 2026 09:39
…order lost (FUT-031)

An amend that replaces an order -- more quantity, another limit or stop --
is now allocated and routed against a trial book with the original
withdrawn, and the trial is kept only if the replacement is placed.
Refused by allocation, risk or the venue's rules, the book is the one
before the amend: the original working, its contribution and reservations
held. The refusal is an AmendRefused naming why; allocation's and risk's
decisions on the replacement stay on their records.

The replacement is a target naming the strategy's position and its other
working orders, so it is exactly the quantity asked and never read
through the sizing model. Cancels in a batch run before its amends, so a
cancelled or filled order is not amended. An order held at a venue is
left to LiveSession.amend, with no atomicity promised there.

Restates test_an_increase_is_judged_by_risk_as_a_new_order, which pinned
the FUT-029 behaviour of withdrawing the original. 12 new or restated
tests; 9 of 9 mutants caught; no result identity moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dds (FUT-036)

An in-place amend now cuts each of the order's reservations to the share
the smaller order still adds.

- The account's reservation is cut by what the order adds to the
  committed position (filled plus every other working order).
- A ceilinged strategy's reservation is cut by what it adds to the
  strategy's position and working share, so freed headroom admits
  another order in the same batch.

An order partly covering a short keeps only its tail. One the book has
since covered keeps the share of its quantity, since the book may move
back.

StrategyCapital.reduce frees the rest: it never reserves, never goes
below zero, and keeps the total in step. Increases go through FUT-031's
trial, so a refused one keeps the original's share.

Under FUT-029 the account's share was scaled by quantity and the
ceiling's held whole until the order ended. 8 integration tests; 8 of 8
mutants caught; no result identity moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hed book (FUT-033)

A halt declaring an AuctionUncross prices its reopening from the
AuctionInterest the venue published during the halt and the run's orders
that waited.

- Price: the price matching the most volume, then the smallest
  imbalance, then a declared tie-break (highest, lowest, or nearest the
  reference in effect).
- Fills: only the run's orders, by price priority with market first,
  then at the marginal price behind the published interest (the
  default) or pro rata with it, rounded to the trading step.
- Nothing crosses without a book, without a match, or outside the band,
  and no price is invented.
- A declared extension holds an out-of-band auction over once. Nothing
  trades meanwhile, and the strategy is told the instrument is halted.

Each instrument's reopening is an AuctionReopening on ExecutionState,
recorded at the first event after the halt and persisted. The pipeline
reads it instead of the market history, so a retention bound no longer
mistakes a later event for the first (FUT-027's caveat). It is written
only when a run holds an auction, so no other identity moves, and the
pipeline state keeps its 16 fields.

30 integration tests; 15 of 15 mutants caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… later word (FUT-034)

A PaymentAmendment names one currency's payment of a declared exchange
and what became of it:

- DELAYED: every awaited part is pending until the new instant. A delay
  known after the clock paid it re-opens the payment.
- FAILED: what is owed is written off and never becomes settled cash.
- CORRECTED: paid per unit exchanged; the difference is booked.
- REVERSED: clawed back. Refused for a payment not yet made.
- REPLACED: paid in another currency at its own instant.

Each amendment is an EXCHANGE ledger entry of its own, naming the
entitlement and the entries it re-dates or closes, plus a PaymentAmended
event. The original is never rewritten, and a restore rebuilds the
pending index by the same rule the run keeps it by. Buying power,
settled cash, settled-basis interest and margin equity read the amended
book.

entitlements(ledger, at) reconciles each payment from the ledger as of
any instant. These are refused when declared: two words on one payment
at one instant, a word after a failure or reversal, and a word naming
no declared exchange. Legs delivered after the effective instant remain
out of scope.

27 integration tests; 10 of 10 mutants caught; no result identity
moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rategy (FUT-035)

A borrow entry can declare a lender's charges and its locates.

Charges:
- buy_in_premium is a fraction of the bought-in value, booked as a fee
  with each fill of the cover.
- buy_in_fee is charged once per buy-in order, even when a venue fills
  it in parts.
- Both are attributed to the strategies the cover was placed for.
- Nothing is charged that is not declared.

Locates:
- allocations names the units located for each strategy, never more
  than is available, so no unit is claimed twice.
- Beyond its own, a strategy draws only on what is shared, first come,
  with its resting shorts counted.
- A recall buys each strategy in for its own excess, not in proportion
  to every short.

Netting stays the account's: an entry locating nothing is the book's
borrow, as before. borrow_claims attributes the borrow, and a fee on
it, to each strategy. New fields are written only when declared; no
result identity moves.

18 integration tests; 11 of 11 mutants caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…roration (FUT-030)

Each holder of an elective action's holding is now exchanged by its own
election, applied to its part of the one position.

- A part election (ELECT with metadata['units']) is a UnitsElected.
- A whole election replaces every earlier one; a part election
  replaces the earlier one for its alternative.
- What a holder does not elect, or elects beyond its holding, goes to
  the default. No preference is inferred.
- A declared record_at records each holder's holding then
  (HoldersRecorded) and bounds its election by it.
- A declared proration factor accepts that fraction of each election,
  in whole units where the share is whole; the default takes the rest.

Each alternative goes through the one exchange path,
apply_exchange(part=), with the basis apportioned by units and the rest
of the position kept. Each part gets its own legs, cash in lieu,
realized P&L and shares.

One holder, or holders agreeing, still exchange the holding whole and
are unchanged. Holders electing apart, who under FUT-024 all took the
default, each take their own, and that test is restated.

21 integration tests; 10 of 10 mutants caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four boundaries of what phase 4 delivered, each verified against the
code and accepted as future work with its limitation, impact, reason,
interim behaviour, horizon, dependencies and completion criteria:

- FUT-037: legs delivered after an exchange's effective instant.
- FUT-038: an elective action's terms corrected after announcement.
- FUT-039: volatility surfaces delivered as observations in a run.
- FUT-040: a strategy's view of a reopening auction's published book.

Each is on ROADMAP's Future work; the horizons are the maintainer's to
confirm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ver a horizon (FUT-032)

A scenario margin model can now declare dated implied-volatility
surfaces: the options package's VolatilitySurface, by strike and
expiry, one per underlying per as-of instant. It reuses that surface
rather than adding a second one.

- A repriced option declaring volatility=None reads the latest surface
  as of the margined instant, never a later one, through
  implied_vol_across_expiries. That is linear between strikes and in
  total variance between expiries.
- Where the surface refuses (beyond the quotes, a missing bracket, a
  calendar arbitrage) or a scenario shift would take the volatility to
  zero or below, the option is margined by its own terms. Nothing is
  extrapolated and no volatility assumed.
- A declared horizon values each scenario at the instant plus it, so
  the loss counts the options' decay.
- Surfaces and the horizon are written to the registry snapshot only
  when declared; a model without them keeps its form.

An 80-option book takes 1.3 ms per margin computation with declared
volatilities and 1.8 ms reading surfaces.

21 integration tests; 6 of 6 mutants caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seven scenarios drive phase 4's capabilities against each other on the
canonical path:

- holders electing apart, one prorated, the cash delayed by the issuer
  and reconciled as of each instant;
- an amend in a price-time queue freeing its ceiling, and a refused
  increase keeping its place and reservation;
- a computed reopening extended past its reference band, then crossing
  within it;
- recalls competing across located strategies, each bought in for its
  own and charged its own premium;
- a failed payment funding nothing and leaving no collateral;
- a volatility spike on a later surface calling the margin and
  liquidating, the call read from that surface;
- the split, prorated and delayed tender resumed from every pause to
  one result, each input applied once.

Example 78 shows a tender split between holders, prorated, delayed and
corrected, using the public API. It is held figure for figure by its own
test and run from every built artifact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…held (REP-006)

The phase 4 global gap audit found that a run's manifest could not
identify the instrument registry it was given. Records, venue controls,
borrow, the margin model and its volatility surfaces are a live object
the record names by type only (ADR-0023). So two runs given different
terms shared a manifest wherever their records agreed.

manifest_for_run(instruments=True) now records instruments_digest, the
SHA-256 of the registry the run held as its snapshot writes it, read
off the run and never supplied. canonical_manifest_key renders it only
when present, verify_manifest recomputes it, and a manifest comparison
names it. Asked for nothing, a manifest is exactly the one it was, so
no existing manifest identity moves.

The gaps report gains the phase 4 audit's findings, and ADR-0051
decision 42 records this one. 3 integration tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Section 11 records phase 4 from bbc4085: each milestone with its
commit, tests and mutation evidence, the two behaviour changes and their
restated tests, the options surface reused rather than duplicated, the
REP-006 fix, and FUT-037 to FUT-040 left open.

Validation is on the final tree: 10,713 tests on fresh CPython 3.12,
3.13 and 3.14, all 78 examples, the certificate, strict twine and the
installed preflight. Also covered: v4.1.0 payload compatibility,
benchmarks level with v4.1.0, and the profiled declared-feature cost.

README states 10,713 tests and 78 examples. The installed smoke check's
summary now names the example 78 it already ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant