diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc57654..c5db4a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,21 +17,35 @@ This changelog starts from the clean Core package baseline.
- Deterministic `run_core_step` and `run_core_wakeup_step` architecture.
- CoreWakeupStep final Strategy evaluation: reduce all entries, then `CoreWakeupStrategyEvaluator` once.
- Canonical Event input models and `EventStreamEntry`/`ProcessingPosition`.
-- Intent Pipeline candidate records with dominance/reconciliation.
+- Intent pipeline candidate records with dominance/reconciliation.
- Risk Engine (policy-only) admission and Execution Control plan/apply integration.
- `CoreStepResult.dispatchable_intents` and `ControlSchedulingObligation` outputs.
- Core-only quickstart example and focused semantics test coverage.
- Root export of `PolicyIntentEvaluator` and documentation of extension points vs convenience implementations.
- Pipeline integration tests for `RiskEngine` as `policy_evaluator` in `run_core_step`.
-- `FillEvent` reducer and Pipeline tests.
+- `FillEvent` reducer and pipeline tests.
- Runnable Risk Engine example at `examples/core_step_with_risk_engine.py`.
- Extension-point docs under `docs/` and U3 candidate list at `docs/roadmap/dead-code-cleanup-candidates.md`.
+- Explicit terminal canonical Order lifecycle Events:
+ `OrderCanceledEvent`, `OrderRejectedEvent`, and `OrderExpiredEvent`.
+- Terminal lifecycle reduction behavior and focused semantics tests for
+ deterministic working-order removal, canonical projection terminal state, and
+ inflight cleanup.
### Changed
- Package metadata, exports, and docs reset for standalone Core library identity.
- Pydantic models established as contract source of truth across public API docs.
-- README clarifies internally wired Pipeline vs externally supplied extension points.
+- README clarifies internally wired pipeline vs externally supplied extension points.
+- Runtime/Core contract wording hardened with a normative Runtime-obligation
+ matrix, explicit `CoreStepResult` output semantics, and clearer
+ `ControlSchedulingObligation` vs `ControlTimeEvent` boundary language.
+- Canonical `MarketEvent` contract wording now explicitly documents the current
+ book-only reduction baseline; trade-shaped payloads are explicitly unsupported
+ for canonical reduction in this slice.
+- Public API docs/tests hardening for Core-F2: root Public API classification,
+ Advanced API labeling, root-vs-non-root clarity, and explicit surface lock
+ coverage for all current root exports. No exports were removed or renamed.
### Removed
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4a74f28..1f984aa 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -48,7 +48,7 @@ python -m build
- Register canonical category handling in `core/domain/event_model.py`.
- Update canonical reduction behavior in `core/domain/processing.py`.
-### CoreStep/CoreWakeupStep Pipeline
+### CoreStep/CoreWakeupStep pipeline
- Update `core/domain/processing_step.py` for deterministic flow changes.
- Keep reconciliation/policy/apply transitions explicit and side-effect-safe.
diff --git a/README.md b/README.md
index f0488ad..4a9e380 100644
--- a/README.md
+++ b/README.md
@@ -62,10 +62,10 @@ stay in the Runtime, Venue Adapter, and Venue—not in Core.
| What you get | Why it matters |
| --- | --- |
-| One deterministic Core Pipeline | Same Event-step path for reduction → evaluation → candidates → Risk Engine → Execution Control apply |
+| One deterministic Core pipeline | Same Event-step path for reduction → evaluation → candidates → Risk Engine → Execution Control apply |
| Canonical Event input model (`EventStreamEntry`) | Aligns with Event Stream + Processing Order; State is `f(Event Stream, Configuration)` |
| Strategy output as Intents | Internal, order/Venue-agnostic commands before Venue Adapter-specific shapes |
-| Risk Engine separated from Execution Control | Risk Engine (policy) vs Queue / scheduling / rate-aware presentation split, as in the Intent Pipeline (Strategy → Risk → Queue → Adapter) |
+| Risk Engine separated from Execution Control | Risk Engine (policy) vs Queue / scheduling / rate-aware presentation split, as in the Intent pipeline (Strategy → Risk → Queue → Adapter) |
| `dispatchable_intents` + optional Control Scheduling Obligation | Runtime performs Execution and may inject canonical `ControlTimeEvent` when a **rate-limit** obligation is realized ([`docs/flows/control-time-and-scheduling.md`](docs/flows/control-time-and-scheduling.md)); inflight deferral does not emit that obligation by default |
Core is designed to reduce decision-logic drift between Backtesting
@@ -103,7 +103,7 @@ flowchart TB
Core never replaces the Runtime: the Runtime is responsible for feeding canonical
Events and for turning `dispatchable_intents` into Venue traffic (and for everything
Kubernetes, credentials, and operations-related). What stays stable is the Core
-Pipeline and contracts; what varies by design is Runtime choice, Venue Adapter,
+pipeline and contracts; what varies by design is Runtime choice, Venue Adapter,
Venue, and deployment.
@@ -133,7 +133,7 @@ Run the quickstart
python examples/core_step_quickstart.py
```
-Optional Risk Engine policy example (same Pipeline, built-in policy):
+Optional Risk Engine policy example (same pipeline, built-in policy):
```bash
python examples/core_step_with_risk_engine.py
@@ -170,9 +170,9 @@ Planned U3 cleanup candidates: [`docs/roadmap/dead-code-cleanup-candidates.md`](
-## Full Pipeline
+## Full pipeline
-Internal processing Pipeline, in sequential order:
+Internal processing pipeline, in sequential order:
```text
Runtime reduces to canonical Events
@@ -189,11 +189,42 @@ Runtime reduces to canonical Events
Runtime dispatches Intents into Orders
```
+Runtime/Core contract matrix (Core perspective): see
+[`docs/reference/events-reference.md`](docs/reference/events-reference.md)
+for the normative Runtime/external outcome -> canonical Event injection table and
+reducer effects.
+
+Terminal Order lifecycle baseline in this Core slice:
+
+- Core now accepts explicit canonical terminal lifecycle Events:
+ `OrderCanceledEvent`, `OrderRejectedEvent`, and `OrderExpiredEvent`.
+- Runtime owns venue I/O and must inject these canonical Events when terminal
+ outcomes are confirmed/reported in the Event Stream.
+- Terminal Event reduction removes active working-order projections, updates
+ canonical order projection state, and clears inflight tracking for
+ `instrument + client_order_id`.
+- `OrderRejectedEvent` is an Order lifecycle execution outcome and is distinct
+ from Policy Admission rejection in the Intent pipeline.
+- Dispatch failure before submission is not automatically `OrderRejectedEvent`.
+- `OrderExecutionFeedbackEvent` is account-level feedback and does not replace
+ `FillEvent` or terminal lifecycle Events.
+- This slice does not add `OrderAcceptedEvent` and does not introduce a full
+ order state machine framework.
+
+Current Market Event baseline contract:
+
+- Core canonical reduction supports book-shaped `MarketEvent` payloads for
+ Market State reduction.
+- Trade-shaped `MarketEvent` payloads are not reduced by the current baseline
+ and are explicitly rejected at canonical processing boundaries.
+- Runtime may normalize venue data into canonical Events, but Core accepts only
+ the documented canonical reduction contract.
+
## Internally wired vs externally supplied
-The clean Core Pipeline is always the same shape; some pieces run inside Core
+The clean Core pipeline is always the same shape; some pieces run inside Core
when you call step APIs, and others must be supplied by your Runtime or tests.
### Internally wired (always part of Core when you call step APIs)
@@ -207,6 +238,7 @@ when you call step APIs, and others must be supplied by your Runtime or tests.
### Externally supplied extension points
- **Strategy evaluator** — `CoreStepStrategyEvaluator` or `CoreWakeupStrategyEvaluator`
+ receives a read-only `StrategyStateView` via `context.state` and returns Intents
- **Policy evaluator** — any object implementing `PolicyIntentEvaluator` (passed via `CorePolicyAdmissionContext`)
- **Execution Control** — `ExecutionControl` instance (passed via `CoreExecutionControlApplyContext`)
- **Configuration** — optional `CoreConfiguration` for positioned market reduction
@@ -223,6 +255,9 @@ The minimal quickstart uses an inline allow-all policy to stay small. That does
built-in Risk Engine policy behavior. See `examples/core_step_quickstart.py` (minimal) and
`examples/core_step_with_risk_engine.py` (Risk Engine variant).
+Strategy code reads State and returns Intents. It must not mutate Core-owned
+State, Queue/inflight substate, or reducer-managed data.
+
## Input / Core / Output / Not Owned By Core
@@ -256,7 +291,7 @@ work is reconsidered after canonical execution Events update State. Runtimes mus
not flush Core Queues outside the normal `run_core_step` / Execution Control apply
path. See [`docs/flows/control-time-and-scheduling.md`](docs/flows/control-time-and-scheduling.md).
-In short: one Pipeline, canonical Events, Intents inside Core, policy vs Execution
+In short: one pipeline, canonical Events, Intents inside Core, policy vs Execution
Control split, dispatchable Intents plus optional Control Scheduling Obligation for
the Runtime, and a boundary that makes parity and testing practical—not a second
copy of decision logic per environment.
diff --git a/SECURITY.md b/SECURITY.md
index aa5e6af..5228104 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -25,7 +25,7 @@ Include:
This policy covers the Core package in this repository, including:
- canonical Event and Intent contracts
-- deterministic CoreStep/CoreWakeupStep decision Pipeline
+- deterministic CoreStep/CoreWakeupStep decision pipeline
- package integrity and dependency usage in `tradingchassis_core`
## Secrets and Credentials Policy
diff --git a/docs/code-map/core-pipeline-map.md b/docs/code-map/core-pipeline-map.md
index ca5336e..eb53918 100644
--- a/docs/code-map/core-pipeline-map.md
+++ b/docs/code-map/core-pipeline-map.md
@@ -1,6 +1,6 @@
-# Core Pipeline Map
+# Core pipeline Map
-This map captures the only supported deterministic decision Pipeline for
+This map captures the only supported deterministic decision pipeline for
TradingChassis Core.
## Step-by-step flow
@@ -17,7 +17,7 @@ TradingChassis Core.
in the current slice—see `../flows/control-time-and-scheduling.md`).
9. Runtime can dispatch later and inject further canonical Events (including
`ControlTimeEvent` when an obligation is realized); Core does not perform
- external dispatch or mutate Queues outside this Pipeline.
+ external dispatch or mutate Queues outside this pipeline.
## Core APIs
@@ -46,7 +46,7 @@ Wakeup flow:
2. `run_core_wakeup_reduction` calls `process_event_entry` for each entry in order.
3. `CoreWakeupStrategyEvaluator.evaluate` runs **once** on the fully reduced State
(`CoreWakeupStrategyContext` carries all entries).
-4. `run_core_wakeup_decision` snapshots queued intents once, combines generated + queued
+4. `run_core_wakeup_decision` snapshots queued Intents once, combines generated + queued
once, applies dominance/reconciliation once, Policy Admission once, and
Execution Control plan/apply once.
5. `CoreStepResult.dispatchable_intents` is returned; Runtime dispatches later.
diff --git a/docs/code-map/repository-map.md b/docs/code-map/repository-map.md
index d3c3d34..e506469 100644
--- a/docs/code-map/repository-map.md
+++ b/docs/code-map/repository-map.md
@@ -6,7 +6,7 @@ High-level map for the standalone Core package.
- `tradingchassis_core/__init__.py`: public package boundary exports
- `tradingchassis_core/core/domain/`: canonical contracts and deterministic
- Pipeline orchestration
+ pipeline orchestration
- `tradingchassis_core/core/risk/`: policy-only Risk Engine evaluator/config
- `tradingchassis_core/core/execution_control/`: Execution Control primitives
- `tradingchassis_core/core/events/`: Event bus/sink utilities (`NullEventBus`; `LoggingEventSink` for Runtime)
diff --git a/docs/flows/control-time-and-scheduling.md b/docs/flows/control-time-and-scheduling.md
index 0d13594..3c3a7d9 100644
--- a/docs/flows/control-time-and-scheduling.md
+++ b/docs/flows/control-time-and-scheduling.md
@@ -11,12 +11,13 @@ to Execution Control deferral.
canonical Event Stream and does not mutate `StrategyState`.
- **Control-Time Event** (`ControlTimeEvent`) — Canonical **control** category Event. It becomes part of the
deterministic history only after the **Runtime** injects it as
- `EventStreamEntry` input (same ingestion path as other canonical Events).
+ `EventStreamEntry` input (same ingestion path as other canonical Events). It is
+ sparse/deadline-style control feedback, not a periodic tick requirement.
- **Inflight** — Core-side **Intent-operation** gating: a sendability / operation
slot (for example keyed by `client_order_id`) is occupied because an earlier
Intent operation is still awaiting **canonical execution feedback**. This is
not the same as venue-side “order ownership”; Core models sendability for the
- decision Pipeline.
+ decision pipeline.
- **Rate-limit deferral** — Execution Control blocks dispatch because the
configured **token / time budget** for orders or cancels is not yet available at
the apply clock (`now_ts_ns_local` in `CoreExecutionControlApplyContext`).
@@ -37,12 +38,17 @@ or “synthetic” obligations for inflight-only waits.
`ControlTimeEvent`. Obligations are for **rate-limit** rechecks in the current
Core slice.
-## Clean Core Pipeline (unchanged)
+`ControlSchedulingObligation` is produced in Execution Control **apply** for
+time-dependent rate-limit deferral. It does not mutate Strategy State and does
+not enter the Event Stream directly. Runtime may realize it later by injecting
+a canonical `ControlTimeEvent`.
+
+## Clean Core pipeline (unchanged)
1. `EventStreamEntry`
2. `process_event_entry` / `process_canonical_event`
3. Strategy evaluator
-4. generated intents
+4. generated Intents
5. candidate records + dominance / reconciliation
6. policy admission
7. Execution Control plan / apply
diff --git a/docs/how-to/update-core-step-pipeline.md b/docs/how-to/update-core-step-pipeline.md
index e888ee0..0364229 100644
--- a/docs/how-to/update-core-step-pipeline.md
+++ b/docs/how-to/update-core-step-pipeline.md
@@ -1,4 +1,4 @@
-# How To Update CoreStep Pipeline Behavior
+# How To Update CoreStep pipeline Behavior
Core step orchestration lives in
`tradingchassis_core/core/domain/processing_step.py`.
@@ -19,7 +19,7 @@ Recommended workflow:
Guardrails:
-- No Runtime dispatch logic in Core Pipeline code.
+- No Runtime dispatch logic in Core pipeline code.
- No legacy compatibility contract restoration.
- Keep deterministic behavior and public API coherence.
diff --git a/docs/index.md b/docs/index.md
index ea5cc1e..5de0a58 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -7,7 +7,7 @@ This documentation set describes the standalone clean Core package baseline.
- `reference/public-api.md`: supported root exports and package boundary
- `reference/events-reference.md`: canonical Events and Intent contracts
- `flows/control-time-and-scheduling.md`: rate-limit vs inflight deferral and obligations
-- `code-map/core-pipeline-map.md`: deterministic Pipeline walkthrough
+- `code-map/core-pipeline-map.md`: deterministic pipeline walkthrough
- `code-map/repository-map.md`: package layout and ownership map
- `how-to/add-canonical-event.md`: extending canonical Event contracts
- `how-to/update-core-step-pipeline.md`: changing CoreStep/CoreWakeupStep behavior
@@ -20,7 +20,7 @@ This documentation set describes the standalone clean Core package baseline.
TradingChassis Core is a deterministic trading decision engine library. It owns
canonical contracts, State reduction, and step-level decision outputs.
-## Clean Core Pipeline
+## Clean Core pipeline
1. `EventStreamEntry`
2. `process_event_entry` / `process_canonical_event`
diff --git a/docs/reference/events-reference.md b/docs/reference/events-reference.md
index f5a7e20..5df2ed0 100644
--- a/docs/reference/events-reference.md
+++ b/docs/reference/events-reference.md
@@ -5,20 +5,82 @@ contracts. Pydantic models are the schema source of truth.
## Canonical Event Models
-- `MarketEvent`: book/trade market data input for State reduction
+- `MarketEvent`: book market data input for Market State reduction in the current Core baseline
- `ControlTimeEvent`: canonical **control** wakeup; becomes stream history only
after Runtime injection. Reducer updates monotone time (and processing cursor
when positioned). Scheduling **obligations** are a separate non-canonical output;
see `../flows/control-time-and-scheduling.md`.
- `OrderSubmittedEvent`: canonical submitted-order acknowledgement
-- `OrderExecutionFeedbackEvent`: canonical account/execution feedback
+- `OrderCanceledEvent`: canonical terminal lifecycle feedback for a canceled Order
+- `OrderRejectedEvent`: canonical terminal lifecycle feedback for a rejected Order
+- `OrderExpiredEvent`: canonical terminal lifecycle feedback for an expired Order
+- `OrderExecutionFeedbackEvent`: canonical account feedback (account/position/balance projection)
- `FillEvent`: canonical fill lifecycle update
+## Runtime obligation matrix (Core perspective)
+
+Core is deterministic reduction and decision logic. Runtime owns Venue I/O,
+adapter dispatch, and canonical Event injection into the Event Stream.
+
+Core never performs external dispatch. Runtime dispatches
+`CoreStepResult.dispatchable_intents` later and injects canonical feedback Events.
+
+| Runtime / external outcome | Canonical Event to inject into Core | Core reducer effect | Notes / non-goals |
+| --- | --- | --- | --- |
+| Book market data input | `MarketEvent` | Updates Market State projection and monotone local time | Current Core baseline is book-only; trade-shaped `MarketEvent` is rejected for canonical reduction. |
+| Rate-limit recheck obligation becomes due | `ControlTimeEvent` | Canonical control-time reduction updates monotone local time | `ControlSchedulingObligation` is non-canonical output and does not enter the Event Stream directly. |
+| Successful external NEW dispatch | `OrderSubmittedEvent` | Creates/updates active Order projection and clears inflight for `instrument + client_order_id` | Submission acknowledgement boundary for NEW dispatch outcomes. |
+| Authoritative fill feedback | `FillEvent` | Updates fill history, cumulative fill quantity, and Order fill progression | Runtime fill ingress is environment-specific and can be deferred by Runtime capability tracks. |
+| Account/execution snapshot feedback | `OrderExecutionFeedbackEvent` | Updates account projection only | Not a replacement for `FillEvent`; does not encode terminal Order Lifecycle status. |
+| Cancel confirmation / terminal cancel outcome | `OrderCanceledEvent` | Removes active working Order, sets terminal canonical projection (`canceled`), clears inflight | Terminal Order Lifecycle feedback path. |
+| Venue/order rejection after dispatch | `OrderRejectedEvent` | Removes active working Order, sets terminal canonical projection (`rejected`), clears inflight | Distinct from Policy Admission rejection (pre-dispatch). |
+| Order expiry | `OrderExpiredEvent` | Removes active working Order, sets terminal canonical projection (`expired`), clears inflight | Terminal lifecycle closure from runtime feedback. |
+
+Explicit contract notes:
+
+- `CoreStepResult.generated_intents`, `candidate_intents`,
+ `candidate_intent_records`, and `core_step_decision` are introspection /
+ diagnostic outputs, not external dispatch obligations.
+- Dispatch failure before submission is not automatically `OrderRejectedEvent`.
+ `OrderRejectedEvent` is a terminal Order Lifecycle feedback Event after
+ dispatch.
+- Policy Admission rejection is not `OrderRejectedEvent`; Policy Admission
+ occurs before dispatch in the Order Intent pipeline.
+- `mark_intent_sent` is not part of the public Runtime/Core contract boundary.
+- Runtime loop progress/no-spin behavior, pending scheduling lifecycle,
+ hftbacktest behavior, and recorder behavior are runtime-repository concerns,
+ not Core contract behavior.
+
+Terminal lifecycle reducer contract in this Core baseline:
+
+- `OrderCanceledEvent`, `OrderRejectedEvent`, and `OrderExpiredEvent` update
+ `StrategyState` deterministically by:
+ - removing the Order from active working-order projections;
+ - updating canonical order projection state (`"canceled"`, `"rejected"`, or
+ `"expired"`);
+ - clearing inflight tracking for `instrument + client_order_id`.
+- Terminal Event reduction is idempotent and non-crashing for unknown orders:
+ Core records terminal canonical projection state when no active working order
+ exists.
+- Order rejection (`OrderRejectedEvent`) is an execution-side Order lifecycle
+ outcome and is distinct from Policy Admission rejection (which occurs before
+ dispatch in the Intent pipeline).
+
Canonical ingestion boundary:
- `process_canonical_event(state, event, ...)`
- `process_event_entry(state, EventStreamEntry(...), ...)`
+### MarketEvent baseline contract
+
+In the current Core baseline, canonical reduction supports only book-shaped
+`MarketEvent` payloads (`event_type="book"` with book levels).
+
+Trade-shaped `MarketEvent` payloads are reserved in the schema but are not part
+of the supported canonical reduction contract in this baseline. If a trade-shaped
+`MarketEvent` reaches canonical reduction, Core rejects it with explicit
+validation error behavior.
+
## Processing Order Models
- `ProcessingPosition`
diff --git a/docs/reference/public-api.md b/docs/reference/public-api.md
index ccecdb6..5ba9edf 100644
--- a/docs/reference/public-api.md
+++ b/docs/reference/public-api.md
@@ -1,109 +1,179 @@
# Public API Reference
-The public package boundary is the `tradingchassis_core` root import.
+The Public API boundary is the `tradingchassis_core` root import.
-## Internally wired vs externally supplied
+This page classifies root exports into:
-### Internally wired (when step APIs run)
+- Public Data Model
+- Public Extension Point
+- Public Orchestration API
+- Public Convenience Implementation
+- Advanced API
-These run inside Core when you call `run_core_step` / CoreWakeupStep APIs (no substitute
-implementation required):
+`ControlSchedulingObligation` is a non-canonical output. `ControlTimeEvent` is the
+canonical Event symbol for the Control-Time Event concept.
-- `process_event_entry` / `process_canonical_event` and canonical reducers
-- Candidate combination, dominance, and reconciliation
-- Policy Admission **mechanism** when `CorePolicyAdmissionContext` is provided
-- Execution Control plan/apply **mechanism** when policy + apply contexts are provided
-- `CoreStepResult` / `CoreStepDecision` production
+## Public Orchestration API
-### Externally supplied extension points
+Primary orchestration entrypoints:
-| Symbol | Role |
-| --- | --- |
-| `CoreStepStrategyEvaluator` / `CoreWakeupStrategyEvaluator` | Strategy evaluation |
-| `PolicyIntentEvaluator` | Policy Admission (`evaluate_policy_intent`) via `CorePolicyAdmissionContext` |
-| `ExecutionControl` | Queue/rate/inflight apply via `CoreExecutionControlApplyContext` |
-| `CoreConfiguration` | Optional instrument metadata for positioned market reduction |
-| `EventBus` / `NullEventBus` | `StrategyState` requires a bus; use `NullEventBus` for standalone Core |
+- `run_core_step`
+- `run_core_wakeup_step`
+- `process_event_entry`
+- `process_canonical_event`
-### Convenience implementations (optional)
+Advanced orchestration entrypoints (split CoreWakeupStep flow):
-| Symbol | Role |
-| --- | --- |
-| Risk Engine (`RiskEngine`) | Built-in `PolicyIntentEvaluator` (not wired by default) |
-| `ExecutionControl` | Default Execution Control apply implementation (you still supply an instance) |
-| `NullEventBus` | Discards events for tests and examples |
+- `run_core_wakeup_reduction`
+- `run_core_wakeup_decision`
-**Internal (not public extension points):** `RiskPolicy`, `ExecutionConstraintsPolicy`,
-and other modules under `core/risk/` except `RiskEngine` / `RiskConfig`.
+Core step orchestration is deterministic: Runtime provides canonical Event Stream
+input, Core reduces canonical Events, evaluates Strategy, applies Policy Admission
+and Execution Control, and returns `CoreStepResult`.
-Examples:
+Primary integrations should start with `run_core_step` or `run_core_wakeup_step`.
+Split APIs are public Advanced API for diagnostics, testing, and Runtime/Core split
+flows.
-- Minimal inline policy: `examples/core_step_quickstart.py`
-- Built-in Risk Engine policy: `examples/core_step_with_risk_engine.py`
+## Public Data Model
-## Canonical Events
+Core and processing models:
+
+- `CoreConfiguration`
+- `ProcessingPosition`
+- `EventStreamEntry`
+- `CoreStepResult`
+- `StrategyStateView`
+
+Canonical Event models:
- `MarketEvent`
- `ControlTimeEvent`
- `OrderSubmittedEvent`
+- `OrderCanceledEvent`
+- `OrderRejectedEvent`
+- `OrderExpiredEvent`
- `OrderExecutionFeedbackEvent`
- `FillEvent`
-## Step APIs
+Order Intent and numeric models:
-- `process_canonical_event`
-- `process_event_entry`
-- `run_core_step`
-- `run_core_wakeup_reduction`
-- `run_core_wakeup_decision`
-- `run_core_wakeup_step` (ordered batch: reduce all entries, then evaluate Strategy once)
+- `OrderIntent`
+- `NewOrderIntent`
+- `CancelOrderIntent`
+- `ReplaceOrderIntent`
+- `Price`
+- `Quantity`
-## Step inputs/outputs
+Risk data models:
-- `EventStreamEntry`
-- `ProcessingPosition`
-- `CorePolicyAdmissionContext` (holds `PolicyIntentEvaluator`)
-- `CoreExecutionControlApplyContext` (holds `ExecutionControl`)
-- `CoreStepDecision`
-- `CoreStepResult`
-- `CoreWakeupReductionResult`
-- `CoreWakeupStrategyContext`
+- `RiskConfig`
+- `RiskConstraints`
+- `NotionalLimits`
+
+Current baseline notes:
+
+- canonical Event reduction for `MarketEvent` is book-only in the current Core
+ baseline.
+- Trade-shaped `MarketEvent` payloads are not reduced in this baseline and are
+ explicitly rejected at canonical processing boundaries.
+- Policy Admission rejection is not an Order Lifecycle terminal Event.
+
+## Public Extension Point
+
+- `CoreStepStrategyEvaluator`
- `CoreWakeupStrategyEvaluator`
+- `PolicyIntentEvaluator`
+- `CorePolicyAdmissionContext`
-## Policy and risk
+Strategy evaluators read `StrategyStateView` through context and return Order Intent
+values. Policy evaluators are supplied through `CorePolicyAdmissionContext`.
-- `PolicyIntentEvaluator` (protocol)
-- `PolicyRiskDecision`
-- `PolicyAdmissionResult`
-- `PolicyRejectedCandidate`
-- Risk Engine (`RiskEngine`) (convenience `PolicyIntentEvaluator`)
-- `RiskConfig`
-- `RiskConstraints` (data model; often built for Strategy via `RiskEngine.build_constraints`)
+## Public Convenience Implementation
-## Supporting deterministic models
+- `RiskEngine`
+- `ExecutionControl`
+- `NullEventBus`
+
+These remain public for convenience and compatibility. Runtime still supplies
+instances and owns external dispatch and canonical Event injection.
+
+## Advanced API
+
+Advanced state, context, and split-step models:
-- `CoreConfiguration`
- `StrategyState`
-- `CandidateIntentRecord`
+- `CoreStepStrategyContext`
+- `CoreWakeupStrategyContext`
+- `CoreWakeupReductionResult`
+- `CoreExecutionControlApplyContext`
+
+Advanced introspection and decision scaffolds:
+
- `CandidateIntentOrigin`
+- `CandidateIntentRecord`
+- `CoreStepDecision`
- `ExecutionControlDecision`
-- `ExecutionControl`
-- `ControlSchedulingObligation` (non-canonical; **rate-limit** recheck hint in the
- current slice—see `../flows/control-time-and-scheduling.md`)
+- `PolicyRiskDecision`
+- `PolicyRejectedCandidate`
+- `PolicyAdmissionResult`
-## Intents and numeric models
+Advanced runtime-facing non-canonical output:
-- `OrderIntent`
-- `NewOrderIntent`
-- `CancelOrderIntent`
-- `ReplaceOrderIntent`
-- `Price`
-- `Quantity`
-- `NotionalLimits`
+- `ControlSchedulingObligation`
-## Runtime-safe utilities
+Advanced deterministic slot helpers:
-- `NullEventBus`
+- `SlotKey`
+- `stable_slot_order_id`
+
+Advanced API symbols are public for compatibility and diagnostics, split-step
+integration, and advanced testing. They are not themselves Runtime dispatch
+obligations. Runtime dispatch obligations remain `CoreStepResult.dispatchable_intents`
+plus canonical Event injection behavior.
+
+## State and Strategy boundary
+
+- `StrategyState` is an advanced mutable Core state container.
+- Core reducers and Execution Control own mutation of Strategy State internals.
+- Strategy code should read `StrategyStateView` via Strategy contexts.
+- `StrategyStateView` is the public read-only Strategy boundary model.
+- Queue/inflight internals and reducer methods are not Strategy mutation APIs.
+
+`CoreStepStrategyContext` is an advanced Strategy adapter context. `context.state` is
+`StrategyStateView`, not mutable `StrategyState`.
+
+## Root export surface
+
+The current root export surface includes all symbols listed on this page and:
+
+- `__version__`
+
+## Root vs non-root API
+
+Some APIs are public but are module-level (non-root) rather than root exports.
+Example:
+
+- `EventBus` from `tradingchassis_core.core.events.event_bus`
+
+`EventBus` is intentionally non-root. `NullEventBus` is the root convenience export.
+
+## Internally wired vs externally supplied
+
+Internally wired when step APIs run:
+
+- canonical Event reduction and candidate reconciliation
+- Policy Admission mechanism when `CorePolicyAdmissionContext` is provided
+- Execution Control plan/apply mechanism when execution apply context is provided
+- `CoreStepResult` and advanced decision scaffolds production
+
+Externally supplied:
+
+- Strategy evaluator (`CoreStepStrategyEvaluator` or `CoreWakeupStrategyEvaluator`)
+- Policy evaluator (`PolicyIntentEvaluator`)
+- Execution Control instance (`ExecutionControl`)
+- optional `CoreConfiguration`
+- Event bus (`EventBus` module-level API, or root `NullEventBus`)
## Publicly absent by design
@@ -114,7 +184,6 @@ Examples:
- `OrderStateEvent`
- `DerivedFillEvent`
- `VenueAdapter` / `VenuePolicy`
-- `RiskPolicy` / `ExecutionConstraintsPolicy` (internal to Risk Engine / `RiskEngine`)
-- `fold_event_stream_entries` (removed U3; loop `process_event_entry` instead)
+- `RiskPolicy` / `ExecutionConstraintsPolicy` (internal to `RiskEngine`)
+- `fold_event_stream_entries` (removed U3; use `process_event_entry` loop)
- `apply_execution_control_plan` and apply detail record types (internal; use `CoreStepResult`)
-- Telemetry event types formerly in `core/events/events.py` (removed U3)
diff --git a/docs/roadmap/dead-code-cleanup-candidates.md b/docs/roadmap/dead-code-cleanup-candidates.md
index 945c14c..0ebe7d7 100644
--- a/docs/roadmap/dead-code-cleanup-candidates.md
+++ b/docs/roadmap/dead-code-cleanup-candidates.md
@@ -7,7 +7,7 @@ what was kept, and what remains deferred.
| Item | Rationale |
| --- | --- |
-| `StrategyState.pop_queued_intents` | No callers in the Core Pipeline; Runtime tests (`core-runtime`) only monkeypatch the name to assert it is **not** invoked |
+| `StrategyState.pop_queued_intents` | No callers in the Core pipeline; Runtime tests (`core-runtime`) only monkeypatch the name to assert it is **not** invoked |
| `fold_event_stream_entries` | Zero callers; batch reduction is `process_event_entry` in a loop |
| `core/events/events.py` telemetry models | Never emitted; only referenced by unused `TELEMETRY_EVENT_TYPES` |
| `combine_candidate_intents` | Unused wrapper around `combine_candidate_intent_records` |
@@ -20,7 +20,7 @@ what was kept, and what remains deferred.
| `RiskEngine.build_constraints` | Called from Runtime (`core-runtime` `strategy_runner.py`) for Strategy evaluation |
| `SlotKey`, `stable_slot_order_id` | Used by Runtime (`core-runtime` `debug_strategy.py`) |
| `sink_logging.LoggingEventSink` | Used by Runtime (`core-runtime` `strategy_runner.py`) |
-| Risk Engine (`RiskEngine`), `RiskPolicy`, `ExecutionConstraintsPolicy`, `PolicyIntentEvaluator`, canonical Events, Core step APIs | Active extension points / Pipeline |
+| Risk Engine (`RiskEngine`), `RiskPolicy`, `ExecutionConstraintsPolicy`, `PolicyIntentEvaluator`, canonical Events, Core step APIs | Active extension points / pipeline |
## Deferred (intentionally not removed)
diff --git a/examples/core_step_quickstart.py b/examples/core_step_quickstart.py
index cd308d8..c7187de 100644
--- a/examples/core_step_quickstart.py
+++ b/examples/core_step_quickstart.py
@@ -119,7 +119,7 @@ def main() -> None:
state = tc.StrategyState(event_bus=tc.NullEventBus())
# Core consumes canonical Events. Here we use ControlTimeEvent as a simple
- # canonical trigger Event to drive the deterministic step Pipeline.
+ # canonical trigger Event to drive the deterministic step pipeline.
result_v1 = run_v1_generated_only(state)
result_v2 = run_v2_with_policy_and_apply(state)
diff --git a/tests/semantics/test_core_pipeline_clean.py b/tests/semantics/test_core_pipeline_clean.py
index 921a3dc..7daae42 100644
--- a/tests/semantics/test_core_pipeline_clean.py
+++ b/tests/semantics/test_core_pipeline_clean.py
@@ -1,4 +1,4 @@
-"""Clean CoreStep/CoreWakeupStep Pipeline tests."""
+"""Clean CoreStep/CoreWakeupStep pipeline tests."""
from __future__ import annotations
diff --git a/tests/semantics/test_fill_event_reduction.py b/tests/semantics/test_fill_event_reduction.py
index f2fb495..191c5c5 100644
--- a/tests/semantics/test_fill_event_reduction.py
+++ b/tests/semantics/test_fill_event_reduction.py
@@ -1,4 +1,4 @@
-"""FillEvent canonical reduction and Core step Pipeline coverage."""
+"""FillEvent canonical reduction and Core step pipeline coverage."""
from __future__ import annotations
diff --git a/tests/semantics/test_market_event_contract.py b/tests/semantics/test_market_event_contract.py
new file mode 100644
index 0000000..017715d
--- /dev/null
+++ b/tests/semantics/test_market_event_contract.py
@@ -0,0 +1,115 @@
+"""MarketEvent canonical contract tests for book-only baseline behavior."""
+
+from __future__ import annotations
+
+import pytest
+
+import tradingchassis_core as tc
+from tradingchassis_core.core.domain.types import BookLevel, BookPayload, TradePayload
+
+INSTRUMENT = "BTC-USDC-PERP"
+BOOK_ONLY_ERROR = (
+ "only book MarketEvent payloads are reduced; trade-shaped MarketEvent payloads "
+ "are not supported"
+)
+
+
+def _book_event(*, ts_ns_local: int) -> tc.MarketEvent:
+ return tc.MarketEvent(
+ ts_ns_exch=ts_ns_local - 1,
+ ts_ns_local=ts_ns_local,
+ instrument=INSTRUMENT,
+ event_type="book",
+ book=BookPayload(
+ book_type="snapshot",
+ bids=[
+ BookLevel(
+ price=tc.Price(currency="USDC", value=99.0),
+ quantity=tc.Quantity(value=1.0, unit="contracts"),
+ )
+ ],
+ asks=[
+ BookLevel(
+ price=tc.Price(currency="USDC", value=101.0),
+ quantity=tc.Quantity(value=2.0, unit="contracts"),
+ )
+ ],
+ depth=1,
+ ),
+ )
+
+
+def _trade_event(*, ts_ns_local: int) -> tc.MarketEvent:
+ return tc.MarketEvent(
+ ts_ns_exch=ts_ns_local - 1,
+ ts_ns_local=ts_ns_local,
+ instrument=INSTRUMENT,
+ event_type="trade",
+ trade=TradePayload(
+ side="buy",
+ price=tc.Price(currency="USDC", value=100.0),
+ quantity=tc.Quantity(value=0.5, unit="contracts"),
+ trade_id="trade-1",
+ ),
+ )
+
+
+def _control_entry(index: int, ts: int) -> tc.EventStreamEntry:
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=tc.ControlTimeEvent(
+ ts_ns_local_control=ts,
+ reason="scheduled_control_recheck",
+ due_ts_ns_local=ts,
+ realized_ts_ns_local=ts,
+ obligation_reason="rate_limit",
+ obligation_due_ts_ns_local=ts,
+ runtime_correlation=None,
+ ),
+ )
+
+
+def test_process_canonical_event_accepts_book_market_event() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ tc.process_canonical_event(state, _book_event(ts_ns_local=200))
+
+ market = state.market[INSTRUMENT]
+ assert market.best_bid == 99.0
+ assert market.best_ask == 101.0
+ assert market.mid == 100.0
+ assert state.sim_ts_ns_local == 200
+
+
+def test_market_event_trade_shape_can_be_constructed_but_reduction_rejects() -> None:
+ trade_event = _trade_event(ts_ns_local=210)
+ assert trade_event.is_trade()
+ assert trade_event.trade is not None
+
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ with pytest.raises(ValueError, match=BOOK_ONLY_ERROR):
+ tc.process_canonical_event(state, trade_event)
+
+
+def test_run_core_step_rejects_trade_market_event() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ trade_entry = tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=0),
+ event=_trade_event(ts_ns_local=220),
+ )
+
+ with pytest.raises(ValueError, match=BOOK_ONLY_ERROR):
+ tc.run_core_step(state, trade_entry)
+
+
+def test_run_core_wakeup_step_rejects_trade_market_event() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ entries = (
+ _control_entry(0, 300),
+ tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=1),
+ event=_trade_event(ts_ns_local=301),
+ ),
+ )
+
+ with pytest.raises(ValueError, match=BOOK_ONLY_ERROR):
+ tc.run_core_wakeup_step(state, entries)
diff --git a/tests/semantics/test_order_terminal_lifecycle.py b/tests/semantics/test_order_terminal_lifecycle.py
new file mode 100644
index 0000000..bed3da9
--- /dev/null
+++ b/tests/semantics/test_order_terminal_lifecycle.py
@@ -0,0 +1,242 @@
+"""Terminal order lifecycle canonical Event reduction coverage."""
+
+from __future__ import annotations
+
+import pytest
+
+import tradingchassis_core as tc
+
+INSTRUMENT = "BTC-USDC-PERP"
+ORDER_ID = "terminal-order-1"
+
+
+def _submitted_entry(index: int, ts: int) -> tc.EventStreamEntry:
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=tc.OrderSubmittedEvent(
+ ts_ns_local_dispatch=ts,
+ instrument=INSTRUMENT,
+ client_order_id=ORDER_ID,
+ side="buy",
+ order_type="limit",
+ intended_price=tc.Price(currency="USDC", value=100.0),
+ intended_qty=tc.Quantity(value=1.0, unit="contracts"),
+ time_in_force="GTC",
+ intent_correlation_id=None,
+ dispatch_attempt_id=None,
+ runtime_correlation=None,
+ ),
+ )
+
+
+def _control_entry(index: int, ts: int) -> tc.EventStreamEntry:
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=tc.ControlTimeEvent(
+ ts_ns_local_control=ts,
+ reason="scheduled_control_recheck",
+ due_ts_ns_local=ts,
+ realized_ts_ns_local=ts,
+ obligation_reason="rate_limit",
+ obligation_due_ts_ns_local=ts,
+ runtime_correlation=None,
+ ),
+ )
+
+
+def _terminal_event(kind: str, ts: int) -> object:
+ if kind == "canceled":
+ return tc.OrderCanceledEvent(
+ ts_ns_local_feedback=ts,
+ instrument=INSTRUMENT,
+ client_order_id=ORDER_ID,
+ )
+ if kind == "rejected":
+ return tc.OrderRejectedEvent(
+ ts_ns_local_feedback=ts,
+ instrument=INSTRUMENT,
+ client_order_id=ORDER_ID,
+ )
+ if kind == "expired":
+ return tc.OrderExpiredEvent(
+ ts_ns_local_feedback=ts,
+ instrument=INSTRUMENT,
+ client_order_id=ORDER_ID,
+ )
+ raise AssertionError(f"unsupported terminal kind: {kind}")
+
+
+def _terminal_entry(index: int, kind: str, ts: int) -> tc.EventStreamEntry:
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=_terminal_event(kind, ts),
+ )
+
+
+@pytest.mark.parametrize("symbol", ("OrderCanceledEvent", "OrderRejectedEvent", "OrderExpiredEvent"))
+def test_terminal_events_are_public_exports(symbol: str) -> None:
+ assert hasattr(tc, symbol)
+
+
+@pytest.mark.parametrize("terminal_kind", ("canceled", "rejected", "expired"))
+def test_submitted_then_terminal_event_removes_working_order_updates_projection_and_clears_inflight(
+ terminal_kind: str,
+) -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ tc.process_event_entry(state, _submitted_entry(0, 100))
+ state.mark_intent_sent(INSTRUMENT, ORDER_ID, "replace")
+ assert state.has_inflight(INSTRUMENT, ORDER_ID)
+ assert state.has_working_order(INSTRUMENT, ORDER_ID)
+
+ tc.process_event_entry(state, _terminal_entry(1, terminal_kind, 101))
+
+ assert not state.has_working_order(INSTRUMENT, ORDER_ID)
+ assert not state.has_inflight(INSTRUMENT, ORDER_ID)
+ projection = state.canonical_orders[(INSTRUMENT, ORDER_ID)]
+ assert projection.state == terminal_kind
+ assert projection.updated_ts_ns_local == 101
+
+
+@pytest.mark.parametrize("terminal_kind", ("canceled", "rejected", "expired"))
+def test_process_canonical_event_routes_terminal_events(terminal_kind: str) -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ tc.process_event_entry(state, _submitted_entry(0, 100))
+ tc.process_canonical_event(state, _terminal_event(terminal_kind, 101))
+
+ assert not state.has_working_order(INSTRUMENT, ORDER_ID)
+ assert state.canonical_orders[(INSTRUMENT, ORDER_ID)].state == terminal_kind
+
+
+@pytest.mark.parametrize("terminal_kind", ("canceled", "rejected", "expired"))
+def test_run_core_step_reduces_terminal_event(terminal_kind: str) -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ tc.process_event_entry(state, _submitted_entry(0, 100))
+
+ result = tc.run_core_step(
+ state,
+ _terminal_entry(1, terminal_kind, 101),
+ )
+
+ assert result.generated_intents == ()
+ assert result.dispatchable_intents == ()
+ assert not state.has_working_order(INSTRUMENT, ORDER_ID)
+ assert state.canonical_orders[(INSTRUMENT, ORDER_ID)].state == terminal_kind
+
+
+@pytest.mark.parametrize("terminal_kind", ("canceled", "rejected", "expired"))
+def test_run_core_wakeup_step_reduces_terminal_events_in_processing_order(
+ terminal_kind: str,
+) -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+
+ class _AssertTerminalReducedEvaluator:
+ def evaluate(self, context: tc.CoreWakeupStrategyContext) -> list[tc.OrderIntent]:
+ assert context.last_position is not None
+ assert context.last_position.index == 2
+ assert not context.state.orders.get(INSTRUMENT, {}).get(ORDER_ID)
+ return []
+
+ result = tc.run_core_wakeup_step(
+ state,
+ (
+ _submitted_entry(0, 100),
+ _control_entry(1, 101),
+ _terminal_entry(2, terminal_kind, 102),
+ ),
+ wakeup_strategy_evaluator=_AssertTerminalReducedEvaluator(),
+ )
+
+ assert result.generated_intents == ()
+ assert not state.has_working_order(INSTRUMENT, ORDER_ID)
+ assert state.canonical_orders[(INSTRUMENT, ORDER_ID)].state == terminal_kind
+
+
+@pytest.mark.parametrize("terminal_kind", ("canceled", "rejected", "expired"))
+def test_repeated_terminal_event_is_idempotent(terminal_kind: str) -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ tc.process_event_entry(state, _submitted_entry(0, 100))
+ first = _terminal_entry(1, terminal_kind, 101)
+ second = _terminal_entry(2, terminal_kind, 101)
+
+ tc.process_event_entry(state, first)
+ projection_after_first = state.canonical_orders[(INSTRUMENT, ORDER_ID)]
+ state_snapshot = (
+ projection_after_first.state,
+ projection_after_first.submitted_ts_ns_local,
+ projection_after_first.updated_ts_ns_local,
+ )
+
+ tc.process_event_entry(state, second)
+ projection_after_second = state.canonical_orders[(INSTRUMENT, ORDER_ID)]
+ assert not state.has_working_order(INSTRUMENT, ORDER_ID)
+ assert (
+ projection_after_second.state,
+ projection_after_second.submitted_ts_ns_local,
+ projection_after_second.updated_ts_ns_local,
+ ) == state_snapshot
+
+
+@pytest.mark.parametrize("terminal_kind", ("canceled", "rejected", "expired"))
+def test_terminal_event_for_unknown_order_is_deterministic_and_non_crashing(
+ terminal_kind: str,
+) -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+
+ tc.process_event_entry(state, _terminal_entry(0, terminal_kind, 100))
+
+ assert not state.has_working_order(INSTRUMENT, ORDER_ID)
+ projection = state.canonical_orders[(INSTRUMENT, ORDER_ID)]
+ assert projection.state == terminal_kind
+ assert projection.submitted_ts_ns_local == 100
+ assert projection.updated_ts_ns_local == 100
+
+
+def test_order_rejected_event_is_distinct_from_policy_admission_rejection() -> None:
+ class _RejectAllPolicy:
+ def evaluate_policy_intent(
+ self,
+ *,
+ intent: tc.OrderIntent,
+ state: tc.StrategyState,
+ now_ts_ns_local: int,
+ ) -> tuple[bool, str | None]:
+ _ = (intent, state, now_ts_ns_local)
+ return False, "blocked_for_test"
+
+ class _OneIntentEvaluator:
+ def evaluate(self, context: object) -> list[tc.NewOrderIntent]:
+ _ = context
+ return [
+ tc.NewOrderIntent(
+ intent_type="new",
+ ts_ns_local=100,
+ instrument=INSTRUMENT,
+ client_order_id="policy-reject-order",
+ intents_correlation_id="corr-policy-reject",
+ side="buy",
+ order_type="limit",
+ intended_qty=tc.Quantity(value=1.0, unit="contracts"),
+ intended_price=tc.Price(currency="USDC", value=100.0),
+ time_in_force="GTC",
+ )
+ ]
+
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ result = tc.run_core_step(
+ state,
+ _control_entry(0, 100),
+ strategy_evaluator=_OneIntentEvaluator(),
+ policy_admission_context=tc.CorePolicyAdmissionContext(
+ policy_evaluator=_RejectAllPolicy(),
+ now_ts_ns_local=100,
+ ),
+ execution_control_apply_context=tc.CoreExecutionControlApplyContext(
+ execution_control=tc.ExecutionControl(),
+ now_ts_ns_local=100,
+ activate_dispatchable_outputs=True,
+ ),
+ )
+
+ assert result.core_step_decision is not None
+ assert len(result.core_step_decision.policy_rejected_intents) == 1
+ assert (INSTRUMENT, "policy-reject-order") not in state.canonical_orders
diff --git a/tests/semantics/test_public_api_clean.py b/tests/semantics/test_public_api_clean.py
index a204efa..44cd30e 100644
--- a/tests/semantics/test_public_api_clean.py
+++ b/tests/semantics/test_public_api_clean.py
@@ -2,52 +2,80 @@
from __future__ import annotations
+import re
+from pathlib import Path
+
import tradingchassis_core as tc
+EXPECTED_ROOT_EXPORTS = (
+ "CoreConfiguration",
+ "RiskConfig",
+ "RiskEngine",
+ "StrategyState",
+ "StrategyStateView",
+ "MarketEvent",
+ "ControlTimeEvent",
+ "OrderSubmittedEvent",
+ "OrderCanceledEvent",
+ "OrderRejectedEvent",
+ "OrderExpiredEvent",
+ "OrderExecutionFeedbackEvent",
+ "FillEvent",
+ "RiskConstraints",
+ "NotionalLimits",
+ "OrderIntent",
+ "NewOrderIntent",
+ "CancelOrderIntent",
+ "ReplaceOrderIntent",
+ "Price",
+ "Quantity",
+ "SlotKey",
+ "stable_slot_order_id",
+ "CandidateIntentOrigin",
+ "CandidateIntentRecord",
+ "ProcessingPosition",
+ "EventStreamEntry",
+ "process_canonical_event",
+ "process_event_entry",
+ "run_core_step",
+ "run_core_wakeup_reduction",
+ "run_core_wakeup_decision",
+ "run_core_wakeup_step",
+ "CoreStepStrategyContext",
+ "CoreStepStrategyEvaluator",
+ "CoreWakeupStrategyContext",
+ "CoreWakeupStrategyEvaluator",
+ "CoreExecutionControlApplyContext",
+ "CorePolicyAdmissionContext",
+ "CoreWakeupReductionResult",
+ "ExecutionControlDecision",
+ "PolicyIntentEvaluator",
+ "PolicyRiskDecision",
+ "PolicyRejectedCandidate",
+ "PolicyAdmissionResult",
+ "CoreStepDecision",
+ "CoreStepResult",
+ "ExecutionControl",
+ "ControlSchedulingObligation",
+ "NullEventBus",
+ "__version__",
+)
+
def test_public_api_exposes_clean_core_symbols() -> None:
- for symbol in (
- "EventStreamEntry",
- "ProcessingPosition",
- "process_canonical_event",
- "process_event_entry",
- "run_core_step",
- "run_core_wakeup_reduction",
- "run_core_wakeup_decision",
- "run_core_wakeup_step",
- "CoreWakeupStrategyContext",
- "CoreWakeupStrategyEvaluator",
- "CoreStepResult",
- "CoreStepDecision",
- "PolicyIntentEvaluator",
- "PolicyRiskDecision",
- "ExecutionControlDecision",
- "CandidateIntentRecord",
- "CandidateIntentOrigin",
- "CorePolicyAdmissionContext",
- "CoreExecutionControlApplyContext",
- "ControlTimeEvent",
- "MarketEvent",
- "OrderSubmittedEvent",
- "OrderExecutionFeedbackEvent",
- "FillEvent",
- "OrderIntent",
- "NewOrderIntent",
- "CancelOrderIntent",
- "ReplaceOrderIntent",
- "Price",
- "Quantity",
- "CoreConfiguration",
- "StrategyState",
- "ExecutionControl",
- "ControlSchedulingObligation",
- "NullEventBus",
- "RiskEngine",
- "RiskConfig",
- ):
+ for symbol in EXPECTED_ROOT_EXPORTS:
assert hasattr(tc, symbol), symbol
+def test_public_api_docs_mention_root_exports() -> None:
+ docs_path = Path(__file__).resolve().parents[2] / "docs" / "reference" / "public-api.md"
+ docs_content = docs_path.read_text(encoding="utf-8")
+ documented_symbols = set(re.findall(r"`([A-Za-z_][A-Za-z0-9_]*)`", docs_content))
+
+ missing = sorted(symbol for symbol in EXPECTED_ROOT_EXPORTS if symbol not in documented_symbols)
+ assert missing == [], f"Root exports missing from public-api docs: {missing}"
+
+
def test_public_api_does_not_expose_removed_compatibility_symbols() -> None:
removed = (
"".join(["Gate", "Decision"]),
diff --git a/tests/semantics/test_risk_engine_pipeline_integration.py b/tests/semantics/test_risk_engine_pipeline_integration.py
index 743a27c..303d844 100644
--- a/tests/semantics/test_risk_engine_pipeline_integration.py
+++ b/tests/semantics/test_risk_engine_pipeline_integration.py
@@ -75,7 +75,7 @@ def _prime_market(state: tc.StrategyState) -> None:
def test_risk_engine_accepts_generated_intent_in_run_core_step() -> None:
- """RiskEngine is a valid optional policy_evaluator for the full step Pipeline."""
+ """RiskEngine is a valid optional policy_evaluator for the full step pipeline."""
state = tc.StrategyState(event_bus=tc.NullEventBus())
_prime_market(state)
policy_engine = tc.RiskEngine(_risk_config(trading_enabled=True))
@@ -109,7 +109,7 @@ def test_risk_engine_accepts_generated_intent_in_run_core_step() -> None:
def test_risk_engine_rejects_generated_intent_when_trading_disabled() -> None:
- """Trading-disabled RiskConfig rejects new intents through policy admission."""
+ """Trading-disabled RiskConfig rejects new Intents through policy admission."""
state = tc.StrategyState(event_bus=tc.NullEventBus())
_prime_market(state)
policy_engine = tc.RiskEngine(_risk_config(trading_enabled=False))
diff --git a/tests/semantics/test_runtime_core_contract.py b/tests/semantics/test_runtime_core_contract.py
new file mode 100644
index 0000000..a0a1836
--- /dev/null
+++ b/tests/semantics/test_runtime_core_contract.py
@@ -0,0 +1,201 @@
+"""Focused Runtime/Core contract tests for E2 hardening."""
+
+from __future__ import annotations
+
+import tradingchassis_core as tc
+
+INSTRUMENT = "BTC-USDC-PERP"
+ORDER_ID = "runtime-contract-order-1"
+
+
+def _submitted_entry(index: int, ts: int) -> tc.EventStreamEntry:
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=tc.OrderSubmittedEvent(
+ ts_ns_local_dispatch=ts,
+ instrument=INSTRUMENT,
+ client_order_id=ORDER_ID,
+ side="buy",
+ order_type="limit",
+ intended_price=tc.Price(currency="USDC", value=100.0),
+ intended_qty=tc.Quantity(value=1.0, unit="contracts"),
+ time_in_force="GTC",
+ intent_correlation_id=None,
+ dispatch_attempt_id=None,
+ runtime_correlation=None,
+ ),
+ )
+
+
+def _feedback_entry(index: int, ts: int) -> tc.EventStreamEntry:
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=tc.OrderExecutionFeedbackEvent(
+ ts_ns_local_feedback=ts,
+ instrument=INSTRUMENT,
+ position=2.5,
+ balance=10_000.0,
+ fee=0.25,
+ trading_volume=5.0,
+ trading_value=500.0,
+ num_trades=7,
+ runtime_correlation=None,
+ ),
+ )
+
+
+def _control_entry(index: int, ts: int) -> tc.EventStreamEntry:
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=tc.ControlTimeEvent(
+ ts_ns_local_control=ts,
+ reason="scheduled_control_recheck",
+ due_ts_ns_local=ts,
+ realized_ts_ns_local=ts,
+ obligation_reason="rate_limit",
+ obligation_due_ts_ns_local=ts,
+ runtime_correlation=None,
+ ),
+ )
+
+
+class _OneIntentEvaluator:
+ def evaluate(self, context: object) -> list[tc.NewOrderIntent]:
+ _ = context
+ return [
+ tc.NewOrderIntent(
+ intent_type="new",
+ ts_ns_local=100,
+ instrument=INSTRUMENT,
+ client_order_id="intent-contract-1",
+ intents_correlation_id="corr-contract-1",
+ side="buy",
+ order_type="limit",
+ intended_qty=tc.Quantity(value=1.0, unit="contracts"),
+ intended_price=tc.Price(currency="USDC", value=100.0),
+ time_in_force="GTC",
+ )
+ ]
+
+
+class _AllowAllPolicy:
+ def evaluate_policy_intent(
+ self,
+ *,
+ intent: tc.OrderIntent,
+ state: tc.StrategyState,
+ now_ts_ns_local: int,
+ ) -> tuple[bool, str | None]:
+ _ = (intent, state, now_ts_ns_local)
+ return True, None
+
+
+def test_order_execution_feedback_event_is_account_only() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ tc.process_event_entry(state, _submitted_entry(0, 100))
+ state.mark_intent_sent(INSTRUMENT, ORDER_ID, "replace")
+
+ assert state.has_working_order(INSTRUMENT, ORDER_ID)
+ assert state.has_inflight(INSTRUMENT, ORDER_ID)
+ assert INSTRUMENT not in state.fills
+
+ projection_before = state.canonical_orders[(INSTRUMENT, ORDER_ID)]
+ projection_state_before = projection_before.state
+ projection_updated_before = projection_before.updated_ts_ns_local
+
+ tc.process_event_entry(state, _feedback_entry(1, 101))
+
+ account = state.account[INSTRUMENT]
+ assert account.position == 2.5
+ assert account.balance == 10_000.0
+ assert account.fee == 0.25
+ assert account.trading_volume == 5.0
+ assert account.trading_value == 500.0
+ assert account.num_trades == 7
+
+ assert state.has_working_order(INSTRUMENT, ORDER_ID)
+ assert state.has_inflight(INSTRUMENT, ORDER_ID)
+ assert INSTRUMENT not in state.fills
+ assert state.fill_cum_qty.get(INSTRUMENT) is None
+
+ projection_after = state.canonical_orders[(INSTRUMENT, ORDER_ID)]
+ assert projection_after.state == projection_state_before
+ assert projection_after.updated_ts_ns_local == projection_updated_before
+
+
+def test_control_scheduling_obligation_is_not_canonical_event() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ obligation = tc.ControlSchedulingObligation(
+ due_ts_ns_local=200,
+ reason="rate_limit",
+ scope_key=f"instrument:{INSTRUMENT}",
+ source="test",
+ )
+
+ try:
+ tc.process_canonical_event(state, obligation)
+ except TypeError as exc:
+ assert "Unsupported non-canonical Event type" in str(exc)
+ else:
+ raise AssertionError("ControlSchedulingObligation must not be accepted as canonical Event")
+
+ tc.process_event_entry(
+ state,
+ tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=0),
+ event=tc.ControlTimeEvent(
+ ts_ns_local_control=200,
+ reason="scheduled_control_recheck",
+ due_ts_ns_local=200,
+ realized_ts_ns_local=200,
+ obligation_reason=obligation.reason,
+ obligation_due_ts_ns_local=obligation.due_ts_ns_local,
+ runtime_correlation=None,
+ ),
+ ),
+ )
+ assert state.sim_ts_ns_local == 200
+
+
+def test_core_step_result_outputs_are_runtime_contract_envelope() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ policy_context = tc.CorePolicyAdmissionContext(
+ policy_evaluator=_AllowAllPolicy(),
+ now_ts_ns_local=100,
+ )
+
+ result_without_dispatchables = tc.run_core_step(
+ state,
+ _control_entry(0, 100),
+ strategy_evaluator=_OneIntentEvaluator(),
+ policy_admission_context=policy_context,
+ execution_control_apply_context=tc.CoreExecutionControlApplyContext(
+ execution_control=tc.ExecutionControl(),
+ now_ts_ns_local=100,
+ activate_dispatchable_outputs=False,
+ ),
+ )
+
+ assert isinstance(result_without_dispatchables.generated_intents, tuple)
+ assert isinstance(result_without_dispatchables.candidate_intents, tuple)
+ assert isinstance(result_without_dispatchables.candidate_intent_records, tuple)
+ assert isinstance(result_without_dispatchables.dispatchable_intents, tuple)
+ assert result_without_dispatchables.generated_intents
+ assert result_without_dispatchables.candidate_intents
+ assert result_without_dispatchables.core_step_decision is not None
+ assert result_without_dispatchables.dispatchable_intents == ()
+
+ state2 = tc.StrategyState(event_bus=tc.NullEventBus())
+ result_with_dispatchables = tc.run_core_step(
+ state2,
+ _control_entry(0, 100),
+ strategy_evaluator=_OneIntentEvaluator(),
+ policy_admission_context=policy_context,
+ execution_control_apply_context=tc.CoreExecutionControlApplyContext(
+ execution_control=tc.ExecutionControl(),
+ now_ts_ns_local=100,
+ activate_dispatchable_outputs=True,
+ ),
+ )
+ assert isinstance(result_with_dispatchables.dispatchable_intents, tuple)
+ assert len(result_with_dispatchables.dispatchable_intents) == 1
diff --git a/tests/semantics/test_strategy_state_view_boundary.py b/tests/semantics/test_strategy_state_view_boundary.py
new file mode 100644
index 0000000..d07e394
--- /dev/null
+++ b/tests/semantics/test_strategy_state_view_boundary.py
@@ -0,0 +1,196 @@
+"""Strategy boundary tests for read-only Strategy State views."""
+
+from __future__ import annotations
+
+import pytest
+
+import tradingchassis_core as tc
+
+INSTRUMENT = "BTC-USDC-PERP"
+WORKING_ORDER_ID = "working-order-1"
+FILL_ORDER_ID = "fill-order-1"
+QUEUED_ORDER_ID = "queued-order-1"
+INFLIGHT_ORDER_ID = "inflight-order-1"
+
+
+def _control_entry(index: int, ts: int) -> tc.EventStreamEntry:
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=tc.ControlTimeEvent(
+ ts_ns_local_control=ts,
+ reason="scheduled_control_recheck",
+ due_ts_ns_local=ts,
+ realized_ts_ns_local=ts,
+ obligation_reason="rate_limit",
+ obligation_due_ts_ns_local=ts,
+ runtime_correlation=None,
+ ),
+ )
+
+
+def _order_submitted_entry(index: int, ts: int, client_order_id: str) -> tc.EventStreamEntry:
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=tc.OrderSubmittedEvent(
+ ts_ns_local_dispatch=ts,
+ instrument=INSTRUMENT,
+ client_order_id=client_order_id,
+ side="buy",
+ order_type="limit",
+ intended_price=tc.Price(currency="USDC", value=100.0),
+ intended_qty=tc.Quantity(value=1.0, unit="contracts"),
+ time_in_force="GTC",
+ intent_correlation_id=None,
+ dispatch_attempt_id=None,
+ runtime_correlation=None,
+ ),
+ )
+
+
+def _execution_feedback_entry(index: int, ts: int) -> tc.EventStreamEntry:
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=tc.OrderExecutionFeedbackEvent(
+ ts_ns_local_feedback=ts,
+ instrument=INSTRUMENT,
+ position=2.5,
+ balance=10_000.0,
+ fee=0.1,
+ trading_volume=1.0,
+ trading_value=100.0,
+ num_trades=1,
+ runtime_correlation=None,
+ ),
+ )
+
+
+def _fill_entry(index: int, ts: int, client_order_id: str) -> tc.EventStreamEntry:
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=tc.FillEvent(
+ ts_ns_exch=ts - 1,
+ ts_ns_local=ts,
+ instrument=INSTRUMENT,
+ client_order_id=client_order_id,
+ side="buy",
+ filled_price=tc.Price(currency="USDC", value=100.0),
+ cum_filled_qty=tc.Quantity(value=1.0, unit="contracts"),
+ remaining_qty=tc.Quantity(value=0.0, unit="contracts"),
+ time_in_force="GTC",
+ liquidity_flag="taker",
+ ),
+ )
+
+
+def _queued_intent() -> tc.NewOrderIntent:
+ return tc.NewOrderIntent(
+ intent_type="new",
+ ts_ns_local=100,
+ instrument=INSTRUMENT,
+ client_order_id=QUEUED_ORDER_ID,
+ intents_correlation_id="queued-corr",
+ side="buy",
+ order_type="limit",
+ intended_qty=tc.Quantity(value=1.0, unit="contracts"),
+ intended_price=tc.Price(currency="USDC", value=99.5),
+ time_in_force="GTC",
+ )
+
+
+def _seed_state() -> tc.StrategyState:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ state.update_market(
+ instrument=INSTRUMENT,
+ best_bid=99.0,
+ best_ask=101.0,
+ best_bid_qty=1.0,
+ best_ask_qty=1.0,
+ tick_size=0.1,
+ lot_size=0.01,
+ contract_size=1.0,
+ ts_ns_local=100,
+ ts_ns_exch=99,
+ )
+ tc.process_event_entry(state, _execution_feedback_entry(0, 101))
+ tc.process_event_entry(state, _order_submitted_entry(1, 102, WORKING_ORDER_ID))
+ tc.process_event_entry(state, _fill_entry(2, 103, FILL_ORDER_ID))
+ state.merge_intents_into_queue(INSTRUMENT, [_queued_intent()])
+ state.mark_intent_sent(INSTRUMENT, INFLIGHT_ORDER_ID, "replace")
+ return state
+
+
+def test_core_step_strategy_state_is_read_only_view() -> None:
+ state = _seed_state()
+
+ class _ReadOnlyStepEvaluator:
+ def evaluate(self, context: tc.CoreStepStrategyContext) -> list[tc.OrderIntent]:
+ assert isinstance(context.state, tc.StrategyStateView)
+ assert context.state.sim_ts_ns_local >= 103
+ assert context.state.market[INSTRUMENT].best_bid == 99.0
+ assert context.state.account[INSTRUMENT].position == 2.5
+ assert context.state.orders[INSTRUMENT][WORKING_ORDER_ID].client_order_id == WORKING_ORDER_ID
+ assert len(context.state.fills[INSTRUMENT]) == 1
+ assert context.state.fill_cum_qty[INSTRUMENT][FILL_ORDER_ID] == 1.0
+
+ with pytest.raises(TypeError):
+ context.state.market[INSTRUMENT] = context.state.market[INSTRUMENT] # type: ignore[index]
+ with pytest.raises(AttributeError):
+ context.state.market[INSTRUMENT].best_bid = 123.0 # type: ignore[misc]
+ with pytest.raises(TypeError):
+ context.state.fill_cum_qty[INSTRUMENT][FILL_ORDER_ID] = 2.0 # type: ignore[index]
+
+ copied_fill = context.state.fills[INSTRUMENT][0]
+ copied_fill.side = "sell"
+
+ assert not hasattr(context.state, "update_market")
+ assert not hasattr(context.state, "update_account")
+ assert not hasattr(context.state, "apply_fill_event")
+ assert not hasattr(context.state, "apply_order_submitted_event")
+ assert not hasattr(context.state, "apply_order_execution_feedback_event")
+ assert not hasattr(context.state, "apply_control_time_event")
+ assert not hasattr(context.state, "merge_intents_into_queue")
+ assert not hasattr(context.state, "pop_queued_intents_for_order")
+ assert not hasattr(context.state, "mark_intent_sent")
+ assert not hasattr(context.state, "_advance_processing_position")
+ assert not hasattr(context.state, "queued_intents")
+ assert not hasattr(context.state, "inflight")
+ return []
+
+ _ = tc.run_core_step(
+ state,
+ _control_entry(3, 104),
+ strategy_evaluator=_ReadOnlyStepEvaluator(),
+ )
+
+ assert state.market[INSTRUMENT].best_bid == 99.0
+ assert state.account[INSTRUMENT].position == 2.5
+ assert state.orders[INSTRUMENT][WORKING_ORDER_ID].intended_price == 100.0
+ assert state.fills[INSTRUMENT][0].side == "buy"
+ assert state.fill_cum_qty[INSTRUMENT][FILL_ORDER_ID] == 1.0
+ assert state.has_queued_intent(INSTRUMENT, QUEUED_ORDER_ID)
+ assert state.has_inflight(INSTRUMENT, INFLIGHT_ORDER_ID)
+
+
+def test_core_wakeup_strategy_state_is_read_only_view() -> None:
+ state = _seed_state()
+
+ class _ReadOnlyWakeupEvaluator:
+ def evaluate(self, context: tc.CoreWakeupStrategyContext) -> list[tc.OrderIntent]:
+ assert isinstance(context.state, tc.StrategyStateView)
+ assert len(context.entries) == 2
+ assert context.last_position is not None
+ assert context.last_position.index == 5
+ assert context.state.market[INSTRUMENT].best_ask == 101.0
+ with pytest.raises(AttributeError):
+ setattr(
+ context.state.orders[INSTRUMENT][WORKING_ORDER_ID],
+ "state",
+ "cancelled",
+ )
+ return []
+
+ _ = tc.run_core_wakeup_step(
+ state,
+ (_control_entry(4, 105), _control_entry(5, 106)),
+ wakeup_strategy_evaluator=_ReadOnlyWakeupEvaluator(),
+ )
diff --git a/tradingchassis_core/__init__.py b/tradingchassis_core/__init__.py
index 52c928b..276f385 100644
--- a/tradingchassis_core/__init__.py
+++ b/tradingchassis_core/__init__.py
@@ -43,7 +43,7 @@
SlotKey,
stable_slot_order_id,
)
-from tradingchassis_core.core.domain.state import StrategyState
+from tradingchassis_core.core.domain.state import StrategyState, StrategyStateView
from tradingchassis_core.core.domain.step_decision import CoreStepDecision
from tradingchassis_core.core.domain.step_result import CoreStepResult
from tradingchassis_core.core.domain.types import (
@@ -53,8 +53,11 @@
MarketEvent,
NewOrderIntent,
NotionalLimits,
+ OrderCanceledEvent,
OrderExecutionFeedbackEvent,
+ OrderExpiredEvent,
OrderIntent,
+ OrderRejectedEvent,
OrderSubmittedEvent,
Price,
Quantity,
@@ -72,9 +75,13 @@
"RiskConfig",
"RiskEngine",
"StrategyState",
+ "StrategyStateView",
"MarketEvent",
"ControlTimeEvent",
"OrderSubmittedEvent",
+ "OrderCanceledEvent",
+ "OrderRejectedEvent",
+ "OrderExpiredEvent",
"OrderExecutionFeedbackEvent",
"FillEvent",
"RiskConstraints",
diff --git a/tradingchassis_core/core/domain/candidate_intent.py b/tradingchassis_core/core/domain/candidate_intent.py
index 4f75467..4822f6c 100644
--- a/tradingchassis_core/core/domain/candidate_intent.py
+++ b/tradingchassis_core/core/domain/candidate_intent.py
@@ -9,7 +9,7 @@
class CandidateIntentOrigin(str, Enum):
- """Origin marker for candidate intents in one Core step."""
+ """Origin marker for candidate Intents in one Core step."""
GENERATED = "generated"
QUEUED = "queued"
diff --git a/tradingchassis_core/core/domain/event_model.py b/tradingchassis_core/core/domain/event_model.py
index f2afe8a..ba22b02 100644
--- a/tradingchassis_core/core/domain/event_model.py
+++ b/tradingchassis_core/core/domain/event_model.py
@@ -8,7 +8,10 @@
ControlTimeEvent,
FillEvent,
MarketEvent,
+ OrderCanceledEvent,
OrderExecutionFeedbackEvent,
+ OrderExpiredEvent,
+ OrderRejectedEvent,
OrderSubmittedEvent,
)
from tradingchassis_core.core.execution_control.types import ControlSchedulingObligation
@@ -30,6 +33,9 @@ class CanonicalEventCategory(str, Enum):
CANONICAL_STREAM_CANDIDATE_CATEGORY_BY_TYPE: dict[type[object], CanonicalEventCategory] = {
MarketEvent: CanonicalEventCategory.MARKET,
OrderSubmittedEvent: CanonicalEventCategory.INTENT_RELATED,
+ OrderCanceledEvent: CanonicalEventCategory.EXECUTION,
+ OrderRejectedEvent: CanonicalEventCategory.EXECUTION,
+ OrderExpiredEvent: CanonicalEventCategory.EXECUTION,
FillEvent: CanonicalEventCategory.EXECUTION,
OrderExecutionFeedbackEvent: CanonicalEventCategory.EXECUTION,
ControlTimeEvent: CanonicalEventCategory.CONTROL,
diff --git a/tradingchassis_core/core/domain/intent_combination.py b/tradingchassis_core/core/domain/intent_combination.py
index 4ae2d88..2e8e803 100644
--- a/tradingchassis_core/core/domain/intent_combination.py
+++ b/tradingchassis_core/core/domain/intent_combination.py
@@ -40,7 +40,7 @@ def combine_candidate_intent_records(
generated_intents: Sequence[OrderIntent],
queued_intents: Sequence[OrderIntent],
) -> tuple[CandidateIntentRecord, ...]:
- """Combine queued + generated intents into a deterministic effective set.
+ """Combine queued + generated Intents into a deterministic effective set.
This helper is pure and does not mutate StrategyState.
Merge order is deterministic: queued first, then generated.
diff --git a/tradingchassis_core/core/domain/processing.py b/tradingchassis_core/core/domain/processing.py
index b7303ec..de006fa 100644
--- a/tradingchassis_core/core/domain/processing.py
+++ b/tradingchassis_core/core/domain/processing.py
@@ -29,7 +29,10 @@
ControlTimeEvent,
FillEvent,
MarketEvent,
+ OrderCanceledEvent,
OrderExecutionFeedbackEvent,
+ OrderExpiredEvent,
+ OrderRejectedEvent,
OrderSubmittedEvent,
)
@@ -110,6 +113,9 @@ def process_canonical_event(
Accepted canonical candidates in the current slice:
- ``MarketEvent`` (category: ``market``)
- ``OrderSubmittedEvent`` (category: ``intent_related``)
+ - ``OrderCanceledEvent`` (category: ``execution``)
+ - ``OrderRejectedEvent`` (category: ``execution``)
+ - ``OrderExpiredEvent`` (category: ``execution``)
- ``FillEvent`` (category: ``execution``)
- ``OrderExecutionFeedbackEvent`` (category: ``execution``)
- ``ControlTimeEvent`` (category: ``control``)
@@ -128,15 +134,22 @@ def process_canonical_event(
category = canonical_category_for_type(record_type)
if category == CanonicalEventCategory.MARKET and isinstance(event, MarketEvent):
- if not event.is_book() or event.book is None:
+ if event.is_trade():
raise ValueError(
- "Unsupported MarketEvent payload for canonical processing: "
- "book snapshot/delta with top-of-book levels is required."
+ "Unsupported MarketEvent for canonical processing in the current Core "
+ "baseline: only book MarketEvent payloads are reduced; trade-shaped "
+ "MarketEvent payloads are not supported."
+ )
+ if event.book is None:
+ raise ValueError(
+ "Unsupported MarketEvent payload for canonical processing in the current "
+ "Core baseline: book payload is required."
)
if not event.book.bids or not event.book.asks:
raise ValueError(
- "Unsupported MarketEvent payload for canonical processing: "
- "book payload must include at least one bid and one ask level."
+ "Unsupported MarketEvent payload for canonical processing in the current "
+ "Core baseline: book payload must include at least one bid and one ask "
+ "level."
)
best_bid_level = event.book.bids[0]
@@ -181,6 +194,24 @@ def process_canonical_event(
state.apply_fill_event(event)
return
+ if category == CanonicalEventCategory.EXECUTION and isinstance(event, OrderCanceledEvent):
+ if position is not None:
+ state._advance_processing_position(position)
+ state.apply_order_canceled_event(event)
+ return
+
+ if category == CanonicalEventCategory.EXECUTION and isinstance(event, OrderRejectedEvent):
+ if position is not None:
+ state._advance_processing_position(position)
+ state.apply_order_rejected_event(event)
+ return
+
+ if category == CanonicalEventCategory.EXECUTION and isinstance(event, OrderExpiredEvent):
+ if position is not None:
+ state._advance_processing_position(position)
+ state.apply_order_expired_event(event)
+ return
+
if (
category == CanonicalEventCategory.EXECUTION
and isinstance(event, OrderExecutionFeedbackEvent)
diff --git a/tradingchassis_core/core/domain/processing_step.py b/tradingchassis_core/core/domain/processing_step.py
index 711c957..44228b4 100644
--- a/tradingchassis_core/core/domain/processing_step.py
+++ b/tradingchassis_core/core/domain/processing_step.py
@@ -27,7 +27,7 @@
)
from tradingchassis_core.core.domain.processing import process_event_entry
from tradingchassis_core.core.domain.processing_order import EventStreamEntry, ProcessingPosition
-from tradingchassis_core.core.domain.state import StrategyState
+from tradingchassis_core.core.domain.state import StrategyState, StrategyStateView
from tradingchassis_core.core.domain.step_decision import CoreStepDecision
from tradingchassis_core.core.domain.step_result import CoreStepResult
from tradingchassis_core.core.domain.types import OrderIntent
@@ -40,7 +40,7 @@
class CoreStepStrategyContext:
"""Deterministic Strategy-evaluation context for one Core step."""
- state: StrategyState
+ state: StrategyStateView
event: object
position: ProcessingPosition
configuration: CoreConfiguration | None = None
@@ -57,7 +57,7 @@ def evaluate(self, context: CoreStepStrategyContext) -> Sequence[OrderIntent]:
class CoreWakeupStrategyContext:
"""Deterministic Strategy-evaluation context for one Core wakeup batch."""
- state: StrategyState
+ state: StrategyStateView
entries: tuple[EventStreamEntry, ...]
configuration: CoreConfiguration | None = None
last_position: ProcessingPosition | None = None
@@ -147,8 +147,9 @@ def run_core_step(
generated_intents: tuple[OrderIntent, ...] = ()
if strategy_evaluator is not None:
+ state_view = StrategyStateView(state)
strategy_context = CoreStepStrategyContext(
- state=state,
+ state=state_view,
event=entry.event,
position=entry.position,
configuration=configuration,
@@ -243,8 +244,9 @@ def run_core_wakeup_reduction(
generated_intents: tuple[OrderIntent, ...] = ()
if wakeup_strategy_evaluator is not None:
last_position = entries_tuple[-1].position if entries_tuple else None
+ state_view = StrategyStateView(state)
wakeup_context = CoreWakeupStrategyContext(
- state=state,
+ state=state_view,
entries=entries_tuple,
configuration=configuration,
last_position=last_position,
diff --git a/tradingchassis_core/core/domain/state.py b/tradingchassis_core/core/domain/state.py
index 06a0821..70a8c9d 100644
--- a/tradingchassis_core/core/domain/state.py
+++ b/tradingchassis_core/core/domain/state.py
@@ -9,7 +9,8 @@
from collections import deque
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Iterable
+from types import MappingProxyType
+from typing import TYPE_CHECKING, Iterable, Mapping
from tradingchassis_core.core.domain.processing_order import ProcessingPosition
@@ -18,8 +19,11 @@
ControlTimeEvent,
FillEvent,
NewOrderIntent,
+ OrderCanceledEvent,
OrderExecutionFeedbackEvent,
+ OrderExpiredEvent,
OrderIntent,
+ OrderRejectedEvent,
OrderSubmittedEvent,
)
from tradingchassis_core.core.events.event_bus import EventBus
@@ -104,6 +108,189 @@ class CanonicalOrderProjection:
intended_qty: float | None = None
+@dataclass(frozen=True, slots=True)
+class MarketStateView:
+ """Immutable market snapshot exposed to Strategy evaluation."""
+
+ last_ts_ns_local: int
+ last_ts_ns_exch: int
+ best_bid: float
+ best_ask: float
+ mid: float
+ best_bid_qty: float
+ best_ask_qty: float
+ tick_size: float
+ lot_size: float
+ contract_size: float
+
+
+@dataclass(frozen=True, slots=True)
+class AccountStateView:
+ """Immutable account snapshot exposed to Strategy evaluation."""
+
+ position: float
+ balance: float
+ fee: float
+ trading_volume: float
+ trading_value: float
+ num_trades: int
+ equity: float
+ initial_equity: float
+ realized_pnl: float
+
+
+@dataclass(frozen=True, slots=True)
+class WorkingOrderView:
+ """Immutable working-order snapshot exposed to Strategy evaluation."""
+
+ instrument: str
+ client_order_id: str
+ side: str
+ intended_price: float
+ intended_qty: float
+ cum_filled_qty: float
+ remaining_qty: float
+ state: str
+ submitted_ts_ns_local: int
+ updated_ts_ns_local: int
+
+
+class StrategyStateView:
+ """Read-only snapshot of Strategy State for Strategy evaluation."""
+
+ __slots__ = (
+ "_sim_ts_ns_local",
+ "_market",
+ "_account",
+ "_orders",
+ "_fills",
+ "_fill_cum_qty",
+ )
+
+ def __init__(self, state: StrategyState) -> None:
+ self._sim_ts_ns_local = state.sim_ts_ns_local
+
+ market_snapshot = {
+ instrument: MarketStateView(
+ last_ts_ns_local=market.last_ts_ns_local,
+ last_ts_ns_exch=market.last_ts_ns_exch,
+ best_bid=market.best_bid,
+ best_ask=market.best_ask,
+ mid=market.mid,
+ best_bid_qty=market.best_bid_qty,
+ best_ask_qty=market.best_ask_qty,
+ tick_size=market.tick_size,
+ lot_size=market.lot_size,
+ contract_size=market.contract_size,
+ )
+ for instrument, market in state.market.items()
+ }
+ self._market: Mapping[str, MarketStateView] = MappingProxyType(market_snapshot)
+
+ account_snapshot = {
+ instrument: AccountStateView(
+ position=account.position,
+ balance=account.balance,
+ fee=account.fee,
+ trading_volume=account.trading_volume,
+ trading_value=account.trading_value,
+ num_trades=account.num_trades,
+ equity=account.equity,
+ initial_equity=account.initial_equity,
+ realized_pnl=account.realized_pnl,
+ )
+ for instrument, account in state.account.items()
+ }
+ self._account: Mapping[str, AccountStateView] = MappingProxyType(account_snapshot)
+
+ orders_snapshot: dict[str, Mapping[str, WorkingOrderView]] = {}
+ for instrument, by_id in state.orders.items():
+ orders_snapshot[instrument] = MappingProxyType(
+ {
+ client_order_id: WorkingOrderView(
+ instrument=working.instrument,
+ client_order_id=working.client_order_id,
+ side=working.side,
+ intended_price=working.intended_price,
+ intended_qty=working.intended_qty,
+ cum_filled_qty=working.cum_filled_qty,
+ remaining_qty=working.remaining_qty,
+ state=working.state,
+ submitted_ts_ns_local=working.submitted_ts_ns_local,
+ updated_ts_ns_local=working.updated_ts_ns_local,
+ )
+ for client_order_id, working in by_id.items()
+ }
+ )
+ self._orders: Mapping[str, Mapping[str, WorkingOrderView]] = MappingProxyType(
+ orders_snapshot
+ )
+
+ fills_snapshot = {
+ instrument: tuple(fill.model_copy(deep=True) for fill in fills)
+ for instrument, fills in state.fills.items()
+ }
+ self._fills: Mapping[str, tuple[FillEvent, ...]] = MappingProxyType(fills_snapshot)
+
+ fill_cum_snapshot: dict[str, Mapping[str, float]] = {}
+ for instrument, fill_cum_by_id in state.fill_cum_qty.items():
+ fill_cum_snapshot[instrument] = MappingProxyType(
+ {
+ client_order_id: float(qty)
+ for client_order_id, qty in fill_cum_by_id.items()
+ }
+ )
+ self._fill_cum_qty: Mapping[str, Mapping[str, float]] = MappingProxyType(
+ fill_cum_snapshot
+ )
+
+ @property
+ def sim_ts_ns_local(self) -> int:
+ return self._sim_ts_ns_local
+
+ @property
+ def market(self) -> Mapping[str, MarketStateView]:
+ return self._market
+
+ @property
+ def account(self) -> Mapping[str, AccountStateView]:
+ return self._account
+
+ @property
+ def orders(self) -> Mapping[str, Mapping[str, WorkingOrderView]]:
+ return self._orders
+
+ @property
+ def fills(self) -> Mapping[str, tuple[FillEvent, ...]]:
+ return self._fills
+
+ @property
+ def fill_cum_qty(self) -> Mapping[str, Mapping[str, float]]:
+ return self._fill_cum_qty
+
+ def get_mid(self, instrument: str) -> float:
+ market = self._market.get(instrument)
+ return 0.0 if market is None else market.mid
+
+ def get_contract_size(self, instrument: str) -> float:
+ market = self._market.get(instrument)
+ return 1.0 if market is None else market.contract_size
+
+ def get_tick_size(self, instrument: str) -> float:
+ market = self._market.get(instrument)
+ return 0.0 if market is None else market.tick_size
+
+ def get_lot_size(self, instrument: str) -> float:
+ market = self._market.get(instrument)
+ return 0.0 if market is None else market.lot_size
+
+ def get_total_equity(self) -> float:
+ return float(sum(account.equity for account in self._account.values()))
+
+ def get_total_pnl(self) -> float:
+ return float(sum(account.realized_pnl for account in self._account.values()))
+
+
class StrategyState:
"""High-level deterministic Strategy state keyed by instrument."""
@@ -212,6 +399,66 @@ def apply_order_submitted_event(self, event: OrderSubmittedEvent) -> None:
)
self._clear_inflight(event.instrument, event.client_order_id)
+ def _apply_terminal_order_event(
+ self,
+ *,
+ instrument: str,
+ client_order_id: str,
+ ts_ns_local_feedback: int,
+ terminal_state: str,
+ ) -> None:
+ self.update_timestamp(ts_ns_local_feedback)
+
+ order_bucket = self.orders.get(instrument)
+ if order_bucket is not None:
+ order_bucket.pop(client_order_id, None)
+
+ key = (instrument, client_order_id)
+ projection = self.canonical_orders.get(key)
+ if projection is None:
+ projection = CanonicalOrderProjection(
+ instrument=instrument,
+ client_order_id=client_order_id,
+ state=terminal_state,
+ submitted_ts_ns_local=ts_ns_local_feedback,
+ updated_ts_ns_local=ts_ns_local_feedback,
+ )
+ self.canonical_orders[key] = projection
+
+ projection.state = terminal_state
+ projection.updated_ts_ns_local = max(
+ projection.updated_ts_ns_local, ts_ns_local_feedback
+ )
+
+ self._clear_inflight(instrument, client_order_id)
+
+ def apply_order_canceled_event(self, event: OrderCanceledEvent) -> None:
+ """Reduce canonical canceled-order feedback into terminal order projection."""
+ self._apply_terminal_order_event(
+ instrument=event.instrument,
+ client_order_id=event.client_order_id,
+ ts_ns_local_feedback=event.ts_ns_local_feedback,
+ terminal_state="canceled",
+ )
+
+ def apply_order_rejected_event(self, event: OrderRejectedEvent) -> None:
+ """Reduce canonical rejected-order feedback into terminal order projection."""
+ self._apply_terminal_order_event(
+ instrument=event.instrument,
+ client_order_id=event.client_order_id,
+ ts_ns_local_feedback=event.ts_ns_local_feedback,
+ terminal_state="rejected",
+ )
+
+ def apply_order_expired_event(self, event: OrderExpiredEvent) -> None:
+ """Reduce canonical expired-order feedback into terminal order projection."""
+ self._apply_terminal_order_event(
+ instrument=event.instrument,
+ client_order_id=event.client_order_id,
+ ts_ns_local_feedback=event.ts_ns_local_feedback,
+ terminal_state="expired",
+ )
+
def apply_control_time_event(self, event: ControlTimeEvent) -> None:
"""Reduce canonical control-time Event without side effects."""
self.update_timestamp(event.ts_ns_local_control)
diff --git a/tradingchassis_core/core/domain/types.py b/tradingchassis_core/core/domain/types.py
index f3261cc..fb26370 100644
--- a/tradingchassis_core/core/domain/types.py
+++ b/tradingchassis_core/core/domain/types.py
@@ -214,6 +214,27 @@ class OrderSubmittedEvent(BaseModel):
model_config = ConfigDict(extra="forbid")
+class OrderCanceledEvent(BaseModel):
+ ts_ns_local_feedback: int = Field(..., gt=0)
+ instrument: str = Field(..., min_length=1)
+ client_order_id: str = Field(..., min_length=1)
+ model_config = ConfigDict(extra="forbid")
+
+
+class OrderRejectedEvent(BaseModel):
+ ts_ns_local_feedback: int = Field(..., gt=0)
+ instrument: str = Field(..., min_length=1)
+ client_order_id: str = Field(..., min_length=1)
+ model_config = ConfigDict(extra="forbid")
+
+
+class OrderExpiredEvent(BaseModel):
+ ts_ns_local_feedback: int = Field(..., gt=0)
+ instrument: str = Field(..., min_length=1)
+ client_order_id: str = Field(..., min_length=1)
+ model_config = ConfigDict(extra="forbid")
+
+
class ControlTimeEvent(BaseModel):
ts_ns_local_control: int = Field(..., gt=0)
reason: str = Field(..., min_length=1)
diff --git a/tradingchassis_core/core/execution_control/execution_control.py b/tradingchassis_core/core/execution_control/execution_control.py
index 4dd3dd7..f008cfa 100644
--- a/tradingchassis_core/core/execution_control/execution_control.py
+++ b/tradingchassis_core/core/execution_control/execution_control.py
@@ -249,7 +249,7 @@ def handle_cancel_against_queued_only_state(
replaced_in_queue: list[tuple[OrderIntent, OrderIntent]],
handled_in_queue: list[OrderIntent],
) -> bool:
- """CANCEL against queued-only state: remove queued intents, do not send cancel."""
+ """CANCEL against queued-only state: remove queued Intents, do not send cancel."""
if it.intent_type != "cancel":
return False
diff --git a/tradingchassis_core/core/risk/execution_constraints_policy.py b/tradingchassis_core/core/risk/execution_constraints_policy.py
index 5a08003..bf50d11 100644
--- a/tradingchassis_core/core/risk/execution_constraints_policy.py
+++ b/tradingchassis_core/core/risk/execution_constraints_policy.py
@@ -1,6 +1,6 @@
"""Execution-constraint normalization and validation logic.
-This module applies instrument/execution constraints to order intents, such as
+This module applies instrument/execution constraints to order Intents, such as
tick/lot rounding, post-only enforcement, and minimum notional checks.
"""
diff --git a/tradingchassis_core/core/risk/risk_policy.py b/tradingchassis_core/core/risk/risk_policy.py
index 83a0e69..1cad450 100644
--- a/tradingchassis_core/core/risk/risk_policy.py
+++ b/tradingchassis_core/core/risk/risk_policy.py
@@ -111,7 +111,7 @@ def normalize_intent(self, it: OrderIntent, state: StrategyState) -> Normalizati
def validate_intent(self, it: OrderIntent, state: StrategyState) -> tuple[bool, str]:
"""Outbound Intent sanity.
- Even if your schemas allow 0 placeholders, outbound intents should still be sensible.
+ Even if your schemas allow 0 placeholders, outbound Intents should still be sensible.
"""
if it.ts_ns_local <= 0:
return False, RejectReason.INVALID_TS
@@ -229,14 +229,14 @@ def portfolio_gross_notional(self, state: StrategyState) -> float | None:
return total
def quote_book_global(self, state: StrategyState) -> dict[tuple[str, str], tuple[float, float]]:
- """Build a best-effort global quote book including queued intents.
+ """Build a best-effort global quote book including queued Intents.
Returns:
Mapping (instrument, client_order_id) -> (abs_notional, signed_notional)
Notes:
- Working orders are sourced from StrategyState.orders.
- - Queued intents in StrategyState.queued_intents are applied on top.
+ - Queued Intents in StrategyState.queued_intents are applied on top.
- This is used only for quote-limits enforcement.
"""
@@ -254,7 +254,7 @@ def quote_book_global(self, state: StrategyState) -> dict[tuple[str, str], tuple
signed = notional if o.side == "buy" else -notional
book[(instr, oid)] = (notional, signed)
- # Queued intents (apply on top of working)
+ # Queued Intents (apply on top of working)
for instr, q in state.queued_intents.items():
contract_size = state.get_contract_size(instr)
for qi in q: