Repository navigation
AlphaLab: integrate research workflows and global derivative lifecycle - #18
Draft
VarunSingh022 wants to merge 41 commits into
Draft
VarunSingh022 wants to merge 41 commits into
VarunSingh022 wants to merge 41 commits into
Conversation
…(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>
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR delivers the v4.1.1 future-readiness program as one commit,
dc508e9, onwork/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 still4.1.0because changing it is the maintainer's decision (section 9 ofdocs/audit/FUTURE_READINESS_V4_1_1.md).Draft, not for merge. Hosted CI has not run on this branch.
Milestones and engine changes
RunScheduleandScheduleCursorcover 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.combine_datasetsgives several datasets read by one run a single derived identity.observationswork onbacktestandreplay. A replay merges observations through the same public functions as a backtest. Two datasets that price one instrument at the same instant are refused.FUT-009: algorithmic execution through backtest and replay
ExecutionPlangiven toBacktestEngine.run,ReplayBacktest.runor the facade routes the runALGORITHMIC.VenueCheck.ExecutionPipeline.work_childunder the run's fill policy, simulator and cost model, and settles on its parent through the canonical report path.NEXT_EVENTa child trades only after its release instant, and never outside its slice or window.AlgorithmBookpersists in replay snapshot schema 2 and is checked against its plan. The manifest recordsexecution_plan_id.AlgorithmicResultcarries the book. Example 74 andbenchmarks/benchmark_algorithmic_execution.pyare new.route_child_ordersends aPaperBroker, 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.termsholdsOptionTermsorFutureTerms, outside the identity key (instrument schema 4). They declare:AMERICANorEUROPEANexercise, with its window;CASHorPHYSICALsettlement against oneDeliverableof any quantity per contract;AutomaticExercisethreshold.Operations.
Exercise,AssignmentandOptionExpiryare both schedule entries andRunEngine/ExecutionPipelineoperations.Calendars.
session_closeandsettles_atturn a declaredMarketCalendarandSettlementRuleinto 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.pyruns 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 intest_lifecycle_conventions.pyand 4 intest_lifecycle_example.py.Exercise and assignment
Settlement
max(S × units × m_deliverable / m − K, 0)against a supplied settlement value. This covers puts, calls and options on futures (deliverable multiplier).strike × multiplierthrough the portfolio's fill authority. The delivered position is re-marked at the market's price, checked at the delivery instant.AssetType.INDEXserves 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
Example 75 (a London calendar with a holiday) covers:
result_id.Sentiment and macro acceptance scenario
examples/72_compose_a_multi_asset_strategy.pyis held bytest_multi_asset_composition.py(29 tests) and is run from every built artifact.Inputs
CombinedDataset.collected_at: a late-collected post, an undated post, a repeat, an unreadable row, all out of order.What the tests establish
PYTHONHASHSEEDs.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.
ruff check,ruff format --checkmypy .(strict, including examples and benchmarks)twine check --strict, release preflightcertify_release.py --checkhistorical_inventory_v4.py --checkgit diff --checkKnown failing tests
These are not hidden, and no test was weakened.
tests/regression/test_api_changes_are_in_the_changelog.py::test_every_removed_or_changed_name_is_in_the_changelogis version-keyed. It needs the version bump.tests/regression/test_nothing_is_left_for_later.py::test_no_ledger_entry_is_assigned_beyond_this_releaseis version-keyed. It refusesv4.2.0ledger entries while the package says 4.1.0.tests/unit/test_package_metadata.py::test_package_exposes_versionfails 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)
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
dist/is untouched.🤖 Generated with Claude Code