diff --git a/.github/pull-request-template.md b/.github/pull-request-template.md
index fd4f083..5434298 100644
--- a/.github/pull-request-template.md
+++ b/.github/pull-request-template.md
@@ -23,7 +23,7 @@ Describe key implementation decisions.
- [ ] Change preserves deterministic behavior
- [ ] Tests confirm reproducibility
-- [ ] No hidden state introduced
+- [ ] No hidden State introduced
## Performance Impact
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 97050db..dc57654 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,20 +4,34 @@ This changelog starts from the clean Core package baseline.
## [Unreleased]
+### Removed
+
+- `StrategyState.pop_queued_intents` (unused; Execution Control uses per-order queue helpers).
+- `fold_event_stream_entries` and root export (use `process_event_entry` in order).
+- Unused telemetry models in `core/events/events.py`.
+- Unused `combine_candidate_intents` helper.
+- Root exports for Execution Control apply detail types and `apply_execution_control_plan` (internal Execution Control apply stage).
+
### Added
- Deterministic `run_core_step` and `run_core_wakeup_step` architecture.
-- CoreWakeupStep final-state Strategy evaluation: reduce all entries, then `CoreWakeupStrategyEvaluator` once.
+- CoreWakeupStep final Strategy evaluation: reduce all entries, then `CoreWakeupStrategyEvaluator` once.
- Canonical Event input models and `EventStreamEntry`/`ProcessingPosition`.
-- Intent candidate record pipeline 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.
+- 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`.
### 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.
### Removed
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1f984aa..4a74f28 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 3428744..fa19876 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# TradingChassis Core
-`tradingchassis_core` is the stable deterministic trading decision kernel
+`tradingchassis_core` is the stable deterministic trading decision engine
for TradingChassis: an Event-step engine that applies ordered canonical Events
(the Event Stream under Processing Order and Configuration)
and produces `CoreStepResult` outputs—including Strategy-generated and
@@ -19,9 +19,45 @@ simulation, Live trading, Venue Adapters, and infrastructure around you change.
> Terminology: Definitions and related terms match the
> [`canonical terminology`](https://tradingchassis.github.io/docs/latest/00-guides/terminology/).
-> In-repo pointers: [`core/docs/README.md`](docs/README.md) and
+> In-repo pointers: [`core/docs/index.md`](docs/index.md) and
> [`core/docs/code-map/core-pipeline-map.md`](docs/code-map/core-pipeline-map.md).
+
+
+## Internally wired vs externally supplied
+
+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)
+
+- `process_event_entry` / `process_canonical_event` and canonical reducers
+- Candidate combination, dominance, and reconciliation (`combine_candidate_intent_records`)
+- Policy admission **mechanism** when `CorePolicyAdmissionContext` is provided
+- Execution Control plan/apply **mechanism** when policy + apply contexts are provided
+- `CoreStepResult` / `CoreStepDecision` production
+
+### Externally supplied extension points
+
+- **Strategy evaluator** — `CoreStepStrategyEvaluator` or `CoreWakeupStrategyEvaluator`
+- **Policy evaluator** — any object implementing `PolicyIntentEvaluator` (passed via `CorePolicyAdmissionContext`)
+- **Execution Control** — `ExecutionControl` instance (passed via `CoreExecutionControlApplyContext`)
+- **Configuration** — optional `CoreConfiguration` for positioned market reduction
+- **Event bus** — `StrategyState` requires an `EventBus`; use `NullEventBus` for standalone Core/tests
+
+### Convenience implementations (optional; not wired by default)
+
+- **Risk Engine** (`RiskEngine`) — provided `PolicyIntentEvaluator` with built-in policy gates
+- **`ExecutionControl`** — provided queue/rate/inflight implementation
+- **`NullEventBus`** — discards observability events for tests and examples
+
+The minimal quickstart uses an inline allow-all policy to stay small. That does
+**not** mean the Risk Engine is unused or dead. Use `RiskEngine` when you want the
+built-in Risk Engine policy behavior. See `examples/core_step_quickstart.py` (minimal) and
+`examples/core_step_with_risk_engine.py` (Risk Engine variant).
+
+
+
## Why it is relevant
Trading systems often drift when Backtesting logic, Live logic, policy limits, and
@@ -35,40 +71,12 @@ deployments, different Venue Adapters) may change; Core should not.
A typical notebook or one-off Backtesting script inlines feed handling, Strategy rules,
Risk Engine (policy) checks, and how orders are sent in one place. That is fast to sketch but
tends to fork: the Live path reimplements similar ideas with different bugs and
-timing. Core keeps the decision kernel in one place: Runtimes normalize inputs into
+timing. Core keeps the decision engine in one place: Runtimes normalize inputs into
canonical Events, invoke Core, and perform Execution and dispatch outside Core using
`CoreStepResult`; Strategy, Risk Engine,
and Execution Control semantics stay identical across those Runtimes when the
Event Stream and Configuration match.
-## What it gives you
-
-| What you get | Why it matters |
-| --- | --- |
-| 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) |
-| `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 |
-
-## Control time and scheduling (Core)
-
-`ControlSchedulingObligation` is a **non-canonical**, time-dependent hint produced
-when Execution Control **apply** defers for **rate limits**. **Inflight** gating is
-**feedback-dependent** and does not, by default, produce this obligation; queued
-work is reconsidered after canonical execution Events update state. Runtimes must
-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).
-| Runtime-independent package | Test trading semantics without production I/O; explicit ownership boundary |
-| Shared kernel across environments | Serious Backtesting and Live parity for the decision engine—no secondary copy of Strategy/Risk Engine/Execution Control code elsewhere |
-
-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.
-
-## Why it matters for trading
-
The gap between tested behavior and Live trading behavior can dominate outcomes. **Backtesting**
is only a reliable guide if the **same** Core decision logic—Strategy,
Risk Engine, Execution Control—can drive Live when the Event Stream and
@@ -76,12 +84,39 @@ Configuration are comparable. Deterministic Core logic driven by canonical Event
makes that logic reproducible and unit-testable without duplicating it in each
Runtime.
-This package does **not** guarantee profitable trading, perfect Backtesting/Live
+This package does **not** guarantee profitable trading, perfect Backtesting and Live
equality, or identical fills. It **does** remove a major class of drift: the
decision engine itself. Wall-clock scheduling, Venue behavior, Venue Adapter
mapping, latency, liquidity, market-data quality, and infrastructure failure modes
stay in the Runtime, Venue Adapter, and Venue—not in Core.
+
+
+## What it gives you
+
+| What you get | Why it matters |
+| --- | --- |
+| 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) |
+| `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
+and Live: the same canonical Event + `run_core_step` / reduction APIs
+can drive both worlds when each Runtime constructs comparable `EventStreamEntry`
+sequences under the same Configuration. Normalizing feeds, timestamps, and
+control semantics before they enter Core narrows unnecessary divergence.
+
+Core does not remove every simulation-vs-production gap. Individual Venue
+behavior, latency, fills and liquidity, market-data quality, Venue Adapter
+behavior, Runtime scheduling, and infrastructure failure modes can still
+differ and must be modeled outside Core. What Core removes is a major
+source of mismatch—duplicating and subtly diverging Strategy/Risk Engine/
+Execution Control itself.
+
+
+
## How it fits into a full system
Backtesting Runtimes, Live Runtimes, and local Research or simulation harnesses can
@@ -93,37 +128,24 @@ Execution, scheduling glue, and Control-Time Event injection when a Control Sche
```mermaid
flowchart TB
- R1["Runtime:
canonical Event"] --> Entry["EventStreamEntry:
canonical Event + ProcessingPosition"]
- Entry --> Core["TradingChassis Core:
CoreStep / CoreWakeupStep"]
- Core --> Result["CoreStepResult:
dispatchable Intents + Control Scheduling Obligation"]
- Result --> R2["Runtime:
dispatch / scheduling / I/O"]
+ R1["**Runtime**
canonical Event"] --> Entry["**EventStreamEntry**
canonical Event + ProcessingPosition"]
+ Entry --> Core["**TradingChassis Core**
CoreStep / CoreWakeupStep"]
+ Core --> Result["**CoreStepResult**
dispatchable Intents + Control Scheduling Obligation"]
+ Result --> R2["**Runtime**
dispatch / scheduling / I/O"]
```
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.
-## Backtesting and Live parity
-
-Core is designed to reduce decision-logic drift between Backtesting
-and Live: the same canonical Event + `run_core_step` / reduction APIs
-can drive both worlds when each Runtime constructs comparable `EventStreamEntry`
-sequences under the same Configuration. Normalizing feeds, timestamps, and
-control semantics before they enter Core narrows unnecessary divergence.
-
-Core does not remove every simulation-vs-production gap. Individual Venue
-behavior, latency, fills and liquidity, market-data quality, Venue Adapter
-behavior, Runtime scheduling, and infrastructure failure modes can still
-differ and must be modeled outside Core. What Core removes is a major
-source of mismatch—duplicating and subtly diverging Strategy/Risk Engine/
-Execution Control itself.
+
## When to use `tradingchassis_core`
- Building an internal trading system where Backtesting and Live should share decision semantics.
-- Wanting a deterministic Strategy / Risk Engine / Execution Control kernel.
+- Wanting a deterministic Strategy / Risk Engine / Execution Control engine.
- Separating trading semantics from Venue Adapters, I/O, and Kubernetes wiring.
- Testing decisions and Intents without full Backtesting or Live machinery.
- Sharing one decision path across simulation and production.
@@ -135,34 +157,7 @@ Execution Control itself.
- You expect this package to ship a full Kubernetes Runtime, deployment manifests, or production operations.
- You expect Core to execute orders, talk to Venues, replace Venue Adapters, or perform external dispatch.
-## Full pipeline
-
-Internal processing pipeline, in sequential order:
-
-```text
-Runtime reduces to canonical Events
-
- -> process_event_entry / process_canonical_event
- -> Strategy evaluation
- -> generated Intents
- -> candidate records
- -> dominance / reconciliation
- -> Risk Engine (policy)
- -> Execution Control plan/apply
- -> CoreStepResult.dispatchable_intents
-
-Runtime dispatches Intents into Orders
-```
-
-## Input / Core / Output / Not Owned By Core
-
-- Input: `EventStreamEntry` values with canonical Events and Event Stream position.
-- Core does: deterministic reduction, Strategy evaluation boundary, candidate
- merge/dominance, Risk Engine (policy), Execution Control planning/apply.
-- Output: `CoreStepResult` with generated/candidate Intents, optional
- `dispatchable_intents`, and optional `control_scheduling_obligation`.
-- Not owned by Core: raw market/feed I/O, Venue Adapters, external dispatch,
- credentials/environment wiring, Runtime orchestration, Kubernetes/deployment.
+
## Quickstart
@@ -172,6 +167,12 @@ Run the quickstart
python examples/core_step_quickstart.py
```
+Optional Risk Engine policy example (same Pipeline, built-in policy):
+
+```bash
+python examples/core_step_with_risk_engine.py
+```
+
or minimal shape:
```python
@@ -197,7 +198,70 @@ print(result.generated_intents, result.dispatchable_intents)
# Expected: () () — no Strategy or Risk Engine/Execution Control path in this snippet.
```
-See `examples/core_step_quickstart.py` for a full runnable walkthrough.
+See `examples/core_step_quickstart.py` for a full runnable walkthrough and
+[`docs/how-to/use-policy-evaluator.md`](docs/how-to/use-policy-evaluator.md) for policy extension points.
+Planned U3 cleanup candidates: [`docs/roadmap/dead-code-cleanup-candidates.md`](docs/roadmap/dead-code-cleanup-candidates.md).
+
+
+
+## Full Pipeline
+
+Internal processing Pipeline, in sequential order:
+
+```text
+Runtime reduces to canonical Events
+
+ -> process_event_entry / process_canonical_event
+ -> Strategy evaluation
+ -> generated Intents
+ -> candidate records
+ -> dominance / reconciliation
+ -> Risk Engine (policy)
+ -> Execution Control plan/apply
+ -> CoreStepResult.dispatchable_intents
+
+Runtime dispatches Intents into Orders
+```
+
+
+
+## Input / Core / Output / Not Owned By Core
+
+- Input: `EventStreamEntry` values with canonical Events and Event Stream position.
+- Core does: deterministic reduction, Strategy evaluation boundary, candidate
+ merge/dominance, Risk Engine (policy), Execution Control planning/apply.
+- Output: `CoreStepResult` with generated/candidate Intents, optional
+ `dispatchable_intents`, and optional `control_scheduling_obligation`.
+- Not owned by Core: raw market/feed I/O, Venue Adapters, external dispatch,
+ credentials/environment wiring, Runtime orchestration, Kubernetes/deployment.
+
+### Ownership Boundary
+
+| Core owns | Runtime owns |
+| --- | --- |
+| canonical models/contracts | raw I/O and feed adapters |
+| State reduction and ordering | Venue Adapters and transport |
+| Strategy evaluation boundary | adapter-side Execution |
+| candidate Intents and reconciliation | credentials/env wiring |
+| Risk Engine (policy) | Backtesting and Live orchestration |
+| Execution Control | Kubernetes/deployment |
+| `CoreStepResult` decision contract | Runtime lifecycle glue |
+
+
+
+## Control time and scheduling (Core)
+
+`ControlSchedulingObligation` is a **non-canonical**, time-dependent hint produced
+when Execution Control **apply** defers for **rate limits**. **Inflight** gating is
+**feedback-dependent** and does not, by default, produce this obligation; queued
+work is reconsidered after canonical execution Events update State. Runtimes must
+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
+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.
## Public Entrypoints
@@ -209,8 +273,8 @@ See `examples/core_step_quickstart.py` for a full runnable walkthrough.
| `run_core_wakeup_step` | Reduce all entries, evaluate Strategy once, then one decision pass |
| `process_event_entry` | Reduce one `EventStreamEntry` into `StrategyState` |
| `process_canonical_event` | Reduce one canonical Event into `StrategyState` |
-
-
+| `PolicyIntentEvaluator` | Protocol for policy admission (`evaluate_policy_intent`) |
+| Risk Engine (`RiskEngine`) | Convenience `PolicyIntentEvaluator` implementation |
## CoreWakeupStep semantics
@@ -221,21 +285,11 @@ canonical entries, and Core reduces them in that order before making one decisio
- `run_core_step` handles one `EventStreamEntry`.
- `run_core_wakeup_step` handles an ordered batch of `EventStreamEntry` values.
- Runtime is responsible for normalizing and ordering simultaneous raw inputs.
-- Core reduces all wakeup entries in order, evaluates Strategy once on the final state,
- then runs Policy Admission and ExecutionControl once.
+- Core reduces all wakeup entries in order, evaluates Strategy once on the final State,
+ then runs Policy Admission and Execution Control once.
- Runtime dispatches after the returned `CoreStepResult`.
-## Ownership Boundary
-
-| Core owns | Runtime owns |
-| --- | --- |
-| canonical models/contracts | raw I/O and feed adapters |
-| State reduction and ordering | Venue Adapters and transport |
-| Strategy evaluation boundary | adapter-side Execution |
-| candidate Intents and reconciliation | credentials/env wiring |
-| Risk Engine (policy) | Backtesting/Live orchestration |
-| Execution Control | Kubernetes/deployment |
-| `CoreStepResult` decision contract | Runtime lifecycle glue |
+
## Developer Commands
@@ -244,6 +298,7 @@ From root:
```bash
python -m pip install -e ".[dev]"
python examples/core_step_quickstart.py
+python examples/core_step_with_risk_engine.py
./scripts/check.sh
python -m build
```
diff --git a/SECURITY.md b/SECURITY.md
index 5228104..aa5e6af 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 cfd4ed3..ca5336e 100644
--- a/docs/code-map/core-pipeline-map.md
+++ b/docs/code-map/core-pipeline-map.md
@@ -1,6 +1,6 @@
# 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
@@ -29,7 +29,7 @@ TradingChassis Core.
## Determinism notes
- Processing Order monotonicity is enforced by `ProcessingPosition`.
-- Core logic is side-effect-safe apart from deterministic state mutation.
+- Core logic is side-effect-safe apart from deterministic State mutation.
- Runtime adapters and external dispatch concerns are outside Core.
@@ -44,12 +44,36 @@ Wakeup flow:
1. Runtime supplies an ordered batch of `EventStreamEntry` values.
2. `run_core_wakeup_reduction` calls `process_event_entry` for each entry in order.
-3. `CoreWakeupStrategyEvaluator.evaluate` runs **once** on the fully reduced state
+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
once, applies dominance/reconciliation once, Policy Admission once, and
- ExecutionControl plan/apply once.
+ Execution Control plan/apply once.
5. `CoreStepResult.dispatchable_intents` is returned; Runtime dispatches later.
`run_core_step` remains single-entry: one reduction, one step-level Strategy evaluation,
one decision pass.
+
+## Internally wired vs externally supplied
+
+### Internally wired
+
+- Steps 1–3, 5, and 8 in the flow above (reduction, candidates, `CoreStepResult`)
+- Policy admission **machinery** when `CorePolicyAdmissionContext` is provided
+- Execution Control plan/apply **machinery** when apply context is provided
+
+### Externally supplied extension points
+
+- **Strategy** — `CoreStepStrategyEvaluator` or `CoreWakeupStrategyEvaluator`
+- **Policy** — `PolicyIntentEvaluator` via `CorePolicyAdmissionContext`
+- **Execution Control instance** — `ExecutionControl` via `CoreExecutionControlApplyContext`
+- **Configuration** — optional `CoreConfiguration`
+- **Event bus** — `StrategyState(event_bus=...)`; `NullEventBus` for standalone use
+
+### Convenience implementations
+
+- Risk Engine (`RiskEngine`) — optional built-in `PolicyIntentEvaluator` (`examples/core_step_with_risk_engine.py`)
+- `ExecutionControl` — default queue/rate/inflight behavior (instance still supplied by caller)
+- `NullEventBus` — no-op bus for tests and examples
+
+See `../reference/public-api.md` and `../how-to/use-policy-evaluator.md`.
diff --git a/docs/code-map/repository-map.md b/docs/code-map/repository-map.md
index d050461..d3c3d34 100644
--- a/docs/code-map/repository-map.md
+++ b/docs/code-map/repository-map.md
@@ -6,15 +6,16 @@ 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/`: internal Event bus/sink utilities
+- `tradingchassis_core/core/events/`: Event bus/sink utilities (`NullEventBus`; `LoggingEventSink` for Runtime)
## Tests and examples
- `tests/semantics/`: focused contract and deterministic behavior tests
-- `examples/core_step_quickstart.py`: public-import quickstart
+- `examples/core_step_quickstart.py`: minimal inline-policy quickstart
+- `examples/core_step_with_risk_engine.py`: Risk Engine policy quickstart
## Top-level package docs and metadata
@@ -30,7 +31,7 @@ Core owns:
- canonical Events and Processing Order contracts
- deterministic reduction and step decisions
-- Intent candidate, Risk Engine (policy), Execution Control outputs
+- Intent candidate records, Risk Engine (policy), Execution Control outputs
Core does not own:
diff --git a/docs/flows/control-time-and-scheduling.md b/docs/flows/control-time-and-scheduling.md
index 1257ce3..0d13594 100644
--- a/docs/flows/control-time-and-scheduling.md
+++ b/docs/flows/control-time-and-scheduling.md
@@ -9,15 +9,15 @@ to Execution Control deferral.
- **ControlSchedulingObligation** — Non-canonical Core output: a structured hint
that a **time-dependent** recheck may be useful. It is **not** part of the
canonical Event Stream and does not mutate `StrategyState`.
-- **ControlTimeEvent** — Canonical **control** category Event. It becomes part of
+- **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).
-- **Inflight** — Core-side **intent-operation** gating: a sendability / operation
+- **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
+ 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.
-- **Rate-limit deferral** — Execution control blocks dispatch because the
+ 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`).
- **Inflight deferral** — Dispatch is blocked because **inflight** gating applies,
@@ -33,11 +33,11 @@ to Execution Control deferral.
**Not in scope for the current contract:** inflight timeout, wall-clock recovery,
or “synthetic” obligations for inflight-only waits.
-**Not implied:** every queued intent produces a scheduling obligation or a future
+**Not implied:** every queued Intent produces a scheduling obligation or a future
`ControlTimeEvent`. Obligations are for **rate-limit** rechecks in the current
Core slice.
-## Clean Core pipeline (unchanged)
+## Clean Core Pipeline (unchanged)
1. `EventStreamEntry`
2. `process_event_entry` / `process_canonical_event`
@@ -55,9 +55,9 @@ they are selected only in the mutable **apply** stage (`apply_execution_control_
## Runtime ownership
-- Runtimes **must not** mutate Core queues (`StrategyState.queued_intents`, etc.)
+- Runtimes **must not** mutate Core Queues (`StrategyState.queued_intents`, etc.)
directly outside the normal Core step / Execution Control apply path.
-- Queue flush / sendability decisions remain **ExecutionControl-owned** inside
+- Queue flush / sendability decisions remain **Execution Control-owned** inside
Core when `CoreExecutionControlApplyContext` is supplied to `run_core_step` /
wakeup APIs.
diff --git a/docs/how-to/update-core-step-pipeline.md b/docs/how-to/update-core-step-pipeline.md
index aed51cb..e888ee0 100644
--- a/docs/how-to/update-core-step-pipeline.md
+++ b/docs/how-to/update-core-step-pipeline.md
@@ -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.
@@ -30,6 +30,6 @@ When updating wakeup behavior:
1. Keep `run_core_wakeup_reduction` as reduction-only (no per-entry Strategy calls).
2. Use `CoreWakeupStrategyEvaluator` and `wakeup_strategy_evaluator=` for batch evaluation.
-3. Preserve one Policy Admission and one ExecutionControl apply per wakeup in
+3. Preserve one Policy Admission and one Execution Control apply per wakeup in
`run_core_wakeup_decision`.
4. Add tests in `tests/semantics/test_core_wakeup_final_state.py`.
diff --git a/docs/how-to/update-policy-and-execution-control.md b/docs/how-to/update-policy-and-execution-control.md
index 3a825be..0768b97 100644
--- a/docs/how-to/update-policy-and-execution-control.md
+++ b/docs/how-to/update-policy-and-execution-control.md
@@ -9,7 +9,9 @@ The Risk Engine (policy) and Execution Control are separate deterministic phases
- Core integration:
`core/domain/policy_risk_decision.py` and `run_core_step` policy phase
- Built-in policy-only evaluator:
- `core/risk/risk_engine.py`
+ `core/risk/risk_engine.py` (public Risk Engine class `RiskEngine`; internal `RiskPolicy` / `ExecutionConstraintsPolicy`)
+- User guide:
+ `use-policy-evaluator.md`
When updating Risk Engine policy behavior:
diff --git a/docs/how-to/use-policy-evaluator.md b/docs/how-to/use-policy-evaluator.md
new file mode 100644
index 0000000..9d9e071
--- /dev/null
+++ b/docs/how-to/use-policy-evaluator.md
@@ -0,0 +1,54 @@
+# How to use a policy evaluator
+
+Core policy admission is optional. When you pass `CorePolicyAdmissionContext`, Core
+calls your evaluator for each **generated** candidate intent (queued candidates
+passthrough unchanged).
+
+## Extension point: `PolicyIntentEvaluator`
+
+Root export: `tradingchassis_core.PolicyIntentEvaluator`
+
+Implement:
+
+```python
+def evaluate_policy_intent(
+ self,
+ *,
+ intent: OrderIntent,
+ state: StrategyState,
+ now_ts_ns_local: int,
+) -> tuple[bool, str | None]:
+ ...
+```
+
+Pass the instance via `CorePolicyAdmissionContext(policy_evaluator=..., now_ts_ns_local=...)`.
+
+Any object satisfying this contract works. Core does not require `RiskEngine`.
+
+## Convenience implementation: Risk Engine (`RiskEngine`)
+
+The built-in **Risk Engine** (`RiskEngine`) implements `PolicyIntentEvaluator` with
+policy gates (trading enabled, max loss, normalization, hard limits). Configure
+with `RiskConfig`.
+
+Runnable example:
+
+```bash
+cd core
+python examples/core_step_with_risk_engine.py
+```
+
+Minimal quickstart (`examples/core_step_quickstart.py`) uses an inline allow-all
+policy to stay small. That does not mean the Risk Engine is unused.
+
+## Execution Control apply
+
+Policy admission alone does not mutate queues or produce dispatchables. Also pass
+`CoreExecutionControlApplyContext` with a supplied `ExecutionControl` instance and
+set `activate_dispatchable_outputs=True` when you want `CoreStepResult.dispatchable_intents`.
+
+See also:
+
+- `reference/public-api.md`
+- `code-map/core-pipeline-map.md`
+- `update-policy-and-execution-control.md`
diff --git a/docs/index.md b/docs/index.md
index 433938a..ea5cc1e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -7,11 +7,13 @@ 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
- `how-to/update-policy-and-execution-control.md`: changing Risk Engine / Execution Control behavior
+- `how-to/use-policy-evaluator.md`: `PolicyIntentEvaluator`, `RiskEngine`, and examples
+- `roadmap/dead-code-cleanup-candidates.md`: U3 removal candidates (audit before delete)
## Package Purpose
@@ -38,7 +40,15 @@ canonical contracts, State reduction, and step-level decision outputs.
Pydantic contract models in `tradingchassis_core/core/domain/types.py` are the
source of truth for canonical Event/Intent schemas.
+## Extension points (summary)
+
+- **Supplied by Runtime/tests:** Strategy evaluators, `PolicyIntentEvaluator`, `ExecutionControl`, `CoreConfiguration`, `EventBus`
+- **Convenience:** `RiskEngine`, `NullEventBus`
+- **Wired inside Core:** reduction, candidate reconciliation, policy/EC mechanisms, `CoreStepResult`
+
+Examples: `examples/core_step_quickstart.py` (inline policy), `examples/core_step_with_risk_engine.py` (Risk Engine).
+
## Out of Scope
- Runtime orchestration and Order lifecycle ownership
-- Venue Adapters, Backtesting/Live I/O, external dispatch
+- Venue Adapters, Backtesting and Live I/O, external dispatch
diff --git a/docs/reference/events-reference.md b/docs/reference/events-reference.md
index e1e79e4..f5a7e20 100644
--- a/docs/reference/events-reference.md
+++ b/docs/reference/events-reference.md
@@ -5,7 +5,7 @@ contracts. Pydantic models are the schema source of truth.
## Canonical Event Models
-- `MarketEvent`: book/trade market data input for state reduction
+- `MarketEvent`: book/trade market data input for State reduction
- `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;
diff --git a/docs/reference/public-api.md b/docs/reference/public-api.md
index ff7a77f..ccecdb6 100644
--- a/docs/reference/public-api.md
+++ b/docs/reference/public-api.md
@@ -2,6 +2,45 @@
The public package boundary is the `tradingchassis_core` root import.
+## Internally wired vs externally supplied
+
+### Internally wired (when step APIs run)
+
+These run inside Core when you call `run_core_step` / CoreWakeupStep APIs (no substitute
+implementation required):
+
+- `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
+
+### Externally supplied extension points
+
+| 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 |
+
+### Convenience implementations (optional)
+
+| 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 |
+
+**Internal (not public extension points):** `RiskPolicy`, `ExecutionConstraintsPolicy`,
+and other modules under `core/risk/` except `RiskEngine` / `RiskConfig`.
+
+Examples:
+
+- Minimal inline policy: `examples/core_step_quickstart.py`
+- Built-in Risk Engine policy: `examples/core_step_with_risk_engine.py`
+
## Canonical Events
- `MarketEvent`
@@ -23,21 +62,30 @@ The public package boundary is the `tradingchassis_core` root import.
- `EventStreamEntry`
- `ProcessingPosition`
-- `CorePolicyAdmissionContext`
-- `CoreExecutionControlApplyContext`
+- `CorePolicyAdmissionContext` (holds `PolicyIntentEvaluator`)
+- `CoreExecutionControlApplyContext` (holds `ExecutionControl`)
- `CoreStepDecision`
- `CoreStepResult`
- `CoreWakeupReductionResult`
- `CoreWakeupStrategyContext`
- `CoreWakeupStrategyEvaluator`
+## Policy and risk
+
+- `PolicyIntentEvaluator` (protocol)
+- `PolicyRiskDecision`
+- `PolicyAdmissionResult`
+- `PolicyRejectedCandidate`
+- Risk Engine (`RiskEngine`) (convenience `PolicyIntentEvaluator`)
+- `RiskConfig`
+- `RiskConstraints` (data model; often built for Strategy via `RiskEngine.build_constraints`)
+
## Supporting deterministic models
- `CoreConfiguration`
- `StrategyState`
- `CandidateIntentRecord`
- `CandidateIntentOrigin`
-- `PolicyRiskDecision`
- `ExecutionControlDecision`
- `ExecutionControl`
- `ControlSchedulingObligation` (non-canonical; **rate-limit** recheck hint in the
@@ -51,12 +99,11 @@ The public package boundary is the `tradingchassis_core` root import.
- `ReplaceOrderIntent`
- `Price`
- `Quantity`
+- `NotionalLimits`
## Runtime-safe utilities
- `NullEventBus`
-- `RiskEngine` (Risk Engine; policy-only)
-- `RiskConfig`
## Publicly absent by design
@@ -67,3 +114,7 @@ The public package boundary is the `tradingchassis_core` root import.
- `OrderStateEvent`
- `DerivedFillEvent`
- `VenueAdapter` / `VenuePolicy`
+- `RiskPolicy` / `ExecutionConstraintsPolicy` (internal to Risk Engine / `RiskEngine`)
+- `fold_event_stream_entries` (removed U3; loop `process_event_entry` instead)
+- `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
new file mode 100644
index 0000000..945c14c
--- /dev/null
+++ b/docs/roadmap/dead-code-cleanup-candidates.md
@@ -0,0 +1,34 @@
+# U3 dead-code cleanup — status
+
+Phase U3 audit and cleanup (completed). This document records what was removed,
+what was kept, and what remains deferred.
+
+## Removed in U3
+
+| 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 |
+| `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` |
+| Root exports: `apply_execution_control_plan`, `ExecutionControlApplyContext`, `ExecutionControlApplyResult`, `ExecutionControlBlockedRecord`, `ExecutionControlDispatchableRecord`, `ExecutionControlHandledRecord` | No monorepo consumers outside Core; pipeline uses `CoreStepResult` / `CoreStepDecision` |
+
+## Kept (monorepo usage)
+
+| Item | Rationale |
+| --- | --- |
+| `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 |
+
+## Deferred (intentionally not removed)
+
+| Item | Evidence needed before future removal |
+| --- | --- |
+| `PolicyAdmissionResult` / `PolicyRejectedCandidate` at root | Exported for Policy Admission introspection; only used inside Core today — narrow export if no external adopters |
+| `SlotKey` in Core-only docs | Document as Runtime/strategy helper if Core export is ever questioned again |
+
+## Not candidates
+
+See U2 extension-point docs: Risk Engine, Execution Control, `FillEvent`, and Core step APIs remain public and tested.
diff --git a/examples/core_step_quickstart.py b/examples/core_step_quickstart.py
index c7187de..cd308d8 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/examples/core_step_with_risk_engine.py b/examples/core_step_with_risk_engine.py
new file mode 100644
index 0000000..d57f0ea
--- /dev/null
+++ b/examples/core_step_with_risk_engine.py
@@ -0,0 +1,115 @@
+"""Core-only example: built-in Risk Engine as ``PolicyIntentEvaluator``.
+
+Demonstrates the full policy + Execution Control apply path. The main quickstart
+(``examples/core_step_quickstart.py``) intentionally uses a minimal inline
+allow-all policy; use this example when you want the provided Risk Engine gates.
+
+Core returns ``dispatchable_intents``; a Runtime performs Execution and Venue
+dispatch later. Core does not dispatch externally.
+"""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+if __package__ in (None, ""):
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+
+import tradingchassis_core as tc
+from tradingchassis_core.core.domain.types import NotionalLimits
+
+INSTRUMENT = "BTC-USDC-PERP"
+
+
+# Strategy Evaluator: emits one generated Intent for this Core step.
+class _OneIntentEvaluator:
+ def evaluate(self, context: object) -> list[tc.NewOrderIntent]:
+ _ = context
+ return [
+ tc.NewOrderIntent(
+ intent_type="new",
+ ts_ns_local=1_000,
+ instrument=INSTRUMENT,
+ client_order_id="risk-example-intent",
+ intents_correlation_id="corr-risk-example",
+ 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",
+ )
+ ]
+
+
+def _control_entry(index: int, ts_ns_local: int) -> tc.EventStreamEntry:
+ # Control-Time Event as a simple canonical driver (scheduling obligations come from apply).
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=tc.ControlTimeEvent(
+ ts_ns_local_control=ts_ns_local,
+ reason="scheduled_control_recheck",
+ due_ts_ns_local=ts_ns_local,
+ realized_ts_ns_local=ts_ns_local,
+ obligation_reason="rate_limit",
+ obligation_due_ts_ns_local=ts_ns_local,
+ runtime_correlation=None,
+ ),
+ )
+
+
+def _risk_config() -> tc.RiskConfig:
+ return tc.RiskConfig(
+ scope="example",
+ trading_enabled=True,
+ notional_limits=NotionalLimits(
+ currency="USDC",
+ max_gross_notional=1_000_000.0,
+ max_single_order_notional=1_000_000.0,
+ ),
+ position_limits=None,
+ quote_limits=None,
+ order_rate_limits=None,
+ max_loss=None,
+ )
+
+
+def main() -> None:
+ 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=1_000,
+ ts_ns_exch=999,
+ )
+
+ # Built-in Risk Engine (`RiskEngine`) satisfies PolicyIntentEvaluator for policy admission.
+ policy_engine = tc.RiskEngine(_risk_config())
+ result = tc.run_core_step(
+ state,
+ _control_entry(0, 1_000),
+ strategy_evaluator=_OneIntentEvaluator(),
+ policy_admission_context=tc.CorePolicyAdmissionContext(
+ policy_evaluator=policy_engine,
+ now_ts_ns_local=1_000,
+ ),
+ execution_control_apply_context=tc.CoreExecutionControlApplyContext(
+ execution_control=tc.ExecutionControl(),
+ now_ts_ns_local=1_000,
+ activate_dispatchable_outputs=True,
+ ),
+ )
+
+ print("CoreStep with Risk Engine (Core-only; Runtime dispatches later)")
+ print("generated:", [i.client_order_id for i in result.generated_intents])
+ print("dispatchable:", [i.client_order_id for i in result.dispatchable_intents])
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pyproject.toml b/pyproject.toml
index c7b6052..bc0b421 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "TradingChassis-core"
version = "0.1.0"
-description = "Deterministic trading decision kernel."
+description = "Deterministic trading decision engine."
readme = "README.md"
requires-python = ">=3.11"
authors = [{ name = "TradingChassis Core Contributors" }]
diff --git a/tests/semantics/test_core_pipeline_clean.py b/tests/semantics/test_core_pipeline_clean.py
index 7daae42..921a3dc 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_core_wakeup_final_state.py b/tests/semantics/test_core_wakeup_final_state.py
index 67b1dab..bcde4f6 100644
--- a/tests/semantics/test_core_wakeup_final_state.py
+++ b/tests/semantics/test_core_wakeup_final_state.py
@@ -1,4 +1,4 @@
-"""Final-state CoreWakeupStep Strategy evaluation semantics (Phase WU2)."""
+"""Final CoreWakeupStep Strategy evaluation semantics (Phase WU2)."""
from __future__ import annotations
diff --git a/tests/semantics/test_fill_event_reduction.py b/tests/semantics/test_fill_event_reduction.py
new file mode 100644
index 0000000..f2fb495
--- /dev/null
+++ b/tests/semantics/test_fill_event_reduction.py
@@ -0,0 +1,93 @@
+"""FillEvent canonical reduction and Core step Pipeline coverage."""
+
+from __future__ import annotations
+
+import tradingchassis_core as tc
+
+INSTRUMENT = "BTC-USDC-PERP"
+CLIENT_ORDER_ID = "fill-order-1"
+FILL_TS = 200
+
+
+def _fill_event(*, cum_qty: float, remaining_qty: float | None) -> tc.FillEvent:
+ return tc.FillEvent(
+ ts_ns_exch=FILL_TS - 1,
+ ts_ns_local=FILL_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=cum_qty, unit="contracts"),
+ remaining_qty=(
+ None
+ if remaining_qty is None
+ else tc.Quantity(value=remaining_qty, unit="contracts")
+ ),
+ time_in_force="GTC",
+ liquidity_flag="taker",
+ )
+
+
+def _order_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=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 _fill_entry(index: int) -> tc.EventStreamEntry:
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=_fill_event(cum_qty=1.0, remaining_qty=0.0),
+ )
+
+
+def test_fill_event_via_process_event_entry_updates_state() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ tc.process_event_entry(state, _order_submitted_entry(0, 100))
+ assert state.has_working_order(INSTRUMENT, CLIENT_ORDER_ID)
+
+ tc.process_event_entry(state, _fill_entry(1))
+
+ assert state.sim_ts_ns_local == FILL_TS
+ fills = state.fills[INSTRUMENT]
+ assert len(fills) == 1
+ assert fills[0].client_order_id == CLIENT_ORDER_ID
+ assert state.fill_cum_qty[INSTRUMENT][CLIENT_ORDER_ID] == 1.0
+ assert not state.has_working_order(INSTRUMENT, CLIENT_ORDER_ID)
+
+
+def test_run_core_step_strategy_evaluator_sees_fill_reduced_state() -> None:
+ class _AssertFillReducedEvaluator:
+ def evaluate(self, context: tc.CoreStepStrategyContext) -> list[tc.OrderIntent]:
+ assert context.state.sim_ts_ns_local == FILL_TS
+ assert INSTRUMENT in context.state.fills
+ assert len(context.state.fills[INSTRUMENT]) == 1
+ assert isinstance(context.event, tc.FillEvent)
+ return []
+
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ tc.process_event_entry(state, _order_submitted_entry(0, 100))
+
+ result = tc.run_core_step(
+ state,
+ _fill_entry(1),
+ strategy_evaluator=_AssertFillReducedEvaluator(),
+ )
+
+ assert result.generated_intents == ()
+ assert result.candidate_intent_records == ()
+ assert result.dispatchable_intents == ()
+ assert result.core_step_decision is None
diff --git a/tests/semantics/test_public_api_clean.py b/tests/semantics/test_public_api_clean.py
index 6eecc4b..a204efa 100644
--- a/tests/semantics/test_public_api_clean.py
+++ b/tests/semantics/test_public_api_clean.py
@@ -19,6 +19,7 @@ def test_public_api_exposes_clean_core_symbols() -> None:
"CoreWakeupStrategyEvaluator",
"CoreStepResult",
"CoreStepDecision",
+ "PolicyIntentEvaluator",
"PolicyRiskDecision",
"ExecutionControlDecision",
"CandidateIntentRecord",
@@ -58,6 +59,13 @@ def test_public_api_does_not_expose_removed_compatibility_symbols() -> None:
"".join(["decide_", "intents"]),
"".join(["Venue", "Adapter"]),
"".join(["Venue", "Policy"]),
+ "fold_event_stream_entries",
+ "apply_execution_control_plan",
+ "ExecutionControlApplyContext",
+ "ExecutionControlApplyResult",
+ "ExecutionControlBlockedRecord",
+ "ExecutionControlDispatchableRecord",
+ "ExecutionControlHandledRecord",
)
for symbol in removed:
assert not hasattr(tc, symbol)
diff --git a/tests/semantics/test_risk_engine_pipeline_integration.py b/tests/semantics/test_risk_engine_pipeline_integration.py
new file mode 100644
index 0000000..743a27c
--- /dev/null
+++ b/tests/semantics/test_risk_engine_pipeline_integration.py
@@ -0,0 +1,141 @@
+"""RiskEngine as PolicyIntentEvaluator inside the real Core step pipeline."""
+
+from __future__ import annotations
+
+import tradingchassis_core as tc
+from tradingchassis_core.core.domain.types import NotionalLimits
+
+INSTRUMENT = "BTC-USDC-PERP"
+NOW_TS = 100
+
+
+class _OneNewIntentEvaluator:
+ def evaluate(self, context: object) -> list[tc.NewOrderIntent]:
+ _ = context
+ return [
+ tc.NewOrderIntent(
+ intent_type="new",
+ ts_ns_local=NOW_TS,
+ instrument=INSTRUMENT,
+ client_order_id="risk-pipeline-intent",
+ intents_correlation_id="corr-risk-pipeline",
+ 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",
+ )
+ ]
+
+
+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 _risk_config(*, trading_enabled: bool) -> tc.RiskConfig:
+ return tc.RiskConfig(
+ scope="test",
+ trading_enabled=trading_enabled,
+ notional_limits=NotionalLimits(
+ currency="USDC",
+ max_gross_notional=1_000_000.0,
+ max_single_order_notional=1_000_000.0,
+ ),
+ position_limits=None,
+ quote_limits=None,
+ order_rate_limits=None,
+ max_loss=None,
+ )
+
+
+def _prime_market(state: tc.StrategyState) -> None:
+ 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=NOW_TS,
+ ts_ns_exch=NOW_TS - 1,
+ )
+
+
+def test_risk_engine_accepts_generated_intent_in_run_core_step() -> None:
+ """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))
+
+ result = tc.run_core_step(
+ state,
+ _control_entry(0, NOW_TS),
+ strategy_evaluator=_OneNewIntentEvaluator(),
+ policy_admission_context=tc.CorePolicyAdmissionContext(
+ policy_evaluator=policy_engine,
+ now_ts_ns_local=NOW_TS,
+ ),
+ execution_control_apply_context=tc.CoreExecutionControlApplyContext(
+ execution_control=tc.ExecutionControl(),
+ now_ts_ns_local=NOW_TS,
+ activate_dispatchable_outputs=True,
+ ),
+ )
+
+ assert tuple(i.client_order_id for i in result.generated_intents) == ("risk-pipeline-intent",)
+ assert result.core_step_decision is not None
+ policy_decision = result.core_step_decision.policy_risk_decision
+ assert policy_decision is not None
+ assert tuple(i.client_order_id for i in policy_decision.accepted_intents) == (
+ "risk-pipeline-intent",
+ )
+ assert policy_decision.rejected_intents == ()
+ assert tuple(i.client_order_id for i in result.dispatchable_intents) == (
+ "risk-pipeline-intent",
+ )
+
+
+def test_risk_engine_rejects_generated_intent_when_trading_disabled() -> None:
+ """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))
+
+ result = tc.run_core_step(
+ state,
+ _control_entry(0, NOW_TS),
+ strategy_evaluator=_OneNewIntentEvaluator(),
+ policy_admission_context=tc.CorePolicyAdmissionContext(
+ policy_evaluator=policy_engine,
+ now_ts_ns_local=NOW_TS,
+ ),
+ execution_control_apply_context=tc.CoreExecutionControlApplyContext(
+ execution_control=tc.ExecutionControl(),
+ now_ts_ns_local=NOW_TS,
+ activate_dispatchable_outputs=True,
+ ),
+ )
+
+ assert tuple(i.client_order_id for i in result.generated_intents) == ("risk-pipeline-intent",)
+ assert len(result.candidate_intent_records) == 1
+ assert result.candidate_intent_records[0].origin is tc.CandidateIntentOrigin.GENERATED
+ assert result.dispatchable_intents == ()
+ assert result.core_step_decision is not None
+ assert len(result.core_step_decision.policy_rejected_intents) == 1
+ policy_decision = result.core_step_decision.policy_risk_decision
+ assert policy_decision is not None
+ assert policy_decision.accepted_intents == ()
+ assert len(policy_decision.rejected_intents) == 1
diff --git a/tests/semantics/test_runnable_risk_engine_example.py b/tests/semantics/test_runnable_risk_engine_example.py
new file mode 100644
index 0000000..3443c7e
--- /dev/null
+++ b/tests/semantics/test_runnable_risk_engine_example.py
@@ -0,0 +1,22 @@
+"""Smoke test for the runnable RiskEngine example script."""
+
+from __future__ import annotations
+
+import subprocess
+import sys
+from pathlib import Path
+
+_CORE_ROOT = Path(__file__).resolve().parents[2]
+_EXAMPLE = _CORE_ROOT / "examples" / "core_step_with_risk_engine.py"
+
+
+def test_runnable_risk_engine_example_exits_zero() -> None:
+ result = subprocess.run(
+ [sys.executable, str(_EXAMPLE)],
+ cwd=_CORE_ROOT,
+ check=False,
+ capture_output=True,
+ text=True,
+ )
+ assert result.returncode == 0, result.stderr
+ assert "risk-example-intent" in result.stdout
diff --git a/tradingchassis_core/__init__.py b/tradingchassis_core/__init__.py
index 4f5f241..52c928b 100644
--- a/tradingchassis_core/__init__.py
+++ b/tradingchassis_core/__init__.py
@@ -9,24 +9,16 @@
CandidateIntentRecord,
)
from tradingchassis_core.core.domain.configuration import CoreConfiguration
-from tradingchassis_core.core.domain.execution_control_apply import (
- ExecutionControlApplyContext,
- ExecutionControlApplyResult,
- ExecutionControlBlockedRecord,
- ExecutionControlDispatchableRecord,
- ExecutionControlHandledRecord,
- apply_execution_control_plan,
-)
from tradingchassis_core.core.domain.execution_control_decision import (
ExecutionControlDecision,
)
from tradingchassis_core.core.domain.policy_risk_decision import (
PolicyAdmissionResult,
+ PolicyIntentEvaluator,
PolicyRejectedCandidate,
PolicyRiskDecision,
)
from tradingchassis_core.core.domain.processing import (
- fold_event_stream_entries,
process_canonical_event,
process_event_entry,
)
@@ -101,7 +93,6 @@
"EventStreamEntry",
"process_canonical_event",
"process_event_entry",
- "fold_event_stream_entries",
"run_core_step",
"run_core_wakeup_reduction",
"run_core_wakeup_decision",
@@ -114,12 +105,7 @@
"CorePolicyAdmissionContext",
"CoreWakeupReductionResult",
"ExecutionControlDecision",
- "ExecutionControlApplyContext",
- "ExecutionControlApplyResult",
- "ExecutionControlBlockedRecord",
- "ExecutionControlDispatchableRecord",
- "ExecutionControlHandledRecord",
- "apply_execution_control_plan",
+ "PolicyIntentEvaluator",
"PolicyRiskDecision",
"PolicyRejectedCandidate",
"PolicyAdmissionResult",
diff --git a/tradingchassis_core/core/domain/__init__.py b/tradingchassis_core/core/domain/__init__.py
index 9fbf55e..1966e7e 100644
--- a/tradingchassis_core/core/domain/__init__.py
+++ b/tradingchassis_core/core/domain/__init__.py
@@ -4,14 +4,6 @@
CandidateIntentOrigin,
CandidateIntentRecord,
)
-from tradingchassis_core.core.domain.execution_control_apply import (
- ExecutionControlApplyContext,
- ExecutionControlApplyResult,
- ExecutionControlBlockedRecord,
- ExecutionControlDispatchableRecord,
- ExecutionControlHandledRecord,
- apply_execution_control_plan,
-)
from tradingchassis_core.core.domain.execution_control_decision import ExecutionControlDecision
from tradingchassis_core.core.domain.policy_risk_decision import (
PolicyAdmissionResult,
@@ -34,12 +26,6 @@
"CandidateIntentOrigin",
"CandidateIntentRecord",
"ExecutionControlDecision",
- "ExecutionControlApplyContext",
- "ExecutionControlApplyResult",
- "ExecutionControlBlockedRecord",
- "ExecutionControlDispatchableRecord",
- "ExecutionControlHandledRecord",
- "apply_execution_control_plan",
"PolicyRiskDecision",
"PolicyRejectedCandidate",
"PolicyAdmissionResult",
diff --git a/tradingchassis_core/core/domain/candidate_intent.py b/tradingchassis_core/core/domain/candidate_intent.py
index 2fba7c8..4f75467 100644
--- a/tradingchassis_core/core/domain/candidate_intent.py
+++ b/tradingchassis_core/core/domain/candidate_intent.py
@@ -1,4 +1,4 @@
-"""Core-owned non-canonical candidate intent provenance models."""
+"""Core-owned non-canonical candidate Intent provenance models."""
from __future__ import annotations
diff --git a/tradingchassis_core/core/domain/event_model.py b/tradingchassis_core/core/domain/event_model.py
index 9f51443..f2afe8a 100644
--- a/tradingchassis_core/core/domain/event_model.py
+++ b/tradingchassis_core/core/domain/event_model.py
@@ -11,12 +11,6 @@
OrderExecutionFeedbackEvent,
OrderSubmittedEvent,
)
-from tradingchassis_core.core.events.events import (
- DerivedPnLEvent,
- ExposureDerivedEvent,
- OrderStateTransitionEvent,
- RiskDecisionEvent,
-)
from tradingchassis_core.core.execution_control.types import ControlSchedulingObligation
@@ -41,15 +35,6 @@ class CanonicalEventCategory(str, Enum):
ControlTimeEvent: CanonicalEventCategory.CONTROL,
}
-TELEMETRY_EVENT_TYPES: frozenset[type[object]] = frozenset(
- {
- RiskDecisionEvent,
- DerivedPnLEvent,
- ExposureDerivedEvent,
- OrderStateTransitionEvent,
- }
-)
-
NON_CANONICAL_CONTROL_HELPER_TYPES: frozenset[type[object]] = frozenset(
{ControlSchedulingObligation}
)
diff --git a/tradingchassis_core/core/domain/execution_control_apply.py b/tradingchassis_core/core/domain/execution_control_apply.py
index ff614b0..ef5f279 100644
--- a/tradingchassis_core/core/domain/execution_control_apply.py
+++ b/tradingchassis_core/core/domain/execution_control_apply.py
@@ -137,7 +137,7 @@ def apply_execution_control_plan(
events.
``control_scheduling_obligation`` is selected only from **rate-limit**
- deferrals (time-dependent). **Inflight** gating queues or blocks work without
+ deferrals (time-dependent). **Inflight** gating Queues or blocks work without
adding a scheduling obligation; that case is resolved when later canonical
events update sendability (not via a Core-derived wake time in this slice).
"""
diff --git a/tradingchassis_core/core/domain/intent_combination.py b/tradingchassis_core/core/domain/intent_combination.py
index 5f575f7..4ae2d88 100644
--- a/tradingchassis_core/core/domain/intent_combination.py
+++ b/tradingchassis_core/core/domain/intent_combination.py
@@ -1,4 +1,4 @@
-"""Pure helper for Core-step candidate intent combination."""
+"""Pure helper for Core-step candidate Intent combination."""
from __future__ import annotations
@@ -35,22 +35,6 @@ def _dominance_rank(intent: OrderIntent) -> int:
return 0
-def combine_candidate_intents(
- *,
- generated_intents: Sequence[OrderIntent],
- queued_intents: Sequence[OrderIntent],
-) -> tuple[OrderIntent, ...]:
- """Compatibility helper returning only effective intent values.
-
- Prefer ``combine_candidate_intent_records`` when origin/provenance is needed.
- """
- records = combine_candidate_intent_records(
- generated_intents=generated_intents,
- queued_intents=queued_intents,
- )
- return tuple(record.intent for record in records)
-
-
def combine_candidate_intent_records(
*,
generated_intents: Sequence[OrderIntent],
diff --git a/tradingchassis_core/core/domain/policy_risk_decision.py b/tradingchassis_core/core/domain/policy_risk_decision.py
index 19e364e..b3551ca 100644
--- a/tradingchassis_core/core/domain/policy_risk_decision.py
+++ b/tradingchassis_core/core/domain/policy_risk_decision.py
@@ -16,7 +16,7 @@
class PolicyIntentEvaluator(Protocol):
- """Side-effect-safe policy evaluator contract for one candidate intent."""
+ """Side-effect-safe policy evaluator contract for one candidate Intent."""
def evaluate_policy_intent(
self,
diff --git a/tradingchassis_core/core/domain/processing.py b/tradingchassis_core/core/domain/processing.py
index d15c8a0..b7303ec 100644
--- a/tradingchassis_core/core/domain/processing.py
+++ b/tradingchassis_core/core/domain/processing.py
@@ -15,7 +15,7 @@
from __future__ import annotations
import math
-from collections.abc import Iterable, Mapping
+from collections.abc import Mapping
from tradingchassis_core.core.domain.configuration import CoreConfiguration
from tradingchassis_core.core.domain.event_model import (
@@ -238,22 +238,3 @@ def process_event_entry(
position=entry.position,
configuration=configuration,
)
-
-
-def fold_event_stream_entries(
- state: StrategyState,
- entries: Iterable[EventStreamEntry],
- *,
- configuration: CoreConfiguration | None = None,
-) -> StrategyState:
- """Fold ordered EventStreamEntry values into the provided state.
-
- This utility is intentionally minimal and deterministic:
- - entries are applied in caller-provided order;
- - each entry is processed through ``process_event_entry``;
- - errors from canonical/ordering validation are propagated unchanged;
- - the same state instance is returned for ergonomic chaining.
- """
- for entry in entries:
- process_event_entry(state, entry, configuration=configuration)
- return state
diff --git a/tradingchassis_core/core/domain/state.py b/tradingchassis_core/core/domain/state.py
index 6aab540..06a0821 100644
--- a/tradingchassis_core/core/domain/state.py
+++ b/tradingchassis_core/core/domain/state.py
@@ -27,7 +27,7 @@
@dataclass(slots=True)
class QueuedIntent:
- """An intent stored for later sending (data-only Queue)."""
+ """An Intent stored for later sending (data-only Queue)."""
intent: OrderIntent
queued_at_ts_ns: int
@@ -575,36 +575,3 @@ def _matching_entries(key: str) -> list[QueuedIntent]:
dropped.append(intent)
return queued, replaced_in_queue, dropped
-
- def pop_queued_intents(
- self,
- instrument: str,
- *,
- max_items: int | None = None,
- ) -> list[OrderIntent]:
- q: deque[QueuedIntent] = self.queued_intents.setdefault(instrument, deque())
- if not q:
- return []
- items = list(q)
- items.sort(key=lambda qi: (qi.priority, qi.queued_at_ts_ns))
-
- filtered: list[QueuedIntent] = []
- for qi in items:
- if self.has_inflight(instrument, qi.intent.client_order_id):
- continue
- filtered.append(qi)
-
- selected = filtered if max_items is None else filtered[:max(0, max_items)]
- if max_items is not None and max_items <= 0:
- return []
-
- selected_ids = {id(x) for x in selected}
- out: list[OrderIntent] = []
- new_q: deque[QueuedIntent] = deque()
- for qi in q:
- if id(qi) in selected_ids:
- out.append(qi.intent)
- else:
- new_q.append(qi)
- self.queued_intents[instrument] = new_q
- return out
diff --git a/tradingchassis_core/core/events/events.py b/tradingchassis_core/core/events/events.py
deleted file mode 100644
index 36d8155..0000000
--- a/tradingchassis_core/core/events/events.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""Non-canonical telemetry records."""
-
-from __future__ import annotations
-
-from dataclasses import dataclass
-
-
-@dataclass(slots=True)
-class OrderStateTransitionEvent:
- """Observability payload for unexpected order-state transitions."""
-
- ts_ns_local: int
- instrument: str
- client_order_id: str
- prev_state: str | None
- next_state: str
-
-
-@dataclass(slots=True)
-class DerivedPnLEvent:
- """Observability payload for derived realized-PnL changes."""
-
- ts_ns_local: int
- instrument: str
- delta_pnl: float
- cum_realized_pnl: float
-
-
-@dataclass(slots=True)
-class ExposureDerivedEvent:
- """Observability payload for derived exposure changes."""
-
- ts_ns_local: int
- instrument: str
- exposure: float
- delta_exposure: float
-
-
-@dataclass(slots=True)
-class RiskDecisionEvent:
- """Observability payload summarizing policy-risk outcomes."""
-
- ts_ns_local: int
- accepted: int
- rejected: int
- reject_reasons: dict[str, int]
diff --git a/tradingchassis_core/core/execution_control/execution_control.py b/tradingchassis_core/core/execution_control/execution_control.py
index b7de265..4dd3dd7 100644
--- a/tradingchassis_core/core/execution_control/execution_control.py
+++ b/tradingchassis_core/core/execution_control/execution_control.py
@@ -92,7 +92,7 @@ def route_after_policy_rate_limit(
max_orders_per_sec: float | None,
max_cancels_per_sec: float | None,
) -> _RateRoutingResult:
- """Route policy-allowed intent by rate-limits (accept now vs stage)."""
+ """Route policy-allowed Intent by rate-limits (accept now vs stage)."""
if it.intent_type == "cancel":
if max_cancels_per_sec is not None:
allowed, wake_ts = self.consume_rate(
diff --git a/tradingchassis_core/core/risk/risk_engine.py b/tradingchassis_core/core/risk/risk_engine.py
index 7d7f7da..912c312 100644
--- a/tradingchassis_core/core/risk/risk_engine.py
+++ b/tradingchassis_core/core/risk/risk_engine.py
@@ -121,7 +121,7 @@ def evaluate_policy_intent(
state: StrategyState,
now_ts_ns_local: int,
) -> tuple[bool, str | None]:
- """Evaluate one intent with policy-only checks and no side effects."""
+ """Evaluate one Intent with policy-only checks and no side effects."""
raw_intents = [intent]
diff --git a/tradingchassis_core/core/risk/risk_policy.py b/tradingchassis_core/core/risk/risk_policy.py
index 0356d6e..83a0e69 100644
--- a/tradingchassis_core/core/risk/risk_policy.py
+++ b/tradingchassis_core/core/risk/risk_policy.py
@@ -109,7 +109,7 @@ def normalize_intent(self, it: OrderIntent, state: StrategyState) -> Normalizati
return self._constraints_policy.normalize_intent(it, state)
def validate_intent(self, it: OrderIntent, state: StrategyState) -> tuple[bool, str]:
- """Outbound intent sanity.
+ """Outbound Intent sanity.
Even if your schemas allow 0 placeholders, outbound intents should still be sensible.
"""
@@ -189,7 +189,7 @@ def hard_checks(
gross_q = sum(v[0] for v in book.values())
net_q = sum(v[1] for v in book.values())
- # Apply delta for this intent (new or replace).
+ # Apply delta for this Intent (new or replace).
new_abs = notional
new_signed = notional if it.side == "buy" else -notional