diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug-report.md
similarity index 86%
rename from .github/ISSUE_TEMPLATE/bug_report.md
rename to .github/ISSUE_TEMPLATE/bug-report.md
index 4b5f5ed..f9bf041 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -27,4 +27,4 @@ What should happen?
## Logs / Output
-Paste relevant logs, stack traces, or event output here.
+Paste relevant logs, traces, or outputs here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature-request.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/feature_request.md
rename to .github/ISSUE_TEMPLATE/feature-request.md
diff --git a/.gitignore b/.gitignore
index df97312..ec65e06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,7 +23,6 @@ venv.bak/
*.key
*.crt
*.pub
-.oci/
# ==============================
# Python packaging / build artifacts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 39c7683..97050db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,60 +1,24 @@
# Changelog
-All notable changes to this project will be documented in this file.
-
-The format is based on Keep a Changelog
-and this project adheres to Semantic Versioning.
+This changelog starts from the clean Core package baseline.
## [Unreleased]
-## [0.1.0] - 2026-02-17
+### Added
-Initial public release of the core.
+- Deterministic `run_core_step` and `run_core_wakeup_step` architecture.
+- CoreWakeupStep final-state Strategy evaluation: reduce all entries, then `CoreWakeupStrategyEvaluator` once.
+- Canonical Event input models and `EventStreamEntry`/`ProcessingPosition`.
+- Intent candidate record pipeline 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.
-### Added
+### Changed
+
+- Package metadata, exports, and docs reset for standalone Core library identity.
+- Pydantic models established as contract source of truth across public API docs.
+
+### Removed
-#### Core Domain
-- Explicit order state machine
-- Structured domain types and reject reasons
-- Slot-based order tracking
-- Event bus and event sink abstractions
-- JSON schema validation for domain events
-
-#### Risk Layer
-- Configurable risk engine
-- Risk constraint enforcement
-- Deterministic risk gating before execution
-
-#### Backtest Layer
-- Integration with [hftbacktest](https://github.com/nkaz001/hftbacktest)
-- Strategy runner abstraction
-- Venue adapter interface
-- Deterministic event processing pipeline
-
-#### Orchestration
-- Segment-based execution model
-- Parameter sweep runtime
-- Experiment and segment entrypoints
-- Prometheus metrics integration
-- MLflow-compatible logging hooks
-
-#### Execution Modes
-- Fully local execution example
-- Cloud-native runtime entrypoints
-- S3-compatible storage adapter
-
-#### Strategy
-- Base strategy interface
-- Structured strategy configuration
-
-#### Testing
-- Semantic invariant test suite
-- Order state transition validation
-- Queue dominance rules
-- Risk constraint validation
-- Schema conformance tests
-
-#### Tooling
-- Dev container configuration
-- Development validation scripts
-- Dependency compilation helper
+- Legacy compatibility-first contracts and references not part of the clean baseline.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0694e11..1f984aa 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,63 +1,80 @@
-# Contributing
+# Contributing to TradingChassis Core
-Thank you for your interest in contributing!
+Contributions should preserve TradingChassis Core as a deterministic,
+Runtime-agnostic library.
-This repository focuses on deterministic, event-driven trading architecture.
-Contributions should preserve clarity, explicitness and reproducibility.
+> Terminology: Definitions and related terms match the [canonical
+> terminology](https://tradingchassis.github.io/docs/latest/00-guides/terminology/).
----
+## Package Scope
-## Design Principles
+- Core owns canonical Events, State reduction, Strategy evaluation boundary,
+ candidate reconciliation, Risk Engine (policy), Execution Control plan/apply,
+ and `CoreStepResult`.
+- Core does not own Runtime orchestration, Venue Adapters, dispatch lifecycle,
+ or deployment/config wiring.
-All contributions must respect the core design philosophy:
+## Development Setup
-- Determinism over convenience
-- Explicit state modeling
-- No hidden side effects
-- Risk-first architecture
-- Clear domain boundaries
+From `core`:
-Avoid introducing implicit behavior or non-deterministic execution paths.
+```bash
+python -m pip install -e ".[dev]"
+```
----
+## Validation Commands
-## Workflow
+Run before opening a PR:
-1. Fork the repository
-2. Create a feature branch
-3. Commit small, logical changes
-4. Open a Pull Request with clear description
+```bash
+python examples/core_step_quickstart.py
+python -m pytest -q
+python -m build
+```
----
+## Architecture Rules
-## Commit Style
+- Core accepts canonical Events through `EventStreamEntry` and
+ `process_event_entry` / `process_canonical_event`.
+- Core returns deterministic `CoreStepResult`; Runtime dispatches.
+- Do not introduce Runtime imports.
+- Pydantic models are the source of truth for contract structure.
-Use clear messages:
+## Changing Core Behavior
-feat: add monitoring overlay
-fix: correct SecretProviderClass parameters
-docs: update bootstrap instructions
+### Canonical Events
----
+- Add Event models in `tradingchassis_core/core/domain/types.py`.
+- Register canonical category handling in `core/domain/event_model.py`.
+- Update canonical reduction behavior in `core/domain/processing.py`.
-## Development Environment
+### CoreStep/CoreWakeupStep pipeline
-Recommended:
+- Update `core/domain/processing_step.py` for deterministic flow changes.
+- Keep reconciliation/policy/apply transitions explicit and side-effect-safe.
-- Python 3.11.x
-- Dev Container (provided in this repository)
+### Risk Engine (policy) behavior
-Alternatively:
+- Implement policy checks in `core/risk/` and wire through
+ `evaluate_policy_intent`.
+- Keep Risk Engine admission as policy-only; no dispatch/Runtime side effects.
-```bash
-pip install -e .
-```
+### Execution Control behavior
+
+- Update plan/apply stages in `core/domain/execution_control_plan.py` and
+ `core/domain/execution_control_apply.py`.
+- Preserve `ControlSchedulingObligation` as non-canonical output.
----
+### Public API exports and docs
-## Testing
+- Update `tradingchassis_core/__init__.py` for intentional public exports only.
+- Sync docs in `README.md` and `docs/reference/public-api.md`.
-Before submitting:
+## Pull Request Checklist
-- The `./scripts/check.sh` script must pass
-- All backtests must complete successfully and produce result artifacts
+- [ ] Package remains Core-only and deterministic.
+- [ ] Public API changes are intentional and tested.
+- [ ] Quickstart still runs via public imports.
+- [ ] `python -m pytest -q` passes.
+- [ ] `python -m build` succeeds.
+- [ ] README/docs/changelog updated to match behavior.
diff --git a/README.md b/README.md
index 5d98c01..3428744 100644
--- a/README.md
+++ b/README.md
@@ -1,192 +1,249 @@
-# TradingChassis — Core
-
-
-
-
-
-Deterministic semantic Core library for TradingChassis.
-
-This repository provides the reusable Core package (`tradingchassis_core`) that defines
-event-driven processing semantics, state derivation boundaries, strategy interfaces, risk policy
-contracts, and execution control primitives.
-
----
-
-## Overview
-
-Core is a library, not a runtime shell.
-
-- Canonical processing model: Event Stream + Configuration -> derived State
-- Explicit Strategy, Risk Engine, and Execution Control boundaries
-- Deterministic behavior under identical Event Stream and Configuration
-- Runtime environments consume this package and provide integration wiring
-
----
-
-## What Core is
-
-Core provides:
-
-- semantic/domain types and value models
-- processing-order and state-derivation primitives
-- risk-policy interfaces and enforcement boundaries
-- execution-control abstractions
-- strategy interfaces for emitting Intents from derived State
-
----
-
-## What Core is not
-
-Core does not provide:
-
-- local/cluster runtime entrypoints
-- Kubernetes or Argo orchestration
-- runtime image/deployment plumbing
-- full runtime ingress, replay, or storage infrastructure
-
-Those responsibilities live in Core Runtime (`core-runtime`).
-
----
-
-## Current semantic status
-
-The transitional semantic upgrade milestone is closed.
-
-Core remains the canonical semantic library, and current runtime usage focuses on canonical
-`MarketEvent`, `OrderSubmittedEvent`, and `ControlTimeEvent` paths.
-
-Compatibility/deferred runtime capabilities still exist and are intentionally not described here as
-fully complete canonical coverage.
-
----
-
-## Key concepts
-
-Terminology follows `docs/docs/00-guides/terminology.md`:
-
-- Event
-- Event Stream
-- Processing Order
-- Configuration
-- State
-- Intent
-- Risk Engine
-- Queue
-- Queue Processing
-- Execution Control
-- Order
-- Core
-- Runtime
-- Venue Adapter
-
----
-
-## Canonical boundary
-
-Core guarantees deterministic semantics and reusable contracts.
-
-Runtimes supply environment-specific concerns such as:
-
-- ingress wiring
-- adapter implementations
-- orchestration entrypoints
-- persistence/replay infrastructure
-
----
-
-## Canonical vs compatibility artifacts
-
-At the Core level:
+# TradingChassis Core
+
+`tradingchassis_core` is the stable deterministic trading decision kernel
+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
+candidate Intents, optional `dispatchable_intents`, and optional
+Control Scheduling Obligation output. It does not perform Venue I/O,
+Execution (adapter-side dispatch), or Runtime orchestration.
+
+**What it is:** a shared library for the decision path only—canonical Event in,
+deterministic Strategy / Risk Engine / Execution Control processing, Intents and
+Execution Control outputs out.
+
+**What it is not:** a one-off Backtesting script, a Venue
+connector, a Live or Kubernetes Runtime, or anything that performs external
+dispatch. The same Core is meant to stay stable while local Research, Backtesting,
+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
+> [`core/docs/code-map/core-pipeline-map.md`](docs/code-map/core-pipeline-map.md).
+
+## Why it is relevant
+
+Trading systems often drift when Backtesting logic, Live logic, policy limits, and
+Strategy throttling are implemented in different places. TradingChassis Core
+centralizes deterministic decision semantics—State reduction, Strategy
+evaluation, Risk Engine (policy) admission, and Execution Control
+(planning apply over reconciled Intents)—in one library. Runtime
+environments (Backtesting, Live, Research tooling, Kubernetes-backed
+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
+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
+Configuration are comparable. Deterministic Core logic driven by canonical Events
+makes that logic reproducible and unit-testable without duplicating it in each
+Runtime.
+
+This package does **not** guarantee profitable trading, perfect Backtesting/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.
+
+## How it fits into a full system
+
+Backtesting Runtimes, Live Runtimes, and local Research or simulation harnesses can
+all feed the **same** Core: they normalize feeds, timestamps, and control semantics
+into canonical Events, build `EventStreamEntry` sequences, and call the same
+`run_core_step` / reduction APIs. Core always returns the same contract
+(`CoreStepResult`) for a given step; each Runtime owns environment-specific
+Execution, scheduling glue, and Control-Time Event injection when a Control Scheduling Obligation is realized.
+
+```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"]
+```
-- Canonical artifacts are semantic models and deterministic processing contracts
-- Compatibility artifacts are transitional runtime-facing paths maintained for migration parity
+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,
+Venue, and deployment.
-The runtime-level capability matrix is documented in `core-runtime/README.md`.
+## 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.
-## Package and import names
+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.
-- Human-facing concept name: Core
-- Distribution/project name: `tradingchassis-core`
-- Python import package: `tradingchassis_core`
+## When to use `tradingchassis_core`
-Install:
+- Building an internal trading system where Backtesting and Live should share decision semantics.
+- Wanting a deterministic Strategy / Risk Engine / Execution Control kernel.
+- 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.
-```bash
-python -m pip install -e .
-```
+## When not to use `tradingchassis_core`
-Install with dev extras:
+- You only need a one-off Backtesting notebook experiment.
+- You want a complete Venue connector or turnkey Live implementation.
+- 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.
-```bash
-python -m pip install -e ".[dev]"
-```
+## Full pipeline
----
-
-## Repository structure
+Internal processing pipeline, in sequential order:
```text
-tradingchassis_core/ Core package root
-tradingchassis_core/core/ Domain and semantic primitives
-tradingchassis_core/strategies/ Strategy interfaces and config
-tests/ Core test suites
-scripts/ Developer helper scripts
+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
```
----
-
-## Development setup
+## Input / Core / Output / Not Owned By Core
-Requirements:
+- 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.
-- Python 3.11+
+## Quickstart
-Recommended local setup:
+Run the quickstart
```bash
-python -m pip install -e ".[dev]"
+python examples/core_step_quickstart.py
```
----
-
-## Test commands
-
-From the `core` repository root:
-
-```bash
-python -m pytest
+or minimal shape:
+
+```python
+import tradingchassis_core as tc
+
+state = tc.StrategyState(event_bus=tc.NullEventBus())
+result = tc.run_core_step(
+ state,
+ tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=0),
+ event=tc.ControlTimeEvent(
+ ts_ns_local_control=1_000,
+ reason="scheduled_control_recheck",
+ due_ts_ns_local=1_000,
+ realized_ts_ns_local=1_000,
+ obligation_reason="rate_limit",
+ obligation_due_ts_ns_local=1_000,
+ runtime_correlation=None,
+ ),
+ ),
+)
+print(result.generated_intents, result.dispatchable_intents)
+# Expected: () () — no Strategy or Risk Engine/Execution Control path in this snippet.
```
-From a monorepo parent containing `core/`:
+See `examples/core_step_quickstart.py` for a full runnable walkthrough.
-```bash
-python -m pytest -q core/tests
-```
+## Public Entrypoints
----
+| Entrypoint | Purpose |
+| --- | --- |
+| `run_core_step` | One-entry deterministic reduce/evaluate/decide/apply step |
+| `run_core_wakeup_reduction` | Multi-entry reduction phase for one wakeup (no per-entry Strategy) |
+| `run_core_wakeup_decision` | Wakeup-level candidate/Risk Engine/Execution Control decision phase |
+| `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` |
-## Relationship to Core Runtime
-Core Runtime (`core-runtime`) provides runtime execution around Core, including:
-- local hftbacktest-backed execution entrypoints
-- Argo/runtime orchestration entrypoints
-- runtime configuration and environment wiring
-- local output artifacts under `.runtime/local/results/`
+## CoreWakeupStep semantics
-Core provides the deterministic semantics those runtime paths consume.
+CoreWakeupStep is not "parallel Event processing".
+It is deterministic batch processing: the Runtime gives Core an ordered sequence of
+canonical entries, and Core reduces them in that order before making one decision.
----
+- `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.
+- Runtime dispatches after the returned `CoreStepResult`.
-## Documentation index
+## Ownership Boundary
-- Terminology source of truth: `docs/docs/00-guides/terminology.md`
-- Runtime capabilities and entrypoints: `core-runtime/README.md`
+| 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
-## License and versioning
+From root:
-MIT licensed. Versioning follows semantic versioning.
+```bash
+python -m pip install -e ".[dev]"
+python examples/core_step_quickstart.py
+./scripts/check.sh
+python -m build
+```
diff --git a/SECURITY.md b/SECURITY.md
index 9e224c4..5228104 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,72 +1,57 @@
# Security Policy
-## Supported Versions
+## Supported Baseline
-Only the latest version on the `main` branch is actively maintained.
+The supported baseline is the clean standalone Core package line (`0.1.x` and
+forward on the active mainline branch).
-Older commits and historical states of the repository may not receive security updates or patches.
-
----
+Older historical commits may not receive fixes.
## Reporting a Vulnerability
-If you discover a security vulnerability, please **do not open a public GitHub issue**.
-
-Instead, report it responsibly via:
-
-- GitHub Security Advisories
-- Direct contact with the repository owner (if necessary)
-
-When submitting a report, please include:
-
-- A clear description of the vulnerability
-- Steps to reproduce (if applicable)
-- Potential impact and affected components
-- Any suggested mitigation or fix
-
-Valid reports will be acknowledged in a timely manner and handled through responsible disclosure.
-
----
-
-## Security Scope
-
-This repository provides:
+Do not report vulnerabilities in public issues.
-- Deterministic backtesting architecture
-- Risk-aware execution simulation
-- Event-driven domain modeling
+Use a private security advisory workflow if available for this repository, or
+contact project maintainers through the configured private channel.
-It does **not** currently provide production-grade live trading infrastructure.
+Include:
-Live exchange connectivity is under development and not feature-complete.
+- affected component(s)
+- reproduction details and impact
+- suggested mitigations (if known)
----
+## Scope
-## Dependency Security
+This policy covers the Core package in this repository, including:
-- Dependencies are explicitly defined
-- Python version is pinned (3.11.x)
-- External libraries should be kept up to date
+- canonical Event and Intent contracts
+- deterministic CoreStep/CoreWakeupStep decision pipeline
+- package integrity and dependency usage in `tradingchassis_core`
-Security-related dependency updates are prioritized.
+## Secrets and Credentials Policy
----
+Never commit live secrets or account-sensitive data, including:
-## Responsible Usage
+- API keys and Venue credentials
+- account identifiers tied to real accounts
+- private trading data dumps
-This code is intended for research and controlled environments.
+Tests and documentation examples must use synthetic or non-sensitive data only.
-Users are responsible for:
+## Runtime and Trading Caveat
-- Secure handling of API credentials
-- Secure deployment of live trading components
-- Validation of risk configurations
+- TradingChassis Core is a library and does not guarantee safe live trading by
+ itself.
+- Runtime orchestration, Venue behavior, and deployment hardening remain outside
+ this package scope and require separate validation.
-This repository does not assume liability for financial losses
-resulting from misuse or incorrect configuration.
+## No Financial Performance Guarantee
----
+This package provides deterministic software behavior, not financial advice or
+performance guarantees.
-## Disclosure Policy
+## Dependency Vulnerability Handling
-Please allow reasonable time for investigation and remediation before public disclosure of any reported vulnerabilities.
+- Keep dependencies minimal and pinned by compatible ranges.
+- Review dependency advisories and patch vulnerable versions promptly.
+- Prefer removing unused dependencies over adding new tooling.
diff --git a/docs/README.md b/docs/README.md
deleted file mode 100644
index c55ee60..0000000
--- a/docs/README.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# Core Docs Contract Index v1
-
-This directory contains implementation-facing contracts and snapshots for `core`.
-
-The main `docs` repository remains the semantic source of truth for architecture
-and terminology. Documents in `core/docs` must not contradict main docs
-semantics.
-
-## Current documents
-
-- **[stable]** [Core Stable Contract v1](core-stable-contract-v1.md)
- Stable snapshot of currently implemented and tested `core` v1 semantic
- guarantees and boundaries.
-
-- **[boundary]** [Runtime-to-CoreConfiguration Contract Boundary v1](runtime-to-coreconfiguration-contract-v1.md)
- Boundary contract for runtime-owned mapping into `CoreConfiguration`
- before calling `core` canonical processing APIs; mapping is implemented in
- `core-runtime`, while this page defines boundary expectations.
-
-- **[boundary/deferred]** [Runtime Execution Feedback Contract v1](runtime-execution-feedback-contract-v1.md)
- Boundary contract freezing eligibility requirements for future canonical
- runtime execution feedback emission (including `FillEvent`), while preserving
- current compatibility projection behavior.
-
-- **[boundary/source-contract]** [Runtime/Adapter Execution Feedback Source Contract v1](runtime-adapter-execution-feedback-source-contract-v1.md)
- Source-authority boundary contract defining eligibility, authority, ordering,
- and no-double-counting requirements before canonical `FillEvent` ingress.
-
-- **[boundary/implemented-transition]** [OrderSubmittedEvent / Dispatch Boundary Contract v1](order-submitted-event-contract-v1.md)
- Implemented-transition boundary contract for dispatch-time canonical
- order-entry semantics and coexistence constraints around `Submitted` authority.
-
-- **[boundary/implemented-transition]** [Control-Time Event Contract v1](control-time-event-contract-v1.md)
- Implemented-transition boundary contract for canonical Control-Time Event
- realization semantics and coexistence constraints with compatibility wakeups.
-
-- **[boundary/compatibility-map]** [Post-Submission Lifecycle Compatibility Map v1](post-submission-lifecycle-compatibility-map-v1.md)
- Docs-only authority split map freezing canonical `Submitted` entry via
- `OrderSubmittedEvent` and compatibility-governed post-submission lifecycle
- progression until execution-feedback source gates are satisfied.
-
-- **[boundary/model]** [Venue Adapter Capability Model v1](venue-adapter-capability-model-v1.md)
- Docs-only venue-agnostic capability model defining adapter/runtime source
- capability categories and semantic authority classifications without API
- implementation or runtime behavior changes.
-
-- **[boundary/deferred-abstraction]** [ProcessingContext / EventStreamCursor Contract v1](processing-context-event-stream-cursor-contract-v1.md)
- Docs-only boundary contract defining ownership and responsibility split for
- runtime-owned `EventStreamCursor` and deferred `ProcessingContext`
- abstraction work, without introducing behavior changes in this slice.
-
-- **[boundary/characterization]** [EventStreamCursor Characterization Note v1](event-stream-cursor-characterization-v1.md)
- Read-only characterization of current runtime `EventStreamCursor` behavior
- and invariants, without introducing implementation or behavior change.
-
-- **[milestone/closure]** [Semantic Core Upgrade Milestone Closure v1](semantic-core-upgrade-milestone-closure-v1.md)
- Docs-only closure snapshot of satisfied, transitional, and deferred semantic
- implementation status and current usability statements for `core` and
- `core-runtime`.
-
-- **[planning/naming]** [Package Rename Stage 0 Decision v1](package-rename-stage-0-decision-v1.md)
- Stage 0 naming decision record defining final naming targets, compatibility
- strategy, and first implementation slice for the package-rename track.
-
-- **[historical/dev-log]** [CoreConfiguration to Positioned Market Contract](coreconfiguration-positioned-market-contract.md)
- Historical closure contract for positioned canonical `MarketEvent`
- configuration-path and validation behavior in `core`.
-
-## Deferred / not implemented here
-
-- Runtime mapping implementation details.
-- Queue/rate reducer migration and full control-time authority migration.
-- FillEvent runtime ingress and source authority rollout.
-- Post-submission execution feedback canonicalization.
-- `OrderStateEvent` canonicalization.
-- Replay/storage/`ProcessingContext` and full runtime stream integration.
diff --git a/docs/code-map/core-pipeline-map.md b/docs/code-map/core-pipeline-map.md
new file mode 100644
index 0000000..cfd4ed3
--- /dev/null
+++ b/docs/code-map/core-pipeline-map.md
@@ -0,0 +1,55 @@
+# Core Pipeline Map
+
+This map captures the only supported deterministic decision pipeline for
+TradingChassis Core.
+
+## Step-by-step flow
+
+1. `EventStreamEntry` arrives with `ProcessingPosition`.
+2. `process_event_entry` forwards to `process_canonical_event`.
+3. Canonical reducer mutates `StrategyState` deterministically.
+4. Strategy evaluation produces generated Intents.
+5. Candidate records are built and reconciled/dominated.
+6. Risk Engine (policy) accepts/rejects generated candidates.
+7. Execution Control plan/apply computes Queue/dispatch/scheduling outputs.
+8. `CoreStepResult` returns `dispatchable_intents` and optional
+ `control_scheduling_obligation` (non-canonical; **rate-limit** deferral only
+ 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.
+
+## Core APIs
+
+- Single-entry flow: `run_core_step`
+- Wakeup reduction: `run_core_wakeup_reduction`
+- Wakeup decision/apply: `run_core_wakeup_decision`
+- Wakeup convenience wrapper: `run_core_wakeup_step`
+
+## Determinism notes
+
+- Processing Order monotonicity is enforced by `ProcessingPosition`.
+- Core logic is side-effect-safe apart from deterministic state mutation.
+- Runtime adapters and external dispatch concerns are outside Core.
+
+
+## CoreWakeupStep batch semantics
+
+`CoreWakeupStep` is not "parallel Event processing".
+It is deterministic batch processing: the Runtime gives Core an ordered sequence of
+canonical `EventStreamEntry` values, and Core reduces them in that order before making
+one decision.
+
+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
+ (`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.
+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.
diff --git a/docs/code-map/repository-map.md b/docs/code-map/repository-map.md
new file mode 100644
index 0000000..d050461
--- /dev/null
+++ b/docs/code-map/repository-map.md
@@ -0,0 +1,38 @@
+# Repository Map
+
+High-level map for the standalone Core package.
+
+## Package layout
+
+- `tradingchassis_core/__init__.py`: public package boundary exports
+- `tradingchassis_core/core/domain/`: canonical contracts and deterministic
+ 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
+
+## Tests and examples
+
+- `tests/semantics/`: focused contract and deterministic behavior tests
+- `examples/core_step_quickstart.py`: public-import quickstart
+
+## Top-level package docs and metadata
+
+- `README.md`: package front door
+- `CHANGELOG.md`: clean baseline changelog
+- `CONTRIBUTING.md`: development and architecture rules
+- `SECURITY.md`: vulnerability handling and scope policy
+- `pyproject.toml`: build and tooling configuration
+
+## Boundary matrix
+
+Core owns:
+
+- canonical Events and Processing Order contracts
+- deterministic reduction and step decisions
+- Intent candidate, Risk Engine (policy), Execution Control outputs
+
+Core does not own:
+
+- Runtime orchestration, Venue Adapters, I/O, deployment
+- dispatch lifecycle beyond `CoreStepResult` outputs
diff --git a/docs/control-time-event-contract-v1.md b/docs/control-time-event-contract-v1.md
deleted file mode 100644
index b3924ab..0000000
--- a/docs/control-time-event-contract-v1.md
+++ /dev/null
@@ -1,213 +0,0 @@
-# Control-Time Event Contract v1
-
----
-
-## Purpose and scope
-
-This document defines an implementation-facing boundary contract snapshot for
-the Control-Time Event transition boundary across `core` and runtime after
-initial model/taxonomy/boundary/runtime injection slices.
-
-This is a docs-contract reconciliation slice only:
-
-- it does not change runtime wakeup behavior;
-- it does not modify `ExecutionControl` behavior;
-- it does not modify queue/rate/inflight behavior;
-- it does not introduce periodic control ticks.
-
----
-
-## Semantic source of truth and precedence
-
-`CTEC-01` - Main `docs` repository remains the semantic source of truth for
-Event semantics, Event Stream, Processing Order, Control Events, and
-Control-Time Event behavior.
-
-`CTEC-02` - This document is a `core` implementation boundary contract snapshot
-for future Control-Time Event canonicalization boundaries. It does not redefine
-architecture semantics.
-
-`CTEC-03` - Existing `core` implementation snapshot semantics remain governed by
-[Core Stable Contract v1](core-stable-contract-v1.md). This contract records
-the current implemented transition slice and what remains deferred.
-
-Normative semantic sources:
-
-- `docs/docs/00-guides/terminology.md`
-- `docs/docs/20-concepts/event-model.md`
-- `docs/docs/20-concepts/time-model.md`
-- `docs/docs/20-concepts/queue-processing.md`
-- `docs/docs/20-concepts/invariants.md`
-
----
-
-## Classification
-
-`CTEC-04` - `ControlSchedulingObligation` remains a non-canonical runtime-facing
-helper in this contract snapshot.
-
-`CTEC-05` - `GateDecision.next_send_ts_ns_local` remains a compatibility
-scheduling surface in this contract snapshot.
-
-`CTEC-06` - A Control-Time Event is a canonical Control Event only once Runtime
-realizes a previously derived control scheduling obligation and injects the
-event into the Event Stream boundary.
-
-`CTEC-06a` - Current implemented transition slice includes:
-
-- `ControlTimeEvent` model in `core` domain types;
-- taxonomy mapping as canonical `CONTROL` category candidate;
-- canonical boundary acceptance via `process_event_entry` / `process_canonical_event`;
-- `StrategyState.apply_control_time_event` as a no-op reducer for this slice.
-
-`CTEC-07` - `EventBus` remains non-canonical transport/integration
-infrastructure and is not a canonical Event Stream record.
-
-`CTEC-08` - Queued intents, inflight markers, and rate state remain
-derived/internal state and are not canonical Events.
-
----
-
-## Runtime realization trigger
-
-`CTEC-09` - A canonical Control-Time Event may be emitted only when Runtime
-realizes a previously derived scheduling obligation/deadline.
-
-`CTEC-10` - Realization is sparse and deadline-style; it is not a periodic tick
-model.
-
-`CTEC-11` - A Control-Time Event must not be emitted merely because wall-clock
-or simulation time passes without a derived obligation boundary.
-
-`CTEC-12` - `ExecutionControl` does not emit canonical Control-Time Events
-directly in this contract snapshot.
-
-`CTEC-12a` - In the current HFT runtime transition slice, injection occurs only
-when a scheduled deadline is realized (`next_send_ts_ns_local` is present and
-runtime local time has reached/passed that deadline), and injection is ordered
-after queued-intent pop and before the gate path.
-
----
-
-## Relationship to ControlSchedulingObligation
-
-`CTEC-13` - Control scheduling obligations are derived by the current
-core execution-control/risk path as non-canonical runtime-facing signals.
-
-`CTEC-14` - A Control scheduling obligation is not Event Stream input and
-produces no canonical State Transition by itself.
-
-`CTEC-15` - A control scheduling obligation may request/suggest a future wakeup
-or deadline (for example through compatibility scheduling surfaces).
-
-`CTEC-16` - Runtime owns future realization of the obligation into canonical
-Control-Time Event stream input.
-
----
-
-## Minimal future Control-Time Event shape
-
-`CTEC-17` - ProcessingPosition authority remains carried by
-`EventStreamEntry.position`, not embedded as an inline event payload field.
-
-`CTEC-18` - The future Control-Time Event payload should include at least:
-
-- `ts_ns_local_control`
-- `reason`
-- `due_ts_ns_local` or `realized_ts_ns_local` (when applicable)
-- optional obligation/correlation metadata
-
-`CTEC-19` - Control-Time Event payload must not introduce market/order/fill
-semantic fields.
-
-`CTEC-20` - Control-Time Event payload must not encode direct queue mutation
-commands/payloads.
-
----
-
-## ProcessingPosition policy
-
-`CTEC-21` - Control-Time Event acceptance ordering must use the global canonical
-ProcessingPosition sequence shared with other canonical candidates, including
-`MarketEvent` and `OrderSubmittedEvent`.
-
-`CTEC-22` - Category-local canonical counters are not allowed.
-
-`CTEC-23` - Processing order authority must not be timestamp-derived.
-
----
-
-## Reducer and processing semantics boundary
-
-`CTEC-24` - Future Control-Time Event processing should allow deterministic
-queue/rate/inflight derived processing to run at the canonical event boundary.
-
-`CTEC-25` - Current implemented reducer semantics are intentionally no-op for
-ControlTimeEvent in this transition slice.
-
-`CTEC-26` - Queue Processing remains deterministic event processing, not
-independent wall-clock mutation.
-
----
-
-## Coexistence with current compatibility behavior
-
-`CTEC-27` - `next_send_ts_ns_local` remains the current compatibility
-scheduling/wakeup surface during transition.
-
-`CTEC-28` - Existing runtime timeout/wakeup behavior remains unchanged in this
-contract snapshot.
-
-`CTEC-29` - Future implementation must avoid dual-authority ambiguity between
-compatibility wakeup surfaces and canonical Control-Time Event stream authority.
-
-`CTEC-30` - `GateDecision` shape remains unchanged in this contract snapshot.
-
-`CTEC-30a` - Current runtime uses one global canonical ProcessingPosition
-counter shared by `MarketEvent`, `OrderSubmittedEvent`, and `ControlTimeEvent`.
-
----
-
-## Explicitly prohibited behavior
-
-`CTEC-31` - Do not classify `ControlSchedulingObligation` as canonical Event.
-
-`CTEC-32` - Do not emit periodic control ticks.
-
-`CTEC-33` - Do not use Event Time as Processing Order authority.
-
-`CTEC-34` - Do not mutate queue/rate state outside canonical processing in
-future strict-mode canonical behavior.
-
-`CTEC-35` - Do not use `EventBus` as canonical Event Stream.
-
----
-
-## Explicitly out of scope
-
-`CTEC-36` - Additional ControlTimeEvent model shape expansion beyond the current
-implemented contract fields.
-
-`CTEC-37` - Further event taxonomy semantic changes beyond current
-`ControlTimeEvent` canonical `CONTROL` mapping.
-
-`CTEC-38` - Runtime injection generalization beyond current scheduled-deadline
-realization transition behavior.
-
-`CTEC-39` - Queue/rate reducer migration.
-
-`CTEC-40` - Replay/storage/`ProcessingContext`/`EventStreamCursor`
-implementation.
-
-`CTEC-41` - `FillEvent` ingress implementation.
-
-`CTEC-42` - `OrderStateEvent` canonicalization.
-
----
-
-## Relationship to existing core contracts
-
-- [Core Stable Contract v1](core-stable-contract-v1.md)
-- [Runtime Execution Feedback Contract v1](runtime-execution-feedback-contract-v1.md)
-- [OrderSubmittedEvent / Dispatch Boundary Contract v1](order-submitted-event-contract-v1.md)
-
diff --git a/docs/core-stable-contract-v1.md b/docs/core-stable-contract-v1.md
deleted file mode 100644
index 78e4270..0000000
--- a/docs/core-stable-contract-v1.md
+++ /dev/null
@@ -1,201 +0,0 @@
-# Core Stable Contract v1
-
----
-
-## Purpose and scope
-
-This page freezes the currently implemented and tested semantic kernel of `core` as a **stable implementation contract snapshot (v1)**.
-
-Repository boundary:
-
-- Semantic definitions (Event, Event Stream, Processing Order, Configuration, State, Intent, Order, etc.) live in the main `docs` repository and remain the semantic source of truth.
-- This page is **implementation-facing** documentation for `core` and only claims what is currently implemented and tested in `core` v1.
-
-This page is intentionally narrow:
-
-- it documents what `core` v1 currently guarantees;
-- it distinguishes implemented guarantees from deferred architecture concepts;
-- it does not introduce new behavior.
-
-Historical provenance for the positioned market configuration closure is recorded in:
-
-- [CoreConfiguration to Positioned Market Contract](coreconfiguration-positioned-market-contract.md)
-
----
-
-## Normative sources and precedence
-
-`CSC-01` — Terminology and architecture concepts in the main `docs` repository remain the semantic source of truth.
-
-`CSC-02` — This page defines the **implementation snapshot contract** for current `core` v1. If architecture/concept docs describe broader target semantics not yet implemented in `core`, this page controls claims about current `core` behavior.
-
-`CSC-03` — Dev logs remain historical decision trails and are not the stable contract surface.
-
----
-
-## Canonical boundary APIs (v1)
-
-`CSC-04` — `core` v1 currently guarantees a minimal canonical processing boundary through:
-
-- `process_canonical_event`
-- `process_event_entry`
-- `fold_event_stream_entries`
-
-`CSC-05` — These APIs define the currently stabilized canonical ingestion/fold surface in `core` v1. They are not a full Event Stream runtime, storage, or replay orchestration API.
-
----
-
-## Canonical event candidate set (v1)
-
-`CSC-06` — `core` v1 currently guarantees the canonical event candidate set:
-
-- `MarketEvent` (market category candidate)
-- `OrderSubmittedEvent` (intent-related category candidate)
-- `FillEvent` (execution category candidate)
-- `ControlTimeEvent` (control category candidate)
-
-`CSC-07` — Current canonical runtime wiring in this snapshot processes positioned canonical `MarketEvent`, `OrderSubmittedEvent`, and `ControlTimeEvent` through the canonical boundary. `ControlTimeEvent` runtime injection is currently realized only for scheduled-deadline wakeup realization and remains a transition slice (no queue/rate/control reducer migration implied). `FillEvent` remains a canonical execution candidate in `core`, while runtime `FillEvent` ingress remains deferred per [Runtime Execution Feedback Contract v1](runtime-execution-feedback-contract-v1.md).
-
----
-
-## Non-canonical artifacts (v1)
-
-`CSC-08` — `OrderStateEvent` remains compatibility-only and is non-canonical at the canonical boundary.
-
-`CSC-09` — `DerivedFillEvent` remains a compatibility projection artifact and is non-canonical.
-
-`CSC-10` — Telemetry/observability records remain non-canonical, including:
-
-- `RiskDecisionEvent`
-- `DerivedPnLEvent`
-- `ExposureDerivedEvent`
-- `OrderStateTransitionEvent`
-
-`CSC-11` — `GateDecision` remains compatibility/non-canonical.
-
-`CSC-12` — `ControlSchedulingObligation` remains a non-canonical runtime-facing helper, not a canonical Event.
-
-`CSC-13` — `EventBus` remains transport/integration infrastructure, not a canonical Event Stream record.
-
----
-
-## ProcessingPosition and Processing Order guarantees
-
-`CSC-14` — `ProcessingPosition` is the explicit boundary metadata for positioned canonical processing in `core` v1.
-
-`CSC-15` — For positioned canonical processing, position indexes are strictly monotonic; repeated or regressing indexes fail.
-
-`CSC-16` — Within the `core` package canonical boundary, processing position cursor advancement is boundary-owned behavior and remains guarded against out-of-boundary mutation patterns by current `core` semantics coverage. This clause does not claim repo-wide enforcement outside `core`.
-
-`CSC-17` — Positioned boundary acceptance order follows `ProcessingPosition` monotonicity, not event timestamp ordering.
-
----
-
-## EventStreamEntry contract
-
-`CSC-18` — `EventStreamEntry` v1 contract shape is:
-
-- `position`
-- `event`
-
-`CSC-19` — `EventStreamEntry` contains no `configuration` field.
-
-`CSC-20` — Configuration remains call-level processing input, not entry-level payload shape.
-
----
-
-## CoreConfiguration contract
-
-`CSC-21` — `CoreConfiguration` v1 currently guarantees:
-
-- explicit `version`;
-- explicit `payload`;
-- stable derived `fingerprint`.
-
-`CSC-22` — Equivalent semantic payloads and version yield stable identity/fingerprint behavior; identity remains stable against source-payload mutation after construction.
-
-`CSC-23` — Canonical processing entry/fold APIs accept configuration as explicit call-level input (`CoreConfiguration | None`) and reject non-`CoreConfiguration` objects.
-
----
-
-## Positioned MarketEvent metadata contract
-
-`CSC-24` — For positioned canonical `MarketEvent` processing, `core` v1 consumes instrument metadata from:
-
-- `payload.market.instruments..tick_size`
-- `payload.market.instruments..lot_size`
-- `payload.market.instruments..contract_size`
-
-`CSC-25` — Positioned canonical market processing is explicit-or-fail for missing/invalid required configuration path or values.
-
-`CSC-26` — Positioned canonical market path has no implicit defaults for these required fields.
-
----
-
-## Fold and minimal replay contract (v1)
-
-`CSC-27` — `fold_event_stream_entries` is a deterministic fold utility over caller-provided ordered `EventStreamEntry` values.
-
-`CSC-28` — `fold_event_stream_entries` in `core` v1 is not a full replay engine, not Event Stream storage, and not runtime orchestration.
-
----
-
-## Compatibility boundaries preserved
-
-`CSC-29` — Unpositioned canonical market compatibility path remains preserved.
-
-`CSC-30` — Direct `StrategyState.update_market(...)` compatibility path remains preserved.
-
-`CSC-31` — `FillEvent` behavior remains preserved (including existing idempotence/no-op characteristics).
-
-`CSC-32` — `OrderStateEvent` compatibility reducer path remains preserved and non-canonical at canonical boundary.
-
----
-
-## Explicitly out of scope for core stable contract v1
-
-`CSC-33` — Runtime/backtest-to-`CoreConfiguration` mapping implementation.
-
-`CSC-34` — Full Control-Time authority migration, including queue/rate reducer migration and broader runtime realization generalization beyond the current transition slice.
-
-`CSC-35` — Introduction of new canonical event categories or canonicalization of currently non-canonical artifacts.
-
-`CSC-36` — Event Stream storage layer.
-
-`CSC-37` — Full replay engine/runtime integration.
-
-`CSC-38` — `ProcessingContext` introduction and broader replay/storage-oriented
-`EventStreamCursor` extraction in `core` scope. (A runtime-only
-`EventStreamCursor` ordering helper exists in `core-runtime` and is outside
-this `core` stable-contract scope.)
-
----
-
-## Change rubric
-
-`CSC-39` — **Breaking change** (v1 contract): any change that alters guaranteed behavior or contract shape in `CSC-04` through `CSC-38` (including canonical/non-canonical classification shifts, positioned market config semantics changes, cursor monotonicity behavior changes, or compatibility boundary behavior changes).
-
-`CSC-40` — **Additive change** (v1-compatible): new capability that does not alter existing guarantees and does not reinterpret current clause semantics.
-
-`CSC-41` — **Docs-only clarification**: wording refinement that improves precision without changing contract meaning or introducing new semantics.
-
----
-
-## Traceability matrix to existing semantics tests
-
-| Clause(s) | Contract statement (summary) | Existing semantics test anchors |
-| --------- | ---------------------------- | ------------------------------- |
-| `CSC-04`, `CSC-05` | Canonical boundary API surface and minimal scope | `core/tests/semantics/models/test_canonical_processing_boundary.py`, `core/tests/semantics/models/test_event_stream_entry_contract.py`, `core/tests/semantics/models/test_fold_event_stream_entries_contract.py` |
-| `CSC-06`, `CSC-07` | Canonical candidate set is MarketEvent + OrderSubmittedEvent + FillEvent + ControlTimeEvent; current runtime wiring path includes MarketEvent + OrderSubmittedEvent + ControlTimeEvent while FillEvent ingress remains deferred | `core/tests/semantics/models/test_event_taxonomy_boundary.py`, `core/tests/semantics/models/test_canonical_processing_boundary.py`, `core-runtime/tests/runtime/test_strategy_runner_canonical_market_adoption.py` |
-| `CSC-08` to `CSC-13` | Non-canonical classifications (compatibility/telemetry/control helper/transport) | `core/tests/semantics/models/test_event_taxonomy_boundary.py`, `core/tests/semantics/models/test_canonical_processing_boundary.py`, `core/tests/semantics/models/test_event_stream_entry_contract.py` |
-| `CSC-14` to `CSC-17` | ProcessingPosition monotonic positioned boundary and cursor guarantees | `core/tests/semantics/models/test_canonical_processing_boundary.py`, `core/tests/semantics/models/test_event_stream_entry_contract.py`, `core/tests/semantics/models/test_fold_event_stream_entries_contract.py`, `core/tests/semantics/models/test_processing_position_cursor_ownership_guard.py` |
-| `CSC-18` to `CSC-20` | EventStreamEntry shape and call-level configuration boundary | `core/tests/semantics/models/test_event_stream_entry_contract.py` |
-| `CSC-21` to `CSC-23` | CoreConfiguration identity and call-level typing contract | `core/tests/semantics/models/test_core_configuration_contract.py`, `core/tests/semantics/models/test_fold_event_stream_entries_contract.py`, `core/tests/semantics/models/test_event_stream_entry_contract.py` |
-| `CSC-24` to `CSC-26` | Positioned market metadata path and explicit-or-fail semantics | `core/tests/semantics/models/test_market_configuration_positioned_contract.py` |
-| `CSC-27`, `CSC-28` | Deterministic fold minimal contract; not full replay/runtime/storage | `core/tests/semantics/models/test_fold_event_stream_entries_contract.py` |
-| `CSC-29` to `CSC-32` | Compatibility boundaries preserved | `core/tests/semantics/models/test_market_configuration_positioned_contract.py`, `core/tests/semantics/models/test_canonical_processing_boundary.py` |
-
-Notes:
-
-- This matrix maps stable contract clauses to existing semantics coverage; it does not claim architecture-complete implementation.
-- Deferred architecture concepts remain governed by their concept/architecture docs and are out of scope for this v1 implementation snapshot.
diff --git a/docs/coreconfiguration-positioned-market-contract.md b/docs/coreconfiguration-positioned-market-contract.md
deleted file mode 100644
index 58dca5b..0000000
--- a/docs/coreconfiguration-positioned-market-contract.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# CoreConfiguration to Positioned Market Contract
-
----
-
-## Context
-
-Introduced strict `CoreConfiguration` consumption for the positioned canonical `MarketEvent` reduction path in `core`.
-
-This note freezes that behavior as an explicit closure contract.
-
----
-
-## Contract (Core-facing)
-
-For **positioned canonical** `MarketEvent` processing in `core`:
-
-1. `core` consumes deterministic semantic configuration **only** through `CoreConfiguration`.
-2. Required payload path:
-
- `CoreConfiguration.payload["market"]["instruments"][instrument]`
-
-3. Required instrument fields:
- - `tick_size`
- - `lot_size`
- - `contract_size`
-4. Semantics are **explicit-or-fail**:
- - missing `CoreConfiguration` fails;
- - missing `market`/`instruments`/`instrument` path fails;
- - missing required fields fails;
- - invalid values (`None`, `bool`, non-numeric, non-finite, non-positive) fail.
-5. Positioned canonical path has **no implicit defaults** for these fields.
-
----
-
-## Boundary and Compatibility Guarantees
-
-1. Validation for positioned canonical `MarketEvent` happens before:
- - `ProcessingPosition` cursor advancement, and
- - `MarketState` mutation.
-2. **Unpositioned** canonical `MarketEvent` compatibility path remains unchanged.
-3. Direct `StrategyState.update_market(...)` compatibility path remains unchanged.
-4. `FillEvent` behavior remains unchanged.
-5. `OrderStateEvent` remains compatibility-only (non-canonical at canonical boundary).
-
----
-
-## Runtime Boundary
-
-1. This contract does **not** introduce runtime/backtest JSON mapping in `core`.
-2. Mapping from runtime/backtest config to `CoreConfiguration` is a **runtime responsibility**.
-3. No `core-runtime` behavior or interfaces are changed by this contract note.
diff --git a/docs/event-stream-cursor-characterization-v1.md b/docs/event-stream-cursor-characterization-v1.md
deleted file mode 100644
index ceeec8a..0000000
--- a/docs/event-stream-cursor-characterization-v1.md
+++ /dev/null
@@ -1,168 +0,0 @@
-# EventStreamCursor Characterization Note v1
-
----
-
-## Purpose and scope
-
-This note characterizes the **current runtime EventStreamCursor behavior** used
-for canonical `ProcessingPosition` assignment and records invariants for future
-runtime extraction/refinement work.
-
-This is read-only characterization/planning documentation:
-
-- it does not introduce new `EventStreamCursor` behavior;
-- it does not implement `ProcessingContext`;
-- it does not change runtime behavior;
-- it does not change reducers or event taxonomy;
-- it does not implement canonical `FillEvent` ingress;
-- it does not add adapter APIs;
-- it does not canonicalize `OrderStateEvent`;
-- it does not change `DerivedFillEvent` behavior;
-- it does not change snapshot ingestion behavior;
-- it does not implement replay/storage/EventStream persistence.
-
-`ESCC-01` - Main `docs` remains the semantic source of truth for Event Stream and
-Processing Order semantics.
-
-`ESCC-02` - This note is implementation-facing characterization only and does not
-redefine existing contracts.
-
-`ESCC-03` - This note must remain consistent with:
-
-- [ProcessingContext / EventStreamCursor Contract v1](processing-context-event-stream-cursor-contract-v1.md)
-- [Core Stable Contract v1](core-stable-contract-v1.md)
-- [Venue Adapter Capability Model v1](venue-adapter-capability-model-v1.md)
-
----
-
-## Current runtime cursor behavior (characterized)
-
-Current behavior is implemented in
-`core-runtime/core_runtime/backtest/engine/strategy_runner.py`.
-
-`ESCC-04` - Runtime runner owns an `EventStreamCursor` instance.
-
-`ESCC-05` - Cursor starts at index `0`.
-
-`ESCC-06` - `_process_canonical_event(...)` calls
-`EventStreamCursor.attempt_position()` and constructs `EventStreamEntry`
-with the returned `ProcessingPosition`.
-
-`ESCC-07` - Runner calls `process_event_entry(state, entry, configuration=core_cfg)`
-for canonical boundary processing.
-
-`ESCC-08` - Cursor advances by exactly `+1` only after successful
-`process_event_entry(...)` return via `commit_success(...)`.
-
-`ESCC-09` - If canonical boundary processing raises, cursor does not advance.
-
-`ESCC-10` - One global cursor sequence is shared by currently wired canonical
-categories:
-
-- `MarketEvent`
-- `OrderSubmittedEvent`
-- `ControlTimeEvent`
-
-`ESCC-11` - Runtime canonical `FillEvent` ingress remains absent/deferred in the
-current runner path.
-
-`ESCC-12` - Compatibility `rc == 3` snapshot branch
-(`update_account` / `ingest_order_snapshots`) bypasses canonical
-`EventStreamEntry` construction and does not define position-allocation authority.
-
-`ESCC-13` - Current ordering authority for canonical boundary acceptance remains
-`ProcessingPosition`, not timestamp-derived ordering.
-
----
-
-## Invariants to preserve for extraction
-
-`ESCC-14` - First canonical event in a stream scope uses index `0`.
-
-`ESCC-15` - Position progression is monotone, global, and stepwise (`+1`) after
-each successful canonical boundary processing call.
-
-`ESCC-16` - Failed canonical processing must not consume/advance position.
-
-`ESCC-17` - Cursor scope remains global across canonical categories; no
-category-local counters.
-
-`ESCC-18` - Compatibility snapshot path (`rc == 3`) remains non-canonical and
-does not allocate canonical positions in this phase.
-
-`ESCC-19` - `EventStreamEntry` remains minimal (`position`, `event`) and
-config-free.
-
-`ESCC-20` - `CoreConfiguration` remains call-level processing input.
-
-`ESCC-21` - Core remains canonical boundary consumer/validator and is not runtime
-position allocator.
-
----
-
-## Future EventStreamCursor extraction semantics (non-implemented)
-
-`ESCC-22` - Any future `EventStreamCursor` work remains runtime-owned and
-ordering-only.
-
-`ESCC-23` - Recommended extraction model is reservation/commit semantics:
-
-- `attempt_position() -> position`
-- `commit_success(position)`
-
-`ESCC-24` - Commit occurs only after successful `process_event_entry(...)`
-completion.
-
-`ESCC-25` - No rollback-after-commit behavior is implied in this slice.
-
-`ESCC-26` - No reset/fork semantics within one canonical stream scope.
-
-`ESCC-27` - No category-local sequencing semantics.
-
-`ESCC-28` - No replay/storage/EventStream persistence semantics are implied by
-cursor extraction characterization.
-
----
-
-## Characterization test anchors
-
-Existing tests that already anchor current behavior:
-
-- Shared global counter across canonical categories:
- - `core-runtime/tests/runtime/test_strategy_runner_canonical_market_adoption.py::test_global_canonical_counter_shared_between_market_and_order_submitted`
- - `core-runtime/tests/runtime/test_strategy_runner_canonical_market_adoption.py::test_global_canonical_counter_shared_with_control_time_market_and_submitted`
-- No advance on failed canonical processing:
- - `core-runtime/tests/runtime/test_strategy_runner_canonical_market_adoption.py::test_canonical_counter_increments_only_after_successful_canonical_processing`
-- Compatibility `rc == 3` snapshot branch remains unchanged:
- - `core-runtime/tests/runtime/test_strategy_runner_canonical_market_adoption.py::test_order_snapshot_branch_keeps_compatibility_path`
- - `core-runtime/tests/runtime/test_hftbacktest_execution_feedback_probe.py::test_runner_contains_rc3_snapshot_branch`
-- Configuration passed to `process_event_entry(...)`:
- - `core-runtime/tests/runtime/test_strategy_runner_canonical_market_adoption.py::test_process_market_event_routes_through_event_entry_with_core_configuration`
-
-Coverage notes / potential direct-test gaps:
-
-`ESCC-29` - Existing tests strongly imply first-position-at-zero behavior, but no
-dedicated runner test is named solely for that invariant.
-
-`ESCC-30` - Existing compatibility snapshot branch tests assert path usage, but no
-dedicated assertion currently checks that runner cursor remains unchanged during
-`rc == 3` processing alone.
-
----
-
-## Out of scope
-
-`ESCC-31` - Additional `EventStreamCursor` feature expansion beyond the current
-runtime ordering helper behavior characterized here.
-
-`ESCC-32` - `ProcessingContext` implementation.
-
-`ESCC-33` - Adapter interface/API design.
-
-`ESCC-34` - Runtime canonical `FillEvent` ingress.
-
-`ESCC-35` - Lifecycle migration away from compatibility snapshot authority.
-
-`ESCC-36` - Replay/storage/EventStream persistence implementation.
-
----
diff --git a/docs/flows/control-time-and-scheduling.md b/docs/flows/control-time-and-scheduling.md
new file mode 100644
index 0000000..1257ce3
--- /dev/null
+++ b/docs/flows/control-time-and-scheduling.md
@@ -0,0 +1,68 @@
+# Control time and scheduling
+
+This note is the **Core package** source of truth for how non-canonical
+`ControlSchedulingObligation` relates to canonical `ControlTimeEvent` input and
+to Execution Control deferral.
+
+## Terms
+
+- **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
+ 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
+ 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.
+- **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,
+ not because a rate-limit wake time is known ahead of time.
+
+## What Core emits today
+
+| Deferral kind | Time-dependent? | `ControlSchedulingObligation` by default? | Expected resolution |
+| --- | --- | --- | --- |
+| Rate limit | Yes | **Yes** (reason such as `rate_limit`) | Runtime may realize the obligation and inject `ControlTimeEvent`; the next `run_core_step` re-runs reduction → Strategy → … → Execution Control apply. |
+| Inflight | No (feedback-dependent) | **No** | Later canonical **execution / lifecycle** Events (for example `OrderSubmittedEvent`, `OrderExecutionFeedbackEvent`, or `FillEvent`, depending on lifecycle) update `StrategyState` so a subsequent step can reconsider queued work. |
+
+**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
+`ControlTimeEvent`. Obligations are for **rate-limit** rechecks in the current
+Core slice.
+
+## Clean Core pipeline (unchanged)
+
+1. `EventStreamEntry`
+2. `process_event_entry` / `process_canonical_event`
+3. Strategy evaluator
+4. generated intents
+5. candidate records + dominance / reconciliation
+6. policy admission
+7. Execution Control plan / apply
+8. `CoreStepResult.dispatchable_intents` and optional `control_scheduling_obligation`
+9. Runtime performs venue dispatch and **injects** further canonical Events (including
+ any `ControlTimeEvent` realized from an obligation).
+
+Pure planning (`plan_execution_control_candidates`) does **not** emit obligations;
+they are selected only in the mutable **apply** stage (`apply_execution_control_plan`).
+
+## Runtime ownership
+
+- 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
+ Core when `CoreExecutionControlApplyContext` is supplied to `run_core_step` /
+ wakeup APIs.
+
+## Further reading
+
+- [`reference/events-reference.md`](../reference/events-reference.md)
+- [`code-map/core-pipeline-map.md`](../code-map/core-pipeline-map.md)
+- Tests: `tests/semantics/test_control_time_scheduling_semantics.py`
diff --git a/docs/how-to/add-canonical-event.md b/docs/how-to/add-canonical-event.md
new file mode 100644
index 0000000..86f86b8
--- /dev/null
+++ b/docs/how-to/add-canonical-event.md
@@ -0,0 +1,16 @@
+# How To Add a Canonical Event
+
+1. Add or update the Pydantic Event model in
+ `tradingchassis_core/core/domain/types.py`.
+2. Register the Event in `core/domain/event_model.py` canonical category mapping.
+3. Add reducer handling in `core/domain/processing.py` within
+ `process_canonical_event`.
+4. Add/update semantics tests under `tests/semantics/`.
+5. Export the model from `tradingchassis_core/__init__.py` if part of public API.
+6. Update `docs/reference/events-reference.md` and `docs/reference/public-api.md`.
+
+Rules:
+
+- Keep canonical processing deterministic.
+- Do not introduce Venue Adapter or dispatch logic in reducers.
+- Keep Pydantic contracts as source of truth.
diff --git a/docs/how-to/update-core-step-pipeline.md b/docs/how-to/update-core-step-pipeline.md
new file mode 100644
index 0000000..aed51cb
--- /dev/null
+++ b/docs/how-to/update-core-step-pipeline.md
@@ -0,0 +1,35 @@
+# How To Update CoreStep Pipeline Behavior
+
+Core step orchestration lives in
+`tradingchassis_core/core/domain/processing_step.py`.
+
+Recommended workflow:
+
+1. Start from `run_core_step` and identify which phase changes:
+ reduction, Strategy evaluation, reconciliation, Risk Engine (policy), or apply.
+2. Keep stage boundaries explicit:
+ - reduction first
+ - Strategy generation second
+ - candidate reconciliation third
+ - Risk Engine (policy) fourth
+ - Execution Control plan/apply fifth
+3. Preserve `CoreStepResult` as the public output contract.
+4. Add or update tests in `tests/semantics/test_core_pipeline_clean.py`.
+5. Confirm quickstart behavior still reflects the public contract.
+
+Guardrails:
+
+- No Runtime dispatch logic in Core pipeline code.
+- No legacy compatibility contract restoration.
+- Keep deterministic behavior and public API coherence.
+
+
+## CoreWakeupStep changes
+
+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
+ `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
new file mode 100644
index 0000000..3a825be
--- /dev/null
+++ b/docs/how-to/update-policy-and-execution-control.md
@@ -0,0 +1,33 @@
+# How To Update Risk Engine and Execution Control
+
+The Risk Engine (policy) and Execution Control are separate deterministic phases.
+
+## Risk Engine updates
+
+- Policy contract entrypoint:
+ `PolicyIntentEvaluator.evaluate_policy_intent(...)`
+- Core integration:
+ `core/domain/policy_risk_decision.py` and `run_core_step` policy phase
+- Built-in policy-only evaluator:
+ `core/risk/risk_engine.py`
+
+When updating Risk Engine policy behavior:
+
+1. Keep evaluation side-effect-free.
+2. Return explicit accept/reject with reason.
+3. Validate behavior with semantics tests.
+
+## Execution Control updates
+
+- Planning model:
+ `core/domain/execution_control_plan.py`
+- Apply stage:
+ `core/domain/execution_control_apply.py`
+- Runtime-facing non-canonical output:
+ `ControlSchedulingObligation`
+
+When updating Execution Control:
+
+1. Keep Queue/dispatchability decisions deterministic.
+2. Preserve `CoreStepResult.dispatchable_intents` contract.
+3. Use `ControlSchedulingObligation` for deferred control signals.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..433938a
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,44 @@
+# TradingChassis Core Docs
+
+This documentation set describes the standalone clean Core package baseline.
+
+## Contents
+
+- `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/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
+
+## Package Purpose
+
+TradingChassis Core is a deterministic trading decision engine library. It owns
+canonical contracts, State reduction, and step-level decision outputs.
+
+## Clean Core Pipeline
+
+1. `EventStreamEntry`
+2. `process_event_entry` / `process_canonical_event`
+3. Strategy evaluation
+4. generated Intents
+5. candidate records + dominance/reconciliation
+6. Risk Engine (policy)
+7. Execution Control plan/apply
+8. `CoreStepResult` outputs (`dispatchable_intents`,
+ optional `control_scheduling_obligation` for **rate-limit** deferral only—see
+ `flows/control-time-and-scheduling.md`)
+9. Runtime dispatch happens later; Runtime injects canonical Events (including
+ optional `ControlTimeEvent` when an obligation is realized)
+
+## Contract source of truth
+
+Pydantic contract models in `tradingchassis_core/core/domain/types.py` are the
+source of truth for canonical Event/Intent schemas.
+
+## Out of Scope
+
+- Runtime orchestration and Order lifecycle ownership
+- Venue Adapters, Backtesting/Live I/O, external dispatch
diff --git a/docs/order-submitted-event-contract-v1.md b/docs/order-submitted-event-contract-v1.md
deleted file mode 100644
index c136f8c..0000000
--- a/docs/order-submitted-event-contract-v1.md
+++ /dev/null
@@ -1,228 +0,0 @@
-# OrderSubmittedEvent / Dispatch Boundary Contract v1
-
----
-
-## Purpose and scope
-
-This document defines an implementation-facing boundary contract snapshot for the
-dispatch-time canonical order-entry record `OrderSubmittedEvent` after initial
-runtime wiring.
-
-This is a docs-contract reconciliation slice only:
-
-- it does not change runtime behavior;
-- it does not change snapshot compatibility reducers;
-- it does not canonicalize `OrderStateEvent`;
-- it does not introduce `FillEvent` ingress;
-- it does not change `mark_intent_sent`, `RiskEngine`, or Execution Control behavior.
-
----
-
-## Semantic source of truth and precedence
-
-`OSEC-01` - Main `docs` remains the semantic source of truth for Event semantics,
-Intent pipeline semantics, Order lifecycle semantics, Event Stream, and
-Processing Order.
-
-`OSEC-02` - This document is a `core` implementation boundary contract snapshot
-for the dispatch-time Submitted boundary. It does not redefine architecture
-semantics.
-
-`OSEC-03` - Existing `core` implementation snapshot semantics remain governed by
-[Core Stable Contract v1](core-stable-contract-v1.md). This contract records the
-implemented Submitted-boundary slice and its transition constraints; it does not
-claim full order/execution lifecycle canonicalization.
-
-Normative semantic sources:
-
-- `docs/docs/00-guides/terminology.md`
-- `docs/docs/10-architecture/intent-pipeline.md`
-- `docs/docs/20-concepts/intent-lifecycle.md`
-- `docs/docs/20-concepts/order-lifecycle.md`
-- `docs/docs/20-concepts/event-model.md`
-- `docs/docs/20-concepts/time-model.md`
-
----
-
-## Classification
-
-`OSEC-04` - `OrderSubmittedEvent` is classified as a canonical
-**Intent-related Event**.
-
-`OSEC-05` - `OrderSubmittedEvent` is not an Execution Event in this contract.
-
-`OSEC-06` - Rationale:
-
-- Execution Events represent venue/simulated-venue execution feedback records.
-- The Submitted boundary record captures a dispatch/submission pipeline outcome
- from infrastructure processing.
-- Therefore the semantic class is Intent-related Event, not Execution Event.
-
-`OSEC-07` - This classification is implemented in current `core` v1 candidate
-taxonomy and canonical processing boundary behavior.
-
----
-
-## Creation trigger
-
-`OSEC-08` - `OrderSubmittedEvent` is created only after successful outbound
-transmission/dispatch of a `new` intent.
-
-`OSEC-09` - In current runtime-oriented terms, the dispatch-success boundary is:
-
-1. intent was accepted for immediate send;
-2. outbound `execution.apply_intents(...)` did not fail for the order key;
-3. dispatch success boundary is reached for that outbound new-order send.
-
-`OSEC-10` - Failed venue/runtime submission creates no `OrderSubmittedEvent`.
-
-`OSEC-11` - Replace/cancel dispatches do not create a new
-`OrderSubmittedEvent`.
-
----
-
-## Required field contract (v1, implemented boundary shape)
-
-`OSEC-12` - Required canonical boundary fields in this implemented slice:
-
-- `ts_ns_local_dispatch`
-- `instrument`
-- `client_order_id`
-- `side`
-- `order_type`
-- `intended_price`
-- `intended_qty`
-- `time_in_force`
-
-Canonical ProcessingPosition authority is carried by `EventStreamEntry.position`
-at canonical ingestion (`process_event_entry` / `process_canonical_event`), not
-as an inline `OrderSubmittedEvent` model field in this slice.
-
-`OSEC-13` - Optional/correlation fields when available:
-
-- `intent_correlation_id`
-- `dispatch_attempt_id` (if introduced in a future runtime boundary)
-- venue/runtime correlation metadata
-
-`OSEC-14` - Optional/correlation fields are not canonical identity authority in
-this contract.
-
----
-
-## Identity and correlation contract
-
-`OSEC-15` - Canonical order key for this v1 boundary is
-`(instrument, client_order_id)`.
-
-`OSEC-16` - `client_order_id` is the stable dispatch/order correlation key in
-this slice.
-
-`OSEC-17` - Venue/runtime IDs remain correlation metadata only for this slice.
-
-`OSEC-18` - Replace/cancel intents target an existing order key and do not
-restart lifecycle from `Submitted`.
-
----
-
-## Projection and coexistence behavior (transitional)
-
-`OSEC-19` - `OrderSubmittedEvent` is the canonical authority for entering
-`Submitted` in the current implemented boundary slice.
-
-`OSEC-20` - `CanonicalOrderProjection` is created/preserved at `submitted` from
-the `OrderSubmittedEvent` reducer path in the current implemented slice.
-
-`OSEC-21` - `mark_intent_sent` remains compatibility/execution-control
-bookkeeping during transition.
-
-`OSEC-22` - In current HFT runtime wiring, `OrderSubmittedEvent` processing is
-performed before `mark_intent_sent` for successful `new` dispatches. Failed
-`new` dispatches produce no `OrderSubmittedEvent`, and replace/cancel dispatches
-produce no `OrderSubmittedEvent`.
-
-`OSEC-23` - Transitional coexistence requirement: `mark_intent_sent`-based
-submitted sidecar seeding must be treated as idempotent/mirrored behavior under
-future coexistence with `OrderSubmittedEvent`.
-
-`OSEC-24` - This contract introduces no post-submission transition authority.
-Post-submission canonical authority remains deferred pending explicit canonical
-execution-feedback source.
-
----
-
-## ProcessingPosition policy
-
-`OSEC-25` - Canonical acceptance order uses one global canonical position
-counter across canonical event categories.
-
-`OSEC-26` - Category-local canonical counters are not allowed.
-
-`OSEC-27` - Position must not be derived from timestamps.
-
-`OSEC-28` - Ordering semantics must be coherent relative to canonical
-`MarketEvent` and future canonical execution-feedback records.
-
----
-
-## Compatibility boundaries preserved
-
-`OSEC-29` - `OrderStateEvent` remains non-canonical.
-
-`OSEC-30` - `ingest_order_snapshots` behavior remains unchanged.
-
-`OSEC-31` - `DerivedFillEvent` remains compatibility projection behavior.
-
-`OSEC-32` - `FillEvent` ingress remains deferred.
-
-`OSEC-33` - Snapshot reducer behavior remains unchanged; no rewrite is introduced
-by this contract.
-
-`OSEC-34` - This docs slice introduces no runtime behavior change.
-
----
-
-## No-double-authority rules
-
-`OSEC-35` - Submitted entry authority belongs to `OrderSubmittedEvent` in this
-implemented slice.
-
-`OSEC-36` - Compatibility snapshots may mirror/advance sidecar projections only
-under transitional compatibility rules; they are not canonical Submitted
-authority.
-
-`OSEC-37` - Post-submission transitions remain deferred until explicit canonical
-execution-feedback sources are defined and contracted.
-
-`OSEC-38` - Snapshot materialization must not become canonical Submitted
-authority in this phase.
-
----
-
-## Explicitly out of scope
-
-`OSEC-39` - Changing `OrderSubmittedEvent` model shape beyond current implemented
-contract fields.
-
-`OSEC-40` - Event taxonomy semantic reclassification beyond current implemented
-`intent_related` status.
-
-`OSEC-41` - Runtime dispatch behavior expansion beyond current successful `new`
-dispatch emission semantics.
-
-`OSEC-42` - `FillEvent` ingress implementation.
-
-`OSEC-43` - `OrderStateEvent` canonicalization.
-
-`OSEC-44` - Replay/storage/`ProcessingContext`/`EventStreamCursor`
-implementation.
-
-`OSEC-45` - Broad order lifecycle migration or snapshot reducer migration.
-
----
-
-## Relationship to existing core contracts
-
-- [Core Stable Contract v1](core-stable-contract-v1.md)
-- [Runtime Execution Feedback Contract v1](runtime-execution-feedback-contract-v1.md)
-- [Runtime-to-CoreConfiguration Contract Boundary v1](runtime-to-coreconfiguration-contract-v1.md)
-
diff --git a/docs/package-rename-stage-0-decision-v1.md b/docs/package-rename-stage-0-decision-v1.md
deleted file mode 100644
index 94b5c8c..0000000
--- a/docs/package-rename-stage-0-decision-v1.md
+++ /dev/null
@@ -1,182 +0,0 @@
-# Package Rename Stage 0 Decision v1
-
----
-
-## Purpose and scope
-
-This document records the Stage 0 naming decision for the package-rename track
-across `core` and `core-runtime`.
-
-This is a planning record only.
-
-This page:
-
-- does not change production code;
-- does not change tests;
-- does not rename packages/directories in implementation yet;
-- does not change imports, pyproject metadata, or JSON configs yet;
-- does not change runtime behavior, adapters, reducers, or event taxonomy;
-- does not implement deferred semantic items (`FillEvent` ingress,
- `ExecutionFeedbackRecordSource`, `ProcessingContext`, replay/storage).
-
----
-
-## Inputs used
-
-- Current core distribution: `trading-framework`
-- Current core import root: `trading_framework`
-- Current core semantic subtree: `trading_framework.core`
-- Current runtime distribution: `trading-runtime`
-- Current runtime import root: `trading_runtime`
-- Desired direction: align names with Core / Core Runtime terminology
-- Critical design gate: top-level `core` import can create `core.core.*` unless
- the current inner `core` package is also renamed/flattened.
-
----
-
-## Final naming targets (decision)
-
-### Core repository (`core`)
-
-- **Repository/folder display name:** keep `core` (already aligned)
-- **Python import root target:** `tradingchassis_core`
-- **Distribution/project name target:** `tradingchassis-core`
-- **Internal package layout target:** keep current structure shape during rename
- slice (`tradingchassis_core/core/...`) to avoid semantic/mechanical coupling
-- **`core.core.*` decision:** avoid
-- **Flatten `trading_framework.core.*` into `core.*` now?:** no (explicitly
- deferred; would be a separate structural refactor)
-
-### Core Runtime repository (`core-runtime`)
-
-- **Repository/folder display name:** keep `core-runtime` (already aligned)
-- **Python import root target:** `core_runtime`
-- **Distribution/project name target:** `tradingchassis-core-runtime`
-- **Internal package layout target:** keep current structure shape during rename
- slice (`core_runtime/...`, same module topology as today)
-- **`trading_runtime.*` to `core_runtime.*`:** yes
-
----
-
-## Candidate option comparison
-
-### A) Import root `core`, flattened layout, distribution `core` or `tradingchassis-core`
-
-- **Readability:** high if fully flattened
-- **Collision risk:** medium/high (`core` is generic)
-- **PyPI realism:** `core` is weak/high-conflict; `tradingchassis-core` is good
-- **Import churn:** very high (import root + subtree flatten)
-- **`class_path` churn:** medium (runtime still changes)
-- **Docs alignment:** good
-- **Maintainability:** potentially good long-term, but high migration risk now
-- **Nested structure simplification:** yes
-
-### B) Import root `core`, accept `core.core.*`
-
-- **Readability:** low (`core.core.*` duplication)
-- **Collision risk:** high (`core`)
-- **PyPI realism:** weak if distribution is `core`
-- **Import churn:** medium
-- **`class_path` churn:** medium
-- **Docs alignment:** partial
-- **Maintainability:** poor naming ergonomics
-- **Nested structure simplification:** no
-
-### C) Import root `tradingchassis_core`, distribution `tradingchassis-core`
-
-- **Readability:** good and explicit
-- **Collision risk:** low
-- **PyPI realism:** good
-- **Import churn:** medium (mechanical, bounded)
-- **`class_path` churn:** medium (runtime rename still required)
-- **Docs alignment:** good (docs can still refer to Core conceptually)
-- **Maintainability:** high (globally unique import root)
-- **Nested structure simplification:** no immediate flatten; deferred
-
-### D) Hybrid: docs/repo names updated, imports remain `trading_framework`
-
-- **Readability:** mixed (conceptual and technical names diverge)
-- **Collision risk:** low
-- **PyPI realism:** unchanged
-- **Import churn:** none now
-- **`class_path` churn:** none now
-- **Docs alignment:** partial
-- **Maintainability:** medium/low (long transitional mismatch)
-- **Nested structure simplification:** no
-
-### E) Compatibility aliases first before final rename
-
-- **Readability:** transitional complexity
-- **Collision risk:** low if final names are unique
-- **PyPI realism:** depends on chosen final names (good with option C targets)
-- **Import churn:** staged, lower immediate blast radius
-- **`class_path` churn:** staged with deprecation window
-- **Docs alignment:** good if clearly documented
-- **Maintainability:** good when time-boxed; poor if indefinite
-- **Nested structure simplification:** not by itself
-
----
-
-## Recommended final target
-
-Adopt **Option C as final naming target** plus a **time-boxed Option E
-compatibility phase** for migration safety.
-
-Rationale:
-
-1. Avoids the `core.core.*` naming trap without forcing an inner package
- flatten/rename in the same slice.
-2. Uses unique, realistic distribution names (`tradingchassis-*`) and avoids
- generic package-name collision risk.
-3. Preserves semantics and structure for a behavior-preserving mechanical rename.
-4. Keeps room for a future separate structural simplification decision after the
- rename has stabilized.
-
----
-
-## Explicit import and class_path mapping targets
-
-- `trading_framework.core.domain.types` ->
- `tradingchassis_core.core.domain.types`
-- `trading_framework.core.domain.processing` ->
- `tradingchassis_core.core.domain.processing`
-- `trading_runtime.backtest.engine.strategy_runner` ->
- `core_runtime.backtest.engine.strategy_runner`
-- `trading_runtime.strategies.debug_strategy:DebugStrategyV1` ->
- `core_runtime.strategies.debug_strategy:DebugStrategyV1`
-
----
-
-## Compatibility strategy decision
-
-Use temporary compatibility shims as an explicit, time-boxed migration bridge:
-
-- Provide temporary re-export compatibility for:
- - `trading_framework` -> `tradingchassis_core`
- - `trading_runtime` -> `core_runtime`
-- Maintain shims for one defined deprecation window (recommended: one minor
- release cycle), with deprecation warnings.
-- Require external JSON `strategy.class_path` and external imports to migrate
- during that window.
-- Remove shims after the window closes to prevent permanent dual-namespace debt.
-
----
-
-## Next implementation slice decision
-
-Choose **D: compatibility alias introduction first** as the smallest safe next
-implementation slice after Stage 0.
-
-Then proceed with coordinated mechanical renames in both repos once compatibility
-coverage is validated.
-
----
-
-## Non-goals for this decision
-
-- No implementation of package rename in this document.
-- No reducer/event/runtime semantic changes.
-- No adapter boundary changes.
-- No replay/storage/event-stream persistence implementation.
-
----
diff --git a/docs/post-submission-lifecycle-compatibility-map-v1.md b/docs/post-submission-lifecycle-compatibility-map-v1.md
deleted file mode 100644
index 4f1b097..0000000
--- a/docs/post-submission-lifecycle-compatibility-map-v1.md
+++ /dev/null
@@ -1,144 +0,0 @@
-# Post-Submission Lifecycle Compatibility Map v1
-
----
-
-## Purpose and scope
-
-This document freezes the current implementation-facing authority split for order
-lifecycle semantics after submission in `core`.
-
-This is a docs-only contract slice:
-
-- it documents current lifecycle authority boundaries;
-- it does not implement behavior;
-- it does not change reducers or runtime behavior;
-- it does not implement `FillEvent` ingress;
-- it does not canonicalize `OrderStateEvent`.
-
-`PSLCM-01` - Main `docs` remains the semantic source of truth for Event, Event
-Stream, Processing Order, Order lifecycle, and determinism semantics.
-
-`PSLCM-02` - This page is implementation-facing and freezes current authority
-split behavior in `core` contracts; it does not redefine architecture semantics.
-
-`PSLCM-03` - This page must remain consistent with:
-
-- [Core Stable Contract v1](core-stable-contract-v1.md)
-- [Runtime Execution Feedback Contract v1](runtime-execution-feedback-contract-v1.md)
-- [Runtime/Adapter Execution Feedback Source Contract v1](runtime-adapter-execution-feedback-source-contract-v1.md)
-- [OrderSubmittedEvent / Dispatch Boundary Contract v1](order-submitted-event-contract-v1.md)
-
-Normative semantic references from main `docs`:
-
-- `docs/docs/00-guides/terminology.md`
-- `docs/docs/20-concepts/event-model.md`
-- `docs/docs/20-concepts/order-lifecycle.md`
-
----
-
-## Canonical authority today
-
-`PSLCM-04` - `OrderSubmittedEvent` is canonical authority for lifecycle entry at
-`Submitted`.
-
-`PSLCM-05` - Current runtime wiring emits/processes `OrderSubmittedEvent` only
-for successful `new` dispatches; failed `new` dispatches create no
-`OrderSubmittedEvent`, and replace/cancel dispatches do not create new
-`OrderSubmittedEvent` records.
-
-`PSLCM-06` - `ProcessingPosition` remains boundary/global acceptance-order
-authority for canonical ingestion. Ordering authority is not timestamp-derived.
-
----
-
-## Compatibility authority today
-
-`PSLCM-07` - `OrderStateEvent` remains compatibility-only and non-canonical at
-the canonical boundary.
-
-`PSLCM-08` - `ingest_order_snapshots` remains the compatibility snapshot
-materialization path.
-
-`PSLCM-09` - `apply_order_state_event` remains the compatibility reducer and
-projection path for post-submission lifecycle progression.
-
-`PSLCM-10` - `DerivedFillEvent` remains a non-canonical compatibility
-projection artifact derived from snapshot progression.
-
-`PSLCM-11` - `mark_intent_sent` remains compatibility execution-control /
-bookkeeping sidecar behavior and must not be interpreted as Event Stream
-authority.
-
----
-
-## Current lifecycle compatibility map (frozen snapshot)
-
-`PSLCM-12` - Post-submission lifecycle progression remains
-compatibility-governed until canonical execution-feedback source gates are
-satisfied.
-
-| lifecycle transition | current source | canonical or compatibility classification | affected state/projection | semantic drift risk | future migration gate |
-| --- | --- | --- | --- | --- | --- |
-| none/new -> `Submitted` | successful `new` dispatch -> `OrderSubmittedEvent` canonical boundary processing (with `mark_intent_sent` bookkeeping sidecar) | canonical entry authority (`OrderSubmittedEvent`); sidecar bookkeeping remains compatibility | canonical order projection (`canonical_orders`) at `submitted`; bookkeeping (`inflight`, `last_sent_intents`) | medium (dual-path coexistence can be misread as dual authority) | retain single entry authority at `OrderSubmittedEvent`; keep `mark_intent_sent` non-authoritative |
-| `Submitted` -> `Accepted` | snapshot ingestion/materialization (`ingest_order_snapshots` -> `OrderStateEvent` -> `apply_order_state_event`) | compatibility | compatibility order snapshots and sidecar lifecycle projection advancement | high (snapshot mapping and compatibility-state normalization) | canonical execution-feedback source and mapping required before authority move |
-| `Submitted` -> `Rejected` | snapshot-derived `OrderStateEvent(state_type="rejected")` | compatibility | compatibility snapshots and sidecar projection | high | canonical execution-feedback source and deterministic correlation required |
-| `Accepted` -> `PartiallyFilled` | snapshot-derived `OrderStateEvent(state_type="partially_filled")` | compatibility | compatibility snapshots; sidecar projection; snapshot-derived fill projection potential | high | canonical execution-feedback source with authoritative cumulative progression |
-| `PartiallyFilled` -> `PartiallyFilled` | repeated snapshot cumulative progression updates | compatibility | compatibility snapshots; `DerivedFillEvent` projection emission on cumulative increase | high | explicit canonical fill granularity and no-double-counting policy |
-| `Accepted`/`PartiallyFilled` -> `Filled` | snapshot-derived terminal state updates | compatibility | compatibility snapshots (terminal removal), sidecar projection terminal progression, snapshot-derived fill projection | high | canonical `FillEvent` ingress gates + explicit cutover policy |
-| `Accepted`/`PartiallyFilled` -> `Canceled` | snapshot-derived terminal state updates | compatibility | compatibility snapshots (terminal removal), sidecar projection terminal progression | high | canonical execution-feedback source and deterministic ordering/correlation |
-
----
-
-## Guardrails (must hold in this phase)
-
-`PSLCM-13` - `OrderStateEvent` must remain rejected at canonical boundary
-processing.
-
-`PSLCM-14` - `DerivedFillEvent` must remain non-canonical compatibility
-projection behavior.
-
-`PSLCM-15` - Runtime `FillEvent` ingress remains gated by execution-feedback
-source-authority requirements (`ExecutionFeedbackRecordSource` contract family).
-
-`PSLCM-16` - `mark_intent_sent` must not be treated as canonical Event Stream
-authority.
-
-`PSLCM-17` - Snapshot progression must not be described or promoted as canonical
-execution feedback in this phase.
-
----
-
-## Future migration gates
-
-`PSLCM-18` - Lifecycle authority migration for post-submission transitions may
-begin only when all of the following are satisfied:
-
-- authoritative `ExecutionFeedbackRecordSource` exists for the target scope;
-- deterministic strictly monotone non-timestamp `source_sequence` exists;
-- source-authoritative liquidity and deterministic canonical correlation exist;
-- explicit global `ProcessingPosition` merge policy exists;
-- explicit no-double-counting cutover policy relative to `DerivedFillEvent`
- exists.
-
-`PSLCM-19` - Post-submission lifecycle authority must move only after these
-gates are satisfied and validated; until then, compatibility authority remains
-frozen as documented here.
-
----
-
-## Explicit non-goals for this slice
-
-`PSLCM-20` - No snapshot-derived canonical `FillEvent` emission.
-
-`PSLCM-21` - No `OrderStateEvent` canonicalization.
-
-`PSLCM-22` - No `DerivedFillEvent` removal or behavior change.
-
-`PSLCM-23` - No lifecycle reducer rewrite.
-
-`PSLCM-24` - No adapter API work.
-
-`PSLCM-25` - No replay/storage/`ProcessingContext`/`EventStreamCursor`
-implementation.
-
----
diff --git a/docs/processing-context-event-stream-cursor-contract-v1.md b/docs/processing-context-event-stream-cursor-contract-v1.md
deleted file mode 100644
index 487559c..0000000
--- a/docs/processing-context-event-stream-cursor-contract-v1.md
+++ /dev/null
@@ -1,198 +0,0 @@
-# ProcessingContext / EventStreamCursor Contract v1
-
----
-
-## Purpose and scope
-
-This document defines docs-only ownership and boundary semantics for deferred
-`ProcessingContext` abstraction work and runtime-owned `EventStreamCursor`
-boundary responsibilities.
-
-This is a planning/contract slice only:
-
-- it does not implement `ProcessingContext`;
-- it does not introduce new `EventStreamCursor` behavior;
-- it does not change runtime behavior;
-- it does not change reducers or event taxonomy;
-- it does not implement canonical `FillEvent` ingress;
-- it does not add adapter APIs;
-- it does not canonicalize `OrderStateEvent`;
-- it does not change `DerivedFillEvent` behavior;
-- it does not change snapshot ingestion behavior;
-- it does not implement replay/storage/EventStream persistence.
-
-`PCESC-01` - Main `docs` remains the semantic source of truth for Event,
-Event Stream, Processing Order, Configuration, Runtime, and Venue Adapter
-semantics.
-
-`PCESC-02` - This page is implementation-facing boundary planning for
-future abstraction ownership only. It does not redefine architecture semantics.
-
-`PCESC-03` - This page must remain consistent with:
-
-- [Core Stable Contract v1](core-stable-contract-v1.md)
-- [Venue Adapter Capability Model v1](venue-adapter-capability-model-v1.md)
-- [Post-Submission Lifecycle Compatibility Map v1](post-submission-lifecycle-compatibility-map-v1.md)
-- [Runtime Execution Feedback Contract v1](runtime-execution-feedback-contract-v1.md)
-- [Runtime/Adapter Execution Feedback Source Contract v1](runtime-adapter-execution-feedback-source-contract-v1.md)
-
-Normative semantic references from main `docs`:
-
-- `docs/docs/00-guides/terminology.md`
-- `docs/docs/20-concepts/event-model.md`
-- `docs/docs/20-concepts/time-model.md`
-- `docs/docs/20-concepts/determinism-model.md`
-
----
-
-## Responsibility split
-
-### EventStreamCursor responsibility (conceptual)
-
-`PCESC-04` - `EventStreamCursor` is an ordering-only abstraction.
-
-`PCESC-05` - `EventStreamCursor` conceptually allocates/advances global
-canonical `ProcessingPosition` values for Runtime canonical entry formation.
-
-`PCESC-06` - Cursor sequence semantics are deterministic and strictly monotone.
-
-`PCESC-07` - `EventStreamCursor` must not carry event payloads.
-
-`PCESC-08` - `EventStreamCursor` must not carry `CoreConfiguration`.
-
-`PCESC-09` - `EventStreamCursor` must not carry adapter handles.
-
-`PCESC-10` - `EventStreamCursor` must not carry persistence/storage handles.
-
-### ProcessingContext responsibility (conceptual)
-
-`PCESC-11` - `ProcessingContext` is runtime-owned invocation scope metadata.
-
-`PCESC-12` - `ProcessingContext` conceptually carries explicit
-`CoreConfiguration` reference for canonical boundary invocation scope.
-
-`PCESC-13` - `ProcessingContext` conceptually carries declared capability scope
-and merge-policy selection metadata.
-
-`PCESC-14` - `ProcessingContext` must not carry canonical event history.
-
-`PCESC-15` - `ProcessingContext` must not mutate `StrategyState` directly.
-
-`PCESC-16` - `ProcessingContext` must not redefine adapter capability semantics.
-
-`PCESC-17` - `ProcessingContext` must not become canonical core input payload
-shape in this contract slice.
-
----
-
-## Ownership model
-
-`PCESC-18` - Runtime owns `ProcessingContext` and `EventStreamCursor`
-abstractions (if introduced in future implementation slices).
-
-`PCESC-19` - Core owns canonical boundary validation and reduction of
-`EventStreamEntry`.
-
-`PCESC-20` - Adapter owns venue/source capability exposure only.
-
-`PCESC-21` - `EventStreamEntry` remains minimal (`position`, `event`).
-
-`PCESC-22` - Configuration remains call-level processing input and must not
-move into `EventStreamEntry` payload shape.
-
----
-
-## Current state snapshot (frozen for this phase)
-
-`PCESC-23` - Current runtime runner uses a runtime-owned `EventStreamCursor`
-ordering helper for canonical positioned entry formation.
-
-`PCESC-24` - Current runtime runner creates `EventStreamEntry` records at the
-runner boundary before calling `process_event_entry(...)`.
-
-`PCESC-25` - Current runtime runner passes `CoreConfiguration` explicitly into
-`process_event_entry(...)` as call-level processing input.
-
-`PCESC-26` - Current compatibility `rc == 3` order/account snapshot branch
-continues to bypass canonical `EventStreamEntry` by design and remains
-compatibility behavior in this phase.
-
----
-
-## Conceptual future relation (non-implemented)
-
-`PCESC-27` - Future slices may extend/refine `EventStreamCursor` integration
-while preserving current global ordering semantics.
-
-`PCESC-28` - If implemented in a future slice, `ProcessingContext` would gather
-run/session invocation-scope metadata without changing canonical payload shapes.
-
-`PCESC-29` - Runtime would remain responsible for constructing
-`EventStreamEntry` values from canonical events and positioned ordering metadata.
-
-`PCESC-30` - Core would remain non-owner of adapter polling and position
-allocation orchestration.
-
-`PCESC-31` - This relation introduces no replay/storage/persistence semantics.
-
----
-
-## Out of scope
-
-`PCESC-32` - Replay engine implementation.
-
-`PCESC-33` - Event Stream storage/persistence implementation.
-
-`PCESC-34` - Adapter interface design or adapter API implementation.
-
-`PCESC-35` - Canonical runtime `FillEvent` ingress implementation.
-
-`PCESC-36` - Post-submission lifecycle migration away from compatibility
-snapshot authority.
-
-`PCESC-37` - ControlTimeEvent queue/rate authority migration.
-
-`PCESC-38` - `OrderStateEvent` canonicalization.
-
-`PCESC-39` - `DerivedFillEvent` behavior change/removal.
-
----
-
-## Guardrails
-
-`PCESC-40` - `EventStreamCursor` must not derive `ProcessingPosition` from
-timestamps.
-
-`PCESC-41` - `EventStreamCursor` must not reset or fork sequence authority
-within one canonical stream scope.
-
-`PCESC-42` - `ProcessingContext` must not hide mutable configuration changes.
-
-`PCESC-43` - `ProcessingContext` must not smuggle venue-specific schemas into
-core canonical processing payload shapes.
-
-`PCESC-44` - `ProcessingContext` must not define hidden state-mutation authority
-outside Event processing.
-
-`PCESC-45` - `EventStreamEntry` must remain config-free and minimal.
-
-`PCESC-46` - `ProcessingPosition` remains global canonical ordering authority
-and must remain non-timestamp-derived.
-
----
-
-## Future implementation prerequisites
-
-`PCESC-47` - A future implementation slice requires an explicit runtime refactor
-plan before code changes.
-
-`PCESC-48` - Tests must preserve existing canonical event ordering behavior.
-
-`PCESC-49` - Tests must preserve existing compatibility snapshot behavior.
-
-`PCESC-50` - Tests must demonstrate that cursor-emitted sequence matches current
-counter sequence for currently wired canonical paths.
-
-`PCESC-51` - First implementation path must not require core reducer changes.
-
----
diff --git a/docs/reference/events-reference.md b/docs/reference/events-reference.md
new file mode 100644
index 0000000..e1e79e4
--- /dev/null
+++ b/docs/reference/events-reference.md
@@ -0,0 +1,47 @@
+# Events and Intents Reference
+
+TradingChassis Core accepts canonical Event contracts and produces Intent/decision
+contracts. Pydantic models are the schema source of truth.
+
+## Canonical Event Models
+
+- `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;
+ see `../flows/control-time-and-scheduling.md`.
+- `OrderSubmittedEvent`: canonical submitted-order acknowledgement
+- `OrderExecutionFeedbackEvent`: canonical account/execution feedback
+- `FillEvent`: canonical fill lifecycle update
+
+Canonical ingestion boundary:
+
+- `process_canonical_event(state, event, ...)`
+- `process_event_entry(state, EventStreamEntry(...), ...)`
+
+## Processing Order Models
+
+- `ProcessingPosition`
+- `EventStreamEntry`
+
+These models provide deterministic ordering metadata without implementing a full
+stream storage/replay subsystem.
+
+## Intent Models
+
+- `OrderIntent` (discriminated union)
+- `NewOrderIntent`
+- `CancelOrderIntent`
+- `ReplaceOrderIntent`
+- `Price`
+- `Quantity`
+
+## Non-canonical Output Models
+
+- `CandidateIntentRecord` with `CandidateIntentOrigin`
+- `PolicyRiskDecision`
+- `ExecutionControlDecision`
+- `CoreStepDecision`
+- `CoreStepResult`
+- `ControlSchedulingObligation` (time-dependent **rate-limit** recheck hint; not
+ emitted for **inflight-only** deferral by default—see `../flows/control-time-and-scheduling.md`)
diff --git a/docs/reference/public-api.md b/docs/reference/public-api.md
new file mode 100644
index 0000000..ff7a77f
--- /dev/null
+++ b/docs/reference/public-api.md
@@ -0,0 +1,69 @@
+# Public API Reference
+
+The public package boundary is the `tradingchassis_core` root import.
+
+## Canonical Events
+
+- `MarketEvent`
+- `ControlTimeEvent`
+- `OrderSubmittedEvent`
+- `OrderExecutionFeedbackEvent`
+- `FillEvent`
+
+## Step APIs
+
+- `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)
+
+## Step inputs/outputs
+
+- `EventStreamEntry`
+- `ProcessingPosition`
+- `CorePolicyAdmissionContext`
+- `CoreExecutionControlApplyContext`
+- `CoreStepDecision`
+- `CoreStepResult`
+- `CoreWakeupReductionResult`
+- `CoreWakeupStrategyContext`
+- `CoreWakeupStrategyEvaluator`
+
+## Supporting deterministic models
+
+- `CoreConfiguration`
+- `StrategyState`
+- `CandidateIntentRecord`
+- `CandidateIntentOrigin`
+- `PolicyRiskDecision`
+- `ExecutionControlDecision`
+- `ExecutionControl`
+- `ControlSchedulingObligation` (non-canonical; **rate-limit** recheck hint in the
+ current slice—see `../flows/control-time-and-scheduling.md`)
+
+## Intents and numeric models
+
+- `OrderIntent`
+- `NewOrderIntent`
+- `CancelOrderIntent`
+- `ReplaceOrderIntent`
+- `Price`
+- `Quantity`
+
+## Runtime-safe utilities
+
+- `NullEventBus`
+- `RiskEngine` (Risk Engine; policy-only)
+- `RiskConfig`
+
+## Publicly absent by design
+
+- `GateDecision`
+- `compat_gate_decision`
+- `ControlTimeQueueReevaluationContext`
+- `CoreDecisionContext`
+- `OrderStateEvent`
+- `DerivedFillEvent`
+- `VenueAdapter` / `VenuePolicy`
diff --git a/docs/runtime-adapter-execution-feedback-source-contract-v1.md b/docs/runtime-adapter-execution-feedback-source-contract-v1.md
deleted file mode 100644
index 9e022fd..0000000
--- a/docs/runtime-adapter-execution-feedback-source-contract-v1.md
+++ /dev/null
@@ -1,811 +0,0 @@
-# Runtime/Adapter Execution Feedback Source Contract v1
-
----
-
-## Purpose and scope
-
-This document defines the source-authority boundary that a future runtime/adapter
-execution-feedback source must satisfy before canonical `FillEvent` ingress can
-be implemented.
-
-This is a docs-contract slice only:
-
-- it does not implement canonical `FillEvent` ingress;
-- it does not add or implement adapter APIs;
-- it does not modify runtime behavior;
-- it does not canonicalize `OrderStateEvent`;
-- it does not change `DerivedFillEvent` behavior;
-- it does not change snapshot ingestion behavior;
-- it does not change reducers or event taxonomy.
-
-`RAEFSC-01` - Current runtime remains ineligible for canonical `FillEvent`
-ingress under the source-authority requirements defined in this contract.
-
-`RAEFSC-02` - Snapshot-derived fill progression remains compatibility projection
-behavior (`DerivedFillEvent`) in this phase.
-
----
-
-## Semantic source of truth and precedence
-
-`RAEFSC-03` - Main `docs` repository remains the semantic source of truth for
-Event semantics, Event Stream semantics, Processing Order, execution/order
-lifecycle, and determinism.
-
-`RAEFSC-04` - This document is an implementation-facing boundary/source contract
-for future runtime/adapter work. It does not redefine architecture semantics.
-
-`RAEFSC-05` - Runtime execution-feedback eligibility statements must remain
-consistent with:
-
-- [Runtime Execution Feedback Contract v1](runtime-execution-feedback-contract-v1.md)
-- [Core Stable Contract v1](core-stable-contract-v1.md)
-
----
-
-## Current decision snapshot
-
-`RAEFSC-06` - No currently available runtime source satisfies canonical
-`FillEvent` source-authority requirements (`REFC-10` through `REFC-16`).
-
-`RAEFSC-07` - Canonical runtime `FillEvent` ingress remains deferred.
-
-`RAEFSC-08` - `OrderStateEvent` remains compatibility-only and non-canonical at
-the canonical boundary.
-
-`RAEFSC-09` - `DerivedFillEvent` remains compatibility projection and
-non-canonical.
-
----
-
-## Source eligibility contract (v1)
-
-`RAEFSC-10` - A source is eligible for future canonical `FillEvent` ingress
-only when records are explicit Venue or simulated-Venue execution-feedback
-records from the execution path.
-
-`RAEFSC-11` - Source is explicitly ineligible when records are inferred from:
-
-- compatibility snapshot deltas;
-- market trade feed inference;
-- submit/modify/cancel synchronous return codes.
-
-`RAEFSC-12` - Offline/recorder artifacts are ineligible as runtime canonical
-ingress unless replayed as authoritative Event Stream input under a positioned
-ingestion contract that preserves deterministic `ProcessingPosition`.
-
----
-
-## Granularity contract (v1)
-
-`RAEFSC-13` - Acceptable v1 canonical execution-feedback granularity is
-per-cumulative execution update.
-
-`RAEFSC-14` - Per-fill execution reports are acceptable only when each report
-either:
-
-- carries authoritative cumulative filled quantity; or
-- can be deterministically represented as cumulative updates without heuristic
- reconstruction.
-
-`RAEFSC-15` - Cumulative filled quantity must be monotone per canonical order
-key for accepted execution-feedback progression.
-
----
-
-## FillEvent field source-authority contract (v1)
-
-`RAEFSC-16` - Required `FillEvent` fields must be authoritative from execution
-feedback source records (or direct deterministic mapping from those records and
-canonical order lineage), not heuristic synthesis:
-
-- `ts_ns_exch`
-- `ts_ns_local`
-- `instrument`
-- `client_order_id`
-- `side`
-- `filled_price`
-- `cum_filled_qty`
-- `time_in_force`
-- `liquidity_flag`
-
-`RAEFSC-17` - Optional `FillEvent` fields, when present, must be source
-authoritative:
-
-- `fee`
-- `intended_price`
-- `intended_qty`
-- `remaining_qty`
-
-`RAEFSC-18` - Heuristic synthesis of required `FillEvent` fields is prohibited
-in v1 unless a future explicit contract revision defines and permits that
-behavior.
-
----
-
-## Liquidity flag policy (v1)
-
-`RAEFSC-19` - `liquidity_flag` classification (`maker`, `taker`, `unknown`) must
-be source-authoritative execution-feedback data.
-
-`RAEFSC-20` - `unknown` is allowed only when the source explicitly reports
-unknown or indeterminate liquidity classification.
-
-`RAEFSC-21` - Synthetic defaulting to `unknown` is prohibited in v1.
-
----
-
-## Identity and correlation contract (v1)
-
-`RAEFSC-22` - Canonical order key for this boundary is
-`instrument + client_order_id`, unless a later explicit contract revision
-changes canonical order identity semantics.
-
-`RAEFSC-23` - Source/runtime must provide deterministic correlation from
-Venue-side order identifiers to canonical `client_order_id`.
-
-`RAEFSC-24` - Correlation to `OrderSubmittedEvent` lineage must be replay-stable
-under equivalent input streams and configuration.
-
-`RAEFSC-25` - Replace/cancel successor identifiers require an explicit
-deterministic mapping chain that preserves canonical order continuity and avoids
-ambiguous identity resolution.
-
----
-
-## Ordering and ProcessingPosition contract (v1)
-
-`RAEFSC-26` - All future canonical `FillEvent` ingress must enter through
-`EventStreamEntry` with global `ProcessingPosition` ordering at the canonical
-boundary.
-
-`RAEFSC-27` - Processing acceptance order must not be derived from timestamps.
-`Event Time` metadata does not define `ProcessingOrder`.
-
-`RAEFSC-28` - Source/adapter sequence contract must be deterministic and
-replay-equivalent for equivalent inputs.
-
-`RAEFSC-29` - Runner merge ordering relative to canonical `MarketEvent`,
-`OrderSubmittedEvent`, and `ControlTimeEvent` must be explicit and
-replay-equivalent under the global positioned boundary.
-
----
-
-## No-double-counting contract (v1)
-
-`RAEFSC-30` - Before canonical `FillEvent` is enabled, one semantic authority
-for fill progression must be defined for each source scope.
-
-`RAEFSC-31` - For overlapping scope, compatibility `DerivedFillEvent` path must
-be either:
-
-- retired; or
-- explicitly constrained to non-semantic observability with no canonical fill
- progression side effects.
-
-`RAEFSC-32` - Duplicate semantic fill progression for the same canonical
-order/cumulative state is prohibited.
-
-`RAEFSC-33` - Shadow/compare validation or explicit cutover reconciliation plan
-is required before production dual-path operation.
-
----
-
-## Runtime/adapter API sketch (conceptual only)
-
-`RAEFSC-34` - A future conceptual source record (`ExecutionFeedbackRecord`)
-should include, at minimum:
-
-- deterministic source sequence and/or source record id;
-- authoritative execution-feedback payload for canonical `FillEvent` mapping;
-- deterministic correlation fields needed to resolve canonical order identity.
-
-`RAEFSC-35` - Adapter guarantees (conceptual):
-
-- records are execution-feedback authoritative per eligibility clauses;
-- sequence/id semantics are stable and deterministic;
-- correlation fields are sufficient for replay-stable canonical mapping.
-
-`RAEFSC-36` - Runner assumptions (conceptual):
-
-- record-to-`FillEvent` mapping can be deterministic;
-- positioned canonical merge can be performed via global `ProcessingPosition`;
-- no-double-counting policy can be enforced at boundary cutover.
-
-`RAEFSC-37` - This section is conceptual only and does not define or introduce
-implementation APIs in this phase.
-
----
-
-## Acceptance criteria for future implementation
-
-`RAEFSC-38` - Future implementation may begin only when all required
-authoritative fields are available under this source contract.
-
-`RAEFSC-39` - Granularity semantics are stable and satisfy cumulative monotone
-requirements per canonical order key.
-
-`RAEFSC-40` - Deterministic global ordering via positioned canonical boundary is
-specified and testable.
-
-`RAEFSC-41` - Identity/correlation mapping is deterministic and replay-stable,
-including replace/cancel successor handling.
-
-`RAEFSC-42` - Liquidity policy requirements are satisfied without synthetic
-defaulting.
-
-`RAEFSC-43` - No-double-counting rules are explicit and testable.
-
-`RAEFSC-44` - Test plans can cover duplicates/regressions/idempotence and
-ordering determinism before ingress rollout.
-
----
-
-## Explicitly out of scope for this contract slice
-
-`RAEFSC-45` - Implementing canonical runtime `FillEvent` ingress.
-
-`RAEFSC-46` - Adapter API implementation.
-
-`RAEFSC-47` - `OrderStateEvent` canonicalization.
-
-`RAEFSC-48` - `DerivedFillEvent` removal or behavior change.
-
-`RAEFSC-49` - Snapshot reducer rewrite or compatibility ingestion redesign.
-
-`RAEFSC-50` - Replay/storage/`ProcessingContext`/`EventStreamCursor`
-implementation.
-
----
-
-## Appendix A: ExecutionFeedbackRecord adapter-facing source shape (Phase 4D)
-
-This appendix is adapter-facing and defines the minimum conceptual source shape
-required before future canonical `FillEvent` ingress work may start.
-
-This appendix is docs-contract only:
-
-- it does not implement `FillEvent` ingress;
-- it does not add adapter APIs;
-- it does not make current runtime eligible;
-- it does not modify runtime behavior;
-- it does not change snapshot compatibility behavior.
-
-`RAEFSC-51` - Current feasibility decision remains **C**: no existing
-runtime-adapter source satisfies this source contract end-to-end.
-
-`RAEFSC-52` - Canonical runtime `FillEvent` ingress remains deferred.
-
-`RAEFSC-53` - Compatibility projection authority is preserved in this phase:
-`DerivedFillEvent` remains the active compatibility path and snapshot
-materialization semantics remain unchanged.
-
----
-
-### A.1 Conceptual ExecutionFeedbackRecord source shape
-
-`RAEFSC-54` - The minimum conceptual adapter-facing source record
-(`ExecutionFeedbackRecord`) for future canonical ingress requires:
-
-- `source_sequence`
-- `ts_ns_exch`
-- `ts_ns_local`
-- `instrument`
-- `client_order_id`
-- optional `venue_order_id`
-- `side`
-- `time_in_force`
-- `filled_price`
-- `cum_filled_qty`
-- `liquidity_flag`
-
-`RAEFSC-55` - Optional authoritative fields, when provided, include:
-
-- `fee`
-- `remaining_qty`
-- `intended_price`
-- `intended_qty`
-- source metadata such as `source_id`, `venue`, or adapter metadata when needed
- for deterministic boundary mapping and observability.
-
-`RAEFSC-56` - This shape is conceptual boundary documentation only and does not
-define or introduce implementation APIs in this phase.
-
----
-
-### A.2 source_sequence contract
-
-`RAEFSC-57` - `source_sequence` must be strictly monotone within the adapter's
-execution-feedback source stream.
-
-`RAEFSC-58` - `source_sequence` must be deterministic for replay-equivalent
-inputs and configuration.
-
-`RAEFSC-59` - `source_sequence` must not be timestamp-derived.
-
-`RAEFSC-60` - `source_sequence` must be stable enough for runner merge into
-global `ProcessingPosition` ordering semantics.
-
----
-
-### A.3 Liquidity authority contract
-
-`RAEFSC-61` - `liquidity_flag` values (`maker`, `taker`, `unknown`) must be
-source-authoritative.
-
-`RAEFSC-62` - `unknown` is allowed only when explicitly reported by the source
-as unknown or indeterminate.
-
-`RAEFSC-63` - Synthetic defaulting to `unknown` is prohibited.
-
----
-
-### A.4 Identity and correlation contract
-
-`RAEFSC-64` - Canonical correlation to `instrument + client_order_id` is
-required for source record eligibility.
-
-`RAEFSC-65` - `venue_order_id` is correlation metadata for v1 unless a future
-explicit contract revision changes canonical identity semantics.
-
-`RAEFSC-66` - Replace/cancel successor correlation mapping must be explicit,
-deterministic, and replay-stable.
-
-`RAEFSC-67` - Source records without deterministic canonical correlation are
-ineligible for canonical ingress.
-
----
-
-### A.5 Ordering and merge contract
-
-`RAEFSC-68` - Adapter/source must provide deterministic source order for
-execution-feedback records.
-
-`RAEFSC-69` - Runner owns merge into global `ProcessingPosition` ordering
-across canonical `MarketEvent`, `OrderSubmittedEvent`, `ControlTimeEvent`, and
-future canonical `FillEvent`.
-
-`RAEFSC-70` - `ProcessingOrder` must not be timestamp-derived.
-
-`RAEFSC-71` - Relative ordering policy for execution feedback versus other
-canonical categories must be explicit before implementation.
-
----
-
-### A.6 No-double-counting cutover policy
-
-`RAEFSC-72` - Compatibility `DerivedFillEvent` progression remains current
-authority until explicit cutover is defined and approved.
-
-`RAEFSC-73` - Future canonical `FillEvent` path must not duplicate semantic
-fill progression for the same canonical order progression.
-
-`RAEFSC-74` - Pre-cutover operation requires either:
-
-- shadow-only comparison phase; or
-- explicit authority cutover/reconciliation policy.
-
-`RAEFSC-75` - Duplicate semantic progression detection should include at least
-`instrument`, `client_order_id`, and `cum_filled_qty`.
-
----
-
-### A.7 Ineligible current source classes (explicit)
-
-`RAEFSC-76` - The following source classes are ineligible in this phase:
-
-- order snapshots (compatibility materialization path);
-- submit/modify/cancel return codes (not execution-feedback records);
-- recorder/offline artifacts unless replayed through an authoritative positioned
- stream contract;
-- market trade feed inference;
-- unwrapped `wait_order_response` without structured authoritative payload,
- deterministic `source_sequence`, and required field authority.
-
----
-
-### A.8 Acceptance criteria before implementation planning
-
-`RAEFSC-77` - Implementation planning for canonical ingress requires all of:
-
-- source record channel exists;
-- required fields are authoritative;
-- liquidity semantics satisfy A.3;
-- deterministic `source_sequence` exists;
-- canonical correlation exists per A.4;
-- merge ordering policy exists per A.5;
-- no-double-counting policy exists per A.6;
-- tests are possible for duplicates/regressions/idempotence/ordering.
-
-`RAEFSC-78` - Until `RAEFSC-77` is satisfied, feasibility remains decision **C**
-and canonical runtime `FillEvent` ingress stays deferred.
-
----
-
-## Appendix B: Adapter API capability contract (Phase 4F)
-
-This appendix defines a docs-only adapter API capability contract for future
-execution-feedback sources.
-
-This appendix is contract-only:
-
-- it does not add or implement production adapter APIs;
-- it does not modify runtime behavior;
-- it does not implement canonical `FillEvent` ingress;
-- it does not canonicalize `OrderStateEvent`;
-- it does not change `DerivedFillEvent` or snapshot compatibility behavior;
-- it does not change reducers or event taxonomy;
-- it does not implement replay/storage/`ProcessingContext`/`EventStreamCursor`.
-
-`RAEFSC-79` - This appendix defines the future adapter-facing capability
-contract for authoritative `ExecutionFeedbackRecord` sourcing only.
-
-`RAEFSC-80` - Current runtime remains ineligible for canonical `FillEvent`
-ingress under this source-authority contract.
-
-`RAEFSC-81` - Snapshot-derived compatibility projection remains the active
-semantic authority in this phase (`DerivedFillEvent` and snapshot path
-unchanged).
-
-`RAEFSC-82` - Canonical runtime `FillEvent` ingress remains deferred.
-
----
-
-### B.1 Ownership and boundary contract
-
-`RAEFSC-83` - The execution-feedback source capability belongs to the
-venue-side adapter boundary.
-
-`RAEFSC-84` - Existing execution command submission boundary remains outbound
-only; it is not redefined by this appendix.
-
-`RAEFSC-85` - Runner remains responsible for orchestration and global
-`ProcessingPosition` merge policy at the canonical boundary.
-
-`RAEFSC-86` - Adapter/source capability must not mutate `StrategyState`
-directly.
-
-`RAEFSC-87` - Adapter/source capability must not emit canonical events directly
-and must not call canonical processing entry points.
-
----
-
-### B.2 Conceptual capability interface (docs only)
-
-`RAEFSC-88` - Future conceptual interface name is
-`ExecutionFeedbackRecordSource`.
-
-`RAEFSC-89` - Conceptual method:
-`drain_execution_feedback_records() -> Sequence[ExecutionFeedbackRecord]`.
-
-`RAEFSC-90` - `drain_execution_feedback_records` is non-blocking.
-
-`RAEFSC-91` - When no records are available, the method returns an empty
-sequence.
-
-`RAEFSC-92` - Already-drained records must not be returned again.
-
-`RAEFSC-93` - Records returned by one drain call must be in deterministic
-source acceptance order.
-
-`RAEFSC-94` - This interface remains conceptual documentation only in Phase 4F
-and introduces no code API additions.
-
----
-
-### B.3 source_sequence requirements
-
-`RAEFSC-95` - `source_sequence` must be strictly monotone within the source
-stream.
-
-`RAEFSC-96` - `source_sequence` must be deterministic for replay-equivalent
-inputs and configuration.
-
-`RAEFSC-97` - `source_sequence` must not be derived from timestamps.
-
-`RAEFSC-98` - Duplicate or regressing `source_sequence` values are hard
-contract failures.
-
-`RAEFSC-99` - `source_sequence` semantics must be suitable for deterministic
-runner merge policy into global `ProcessingPosition`.
-
----
-
-### B.4 Error semantics contract
-
-`RAEFSC-100` - Missing required authoritative fields for
-`ExecutionFeedbackRecord` are hard contract failures.
-
-`RAEFSC-101` - Non-monotone `source_sequence` is a hard contract failure.
-
-`RAEFSC-102` - Invalid liquidity semantics relative to A.3 are hard contract
-failures.
-
-`RAEFSC-103` - Unresolved canonical correlation relative to A.4 is a hard
-contract failure.
-
-`RAEFSC-104` - Malformed authoritative records must not be silently dropped.
-
----
-
-### B.5 Runtime loop integration contract (future implementation boundary)
-
-`RAEFSC-105` - Future runner integration may perform at most one non-blocking
-feedback drain per wakeup after timestamp adoption and before rc-specific
-branch processing.
-
-`RAEFSC-106` - Feedback draining is orthogonal to rc-specific market (`rc == 2`)
-and snapshot/order-response (`rc == 3`) branches.
-
-`RAEFSC-107` - Current market and snapshot branch behavior remains unchanged
-until a later explicit implementation phase.
-
-`RAEFSC-108` - Drained execution-feedback records are source records only and do
-not directly mutate state until mapped to canonical boundary events by the
-runner.
-
-`RAEFSC-109` - Global `ProcessingPosition` assignment for canonical merge
-remains runner responsibility.
-
----
-
-### B.6 FillEvent mapping boundary contract
-
-`RAEFSC-110` - Adapter/source provides `ExecutionFeedbackRecord` only.
-
-`RAEFSC-111` - Runner maps eligible records to canonical `FillEvent` at a later
-implementation phase.
-
-`RAEFSC-112` - Adapter/source must not construct canonical `FillEvent`.
-
-`RAEFSC-113` - Adapter/source must not invoke canonical `process_event_entry`.
-
-`RAEFSC-114` - `liquidity_flag` must come from source records only under A.3.
-
-`RAEFSC-115` - Synthetic population of required canonical mapping fields is
-prohibited.
-
----
-
-### B.7 No-double-counting and cutover contract
-
-`RAEFSC-116` - First implementation path should be shadow-only unless a
-separate explicit cutover decision is approved.
-
-`RAEFSC-117` - During shadow-only operation, `DerivedFillEvent` and snapshot
-compatibility path remain semantic authority.
-
-`RAEFSC-118` - Authority cutover by source scope requires explicit subsequent
-decision and test-backed reconciliation rules.
-
-`RAEFSC-119` - Duplicate semantic progression key must include at least
-`instrument`, `client_order_id`, and `cum_filled_qty`.
-
----
-
-### B.8 Test obligations before implementation
-
-`RAEFSC-120` - Adapter contract tests are required.
-
-`RAEFSC-121` - Deterministic `source_sequence` tests are required.
-
-`RAEFSC-122` - Drain idempotence tests are required.
-
-`RAEFSC-123` - Mapping contract tests are required.
-
-`RAEFSC-124` - No-double-counting shadow tests are required.
-
-`RAEFSC-125` - Runtime global merge ordering tests are required.
-
-`RAEFSC-126` - Snapshot and `DerivedFillEvent` regression guards are required.
-
----
-
-### B.9 Explicitly out of scope for Phase 4F
-
-`RAEFSC-127` - Code interface addition.
-
-`RAEFSC-128` - hftbacktest or other adapter implementation work.
-
-`RAEFSC-129` - Runtime canonical `FillEvent` ingress implementation.
-
-`RAEFSC-130` - Reducer changes.
-
-`RAEFSC-131` - `OrderStateEvent` canonicalization.
-
-`RAEFSC-132` - `DerivedFillEvent` removal or behavior change.
-
-`RAEFSC-133` - Replay/storage/`EventStreamCursor`/`ProcessingContext`
-implementation.
-
----
-
-## Appendix C: hftbacktest source feasibility and gap decision (Phase 4H)
-
-This appendix records the hftbacktest-specific feasibility decision from Phase
-4G and documents the exact source/adapter gap required before any canonical
-`FillEvent` ingress planning.
-
-This appendix is docs-contract only:
-
-- it does not implement canonical `FillEvent` ingress;
-- it does not add or implement adapter APIs;
-- it does not modify runtime behavior;
-- it does not canonicalize `OrderStateEvent`;
-- it does not change `DerivedFillEvent` behavior;
-- it does not change snapshot ingestion behavior;
-- it does not change reducers or event taxonomy;
-- it does not implement replay/storage/`ProcessingContext`/`EventStreamCursor`.
-
-`RAEFSC-134` - Appendix C scope is hftbacktest-specific feasibility and gap
-documentation only; no implementation behavior changes are introduced.
-
----
-
-### C.1 Decision snapshot
-
-`RAEFSC-135` - Current hftbacktest/core-runtime integration feasibility remains
-decision **C** for `ExecutionFeedbackRecordSource` eligibility.
-
-`RAEFSC-136` - No currently exposed hftbacktest/core-runtime source satisfies
-the `ExecutionFeedbackRecordSource` contract end-to-end under Appendices A and
-B.
-
-`RAEFSC-137` - Canonical runtime `FillEvent` ingress remains deferred for the
-hftbacktest integration in this phase.
-
----
-
-### C.2 Current exposed source classes and classification
-
-`RAEFSC-138` - `rc == 3` order snapshot path (`orders()` materialization and
-compatibility ingestion) is classified as **partial/ineligible** for canonical
-source authority:
-
-- some required fields may be present in snapshots;
-- source class remains compatibility snapshot materialization, not explicit
- execution-feedback records;
-- required deterministic non-timestamp `source_sequence` is not exposed;
-- source-authoritative `liquidity_flag` is not satisfied in current runtime
- exposure;
-- therefore it is ineligible for canonical ingress under A.7/B requirements.
-
-`RAEFSC-139` - submit/modify/cancel synchronous return codes are classified as
-**ineligible**:
-
-- they represent outbound command status only;
-- they are not execution-feedback records and cannot satisfy required
- authoritative field payload requirements.
-
-`RAEFSC-140` - `wait_next(... include_order_resp=True)` response signaling is
-classified as **insufficient/ineligible**:
-
-- it provides wakeup signaling that an order response occurred;
-- it does not provide structured authoritative execution-feedback payload.
-
-`RAEFSC-141` - `wait_order_response` hook (as currently unwrapped/unused in
-runtime boundary) is classified as **insufficient/ineligible**:
-
-- current exposure does not provide a structured authoritative source record
- satisfying Appendix A required shape;
-- deterministic source sequencing and full field authority are not provided by
- current integration boundary.
-
-`RAEFSC-142` - `last_trades` market-trade feed is classified as **ineligible**
-for canonical order execution feedback:
-
-- it is market-trade data, not deterministic own-order execution-feedback
- records with canonical order correlation guarantees.
-
-`RAEFSC-143` - Latent hftbacktest order-structure fields (including potential
-maker/taker-style flags) are classified as **insufficient unless surfaced
-through explicit authoritative execution-feedback records**:
-
-- latent/internal field presence alone does not satisfy source-channel
- eligibility;
-- required source contract semantics must be satisfied at the adapter-facing
- record boundary.
-
----
-
-### C.3 Exact missing requirements
-
-`RAEFSC-144` - Current hftbacktest/core-runtime integration lacks an explicit
-adapter-facing execution-feedback record channel matching Appendix A required
-shape and Appendix B drain semantics.
-
-`RAEFSC-145` - Current integration lacks deterministic, strictly monotone,
-non-timestamp-derived `source_sequence` semantics suitable for runner merge.
-
-`RAEFSC-146` - Current integration lacks source-authoritative `liquidity_flag`
-semantics satisfying A.3 without synthetic defaulting.
-
-`RAEFSC-147` - Current integration does not provide contracted authoritative
-per-cumulative execution update granularity for canonical source records.
-
-`RAEFSC-148` - Current integration does not expose deterministic replay-stable
-canonical correlation guarantees to `instrument + client_order_id` through an
-explicit source record channel.
-
-`RAEFSC-149` - Deterministic replace/cancel successor correlation mapping chain
-requirements are not currently satisfied at an authoritative source-record
-boundary.
-
-`RAEFSC-150` - Global `ProcessingPosition` merge policy for future execution
-feedback remains runner-owned and must be explicitly specified before
-implementation.
-
-`RAEFSC-151` - No-double-counting cutover policy relative to
-`DerivedFillEvent` compatibility progression remains required before canonical
-dual-path operation.
-
----
-
-### C.4 Minimum required extension boundary (future, non-implemented)
-
-`RAEFSC-152` - Minimum required extension is a hftbacktest wrapper/adapter
-capability that provides authoritative `ExecutionFeedbackRecordSource`
-semantics at the venue-side adapter boundary.
-
-`RAEFSC-153` - Future source records must satisfy Appendix A required source
-shape, field authority, correlation, granularity, and liquidity clauses.
-
-`RAEFSC-154` - Adapter/source capability must satisfy Appendix B drain
-capability semantics (`drain_execution_feedback_records`) including deterministic
-ordering and non-replay of drained records.
-
-`RAEFSC-155` - Adapter/source capability must own and enforce deterministic
-strictly monotone non-timestamp `source_sequence` semantics.
-
-`RAEFSC-156` - Runner remains owner of global `ProcessingPosition` merge policy
-and canonical positioned ingestion ordering across categories.
-
-`RAEFSC-157` - Until explicit authority cutover, current snapshot compatibility
-path (`OrderStateEvent` materialization and `DerivedFillEvent`) remains
-unchanged semantic authority, and first future implementation path should be
-shadow-only unless separately approved.
-
----
-
-### C.5 Explicit non-goals for Phase 4H
-
-`RAEFSC-158` - Do not promote order snapshots to canonical execution-feedback
-authority in this phase.
-
-`RAEFSC-159` - Do not treat submit/modify/cancel return codes as canonical
-execution feedback.
-
-`RAEFSC-160` - Do not infer canonical fill authority from market-trade feed.
-
-`RAEFSC-161` - Do not synthesize `liquidity_flag` to satisfy required field
-authority.
-
-`RAEFSC-162` - Do not canonicalize `OrderStateEvent` in this phase.
-
-`RAEFSC-163` - Do not remove or alter `DerivedFillEvent` behavior in this
-phase.
-
-`RAEFSC-164` - Do not implement adapter APIs in this phase.
-
----
-
-### C.6 Future implementation gate for hftbacktest scope
-
-`RAEFSC-165` - Canonical `FillEvent` implementation planning for hftbacktest
-scope may begin only after all C.3 missing requirements are satisfied under
-Appendix A/B contracts.
-
-`RAEFSC-166` - First implementation path should remain shadow-only unless a
-separate explicit authority cutover decision is approved.
-
-`RAEFSC-167` - Before implementation/cutover, tests must be possible and
-planned for:
-
-- deterministic `source_sequence` monotonicity and non-timestamp derivation;
-- required-field source authority (including `liquidity_flag`);
-- deterministic canonical correlation (including successor mapping where
- applicable);
-- no-double-counting behavior relative to `DerivedFillEvent`;
-- deterministic global merge ordering under runner-owned `ProcessingPosition`.
-
----
diff --git a/docs/runtime-execution-feedback-contract-v1.md b/docs/runtime-execution-feedback-contract-v1.md
deleted file mode 100644
index 395c4c0..0000000
--- a/docs/runtime-execution-feedback-contract-v1.md
+++ /dev/null
@@ -1,166 +0,0 @@
-# Runtime Execution Feedback Contract v1
-
----
-
-## Purpose and scope
-
-This document defines a boundary contract for when runtime is allowed to emit
-canonical execution feedback into `core`, specifically `FillEvent`.
-
-This is a docs-contract slice only:
-
-- it does not implement `FillEvent` ingress;
-- it does not change runtime behavior;
-- it does not canonicalize `OrderStateEvent`;
-- it does not change compatibility projection behavior (`DerivedFillEvent`);
-- it does not introduce new canonical event types.
-
----
-
-## Normative sources and precedence
-
-`REFC-01` - Main `docs` repository remains the semantic source of truth for
-Event semantics, Event Stream, Processing Order, and execution/order lifecycle:
-
-- `docs/docs/00-guides/terminology.md`
-- `docs/docs/20-concepts/event-model.md`
-- `docs/docs/20-concepts/order-lifecycle.md`
-- `docs/docs/20-concepts/time-model.md`
-- `docs/docs/20-concepts/state-model.md`
-
-`REFC-02` - `core` implementation snapshot semantics are governed by
-[Core Stable Contract v1](core-stable-contract-v1.md).
-
-`REFC-03` - This page is an implementation-facing core/runtime boundary contract
-for current and near-future runtime execution feedback eligibility. It does not
-redefine architecture semantics.
-
----
-
-## Current classification snapshot
-
-`REFC-04` - `FillEvent` is a canonical execution-event candidate in `core`.
-
-`REFC-05` - `DerivedFillEvent` is a compatibility projection artifact and is
-non-canonical.
-
-`REFC-06` - `OrderStateEvent` is compatibility-only and non-canonical at the
-canonical boundary.
-
-`REFC-07` - Snapshot-derived cumulative fill progression in current runtime flow
-is not canonical-grade execution feedback for canonical `FillEvent` emission.
-
----
-
-## Runtime execution feedback contract v1
-
-`REFC-08` - Runtime may emit canonical `FillEvent` only when source records are
-explicit authoritative execution-feedback records from Venue or simulated Venue
-execution path.
-
-`REFC-09` - Runtime must not emit canonical `FillEvent` from inference based
-solely on compatibility order snapshots (`OrderStateEvent` materialization and
-derived cumulative progression deltas).
-
----
-
-## FillEvent field source-authority matrix (v1)
-
-Current runtime-source statements below describe the current snapshot-driven path
-as observed in this slice (`orders` snapshots -> `OrderStateEvent` ->
-`DerivedFillEvent`), not a canonical execution feedback path.
-
-| FillEvent field | Required authority | Current runtime source availability | Sufficient now? | Reason if insufficient |
-| --- | --- | --- | --- | --- |
-| `ts_ns_exch` | Execution-feedback record timestamp for the execution update | Present from order snapshot timestamp | No | Snapshot timestamp is not guaranteed to represent an explicit canonical execution-feedback record boundary |
-| `ts_ns_local` | Runtime receipt timestamp for the execution-feedback record | Present from order snapshot timestamp | No | Same boundary issue as `ts_ns_exch`; snapshot materialization is compatibility path |
-| `instrument` | Execution-feedback record instrument identity | Present in snapshot/materialization context | No (as full contract) | Field exists, but source channel is compatibility snapshot path, not explicit execution feedback channel |
-| `client_order_id` | Stable execution-feedback order identity | Present from snapshot order id | No (as full contract) | Identity exists, but source granularity/channel remains snapshot compatibility path |
-| `side` | Authoritative side in execution feedback | Present in snapshot order view | No (as full contract) | Side exists, but source granularity/channel remains snapshot compatibility path |
-| `filled_price` | Authoritative fill/execution-report price for emitted event granularity | Best-effort snapshot exec price may be present | No | Snapshot-provided price semantics are not contracted here as canonical execution-feedback granularity |
-| `cum_filled_qty` | Authoritative cumulative filled quantity bound to execution-feedback record | Present as snapshot cumulative execution quantity | No | Available only via snapshot progression; not explicit execution feedback record channel |
-| `time_in_force` | Authoritative order execution context | Present in snapshot order view | No (as full contract) | Field exists, but channel is compatibility snapshot materialization |
-| `liquidity_flag` | Authoritative maker/taker/unknown classification in execution feedback contract | Not available in current snapshot-derived path | No | Required field lacks authoritative source in current runtime path |
-| `intended_price` | Authoritative intended order price context when provided | Present in snapshot order view | No (as full contract) | Optional field may be present, but canonical source-channel requirements are unmet |
-| `intended_qty` | Authoritative intended order quantity context when provided | Present in snapshot order view | No (as full contract) | Optional field may be present, but canonical source-channel requirements are unmet |
-| `remaining_qty` | Authoritative remaining quantity context when provided | Present in snapshot order view | No (as full contract) | Optional field may be present, but canonical source-channel requirements are unmet |
-| `fee` | Authoritative execution fee/rebate from execution feedback | Not available in current snapshot-derived path | No | Optional field unavailable in current path; no authoritative execution feedback source |
-
----
-
-## Minimum eligibility criteria for canonical FillEvent emission
-
-`REFC-10` - Source must be explicit execution feedback (Venue or simulated
-Venue execution path), not inferred solely from compatibility snapshots.
-
-`REFC-11` - Runtime must define stable emitted-event granularity (for example,
-per execution report or per cumulative execution update) and preserve it
-deterministically across replay-equivalent runs.
-
-`REFC-12` - All required `FillEvent` fields must come from authoritative source
-records under the runtime execution feedback contract.
-
-`REFC-13` - Required fields must not be heuristic/synthetic unless a future
-explicit contract revision defines and permits such synthesis semantics.
-
-`REFC-14` - Canonical acceptance ordering must be deterministic via
-`ProcessingPosition`, not timestamp-derived ordering.
-
-`REFC-15` - Runtime-emitted canonical `FillEvent` behavior must align with
-existing `apply_fill_event` idempotence semantics (duplicate/regressing
-cumulative progression as no-op).
-
-`REFC-16` - Runtime must define no-double-counting behavior between canonical
-execution feedback path and compatibility projection path before any dual-path
-operation.
-
----
-
-## Compatibility boundary preserved
-
-`REFC-17` - Snapshot-derived cumulative progression remains compatibility
-projection (`DerivedFillEvent`) in current flow.
-
-`REFC-18` - `OrderStateEvent` remains compatibility-only and non-canonical at
-the canonical boundary.
-
-`REFC-19` - This contract does not modify snapshot ingestion behavior.
-
----
-
-## Current deferred status
-
-`REFC-20` - Current runtime does not satisfy this v1 execution-feedback
-contract for canonical `FillEvent` emission.
-
-`REFC-21` - Explicit runtime `FillEvent` ingress remains deferred.
-
-`REFC-22` - Snapshot-derived cumulative progression remains compatibility
-projection behavior in this phase.
-
----
-
-## Prohibited behavior in this phase
-
-`REFC-23` - Do not promote `OrderStateEvent` to canonical execution event in
-this phase.
-
-`REFC-24` - Do not derive canonical `FillEvent` from snapshot deltas alone.
-
-`REFC-25` - Do not synthesize required `liquidity_flag` as `"unknown"` unless a
-future explicit contract revision permits and defines that behavior.
-
-`REFC-26` - Do not dual-write canonical `FillEvent` and `DerivedFillEvent` for
-the same source path without explicit reconciliation/no-double-counting rules.
-
----
-
-## Future implementation gate
-
-`REFC-27` - Runtime `FillEvent` ingress implementation may start only after a
-runtime adapter/source provides authoritative execution-feedback records that
-satisfy `REFC-10` through `REFC-16`.
-
-`REFC-28` - Until then, execution feedback canonicalization remains deferred and
-compatibility projection behavior remains unchanged.
-
diff --git a/docs/runtime-to-coreconfiguration-contract-v1.md b/docs/runtime-to-coreconfiguration-contract-v1.md
deleted file mode 100644
index 6a5dc22..0000000
--- a/docs/runtime-to-coreconfiguration-contract-v1.md
+++ /dev/null
@@ -1,207 +0,0 @@
-# Runtime-to-CoreConfiguration Contract Boundary v1
-
----
-
-## Purpose and scope
-
-This document defines a **boundary contract draft (v1)** for how runtime-owned run
-configuration is mapped into `CoreConfiguration` before calling core canonical
-processing APIs.
-
-This is a boundary-contract slice that originated as planning-oriented guidance
-and now documents the currently implemented ownership boundary:
-
-- it defines ownership boundaries and validation expectations;
-- it documents the minimum mapping target required by current core behavior;
-- runtime mapping is implemented in `core-runtime` under runtime ownership;
-- this page does not redefine runtime implementation internals;
-- it does not introduce new core behavior.
-
----
-
-## Normative sources and precedence
-
-`RCC-01` — Semantic definitions remain in the main `docs` repository and are the
-source of truth, including:
-
-- `docs/docs/00-guides/terminology.md`
-- `docs/docs/20-concepts/event-model.md`
-- `docs/docs/20-concepts/state-model.md`
-- `docs/docs/20-concepts/time-model.md`
-
-`RCC-02` — Current core implementation guarantees are defined by:
-
-- [Core Stable Contract v1](core-stable-contract-v1.md)
-- [CoreConfiguration to Positioned Market Contract](coreconfiguration-positioned-market-contract.md)
-
-`RCC-03` — If broader architecture targets in main docs exceed current core
-implementation, this contract only defines runtime-to-core boundary obligations
-needed to satisfy current core v1 behavior.
-
----
-
-## Boundary ownership model
-
-`RCC-04` — `core` consumes semantic configuration only through
-`CoreConfiguration`.
-
-`RCC-05` — Runtime owns reading external run configuration inputs (for example:
-run JSON, live config, backtest config).
-
-`RCC-06` — Runtime owns mapping run configuration into `CoreConfiguration`
-before invoking core canonical processing/fold APIs.
-
-`RCC-07` — `core` must not read runtime JSON files directly.
-
-`RCC-08` — `core` must not depend on runtime/engine config classes (including
-`HftEngineConfig`, live engine config types, or runtime config classes) at the
-configuration boundary.
-
-`RCC-09` — A run config may contain multiple sections (for example `engine`,
-`strategy`, `risk`, `core`), but `core` receives only `CoreConfiguration`.
-
-`RCC-10` — No duplicate maintenance principle:
-
-- core-semantic values must have one semantic source of truth in run config;
-- if runtime also needs those values, runtime reuses/maps from that same source,
- rather than maintaining divergent duplicates.
-
----
-
-## Minimum v1 mapping target for current core behavior
-
-`RCC-11` — Runtime-produced `CoreConfiguration` must provide:
-
-- `CoreConfiguration.version`
-- `CoreConfiguration.payload.market.instruments..tick_size`
-- `CoreConfiguration.payload.market.instruments..lot_size`
-- `CoreConfiguration.payload.market.instruments..contract_size`
-
-`RCC-12` — This v1 target is intentionally minimal and reflects current core
-contract needs. It does not define a complete future runtime schema.
-
----
-
-## Validation and failure expectations
-
-`RCC-13` — Missing core-semantic configuration section at runtime boundary must
-fail before canonical event processing (**explicit-or-fail**).
-
-`RCC-14` — Missing `market` / `instruments` / `` mapping path must
-fail.
-
-`RCC-15` — Missing required instrument fields (`tick_size`, `lot_size`,
-`contract_size`) must fail.
-
-`RCC-16` — Invalid values must fail, including:
-
-- `None`
-- `bool`
-- non-numeric
-- non-finite
-- non-positive
-
-`RCC-17` — Boundary failures must occur before events are folded into core
-state transitions.
-
-`RCC-18` — Runtime validates before calling core; core boundary validation still
-remains authoritative at call time.
-
----
-
-## Illustrative run-config shape (non-normative)
-
-This shape is an example for boundary explanation only; it is not a required
-schema.
-
-```json
-{
- "engine": {
- "...": "..."
- },
- "strategy": {
- "...": "..."
- },
- "risk": {
- "...": "..."
- },
- "core": {
- "version": "v1",
- "market": {
- "instruments": {
- "": {
- "tick_size": 0.01,
- "lot_size": 0.001,
- "contract_size": 1.0
- }
- }
- }
- }
-}
-```
-
----
-
-## Corresponding CoreConfiguration shape produced by runtime
-
-```json
-{
- "version": "v1",
- "payload": {
- "market": {
- "instruments": {
- "": {
- "tick_size": 0.01,
- "lot_size": 0.001,
- "contract_size": 1.0
- }
- }
- }
- }
-}
-```
-
----
-
-## Boundary responsibility table
-
-| Boundary concern | Owner | Contract expectation |
-| --- | --- | --- |
-| Run JSON / live config / backtest config reading | Runtime | Runtime reads/parses external configuration inputs. |
-| `CoreConfiguration` object construction | Runtime (constructs), core (consumes) | Runtime constructs `CoreConfiguration`; core accepts only `CoreConfiguration` at boundary APIs. |
-| Reducer semantics | Core | Core owns deterministic reducer behavior and canonical boundary semantics. |
-| Validation | Runtime + core | Runtime validates before call; core boundary still validates and rejects invalid/missing required semantics. |
-
----
-
-## Explicitly out of scope for this v1 draft
-
-`RCC-19` — Runtime implementation details.
-
-`RCC-20` — JSON schema implementation.
-
-`RCC-21` — Live/backtest adapter-specific mapping internals.
-
-`RCC-22` — Runtime storage/persistence semantics.
-
-`RCC-23` — Event Stream storage or replay engine implementation.
-
-`RCC-24` — Control-Time Event injection implementation details.
-
-`RCC-25` — New canonical event type introduction.
-
-`RCC-26` — `OrderStateEvent` canonicalization.
-
-`RCC-27` — Any change to `FillEvent`, `CoreConfiguration`, `EventStreamEntry`,
-or core processing API behavior.
-
-`RCC-28` — `ProcessingContext` / `EventStreamCursor` introduction.
-
----
-
-## Future work notes (non-binding)
-
-- Future runtime phases may define concrete mapping mechanics and schemas under
- runtime ownership.
-- Any future expansion of canonical event taxonomy must be handled as a separate
- explicit semantic change, not as part of this boundary draft.
diff --git a/docs/semantic-core-upgrade-milestone-closure-v1.md b/docs/semantic-core-upgrade-milestone-closure-v1.md
deleted file mode 100644
index 7ba3c1c..0000000
--- a/docs/semantic-core-upgrade-milestone-closure-v1.md
+++ /dev/null
@@ -1,109 +0,0 @@
-# Semantic Core Upgrade Milestone Closure v1
-
----
-
-## Purpose and scope
-
-This document records a docs-only milestone closure snapshot for the current
-Semantic Core Upgrade state across `core` and `core-runtime`.
-
-This page:
-
-- does not change production code;
-- does not change runtime behavior;
-- does not change reducers or event taxonomy;
-- does not implement `FillEvent` runtime ingress;
-- does not add adapter APIs;
-- does not canonicalize `OrderStateEvent`;
-- does not change `DerivedFillEvent` or snapshot ingestion behavior;
-- does not implement `ProcessingContext`;
-- does not implement replay/storage/EventStream persistence.
-
----
-
-## Semantic source and contract references
-
-Main semantic source of truth remains the main `docs` repository, including:
-
-- `docs/docs/00-guides/terminology.md`
-- `docs/docs/20-concepts/event-model.md`
-- `docs/docs/20-concepts/order-lifecycle.md`
-- `docs/docs/20-concepts/determinism-model.md`
-- `docs/docs/20-concepts/state-model.md`
-
-Implementation-facing contract references in `core/docs`:
-
-- [Core Stable Contract v1](core-stable-contract-v1.md)
-- [Runtime-to-CoreConfiguration Contract Boundary v1](runtime-to-coreconfiguration-contract-v1.md)
-- [Runtime Execution Feedback Contract v1](runtime-execution-feedback-contract-v1.md)
-- [Runtime/Adapter Execution Feedback Source Contract v1](runtime-adapter-execution-feedback-source-contract-v1.md)
-- [Post-Submission Lifecycle Compatibility Map v1](post-submission-lifecycle-compatibility-map-v1.md)
-- [Venue Adapter Capability Model v1](venue-adapter-capability-model-v1.md)
-- [ProcessingContext / EventStreamCursor Contract v1](processing-context-event-stream-cursor-contract-v1.md)
-- [EventStreamCursor Characterization Note v1](event-stream-cursor-characterization-v1.md)
-- [OrderSubmittedEvent / Dispatch Boundary Contract v1](order-submitted-event-contract-v1.md)
-- [Control-Time Event Contract v1](control-time-event-contract-v1.md)
-
----
-
-## Milestone status snapshot
-
-### Satisfied in current implementation
-
-- `EventStreamEntry` minimal contract (`position`, `event`) and call-level configuration input.
-- `ProcessingPosition` monotonic canonical boundary ordering.
-- `CoreConfiguration` (`version` / `payload` / stable `fingerprint`) with boundary typing.
-- Positioned canonical `MarketEvent` path consuming `CoreConfiguration` instrument metadata with explicit-or-fail validation.
-- Dispatch-time canonical `OrderSubmittedEvent` boundary for successful `new` dispatch.
-- Canonical `ControlTimeEvent` runtime injection on realized scheduled deadline boundary.
-- Runtime-only `EventStreamCursor` ordering helper implemented in `core-runtime` and used by strategy runner canonical paths.
-- Compatibility boundary guards and semantics coverage remain in place (`OrderStateEvent` and `DerivedFillEvent` non-canonical at canonical boundary).
-- Runtime-to-`CoreConfiguration` mapping implemented in `core-runtime` and validated at runtime boundary.
-
-### Transitional in current implementation
-
-- `StrategyState` contains canonical reducer paths and compatibility reducer/projection paths concurrently.
-- Post-submission lifecycle progression after `Submitted` remains snapshot/compatibility-driven (`ingest_order_snapshots` / `OrderStateEvent` / `apply_order_state_event` / `DerivedFillEvent` projection).
-- `ControlTimeEvent` reducer behavior is currently no-op transition slice (no queue/rate/control reducer migration implied).
-- hftbacktest capability support is partial in the model: market/submitted/control-time boundaries are wired; execution-feedback source capability remains unsatisfied.
-
-### Deferred in current implementation
-
-- Runtime canonical `FillEvent` ingress.
-- `ExecutionFeedbackRecordSource` capability satisfaction.
-- Full post-submission lifecycle migration to canonical execution-feedback authority.
-- Replay/storage/EventStream persistence implementation.
-- `ProcessingContext` implementation.
-- Full adapter interface abstraction rollout.
-
----
-
-## Usability statement
-
-Current usability decision:
-
-- Usable for current hftbacktest backtests: **Yes**.
-- Usable as a transitional semantic milestone: **Yes**.
-- Usable as final full canonical Event Stream implementation: **No**.
-
----
-
-## Test status at closure snapshot
-
-Requested suite status used for this closure snapshot:
-
-- `python -m pytest -q core/tests/semantics` -> `193 passed`
-- `python -m pytest -q core-runtime/tests` -> `71 passed`
-
----
-
-## Closure decision
-
-For this milestone scope, the Semantic Core Upgrade milestone is considered
-**closed as a transitional semantic implementation milestone**.
-
-This closure does not claim final canonical Event Stream completeness and does
-not alter deferred gates documented in the execution-feedback, compatibility-map,
-and adapter capability contracts.
-
----
diff --git a/docs/venue-adapter-capability-model-v1.md b/docs/venue-adapter-capability-model-v1.md
deleted file mode 100644
index ffabd5e..0000000
--- a/docs/venue-adapter-capability-model-v1.md
+++ /dev/null
@@ -1,325 +0,0 @@
-# Venue Adapter Capability Model v1
-
----
-
-## Purpose and scope
-
-This document defines a docs-only, venue-agnostic capability model for Runtime /
-Venue Adapter source boundaries used by `core` processing.
-
-This slice is architecture-boundary documentation only:
-
-- it does not implement adapter APIs;
-- it does not implement canonical `FillEvent` ingress;
-- it does not change runtime behavior;
-- it does not canonicalize `OrderStateEvent`;
-- it does not change `DerivedFillEvent` behavior;
-- it does not change snapshot ingestion behavior;
-- it does not change reducers or event taxonomy;
-- it does not implement replay/storage/`ProcessingContext`/`EventStreamCursor`.
-
-`VACM-01` - Main `docs` remains the semantic source of truth for Event,
-Event Stream, Processing Order, Configuration, State, Intent, Order lifecycle,
-determinism, Runtime, and Venue Adapter semantics.
-
-`VACM-02` - This page is implementation-facing boundary documentation for
-capability classification and authority mapping only. It does not redefine
-architecture semantics.
-
-`VACM-03` - `core` remains venue-agnostic. Runtime/adapters expose source
-capabilities; `core` consumes canonical Events and explicit configuration
-through existing contracts.
-
-`VACM-04` - This page must remain consistent with:
-
-- [Core Stable Contract v1](core-stable-contract-v1.md)
-- [Post-Submission Lifecycle Compatibility Map v1](post-submission-lifecycle-compatibility-map-v1.md)
-- [Runtime Execution Feedback Contract v1](runtime-execution-feedback-contract-v1.md)
-- [Runtime/Adapter Execution Feedback Source Contract v1](runtime-adapter-execution-feedback-source-contract-v1.md)
-
-Normative semantic references from main `docs`:
-
-- `docs/docs/00-guides/terminology.md`
-- `docs/docs/20-concepts/event-model.md`
-- `docs/docs/20-concepts/order-lifecycle.md`
-- `docs/docs/20-concepts/snapshot-driven-inputs.md`
-- `docs/docs/20-concepts/determinism-model.md`
-
----
-
-## Authority classification model
-
-`VACM-05` - Adapter/runtime source capabilities are classified by semantic
-authority at the canonical boundary:
-
-- **canonical event capable**
-- **compatibility projection only**
-- **runtime/internal only**
-- **optional future capability**
-
-`VACM-06` - **canonical event capable** means the capability can provide source
-input that may be represented as canonical Event Stream input under positioned
-canonical ingestion and global `ProcessingPosition` ordering authority.
-
-`VACM-07` - **compatibility projection only** means the capability may feed
-compatibility materialization/projection paths but must not be treated as
-canonical Event Stream authority in this phase.
-
-`VACM-08` - **runtime/internal only** means the capability is orchestration or
-transport behavior and must not be promoted to canonical Event Stream authority
-without explicit separate contract changes.
-
-`VACM-09` - **optional future capability** means the capability is recognized as
-architecturally valid but is not currently satisfied for canonical authority and
-remains gated by explicit contracts before canonicalization.
-
-`VACM-10` - Data-field presence alone does not grant canonical authority.
-Canonical authority requires eligible source class, deterministic ordering
-contract, and boundary eligibility under existing contracts.
-
----
-
-## Capability categories and authority implications
-
-This section defines the capability categories in scope and their current
-boundary implications.
-
-### 1) Market input capability
-
-`VACM-11` - Purpose: provide market observations/snapshots/deltas as Runtime
-input that can be represented as canonical `MarketEvent` stream input.
-
-`VACM-12` - Possible classifications:
-
-- canonical event capable (current canonical path);
-- compatibility projection only (if a specific runtime path uses non-canonical
- projection materialization);
-- runtime/internal only (for transport plumbing not entering canonical boundary).
-
-`VACM-13` - Current implication: market capability can produce canonical Event
-Stream input when represented through canonical `MarketEvent` boundary handling.
-
-`VACM-14` - Guardrails/non-goals:
-
-- no timestamp-derived `ProcessingOrder`;
-- no hidden mutable snapshot state outside Event processing;
-- no renaming-only promotion of non-canonical snapshot plumbing to canonical
- authority.
-
-### 2) Order submission result boundary capability
-
-`VACM-15` - Purpose: expose dispatch-success boundary semantics for order-entry
-authority (`Submitted`) via canonical `OrderSubmittedEvent`.
-
-`VACM-16` - Possible classifications:
-
-- canonical event capable for dispatch-time Submitted entry authority;
-- runtime/internal only for outbound command transport details.
-
-`VACM-17` - Current implication: successful `new` dispatch boundary can produce
-canonical `OrderSubmittedEvent`; failed dispatch and non-entry command classes
-remain non-entry behaviors per existing contract.
-
-`VACM-18` - Guardrails/non-goals:
-
-- no post-submission lifecycle authority is introduced by this capability;
-- no reclassification of `OrderSubmittedEvent` as execution feedback;
-- no change to existing compatibility sidecar bookkeeping semantics.
-
-### 3) Order snapshot capability
-
-`VACM-19` - Purpose: provide order-condition snapshots used by compatibility
-materialization/projection paths after submission.
-
-`VACM-20` - Possible classifications:
-
-- compatibility projection only (current authority status);
-- runtime/internal only (transport/materialization mechanisms).
-
-`VACM-21` - Current implication: snapshot order capability remains
-compatibility-only via `ingest_order_snapshots` / `OrderStateEvent` /
-`DerivedFillEvent` projection paths.
-
-`VACM-22` - Canonical Event Stream production from this capability is not
-permitted in this phase for execution-feedback authority.
-
-`VACM-23` - Guardrails/non-goals:
-
-- no `OrderStateEvent` canonicalization;
-- no snapshot-derived canonical execution feedback promotion;
-- no reducer or snapshot lifecycle rewrite in this slice.
-
-### 4) Account snapshot capability
-
-`VACM-24` - Purpose: provide account-condition snapshots (balances/positions and
-related account views) for runtime and/or compatibility projections.
-
-`VACM-25` - Possible classifications:
-
-- compatibility projection only;
-- runtime/internal only;
-- optional future capability for explicit canonical representation under a
- separate contract.
-
-`VACM-26` - Current implication: account snapshot capability is
-compatibility/runtime-internal unless separately and explicitly canonicalized in
-future contract work.
-
-`VACM-27` - Guardrails/non-goals:
-
-- snapshot naming must not imply canonical authority;
-- any future canonicalization must be explicit, versioned, and replay-stable;
-- no implicit event taxonomy expansion in this slice.
-
-### 5) Control-time realization capability
-
-`VACM-28` - Purpose: realize non-canonical control scheduling obligations into
-canonical `ControlTimeEvent` injection boundaries.
-
-`VACM-29` - Possible classifications:
-
-- canonical event capable for realized control-time boundaries;
-- runtime/internal only for scheduling orchestration mechanics.
-
-`VACM-30` - Current implication: capability is canonical event capable for the
-current sparse scheduled-deadline transition behavior.
-
-`VACM-31` - Guardrails/non-goals:
-
-- no periodic control tick introduction;
-- no separate runtime tick authority outside Event processing;
-- no queue/rate reducer migration introduced here.
-
-### 6) Execution feedback capability
-
-`VACM-32` - Purpose: provide authoritative execution-feedback source records that
-may enable future canonical `FillEvent` mapping and ingress.
-
-`VACM-33` - Possible classifications:
-
-- optional future capability (current primary classification);
-- canonical event capable only after explicit gate satisfaction under REFC/RAEFSC;
-- runtime/internal only for ineligible signaling paths.
-
-`VACM-34` - Current implication: canonical `FillEvent` ingress remains deferred.
-Snapshot-derived progression and compatibility artifacts remain non-canonical.
-
-`VACM-35` - Canonical Event Stream production from execution feedback capability
-is gated and not enabled by this document.
-
-`VACM-36` - Guardrails/non-goals:
-
-- no `FillEvent` ingress implementation;
-- no synthetic required-field authority (including `liquidity_flag`);
-- no dual-authority fill progression without explicit no-double-counting policy.
-
----
-
-## Current hftbacktest capability map (Phase 6C snapshot)
-
-`VACM-37` - This table records current capability support classification for the
-hftbacktest adapter/runtime integration without changing behavior.
-
-| capability | current hftbacktest support | classification | current event/artifact path | notes / limitations |
-| --- | --- | --- | --- | --- |
-| market input capability | supported | canonical event capable | canonical `MarketEvent` positioned ingestion path | canonical market path active; ordering remains `ProcessingPosition` authority |
-| order submission result boundary capability | supported (entry boundary) | canonical event capable | successful `new` dispatch -> canonical `OrderSubmittedEvent` | failed `new` dispatch emits no `OrderSubmittedEvent`; replace/cancel do not create new entry event |
-| order snapshot capability | supported | compatibility projection only | `ingest_order_snapshots` -> `OrderStateEvent` -> `apply_order_state_event`; `DerivedFillEvent` projection | post-submission lifecycle remains compatibility authority in current phase |
-| account snapshot capability | partially supported as runtime/compatibility views | compatibility projection only / runtime-internal only | runtime/account snapshot views and compatibility materialization where present | not canonical authority unless later explicit canonical contract work |
-| control-time realization capability | supported (current transition slice) | canonical event capable | realized deadline obligation -> canonical `ControlTimeEvent` injection | sparse/deadline-style realization only; no periodic tick model |
-| execution feedback capability | not supported as authoritative source | optional future capability (currently missing/ineligible) | no eligible `ExecutionFeedbackRecordSource` path in current integration | blocked by missing authoritative source channel, deterministic non-timestamp `source_sequence`, source-authoritative liquidity, and explicit canonical correlation gates |
-
-`VACM-38` - Current hftbacktest execution-feedback feasibility remains blocked by
-the missing authoritative `ExecutionFeedbackRecordSource` capability.
-
-`VACM-39` - Snapshot compatibility path remains active semantic authority for
-post-submission progression in this phase.
-
----
-
-## Future live venue capability expectations (non-implemented)
-
-`VACM-40` - A future live venue adapter may expose native execution-report
-records that can satisfy `ExecutionFeedbackRecordSource` source-authority
-requirements.
-
-`VACM-41` - A future live venue adapter may expose source-authoritative
-liquidity classification (`maker` / `taker` / explicit `unknown`) suitable for
-required-field authority.
-
-`VACM-42` - A future live venue adapter may expose deterministic replay-stable
-correlation to canonical order identity (`instrument + client_order_id`),
-including explicit successor-mapping chain behavior where applicable.
-
-`VACM-43` - A future live venue adapter may expose deterministic non-timestamp
-`source_sequence` semantics suitable for runner merge policy into global
-`ProcessingPosition`.
-
-`VACM-44` - Canonical runtime `FillEvent` ingress remains gated by REFC/RAEFSC
-contracts and is not enabled by capability expectation statements alone.
-
----
-
-## Canonical vs compatibility implications
-
-`VACM-45` - Data availability does not equal canonical authority.
-
-`VACM-46` - Snapshot field availability must not be promoted to canonical
-execution-feedback authority without explicit eligible source contract
-satisfaction.
-
-`VACM-47` - Runtime/internal wakeups, signaling hooks, and synchronous return
-codes are not canonical Event Stream authority.
-
-`VACM-48` - Compatibility projection paths remain compatibility authority until
-explicit gates are satisfied and separately approved for canonical cutover.
-
-`VACM-49` - Optional future capabilities require explicit gate satisfaction,
-ordering policy, and no-double-counting policy before any canonicalization
-planning.
-
----
-
-## Guardrails
-
-`VACM-50` - `core` consumes canonical Events and explicit configuration at the
-boundary; `core` does not consume venue-specific internal structures as semantic
-authority.
-
-`VACM-51` - Adapter/runtime naming must not promote snapshots or internal
-signals to canonical authority by terminology alone.
-
-`VACM-52` - Execution feedback capability must satisfy REFC/RAEFSC eligibility,
-field authority, identity/correlation, deterministic ordering, and
-no-double-counting requirements before canonical `FillEvent` ingress planning.
-
-`VACM-53` - `ProcessingPosition` remains global canonical acceptance-order
-authority across canonical categories.
-
-`VACM-54` - `ProcessingOrder` must not be timestamp-derived.
-
-`VACM-55` - This model does not alter current canonical/non-canonical taxonomy
-or compatibility boundaries in existing contracts.
-
----
-
-## Explicit non-goals for Phase 6C
-
-`VACM-56` - No adapter API methods/signatures are defined or implemented.
-
-`VACM-57` - No hftbacktest-specific `core` semantics are introduced.
-
-`VACM-58` - No runtime canonical `FillEvent` ingress implementation.
-
-`VACM-59` - No `OrderStateEvent` canonicalization.
-
-`VACM-60` - No `DerivedFillEvent` removal or behavior change.
-
-`VACM-61` - No snapshot lifecycle rewrite.
-
-`VACM-62` - No reducer or runtime behavior change.
-
-`VACM-63` - No replay/storage/`ProcessingContext`/`EventStreamCursor`
-implementation.
-
----
diff --git a/examples/core_step_quickstart.py b/examples/core_step_quickstart.py
new file mode 100644
index 0000000..c7187de
--- /dev/null
+++ b/examples/core_step_quickstart.py
@@ -0,0 +1,139 @@
+"""Core-only CoreStep quickstart example.
+
+For ordered multi-entry wakeup batches see run_core_wakeup_step in the README.
+"""
+
+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
+
+INSTRUMENT = "BTC-USDC-PERP"
+INTENT_ID_V1 = "quickstart-new-v1"
+INTENT_ID_V2 = "quickstart-new-v2"
+
+
+class OneIntentEvaluator:
+ """Small evaluator that emits one deterministic new-order Intent."""
+
+ def __init__(self, client_order_id: str) -> None:
+ self._client_order_id = client_order_id
+
+ def evaluate(self, context: object) -> list[tc.NewOrderIntent]:
+ _ = context
+ return [
+ tc.NewOrderIntent(
+ ts_ns_local=1_000,
+ instrument=INSTRUMENT,
+ client_order_id=self._client_order_id,
+ intents_correlation_id=f"corr-{self._client_order_id}",
+ 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:
+ """Policy evaluator that admits every generated candidate Intent."""
+
+ 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 _control_time_entry(*, index: int, ts_ns_local: int) -> tc.EventStreamEntry:
+ # EventStreamEntry is the ordered Core input unit: a canonical Event plus
+ # ProcessingPosition telling Core where this Event sits in the Event Stream.
+ # ControlTimeEvent here is only a driver Event; scheduling obligations come
+ # from Execution Control apply (e.g. rate-limit deferral), not from every step.
+ 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 run_v1_generated_only(state: tc.StrategyState) -> tc.CoreStepResult:
+ # v1 shows the minimum deterministic step: Core reduces one canonical Event
+ # and Strategy evaluation emits generated Intents. No policy/apply contexts
+ # are provided yet, so Core returns zero dispatchable Intents by design.
+ result = tc.run_core_step(
+ state,
+ _control_time_entry(index=0, ts_ns_local=1_000),
+ strategy_evaluator=OneIntentEvaluator(INTENT_ID_V1),
+ )
+ assert len(result.generated_intents) == 1
+ assert result.generated_intents[0].client_order_id == INTENT_ID_V1
+ assert len(result.candidate_intent_records) == 1
+ assert result.candidate_intent_records[0].origin is tc.CandidateIntentOrigin.GENERATED
+ assert result.dispatchable_intents == ()
+ return result
+
+
+def run_v2_with_policy_and_apply(state: tc.StrategyState) -> tc.CoreStepResult:
+ # v2 adds policy admission and Execution Control apply. With dispatchable
+ # outputs activated, Core exposes Intents that Runtime can dispatch.
+ result = tc.run_core_step(
+ state,
+ _control_time_entry(index=1, ts_ns_local=1_001),
+ strategy_evaluator=OneIntentEvaluator(INTENT_ID_V2),
+ policy_admission_context=tc.CorePolicyAdmissionContext(
+ policy_evaluator=AllowAllPolicy(),
+ now_ts_ns_local=1_001,
+ ),
+ execution_control_apply_context=tc.CoreExecutionControlApplyContext(
+ execution_control=tc.ExecutionControl(),
+ now_ts_ns_local=1_001,
+ activate_dispatchable_outputs=True,
+ ),
+ )
+ assert len(result.dispatchable_intents) == 1
+ assert result.dispatchable_intents[0].client_order_id == INTENT_ID_V2
+ return result
+
+
+def main() -> None:
+ # StrategyState holds deterministic Core memory across steps
+ # (market snapshots, queued Intents, monotone timestamps, etc.).
+ 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.
+ result_v1 = run_v1_generated_only(state)
+ result_v2 = run_v2_with_policy_and_apply(state)
+
+ print("CoreStep quickstart (Core-only deterministic engine)")
+ print("v1 generated:", [Intent.client_order_id for Intent in result_v1.generated_intents])
+ print(
+ "v1 candidate origins:",
+ [record.origin.value for record in result_v1.candidate_intent_records],
+ )
+ print("v1 dispatchable: [] (Core does not dispatch externally)")
+ print("v2 dispatchable:", [Intent.client_order_id for Intent in result_v2.dispatchable_intents])
+ print("v2 obligation:", result_v2.control_scheduling_obligation)
+ print("Runtime dispatches these; Core only returns decisions/Intents.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pyproject.toml b/pyproject.toml
index 4589da3..c7b6052 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,18 +3,23 @@ requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"
[project]
-name = "tradingchassis-core"
+name = "TradingChassis-core"
version = "0.1.0"
-description = "Deterministic, event-driven core, with explicit risk management, order state machines, queue semantics, and research orchestration."
+description = "Deterministic trading decision kernel."
readme = "README.md"
requires-python = ">=3.11"
-authors = [{ name = "tradingeng@protonmail.com" }]
-license = { text = "MIT" }
+authors = [{ name = "TradingChassis Core Contributors" }]
+maintainers = [{ name = "TradingChassis Core Maintainers" }]
+license = "MIT"
+keywords = ["trading", "deterministic", "pydantic", "event-driven", "strategy"]
classifiers = [
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "Topic :: Office/Business :: Financial :: Investment",
+ "Topic :: Software Development :: Libraries",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
- "License :: OSI Approved :: MIT License",
"Operating System :: OS Independent"
]
@@ -28,30 +33,25 @@ dev = [
"import-linter>=1.11,<2",
"ruff>=0.4,<1",
"mypy>=1.9,<2",
- "hftbacktest>=2,<3",
- "jsonschema>=4,<5",
- "matplotlib>=3,<4",
- "numpy>=2.0,<2.3",
- "referencing>=0.37,<1",
+ "build>=1,<2",
]
-# --------------------------------------------------
-# Explicit package discovery
-# --------------------------------------------------
+[project.urls]
+Homepage = "https://github.com/TradingChassis"
+Repository = "https://github.com/TradingChassis/core"
+Documentation = "https://github.com/TradingChassis/core/tree/main/core/docs"
+Changelog = "https://github.com/TradingChassis/core/blob/main/core/CHANGELOG.md"
+Issues = "https://github.com/TradingChassis/core/issues"
+
[tool.setuptools.packages.find]
include = ["tradingchassis_core*"]
-# --------------------------------------------------
-# Pytest
-# --------------------------------------------------
[tool.pytest.ini_options]
minversion = "9.0"
addopts = "-ra"
testpaths = ["tests"]
+pythonpath = ["."]
-# --------------------------------------------------
-# Ruff
-# --------------------------------------------------
[tool.ruff]
target-version = "py311"
line-length = 100
@@ -60,30 +60,19 @@ line-length = 100
select = ["E", "F", "I"]
ignore = ["E501"]
-# --------------------------------------------------
-# MyPy (static typing)
-# --------------------------------------------------
[tool.mypy]
python_version = "3.11"
warn_unused_configs = true
ignore_missing_imports = true
pretty = true
show_error_codes = true
-ignore_errors = true
-# --------------------------------------------------
-# Import Linter
-# --------------------------------------------------
[tool.importlinter]
root_package = "tradingchassis_core"
include_external_packages = true
-# Core stays pure
[[tool.importlinter.contracts]]
-name = "Core must be pure"
+name = "Core stays runtime-independent"
type = "forbidden"
-source_modules = ["tradingchassis_core.core"]
-forbidden_modules = [
- "tradingchassis_core.strategies"
-]
-
+source_modules = ["tradingchassis_core"]
+forbidden_modules = []
diff --git a/tests/semantics/gate_risk_invariants/test_cancel_non_existing_rejected.py b/tests/semantics/gate_risk_invariants/test_cancel_non_existing_rejected.py
deleted file mode 100644
index b346d93..0000000
--- a/tests/semantics/gate_risk_invariants/test_cancel_non_existing_rejected.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""
-Semantic test: CANCEL intent for a non-existing order must be rejected.
-
-Invariant:
-A CANCEL intent requires an existing order with the same
-(instrument, client_order_id). Otherwise it must be rejected.
-"""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.reject_reasons import RejectReason
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- CancelOrderIntent,
- NotionalLimits,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-from tradingchassis_core.core.risk.risk_config import RiskConfig
-from tradingchassis_core.core.risk.risk_engine import RiskEngine
-
-
-def test_cancel_for_non_existing_order_is_rejected() -> None:
- """Reject CANCEL intent when no order with the given id exists."""
-
- instrument = "BTC-USDC-PERP"
- client_order_id = "missing-order"
-
- state = StrategyState(event_bus=NullEventBus())
-
- risk_cfg = RiskConfig(
- scope="test",
- trading_enabled=True,
- notional_limits=NotionalLimits(
- currency="USDC",
- max_gross_notional=1e18,
- max_single_order_notional=1e18,
- ),
- extra={},
- )
- risk_engine = RiskEngine(risk_cfg=risk_cfg, event_bus=NullEventBus())
-
- cancel_intent = CancelOrderIntent(
- ts_ns_local=1,
- instrument=instrument,
- client_order_id=client_order_id,
- intents_correlation_id=None,
- )
-
- decision = risk_engine.decide_intents(
- raw_intents=[cancel_intent],
- state=state,
- now_ts_ns_local=1,
- )
-
- # ---------- assert decision ----------
- assert decision.accepted_now == []
- assert decision.queued == []
- assert decision.handled_in_queue == []
- assert len(decision.rejected) == 1
- assert decision.rejected[0].reason == RejectReason.ORDER_NOT_FOUND
-
- # ---------- assert no side effects ----------
- assert not state.has_working_order(instrument, client_order_id)
- assert not state.has_queued_intent(instrument, client_order_id)
diff --git a/tests/semantics/gate_risk_invariants/test_duplicate_new_rejected.py b/tests/semantics/gate_risk_invariants/test_duplicate_new_rejected.py
deleted file mode 100644
index 2c531fe..0000000
--- a/tests/semantics/gate_risk_invariants/test_duplicate_new_rejected.py
+++ /dev/null
@@ -1,93 +0,0 @@
-"""
-Semantic test: Duplicate NEW intent must be rejected.
-
-Invariant:
-(client_order_id, instrument) must be unique while an order id is busy
-(working ∪ queued). A NEW with the same id must be rejected.
-"""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.reject_reasons import RejectReason
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- NewOrderIntent,
- NotionalLimits,
- OrderStateEvent,
- Price,
- Quantity,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-from tradingchassis_core.core.risk.risk_config import RiskConfig
-from tradingchassis_core.core.risk.risk_engine import RiskEngine
-
-
-def test_duplicate_new_is_rejected_when_working_order_exists() -> None:
- """Reject NEW intent when a working order with same (instrument, client_order_id) exists."""
-
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-1"
-
- state = StrategyState(event_bus=NullEventBus())
-
- # Create an existing WORKING order in state via canonical snapshot ingestion.
- existing_order = OrderStateEvent(
- ts_ns_exch=1,
- ts_ns_local=1,
- instrument=instrument,
- client_order_id=client_order_id,
- order_type="limit",
- state_type="working",
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=None,
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=None,
- remaining_qty=None,
- time_in_force="GTC",
- reason=None,
- raw={"req": 0, "source": "snapshot"},
- )
- state.apply_order_state_event(existing_order)
-
- # Minimal risk config (notional_limits is required by validation).
- risk_cfg = RiskConfig(
- scope="test",
- trading_enabled=True,
- notional_limits=NotionalLimits(
- currency="USDC",
- max_gross_notional=1e18,
- max_single_order_notional=1e18,
- ),
- extra={},
- )
- risk_engine = RiskEngine(risk_cfg=risk_cfg, event_bus=NullEventBus())
-
- duplicate_new = NewOrderIntent(
- ts_ns_local=2,
- instrument=instrument,
- client_order_id=client_order_id,
- intents_correlation_id=None,
- side="buy",
- order_type="limit",
- intended_qty=Quantity(unit="contracts", value=1.0),
- intended_price=Price(currency="USDC", value=101.0),
- time_in_force="GTC",
- )
-
- decision = risk_engine.decide_intents(
- raw_intents=[duplicate_new],
- state=state,
- now_ts_ns_local=2,
- )
-
- # Must be rejected with DUPLICATE_ID.
- assert decision.accepted_now == []
- assert decision.queued == []
- assert decision.handled_in_queue == []
- assert len(decision.rejected) == 1
- assert decision.rejected[0].reason == RejectReason.DUPLICATE_ID
-
- # State must remain: working still exists; no queued intent for same id.
- assert state.has_working_order(instrument, client_order_id)
- assert not state.has_queued_intent(instrument, client_order_id)
diff --git a/tests/semantics/gate_risk_invariants/test_inflight_blocks_replace.py b/tests/semantics/gate_risk_invariants/test_inflight_blocks_replace.py
deleted file mode 100644
index 2d5c62c..0000000
--- a/tests/semantics/gate_risk_invariants/test_inflight_blocks_replace.py
+++ /dev/null
@@ -1,98 +0,0 @@
-"""
-Semantic test: inflight intent blocks further REPLACE intents.
-
-Invariant:
-While an order id is inflight, no additional REPLACE
-may be sent. Such intents must be queued instead.
-
-NEW with the same client_order_id is always rejected.
-CANCEL is always allowed.
-"""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- NotionalLimits,
- OrderStateEvent,
- Price,
- Quantity,
- ReplaceOrderIntent,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-from tradingchassis_core.core.risk.risk_config import RiskConfig
-from tradingchassis_core.core.risk.risk_engine import RiskEngine
-
-
-def test_inflight_blocks_new_intent_and_queues_it() -> None:
- """NEW intent must be queued when an inflight action exists for the same id."""
-
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-1"
-
- state = StrategyState(event_bus=NullEventBus())
-
- # Existing working order
- existing_order = OrderStateEvent(
- ts_ns_exch=1,
- ts_ns_local=1,
- instrument=instrument,
- client_order_id=client_order_id,
- order_type="limit",
- state_type="working",
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=None,
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=None,
- remaining_qty=None,
- time_in_force="GTC",
- reason=None,
- raw={"req": 1, "source": "snapshot"},
- )
- state.apply_order_state_event(existing_order)
-
- # Simulate inflight action (previous replace already sent)
- state.mark_intent_sent(
- instrument=instrument,
- client_order_id=client_order_id,
- intent_type="replace",
- )
-
- risk_cfg = RiskConfig(
- scope="test",
- trading_enabled=True,
- notional_limits=NotionalLimits(
- currency="USDC",
- max_gross_notional=1e18,
- max_single_order_notional=1e18,
- ),
- extra={},
- )
- risk_engine = RiskEngine(risk_cfg=risk_cfg, event_bus=NullEventBus())
-
- replace_intent = ReplaceOrderIntent(
- ts_ns_local=2,
- instrument=instrument,
- client_order_id=client_order_id,
- intents_correlation_id=None,
- side="buy",
- intended_price=Price(currency="USDC", value=101.0),
- intended_qty=Quantity(unit="contracts", value=1.0),
- )
-
- decision = risk_engine.decide_intents(
- raw_intents=[replace_intent],
- state=state,
- now_ts_ns_local=2,
- )
-
- # ---------- assert decision ----------
- assert decision.accepted_now == []
- assert decision.rejected == []
- assert decision.handled_in_queue == []
- assert len(decision.queued) == 1
-
- # ---------- assert state ----------
- assert state.has_working_order(instrument, client_order_id)
- assert state.has_queued_intent(instrument, client_order_id)
diff --git a/tests/semantics/gate_risk_invariants/test_rejected_intents_do_not_enter_queue_characterization.py b/tests/semantics/gate_risk_invariants/test_rejected_intents_do_not_enter_queue_characterization.py
deleted file mode 100644
index 8460b73..0000000
--- a/tests/semantics/gate_risk_invariants/test_rejected_intents_do_not_enter_queue_characterization.py
+++ /dev/null
@@ -1,63 +0,0 @@
-"""
-Characterization test: rejected/denied intents must not enter the queue.
-
-This pins current behavior that hard rejects do not mutate StrategyState.queued_intents.
-"""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.reject_reasons import RejectReason
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- NewOrderIntent,
- NotionalLimits,
- Price,
- Quantity,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-from tradingchassis_core.core.risk.risk_config import RiskConfig
-from tradingchassis_core.core.risk.risk_engine import RiskEngine
-
-
-def test_trading_disabled_rejects_new_without_queue_side_effects_characterization() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-1"
-
- state = StrategyState(event_bus=NullEventBus())
-
- risk_cfg = RiskConfig(
- scope="test",
- trading_enabled=False,
- notional_limits=NotionalLimits(
- currency="USDC",
- max_gross_notional=1e18,
- max_single_order_notional=1e18,
- ),
- )
- risk_engine = RiskEngine(risk_cfg=risk_cfg, event_bus=NullEventBus())
-
- new_intent = NewOrderIntent(
- ts_ns_local=1,
- instrument=instrument,
- client_order_id=client_order_id,
- intents_correlation_id=None,
- side="buy",
- order_type="limit",
- intended_qty=Quantity(unit="contracts", value=1.0),
- intended_price=Price(currency="USDC", value=100.0),
- time_in_force="GTC",
- )
-
- decision = risk_engine.decide_intents(
- raw_intents=[new_intent],
- state=state,
- now_ts_ns_local=1,
- )
-
- assert decision.accepted_now == []
- assert decision.queued == []
- assert decision.handled_in_queue == []
- assert len(decision.rejected) == 1
- assert decision.rejected[0].reason == RejectReason.TRADING_DISABLED
-
- assert not state.has_queued_intent(instrument, client_order_id)
diff --git a/tests/semantics/gate_risk_invariants/test_replace_noop_handled.py b/tests/semantics/gate_risk_invariants/test_replace_noop_handled.py
deleted file mode 100644
index fdd95c7..0000000
--- a/tests/semantics/gate_risk_invariants/test_replace_noop_handled.py
+++ /dev/null
@@ -1,88 +0,0 @@
-"""
-Semantic test: REPLACE intent with no effective change must be handled as no-op.
-
-Invariant:
-A REPLACE that does not change price or quantity after normalization
-must not be sent, queued, or rejected. It must be handled as no-op.
-"""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- NotionalLimits,
- OrderStateEvent,
- Price,
- Quantity,
- ReplaceOrderIntent,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-from tradingchassis_core.core.risk.risk_config import RiskConfig
-from tradingchassis_core.core.risk.risk_engine import RiskEngine
-
-
-def test_replace_without_effective_change_is_handled_noop() -> None:
- """REPLACE with identical price/qty must be handled and dropped."""
-
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-1"
-
- state = StrategyState(event_bus=NullEventBus())
-
- # Existing working order
- existing_order = OrderStateEvent(
- ts_ns_exch=1,
- ts_ns_local=1,
- instrument=instrument,
- client_order_id=client_order_id,
- order_type="limit",
- state_type="working",
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=None,
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=None,
- remaining_qty=None,
- time_in_force="GTC",
- reason=None,
- raw={"req": 0, "source": "snapshot"},
- )
- state.apply_order_state_event(existing_order)
-
- risk_cfg = RiskConfig(
- scope="test",
- trading_enabled=True,
- notional_limits=NotionalLimits(
- currency="USDC",
- max_gross_notional=1e18,
- max_single_order_notional=1e18,
- ),
- extra={},
- )
- risk_engine = RiskEngine(risk_cfg=risk_cfg, event_bus=NullEventBus())
-
- replace_noop = ReplaceOrderIntent(
- ts_ns_local=2,
- instrument=instrument,
- client_order_id=client_order_id,
- intents_correlation_id=None,
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- intended_qty=Quantity(unit="contracts", value=1.0),
- )
-
- decision = risk_engine.decide_intents(
- raw_intents=[replace_noop],
- state=state,
- now_ts_ns_local=2,
- )
-
- # ---------- assert decision ----------
- assert decision.accepted_now == []
- assert decision.queued == []
- assert decision.rejected == []
- assert len(decision.handled_in_queue) == 1
-
- # ---------- assert no side effects ----------
- assert state.has_working_order(instrument, client_order_id)
- assert not state.has_queued_intent(instrument, client_order_id)
diff --git a/tests/semantics/models/test_canonical_processing_boundary.py b/tests/semantics/models/test_canonical_processing_boundary.py
deleted file mode 100644
index b7bda90..0000000
--- a/tests/semantics/models/test_canonical_processing_boundary.py
+++ /dev/null
@@ -1,848 +0,0 @@
-"""Semantics tests for the minimal canonical processing boundary."""
-
-from __future__ import annotations
-
-import copy
-
-import pytest
-
-from tradingchassis_core.core.domain.configuration import CoreConfiguration
-from tradingchassis_core.core.domain.event_model import is_canonical_stream_candidate_type
-from tradingchassis_core.core.domain.processing import process_canonical_event, 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.types import (
- ControlTimeEvent,
- FillEvent,
- MarketEvent,
- OrderStateEvent,
- OrderSubmittedEvent,
- Price,
- Quantity,
-)
-from tradingchassis_core.core.events.event_bus import EventBus
-from tradingchassis_core.core.events.events import DerivedFillEvent, RiskDecisionEvent
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-
-
-def _state_subset_snapshot(state: StrategyState) -> dict[str, object]:
- return {
- "market": copy.deepcopy(state.market),
- "fills": copy.deepcopy(state.fills),
- "fill_cum_qty": copy.deepcopy(state.fill_cum_qty),
- }
-
-
-def _book_market_event(
- *,
- instrument: str,
- ts_ns_local: int,
- ts_ns_exch: int,
- best_bid: float = 100.0,
- best_ask: float = 101.0,
- best_bid_qty: float = 2.0,
- best_ask_qty: float = 3.0,
-) -> MarketEvent:
- return MarketEvent(
- ts_ns_local=ts_ns_local,
- ts_ns_exch=ts_ns_exch,
- instrument=instrument,
- event_type="book",
- book={
- "book_type": "snapshot",
- "bids": [
- {
- "price": {"currency": "USDC", "value": best_bid},
- "quantity": {"unit": "contracts", "value": best_bid_qty},
- }
- ],
- "asks": [
- {
- "price": {"currency": "USDC", "value": best_ask},
- "quantity": {"unit": "contracts", "value": best_ask_qty},
- }
- ],
- "depth": 1,
- },
- trade=None,
- )
-
-
-def _fill_event(
- *,
- instrument: str,
- client_order_id: str,
- ts_ns_local: int,
- ts_ns_exch: int,
- cum_filled_qty: float = 0.25,
-) -> FillEvent:
- return FillEvent(
- ts_ns_local=ts_ns_local,
- ts_ns_exch=ts_ns_exch,
- instrument=instrument,
- client_order_id=client_order_id,
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=Price(currency="USDC", value=100.5),
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=Quantity(unit="contracts", value=cum_filled_qty),
- remaining_qty=Quantity(unit="contracts", value=max(0.0, 1.0 - cum_filled_qty)),
- time_in_force="GTC",
- liquidity_flag="maker",
- fee=None,
- )
-
-
-def _order_state_event(
- *,
- instrument: str,
- client_order_id: str,
- ts_ns_local: int,
- ts_ns_exch: int,
- state_type: str = "accepted",
-) -> OrderStateEvent:
- return OrderStateEvent(
- ts_ns_local=ts_ns_local,
- ts_ns_exch=ts_ns_exch,
- instrument=instrument,
- client_order_id=client_order_id,
- order_type="limit",
- state_type=state_type,
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=None,
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=None,
- remaining_qty=None,
- time_in_force="GTC",
- reason=None,
- raw={"req": 0, "source": "snapshot"},
- )
-
-
-def _order_submitted_event(
- *,
- instrument: str,
- client_order_id: str,
- ts_ns_local_dispatch: int,
-) -> OrderSubmittedEvent:
- return OrderSubmittedEvent(
- ts_ns_local_dispatch=ts_ns_local_dispatch,
- instrument=instrument,
- client_order_id=client_order_id,
- side="buy",
- order_type="limit",
- intended_price=Price(currency="USDC", value=100.0),
- intended_qty=Quantity(unit="contracts", value=1.0),
- time_in_force="GTC",
- intent_correlation_id="corr-1",
- dispatch_attempt_id="attempt-1",
- runtime_correlation={"engine": "backtest", "seq": 1},
- )
-
-
-def _control_time_event(
- *,
- ts_ns_local_control: int,
- reason: str = "rate_limit_recheck",
- due_ts_ns_local: int | None = None,
- realized_ts_ns_local: int | None = None,
-) -> ControlTimeEvent:
- return ControlTimeEvent(
- ts_ns_local_control=ts_ns_local_control,
- reason=reason,
- due_ts_ns_local=due_ts_ns_local,
- realized_ts_ns_local=realized_ts_ns_local,
- obligation_reason="rate_limit",
- obligation_due_ts_ns_local=due_ts_ns_local,
- runtime_correlation={"engine": "backtest", "seq": 1},
- )
-
-
-def _market_configuration(
- *,
- instrument: str = "BTC-USDC-PERP",
- tick_size: float = 0.1,
- lot_size: float = 0.01,
- contract_size: float = 1.0,
-) -> CoreConfiguration:
- return CoreConfiguration(
- version="v1",
- payload={
- "market": {
- "instruments": {
- instrument: {
- "tick_size": tick_size,
- "lot_size": lot_size,
- "contract_size": contract_size,
- }
- }
- }
- },
- )
-
-
-def _control_state_snapshot(state: StrategyState) -> dict[str, object]:
- return {
- "queued_intents": copy.deepcopy(state.queued_intents),
- "inflight": copy.deepcopy(state.inflight),
- "orders": copy.deepcopy(state.orders),
- "canonical_orders": copy.deepcopy(state.canonical_orders),
- "fills": copy.deepcopy(state.fills),
- "market": copy.deepcopy(state.market),
- "account": copy.deepcopy(state.account),
- }
-
-
-def test_process_canonical_event_accepts_market_event() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=100, ts_ns_exch=90)
-
- process_canonical_event(state, event)
-
- market = state.market["BTC-USDC-PERP"]
- assert market.last_ts_ns_local == 100
- assert market.last_ts_ns_exch == 90
- assert market.best_bid == 100.0
- assert market.best_ask == 101.0
- assert market.best_bid_qty == 2.0
- assert market.best_ask_qty == 3.0
- assert market.mid == 100.5
-
-
-def test_process_canonical_event_accepts_market_event_with_processing_position() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=100, ts_ns_exch=90)
- position = ProcessingPosition(index=5)
-
- process_canonical_event(state, event, position=position, configuration=_market_configuration())
-
- market = state.market["BTC-USDC-PERP"]
- assert market.last_ts_ns_local == 100
- assert market.last_ts_ns_exch == 90
- assert market.best_bid == 100.0
- assert market.best_ask == 101.0
- assert market.best_bid_qty == 2.0
- assert market.best_ask_qty == 3.0
- assert market.mid == 100.5
- assert state._last_processing_position_index == 5
-
-
-def test_process_canonical_event_accepts_fill_event() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=200,
- ts_ns_exch=180,
- )
-
- process_canonical_event(state, event)
-
- fills = state.fills["BTC-USDC-PERP"]
- assert len(fills) == 1
- assert fills[0] == event
- assert state.fill_cum_qty["BTC-USDC-PERP"]["order-1"] == 0.25
-
-
-def test_process_canonical_event_accepts_fill_event_with_processing_position() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=200,
- ts_ns_exch=180,
- )
- position = ProcessingPosition(index=12)
-
- process_canonical_event(state, event, position=position)
-
- fills = state.fills["BTC-USDC-PERP"]
- assert len(fills) == 1
- assert fills[0] == event
- assert state.fill_cum_qty["BTC-USDC-PERP"]["order-1"] == 0.25
- assert state._last_processing_position_index == 12
-
-
-def test_process_canonical_event_accepts_order_submitted_event() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _order_submitted_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-submitted-1",
- ts_ns_local_dispatch=300,
- )
-
- process_canonical_event(state, event)
-
- projection = state.canonical_orders[("BTC-USDC-PERP", "order-submitted-1")]
- assert projection.state == "submitted"
- assert projection.submitted_ts_ns_local == 300
- assert projection.updated_ts_ns_local == 300
-
-
-def test_process_canonical_event_accepts_order_submitted_event_with_processing_position() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _order_submitted_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-submitted-1",
- ts_ns_local_dispatch=300,
- )
-
- process_canonical_event(state, event, position=ProcessingPosition(index=13))
-
- projection = state.canonical_orders[("BTC-USDC-PERP", "order-submitted-1")]
- assert projection.state == "submitted"
- assert projection.submitted_ts_ns_local == 300
- assert projection.updated_ts_ns_local == 300
- assert state._last_processing_position_index == 13
-
-
-def test_control_time_event_requires_due_or_realized_timestamp() -> None:
- with pytest.raises(
- ValueError,
- match="at least one of due_ts_ns_local or realized_ts_ns_local is required",
- ):
- _control_time_event(ts_ns_local_control=500)
-
-
-def test_control_time_event_rejects_extra_fields() -> None:
- with pytest.raises(ValueError, match="Extra inputs are not permitted"):
- ControlTimeEvent(
- ts_ns_local_control=501,
- reason="rate_limit_recheck",
- due_ts_ns_local=600,
- extra_field="unexpected",
- )
-
-
-def test_process_canonical_event_accepts_control_time_event_with_processing_position() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _control_time_event(
- ts_ns_local_control=510,
- due_ts_ns_local=520,
- )
-
- process_canonical_event(state, event, position=ProcessingPosition(index=14))
-
- assert state._last_processing_position_index == 14
-
-
-def test_process_canonical_event_control_time_event_does_not_mutate_state_buckets() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _control_time_event(
- ts_ns_local_control=530,
- realized_ts_ns_local=531,
- )
- before = _control_state_snapshot(state)
-
- process_canonical_event(state, event, position=ProcessingPosition(index=15))
-
- after = _control_state_snapshot(state)
- assert after == before
- assert state._last_processing_position_index == 15
-
-
-def test_control_time_event_still_obeys_global_processing_position_monotonicity() -> None:
- state = StrategyState(event_bus=NullEventBus())
- first = _control_time_event(
- ts_ns_local_control=540,
- due_ts_ns_local=550,
- )
- repeated = _control_time_event(
- ts_ns_local_control=541,
- due_ts_ns_local=551,
- )
-
- process_canonical_event(state, first, position=ProcessingPosition(index=16))
- with pytest.raises(ValueError, match="Non-monotonic ProcessingPosition index"):
- process_canonical_event(state, repeated, position=ProcessingPosition(index=16))
-
- assert state._last_processing_position_index == 16
-
-
-def test_first_positioned_event_is_accepted() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=100, ts_ns_exch=90)
-
- process_canonical_event(
- state,
- event,
- position=ProcessingPosition(index=0),
- configuration=_market_configuration(),
- )
-
- assert state._last_processing_position_index == 0
-
-
-def test_increasing_positions_are_accepted() -> None:
- state = StrategyState(event_bus=NullEventBus())
- first = _book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=100, ts_ns_exch=90)
- second = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=101,
- ts_ns_exch=91,
- )
-
- process_canonical_event(
- state,
- first,
- position=ProcessingPosition(index=10),
- configuration=_market_configuration(),
- )
- process_canonical_event(state, second, position=ProcessingPosition(index=11))
-
- assert state._last_processing_position_index == 11
-
-
-def test_repeated_position_is_rejected_without_state_mutation() -> None:
- state = StrategyState(event_bus=NullEventBus())
- accepted = _book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=100, ts_ns_exch=90)
- rejected = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=101,
- ts_ns_exch=91,
- )
-
- process_canonical_event(
- state,
- accepted,
- position=ProcessingPosition(index=3),
- configuration=_market_configuration(),
- )
- before = _state_subset_snapshot(state)
-
- with pytest.raises(ValueError, match="Non-monotonic ProcessingPosition index"):
- process_canonical_event(state, rejected, position=ProcessingPosition(index=3))
-
- after = _state_subset_snapshot(state)
- assert after == before
- assert state._last_processing_position_index == 3
-
-
-def test_regressing_position_is_rejected_without_state_mutation() -> None:
- state = StrategyState(event_bus=NullEventBus())
- accepted = _book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=100, ts_ns_exch=90)
- rejected = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=102,
- ts_ns_exch=92,
- )
-
- process_canonical_event(
- state,
- accepted,
- position=ProcessingPosition(index=8),
- configuration=_market_configuration(),
- )
- before = _state_subset_snapshot(state)
-
- with pytest.raises(ValueError, match="Non-monotonic ProcessingPosition index"):
- process_canonical_event(state, rejected, position=ProcessingPosition(index=7))
-
- after = _state_subset_snapshot(state)
- assert after == before
- assert state._last_processing_position_index == 8
-
-
-def test_position_none_remains_allowed_and_does_not_advance_cursor() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=100, ts_ns_exch=90)
-
- process_canonical_event(state, event, position=None)
-
- assert state._last_processing_position_index is None
-
- positioned = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=101,
- ts_ns_exch=91,
- )
- process_canonical_event(state, positioned, position=ProcessingPosition(index=0))
- assert state._last_processing_position_index == 0
-
-
-def test_processing_position_is_not_derived_from_event_time() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=1_000_000, ts_ns_exch=900_000)
- position = ProcessingPosition(index=1)
-
- process_canonical_event(state, event, position=position, configuration=_market_configuration())
-
- market = state.market["BTC-USDC-PERP"]
- assert market.last_ts_ns_local == event.ts_ns_local
- assert market.last_ts_ns_exch == event.ts_ns_exch
-
-
-def test_event_time_out_of_order_but_position_increasing_is_accepted_at_boundary() -> None:
- state = StrategyState(event_bus=NullEventBus())
- first = _book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=200, ts_ns_exch=190)
- second = _book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=100, ts_ns_exch=95)
-
- configuration = _market_configuration()
- process_canonical_event(state, first, position=ProcessingPosition(index=1), configuration=configuration)
- process_canonical_event(state, second, position=ProcessingPosition(index=2), configuration=configuration)
-
- assert state._last_processing_position_index == 2
- # Positioned canonical market events are now ProcessingPosition-driven.
- market = state.market["BTC-USDC-PERP"]
- assert market.last_ts_ns_local == 100
- assert market.last_ts_ns_exch == 95
-
-
-def test_position_out_of_order_but_event_time_increasing_is_rejected_at_boundary() -> None:
- state = StrategyState(event_bus=NullEventBus())
- first = _book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=100, ts_ns_exch=90)
- second = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=200,
- ts_ns_exch=180,
- )
-
- process_canonical_event(
- state,
- first,
- position=ProcessingPosition(index=5),
- configuration=_market_configuration(),
- )
- before = _state_subset_snapshot(state)
-
- with pytest.raises(ValueError, match="Non-monotonic ProcessingPosition index"):
- process_canonical_event(state, second, position=ProcessingPosition(index=4))
-
- after = _state_subset_snapshot(state)
- assert after == before
- assert state._last_processing_position_index == 5
-
-
-@pytest.mark.parametrize("second_cum_filled_qty", [0.25, 0.20])
-def test_positioned_fill_ordering_divergence_advances_cursor_but_keeps_fill_state_idempotent(
- second_cum_filled_qty: float,
-) -> None:
- state = StrategyState(event_bus=NullEventBus())
- first = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=200,
- ts_ns_exch=180,
- cum_filled_qty=0.25,
- )
- second = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=201,
- ts_ns_exch=181,
- cum_filled_qty=second_cum_filled_qty,
- )
-
- process_canonical_event(state, first, position=ProcessingPosition(index=20))
- fills_before = copy.deepcopy(state.fills)
- fill_cum_before = copy.deepcopy(state.fill_cum_qty)
-
- process_canonical_event(state, second, position=ProcessingPosition(index=21))
-
- assert state._last_processing_position_index == 21
- assert state.fills == fills_before
- assert state.fill_cum_qty == fill_cum_before
- assert len(state.fills["BTC-USDC-PERP"]) == 1
- assert state.fill_cum_qty["BTC-USDC-PERP"]["order-1"] == 0.25
-
-
-def test_interleaved_positioned_and_unpositioned_processing_preserves_cursor_monotonicity() -> None:
- state = StrategyState(event_bus=NullEventBus())
- positioned_10 = _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=100,
- ts_ns_exch=90,
- )
- unpositioned = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=101,
- ts_ns_exch=91,
- cum_filled_qty=0.25,
- )
- positioned_11 = _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=102,
- ts_ns_exch=92,
- )
- rejected = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=103,
- ts_ns_exch=93,
- cum_filled_qty=0.50,
- )
-
- configuration = _market_configuration()
- process_canonical_event(
- state,
- positioned_10,
- position=ProcessingPosition(index=10),
- configuration=configuration,
- )
- assert state._last_processing_position_index == 10
-
- process_canonical_event(state, unpositioned, position=None)
- assert state._last_processing_position_index == 10
-
- process_canonical_event(
- state,
- positioned_11,
- position=ProcessingPosition(index=11),
- configuration=configuration,
- )
- assert state._last_processing_position_index == 11
-
- with pytest.raises(ValueError, match="Non-monotonic ProcessingPosition index"):
- process_canonical_event(state, rejected, position=ProcessingPosition(index=10))
- with pytest.raises(ValueError, match="Non-monotonic ProcessingPosition index"):
- process_canonical_event(state, rejected, position=ProcessingPosition(index=11))
-
- assert state._last_processing_position_index == 11
-
-
-def test_positioned_market_tiebreak_no_longer_gates_positioned_market_updates() -> None:
- state = StrategyState(event_bus=NullEventBus())
- base = _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=300,
- ts_ns_exch=200,
- best_bid=100.0,
- best_ask=101.0,
- )
- lower_exch = _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=300,
- ts_ns_exch=199,
- best_bid=80.0,
- best_ask=81.0,
- )
- higher_exch = _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=300,
- ts_ns_exch=201,
- best_bid=120.0,
- best_ask=121.0,
- )
-
- configuration = _market_configuration()
- process_canonical_event(state, base, position=ProcessingPosition(index=30), configuration=configuration)
- process_canonical_event(
- state,
- lower_exch,
- position=ProcessingPosition(index=31),
- configuration=configuration,
- )
-
- market = state.market["BTC-USDC-PERP"]
- assert state._last_processing_position_index == 31
- assert market.last_ts_ns_local == 300
- assert market.last_ts_ns_exch == 199
- assert market.best_bid == 80.0
- assert market.best_ask == 81.0
-
- process_canonical_event(
- state,
- higher_exch,
- position=ProcessingPosition(index=32),
- configuration=configuration,
- )
-
- market_after_higher = state.market["BTC-USDC-PERP"]
- assert state._last_processing_position_index == 32
- assert market_after_higher.last_ts_ns_local == 300
- assert market_after_higher.last_ts_ns_exch == 201
- assert market_after_higher.best_bid == 120.0
- assert market_after_higher.best_ask == 121.0
-
-
-def test_valid_processing_position_can_authorize_boundary_order_while_reducer_noops() -> None:
- """Valid ProcessingPosition advances causal boundary while reducer may still no-op."""
- state = StrategyState(event_bus=NullEventBus())
- first = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=400,
- ts_ns_exch=390,
- cum_filled_qty=0.40,
- )
- duplicate = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=401,
- ts_ns_exch=391,
- cum_filled_qty=0.40,
- )
-
- process_canonical_event(state, first, position=ProcessingPosition(index=40))
- fills_before = copy.deepcopy(state.fills)
- fill_cum_before = copy.deepcopy(state.fill_cum_qty)
-
- process_canonical_event(state, duplicate, position=ProcessingPosition(index=41))
-
- assert state._last_processing_position_index == 41
- assert state.fills == fills_before
- assert state.fill_cum_qty == fill_cum_before
-
-
-def test_positioned_order_submitted_duplicate_is_idempotent_while_cursor_advances() -> None:
- state = StrategyState(event_bus=NullEventBus())
- first = _order_submitted_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-submitted-dup-1",
- ts_ns_local_dispatch=700,
- )
- duplicate = _order_submitted_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-submitted-dup-1",
- ts_ns_local_dispatch=701,
- )
-
- process_canonical_event(state, first, position=ProcessingPosition(index=42))
- projection_before = copy.deepcopy(
- state.canonical_orders[("BTC-USDC-PERP", "order-submitted-dup-1")]
- )
-
- process_canonical_event(state, duplicate, position=ProcessingPosition(index=43))
-
- projection_after = state.canonical_orders[("BTC-USDC-PERP", "order-submitted-dup-1")]
- assert state._last_processing_position_index == 43
- assert projection_after == projection_before
-
-
-def test_order_submitted_event_does_not_regress_existing_canonical_state() -> None:
- state = StrategyState(event_bus=NullEventBus())
- key = ("BTC-USDC-PERP", "order-no-regress-1")
- first = _order_submitted_event(
- instrument=key[0],
- client_order_id=key[1],
- ts_ns_local_dispatch=800,
- )
- accepted = _fill_event(
- instrument=key[0],
- client_order_id=key[1],
- ts_ns_local=810,
- ts_ns_exch=805,
- cum_filled_qty=0.25,
- )
- late_submitted = _order_submitted_event(
- instrument=key[0],
- client_order_id=key[1],
- ts_ns_local_dispatch=820,
- )
-
- process_canonical_event(state, first, position=ProcessingPosition(index=50))
- state.apply_order_state_event(
- _order_state_event(
- instrument=key[0],
- client_order_id=key[1],
- ts_ns_local=815,
- ts_ns_exch=815,
- state_type="accepted",
- )
- )
- process_canonical_event(state, accepted, position=ProcessingPosition(index=51))
- process_canonical_event(state, late_submitted, position=ProcessingPosition(index=52))
-
- projection = state.canonical_orders[key]
- assert projection.state == "accepted"
- assert projection.submitted_ts_ns_local == 800
- assert projection.updated_ts_ns_local == 815
- assert state._last_processing_position_index == 52
-
-
-def test_order_submitted_event_does_not_mutate_snapshot_orders() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _order_submitted_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-snapshot-isolation-1",
- ts_ns_local_dispatch=900,
- )
-
- process_canonical_event(state, event, position=ProcessingPosition(index=60))
-
- assert state.orders == {}
- assert state.canonical_orders[("BTC-USDC-PERP", "order-snapshot-isolation-1")].state == "submitted"
-
-
-def test_process_canonical_event_rejects_order_state_event() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _order_state_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-compat-1",
- ts_ns_local=300,
- ts_ns_exch=290,
- )
-
- with pytest.raises(TypeError, match="Unsupported non-canonical event type"):
- process_canonical_event(state, event)
-
-
-def test_process_canonical_event_rejects_order_state_event_with_processing_position() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _order_state_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-compat-1",
- ts_ns_local=300,
- ts_ns_exch=290,
- )
- position = ProcessingPosition(index=20)
-
- with pytest.raises(TypeError, match="Unsupported non-canonical event type"):
- process_canonical_event(state, event, position=position)
-
-
-def test_process_event_entry_rejects_derived_fill_event() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = DerivedFillEvent(
- ts_ns_local=300,
- instrument="BTC-USDC-PERP",
- client_order_id="order-compat-derived-1",
- side="buy",
- delta_qty=0.25,
- cum_qty=0.25,
- price=100.0,
- )
- entry = EventStreamEntry(
- position=ProcessingPosition(index=21),
- event=event,
- )
-
- with pytest.raises(TypeError, match="Unsupported non-canonical event type"):
- process_event_entry(state, entry)
-
-
-def test_process_canonical_event_rejects_telemetry_record() -> None:
- state = StrategyState(event_bus=NullEventBus())
- telemetry = RiskDecisionEvent(
- ts_ns_local=400,
- accepted=1,
- queued=0,
- rejected=0,
- handled=0,
- reject_reasons={},
- )
-
- with pytest.raises(TypeError, match="Unsupported non-canonical event type"):
- process_canonical_event(state, telemetry)
-
-
-def test_event_bus_remains_non_canonical() -> None:
- assert is_canonical_stream_candidate_type(EventBus) is False
-
-
-def test_processing_position_zero_index_is_valid() -> None:
- position = ProcessingPosition(index=0)
- assert position.index == 0
-
-
-def test_processing_position_negative_index_is_rejected() -> None:
- with pytest.raises(ValueError, match="must be non-negative"):
- ProcessingPosition(index=-1)
-
diff --git a/tests/semantics/models/test_canonical_processing_differential_harness.py b/tests/semantics/models/test_canonical_processing_differential_harness.py
deleted file mode 100644
index 85b3102..0000000
--- a/tests/semantics/models/test_canonical_processing_differential_harness.py
+++ /dev/null
@@ -1,362 +0,0 @@
-"""Differential characterization tests for canonical reducer boundary parity."""
-
-from __future__ import annotations
-
-import copy
-
-import pytest
-
-from tradingchassis_core.core.domain.processing import process_canonical_event
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- FillEvent,
- MarketEvent,
- OrderStateEvent,
- Price,
- Quantity,
-)
-from tradingchassis_core.core.events.events import RiskDecisionEvent
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-
-
-def _book_market_event(
- *,
- instrument: str,
- ts_ns_local: int,
- ts_ns_exch: int,
- best_bid: float,
- best_ask: float,
- best_bid_qty: float = 2.0,
- best_ask_qty: float = 3.0,
-) -> MarketEvent:
- return MarketEvent(
- ts_ns_local=ts_ns_local,
- ts_ns_exch=ts_ns_exch,
- instrument=instrument,
- event_type="book",
- book={
- "book_type": "snapshot",
- "bids": [
- {
- "price": {"currency": "USDC", "value": best_bid},
- "quantity": {"unit": "contracts", "value": best_bid_qty},
- }
- ],
- "asks": [
- {
- "price": {"currency": "USDC", "value": best_ask},
- "quantity": {"unit": "contracts", "value": best_ask_qty},
- }
- ],
- "depth": 1,
- },
- trade=None,
- )
-
-
-def _apply_market_direct(state: StrategyState, event: MarketEvent) -> None:
- assert event.book is not None
- best_bid_level = event.book.bids[0]
- best_ask_level = event.book.asks[0]
- state.update_market(
- instrument=event.instrument,
- best_bid=best_bid_level.price.value,
- best_ask=best_ask_level.price.value,
- best_bid_qty=best_bid_level.quantity.value,
- best_ask_qty=best_ask_level.quantity.value,
- tick_size=0.0,
- lot_size=0.0,
- contract_size=1.0,
- ts_ns_local=event.ts_ns_local,
- ts_ns_exch=event.ts_ns_exch,
- )
-
-
-def _fill_event(
- *,
- instrument: str,
- client_order_id: str,
- ts_ns_local: int,
- ts_ns_exch: int,
- cum_qty: float,
-) -> FillEvent:
- remaining = max(0.0, 1.0 - cum_qty)
- return FillEvent(
- ts_ns_local=ts_ns_local,
- ts_ns_exch=ts_ns_exch,
- instrument=instrument,
- client_order_id=client_order_id,
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=Price(currency="USDC", value=100.5),
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=Quantity(unit="contracts", value=cum_qty),
- remaining_qty=Quantity(unit="contracts", value=remaining),
- time_in_force="GTC",
- liquidity_flag="maker",
- fee=None,
- )
-
-
-def _order_state_event(*, instrument: str, client_order_id: str) -> OrderStateEvent:
- return OrderStateEvent(
- ts_ns_local=300,
- ts_ns_exch=290,
- instrument=instrument,
- client_order_id=client_order_id,
- order_type="limit",
- state_type="accepted",
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=None,
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=None,
- remaining_qty=None,
- time_in_force="GTC",
- reason=None,
- raw={"req": 0, "source": "snapshot"},
- )
-
-
-def _state_subset_snapshot(state: StrategyState) -> dict[str, object]:
- return {
- "market": copy.deepcopy(state.market),
- "fills": copy.deepcopy(state.fills),
- "fill_cum_qty": copy.deepcopy(state.fill_cum_qty),
- "orders": copy.deepcopy(state.orders),
- "canonical_orders": copy.deepcopy(state.canonical_orders),
- }
-
-
-def test_market_parity_single_event_canonical_equals_direct() -> None:
- instrument = "BTC-USDC-PERP"
- event = _book_market_event(
- instrument=instrument,
- ts_ns_local=100,
- ts_ns_exch=90,
- best_bid=100.0,
- best_ask=101.0,
- )
-
- canonical_state = StrategyState(event_bus=NullEventBus())
- direct_state = StrategyState(event_bus=NullEventBus())
-
- process_canonical_event(canonical_state, event)
- _apply_market_direct(direct_state, event)
-
- assert canonical_state.market == direct_state.market
-
-
-def test_market_parity_newer_local_timestamp_replaces_older() -> None:
- instrument = "BTC-USDC-PERP"
- older = _book_market_event(
- instrument=instrument,
- ts_ns_local=100,
- ts_ns_exch=90,
- best_bid=100.0,
- best_ask=101.0,
- )
- newer = _book_market_event(
- instrument=instrument,
- ts_ns_local=101,
- ts_ns_exch=80,
- best_bid=102.0,
- best_ask=103.0,
- )
-
- canonical_state = StrategyState(event_bus=NullEventBus())
- direct_state = StrategyState(event_bus=NullEventBus())
-
- process_canonical_event(canonical_state, older)
- process_canonical_event(canonical_state, newer)
- _apply_market_direct(direct_state, older)
- _apply_market_direct(direct_state, newer)
-
- assert canonical_state.market == direct_state.market
- assert canonical_state.market[instrument].best_bid == 102.0
- assert canonical_state.market[instrument].best_ask == 103.0
-
-
-def test_market_parity_older_local_timestamp_is_ignored() -> None:
- instrument = "BTC-USDC-PERP"
- newer = _book_market_event(
- instrument=instrument,
- ts_ns_local=200,
- ts_ns_exch=120,
- best_bid=105.0,
- best_ask=106.0,
- )
- older = _book_market_event(
- instrument=instrument,
- ts_ns_local=199,
- ts_ns_exch=500,
- best_bid=90.0,
- best_ask=91.0,
- )
-
- canonical_state = StrategyState(event_bus=NullEventBus())
- direct_state = StrategyState(event_bus=NullEventBus())
-
- process_canonical_event(canonical_state, newer)
- process_canonical_event(canonical_state, older)
- _apply_market_direct(direct_state, newer)
- _apply_market_direct(direct_state, older)
-
- assert canonical_state.market == direct_state.market
- assert canonical_state.market[instrument].best_bid == 105.0
- assert canonical_state.market[instrument].best_ask == 106.0
-
-
-def test_market_parity_equal_local_timestamp_uses_exchange_tiebreak() -> None:
- instrument = "BTC-USDC-PERP"
- base = _book_market_event(
- instrument=instrument,
- ts_ns_local=300,
- ts_ns_exch=100,
- best_bid=110.0,
- best_ask=111.0,
- )
- higher_exch = _book_market_event(
- instrument=instrument,
- ts_ns_local=300,
- ts_ns_exch=101,
- best_bid=112.0,
- best_ask=113.0,
- )
- lower_exch = _book_market_event(
- instrument=instrument,
- ts_ns_local=300,
- ts_ns_exch=99,
- best_bid=80.0,
- best_ask=81.0,
- )
-
- canonical_state = StrategyState(event_bus=NullEventBus())
- direct_state = StrategyState(event_bus=NullEventBus())
-
- process_canonical_event(canonical_state, base)
- process_canonical_event(canonical_state, higher_exch)
- process_canonical_event(canonical_state, lower_exch)
- _apply_market_direct(direct_state, base)
- _apply_market_direct(direct_state, higher_exch)
- _apply_market_direct(direct_state, lower_exch)
-
- assert canonical_state.market == direct_state.market
- assert canonical_state.market[instrument].best_bid == 112.0
- assert canonical_state.market[instrument].best_ask == 113.0
- assert canonical_state.market[instrument].last_ts_ns_exch == 101
-
-
-def test_fill_parity_single_event_canonical_equals_direct() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-1"
- event = _fill_event(
- instrument=instrument,
- client_order_id=client_order_id,
- ts_ns_local=400,
- ts_ns_exch=390,
- cum_qty=0.25,
- )
-
- canonical_state = StrategyState(event_bus=NullEventBus())
- direct_state = StrategyState(event_bus=NullEventBus())
-
- process_canonical_event(canonical_state, event)
- direct_state.apply_fill_event(event)
-
- assert canonical_state.fills == direct_state.fills
- assert canonical_state.fill_cum_qty == direct_state.fill_cum_qty
-
-
-def test_fill_parity_duplicate_and_non_increasing_cumulative_are_idempotent() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-1"
- first = _fill_event(
- instrument=instrument,
- client_order_id=client_order_id,
- ts_ns_local=500,
- ts_ns_exch=490,
- cum_qty=0.25,
- )
- duplicate = _fill_event(
- instrument=instrument,
- client_order_id=client_order_id,
- ts_ns_local=501,
- ts_ns_exch=491,
- cum_qty=0.25,
- )
- lower = _fill_event(
- instrument=instrument,
- client_order_id=client_order_id,
- ts_ns_local=502,
- ts_ns_exch=492,
- cum_qty=0.20,
- )
- higher = _fill_event(
- instrument=instrument,
- client_order_id=client_order_id,
- ts_ns_local=503,
- ts_ns_exch=493,
- cum_qty=0.40,
- )
-
- canonical_state = StrategyState(event_bus=NullEventBus())
- direct_state = StrategyState(event_bus=NullEventBus())
-
- for event in (first, duplicate, lower, higher):
- process_canonical_event(canonical_state, event)
- direct_state.apply_fill_event(event)
-
- assert canonical_state.fills == direct_state.fills
- assert canonical_state.fill_cum_qty == direct_state.fill_cum_qty
- assert len(canonical_state.fills[instrument]) == 2
- assert canonical_state.fill_cum_qty[instrument][client_order_id] == 0.4
-
-
-@pytest.mark.parametrize(
- "artifact",
- [
- pytest.param("order_state_event", id="order-state-event"),
- pytest.param("risk_decision_event", id="risk-decision-telemetry"),
- ],
-)
-def test_rejected_non_canonical_artifacts_do_not_mutate_state(artifact: str) -> None:
- instrument = "BTC-USDC-PERP"
- state = StrategyState(event_bus=NullEventBus())
-
- seed_market = _book_market_event(
- instrument=instrument,
- ts_ns_local=700,
- ts_ns_exch=690,
- best_bid=120.0,
- best_ask=121.0,
- )
- seed_fill = _fill_event(
- instrument=instrument,
- client_order_id="order-1",
- ts_ns_local=710,
- ts_ns_exch=700,
- cum_qty=0.25,
- )
- process_canonical_event(state, seed_market)
- process_canonical_event(state, seed_fill)
-
- before = _state_subset_snapshot(state)
-
- if artifact == "order_state_event":
- non_canonical = _order_state_event(instrument=instrument, client_order_id="order-compat-1")
- else:
- non_canonical = RiskDecisionEvent(
- ts_ns_local=720,
- accepted=1,
- queued=0,
- rejected=0,
- handled=0,
- reject_reasons={},
- )
-
- with pytest.raises(TypeError, match="Unsupported non-canonical event type"):
- process_canonical_event(state, non_canonical)
-
- after = _state_subset_snapshot(state)
- assert after == before
diff --git a/tests/semantics/models/test_canonical_reducer_authority_guard.py b/tests/semantics/models/test_canonical_reducer_authority_guard.py
deleted file mode 100644
index 480154c..0000000
--- a/tests/semantics/models/test_canonical_reducer_authority_guard.py
+++ /dev/null
@@ -1,60 +0,0 @@
-"""Architectural guard for canonical reducer authority hardening."""
-
-from __future__ import annotations
-
-import ast
-from pathlib import Path
-
-_ALLOWED_CALLER = Path("tradingchassis_core/core/domain/processing.py")
-_TARGET_METHODS = frozenset(
- {
- "update_market",
- "apply_fill_event",
- "apply_order_submitted_event",
- "apply_control_time_event",
- }
-)
-
-
-def _iter_python_files(root: Path) -> list[Path]:
- return sorted(path for path in root.rglob("*.py") if path.is_file())
-
-
-def _find_target_calls(path: Path) -> list[tuple[int, int, str]]:
- tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
- calls: list[tuple[int, int, str]] = []
-
- for node in ast.walk(tree):
- if not isinstance(node, ast.Call):
- continue
- if not isinstance(node.func, ast.Attribute):
- continue
- method_name = node.func.attr
- if method_name not in _TARGET_METHODS:
- continue
- calls.append((node.lineno, node.col_offset, method_name))
-
- return calls
-
-
-def test_direct_reducer_calls_are_limited_to_canonical_processing_boundary() -> None:
- repo_root = Path(__file__).resolve().parents[3]
- production_root = repo_root / "tradingchassis_core"
-
- violations: list[str] = []
-
- for file_path in _iter_python_files(production_root):
- relative_path = file_path.relative_to(repo_root)
- calls = _find_target_calls(file_path)
- if not calls:
- continue
-
- if relative_path == _ALLOWED_CALLER:
- continue
-
- for lineno, col, method_name in calls:
- violations.append(f"{relative_path}:{lineno}:{col} calls {method_name}(...)")
-
- assert not violations, "Unexpected direct reducer calls outside canonical boundary:\n" + "\n".join(
- violations
- )
diff --git a/tests/semantics/models/test_core_configuration_contract.py b/tests/semantics/models/test_core_configuration_contract.py
deleted file mode 100644
index 251d294..0000000
--- a/tests/semantics/models/test_core_configuration_contract.py
+++ /dev/null
@@ -1,70 +0,0 @@
-"""Semantics tests for CoreConfiguration identity and stability contract."""
-
-from __future__ import annotations
-
-import pytest
-
-from tradingchassis_core.core.domain.configuration import CoreConfiguration
-
-
-def test_same_version_and_semantic_payload_produce_same_fingerprint() -> None:
- left = CoreConfiguration(
- version="v1",
- payload={
- "a": 1,
- "b": [True, {"x": "y", "z": None}],
- },
- )
- right = CoreConfiguration(
- version="v1",
- payload={
- "b": [True, {"z": None, "x": "y"}],
- "a": 1,
- },
- )
-
- assert left.fingerprint == right.fingerprint
- assert left.payload == right.payload
-
-
-def test_different_payload_produces_different_fingerprint() -> None:
- left = CoreConfiguration(version="v1", payload={"a": 1})
- right = CoreConfiguration(version="v1", payload={"a": 2})
-
- assert left.fingerprint != right.fingerprint
-
-
-def test_different_version_produces_different_fingerprint() -> None:
- left = CoreConfiguration(version="v1", payload={"a": 1})
- right = CoreConfiguration(version="v2", payload={"a": 1})
-
- assert left.fingerprint != right.fingerprint
-
-
-def test_rejects_unsupported_payload_values() -> None:
- with pytest.raises(TypeError, match="Unsupported configuration payload value type"):
- CoreConfiguration(version="v1", payload={"unsupported": object()})
-
- with pytest.raises(TypeError, match="mapping keys must be strings"):
- CoreConfiguration(version="v1", payload={1: "x"}) # type: ignore[dict-item]
-
-
-def test_external_payload_mutation_does_not_change_configuration_identity() -> None:
- source = {
- "limits": {
- "max_orders": 10,
- "enabled": True,
- },
- "symbols": ["BTC-USDC-PERP", "ETH-USDC-PERP"],
- }
-
- configuration = CoreConfiguration(version="v1", payload=source)
- original_fingerprint = configuration.fingerprint
- original_payload = configuration.payload
-
- source["limits"]["max_orders"] = 99
- source["symbols"].append("SOL-USDC-PERP")
- source["limits"]["new_key"] = "added"
-
- assert configuration.fingerprint == original_fingerprint
- assert configuration.payload == original_payload
diff --git a/tests/semantics/models/test_event_stream_entry_contract.py b/tests/semantics/models/test_event_stream_entry_contract.py
deleted file mode 100644
index e9c0923..0000000
--- a/tests/semantics/models/test_event_stream_entry_contract.py
+++ /dev/null
@@ -1,378 +0,0 @@
-"""Semantics tests for minimal EventStreamEntry contract (Phase 2B.1)."""
-
-from __future__ import annotations
-
-import copy
-import dataclasses
-import inspect
-
-import pytest
-
-from tradingchassis_core.core.domain.configuration import CoreConfiguration
-from tradingchassis_core.core.domain.event_model import is_canonical_stream_candidate_type
-from tradingchassis_core.core.domain.processing import (
- fold_event_stream_entries,
- 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.types import (
- ControlTimeEvent,
- FillEvent,
- MarketEvent,
- OrderStateEvent,
- OrderSubmittedEvent,
- Price,
- Quantity,
-)
-from tradingchassis_core.core.events.event_bus import EventBus
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-
-
-def _book_market_event(*, instrument: str, ts_ns_local: int, ts_ns_exch: int) -> MarketEvent:
- return MarketEvent(
- ts_ns_local=ts_ns_local,
- ts_ns_exch=ts_ns_exch,
- instrument=instrument,
- event_type="book",
- book={
- "book_type": "snapshot",
- "bids": [
- {
- "price": {"currency": "USDC", "value": 100.0},
- "quantity": {"unit": "contracts", "value": 2.0},
- }
- ],
- "asks": [
- {
- "price": {"currency": "USDC", "value": 101.0},
- "quantity": {"unit": "contracts", "value": 3.0},
- }
- ],
- "depth": 1,
- },
- trade=None,
- )
-
-
-def _fill_event(
- *,
- instrument: str,
- client_order_id: str,
- ts_ns_local: int,
- ts_ns_exch: int,
- cum_filled_qty: float = 0.25,
-) -> FillEvent:
- return FillEvent(
- ts_ns_local=ts_ns_local,
- ts_ns_exch=ts_ns_exch,
- instrument=instrument,
- client_order_id=client_order_id,
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=Price(currency="USDC", value=100.5),
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=Quantity(unit="contracts", value=cum_filled_qty),
- remaining_qty=Quantity(unit="contracts", value=max(0.0, 1.0 - cum_filled_qty)),
- time_in_force="GTC",
- liquidity_flag="maker",
- fee=None,
- )
-
-
-def _order_state_event(*, instrument: str, client_order_id: str) -> OrderStateEvent:
- return OrderStateEvent(
- ts_ns_local=300,
- ts_ns_exch=290,
- instrument=instrument,
- client_order_id=client_order_id,
- order_type="limit",
- state_type="accepted",
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=None,
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=None,
- remaining_qty=None,
- time_in_force="GTC",
- reason=None,
- raw={"req": 0, "source": "snapshot"},
- )
-
-
-def _order_submitted_event(
- *,
- instrument: str,
- client_order_id: str,
- ts_ns_local_dispatch: int,
-) -> OrderSubmittedEvent:
- return OrderSubmittedEvent(
- ts_ns_local_dispatch=ts_ns_local_dispatch,
- instrument=instrument,
- client_order_id=client_order_id,
- side="buy",
- order_type="limit",
- intended_price=Price(currency="USDC", value=100.0),
- intended_qty=Quantity(unit="contracts", value=1.0),
- time_in_force="GTC",
- intent_correlation_id="corr-1",
- dispatch_attempt_id="attempt-1",
- runtime_correlation={"engine": "backtest", "seq": 1},
- )
-
-
-def _control_time_event(
- *,
- ts_ns_local_control: int,
- due_ts_ns_local: int | None = None,
- realized_ts_ns_local: int | None = None,
-) -> ControlTimeEvent:
- return ControlTimeEvent(
- ts_ns_local_control=ts_ns_local_control,
- reason="rate_limit_recheck",
- due_ts_ns_local=due_ts_ns_local,
- realized_ts_ns_local=realized_ts_ns_local,
- obligation_reason="rate_limit",
- obligation_due_ts_ns_local=due_ts_ns_local,
- runtime_correlation={"engine": "backtest", "seq": 1},
- )
-
-
-def _state_subset_snapshot(state: StrategyState) -> dict[str, object]:
- return {
- "market": copy.deepcopy(state.market),
- "fills": copy.deepcopy(state.fills),
- "fill_cum_qty": copy.deepcopy(state.fill_cum_qty),
- }
-
-
-def _market_configuration(
- *,
- instrument: str = "BTC-USDC-PERP",
- tick_size: float = 0.1,
- lot_size: float = 0.01,
- contract_size: float = 1.0,
-) -> CoreConfiguration:
- return CoreConfiguration(
- version="v1",
- payload={
- "market": {
- "instruments": {
- instrument: {
- "tick_size": tick_size,
- "lot_size": lot_size,
- "contract_size": contract_size,
- }
- }
- }
- },
- )
-
-
-def test_event_stream_entry_requires_processing_position() -> None:
- with pytest.raises(TypeError, match="position must be a ProcessingPosition"):
- EventStreamEntry(position=object(), event={"x": 1})
-
-
-def test_event_stream_entry_contract_has_no_configuration_field() -> None:
- field_names = {field.name for field in dataclasses.fields(EventStreamEntry)}
- assert field_names == {"position", "event"}
- assert "configuration" not in field_names
-
-
-def test_configuration_is_call_level_input_not_entry_level_shape() -> None:
- process_signature = inspect.signature(process_event_entry)
- fold_signature = inspect.signature(fold_event_stream_entries)
-
- assert "configuration" in process_signature.parameters
- assert "configuration" in fold_signature.parameters
- assert "configuration" not in {field.name for field in dataclasses.fields(EventStreamEntry)}
-
-
-def test_process_event_entry_processes_market_and_advances_state() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=100, ts_ns_exch=90)
- entry = EventStreamEntry(position=ProcessingPosition(index=0), event=event)
-
- process_event_entry(state, entry, configuration=_market_configuration())
-
- market = state.market["BTC-USDC-PERP"]
- assert state._last_processing_position_index == 0
- assert market.best_bid == 100.0
- assert market.best_ask == 101.0
- assert market.last_ts_ns_local == 100
- assert market.last_ts_ns_exch == 90
-
-
-def test_process_event_entry_processes_fill_and_updates_fill_state() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=200,
- ts_ns_exch=180,
- )
- entry = EventStreamEntry(position=ProcessingPosition(index=5), event=event)
-
- process_event_entry(state, entry)
-
- assert state._last_processing_position_index == 5
- assert len(state.fills["BTC-USDC-PERP"]) == 1
- assert state.fill_cum_qty["BTC-USDC-PERP"]["order-1"] == 0.25
-
-
-def test_process_event_entry_processes_order_submitted_and_updates_projection() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _order_submitted_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-submitted-1",
- ts_ns_local_dispatch=250,
- )
- entry = EventStreamEntry(position=ProcessingPosition(index=6), event=event)
-
- process_event_entry(state, entry)
-
- assert state._last_processing_position_index == 6
- projection = state.canonical_orders[("BTC-USDC-PERP", "order-submitted-1")]
- assert projection.state == "submitted"
- assert projection.submitted_ts_ns_local == 250
- assert projection.updated_ts_ns_local == 250
-
-
-def test_process_event_entry_processes_control_time_event_and_advances_cursor() -> None:
- state = StrategyState(event_bus=NullEventBus())
- event = _control_time_event(
- ts_ns_local_control=260,
- due_ts_ns_local=300,
- )
- entry = EventStreamEntry(position=ProcessingPosition(index=7), event=event)
- before = {
- "queued_intents": copy.deepcopy(state.queued_intents),
- "inflight": copy.deepcopy(state.inflight),
- "orders": copy.deepcopy(state.orders),
- "canonical_orders": copy.deepcopy(state.canonical_orders),
- "fills": copy.deepcopy(state.fills),
- "market": copy.deepcopy(state.market),
- "account": copy.deepcopy(state.account),
- }
-
- process_event_entry(state, entry)
-
- after = {
- "queued_intents": copy.deepcopy(state.queued_intents),
- "inflight": copy.deepcopy(state.inflight),
- "orders": copy.deepcopy(state.orders),
- "canonical_orders": copy.deepcopy(state.canonical_orders),
- "fills": copy.deepcopy(state.fills),
- "market": copy.deepcopy(state.market),
- "account": copy.deepcopy(state.account),
- }
- assert state._last_processing_position_index == 7
- assert after == before
-
-
-def test_process_event_entry_rejects_non_canonical_payload() -> None:
- state = StrategyState(event_bus=NullEventBus())
- compat_event = _order_state_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-compat-1",
- )
- entry = EventStreamEntry(position=ProcessingPosition(index=1), event=compat_event)
-
- with pytest.raises(TypeError, match="Unsupported non-canonical event type"):
- process_event_entry(state, entry)
-
-
-def test_process_event_entry_enforces_processing_position_monotonicity() -> None:
- state = StrategyState(event_bus=NullEventBus())
- first = EventStreamEntry(
- position=ProcessingPosition(index=10),
- event=_book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=100, ts_ns_exch=90),
- )
- second = EventStreamEntry(
- position=ProcessingPosition(index=11),
- event=_fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=101,
- ts_ns_exch=91,
- ),
- )
- repeated = EventStreamEntry(
- position=ProcessingPosition(index=11),
- event=_fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=102,
- ts_ns_exch=92,
- ),
- )
- regressing = EventStreamEntry(
- position=ProcessingPosition(index=9),
- event=_fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=103,
- ts_ns_exch=93,
- ),
- )
-
- process_event_entry(state, first, configuration=_market_configuration())
- process_event_entry(state, second, configuration=None)
- assert state._last_processing_position_index == 11
- before = _state_subset_snapshot(state)
-
- with pytest.raises(ValueError, match="Non-monotonic ProcessingPosition index"):
- process_event_entry(state, repeated)
- with pytest.raises(ValueError, match="Non-monotonic ProcessingPosition index"):
- process_event_entry(state, regressing)
-
- assert _state_subset_snapshot(state) == before
- assert state._last_processing_position_index == 11
-
-
-def test_process_event_entry_positioned_market_requires_configuration() -> None:
- event = _book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=100, ts_ns_exch=90)
- entry = EventStreamEntry(position=ProcessingPosition(index=0), event=event)
- state = StrategyState(event_bus=NullEventBus())
-
- with pytest.raises(
- ValueError,
- match="CoreConfiguration is required for positioned canonical MarketEvent processing",
- ):
- process_event_entry(state, entry, configuration=None)
-
-
-def test_process_event_entry_positioned_fill_remains_configuration_agnostic() -> None:
- event = _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=200,
- ts_ns_exch=180,
- )
- entry = EventStreamEntry(position=ProcessingPosition(index=5), event=event)
- state = StrategyState(event_bus=NullEventBus())
-
- process_event_entry(state, entry, configuration=None)
-
- assert state._last_processing_position_index == 5
- assert len(state.fills["BTC-USDC-PERP"]) == 1
- assert state.fill_cum_qty["BTC-USDC-PERP"]["order-1"] == 0.25
-
-
-def test_process_event_entry_rejects_non_core_configuration() -> None:
- event = _book_market_event(instrument="BTC-USDC-PERP", ts_ns_local=100, ts_ns_exch=90)
- entry = EventStreamEntry(position=ProcessingPosition(index=0), event=event)
- state = StrategyState(event_bus=NullEventBus())
-
- with pytest.raises(TypeError, match="configuration must be CoreConfiguration or None"):
- process_event_entry(state, entry, configuration={"version": "v1"})
-
-
-def test_event_bus_remains_non_canonical_event_stream_input() -> None:
- assert is_canonical_stream_candidate_type(EventBus) is False
-
- state = StrategyState(event_bus=NullEventBus())
- entry = EventStreamEntry(position=ProcessingPosition(index=0), event=EventBus())
- with pytest.raises(TypeError, match="Unsupported non-canonical event type"):
- process_event_entry(state, entry)
diff --git a/tests/semantics/models/test_event_taxonomy_boundary.py b/tests/semantics/models/test_event_taxonomy_boundary.py
deleted file mode 100644
index ac622f5..0000000
--- a/tests/semantics/models/test_event_taxonomy_boundary.py
+++ /dev/null
@@ -1,163 +0,0 @@
-"""Semantics tests for the lightweight core event taxonomy boundary."""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.event_model import (
- CANONICAL_EVENT_CATEGORY_NAMES,
- COMPATIBILITY_PROJECTION_TYPES,
- NON_CANONICAL_CONTROL_HELPER_TYPES,
- TELEMETRY_EVENT_TYPES,
- CanonicalEventCategory,
- canonical_category_for_type,
- is_canonical_stream_candidate_type,
-)
-from tradingchassis_core.core.domain.processing import process_canonical_event
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- ControlTimeEvent,
- FillEvent,
- MarketEvent,
- OrderStateEvent,
- OrderSubmittedEvent,
-)
-from tradingchassis_core.core.events.event_bus import EventBus
-from tradingchassis_core.core.events.events import (
- DerivedFillEvent,
- DerivedPnLEvent,
- ExposureDerivedEvent,
- OrderStateTransitionEvent,
- RiskDecisionEvent,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-from tradingchassis_core.core.execution_control.types import ControlSchedulingObligation
-from tradingchassis_core.core.risk.risk_engine import GateDecision
-
-
-def test_canonical_event_category_names_are_stable() -> None:
- """Canonical category names remain docs-aligned and stable."""
-
- assert CANONICAL_EVENT_CATEGORY_NAMES == (
- "market",
- "intent_related",
- "execution",
- "control",
- )
-
-
-def test_canonical_stream_candidate_classification_current_slice() -> None:
- """Current slice markers keep canonical candidates explicit and minimal."""
-
- assert is_canonical_stream_candidate_type(MarketEvent) is True
- assert canonical_category_for_type(MarketEvent) == CanonicalEventCategory.MARKET
-
- assert is_canonical_stream_candidate_type(FillEvent) is True
- assert canonical_category_for_type(FillEvent) == CanonicalEventCategory.EXECUTION
-
- assert is_canonical_stream_candidate_type(OrderSubmittedEvent) is True
- assert (
- canonical_category_for_type(OrderSubmittedEvent)
- == CanonicalEventCategory.INTENT_RELATED
- )
-
- assert is_canonical_stream_candidate_type(ControlTimeEvent) is True
- assert canonical_category_for_type(ControlTimeEvent) == CanonicalEventCategory.CONTROL
-
- # Compatibility execution feedback remains non-canonical in this slice.
- assert is_canonical_stream_candidate_type(OrderStateEvent) is False
- assert OrderStateEvent in COMPATIBILITY_PROJECTION_TYPES
-
-
-def test_event_bus_is_not_canonical_stream_record() -> None:
- """EventBus remains a transport abstraction, not a canonical event."""
-
- assert is_canonical_stream_candidate_type(EventBus) is False
- assert canonical_category_for_type(EventBus) is None
-
-
-def test_gate_decision_is_not_canonical_stream_record() -> None:
- """GateDecision remains a compatibility decision contract, not an event."""
-
- assert is_canonical_stream_candidate_type(GateDecision) is False
- assert canonical_category_for_type(GateDecision) is None
-
-
-def test_control_scheduling_obligation_is_not_an_event() -> None:
- """ControlSchedulingObligation is explicitly non-canonical."""
-
- assert is_canonical_stream_candidate_type(ControlSchedulingObligation) is False
- assert canonical_category_for_type(ControlSchedulingObligation) is None
- assert ControlSchedulingObligation in NON_CANONICAL_CONTROL_HELPER_TYPES
-
-
-def test_telemetry_records_are_not_canonical_stream_candidates() -> None:
- """Telemetry/observability records remain outside canonical stream markers."""
-
- telemetry_types = (
- RiskDecisionEvent,
- DerivedPnLEvent,
- ExposureDerivedEvent,
- OrderStateTransitionEvent,
- )
-
- for record_type in telemetry_types:
- assert record_type in TELEMETRY_EVENT_TYPES
- assert is_canonical_stream_candidate_type(record_type) is False
- assert canonical_category_for_type(record_type) is None
-
- # Compatibility projection artifact is also non-canonical.
- assert DerivedFillEvent in COMPATIBILITY_PROJECTION_TYPES
- assert is_canonical_stream_candidate_type(DerivedFillEvent) is False
- assert canonical_category_for_type(DerivedFillEvent) is None
-
-
-def test_process_canonical_event_rejects_order_state_event_guard() -> None:
- """Canonical processing boundary rejects compatibility OrderStateEvent records."""
-
- state = StrategyState(event_bus=NullEventBus())
- compatibility_record = OrderStateEvent(
- ts_ns_local=1,
- ts_ns_exch=1,
- instrument="BTC-USDC-PERP",
- client_order_id="compat-1",
- order_type="limit",
- state_type="accepted",
- side="buy",
- intended_price={"currency": "USDC", "value": 100.0},
- filled_price=None,
- intended_qty={"unit": "contracts", "value": 1.0},
- cum_filled_qty=None,
- remaining_qty=None,
- time_in_force="GTC",
- reason=None,
- raw={"req": 0, "source": "snapshot"},
- )
-
- try:
- process_canonical_event(state, compatibility_record)
- except TypeError as exc:
- assert "Unsupported non-canonical event type" in str(exc)
- else:
- raise AssertionError("Expected process_canonical_event to reject OrderStateEvent")
-
-
-def test_process_canonical_event_rejects_derived_fill_event_guard() -> None:
- """Canonical processing boundary rejects compatibility DerivedFillEvent records."""
-
- state = StrategyState(event_bus=NullEventBus())
- compatibility_record = DerivedFillEvent(
- ts_ns_local=1,
- instrument="BTC-USDC-PERP",
- client_order_id="compat-derived-1",
- side="buy",
- delta_qty=0.1,
- cum_qty=0.1,
- price=100.5,
- )
-
- try:
- process_canonical_event(state, compatibility_record)
- except TypeError as exc:
- assert "Unsupported non-canonical event type" in str(exc)
- else:
- raise AssertionError("Expected process_canonical_event to reject DerivedFillEvent")
-
diff --git a/tests/semantics/models/test_fold_event_stream_entries_contract.py b/tests/semantics/models/test_fold_event_stream_entries_contract.py
deleted file mode 100644
index 2b3d212..0000000
--- a/tests/semantics/models/test_fold_event_stream_entries_contract.py
+++ /dev/null
@@ -1,630 +0,0 @@
-"""Semantics tests for minimal deterministic fold/replay contract (Phase 2B.2)."""
-
-from __future__ import annotations
-
-import copy
-
-import pytest
-
-from tradingchassis_core.core.domain.configuration import CoreConfiguration
-from tradingchassis_core.core.domain.processing import fold_event_stream_entries
-from tradingchassis_core.core.domain.processing_order import EventStreamEntry, ProcessingPosition
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- ControlTimeEvent,
- FillEvent,
- MarketEvent,
- OrderStateEvent,
- OrderSubmittedEvent,
- Price,
- Quantity,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-
-
-def _book_market_event(
- *,
- instrument: str,
- ts_ns_local: int,
- ts_ns_exch: int,
- best_bid: float,
- best_ask: float,
-) -> MarketEvent:
- return MarketEvent(
- ts_ns_local=ts_ns_local,
- ts_ns_exch=ts_ns_exch,
- instrument=instrument,
- event_type="book",
- book={
- "book_type": "snapshot",
- "bids": [
- {
- "price": {"currency": "USDC", "value": best_bid},
- "quantity": {"unit": "contracts", "value": 2.0},
- }
- ],
- "asks": [
- {
- "price": {"currency": "USDC", "value": best_ask},
- "quantity": {"unit": "contracts", "value": 3.0},
- }
- ],
- "depth": 1,
- },
- trade=None,
- )
-
-
-def _fill_event(
- *,
- instrument: str,
- client_order_id: str,
- ts_ns_local: int,
- ts_ns_exch: int,
- cum_filled_qty: float,
-) -> FillEvent:
- return FillEvent(
- ts_ns_local=ts_ns_local,
- ts_ns_exch=ts_ns_exch,
- instrument=instrument,
- client_order_id=client_order_id,
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=Price(currency="USDC", value=100.5),
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=Quantity(unit="contracts", value=cum_filled_qty),
- remaining_qty=Quantity(unit="contracts", value=max(0.0, 1.0 - cum_filled_qty)),
- time_in_force="GTC",
- liquidity_flag="maker",
- fee=None,
- )
-
-
-def _order_submitted_event(
- *,
- instrument: str,
- client_order_id: str,
- ts_ns_local_dispatch: int,
-) -> OrderSubmittedEvent:
- return OrderSubmittedEvent(
- ts_ns_local_dispatch=ts_ns_local_dispatch,
- instrument=instrument,
- client_order_id=client_order_id,
- side="buy",
- order_type="limit",
- intended_price=Price(currency="USDC", value=100.0),
- intended_qty=Quantity(unit="contracts", value=1.0),
- time_in_force="GTC",
- intent_correlation_id="corr-1",
- dispatch_attempt_id="attempt-1",
- runtime_correlation={"engine": "backtest", "seq": 1},
- )
-
-
-def _control_time_event(
- *,
- ts_ns_local_control: int,
- due_ts_ns_local: int | None = None,
- realized_ts_ns_local: int | None = None,
-) -> ControlTimeEvent:
- return ControlTimeEvent(
- ts_ns_local_control=ts_ns_local_control,
- reason="rate_limit_recheck",
- due_ts_ns_local=due_ts_ns_local,
- realized_ts_ns_local=realized_ts_ns_local,
- obligation_reason="rate_limit",
- obligation_due_ts_ns_local=due_ts_ns_local,
- runtime_correlation={"engine": "backtest", "seq": 1},
- )
-
-
-def _order_state_event(*, instrument: str, client_order_id: str) -> OrderStateEvent:
- return OrderStateEvent(
- ts_ns_local=300,
- ts_ns_exch=290,
- instrument=instrument,
- client_order_id=client_order_id,
- order_type="limit",
- state_type="accepted",
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=None,
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=None,
- remaining_qty=None,
- time_in_force="GTC",
- reason=None,
- raw={"req": 0, "source": "snapshot"},
- )
-
-
-def _state_subset_snapshot(state: StrategyState) -> dict[str, object]:
- return {
- "market": copy.deepcopy(state.market),
- "fills": copy.deepcopy(state.fills),
- "fill_cum_qty": copy.deepcopy(state.fill_cum_qty),
- "processing_position": state._last_processing_position_index,
- }
-
-
-def _entry(position: int, event: object) -> EventStreamEntry:
- return EventStreamEntry(position=ProcessingPosition(index=position), event=event)
-
-
-def _market_configuration(
- *,
- instrument: str = "BTC-USDC-PERP",
- tick_size: float = 0.1,
- lot_size: float = 0.01,
- contract_size: float = 1.0,
- version: str = "v1",
-) -> CoreConfiguration:
- return CoreConfiguration(
- version=version,
- payload={
- "market": {
- "instruments": {
- instrument: {
- "tick_size": tick_size,
- "lot_size": lot_size,
- "contract_size": contract_size,
- }
- }
- }
- },
- )
-
-
-def test_fold_same_entries_same_configuration_produces_equivalent_final_state() -> None:
- entries = [
- _entry(
- 0,
- _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=200,
- ts_ns_exch=190,
- best_bid=100.0,
- best_ask=101.0,
- ),
- ),
- _entry(
- 1,
- _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=201,
- ts_ns_exch=191,
- cum_filled_qty=0.25,
- ),
- ),
- ]
- configuration = _market_configuration()
-
- left = StrategyState(event_bus=NullEventBus())
- right = StrategyState(event_bus=NullEventBus())
-
- fold_event_stream_entries(left, entries, configuration=configuration)
- fold_event_stream_entries(right, entries, configuration=configuration)
-
- assert _state_subset_snapshot(left) == _state_subset_snapshot(right)
-
-
-def test_fold_uses_single_explicit_configuration_input_with_stable_identity() -> None:
- """Phase 2B guardrail: one fold call has one explicit CoreConfiguration input."""
- entries = [
- _entry(
- 0,
- _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=200,
- ts_ns_exch=190,
- best_bid=100.0,
- best_ask=101.0,
- ),
- ),
- _entry(
- 1,
- _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=201,
- ts_ns_exch=191,
- cum_filled_qty=0.25,
- ),
- ),
- ]
- cfg_v1_left = _market_configuration(version="v1")
- cfg_v1_right = _market_configuration(version="v1")
-
- left = StrategyState(event_bus=NullEventBus())
- right = StrategyState(event_bus=NullEventBus())
-
- fold_event_stream_entries(left, entries, configuration=cfg_v1_left)
- fold_event_stream_entries(right, entries, configuration=cfg_v1_right)
-
- assert cfg_v1_left.fingerprint == cfg_v1_right.fingerprint
- assert _state_subset_snapshot(left) == _state_subset_snapshot(right)
-
-
-def test_fold_same_prefix_produces_equivalent_prefix_state() -> None:
- entries = [
- _entry(
- 0,
- _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=200,
- ts_ns_exch=190,
- best_bid=100.0,
- best_ask=101.0,
- ),
- ),
- _entry(
- 1,
- _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=100,
- ts_ns_exch=95,
- best_bid=120.0,
- best_ask=121.0,
- ),
- ),
- _entry(
- 2,
- _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=202,
- ts_ns_exch=192,
- cum_filled_qty=0.25,
- ),
- ),
- ]
- configuration = _market_configuration()
-
- left = StrategyState(event_bus=NullEventBus())
- right = StrategyState(event_bus=NullEventBus())
-
- fold_event_stream_entries(left, entries[:2], configuration=configuration)
- fold_event_stream_entries(right, entries[:2], configuration=configuration)
-
- assert _state_subset_snapshot(left) == _state_subset_snapshot(right)
-
-
-def test_fold_repeated_or_regressing_processing_position_raises_deterministically() -> None:
- repeated_state = StrategyState(event_bus=NullEventBus())
- repeated_entries = [
- _entry(
- 10,
- _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=200,
- ts_ns_exch=190,
- best_bid=100.0,
- best_ask=101.0,
- ),
- ),
- _entry(
- 10,
- _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=201,
- ts_ns_exch=191,
- cum_filled_qty=0.25,
- ),
- ),
- ]
-
- with pytest.raises(ValueError, match="Non-monotonic ProcessingPosition index"):
- fold_event_stream_entries(repeated_state, repeated_entries, configuration=_market_configuration())
-
- regressing_state = StrategyState(event_bus=NullEventBus())
- regressing_entries = [
- _entry(
- 11,
- _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=200,
- ts_ns_exch=190,
- best_bid=100.0,
- best_ask=101.0,
- ),
- ),
- _entry(
- 9,
- _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=202,
- ts_ns_exch=192,
- cum_filled_qty=0.50,
- ),
- ),
- ]
-
- with pytest.raises(ValueError, match="Non-monotonic ProcessingPosition index"):
- fold_event_stream_entries(regressing_state, regressing_entries, configuration=_market_configuration())
-
-
-def test_fold_positioned_market_ordering_follows_processing_position_not_event_time() -> None:
- state = StrategyState(event_bus=NullEventBus())
- entries = [
- _entry(
- 1,
- _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=200,
- ts_ns_exch=190,
- best_bid=100.0,
- best_ask=101.0,
- ),
- ),
- _entry(
- 2,
- _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=100,
- ts_ns_exch=95,
- best_bid=120.0,
- best_ask=121.0,
- ),
- ),
- ]
-
- fold_event_stream_entries(state, entries, configuration=_market_configuration())
-
- market = state.market["BTC-USDC-PERP"]
- assert market.best_bid == 120.0
- assert market.best_ask == 121.0
- assert market.last_ts_ns_local == 100
- assert market.last_ts_ns_exch == 95
- assert state._last_processing_position_index == 2
-
-
-def test_fold_interleaved_market_submitted_control_uses_single_global_cursor() -> None:
- state = StrategyState(event_bus=NullEventBus())
- entries = [
- _entry(
- 0,
- _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=200,
- ts_ns_exch=190,
- best_bid=100.0,
- best_ask=101.0,
- ),
- ),
- _entry(
- 1,
- _order_submitted_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-submitted-1",
- ts_ns_local_dispatch=205,
- ),
- ),
- _entry(
- 2,
- _control_time_event(
- ts_ns_local_control=206,
- due_ts_ns_local=210,
- ),
- ),
- ]
-
- fold_event_stream_entries(state, entries, configuration=_market_configuration())
-
- assert state._last_processing_position_index == 2
- projection = state.canonical_orders[("BTC-USDC-PERP", "order-submitted-1")]
- assert projection.state == "submitted"
-
-
-def test_fold_fill_event_cumulative_idempotence_remains_unchanged() -> None:
- state = StrategyState(event_bus=NullEventBus())
- entries = [
- _entry(
- 20,
- _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=400,
- ts_ns_exch=390,
- cum_filled_qty=0.25,
- ),
- ),
- _entry(
- 21,
- _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=401,
- ts_ns_exch=391,
- cum_filled_qty=0.25,
- ),
- ),
- _entry(
- 22,
- _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=402,
- ts_ns_exch=392,
- cum_filled_qty=0.20,
- ),
- ),
- ]
-
- fold_event_stream_entries(state, entries)
-
- assert len(state.fills["BTC-USDC-PERP"]) == 1
- assert state.fill_cum_qty["BTC-USDC-PERP"]["order-1"] == 0.25
- assert state._last_processing_position_index == 22
-
-
-def test_fold_rejects_non_canonical_entry_payload_via_existing_boundary() -> None:
- state = StrategyState(event_bus=NullEventBus())
- entries = [
- _entry(
- 1,
- _order_state_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-compat-1",
- ),
- )
- ]
-
- with pytest.raises(TypeError, match="Unsupported non-canonical event type"):
- fold_event_stream_entries(state, entries)
-
-
-def test_fold_returns_same_state_object_for_ergonomics() -> None:
- state = StrategyState(event_bus=NullEventBus())
- entries = [
- _entry(
- 0,
- _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=200,
- ts_ns_exch=190,
- best_bid=100.0,
- best_ask=101.0,
- ),
- )
- ]
-
- configuration = _market_configuration()
- returned = fold_event_stream_entries(state, entries, configuration=configuration)
-
- assert returned is state
-
-
-def test_fold_rejects_non_core_configuration() -> None:
- state = StrategyState(event_bus=NullEventBus())
- entries = [
- _entry(
- 0,
- _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=200,
- ts_ns_exch=190,
- best_bid=100.0,
- best_ask=101.0,
- ),
- )
- ]
-
- with pytest.raises(TypeError, match="configuration must be CoreConfiguration or None"):
- fold_event_stream_entries(state, entries, configuration={"version": "v1"})
-
-
-def test_fold_different_market_configuration_values_produce_different_market_metadata() -> None:
- entries = [
- _entry(
- 0,
- _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=200,
- ts_ns_exch=190,
- best_bid=100.0,
- best_ask=101.0,
- ),
- ),
- _entry(
- 1,
- _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=201,
- ts_ns_exch=191,
- cum_filled_qty=0.25,
- ),
- ),
- ]
- configuration_a = _market_configuration(
- tick_size=0.1,
- lot_size=0.01,
- contract_size=1.0,
- version="v1",
- )
- configuration_b = _market_configuration(
- tick_size=0.5,
- lot_size=0.05,
- contract_size=2.0,
- version="v2",
- )
-
- left = StrategyState(event_bus=NullEventBus())
- right = StrategyState(event_bus=NullEventBus())
-
- fold_event_stream_entries(left, entries, configuration=configuration_a)
- fold_event_stream_entries(right, entries, configuration=configuration_b)
-
- assert configuration_a.fingerprint != configuration_b.fingerprint
- assert _state_subset_snapshot(left) != _state_subset_snapshot(right)
- left_market = left.market["BTC-USDC-PERP"]
- right_market = right.market["BTC-USDC-PERP"]
- assert (left_market.tick_size, left_market.lot_size, left_market.contract_size) == (
- 0.1,
- 0.01,
- 1.0,
- )
- assert (right_market.tick_size, right_market.lot_size, right_market.contract_size) == (
- 0.5,
- 0.05,
- 2.0,
- )
-
-
-def test_fold_configuration_identity_stays_stable_after_source_payload_mutation() -> None:
- """Transitional guardrail: configuration identity remains stable during fold."""
- entries = [
- _entry(
- 0,
- _book_market_event(
- instrument="BTC-USDC-PERP",
- ts_ns_local=200,
- ts_ns_exch=190,
- best_bid=100.0,
- best_ask=101.0,
- ),
- ),
- _entry(
- 1,
- _fill_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- ts_ns_local=201,
- ts_ns_exch=191,
- cum_filled_qty=0.25,
- ),
- ),
- ]
- source_payload = {
- "market": {
- "instruments": {
- "BTC-USDC-PERP": {
- "tick_size": 0.1,
- "lot_size": 0.01,
- "contract_size": 1.0,
- }
- }
- }
- }
- configuration = CoreConfiguration(version="v1", payload=source_payload)
- fingerprint_before = configuration.fingerprint
- payload_before = configuration.payload
-
- source_payload["market"]["instruments"]["BTC-USDC-PERP"]["tick_size"] = 0.5
- source_payload["market"]["instruments"]["BTC-USDC-PERP"]["lot_size"] = 0.5
- source_payload["market"]["instruments"]["BTC-USDC-PERP"]["contract_size"] = 5.0
-
- left = StrategyState(event_bus=NullEventBus())
- right = StrategyState(event_bus=NullEventBus())
-
- fold_event_stream_entries(left, entries, configuration=configuration)
- fold_event_stream_entries(right, entries, configuration=configuration)
-
- source_payload["market"]["instruments"]["BTC-USDC-PERP"]["tick_size"] = 99.0
-
- assert configuration.fingerprint == fingerprint_before
- assert configuration.payload == payload_before
- assert _state_subset_snapshot(left) == _state_subset_snapshot(right)
diff --git a/tests/semantics/models/test_import_compatibility_shim.py b/tests/semantics/models/test_import_compatibility_shim.py
deleted file mode 100644
index 320e8bb..0000000
--- a/tests/semantics/models/test_import_compatibility_shim.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from __future__ import annotations
-
-import warnings
-
-
-def test_nested_modules_share_identity_across_import_sites() -> None:
- with warnings.catch_warnings():
- warnings.simplefilter("ignore", DeprecationWarning)
- import tradingchassis_core.core.domain.processing as old_processing
- import tradingchassis_core.core.domain.types as old_types
-
- import tradingchassis_core.core.domain.processing as new_processing
- import tradingchassis_core.core.domain.types as new_types
-
- assert old_types is new_types
- assert old_processing is new_processing
-
-
-def test_symbols_share_identity_across_import_sites() -> None:
- with warnings.catch_warnings():
- warnings.simplefilter("ignore", DeprecationWarning)
- from tradingchassis_core.core.domain.configuration import (
- CoreConfiguration as OldCoreConfiguration,
- )
- from tradingchassis_core.core.domain.types import (
- ControlTimeEvent as OldControlTimeEvent,
- )
- from tradingchassis_core.core.domain.types import (
- MarketEvent as OldMarketEvent,
- )
- from tradingchassis_core.core.domain.types import (
- OrderSubmittedEvent as OldOrderSubmittedEvent,
- )
-
- from tradingchassis_core.core.domain.configuration import (
- CoreConfiguration as NewCoreConfiguration,
- )
- from tradingchassis_core.core.domain.types import (
- ControlTimeEvent as NewControlTimeEvent,
- )
- from tradingchassis_core.core.domain.types import (
- MarketEvent as NewMarketEvent,
- )
- from tradingchassis_core.core.domain.types import (
- OrderSubmittedEvent as NewOrderSubmittedEvent,
- )
-
- assert OldMarketEvent is NewMarketEvent
- assert OldOrderSubmittedEvent is NewOrderSubmittedEvent
- assert OldControlTimeEvent is NewControlTimeEvent
- assert OldCoreConfiguration is NewCoreConfiguration
diff --git a/tests/semantics/models/test_market_configuration_positioned_contract.py b/tests/semantics/models/test_market_configuration_positioned_contract.py
deleted file mode 100644
index 8e1b6ac..0000000
--- a/tests/semantics/models/test_market_configuration_positioned_contract.py
+++ /dev/null
@@ -1,398 +0,0 @@
-"""Semantics contract matrix for positioned MarketEvent configuration consumption.
-
-Phase 3A.3 treats this module as the primary guardrail reference for the
-CoreConfiguration -> positioned canonical MarketEvent contract.
-"""
-
-from __future__ import annotations
-
-import ast
-import copy
-from pathlib import Path
-
-import pytest
-
-from tradingchassis_core.core.domain.configuration import CoreConfiguration
-from tradingchassis_core.core.domain.processing import (
- fold_event_stream_entries,
- process_canonical_event,
- 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.types import (
- FillEvent,
- MarketEvent,
- OrderStateEvent,
- Price,
- Quantity,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-
-
-def _book_market_event(
- *,
- instrument: str = "BTC-USDC-PERP",
- ts_ns_local: int = 100,
- ts_ns_exch: int = 90,
- best_bid: float = 100.0,
- best_ask: float = 101.0,
-) -> MarketEvent:
- return MarketEvent(
- ts_ns_local=ts_ns_local,
- ts_ns_exch=ts_ns_exch,
- instrument=instrument,
- event_type="book",
- book={
- "book_type": "snapshot",
- "bids": [
- {
- "price": {"currency": "USDC", "value": best_bid},
- "quantity": {"unit": "contracts", "value": 2.0},
- }
- ],
- "asks": [
- {
- "price": {"currency": "USDC", "value": best_ask},
- "quantity": {"unit": "contracts", "value": 3.0},
- }
- ],
- "depth": 1,
- },
- trade=None,
- )
-
-
-def _fill_event(*, instrument: str = "BTC-USDC-PERP", cum_qty: float = 0.25) -> FillEvent:
- return FillEvent(
- ts_ns_local=200,
- ts_ns_exch=190,
- instrument=instrument,
- client_order_id="order-1",
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=Price(currency="USDC", value=100.5),
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=Quantity(unit="contracts", value=cum_qty),
- remaining_qty=Quantity(unit="contracts", value=max(0.0, 1.0 - cum_qty)),
- time_in_force="GTC",
- liquidity_flag="maker",
- fee=None,
- )
-
-
-def _order_state_event() -> OrderStateEvent:
- return OrderStateEvent(
- ts_ns_local=300,
- ts_ns_exch=290,
- instrument="BTC-USDC-PERP",
- client_order_id="order-compat-1",
- order_type="limit",
- state_type="accepted",
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=None,
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=None,
- remaining_qty=None,
- time_in_force="GTC",
- reason=None,
- raw={"req": 0, "source": "snapshot"},
- )
-
-
-def _market_configuration(*, instrument: str = "BTC-USDC-PERP", tick: object = 0.1, lot: object = 0.01, contract: object = 1.0) -> CoreConfiguration:
- return CoreConfiguration(
- version="v1",
- payload={
- "market": {
- "instruments": {
- instrument: {
- "tick_size": tick,
- "lot_size": lot,
- "contract_size": contract,
- }
- }
- }
- },
- )
-
-
-def _entry(position: int, event: object) -> EventStreamEntry:
- return EventStreamEntry(position=ProcessingPosition(index=position), event=event)
-
-
-def _market_and_cursor_snapshot(state: StrategyState) -> tuple[dict[str, object], int | None]:
- return copy.deepcopy(state.market), state._last_processing_position_index
-
-
-def test_fold_positioned_market_requires_configuration_when_none() -> None:
- state = StrategyState(event_bus=NullEventBus())
- entries = [_entry(0, _book_market_event())]
-
- with pytest.raises(
- ValueError,
- match="CoreConfiguration is required for positioned canonical MarketEvent processing",
- ):
- fold_event_stream_entries(state, entries, configuration=None)
-
-
-def test_process_event_entry_missing_market_raises() -> None:
- state = StrategyState(event_bus=NullEventBus())
- entry = _entry(0, _book_market_event())
- cfg = CoreConfiguration(version="v1", payload={"not_market": {}})
-
- with pytest.raises(ValueError, match="payload.market"):
- process_event_entry(state, entry, configuration=cfg)
-
-
-def test_process_event_entry_missing_instruments_raises() -> None:
- state = StrategyState(event_bus=NullEventBus())
- entry = _entry(0, _book_market_event())
- cfg = CoreConfiguration(version="v1", payload={"market": {"not_instruments": {}}})
-
- with pytest.raises(ValueError, match="payload.market.instruments"):
- process_event_entry(state, entry, configuration=cfg)
-
-
-def test_process_event_entry_missing_instrument_entry_raises() -> None:
- state = StrategyState(event_bus=NullEventBus())
- entry = _entry(0, _book_market_event(instrument="BTC-USDC-PERP"))
- cfg = _market_configuration(instrument="ETH-USDC-PERP")
-
- with pytest.raises(ValueError, match="payload.market.instruments.BTC-USDC-PERP"):
- process_event_entry(state, entry, configuration=cfg)
-
-
-@pytest.mark.parametrize(
- ("payload", "expected"),
- [
- ({"lot_size": 0.01, "contract_size": 1.0}, "tick_size"),
- ({"tick_size": 0.1, "contract_size": 1.0}, "lot_size"),
- ({"tick_size": 0.1, "lot_size": 0.01}, "contract_size"),
- ],
-)
-def test_process_event_entry_missing_required_field_raises(
- payload: dict[str, object],
- expected: str,
-) -> None:
- state = StrategyState(event_bus=NullEventBus())
- entry = _entry(0, _book_market_event())
- cfg = CoreConfiguration(
- version="v1",
- payload={"market": {"instruments": {"BTC-USDC-PERP": payload}}},
- )
-
- with pytest.raises(ValueError, match=expected):
- process_event_entry(state, entry, configuration=cfg)
-
-
-@pytest.mark.parametrize("field_name", ["tick_size", "lot_size", "contract_size"])
-def test_process_event_entry_none_field_raises(field_name: str) -> None:
- state = StrategyState(event_bus=NullEventBus())
- entry = _entry(0, _book_market_event())
- payload = {"tick_size": 0.1, "lot_size": 0.01, "contract_size": 1.0}
- payload[field_name] = None
- cfg = CoreConfiguration(
- version="v1",
- payload={"market": {"instruments": {"BTC-USDC-PERP": payload}}},
- )
-
- with pytest.raises(ValueError, match=field_name):
- process_event_entry(state, entry, configuration=cfg)
-
-
-@pytest.mark.parametrize("field_name", ["tick_size", "lot_size", "contract_size"])
-def test_process_event_entry_invalid_type_field_raises(field_name: str) -> None:
- state = StrategyState(event_bus=NullEventBus())
- entry = _entry(0, _book_market_event())
- payload = {"tick_size": 0.1, "lot_size": 0.01, "contract_size": 1.0}
- payload[field_name] = "invalid"
- cfg = CoreConfiguration(
- version="v1",
- payload={"market": {"instruments": {"BTC-USDC-PERP": payload}}},
- )
-
- with pytest.raises(TypeError, match="must be numeric"):
- process_event_entry(state, entry, configuration=cfg)
-
-
-@pytest.mark.parametrize("field_name", ["tick_size", "lot_size", "contract_size"])
-def test_process_event_entry_bool_field_raises(field_name: str) -> None:
- state = StrategyState(event_bus=NullEventBus())
- entry = _entry(0, _book_market_event())
- payload = {"tick_size": 0.1, "lot_size": 0.01, "contract_size": 1.0}
- payload[field_name] = True
- cfg = CoreConfiguration(
- version="v1",
- payload={"market": {"instruments": {"BTC-USDC-PERP": payload}}},
- )
-
- with pytest.raises(TypeError, match="must be numeric"):
- process_event_entry(state, entry, configuration=cfg)
-
-
-@pytest.mark.parametrize("field_name", ["tick_size", "lot_size", "contract_size"])
-@pytest.mark.parametrize("value", [0.0, -1.0])
-def test_process_event_entry_non_positive_field_raises(field_name: str, value: float) -> None:
- state = StrategyState(event_bus=NullEventBus())
- entry = _entry(0, _book_market_event())
- payload = {"tick_size": 0.1, "lot_size": 0.01, "contract_size": 1.0}
- payload[field_name] = value
- cfg = CoreConfiguration(
- version="v1",
- payload={"market": {"instruments": {"BTC-USDC-PERP": payload}}},
- )
-
- with pytest.raises(ValueError, match="must be > 0"):
- process_event_entry(state, entry, configuration=cfg)
-
-
-@pytest.mark.parametrize("bad", [float("nan"), float("inf"), float("-inf")])
-def test_non_finite_market_metadata_rejected_by_core_configuration_validation(bad: float) -> None:
- with pytest.raises(ValueError, match="non-finite float"):
- _market_configuration(tick=bad)
-
-
-def test_positioned_market_failure_does_not_mutate_market_or_cursor() -> None:
- state = StrategyState(event_bus=NullEventBus())
- seed_entry = _entry(1, _book_market_event(ts_ns_local=100, ts_ns_exch=90))
- bad_entry = _entry(2, _book_market_event(ts_ns_local=101, ts_ns_exch=91))
- good_cfg = _market_configuration()
- bad_cfg = CoreConfiguration(
- version="v1",
- payload={"market": {"instruments": {"BTC-USDC-PERP": {"tick_size": 0.1}}}},
- )
-
- process_event_entry(state, seed_entry, configuration=good_cfg)
- before_market, before_cursor = _market_and_cursor_snapshot(state)
-
- with pytest.raises(ValueError):
- process_event_entry(state, bad_entry, configuration=bad_cfg)
-
- after_market, after_cursor = _market_and_cursor_snapshot(state)
- assert after_market == before_market
- assert after_cursor == before_cursor
-
-
-def test_same_positioned_market_stream_semantically_equivalent_configuration_equivalent_state() -> None:
- left = StrategyState(event_bus=NullEventBus())
- right = StrategyState(event_bus=NullEventBus())
- entries = [
- _entry(0, _book_market_event(ts_ns_local=100, ts_ns_exch=90)),
- _entry(1, _book_market_event(ts_ns_local=101, ts_ns_exch=91, best_bid=102.0, best_ask=103.0)),
- ]
- cfg_left = _market_configuration(tick=0.1, lot=0.01, contract=1)
- cfg_right = _market_configuration(tick=0.1, lot=0.01, contract=1.0)
-
- fold_event_stream_entries(left, entries, configuration=cfg_left)
- fold_event_stream_entries(right, entries, configuration=cfg_right)
-
- assert left.market == right.market
-
-
-def test_direct_update_market_compatibility_path_unchanged() -> None:
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_market(
- instrument="BTC-USDC-PERP",
- best_bid=100.0,
- best_ask=101.0,
- best_bid_qty=2.0,
- best_ask_qty=3.0,
- tick_size=0.0,
- lot_size=0.0,
- contract_size=1.0,
- ts_ns_local=200,
- ts_ns_exch=190,
- )
- state.update_market(
- instrument="BTC-USDC-PERP",
- best_bid=120.0,
- best_ask=121.0,
- best_bid_qty=2.0,
- best_ask_qty=3.0,
- tick_size=0.0,
- lot_size=0.0,
- contract_size=1.0,
- ts_ns_local=100,
- ts_ns_exch=95,
- )
-
- market = state.market["BTC-USDC-PERP"]
- assert market.best_bid == 100.0
- assert market.best_ask == 101.0
- assert market.last_ts_ns_local == 200
- assert market.last_ts_ns_exch == 190
-
-
-def test_unpositioned_market_compatibility_path_unchanged() -> None:
- state = StrategyState(event_bus=NullEventBus())
- first = _book_market_event(ts_ns_local=200, ts_ns_exch=190, best_bid=100.0, best_ask=101.0)
- second = _book_market_event(ts_ns_local=100, ts_ns_exch=95, best_bid=120.0, best_ask=121.0)
- cfg = _market_configuration(tick=0.5, lot=0.5, contract=5.0)
-
- process_canonical_event(state, first, position=None, configuration=cfg)
- process_canonical_event(state, second, position=None, configuration=cfg)
-
- market = state.market["BTC-USDC-PERP"]
- assert market.best_bid == 100.0
- assert market.best_ask == 101.0
- assert market.tick_size == 0.0
- assert market.lot_size == 0.0
- assert market.contract_size == 1.0
-
-
-def test_fill_event_behavior_remains_unchanged() -> None:
- state = StrategyState(event_bus=NullEventBus())
- first = _entry(10, _fill_event(cum_qty=0.25))
- duplicate = _entry(11, _fill_event(cum_qty=0.25))
- regressing = _entry(12, _fill_event(cum_qty=0.20))
-
- process_event_entry(state, first, configuration=None)
- fills_before = copy.deepcopy(state.fills)
- cum_before = copy.deepcopy(state.fill_cum_qty)
- process_event_entry(state, duplicate, configuration=None)
- process_event_entry(state, regressing, configuration=None)
-
- assert state.fills == fills_before
- assert state.fill_cum_qty == cum_before
- assert state._last_processing_position_index == 12
-
-
-def test_order_state_event_remains_compatibility_only() -> None:
- state = StrategyState(event_bus=NullEventBus())
- entry = _entry(0, _order_state_event())
-
- with pytest.raises(TypeError, match="Unsupported non-canonical event type"):
- process_event_entry(state, entry, configuration=_market_configuration())
-
-
-def test_positioned_market_contract_does_not_import_runtime_configuration_mapping() -> None:
- """Guardrail: canonical market reducer contract stays CoreConfiguration-only."""
- repo_root = Path(__file__).resolve().parents[3]
- processing_path = repo_root / "tradingchassis_core/core/domain/processing.py"
- tree = ast.parse(processing_path.read_text(encoding="utf-8"), filename=str(processing_path))
-
- forbidden_modules = (
- "core_runtime",
- "trading_runtime",
- "hft_engine_config",
- "live_engine_config",
- )
- forbidden_symbols = {
- "HftEngineConfig",
- "LiveEngineConfig",
- "RiskConfig",
- }
-
- for node in ast.walk(tree):
- if isinstance(node, ast.Import):
- for alias in node.names:
- assert not alias.name.startswith(forbidden_modules)
- assert alias.name not in forbidden_symbols
- if isinstance(node, ast.ImportFrom):
- if node.module is not None:
- assert not node.module.startswith(forbidden_modules)
- for alias in node.names:
- assert alias.name not in forbidden_symbols
diff --git a/tests/semantics/models/test_market_reducer_positioned_target.py b/tests/semantics/models/test_market_reducer_positioned_target.py
deleted file mode 100644
index 94d2758..0000000
--- a/tests/semantics/models/test_market_reducer_positioned_target.py
+++ /dev/null
@@ -1,331 +0,0 @@
-"""Target tests for positioned MarketEvent reducer-ordering migration (Phase 2 / Slice 2A.3A).
-
-This file intentionally includes docs-aligned target tests that are expected-red
-until the production market reducer migrates from timestamp-compatibility
-ordering to ProcessingPosition-driven causal ordering for positioned canonical
-MarketEvents.
-"""
-
-from __future__ import annotations
-
-import copy
-
-import pytest
-
-from tradingchassis_core.core.domain.configuration import CoreConfiguration
-from tradingchassis_core.core.domain.processing import process_canonical_event
-from tradingchassis_core.core.domain.processing_order import ProcessingPosition
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- FillEvent,
- MarketEvent,
- OrderStateEvent,
- Price,
- Quantity,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-
-
-def _market_configuration(
- *,
- instrument: str = "BTC-USDC-PERP",
- tick_size: float = 0.1,
- lot_size: float = 0.01,
- contract_size: float = 1.0,
-) -> CoreConfiguration:
- return CoreConfiguration(
- version="v1",
- payload={
- "market": {
- "instruments": {
- instrument: {
- "tick_size": tick_size,
- "lot_size": lot_size,
- "contract_size": contract_size,
- }
- }
- }
- },
- )
-
-
-def _book_market_event(
- *,
- instrument: str,
- ts_ns_local: int,
- ts_ns_exch: int,
- best_bid: float,
- best_ask: float,
- best_bid_qty: float = 2.0,
- best_ask_qty: float = 3.0,
-) -> MarketEvent:
- return MarketEvent(
- ts_ns_local=ts_ns_local,
- ts_ns_exch=ts_ns_exch,
- instrument=instrument,
- event_type="book",
- book={
- "book_type": "snapshot",
- "bids": [
- {
- "price": {"currency": "USDC", "value": best_bid},
- "quantity": {"unit": "contracts", "value": best_bid_qty},
- }
- ],
- "asks": [
- {
- "price": {"currency": "USDC", "value": best_ask},
- "quantity": {"unit": "contracts", "value": best_ask_qty},
- }
- ],
- "depth": 1,
- },
- trade=None,
- )
-
-
-def _fill_event(
- *,
- instrument: str,
- client_order_id: str,
- ts_ns_local: int,
- ts_ns_exch: int,
- cum_qty: float,
-) -> FillEvent:
- return FillEvent(
- ts_ns_local=ts_ns_local,
- ts_ns_exch=ts_ns_exch,
- instrument=instrument,
- client_order_id=client_order_id,
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=Price(currency="USDC", value=100.5),
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=Quantity(unit="contracts", value=cum_qty),
- remaining_qty=Quantity(unit="contracts", value=max(0.0, 1.0 - cum_qty)),
- time_in_force="GTC",
- liquidity_flag="maker",
- fee=None,
- )
-
-
-def _order_state_event(*, instrument: str, client_order_id: str) -> OrderStateEvent:
- return OrderStateEvent(
- ts_ns_local=300,
- ts_ns_exch=290,
- instrument=instrument,
- client_order_id=client_order_id,
- order_type="limit",
- state_type="accepted",
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=None,
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=None,
- remaining_qty=None,
- time_in_force="GTC",
- reason=None,
- raw={"req": 0, "source": "snapshot"},
- )
-
-
-def test_target_positioned_market_lower_local_timestamp_still_advances_state() -> None:
- """TARGET (expected-red pre-migration): positioned MarketEvent follows ProcessingPosition causality."""
- instrument = "BTC-USDC-PERP"
- state = StrategyState(event_bus=NullEventBus())
-
- first = _book_market_event(
- instrument=instrument,
- ts_ns_local=200,
- ts_ns_exch=190,
- best_bid=100.0,
- best_ask=101.0,
- )
- older_local_second = _book_market_event(
- instrument=instrument,
- ts_ns_local=100,
- ts_ns_exch=95,
- best_bid=120.0,
- best_ask=121.0,
- )
-
- configuration = _market_configuration(instrument=instrument)
- process_canonical_event(
- state,
- first,
- position=ProcessingPosition(index=1),
- configuration=configuration,
- )
- process_canonical_event(
- state,
- older_local_second,
- position=ProcessingPosition(index=2),
- configuration=configuration,
- )
-
- market = state.market[instrument]
- assert state._last_processing_position_index == 2
- # Docs-aligned target: positioned acceptance implies reducer advancement.
- assert market.best_bid == 120.0
- assert market.best_ask == 121.0
- assert market.last_ts_ns_local == 100
- assert market.last_ts_ns_exch == 95
-
-
-def test_target_positioned_market_lower_exchange_timestamp_still_advances_state() -> None:
- """TARGET (expected-red pre-migration): exchange-time tie-break must not gate positioned events."""
- instrument = "BTC-USDC-PERP"
- state = StrategyState(event_bus=NullEventBus())
-
- base = _book_market_event(
- instrument=instrument,
- ts_ns_local=300,
- ts_ns_exch=200,
- best_bid=100.0,
- best_ask=101.0,
- )
- lower_exchange_second = _book_market_event(
- instrument=instrument,
- ts_ns_local=300,
- ts_ns_exch=199,
- best_bid=80.0,
- best_ask=81.0,
- )
-
- configuration = _market_configuration(instrument=instrument)
- process_canonical_event(
- state,
- base,
- position=ProcessingPosition(index=10),
- configuration=configuration,
- )
- process_canonical_event(
- state,
- lower_exchange_second,
- position=ProcessingPosition(index=11),
- configuration=configuration,
- )
-
- market = state.market[instrument]
- assert state._last_processing_position_index == 11
- # Docs-aligned target: ProcessingPosition is causal; event-time fields are metadata.
- assert market.best_bid == 80.0
- assert market.best_ask == 81.0
- assert market.last_ts_ns_local == 300
- assert market.last_ts_ns_exch == 199
-
-
-def test_migration_guard_unpositioned_canonical_market_keeps_timestamp_compatibility_behavior() -> None:
- instrument = "BTC-USDC-PERP"
- state = StrategyState(event_bus=NullEventBus())
-
- first = _book_market_event(
- instrument=instrument,
- ts_ns_local=200,
- ts_ns_exch=190,
- best_bid=100.0,
- best_ask=101.0,
- )
- second = _book_market_event(
- instrument=instrument,
- ts_ns_local=100,
- ts_ns_exch=95,
- best_bid=120.0,
- best_ask=121.0,
- )
-
- process_canonical_event(state, first, position=None)
- process_canonical_event(state, second, position=None)
-
- market = state.market[instrument]
- assert market.best_bid == 100.0
- assert market.best_ask == 101.0
- assert market.last_ts_ns_local == 200
- assert market.last_ts_ns_exch == 190
-
-
-def test_migration_guard_direct_update_market_keeps_timestamp_compatibility_behavior() -> None:
- instrument = "BTC-USDC-PERP"
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_market(
- instrument=instrument,
- best_bid=100.0,
- best_ask=101.0,
- best_bid_qty=2.0,
- best_ask_qty=3.0,
- tick_size=0.0,
- lot_size=0.0,
- contract_size=1.0,
- ts_ns_local=200,
- ts_ns_exch=190,
- )
- state.update_market(
- instrument=instrument,
- best_bid=120.0,
- best_ask=121.0,
- best_bid_qty=2.0,
- best_ask_qty=3.0,
- tick_size=0.0,
- lot_size=0.0,
- contract_size=1.0,
- ts_ns_local=100,
- ts_ns_exch=95,
- )
-
- market = state.market[instrument]
- assert market.best_bid == 100.0
- assert market.best_ask == 101.0
- assert market.last_ts_ns_local == 200
- assert market.last_ts_ns_exch == 190
-
-
-def test_migration_guard_fill_event_cumulative_idempotence_remains_unchanged() -> None:
- state = StrategyState(event_bus=NullEventBus())
- instrument = "BTC-USDC-PERP"
- order_id = "order-1"
-
- first = _fill_event(
- instrument=instrument,
- client_order_id=order_id,
- ts_ns_local=400,
- ts_ns_exch=390,
- cum_qty=0.25,
- )
- duplicate = _fill_event(
- instrument=instrument,
- client_order_id=order_id,
- ts_ns_local=401,
- ts_ns_exch=391,
- cum_qty=0.25,
- )
- regressing = _fill_event(
- instrument=instrument,
- client_order_id=order_id,
- ts_ns_local=402,
- ts_ns_exch=392,
- cum_qty=0.20,
- )
-
- process_canonical_event(state, first, position=ProcessingPosition(index=20))
- fills_before = copy.deepcopy(state.fills)
- fill_cum_before = copy.deepcopy(state.fill_cum_qty)
-
- process_canonical_event(state, duplicate, position=ProcessingPosition(index=21))
- process_canonical_event(state, regressing, position=ProcessingPosition(index=22))
-
- assert state.fills == fills_before
- assert state.fill_cum_qty == fill_cum_before
- assert len(state.fills[instrument]) == 1
- assert state.fill_cum_qty[instrument][order_id] == 0.25
-
-
-def test_migration_guard_order_state_event_remains_rejected_by_canonical_boundary() -> None:
- state = StrategyState(event_bus=NullEventBus())
- compat_event = _order_state_event(
- instrument="BTC-USDC-PERP",
- client_order_id="order-compat-1",
- )
-
- with pytest.raises(TypeError, match="Unsupported non-canonical event type"):
- process_canonical_event(state, compat_event, position=ProcessingPosition(index=1))
diff --git a/tests/semantics/models/test_models_against_schemas.py b/tests/semantics/models/test_models_against_schemas.py
deleted file mode 100644
index 6a44694..0000000
--- a/tests/semantics/models/test_models_against_schemas.py
+++ /dev/null
@@ -1,580 +0,0 @@
-"""Schema conformance tests for core Pydantic models.
-
-This test suite validates that core Pydantic models both accept valid inputs
-and reject invalid ones in strict alignment with their corresponding JSON
-Schemas. The tests are intentionally verbose and repetitive to ensure full
-coverage and explicit failure modes.
-"""
-
-# pylint: disable=line-too-long,missing-function-docstring
-# pylint: disable=redefined-outer-name,global-statement
-from __future__ import annotations
-
-import json
-from pathlib import Path
-from typing import Any, TypeVar
-
-import pytest
-from jsonschema import ValidationError as JsonSchemaValidationError
-from jsonschema import validate as jsonschema_validate
-from pydantic import TypeAdapter
-from pydantic import ValidationError as PydanticValidationError
-from referencing import Registry, Resource
-from referencing.jsonschema import DRAFT202012
-
-from tradingchassis_core.core.domain.types import (
- FillEvent,
- MarketEvent,
- OrderIntent,
- OrderStateEvent,
- RiskConstraints,
-)
-
-SCHEMA_REGISTRY = Registry()
-
-
-# ---------------------------------------------------------------------------
-# Helpers
-# ---------------------------------------------------------------------------
-
-def load_schema(name: str) -> dict:
- """
- Load JSON schema from project root.
- """
- global SCHEMA_REGISTRY
-
- root = Path(__file__).parent.parent.parent.parent
- name = "tradingchassis_core/core/schemas/" + name
- schema_path = root / name
-
- with schema_path.open("r", encoding="utf-8") as f:
- schema = json.load(f)
-
- schema_id = schema.get("$id")
- if isinstance(schema_id, str) and schema_id:
- resource = Resource.from_contents(schema, default_specification=DRAFT202012)
- SCHEMA_REGISTRY = SCHEMA_REGISTRY.with_resource(schema_id, resource)
-
- return schema
-
-
-def dump_for_jsonschema(model: Any) -> dict:
- """
- Dump a Pydantic model to a JSON-compatible dict for schema validation.
- Excludes None values so optional fields are omitted instead of null.
- """
- # All validated objects in this test suite are BaseModel instances.
- return model.model_dump(mode="json", exclude_none=True)
-
-
-T = TypeVar("T")
-
-
-def pydantic_validate(model_type: Any, data: dict[str, Any]) -> Any:
- """
- Validate input using Pydantic for both:
- - BaseModel subclasses (e.g., MarketEvent)
- - Discriminated unions (e.g., OrderIntent is an Annotated Union)
- """
- adapter = TypeAdapter(model_type)
- return adapter.validate_python(data)
-
-
-def assert_pydantic_then_schema_ok(model_type: Any, data: dict[str, Any], schema: dict[str, Any]) -> dict:
- """
- Validate with Pydantic first, then validate the dumped instance with JSON Schema.
- Returns the dumped instance.
- """
- obj = pydantic_validate(model_type, data)
- instance = dump_for_jsonschema(obj)
- jsonschema_validate(instance=instance, schema=schema, registry=SCHEMA_REGISTRY)
- return instance
-
-
-def assert_schema_invalid_but_pydantic_rejects(model_type: Any, data: dict[str, Any], schema: dict[str, Any]):
- """
- Ensures Pydantic is at least as strict as the JSON Schema for the given input.
- If schema rejects, Pydantic must reject too (otherwise model is too lax).
- """
- with pytest.raises(JsonSchemaValidationError):
- jsonschema_validate(instance=data, schema=schema, registry=SCHEMA_REGISTRY)
-
- with pytest.raises(PydanticValidationError):
- pydantic_validate(model_type, data)
-
-
-def mk_price(value: float, currency: str = "USD") -> dict[str, Any]:
- return {"currency": currency, "value": value}
-
-
-def mk_qty(value: float, unit: str = "contracts") -> dict[str, Any]:
- return {"value": value, "unit": unit}
-
-
-# ---------------------------------------------------------------------------
-# Fixtures
-# ---------------------------------------------------------------------------
-
-@pytest.fixture(scope="module", autouse=True)
-def _load_common_schema() -> None:
- load_schema("common.schema.json")
-
-
-@pytest.fixture(scope="module")
-def market_event_schema() -> dict:
- return load_schema("market_event.schema.json")
-
-
-@pytest.fixture(scope="module")
-def order_intent_schema() -> dict:
- return load_schema("order_intent.schema.json")
-
-
-@pytest.fixture(scope="module")
-def risk_constraints_schema() -> dict:
- return load_schema("risk_constraints.schema.json")
-
-
-@pytest.fixture(scope="module")
-def fill_event_schema() -> dict:
- return load_schema("fill_event.schema.json")
-
-
-@pytest.fixture(scope="module")
-def order_state_event_schema() -> dict:
- return load_schema("order_state_event.schema.json")
-
-
-# ---------------------------------------------------------------------------
-# MarketEvent
-# ---------------------------------------------------------------------------
-
-def make_book_event(**book_overrides) -> dict[str, Any]:
- book = {
- "book_type": "snapshot",
- "bids": [{"price": mk_price(100.0), "quantity": mk_qty(1.0)}],
- "asks": [{"price": mk_price(100.5), "quantity": mk_qty(1.5)}],
- }
- book.update(book_overrides)
- return {
- "ts_ns_local": 123456789,
- "ts_ns_exch": 123456089,
- "instrument": "BTC-USD",
- "event_type": "book",
- "book": book,
- }
-
-
-def make_trade_event(**trade_overrides) -> dict[str, Any]:
- trade = {
- "side": "buy",
- "price": mk_price(100.25),
- "quantity": mk_qty(0.5),
- }
- trade.update(trade_overrides)
- return {
- "ts_ns_local": 123456790,
- "ts_ns_exch": 123456789,
- "instrument": "BTC-USD",
- "event_type": "trade",
- "trade": trade,
- }
-
-
-def test_market_event_book_valid_minimal(market_event_schema):
- assert_pydantic_then_schema_ok(MarketEvent, make_book_event(), market_event_schema)
-
-
-def test_market_event_trade_valid_minimal(market_event_schema):
- assert_pydantic_then_schema_ok(MarketEvent, make_trade_event(trade_id="T123"), market_event_schema)
-
-
-def test_market_event_enforces_payload_presence():
- with pytest.raises(PydanticValidationError):
- pydantic_validate(
- MarketEvent,
- {"ts_ns_local": 2, "ts_ns_exch": 1, "instrument": "BTC-USD", "event_type": "book"},
- )
- with pytest.raises(PydanticValidationError):
- pydantic_validate(
- MarketEvent,
- {"ts_ns_local": 2, "ts_ns_exch": 1, "instrument": "BTC-USD", "event_type": "trade"},
- )
-
-
-def test_market_event_rejects_extra_top_level_fields(market_event_schema):
- data = make_book_event()
- data["unexpected"] = 123
-
- with pytest.raises(PydanticValidationError):
- pydantic_validate(MarketEvent, data)
-
- with pytest.raises(JsonSchemaValidationError):
- jsonschema_validate(instance=data, schema=market_event_schema, registry=SCHEMA_REGISTRY)
-
-
-def test_market_event_book_depth_optional_valid_values(market_event_schema):
- assert_pydantic_then_schema_ok(MarketEvent, make_book_event(depth=0), market_event_schema)
- assert_pydantic_then_schema_ok(MarketEvent, make_book_event(depth=1), market_event_schema)
-
-
-def test_market_event_book_depth_negative_rejected(market_event_schema):
- bad = make_book_event(depth=-1)
- assert_schema_invalid_but_pydantic_rejects(MarketEvent, bad, market_event_schema)
-
-
-def test_market_event_trade_id_min_length(market_event_schema):
- assert_pydantic_then_schema_ok(MarketEvent, make_trade_event(trade_id="X"), market_event_schema)
-
- bad = make_trade_event(trade_id="")
- assert_schema_invalid_but_pydantic_rejects(MarketEvent, bad, market_event_schema)
-
-
-def test_market_event_instrument_min_length(market_event_schema):
- bad = make_book_event()
- bad["instrument"] = ""
- assert_schema_invalid_but_pydantic_rejects(MarketEvent, bad, market_event_schema)
-
-
-def test_market_event_ts_ns_exclusive_minimum(market_event_schema):
- bad = make_book_event()
- bad["ts_ns_local"] = 0
- assert_schema_invalid_but_pydantic_rejects(MarketEvent, bad, market_event_schema)
-
-
-def test_market_event_xor_book_trade_enforced(market_event_schema):
- # Schema forbids having both book and trade.
- bad = make_book_event()
- bad["trade"] = make_trade_event()["trade"]
-
- with pytest.raises(JsonSchemaValidationError):
- jsonschema_validate(instance=bad, schema=market_event_schema, registry=SCHEMA_REGISTRY)
-
- with pytest.raises(PydanticValidationError):
- pydantic_validate(MarketEvent, bad)
-
-
-# ---------------------------------------------------------------------------
-# OrderIntent
-# ---------------------------------------------------------------------------
-
-def make_new_intent(**overrides) -> dict[str, Any]:
- """
- Build a valid minimal 'new' intent (limit by default).
- Note: intended_price is required for both limit and market orders in this system.
- """
- data: dict[str, Any] = {
- "ts_ns_local": 123456790,
- "client_order_id": "C-1",
- "instrument": "BTC-USD",
- "intent_type": "new",
- "order_type": "limit",
- "side": "buy",
- "intended_qty": mk_qty(1.0),
- "intended_price": mk_price(100.0),
- "time_in_force": "GTC",
- }
- data.update(overrides)
- return data
-
-
-def make_cancel_intent(**overrides) -> dict[str, Any]:
- """
- Build a valid minimal 'cancel' intent.
- """
- data: dict[str, Any] = {
- "ts_ns_local": 123456791,
- "client_order_id": "C-1",
- "instrument": "BTC-USD",
- "intent_type": "cancel",
- }
- data.update(overrides)
- return data
-
-
-def make_replace_intent(**overrides) -> dict[str, Any]:
- """
- Build a valid minimal 'replace' intent (limit-only).
- time_in_force is not allowed because it is not modifiable by the execution binding.
- """
- data: dict[str, Any] = {
- "ts_ns_local": 123456792,
- "client_order_id": "C-1",
- "instrument": "BTC-USD",
- "intent_type": "replace",
- "side": "buy",
- "order_type": "limit",
- "intended_qty": mk_qty(2.0),
- "intended_price": mk_price(101.0),
- }
- data.update(overrides)
- return data
-
-
-def test_order_intent_valid_new_limit_with_optional_correlation_id(order_intent_schema):
- data = make_new_intent(intents_correlation_id="CORR-1")
- assert_pydantic_then_schema_ok(OrderIntent, data, order_intent_schema)
-
-
-def test_order_intent_new_market_requires_intended_price(order_intent_schema):
- data = make_new_intent(order_type="market")
- assert_pydantic_then_schema_ok(OrderIntent, data, order_intent_schema)
-
- bad = make_new_intent(order_type="market")
- bad.pop("intended_price", None)
- assert_schema_invalid_but_pydantic_rejects(OrderIntent, bad, order_intent_schema)
-
-
-def test_order_intent_new_limit_requires_intended_price(order_intent_schema):
- bad = make_new_intent(order_type="limit")
- bad.pop("intended_price", None)
- assert_schema_invalid_but_pydantic_rejects(OrderIntent, bad, order_intent_schema)
-
-
-def test_order_intent_cancel_valid_minimal(order_intent_schema):
- assert_pydantic_then_schema_ok(OrderIntent, make_cancel_intent(), order_intent_schema)
-
-
-def test_order_intent_cancel_forbids_order_fields(order_intent_schema):
- # Cancel must not contain order-creation fields.
- bad = make_cancel_intent(side="buy")
- assert_schema_invalid_but_pydantic_rejects(OrderIntent, bad, order_intent_schema)
-
- bad = make_cancel_intent(order_type="limit")
- assert_schema_invalid_but_pydantic_rejects(OrderIntent, bad, order_intent_schema)
-
- bad = make_cancel_intent(intended_qty=1.0)
- assert_schema_invalid_but_pydantic_rejects(OrderIntent, bad, order_intent_schema)
-
- bad = make_cancel_intent(intended_price=100.0)
- assert_schema_invalid_but_pydantic_rejects(OrderIntent, bad, order_intent_schema)
-
- bad = make_cancel_intent(time_in_force="GTC")
- assert_schema_invalid_but_pydantic_rejects(OrderIntent, bad, order_intent_schema)
-
-
-def test_order_intent_replace_valid_minimal(order_intent_schema):
- assert_pydantic_then_schema_ok(OrderIntent, make_replace_intent(), order_intent_schema)
-
-
-def test_order_intent_replace_requires_limit(order_intent_schema):
- bad = make_replace_intent(order_type="market")
- assert_schema_invalid_but_pydantic_rejects(OrderIntent, bad, order_intent_schema)
-
-
-def test_order_intent_replace_forbids_time_in_force(order_intent_schema):
- # Replace must not contain time_in_force (not modifiable).
- bad = make_replace_intent(time_in_force="GTC")
- assert_schema_invalid_but_pydantic_rejects(OrderIntent, bad, order_intent_schema)
-
-
-def test_order_intent_min_length_optionals(order_intent_schema):
- bad_corr = make_new_intent(intents_correlation_id="")
- assert_schema_invalid_but_pydantic_rejects(OrderIntent, bad_corr, order_intent_schema)
-
-
-def test_order_intent_exclusive_minimum_constraints(order_intent_schema):
- bad_ts = make_new_intent(ts_ns_local=0)
- assert_schema_invalid_but_pydantic_rejects(OrderIntent, bad_ts, order_intent_schema)
-
- bad_price = make_new_intent(intended_price=mk_price(-1.0))
- assert_schema_invalid_but_pydantic_rejects(OrderIntent, bad_price, order_intent_schema)
-
- bad_qty = make_new_intent(intended_qty=mk_qty(-1.0))
- assert_schema_invalid_but_pydantic_rejects(OrderIntent, bad_qty, order_intent_schema)
-
-
-def test_order_intent_rejects_additional_properties(order_intent_schema):
- data = make_new_intent()
- data["unexpected"] = "x"
-
- with pytest.raises(PydanticValidationError):
- pydantic_validate(OrderIntent, data)
-
- with pytest.raises(JsonSchemaValidationError):
- jsonschema_validate(instance=data, schema=order_intent_schema, registry=SCHEMA_REGISTRY)
-
-
-# ---------------------------------------------------------------------------
-# RiskConstraints
-# ---------------------------------------------------------------------------
-
-def make_risk_constraints(**overrides) -> dict[str, Any]:
- data = {
- "ts_ns_local": 987654321,
- "scope": "BTC-USD:default",
- "trading_enabled": True,
- }
- data.update(overrides)
- return data
-
-
-def test_risk_constraints_valid_with_optionals(risk_constraints_schema):
- data = make_risk_constraints(
- notional_limits={
- "currency": "USD",
- "max_gross_notional": 1_000_000.0,
- "max_single_order_notional": 100_000.0,
- },
- position_limits={"currency": "BTC", "max_position": 10.0},
- quote_limits={
- "currency": "USD",
- "max_gross_quote_notional": 500_000.0,
- "max_net_quote_notional": 0.0,
- "max_active_quotes": 100,
- },
- order_rate_limits={"max_orders_per_second": 50.0, "max_cancels_per_second": 100.0},
- max_loss={"currency": "USD", "max_drawdown": -10_000.0, "rolling_loss": -1000.0, "rolling_loss_window": 60},
- extra={"desk": "alpha", "risk_mode": "conservative", "debug": None},
- )
- assert_pydantic_then_schema_ok(RiskConstraints, data, risk_constraints_schema)
-
-
-def test_risk_constraints_missing_optional_notional_limits_is_ok(risk_constraints_schema):
- data = make_risk_constraints()
- assert_pydantic_then_schema_ok(RiskConstraints, data, risk_constraints_schema)
-
-
-def test_risk_constraints_minimum_constraints(risk_constraints_schema):
- bad_pos = make_risk_constraints(position_limits={"currency": "BTC", "max_position": -1.0})
- assert_schema_invalid_but_pydantic_rejects(RiskConstraints, bad_pos, risk_constraints_schema)
-
- bad_notional = make_risk_constraints(notional_limits={"currency": "USD", "max_gross_notional": -1.0})
- assert_schema_invalid_but_pydantic_rejects(RiskConstraints, bad_notional, risk_constraints_schema)
-
- bad_quotes = make_risk_constraints(quote_limits={"currency": "USD", "max_gross_quote_notional": -1.0})
- assert_schema_invalid_but_pydantic_rejects(RiskConstraints, bad_quotes, risk_constraints_schema)
-
- bad_active = make_risk_constraints(quote_limits={"currency": "USD", "max_gross_quote_notional": 1.0, "max_active_quotes": -1})
- assert_schema_invalid_but_pydantic_rejects(RiskConstraints, bad_active, risk_constraints_schema)
-
- bad_rate = make_risk_constraints(order_rate_limits={"max_orders_per_second": -0.1})
- assert_schema_invalid_but_pydantic_rejects(RiskConstraints, bad_rate, risk_constraints_schema)
-
-
-def test_risk_constraints_rejects_additional_properties(risk_constraints_schema):
- data = make_risk_constraints()
- data["unexpected"] = 1
-
- with pytest.raises(PydanticValidationError):
- pydantic_validate(RiskConstraints, data)
-
- with pytest.raises(JsonSchemaValidationError):
- jsonschema_validate(instance=data, schema=risk_constraints_schema, registry=SCHEMA_REGISTRY)
-
-
-def test_risk_constraints_extra_values_are_schema_compatible(risk_constraints_schema):
- data = make_risk_constraints(extra={"ok": "x", "n": 1, "b": True, "z": None})
- assert_pydantic_then_schema_ok(RiskConstraints, data, risk_constraints_schema)
-
- bad = make_risk_constraints(extra={"nested": {"a": 1}})
- with pytest.raises(JsonSchemaValidationError):
- jsonschema_validate(instance=bad, schema=risk_constraints_schema, registry=SCHEMA_REGISTRY)
- # Pydantic will reject too because extra is typed as primitive union.
-
-
-# ---------------------------------------------------------------------------
-# FillEvent
-# ---------------------------------------------------------------------------
-
-def make_fill(**overrides) -> dict[str, Any]:
- data = {
- "ts_ns_local": 123456789,
- "ts_ns_exch": 123456089,
- "instrument": "BTC-USD",
- "client_order_id": "C-1",
- "side": "buy",
- "filled_price": mk_price(100.5),
- "cum_filled_qty": mk_qty(0.25),
- "time_in_force": "GTC",
- "liquidity_flag": "maker",
- }
- data.update(overrides)
- return data
-
-
-def test_fill_event_valid_with_all_optionals(fill_event_schema):
- data = make_fill(
- intended_price=mk_price(100.0),
- intended_qty=mk_qty(1.0),
- remaining_qty=mk_qty(0.75),
- fee={"currency": "USD", "amount": -0.1},
- )
- assert_pydantic_then_schema_ok(FillEvent, data, fill_event_schema)
-
-
-def test_fill_event_exclusive_minimum_constraints(fill_event_schema):
- bad_ts = make_fill(ts_ns_local=0)
- assert_schema_invalid_but_pydantic_rejects(FillEvent, bad_ts, fill_event_schema)
-
-
-def test_fill_event_min_length_optionals(fill_event_schema):
- bad_order_id = make_fill(client_order_id="")
- assert_schema_invalid_but_pydantic_rejects(FillEvent, bad_order_id, fill_event_schema)
-
-
-def test_fill_event_rejects_additional_properties(fill_event_schema):
- data = make_fill()
- data["unexpected"] = "x"
-
- with pytest.raises(PydanticValidationError):
- pydantic_validate(FillEvent, data)
-
- with pytest.raises(JsonSchemaValidationError):
- jsonschema_validate(instance=data, schema=fill_event_schema, registry=SCHEMA_REGISTRY)
-
-
-# ---------------------------------------------------------------------------
-# OrderStateEvent
-# ---------------------------------------------------------------------------
-
-def make_state(**overrides) -> dict[str, Any]:
- data = {
- "ts_ns_local": 123456789,
- "ts_ns_exch": 123456089,
- "instrument": "BTC-USD",
- "client_order_id": "C-1",
- "order_type": "limit",
- "state_type": "accepted",
- "side": "buy",
- "intended_price": mk_price(100.0),
- "intended_qty": mk_qty(1.0),
- "time_in_force": "GTC",
- }
- data.update(overrides)
- return data
-
-
-def test_order_state_event_valid_with_all_optionals(order_state_event_schema):
- data = make_state(
- filled_price=mk_price(100.1),
- cum_filled_qty=mk_qty(0.5),
- remaining_qty=mk_qty(0.5),
- reason="Partial fill",
- raw={"venue_status": "PARTIAL"},
- )
- assert_pydantic_then_schema_ok(OrderStateEvent, data, order_state_event_schema)
-
-
-def test_order_state_event_min_length_optionals(order_state_event_schema):
- bad_order_id = make_state(client_order_id="")
- assert_schema_invalid_but_pydantic_rejects(OrderStateEvent, bad_order_id, order_state_event_schema)
-
- bad_reason = make_state(reason="")
- assert_schema_invalid_but_pydantic_rejects(OrderStateEvent, bad_reason, order_state_event_schema)
-
-
-def test_order_state_event_exclusive_minimum_ts(order_state_event_schema):
- bad = make_state(ts_ns_local=0)
- assert_schema_invalid_but_pydantic_rejects(OrderStateEvent, bad, order_state_event_schema)
-
-
-def test_order_state_event_rejects_additional_properties(order_state_event_schema):
- data = make_state()
- data["unexpected"] = 1
-
- with pytest.raises(PydanticValidationError):
- pydantic_validate(OrderStateEvent, data)
-
- with pytest.raises(JsonSchemaValidationError):
- jsonschema_validate(instance=data, schema=order_state_event_schema, registry=SCHEMA_REGISTRY)
diff --git a/tests/semantics/models/test_processing_position_cursor_ownership_guard.py b/tests/semantics/models/test_processing_position_cursor_ownership_guard.py
deleted file mode 100644
index 429d3ae..0000000
--- a/tests/semantics/models/test_processing_position_cursor_ownership_guard.py
+++ /dev/null
@@ -1,125 +0,0 @@
-"""Architectural guard for ProcessingPosition cursor ownership."""
-
-from __future__ import annotations
-
-import ast
-from pathlib import Path
-
-_ALLOWED_CALLER = Path("tradingchassis_core/core/domain/processing.py")
-_ALLOWED_MUTATION_FILE = Path("tradingchassis_core/core/domain/state.py")
-_TARGET_METHOD = "_advance_processing_position"
-_TARGET_ATTR = "_last_processing_position_index"
-_POSITIONED_MARKET_TARGET_METHOD = "_update_market_from_positioned_canonical_event"
-
-
-def _iter_python_files(root: Path) -> list[Path]:
- return sorted(path for path in root.rglob("*.py") if path.is_file())
-
-
-def _find_target_method_calls(path: Path) -> list[tuple[int, int]]:
- tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
- calls: list[tuple[int, int]] = []
-
- for node in ast.walk(tree):
- if not isinstance(node, ast.Call):
- continue
- if not isinstance(node.func, ast.Attribute):
- continue
- if node.func.attr != _TARGET_METHOD:
- continue
- calls.append((node.lineno, node.col_offset))
-
- return calls
-
-
-def _find_positioned_market_target_method_calls(path: Path) -> list[tuple[int, int]]:
- tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
- calls: list[tuple[int, int]] = []
-
- for node in ast.walk(tree):
- if not isinstance(node, ast.Call):
- continue
- if not isinstance(node.func, ast.Attribute):
- continue
- if node.func.attr != _POSITIONED_MARKET_TARGET_METHOD:
- continue
- calls.append((node.lineno, node.col_offset))
-
- return calls
-
-
-def _find_target_attr_mutations(path: Path) -> list[tuple[int, int]]:
- tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
- writes: list[tuple[int, int]] = []
-
- for node in ast.walk(tree):
- if isinstance(node, ast.Assign):
- targets = node.targets
- elif isinstance(node, ast.AnnAssign):
- targets = [node.target]
- elif isinstance(node, ast.AugAssign):
- targets = [node.target]
- else:
- continue
-
- for target in targets:
- if isinstance(target, ast.Attribute) and target.attr == _TARGET_ATTR:
- writes.append((target.lineno, target.col_offset))
-
- return writes
-
-
-def test_processing_position_cursor_is_mutated_only_via_canonical_boundary() -> None:
- repo_root = Path(__file__).resolve().parents[3]
- production_root = repo_root / "tradingchassis_core"
-
- call_violations: list[str] = []
- mutation_violations: list[str] = []
-
- for file_path in _iter_python_files(production_root):
- relative_path = file_path.relative_to(repo_root)
-
- method_calls = _find_target_method_calls(file_path)
- if method_calls and relative_path != _ALLOWED_CALLER:
- for lineno, col in method_calls:
- call_violations.append(
- f"{relative_path}:{lineno}:{col} calls {_TARGET_METHOD}(...)"
- )
-
- attr_writes = _find_target_attr_mutations(file_path)
- if attr_writes and relative_path != _ALLOWED_MUTATION_FILE:
- for lineno, col in attr_writes:
- mutation_violations.append(
- f"{relative_path}:{lineno}:{col} writes {_TARGET_ATTR}"
- )
-
- assert not call_violations, (
- "Unexpected ProcessingPosition cursor helper calls outside canonical boundary:\n"
- + "\n".join(call_violations)
- )
- assert not mutation_violations, (
- "Unexpected ProcessingPosition cursor mutations outside StrategyState:\n"
- + "\n".join(mutation_violations)
- )
-
-
-def test_positioned_market_helper_is_called_only_via_canonical_boundary() -> None:
- repo_root = Path(__file__).resolve().parents[3]
- production_root = repo_root / "tradingchassis_core"
-
- call_violations: list[str] = []
-
- for file_path in _iter_python_files(production_root):
- relative_path = file_path.relative_to(repo_root)
- method_calls = _find_positioned_market_target_method_calls(file_path)
- if method_calls and relative_path != _ALLOWED_CALLER:
- for lineno, col in method_calls:
- call_violations.append(
- f"{relative_path}:{lineno}:{col} calls "
- f"{_POSITIONED_MARKET_TARGET_METHOD}(...)"
- )
-
- assert not call_violations, (
- "Unexpected positioned market helper calls outside canonical boundary:\n"
- + "\n".join(call_violations)
- )
diff --git a/tests/semantics/models/test_public_canonical_api_surface.py b/tests/semantics/models/test_public_canonical_api_surface.py
deleted file mode 100644
index 0244701..0000000
--- a/tests/semantics/models/test_public_canonical_api_surface.py
+++ /dev/null
@@ -1,50 +0,0 @@
-"""Public import-surface contract for canonical core processing APIs."""
-
-from __future__ import annotations
-
-import tradingchassis_core as tc
-from tradingchassis_core.core.domain.configuration import CoreConfiguration
-from tradingchassis_core.core.domain.processing import (
- fold_event_stream_entries,
- 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.types import ControlTimeEvent
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-
-
-def test_public_root_exposes_canonical_processing_symbols() -> None:
- assert hasattr(tc, "CoreConfiguration")
- assert hasattr(tc, "ProcessingPosition")
- assert hasattr(tc, "EventStreamEntry")
- assert hasattr(tc, "process_event_entry")
- assert hasattr(tc, "fold_event_stream_entries")
-
-
-def test_public_root_canonical_processing_symbols_have_identity_with_deep_implementations() -> None:
- assert tc.CoreConfiguration is CoreConfiguration
- assert tc.ProcessingPosition is ProcessingPosition
- assert tc.EventStreamEntry is EventStreamEntry
- assert tc.process_event_entry is process_event_entry
- assert tc.fold_event_stream_entries is fold_event_stream_entries
-
-
-def test_public_process_event_entry_smoke_for_non_market_canonical_event() -> None:
- state = StrategyState(event_bus=NullEventBus())
- entry = tc.EventStreamEntry(
- position=tc.ProcessingPosition(index=0),
- event=ControlTimeEvent(
- ts_ns_local_control=100,
- reason="rate_limit_recheck",
- due_ts_ns_local=110,
- realized_ts_ns_local=None,
- obligation_reason="rate_limit",
- obligation_due_ts_ns_local=110,
- runtime_correlation={"engine": "test", "seq": 1},
- ),
- )
-
- tc.process_event_entry(state, entry)
-
- assert state._last_processing_position_index == 0
diff --git a/tests/semantics/queue_semantics/test_control_scheduling_obligation_characterization.py b/tests/semantics/queue_semantics/test_control_scheduling_obligation_characterization.py
deleted file mode 100644
index e886c86..0000000
--- a/tests/semantics/queue_semantics/test_control_scheduling_obligation_characterization.py
+++ /dev/null
@@ -1,123 +0,0 @@
-"""Characterization tests for internal scheduling obligation mapping."""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- CancelOrderIntent,
- NewOrderIntent,
- NotionalLimits,
- OrderRateLimits,
- OrderStateEvent,
- Price,
- Quantity,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-from tradingchassis_core.core.execution_control import ExecutionControl
-from tradingchassis_core.core.risk.risk_config import RiskConfig
-from tradingchassis_core.core.risk.risk_engine import RiskEngine
-
-
-def test_rate_limited_mixed_intents_keep_minimum_next_send_timestamp() -> None:
- """Compatibility: next_send_ts remains the minimum blocked wake timestamp."""
-
- instrument = "BTC-USDC-PERP"
- new_client_order_id = "order-new"
- cancel_client_order_id = "order-cancel"
- state = StrategyState(event_bus=NullEventBus())
-
- # CANCEL requires a known working order to pass existence gating.
- state.apply_order_state_event(
- OrderStateEvent(
- ts_ns_exch=1,
- ts_ns_local=1,
- instrument=instrument,
- client_order_id=cancel_client_order_id,
- order_type="limit",
- state_type="working",
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=None,
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=None,
- remaining_qty=None,
- time_in_force="GTC",
- reason=None,
- raw={"req": 0, "source": "snapshot"},
- )
- )
-
- risk_cfg = RiskConfig(
- scope="test",
- trading_enabled=True,
- notional_limits=NotionalLimits(
- currency="USDC",
- max_gross_notional=1e18,
- max_single_order_notional=1e18,
- ),
- order_rate_limits=OrderRateLimits(
- max_orders_per_second=0, # wake: next second boundary at 1_000_000_000
- max_cancels_per_second=2, # wake: 0.5s at 500_000_000
- ),
- )
- risk_engine = RiskEngine(risk_cfg=risk_cfg, event_bus=NullEventBus())
-
- new_intent = NewOrderIntent(
- ts_ns_local=1,
- instrument=instrument,
- client_order_id=new_client_order_id,
- intents_correlation_id=None,
- side="buy",
- order_type="limit",
- intended_qty=Quantity(unit="contracts", value=1.0),
- intended_price=Price(currency="USDC", value=100.0),
- time_in_force="GTC",
- )
- cancel_intent = CancelOrderIntent(
- ts_ns_local=1,
- instrument=instrument,
- client_order_id=cancel_client_order_id,
- intents_correlation_id=None,
- )
-
- decision = risk_engine.decide_intents(
- raw_intents=[new_intent, cancel_intent],
- state=state,
- now_ts_ns_local=1,
- )
-
- assert decision.accepted_now == []
- assert decision.rejected == []
- assert len(decision.queued) == 2
- assert decision.next_send_ts_ns_local == 500_000_000
-
-
-def test_rate_limit_routing_sets_internal_obligation_reason_characterization() -> None:
- """Internal semantic contract: rate-limit blocking emits a rate_limit obligation."""
-
- execution_control = ExecutionControl()
- new_intent = NewOrderIntent(
- ts_ns_local=1,
- instrument="BTC-USDC-PERP",
- client_order_id="order-1",
- intents_correlation_id=None,
- side="buy",
- order_type="limit",
- intended_qty=Quantity(unit="contracts", value=1.0),
- intended_price=Price(currency="USDC", value=100.0),
- time_in_force="GTC",
- )
-
- result = execution_control.route_after_policy_rate_limit(
- new_intent,
- now_ts_ns_local=1,
- max_orders_per_sec=0,
- max_cancels_per_sec=None,
- )
-
- assert result.accept_now is False
- assert result.stage_to_queue is True
- assert result.scheduling_obligation is not None
- assert result.scheduling_obligation.ts_ns_local == 1_000_000_000
- assert result.scheduling_obligation.reason == "rate_limit"
-
diff --git a/tests/semantics/queue_semantics/test_mixed_queued_intent_dominance_sequences_characterization.py b/tests/semantics/queue_semantics/test_mixed_queued_intent_dominance_sequences_characterization.py
deleted file mode 100644
index 04c091b..0000000
--- a/tests/semantics/queue_semantics/test_mixed_queued_intent_dominance_sequences_characterization.py
+++ /dev/null
@@ -1,103 +0,0 @@
-"""
-Characterization tests: mixed dominance sequences within queued intents.
-
-These pin the current behavior for sequences like:
-NEW queued -> REPLACE on same logical key -> CANCEL on same logical key
-
-This suite is intentionally descriptive of current behavior (not prescriptive).
-"""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- CancelOrderIntent,
- NewOrderIntent,
- NotionalLimits,
- OrderRateLimits,
- Price,
- Quantity,
- ReplaceOrderIntent,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-from tradingchassis_core.core.risk.risk_config import RiskConfig
-from tradingchassis_core.core.risk.risk_engine import RiskEngine
-
-
-def test_new_then_replace_then_cancel_on_same_key_characterization() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-1"
-
- state = StrategyState(event_bus=NullEventBus())
-
- risk_cfg = RiskConfig(
- scope="test",
- trading_enabled=True,
- notional_limits=NotionalLimits(
- currency="USDC",
- max_gross_notional=1e18,
- max_single_order_notional=1e18,
- ),
- order_rate_limits=OrderRateLimits(
- max_orders_per_second=0,
- max_cancels_per_second=0,
- ),
- )
- risk_engine = RiskEngine(risk_cfg=risk_cfg, event_bus=NullEventBus())
-
- # Step 1: NEW is queued due to order rate-limit.
- new_intent = NewOrderIntent(
- ts_ns_local=1,
- instrument=instrument,
- client_order_id=client_order_id,
- intents_correlation_id=None,
- side="buy",
- order_type="limit",
- intended_qty=Quantity(unit="contracts", value=1.0),
- intended_price=Price(currency="USDC", value=100.0),
- time_in_force="GTC",
- )
- d1 = risk_engine.decide_intents(raw_intents=[new_intent], state=state, now_ts_ns_local=1)
- assert d1.accepted_now == []
- assert d1.rejected == []
- assert [it.intent_type for it in d1.queued] == ["new"]
- assert state.has_queued_intent(instrument, client_order_id)
-
- # Step 2: REPLACE arrives while there is no working order, but a queued NEW exists.
- # Characterization: the REPLACE is handled locally and results in an updated queued NEW.
- replace_intent = ReplaceOrderIntent(
- ts_ns_local=2,
- instrument=instrument,
- client_order_id=client_order_id,
- intents_correlation_id=None,
- side="buy",
- intended_price=Price(currency="USDC", value=101.0),
- intended_qty=Quantity(unit="contracts", value=2.0),
- )
- d2 = risk_engine.decide_intents(raw_intents=[replace_intent], state=state, now_ts_ns_local=2)
- assert d2.accepted_now == []
- assert d2.rejected == []
- assert len(d2.handled_in_queue) == 1
- assert d2.handled_in_queue[0].intent_type == "replace"
- assert [it.intent_type for it in d2.queued] == ["new"]
-
- queued_new = state.find_queued_new_intent(instrument, client_order_id)
- assert queued_new is not None
- assert queued_new.intended_price.value == 101.0
- assert queued_new.intended_qty.value == 2.0
-
- # Step 3: CANCEL arrives while only queued state exists (no working order).
- # Characterization: the CANCEL clears queued intents for that key and is handled locally.
- cancel_intent = CancelOrderIntent(
- ts_ns_local=3,
- instrument=instrument,
- client_order_id=client_order_id,
- intents_correlation_id=None,
- )
- d3 = risk_engine.decide_intents(raw_intents=[cancel_intent], state=state, now_ts_ns_local=3)
- assert d3.accepted_now == []
- assert d3.rejected == []
- assert len(d3.handled_in_queue) == 1
- assert d3.handled_in_queue[0].intent_type == "cancel"
- assert d3.queued == []
- assert not state.has_queued_intent(instrument, client_order_id)
diff --git a/tests/semantics/queue_semantics/test_new_queued_on_rate_limit.py b/tests/semantics/queue_semantics/test_new_queued_on_rate_limit.py
deleted file mode 100644
index 6d4ecd8..0000000
--- a/tests/semantics/queue_semantics/test_new_queued_on_rate_limit.py
+++ /dev/null
@@ -1,134 +0,0 @@
-"""
-Semantic test: NEW intents are queued under active rate-limit backpressure.
-
-Invariant:
-Queue is a backpressure buffer. NEW intents must be queued (not accepted_now)
-if a temporary send blocker exists (e.g. rate limit exhausted).
-"""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- CancelOrderIntent,
- NewOrderIntent,
- NotionalLimits,
- OrderRateLimits,
- OrderStateEvent,
- Price,
- Quantity,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-from tradingchassis_core.core.risk.risk_config import RiskConfig
-from tradingchassis_core.core.risk.risk_engine import RiskEngine
-
-
-def test_new_is_queued_when_rate_limit_blocks() -> None:
- """NEW intent must be queued when order rate-limit blocks sending."""
-
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-1"
-
- state = StrategyState(event_bus=NullEventBus())
-
- # Configure strict order rate-limit to force backpressure
- risk_cfg = RiskConfig(
- scope="test",
- trading_enabled=True,
- notional_limits=NotionalLimits(
- currency="USDC",
- max_gross_notional=1e18,
- max_single_order_notional=1e18,
- ),
- order_rate_limits=OrderRateLimits(
- max_orders_per_second=0,
- ),
- )
-
- risk_engine = RiskEngine(risk_cfg=risk_cfg, event_bus=NullEventBus())
-
- new_intent = NewOrderIntent(
- ts_ns_local=1,
- instrument=instrument,
- client_order_id=client_order_id,
- intents_correlation_id=None,
- side="buy",
- order_type="limit",
- intended_qty=Quantity(unit="contracts", value=1.0),
- intended_price=Price(currency="USDC", value=100.0),
- time_in_force="GTC",
- )
-
- decision = risk_engine.decide_intents(
- raw_intents=[new_intent],
- state=state,
- now_ts_ns_local=1,
- )
-
- assert decision.accepted_now == []
- assert decision.rejected == []
- assert len(decision.queued) == 1
- # Characterization: rate-limit backpressure sets a "wake up no earlier than" timestamp.
- # With ts_ns_local=1 and max_orders_per_second=0, wake timestamp is next local-second boundary.
- assert decision.next_send_ts_ns_local == 1_000_000_000
-
-
-def test_cancel_is_queued_when_cancel_rate_limit_blocks_and_sets_next_send_characterization() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-1"
-
- state = StrategyState(event_bus=NullEventBus())
-
- # A CANCEL only passes existence gating if a working order exists.
- state.apply_order_state_event(
- OrderStateEvent(
- ts_ns_exch=1,
- ts_ns_local=1,
- instrument=instrument,
- client_order_id=client_order_id,
- order_type="limit",
- state_type="working",
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=None,
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=None,
- remaining_qty=None,
- time_in_force="GTC",
- reason=None,
- raw={"req": 0, "source": "snapshot"},
- )
- )
-
- risk_cfg = RiskConfig(
- scope="test",
- trading_enabled=True,
- notional_limits=NotionalLimits(
- currency="USDC",
- max_gross_notional=1e18,
- max_single_order_notional=1e18,
- ),
- order_rate_limits=OrderRateLimits(
- max_cancels_per_second=0,
- ),
- )
-
- risk_engine = RiskEngine(risk_cfg=risk_cfg, event_bus=NullEventBus())
-
- cancel_intent = CancelOrderIntent(
- ts_ns_local=1,
- instrument=instrument,
- client_order_id=client_order_id,
- intents_correlation_id=None,
- )
-
- decision = risk_engine.decide_intents(
- raw_intents=[cancel_intent],
- state=state,
- now_ts_ns_local=1,
- )
-
- assert decision.accepted_now == []
- assert decision.rejected == []
- assert [it.intent_type for it in decision.queued] == ["cancel"]
- assert decision.next_send_ts_ns_local == 1_000_000_000
diff --git a/tests/semantics/queue_semantics/test_queue_cancel_dominates_new.py b/tests/semantics/queue_semantics/test_queue_cancel_dominates_new.py
deleted file mode 100644
index 428b4d9..0000000
--- a/tests/semantics/queue_semantics/test_queue_cancel_dominates_new.py
+++ /dev/null
@@ -1,94 +0,0 @@
-"""
-Semantic test: CANCEL dominates queued NEW intents.
-
-Invariant:
-If a NEW intent is already queued for an order id and a CANCEL intent
-for the same order id arrives while the order is still not inflight,
-the queued NEW must be removed and replaced by the CANCEL.
-"""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- CancelOrderIntent,
- NewOrderIntent,
- NotionalLimits,
- OrderRateLimits,
- Price,
- Quantity,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-from tradingchassis_core.core.risk.risk_config import RiskConfig
-from tradingchassis_core.core.risk.risk_engine import RiskEngine
-
-
-def test_cancel_dominates_queued_new() -> None:
- """CANCEL must remove a queued NEW for the same order id."""
-
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-1"
-
- state = StrategyState(event_bus=NullEventBus())
-
- # Configure rate-limit to force backpressure (queueing)
- risk_cfg = RiskConfig(
- scope="test",
- trading_enabled=True,
- notional_limits=NotionalLimits(
- currency="USDC",
- max_gross_notional=1e18,
- max_single_order_notional=1e18,
- ),
- order_rate_limits=OrderRateLimits(
- max_orders_per_second=0,
- ),
- )
-
- risk_engine = RiskEngine(risk_cfg=risk_cfg, event_bus=NullEventBus())
-
- # Step 1: NEW intent is queued due to rate-limit backpressure
- new_intent = NewOrderIntent(
- ts_ns_local=1,
- instrument=instrument,
- client_order_id=client_order_id,
- intents_correlation_id=None,
- side="buy",
- order_type="limit",
- intended_qty=Quantity(unit="contracts", value=1.0),
- intended_price=Price(currency="USDC", value=100.0),
- time_in_force="GTC",
- )
-
- decision_1 = risk_engine.decide_intents(
- raw_intents=[new_intent],
- state=state,
- now_ts_ns_local=1,
- )
-
- assert decision_1.accepted_now == []
- assert decision_1.rejected == []
- assert len(decision_1.queued) == 1
-
- # Step 2: CANCEL arrives for the same order id
- cancel_intent = CancelOrderIntent(
- ts_ns_local=2,
- instrument=instrument,
- client_order_id=client_order_id,
- intents_correlation_id=None,
- )
-
- decision_2 = risk_engine.decide_intents(
- raw_intents=[cancel_intent],
- state=state,
- now_ts_ns_local=2,
- )
-
- # --- Queue mutation assertions ---
- assert decision_2.accepted_now == []
- assert decision_2.rejected == []
- assert len(decision_2.handled_in_queue) == 1
- assert decision_2.queued == []
-
- # Queue must contain only CANCEL
- assert not state.has_queued_intent(instrument, client_order_id)
diff --git a/tests/semantics/queue_semantics/test_strategy_state_pop_queued_intents_characterization.py b/tests/semantics/queue_semantics/test_strategy_state_pop_queued_intents_characterization.py
deleted file mode 100644
index 5bf4505..0000000
--- a/tests/semantics/queue_semantics/test_strategy_state_pop_queued_intents_characterization.py
+++ /dev/null
@@ -1,158 +0,0 @@
-"""
-Characterization tests: StrategyState outbox queue pop semantics.
-
-These tests pin the *current* behavior of:
-- StrategyState.pop_queued_intents ordering (priority then FIFO)
-- inflight filtering (skips blocked ids without dequeuing them)
-
-This suite is intentionally explicit and should not be interpreted as desired semantics.
-"""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- CancelOrderIntent,
- NewOrderIntent,
- Price,
- Quantity,
- ReplaceOrderIntent,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-
-
-def test_pop_queued_intents_orders_by_priority_then_fifo_characterization() -> None:
- instrument = "BTC-USDC-PERP"
-
- state = StrategyState(event_bus=NullEventBus())
-
- new_1 = NewOrderIntent(
- ts_ns_local=30,
- instrument=instrument,
- client_order_id="new-1",
- intents_correlation_id=None,
- side="buy",
- order_type="limit",
- intended_qty=Quantity(unit="contracts", value=1.0),
- intended_price=Price(currency="USDC", value=100.0),
- time_in_force="GTC",
- )
- new_2 = NewOrderIntent(
- ts_ns_local=10,
- instrument=instrument,
- client_order_id="new-2",
- intents_correlation_id=None,
- side="buy",
- order_type="limit",
- intended_qty=Quantity(unit="contracts", value=1.0),
- intended_price=Price(currency="USDC", value=101.0),
- time_in_force="GTC",
- )
- replace_1 = ReplaceOrderIntent(
- ts_ns_local=20,
- instrument=instrument,
- client_order_id="replace-1",
- intents_correlation_id=None,
- side="buy",
- intended_price=Price(currency="USDC", value=102.0),
- intended_qty=Quantity(unit="contracts", value=1.0),
- )
- cancel_1 = CancelOrderIntent(
- ts_ns_local=40,
- instrument=instrument,
- client_order_id="cancel-1",
- intents_correlation_id=None,
- )
- cancel_2 = CancelOrderIntent(
- ts_ns_local=5,
- instrument=instrument,
- client_order_id="cancel-2",
- intents_correlation_id=None,
- )
-
- state.merge_intents_into_queue(
- instrument=instrument,
- intents=[new_1, replace_1, cancel_1, new_2, cancel_2],
- )
-
- popped = state.pop_queued_intents(instrument)
- popped_ids = [it.client_order_id for it in popped]
-
- # Characterization: selection is computed by priority + queued_at_ts_ns,
- # but the returned list preserves the queue's iteration order for the selected set.
- # Since all intents are eligible here, this matches enqueue order.
- assert popped_ids == ["new-1", "replace-1", "cancel-1", "new-2", "cancel-2"]
-
-
-def test_pop_queued_intents_filters_inflight_without_dequeuing_characterization() -> None:
- instrument = "BTC-USDC-PERP"
-
- state = StrategyState(event_bus=NullEventBus())
-
- blocked_new = NewOrderIntent(
- ts_ns_local=1,
- instrument=instrument,
- client_order_id="blocked",
- intents_correlation_id=None,
- side="buy",
- order_type="limit",
- intended_qty=Quantity(unit="contracts", value=1.0),
- intended_price=Price(currency="USDC", value=100.0),
- time_in_force="GTC",
- )
- allowed_cancel = CancelOrderIntent(
- ts_ns_local=2,
- instrument=instrument,
- client_order_id="allowed",
- intents_correlation_id=None,
- )
-
- state.merge_intents_into_queue(instrument=instrument, intents=[blocked_new, allowed_cancel])
-
- state.mark_intent_sent(
- instrument=instrument,
- client_order_id="blocked",
- intent_type="new",
- )
-
- popped_1 = state.pop_queued_intents(instrument)
- assert [it.client_order_id for it in popped_1] == ["allowed"]
-
- # Characterization: the inflight-blocked intent remains queued (not removed).
- assert state.has_queued_intent(instrument, "blocked")
- assert not state.has_queued_intent(instrument, "allowed")
-
- # After inflight clears, it becomes eligible.
- state._clear_inflight(instrument=instrument, client_order_id="blocked")
- popped_2 = state.pop_queued_intents(instrument)
- assert [it.client_order_id for it in popped_2] == ["blocked"]
-
-
-def test_pop_queued_intents_respects_max_items_characterization() -> None:
- instrument = "BTC-USDC-PERP"
- state = StrategyState(event_bus=NullEventBus())
-
- cancel_a = CancelOrderIntent(
- ts_ns_local=1,
- instrument=instrument,
- client_order_id="a",
- intents_correlation_id=None,
- )
- cancel_b = CancelOrderIntent(
- ts_ns_local=2,
- instrument=instrument,
- client_order_id="b",
- intents_correlation_id=None,
- )
- cancel_c = CancelOrderIntent(
- ts_ns_local=3,
- instrument=instrument,
- client_order_id="c",
- intents_correlation_id=None,
- )
-
- state.merge_intents_into_queue(instrument=instrument, intents=[cancel_c, cancel_a, cancel_b])
-
- popped = state.pop_queued_intents(instrument, max_items=2)
- assert [it.client_order_id for it in popped] == ["a", "b"]
- assert state.has_queued_intent(instrument, "c")
diff --git a/tests/semantics/state_transitions/test_new_to_working.py b/tests/semantics/state_transitions/test_new_to_working.py
deleted file mode 100644
index 7adb2cd..0000000
--- a/tests/semantics/state_transitions/test_new_to_working.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""
-Semantic test: NEW -> working.
-
-Invariant:
-After a NEW intent has been sent (accepted_now),
-the order must be present in the working orders state.
-"""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-
-
-def test_new_transitions_to_working() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-1"
-
- state = StrategyState(event_bus=NullEventBus())
-
- # Simulate that the order is acknowledged as working
- state.orders[instrument] = {
- client_order_id: {
- "status": "working",
- }
- }
-
- assert instrument in state.orders
- assert client_order_id in state.orders[instrument]
- assert state.orders[instrument][client_order_id]["status"] == "working"
diff --git a/tests/semantics/state_transitions/test_replace_to_replaced.py b/tests/semantics/state_transitions/test_replace_to_replaced.py
deleted file mode 100644
index d00004d..0000000
--- a/tests/semantics/state_transitions/test_replace_to_replaced.py
+++ /dev/null
@@ -1,36 +0,0 @@
-"""
-Semantic test: replace -> replaced.
-
-Invariant:
-A replace operation must not result in duplicate working orders
-for the same client_order_id.
-"""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-
-
-def test_replace_transitions_to_replaced() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-1"
-
- state = StrategyState(event_bus=NullEventBus())
-
- # Existing working order
- state.orders[instrument] = {
- client_order_id: {
- "status": "working",
- "version": 1,
- }
- }
-
- # Simulate replace acknowledgment
- state.orders[instrument][client_order_id] = {
- "status": "working",
- "version": 2,
- }
-
- assert len(state.orders[instrument]) == 1
- assert state.orders[instrument][client_order_id]["version"] == 2
diff --git a/tests/semantics/state_transitions/test_submitted_boundary_characterization.py b/tests/semantics/state_transitions/test_submitted_boundary_characterization.py
deleted file mode 100644
index 6179981..0000000
--- a/tests/semantics/state_transitions/test_submitted_boundary_characterization.py
+++ /dev/null
@@ -1,619 +0,0 @@
-"""
-Characterization and semantic tests for submitted boundary behavior.
-
-This suite pins compatibility behavior while introducing an internal canonical
-order lifecycle projection that begins at dispatch/submission.
-"""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.domain.types import (
- NewOrderIntent,
- OrderStateEvent,
- OrderSubmittedEvent,
- Price,
- Quantity,
-)
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-
-
-def _new_intent(instrument: str, client_order_id: str, *, ts_ns_local: int) -> NewOrderIntent:
- return NewOrderIntent(
- ts_ns_local=ts_ns_local,
- instrument=instrument,
- client_order_id=client_order_id,
- intents_correlation_id=None,
- side="buy",
- order_type="limit",
- intended_qty=Quantity(unit="contracts", value=1.0),
- intended_price=Price(currency="USDC", value=100.0),
- time_in_force="GTC",
- )
-
-
-def _order_state_event(
- instrument: str,
- client_order_id: str,
- *,
- ts_ns_local: int,
- ts_ns_exch: int,
- state_type: str,
- req: int = 0,
-) -> OrderStateEvent:
- return OrderStateEvent(
- ts_ns_exch=ts_ns_exch,
- ts_ns_local=ts_ns_local,
- instrument=instrument,
- client_order_id=client_order_id,
- order_type="limit",
- state_type=state_type,
- side="buy",
- intended_price=Price(currency="USDC", value=100.0),
- filled_price=None,
- intended_qty=Quantity(unit="contracts", value=1.0),
- cum_filled_qty=None,
- remaining_qty=None,
- time_in_force="GTC",
- reason=None,
- raw={"req": req, "source": "snapshot"},
- )
-
-
-def _order_submitted_event(
- instrument: str,
- client_order_id: str,
- *,
- ts_ns_local_dispatch: int,
-) -> OrderSubmittedEvent:
- return OrderSubmittedEvent(
- ts_ns_local_dispatch=ts_ns_local_dispatch,
- instrument=instrument,
- client_order_id=client_order_id,
- side="buy",
- order_type="limit",
- intended_price=Price(currency="USDC", value=100.0),
- intended_qty=Quantity(unit="contracts", value=1.0),
- time_in_force="GTC",
- intent_correlation_id="corr-1",
- dispatch_attempt_id="attempt-1",
- runtime_correlation={"engine": "backtest", "seq": 1},
- )
-
-
-def test_mark_intent_sent_new_preserves_inflight_compatibility_characterization() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-new-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_timestamp(101)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
-
- assert state.has_inflight(instrument, client_order_id)
- assert state.inflight[instrument][client_order_id].action == "new"
- assert state.inflight[instrument][client_order_id].ts_sent_ns_local == 101
- assert state.last_sent_intents[instrument][client_order_id] == (101, "new")
-
-
-def test_mark_intent_sent_new_does_not_mutate_existing_strategy_state_orders_characterization() -> None:
- instrument = "BTC-USDC-PERP"
- existing_order_id = "existing-order-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- existing_order_id,
- ts_ns_local=100,
- ts_ns_exch=100,
- state_type="working",
- )
- )
- before = state.orders[instrument][existing_order_id]
-
- state.update_timestamp(150)
- state.mark_intent_sent(instrument=instrument, client_order_id="new-order-1", intent_type="new")
-
- assert state.orders[instrument][existing_order_id] is before
- assert state.orders[instrument][existing_order_id].state_type == "working"
-
-
-def test_strategy_state_orders_remains_snapshot_driven_characterization() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-snapshot-driven-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.merge_intents_into_queue(
- instrument=instrument,
- intents=[_new_intent(instrument, client_order_id, ts_ns_local=10)],
- )
- state.update_timestamp(11)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
-
- assert not state.has_working_order(instrument, client_order_id)
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=12,
- ts_ns_exch=12,
- state_type="working",
- )
- )
- assert state.has_working_order(instrument, client_order_id)
-
-
-def test_none_to_pending_new_compatibility_transition_remains_valid_characterization() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-pending-new-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=200,
- ts_ns_exch=200,
- state_type="pending_new",
- req=1,
- )
- )
-
- assert state.orders[instrument][client_order_id].state_type == "pending_new"
-
-
-def test_mark_intent_sent_new_creates_canonical_submitted_projection() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-canonical-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_timestamp(300)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
-
- projection = state.canonical_orders[(instrument, client_order_id)]
- assert projection.instrument == instrument
- assert projection.client_order_id == client_order_id
- assert projection.state == "submitted"
- assert projection.submitted_ts_ns_local == 300
- assert projection.updated_ts_ns_local == 300
-
-
-def test_mark_intent_sent_new_does_not_advance_processing_position_cursor() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-cursor-guard-1"
- state = StrategyState(event_bus=NullEventBus())
-
- assert state._last_processing_position_index is None
-
- state.update_timestamp(301)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
-
- # mark_intent_sent sidecar behavior remains available without canonical entry metadata.
- assert state.canonical_orders[(instrument, client_order_id)].state == "submitted"
- assert state.has_inflight(instrument, client_order_id)
- assert state._last_processing_position_index is None
-
-
-def test_order_submitted_event_creates_projection_without_mark_intent_sent() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-stream-submitted-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.apply_order_submitted_event(
- _order_submitted_event(
- instrument,
- client_order_id,
- ts_ns_local_dispatch=305,
- )
- )
-
- projection = state.canonical_orders[(instrument, client_order_id)]
- assert projection.state == "submitted"
- assert projection.submitted_ts_ns_local == 305
- assert projection.updated_ts_ns_local == 305
- assert state.orders == {}
-
-
-def test_mark_intent_sent_new_remains_unchanged_when_projection_preexists() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-coexistence-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.apply_order_submitted_event(
- _order_submitted_event(
- instrument,
- client_order_id,
- ts_ns_local_dispatch=310,
- )
- )
- before = state.canonical_orders[(instrument, client_order_id)]
-
- state.update_timestamp(320)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
-
- after = state.canonical_orders[(instrument, client_order_id)]
- assert after.submitted_ts_ns_local == before.submitted_ts_ns_local
- assert after.updated_ts_ns_local == before.updated_ts_ns_local
- assert after.state == "submitted"
- # Existing compatibility bookkeeping behavior remains intact.
- assert state.has_inflight(instrument, client_order_id)
- assert state.last_sent_intents[instrument][client_order_id] == (320, "new")
-
-
-def test_queue_residency_alone_does_not_create_canonical_order() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-queued-only-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.merge_intents_into_queue(
- instrument=instrument,
- intents=[_new_intent(instrument, client_order_id, ts_ns_local=1)],
- )
-
- assert state.canonical_orders == {}
-
-
-def test_mark_intent_sent_replace_and_cancel_do_not_create_canonical_submitted_order() -> None:
- instrument = "BTC-USDC-PERP"
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_timestamp(400)
- state.mark_intent_sent(instrument=instrument, client_order_id="existing-1", intent_type="new")
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- "existing-1",
- ts_ns_local=410,
- ts_ns_exch=410,
- state_type="working",
- )
- )
-
- state.mark_intent_sent(instrument=instrument, client_order_id="replace-1", intent_type="replace")
- state.mark_intent_sent(instrument=instrument, client_order_id="cancel-1", intent_type="cancel")
- state.mark_intent_sent(instrument=instrument, client_order_id="existing-1", intent_type="replace")
- state.mark_intent_sent(instrument=instrument, client_order_id="existing-1", intent_type="cancel")
-
- assert (instrument, "replace-1") not in state.canonical_orders
- assert (instrument, "cancel-1") not in state.canonical_orders
- assert state.canonical_orders[(instrument, "existing-1")].state == "accepted"
-
-
-def test_post_dispatch_feedback_advances_existing_canonical_projection() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-canonical-advance-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_timestamp(500)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
- assert state.canonical_orders[(instrument, client_order_id)].state == "submitted"
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=550,
- ts_ns_exch=550,
- state_type="working",
- )
- )
-
- projection = state.canonical_orders[(instrument, client_order_id)]
- assert projection.state == "accepted"
- assert projection.updated_ts_ns_local == 550
- assert state.orders[instrument][client_order_id].state_type == "working"
-
-
-def test_pending_new_does_not_advance_canonical_submitted_projection() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-canonical-pending-new-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_timestamp(600)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
- before = state.canonical_orders[(instrument, client_order_id)]
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=610,
- ts_ns_exch=610,
- state_type="pending_new",
- req=1,
- )
- )
-
- projection = state.canonical_orders[(instrument, client_order_id)]
- assert projection.state == "submitted"
- assert projection.updated_ts_ns_local == before.updated_ts_ns_local
- assert state.orders[instrument][client_order_id].state_type == "pending_new"
-
-
-def test_accepted_advances_submitted_to_accepted() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-canonical-accepted-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_timestamp(700)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=710,
- ts_ns_exch=710,
- state_type="accepted",
- )
- )
-
- projection = state.canonical_orders[(instrument, client_order_id)]
- assert projection.state == "accepted"
- assert projection.updated_ts_ns_local == 710
- assert state.orders[instrument][client_order_id].state_type == "accepted"
-
-
-def test_rejected_advances_submitted_to_rejected_terminal() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-canonical-rejected-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_timestamp(800)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=810,
- ts_ns_exch=810,
- state_type="rejected",
- )
- )
-
- projection = state.canonical_orders[(instrument, client_order_id)]
- assert projection.state == "rejected"
- assert projection.updated_ts_ns_local == 810
- assert client_order_id not in state.orders.get(instrument, {})
-
-
-def test_partially_filled_and_filled_canonical_progression() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-canonical-fill-progression-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_timestamp(900)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=910,
- ts_ns_exch=910,
- state_type="working",
- )
- )
- assert state.canonical_orders[(instrument, client_order_id)].state == "accepted"
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=920,
- ts_ns_exch=920,
- state_type="partially_filled",
- )
- )
- assert state.canonical_orders[(instrument, client_order_id)].state == "partially_filled"
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=930,
- ts_ns_exch=930,
- state_type="filled",
- )
- )
- projection = state.canonical_orders[(instrument, client_order_id)]
- assert projection.state == "filled"
- assert projection.updated_ts_ns_local == 930
- assert client_order_id not in state.orders.get(instrument, {})
-
-
-def test_partially_filled_to_canceled_canonical_progression() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-canonical-cancel-progression-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_timestamp(950)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=960,
- ts_ns_exch=960,
- state_type="accepted",
- )
- )
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=970,
- ts_ns_exch=970,
- state_type="partially_filled",
- )
- )
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=980,
- ts_ns_exch=980,
- state_type="canceled",
- )
- )
-
- projection = state.canonical_orders[(instrument, client_order_id)]
- assert projection.state == "canceled"
- assert projection.updated_ts_ns_local == 980
- assert client_order_id not in state.orders.get(instrument, {})
-
-
-def test_terminal_canonical_state_is_final_noop_on_later_updates() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-canonical-terminal-final-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_timestamp(1000)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=1010,
- ts_ns_exch=1010,
- state_type="working",
- )
- )
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=1020,
- ts_ns_exch=1020,
- state_type="filled",
- )
- )
- assert state.canonical_orders[(instrument, client_order_id)].state == "filled"
- assert state.canonical_orders[(instrument, client_order_id)].updated_ts_ns_local == 1020
-
- # Invalid terminal transition should remain a no-op for canonical state.
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=1030,
- ts_ns_exch=1030,
- state_type="canceled",
- )
- )
-
- projection = state.canonical_orders[(instrument, client_order_id)]
- assert projection.state == "filled"
- assert projection.updated_ts_ns_local == 1020
-
-
-def test_replaced_does_not_advance_canonical_lifecycle() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-canonical-replaced-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_timestamp(1100)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=1110,
- ts_ns_exch=1110,
- state_type="working",
- )
- )
- before = state.canonical_orders[(instrument, client_order_id)]
- assert before.state == "accepted"
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=1120,
- ts_ns_exch=1120,
- state_type="replaced",
- )
- )
-
- projection = state.canonical_orders[(instrument, client_order_id)]
- assert projection.state == "accepted"
- assert projection.updated_ts_ns_local == 1110
-
-
-def test_expired_does_not_introduce_canonical_expired_state() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-canonical-expired-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_timestamp(1200)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
-
- state.apply_order_state_event(
- _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=1210,
- ts_ns_exch=1210,
- state_type="expired",
- )
- )
-
- projection = state.canonical_orders[(instrument, client_order_id)]
- assert projection.state == "submitted"
- assert projection.updated_ts_ns_local == 1200
- assert client_order_id not in state.orders.get(instrument, {})
-
-
-def test_snapshot_fill_progression_does_not_mutate_canonical_fill_reducer_buckets() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-snapshot-fill-guard-1"
- state = StrategyState(event_bus=NullEventBus())
-
- state.update_timestamp(1300)
- state.mark_intent_sent(instrument=instrument, client_order_id=client_order_id, intent_type="new")
-
- first_partial = _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=1310,
- ts_ns_exch=1310,
- state_type="partially_filled",
- ).model_copy(
- update={
- "filled_price": Price(currency="USDC", value=100.25),
- "cum_filled_qty": Quantity(unit="contracts", value=0.25),
- "remaining_qty": Quantity(unit="contracts", value=0.75),
- }
- )
- second_partial = _order_state_event(
- instrument,
- client_order_id,
- ts_ns_local=1320,
- ts_ns_exch=1320,
- state_type="partially_filled",
- ).model_copy(
- update={
- "filled_price": Price(currency="USDC", value=100.50),
- "cum_filled_qty": Quantity(unit="contracts", value=0.50),
- "remaining_qty": Quantity(unit="contracts", value=0.50),
- }
- )
-
- state.apply_order_state_event(first_partial)
- state.apply_order_state_event(second_partial)
-
- # Compatibility snapshot/projection path remains active.
- assert state.orders[instrument][client_order_id].state_type == "partially_filled"
- assert state.orders[instrument][client_order_id].cum_filled_qty == 0.50
- assert state.canonical_orders[(instrument, client_order_id)].state == "submitted"
- assert state.canonical_orders[(instrument, client_order_id)].updated_ts_ns_local == 1300
-
- # Snapshot progression must not mutate canonical FillEvent reducer buckets.
- assert state.fills == {}
- assert state.fill_cum_qty == {}
diff --git a/tests/semantics/state_transitions/test_terminal_clears_inflight.py b/tests/semantics/state_transitions/test_terminal_clears_inflight.py
deleted file mode 100644
index 6bada2c..0000000
--- a/tests/semantics/state_transitions/test_terminal_clears_inflight.py
+++ /dev/null
@@ -1,26 +0,0 @@
-"""
-Semantic test: terminal clears inflight.
-
-Invariant:
-Any terminal order event must clear inflight state.
-"""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-
-
-def test_terminal_clears_inflight() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-1"
-
- state = StrategyState(event_bus=NullEventBus())
-
- # Precondition: order is inflight
- state.inflight[instrument] = {client_order_id}
-
- # Simulate terminal event cleanup
- state.inflight[instrument].discard(client_order_id)
-
- assert client_order_id not in state.inflight.get(instrument, set())
diff --git a/tests/semantics/state_transitions/test_working_to_filled.py b/tests/semantics/state_transitions/test_working_to_filled.py
deleted file mode 100644
index de5a89f..0000000
--- a/tests/semantics/state_transitions/test_working_to_filled.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""
-Semantic test: working -> filled.
-
-Invariant:
-A filled order must be removed from the working orders state.
-"""
-
-from __future__ import annotations
-
-from tradingchassis_core.core.domain.state import StrategyState
-from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
-
-
-def test_working_transitions_to_filled() -> None:
- instrument = "BTC-USDC-PERP"
- client_order_id = "order-1"
-
- state = StrategyState(event_bus=NullEventBus())
-
- # Precondition: order is working
- state.orders[instrument] = {
- client_order_id: {
- "status": "working",
- }
- }
-
- # Simulate terminal fill
- del state.orders[instrument][client_order_id]
-
- assert client_order_id not in state.orders.get(instrument, {})
diff --git a/tests/semantics/test_control_time_scheduling_semantics.py b/tests/semantics/test_control_time_scheduling_semantics.py
new file mode 100644
index 0000000..8b19f95
--- /dev/null
+++ b/tests/semantics/test_control_time_scheduling_semantics.py
@@ -0,0 +1,202 @@
+"""Control scheduling obligation semantics: rate-limit vs inflight deferral.
+
+See ``docs/flows/control-time-and-scheduling.md`` for the normative description.
+"""
+
+from __future__ import annotations
+
+import tradingchassis_core as tc
+
+
+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 _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_dispatch: int,
+ *,
+ client_order_id: str = "order-a",
+ price: float = 100.0,
+) -> tc.EventStreamEntry:
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=tc.OrderSubmittedEvent(
+ ts_ns_local_dispatch=ts_dispatch,
+ instrument="BTC-USDC-PERP",
+ client_order_id=client_order_id,
+ side="buy",
+ order_type="limit",
+ intended_price=tc.Price(currency="USDC", value=price),
+ intended_qty=tc.Quantity(value=1.0, unit="contracts"),
+ time_in_force="GTC",
+ intent_correlation_id=None,
+ dispatch_attempt_id=None,
+ runtime_correlation=None,
+ ),
+ )
+
+
+class _NewIntentEvaluator:
+ def evaluate(self, context: object) -> list[tc.NewOrderIntent]:
+ _ = context
+ return [
+ tc.NewOrderIntent(
+ intent_type="new",
+ ts_ns_local=10,
+ instrument="BTC-USDC-PERP",
+ client_order_id="intent-1",
+ intents_correlation_id="corr-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 _ReplaceIntentEvaluator:
+ """Emits a single replace against ``order-a`` (requires a working order)."""
+
+ def evaluate(self, context: object) -> list[tc.ReplaceOrderIntent]:
+ _ = context
+ return [
+ tc.ReplaceOrderIntent(
+ intent_type="replace",
+ ts_ns_local=100,
+ instrument="BTC-USDC-PERP",
+ client_order_id="order-a",
+ intents_correlation_id="corr-repl",
+ side="buy",
+ order_type="limit",
+ intended_qty=tc.Quantity(value=1.0, unit="contracts"),
+ intended_price=tc.Price(currency="USDC", value=99.0),
+ )
+ ]
+
+
+def _policy_and_apply(
+ *,
+ now_ts: int,
+ max_orders_per_sec: float | None = None,
+) -> tuple[tc.CorePolicyAdmissionContext, tc.CoreExecutionControlApplyContext]:
+ return (
+ tc.CorePolicyAdmissionContext(
+ policy_evaluator=_AllowAllPolicy(),
+ now_ts_ns_local=now_ts,
+ ),
+ tc.CoreExecutionControlApplyContext(
+ execution_control=tc.ExecutionControl(),
+ now_ts_ns_local=now_ts,
+ max_orders_per_sec=max_orders_per_sec,
+ activate_dispatchable_outputs=True,
+ ),
+ )
+
+
+def test_rate_limit_deferral_emits_control_scheduling_obligation() -> None:
+ """Time-dependent rate limiting produces a non-canonical scheduling obligation."""
+ now_ts = 100
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ policy_ctx, apply_ctx = _policy_and_apply(now_ts=now_ts, max_orders_per_sec=0.0)
+
+ result = tc.run_core_step(
+ state,
+ _control_entry(0, now_ts),
+ strategy_evaluator=_NewIntentEvaluator(),
+ policy_admission_context=policy_ctx,
+ execution_control_apply_context=apply_ctx,
+ )
+
+ assert result.dispatchable_intents == ()
+ obl = result.control_scheduling_obligation
+ assert obl is not None
+ assert obl.reason == "rate_limit"
+ assert obl.source == "execution_control_rate_limit"
+ assert obl.due_ts_ns_local >= now_ts
+ assert obl.scope_key == "instrument:BTC-USDC-PERP"
+
+
+def test_inflight_deferral_does_not_emit_control_scheduling_obligation() -> None:
+ """Inflight gating is feedback-dependent; Core does not emit a wake obligation."""
+ now_ts = 100
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ tc.process_event_entry(state, _order_submitted_entry(0, now_ts))
+ assert state.has_working_order("BTC-USDC-PERP", "order-a")
+
+ state.mark_intent_sent("BTC-USDC-PERP", "order-a", "replace")
+ assert state.has_inflight("BTC-USDC-PERP", "order-a")
+
+ policy_ctx, apply_ctx = _policy_and_apply(now_ts=now_ts, max_orders_per_sec=None)
+
+ result = tc.run_core_step(
+ state,
+ _control_entry(1, now_ts),
+ strategy_evaluator=_ReplaceIntentEvaluator(),
+ policy_admission_context=policy_ctx,
+ execution_control_apply_context=apply_ctx,
+ )
+
+ assert result.control_scheduling_obligation is None
+ assert result.dispatchable_intents == ()
+ assert result.core_step_decision is not None
+ assert len(result.core_step_decision.queued_effective_intents) >= 1
+ queued = state.queued_intents.get("BTC-USDC-PERP")
+ assert queued is not None and len(queued) == 1
+ assert queued[0].intent.intent_type == "replace"
+
+
+def test_inflight_queued_replace_reprocessed_after_order_submitted_feedback() -> None:
+ """Canonical OrderSubmittedEvent clears inflight so a later step can dispatch Queue."""
+ now_ts = 100
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ tc.process_event_entry(state, _order_submitted_entry(0, now_ts))
+ state.mark_intent_sent("BTC-USDC-PERP", "order-a", "replace")
+
+ policy_ctx, apply_ctx = _policy_and_apply(now_ts=now_ts, max_orders_per_sec=None)
+ blocked = tc.run_core_step(
+ state,
+ _control_entry(1, now_ts),
+ strategy_evaluator=_ReplaceIntentEvaluator(),
+ policy_admission_context=policy_ctx,
+ execution_control_apply_context=apply_ctx,
+ )
+ assert blocked.dispatchable_intents == ()
+ assert blocked.control_scheduling_obligation is None
+
+ policy_ctx2, apply_ctx2 = _policy_and_apply(now_ts=now_ts + 1, max_orders_per_sec=None)
+ cleared = tc.run_core_step(
+ state,
+ _order_submitted_entry(2, now_ts + 1, price=99.0),
+ policy_admission_context=policy_ctx2,
+ execution_control_apply_context=apply_ctx2,
+ )
+
+ assert not state.has_inflight("BTC-USDC-PERP", "order-a")
+ assert len(cleared.dispatchable_intents) == 1
+ assert cleared.dispatchable_intents[0].intent_type == "replace"
+ assert cleared.control_scheduling_obligation is None
diff --git a/tests/semantics/test_core_pipeline_clean.py b/tests/semantics/test_core_pipeline_clean.py
new file mode 100644
index 0000000..7daae42
--- /dev/null
+++ b/tests/semantics/test_core_pipeline_clean.py
@@ -0,0 +1,264 @@
+"""Clean CoreStep/CoreWakeupStep pipeline tests."""
+
+from __future__ import annotations
+
+import tradingchassis_core as tc
+from tradingchassis_core.core.domain.types import BookLevel, BookPayload
+
+
+class _OneIntentEvaluator:
+ def evaluate(self, context: object) -> list[tc.NewOrderIntent]:
+ _ = context
+ return [
+ tc.NewOrderIntent(
+ intent_type="new",
+ ts_ns_local=10,
+ instrument="BTC-USDC-PERP",
+ client_order_id="intent-1",
+ intents_correlation_id="corr-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
+
+
+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 _DuplicateIntentEvaluator:
+ def evaluate(self, context: object) -> list[tc.NewOrderIntent]:
+ _ = context
+ first = tc.NewOrderIntent(
+ intent_type="new",
+ ts_ns_local=10,
+ instrument="BTC-USDC-PERP",
+ client_order_id="dup-intent",
+ intents_correlation_id="corr-a",
+ 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",
+ )
+ second = tc.NewOrderIntent(
+ intent_type="new",
+ ts_ns_local=11,
+ instrument="BTC-USDC-PERP",
+ client_order_id="dup-intent",
+ intents_correlation_id="corr-b",
+ side="buy",
+ order_type="limit",
+ intended_qty=tc.Quantity(value=2.0, unit="contracts"),
+ intended_price=tc.Price(currency="USDC", value=101.0),
+ time_in_force="GTC",
+ )
+ return [first, second]
+
+
+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_run_core_step_clean_pipeline_dispatchable() -> None:
+ 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=_AllowAllPolicy(),
+ 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 tuple(intent.client_order_id for intent in result.generated_intents) == ("intent-1",)
+ assert tuple(intent.client_order_id for intent in result.candidate_intents) == ("intent-1",)
+ assert tuple(record.origin for record in result.candidate_intent_records) == (
+ tc.CandidateIntentOrigin.GENERATED,
+ )
+ assert tuple(intent.client_order_id for intent in result.dispatchable_intents) == ("intent-1",)
+ assert result.core_step_decision is not None
+
+
+def test_run_core_step_processes_entry_before_strategy_evaluation() -> None:
+ class _ChecksReducedStateEvaluator:
+ def evaluate(self, context: tc.CoreStepStrategyContext) -> list[tc.OrderIntent]:
+ # ControlTimeEvent reduction updates monotone timestamp before evaluation.
+ assert context.state.sim_ts_ns_local == 100
+ assert isinstance(context.event, tc.ControlTimeEvent)
+ assert context.position.index == 0
+ return []
+
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ _ = tc.run_core_step(
+ state,
+ _control_entry(0, 100),
+ strategy_evaluator=_ChecksReducedStateEvaluator(),
+ )
+ assert state._last_processing_position_index == 0
+
+
+def test_run_core_wakeup_step_clean_pipeline_dispatchable() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ result = tc.run_core_wakeup_step(
+ state,
+ (_control_entry(0, 100), _control_entry(1, 101)),
+ wakeup_strategy_evaluator=_OneIntentEvaluator(),
+ policy_admission_context=tc.CorePolicyAdmissionContext(
+ policy_evaluator=_AllowAllPolicy(),
+ now_ts_ns_local=101,
+ ),
+ execution_control_apply_context=tc.CoreExecutionControlApplyContext(
+ execution_control=tc.ExecutionControl(),
+ now_ts_ns_local=101,
+ activate_dispatchable_outputs=True,
+ ),
+ )
+ assert len(result.generated_intents) == 1
+ assert len(result.candidate_intent_records) == 1
+ assert len(result.dispatchable_intents) == 1
+
+
+def test_candidate_reconciliation_prefers_latest_same_key_generated_intent() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ result = tc.run_core_step(
+ state,
+ _control_entry(0, 100),
+ strategy_evaluator=_DuplicateIntentEvaluator(),
+ )
+ assert len(result.generated_intents) == 2
+ assert len(result.candidate_intent_records) == 1
+ winner = result.candidate_intent_records[0].intent
+ assert isinstance(winner, tc.NewOrderIntent)
+ assert winner.client_order_id == "dup-intent"
+ assert winner.intended_qty.value == 2.0
+
+
+def test_policy_rejection_prevents_dispatchable_intents() -> None:
+ 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.dispatchable_intents == ()
+ assert result.core_step_decision is not None
+ assert len(result.core_step_decision.policy_rejected_intents) == 1
+ assert result.core_step_decision.policy_risk_decision is not None
+ assert len(result.core_step_decision.policy_risk_decision.accepted_intents) == 0
+ assert len(result.core_step_decision.policy_risk_decision.rejected_intents) == 1
+
+
+def test_execution_control_deferral_returns_scheduling_obligation() -> None:
+ 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=_AllowAllPolicy(),
+ now_ts_ns_local=100,
+ ),
+ execution_control_apply_context=tc.CoreExecutionControlApplyContext(
+ execution_control=tc.ExecutionControl(),
+ now_ts_ns_local=100,
+ max_orders_per_sec=0.0,
+ activate_dispatchable_outputs=True,
+ ),
+ )
+ assert result.dispatchable_intents == ()
+ assert result.control_scheduling_obligation is not None
+ assert result.control_scheduling_obligation.reason == "rate_limit"
+
+
+def test_core_step_generated_only_mode_never_dispatches_externally() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ result = tc.run_core_step(
+ state,
+ _control_entry(0, 100),
+ strategy_evaluator=_OneIntentEvaluator(),
+ )
+ assert tuple(intent.client_order_id for intent in result.generated_intents) == ("intent-1",)
+ assert result.core_step_decision is None
+ assert result.dispatchable_intents == ()
+
+
+def test_process_canonical_event_reduces_market_event() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ tc.process_canonical_event(
+ state,
+ tc.MarketEvent(
+ ts_ns_exch=200,
+ ts_ns_local=201,
+ instrument="BTC-USDC-PERP",
+ 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,
+ ),
+ ),
+ )
+ assert state.sim_ts_ns_local == 201
+ market = state.market["BTC-USDC-PERP"]
+ assert market.best_bid == 99.0
+ assert market.best_ask == 101.0
diff --git a/tests/semantics/test_core_wakeup_final_state.py b/tests/semantics/test_core_wakeup_final_state.py
new file mode 100644
index 0000000..67b1dab
--- /dev/null
+++ b/tests/semantics/test_core_wakeup_final_state.py
@@ -0,0 +1,298 @@
+"""Final-state CoreWakeupStep Strategy evaluation semantics (Phase WU2)."""
+
+from __future__ import annotations
+
+import tradingchassis_core as tc
+from tradingchassis_core.core.domain.types import BookLevel, BookPayload
+
+INSTRUMENT = "BTC-USDC-PERP"
+
+_TEST_CONFIGURATION = tc.CoreConfiguration(
+ version="test-v1",
+ payload={
+ "market": {
+ "instruments": {
+ INSTRUMENT: {
+ "tick_size": 0.01,
+ "lot_size": 0.001,
+ "contract_size": 1.0,
+ }
+ }
+ }
+ },
+)
+
+
+
+class _OneIntentEvaluator:
+ def evaluate(self, context: tc.CoreWakeupStrategyContext) -> list[tc.NewOrderIntent]:
+ _ = context
+ return [
+ tc.NewOrderIntent(
+ intent_type="new",
+ ts_ns_local=10,
+ instrument=INSTRUMENT,
+ client_order_id="wake-generated",
+ intents_correlation_id="corr-wake",
+ 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 _CountingWakeupEvaluator:
+ def __init__(self) -> None:
+ self.call_count = 0
+ self.last_context: tc.CoreWakeupStrategyContext | None = None
+
+ def evaluate(self, context: tc.CoreWakeupStrategyContext) -> list[tc.OrderIntent]:
+ self.call_count += 1
+ self.last_context = context
+ return []
+
+
+class _FinalStateAwareEvaluator:
+ def evaluate(self, context: tc.CoreWakeupStrategyContext) -> list[tc.OrderIntent]:
+ assert context.state.sim_ts_ns_local == 203
+ market = context.state.market[INSTRUMENT]
+ assert market.best_bid == 99.0
+ assert market.best_ask == 101.0
+ account = context.state.account[INSTRUMENT]
+ assert account.position == 2.5
+ assert len(context.entries) == 3
+ assert isinstance(context.entries[0].event, tc.OrderExecutionFeedbackEvent)
+ assert isinstance(context.entries[1].event, tc.MarketEvent)
+ assert isinstance(context.entries[2].event, tc.ControlTimeEvent)
+ assert context.last_position is not None
+ assert context.last_position.index == 2
+ return []
+
+
+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 _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 _market_entry(index: int, ts: int) -> tc.EventStreamEntry:
+ return tc.EventStreamEntry(
+ position=tc.ProcessingPosition(index=index),
+ event=tc.MarketEvent(
+ ts_ns_exch=ts - 1,
+ ts_ns_local=ts,
+ 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 _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 _queued_intent(client_order_id: str) -> tc.NewOrderIntent:
+ return tc.NewOrderIntent(
+ intent_type="new",
+ ts_ns_local=50,
+ instrument=INSTRUMENT,
+ client_order_id=client_order_id,
+ intents_correlation_id="queued-corr",
+ side="sell",
+ 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 test_wakeup_reduces_all_entries_before_strategy_evaluation() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ entries = (
+ _execution_feedback_entry(0, 200),
+ _market_entry(1, 201),
+ _control_entry(2, 203),
+ )
+ _ = tc.run_core_wakeup_step(
+ state,
+ entries,
+ configuration=_TEST_CONFIGURATION,
+ wakeup_strategy_evaluator=_FinalStateAwareEvaluator(),
+ )
+
+
+def test_wakeup_strategy_evaluator_called_exactly_once() -> None:
+ entries = (_control_entry(0, 100), _control_entry(1, 101))
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ evaluator = _CountingWakeupEvaluator()
+ _ = tc.run_core_wakeup_step(state, entries, wakeup_strategy_evaluator=evaluator)
+ assert evaluator.call_count == 1
+ assert evaluator.last_context is not None
+ assert evaluator.last_context.entries == entries
+ assert evaluator.last_context.state.sim_ts_ns_local == 101
+ assert evaluator.last_context.last_position == entries[-1].position
+
+
+def test_wakeup_generated_intents_combined_once_with_queued_intents() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ state.merge_intents_into_queue(INSTRUMENT, [_queued_intent("queued-1")])
+ result = tc.run_core_wakeup_step(
+ state,
+ (_control_entry(0, 100),),
+ wakeup_strategy_evaluator=_OneIntentEvaluator(),
+ )
+ assert tuple(intent.client_order_id for intent in result.generated_intents) == (
+ "wake-generated",
+ )
+ origins = tuple(record.origin for record in result.candidate_intent_records)
+ assert tc.CandidateIntentOrigin.GENERATED in origins
+ assert tc.CandidateIntentOrigin.QUEUED in origins
+ client_ids = {record.intent.client_order_id for record in result.candidate_intent_records}
+ assert client_ids == {"wake-generated", "queued-1"}
+
+
+def test_wakeup_policy_and_execution_control_apply_once() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ result = tc.run_core_wakeup_step(
+ state,
+ (_control_entry(0, 100), _control_entry(1, 101)),
+ wakeup_strategy_evaluator=_OneIntentEvaluator(),
+ policy_admission_context=tc.CorePolicyAdmissionContext(
+ policy_evaluator=_AllowAllPolicy(),
+ now_ts_ns_local=101,
+ ),
+ execution_control_apply_context=tc.CoreExecutionControlApplyContext(
+ execution_control=tc.ExecutionControl(),
+ now_ts_ns_local=101,
+ activate_dispatchable_outputs=True,
+ ),
+ )
+ assert len(result.generated_intents) == 1
+ assert len(result.dispatchable_intents) == 1
+ assert result.core_step_decision is not None
+ policy_decision = result.core_step_decision.policy_risk_decision
+ assert policy_decision is not None
+ assert len(policy_decision.accepted_intents) == 1
+
+
+def test_empty_wakeup_batch_is_valid_without_evaluator() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ reduction = tc.run_core_wakeup_reduction(state, ())
+ assert reduction.entries == ()
+ assert reduction.generated_intents == ()
+ result = tc.run_core_wakeup_decision(state, reduction)
+ assert result.generated_intents == ()
+ assert result.candidate_intent_records == ()
+
+
+def test_empty_wakeup_batch_with_evaluator_runs_once() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ evaluator = _CountingWakeupEvaluator()
+ reduction = tc.run_core_wakeup_reduction(state, (), wakeup_strategy_evaluator=evaluator)
+ assert evaluator.call_count == 1
+ assert reduction.generated_intents == ()
+ assert evaluator.last_context is not None
+ assert evaluator.last_context.entries == ()
+ assert evaluator.last_context.last_position is None
+
+
+def test_single_entry_wakeup_evaluates_once() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ evaluator = _CountingWakeupEvaluator()
+ _ = tc.run_core_wakeup_step(state, (_control_entry(0, 100),), wakeup_strategy_evaluator=evaluator)
+ assert evaluator.call_count == 1
+
+
+def test_run_core_wakeup_step_matches_reduction_then_decision_path() -> None:
+ entries = (_control_entry(0, 100), _control_entry(1, 101))
+ reduction_state = tc.StrategyState(event_bus=tc.NullEventBus())
+ reduction = tc.run_core_wakeup_reduction(
+ reduction_state,
+ entries,
+ wakeup_strategy_evaluator=_OneIntentEvaluator(),
+ )
+ decision_result = tc.run_core_wakeup_decision(
+ reduction_state,
+ reduction,
+ policy_admission_context=tc.CorePolicyAdmissionContext(
+ policy_evaluator=_AllowAllPolicy(),
+ now_ts_ns_local=101,
+ ),
+ execution_control_apply_context=tc.CoreExecutionControlApplyContext(
+ execution_control=tc.ExecutionControl(),
+ now_ts_ns_local=101,
+ activate_dispatchable_outputs=True,
+ ),
+ )
+
+ step_state = tc.StrategyState(event_bus=tc.NullEventBus())
+ step_result = tc.run_core_wakeup_step(
+ step_state,
+ entries,
+ wakeup_strategy_evaluator=_OneIntentEvaluator(),
+ policy_admission_context=tc.CorePolicyAdmissionContext(
+ policy_evaluator=_AllowAllPolicy(),
+ now_ts_ns_local=101,
+ ),
+ execution_control_apply_context=tc.CoreExecutionControlApplyContext(
+ execution_control=tc.ExecutionControl(),
+ now_ts_ns_local=101,
+ activate_dispatchable_outputs=True,
+ ),
+ )
+
+ assert decision_result == step_result
+ assert len(step_result.generated_intents) == 1
+ assert len(step_result.dispatchable_intents) == 1
diff --git a/tests/semantics/test_public_api_clean.py b/tests/semantics/test_public_api_clean.py
new file mode 100644
index 0000000..6eecc4b
--- /dev/null
+++ b/tests/semantics/test_public_api_clean.py
@@ -0,0 +1,63 @@
+"""Public API surface checks for clean Core exports."""
+
+from __future__ import annotations
+
+import tradingchassis_core as tc
+
+
+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",
+ "PolicyRiskDecision",
+ "ExecutionControlDecision",
+ "CandidateIntentRecord",
+ "CandidateIntentOrigin",
+ "CorePolicyAdmissionContext",
+ "CoreExecutionControlApplyContext",
+ "ControlTimeEvent",
+ "MarketEvent",
+ "OrderSubmittedEvent",
+ "OrderExecutionFeedbackEvent",
+ "FillEvent",
+ "OrderIntent",
+ "NewOrderIntent",
+ "CancelOrderIntent",
+ "ReplaceOrderIntent",
+ "Price",
+ "Quantity",
+ "CoreConfiguration",
+ "StrategyState",
+ "ExecutionControl",
+ "ControlSchedulingObligation",
+ "NullEventBus",
+ "RiskEngine",
+ "RiskConfig",
+ ):
+ assert hasattr(tc, symbol), symbol
+
+
+def test_public_api_does_not_expose_removed_compatibility_symbols() -> None:
+ removed = (
+ "".join(["Gate", "Decision"]),
+ "".join(["compat_", "gate_decision"]),
+ "".join(["ControlTimeQueue", "ReevaluationContext"]),
+ "".join(["Core", "DecisionContext"]),
+ "".join(["OrderState", "Event"]),
+ "".join(["Derived", "FillEvent"]),
+ "".join(["decide_", "intents"]),
+ "".join(["Venue", "Adapter"]),
+ "".join(["Venue", "Policy"]),
+ )
+ for symbol in removed:
+ assert not hasattr(tc, symbol)
diff --git a/tests/semantics/test_risk_engine_policy_only.py b/tests/semantics/test_risk_engine_policy_only.py
new file mode 100644
index 0000000..9fd7b98
--- /dev/null
+++ b/tests/semantics/test_risk_engine_policy_only.py
@@ -0,0 +1,61 @@
+"""RiskEngine policy-only behavior tests."""
+
+from __future__ import annotations
+
+import tradingchassis_core as tc
+from tradingchassis_core.core.domain.types import NotionalLimits
+
+
+def _risk_config() -> tc.RiskConfig:
+ return tc.RiskConfig(
+ scope="test",
+ 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 _intent() -> tc.NewOrderIntent:
+ return tc.NewOrderIntent(
+ intent_type="new",
+ ts_ns_local=10,
+ instrument="BTC-USDC-PERP",
+ client_order_id="risk-intent-1",
+ intents_correlation_id="corr-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",
+ )
+
+
+def test_risk_engine_evaluate_policy_intent_accepts_valid_intent() -> None:
+ state = tc.StrategyState(event_bus=tc.NullEventBus())
+ state.update_market(
+ instrument="BTC-USDC-PERP",
+ 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=10,
+ ts_ns_exch=9,
+ )
+ engine = tc.RiskEngine(_risk_config())
+ accepted, reason = engine.evaluate_policy_intent(
+ intent=_intent(),
+ state=state,
+ now_ts_ns_local=10,
+ )
+ assert accepted is True
+ assert reason is None
diff --git a/tradingchassis_core/__init__.py b/tradingchassis_core/__init__.py
index e4b0063..4f5f241 100644
--- a/tradingchassis_core/__init__.py
+++ b/tradingchassis_core/__init__.py
@@ -1,102 +1,136 @@
-"""Public API for the tradingchassis_core package.
-
-Only symbols imported here are considered part of the stable,
-supported external interface.
-"""
+"""Public API for the tradingchassis_core package."""
from __future__ import annotations
from importlib.metadata import PackageNotFoundError, version
+from tradingchassis_core.core.domain.candidate_intent import (
+ CandidateIntentOrigin,
+ 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,
+ PolicyRejectedCandidate,
+ PolicyRiskDecision,
+)
from tradingchassis_core.core.domain.processing import (
fold_event_stream_entries,
+ process_canonical_event,
process_event_entry,
)
from tradingchassis_core.core.domain.processing_order import (
EventStreamEntry,
ProcessingPosition,
)
-
-# ----------------------------------------------------------------------
-# Backtest Engine API
-# ----------------------------------------------------------------------
-#
-# Backtest engine/runtime code is runtime-owned and has moved to the
-# Core Runtime repository (import from `core_runtime.backtest.*`).
-#
-# This semantic-core package must remain importable without the runtime layer.
+from tradingchassis_core.core.domain.processing_step import (
+ CoreExecutionControlApplyContext,
+ CorePolicyAdmissionContext,
+ CoreStepStrategyContext,
+ CoreStepStrategyEvaluator,
+ CoreWakeupReductionResult,
+ CoreWakeupStrategyContext,
+ CoreWakeupStrategyEvaluator,
+ run_core_step,
+ run_core_wakeup_decision,
+ run_core_wakeup_reduction,
+ run_core_wakeup_step,
+)
from tradingchassis_core.core.domain.slots import (
SlotKey,
stable_slot_order_id,
)
-
-# ----------------------------------------------------------------------
-# Domain Types (used by strategies)
-# ----------------------------------------------------------------------
from tradingchassis_core.core.domain.state import StrategyState
+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 (
+ CancelOrderIntent,
+ ControlTimeEvent,
+ FillEvent,
MarketEvent,
NewOrderIntent,
+ NotionalLimits,
+ OrderExecutionFeedbackEvent,
OrderIntent,
+ OrderSubmittedEvent,
Price,
Quantity,
ReplaceOrderIntent,
RiskConstraints,
)
-from tradingchassis_core.core.ports.engine_context import EngineContext
-
-# ----------------------------------------------------------------------
-# Config API (used by consumers)
-# ----------------------------------------------------------------------
+from tradingchassis_core.core.events.sinks.null_event_bus import NullEventBus
+from tradingchassis_core.core.execution_control.execution_control import ExecutionControl
+from tradingchassis_core.core.execution_control.types import ControlSchedulingObligation
from tradingchassis_core.core.risk.risk_config import RiskConfig
-from tradingchassis_core.core.risk.risk_engine import GateDecision
-
-# ----------------------------------------------------------------------
-# Strategy Interface
-# ----------------------------------------------------------------------
-from tradingchassis_core.strategies.base import Strategy
-from tradingchassis_core.strategies.strategy_config import StrategyConfig
-
-# ----------------------------------------------------------------------
-# Public API definition
-# ----------------------------------------------------------------------
+from tradingchassis_core.core.risk.risk_engine import RiskEngine
__all__ = [
- # Config
+ "CoreConfiguration",
"RiskConfig",
- "StrategyConfig",
-
- # Strategy interface
- "Strategy",
-
- # Strategy-facing domain API
+ "RiskEngine",
"StrategyState",
"MarketEvent",
+ "ControlTimeEvent",
+ "OrderSubmittedEvent",
+ "OrderExecutionFeedbackEvent",
+ "FillEvent",
"RiskConstraints",
+ "NotionalLimits",
"OrderIntent",
"NewOrderIntent",
+ "CancelOrderIntent",
"ReplaceOrderIntent",
"Price",
"Quantity",
"SlotKey",
"stable_slot_order_id",
- "EngineContext",
- "GateDecision",
- "CoreConfiguration",
+ "CandidateIntentOrigin",
+ "CandidateIntentRecord",
"ProcessingPosition",
"EventStreamEntry",
+ "process_canonical_event",
"process_event_entry",
"fold_event_stream_entries",
-
- # Version
+ "run_core_step",
+ "run_core_wakeup_reduction",
+ "run_core_wakeup_decision",
+ "run_core_wakeup_step",
+ "CoreStepStrategyContext",
+ "CoreStepStrategyEvaluator",
+ "CoreWakeupStrategyContext",
+ "CoreWakeupStrategyEvaluator",
+ "CoreExecutionControlApplyContext",
+ "CorePolicyAdmissionContext",
+ "CoreWakeupReductionResult",
+ "ExecutionControlDecision",
+ "ExecutionControlApplyContext",
+ "ExecutionControlApplyResult",
+ "ExecutionControlBlockedRecord",
+ "ExecutionControlDispatchableRecord",
+ "ExecutionControlHandledRecord",
+ "apply_execution_control_plan",
+ "PolicyRiskDecision",
+ "PolicyRejectedCandidate",
+ "PolicyAdmissionResult",
+ "CoreStepDecision",
+ "CoreStepResult",
+ "ExecutionControl",
+ "ControlSchedulingObligation",
+ "NullEventBus",
"__version__",
]
-# ----------------------------------------------------------------------
-# Package version
-# ----------------------------------------------------------------------
-
try:
__version__ = version("tradingchassis-core")
except PackageNotFoundError:
diff --git a/tradingchassis_core/core/domain/__init__.py b/tradingchassis_core/core/domain/__init__.py
index e69de29..9fbf55e 100644
--- a/tradingchassis_core/core/domain/__init__.py
+++ b/tradingchassis_core/core/domain/__init__.py
@@ -0,0 +1,55 @@
+"""Public exports for core domain value objects."""
+
+from tradingchassis_core.core.domain.candidate_intent import (
+ 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,
+ PolicyRejectedCandidate,
+ PolicyRiskDecision,
+)
+from tradingchassis_core.core.domain.processing_step import (
+ CoreExecutionControlApplyContext,
+ CorePolicyAdmissionContext,
+ CoreWakeupReductionResult,
+ run_core_step,
+ run_core_wakeup_decision,
+ run_core_wakeup_reduction,
+ run_core_wakeup_step,
+)
+from tradingchassis_core.core.domain.step_decision import CoreStepDecision
+from tradingchassis_core.core.domain.step_result import CoreStepResult
+
+__all__ = [
+ "CandidateIntentOrigin",
+ "CandidateIntentRecord",
+ "ExecutionControlDecision",
+ "ExecutionControlApplyContext",
+ "ExecutionControlApplyResult",
+ "ExecutionControlBlockedRecord",
+ "ExecutionControlDispatchableRecord",
+ "ExecutionControlHandledRecord",
+ "apply_execution_control_plan",
+ "PolicyRiskDecision",
+ "PolicyRejectedCandidate",
+ "PolicyAdmissionResult",
+ "CoreStepDecision",
+ "CoreStepResult",
+ "CoreExecutionControlApplyContext",
+ "CorePolicyAdmissionContext",
+ "CoreWakeupReductionResult",
+ "run_core_wakeup_reduction",
+ "run_core_wakeup_decision",
+ "run_core_wakeup_step",
+ "run_core_step",
+]
diff --git a/tradingchassis_core/core/domain/candidate_intent.py b/tradingchassis_core/core/domain/candidate_intent.py
new file mode 100644
index 0000000..2fba7c8
--- /dev/null
+++ b/tradingchassis_core/core/domain/candidate_intent.py
@@ -0,0 +1,26 @@
+"""Core-owned non-canonical candidate intent provenance models."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from enum import Enum
+
+from tradingchassis_core.core.domain.types import OrderIntent
+
+
+class CandidateIntentOrigin(str, Enum):
+ """Origin marker for candidate intents in one Core step."""
+
+ GENERATED = "generated"
+ QUEUED = "queued"
+
+
+@dataclass(frozen=True, slots=True)
+class CandidateIntentRecord:
+ """Non-canonical Core-step candidate record with explicit provenance."""
+
+ intent: OrderIntent
+ origin: CandidateIntentOrigin
+ logical_key: str
+ merge_index: int
+ priority: int
diff --git a/tradingchassis_core/core/domain/event_model.py b/tradingchassis_core/core/domain/event_model.py
index 126233c..9f51443 100644
--- a/tradingchassis_core/core/domain/event_model.py
+++ b/tradingchassis_core/core/domain/event_model.py
@@ -1,12 +1,4 @@
-"""Docs-aligned event taxonomy markers for core.
-
-This module is intentionally lightweight. It defines semantic markers used to
-disambiguate canonical Event Stream candidates from non-canonical artifacts in
-the current core codebase.
-
-It does not implement Event Stream append semantics, Processing Order, replay,
-or transport behavior.
-"""
+"""Canonical Event taxonomy markers for core."""
from __future__ import annotations
@@ -16,11 +8,10 @@
ControlTimeEvent,
FillEvent,
MarketEvent,
- OrderStateEvent,
+ OrderExecutionFeedbackEvent,
OrderSubmittedEvent,
)
from tradingchassis_core.core.events.events import (
- DerivedFillEvent,
DerivedPnLEvent,
ExposureDerivedEvent,
OrderStateTransitionEvent,
@@ -30,7 +21,7 @@
class CanonicalEventCategory(str, Enum):
- """Canonical Event Stream categories from docs."""
+ """Canonical Event Stream categories."""
MARKET = "market"
INTENT_RELATED = "intent_related"
@@ -42,19 +33,14 @@ class CanonicalEventCategory(str, Enum):
category.value for category in CanonicalEventCategory
)
-
-# Canonical Event Stream candidates recognized in this slice.
-# Note: FillEvent is tracked as a canonical execution-event candidate, but
-# candidate status does not imply it is newly wired into runtime flow.
CANONICAL_STREAM_CANDIDATE_CATEGORY_BY_TYPE: dict[type[object], CanonicalEventCategory] = {
MarketEvent: CanonicalEventCategory.MARKET,
OrderSubmittedEvent: CanonicalEventCategory.INTENT_RELATED,
FillEvent: CanonicalEventCategory.EXECUTION,
+ OrderExecutionFeedbackEvent: CanonicalEventCategory.EXECUTION,
ControlTimeEvent: CanonicalEventCategory.CONTROL,
}
-
-# Non-canonical telemetry / observability records.
TELEMETRY_EVENT_TYPES: frozenset[type[object]] = frozenset(
{
RiskDecisionEvent,
@@ -64,17 +50,6 @@ class CanonicalEventCategory(str, Enum):
}
)
-
-# Compatibility projection records (kept for current snapshot-driven flow).
-COMPATIBILITY_PROJECTION_TYPES: frozenset[type[object]] = frozenset(
- {
- OrderStateEvent,
- DerivedFillEvent,
- }
-)
-
-
-# Non-canonical runtime-facing control helper. This is intentionally not an Event.
NON_CANONICAL_CONTROL_HELPER_TYPES: frozenset[type[object]] = frozenset(
{ControlSchedulingObligation}
)
@@ -82,12 +57,9 @@ class CanonicalEventCategory(str, Enum):
def canonical_category_for_type(record_type: type[object]) -> CanonicalEventCategory | None:
"""Return canonical category for recognized canonical stream candidates."""
-
return CANONICAL_STREAM_CANDIDATE_CATEGORY_BY_TYPE.get(record_type)
def is_canonical_stream_candidate_type(record_type: type[object]) -> bool:
"""Return True when the type is marked as a canonical Event candidate."""
-
return record_type in CANONICAL_STREAM_CANDIDATE_CATEGORY_BY_TYPE
-
diff --git a/tradingchassis_core/core/domain/execution_control_apply.py b/tradingchassis_core/core/domain/execution_control_apply.py
new file mode 100644
index 0000000..ff614b0
--- /dev/null
+++ b/tradingchassis_core/core/domain/execution_control_apply.py
@@ -0,0 +1,336 @@
+"""Mutable Execution Control apply stage over a pure Execution Control plan."""
+
+from __future__ import annotations
+
+from collections import defaultdict
+from dataclasses import dataclass, field
+
+from tradingchassis_core.core.domain.candidate_intent import (
+ CandidateIntentOrigin,
+ CandidateIntentRecord,
+)
+from tradingchassis_core.core.domain.execution_control_decision import (
+ ExecutionControlDecision,
+)
+from tradingchassis_core.core.domain.execution_control_plan import (
+ ExecutionControlPlan,
+)
+from tradingchassis_core.core.domain.state import StrategyState
+from tradingchassis_core.core.execution_control.execution_control import ExecutionControl
+from tradingchassis_core.core.execution_control.types import ControlSchedulingObligation
+
+
+@dataclass(frozen=True, slots=True)
+class ExecutionControlApplyContext:
+ """Mutable apply inputs for one deterministic apply operation."""
+
+ state: StrategyState
+ execution_control: ExecutionControl
+ now_ts_ns_local: int
+ max_orders_per_sec: float | None = None
+ max_cancels_per_sec: float | None = None
+
+
+@dataclass(frozen=True, slots=True)
+class ExecutionControlDispatchableRecord:
+ """Candidate record selected as dispatchable in this apply pass."""
+
+ record: CandidateIntentRecord
+
+
+@dataclass(frozen=True, slots=True)
+class ExecutionControlBlockedRecord:
+ """Candidate record blocked from immediate dispatch."""
+
+ record: CandidateIntentRecord
+ reason: str
+ scheduling_obligation: ControlSchedulingObligation | None = None
+
+
+@dataclass(frozen=True, slots=True)
+class ExecutionControlHandledRecord:
+ """Candidate record fully handled by queue-local semantics."""
+
+ record: CandidateIntentRecord
+ reason: str
+
+
+@dataclass(frozen=True, slots=True)
+class ExecutionControlApplyResult:
+ """Result of mutable Execution Control apply over one plan state."""
+
+ queued_effective_records: tuple[CandidateIntentRecord, ...] = ()
+ dispatchable_records: tuple[ExecutionControlDispatchableRecord, ...] = ()
+ execution_handled_records: tuple[ExecutionControlHandledRecord, ...] = ()
+ blocked_records: tuple[ExecutionControlBlockedRecord, ...] = ()
+ control_scheduling_obligation: ControlSchedulingObligation | None = None
+ execution_control_decision: ExecutionControlDecision = field(
+ default_factory=ExecutionControlDecision
+ )
+
+ def __post_init__(self) -> None:
+ if not isinstance(self.queued_effective_records, tuple):
+ object.__setattr__(
+ self,
+ "queued_effective_records",
+ tuple(self.queued_effective_records),
+ )
+ if not isinstance(self.dispatchable_records, tuple):
+ object.__setattr__(
+ self,
+ "dispatchable_records",
+ tuple(self.dispatchable_records),
+ )
+ if not isinstance(self.execution_handled_records, tuple):
+ object.__setattr__(
+ self,
+ "execution_handled_records",
+ tuple(self.execution_handled_records),
+ )
+ if not isinstance(self.blocked_records, tuple):
+ object.__setattr__(
+ self,
+ "blocked_records",
+ tuple(self.blocked_records),
+ )
+
+
+def _float_equal(a: float, b: float) -> bool:
+ return abs(a - b) <= 1e-12
+
+
+def _select_effective_control_scheduling_obligation(
+ obligations: list[ControlSchedulingObligation],
+) -> ControlSchedulingObligation | None:
+ if not obligations:
+ return None
+ return min(
+ obligations,
+ key=lambda obligation: (
+ obligation.due_ts_ns_local,
+ obligation.obligation_key,
+ ),
+ )
+
+
+def _record_is_currently_queued(
+ state: StrategyState,
+ record: CandidateIntentRecord,
+) -> bool:
+ queue = state.queued_intents.get(record.intent.instrument)
+ if queue is None:
+ return False
+ return any(
+ queued.intent == record.intent and queued.logical_key == record.logical_key
+ for queued in queue
+ )
+
+
+def apply_execution_control_plan(
+ plan: ExecutionControlPlan,
+ context: ExecutionControlApplyContext,
+) -> ExecutionControlApplyResult:
+ """Apply mutable Execution Control semantics over planned active records.
+
+ This function mutates only StrategyState Queue data and ExecutionControl
+ rate state. It does not perform venue dispatch and does not emit canonical
+ events.
+
+ ``control_scheduling_obligation`` is selected only from **rate-limit**
+ 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).
+ """
+
+ state = context.state
+ execution_control = context.execution_control
+
+ dispatchable_records: list[ExecutionControlDispatchableRecord] = []
+ execution_handled_records: list[ExecutionControlHandledRecord] = []
+ blocked_records: list[ExecutionControlBlockedRecord] = []
+ obligations: list[ControlSchedulingObligation] = []
+
+ processed_keys: set[str] = set()
+
+ to_queue_by_instr: defaultdict[str, list] = defaultdict(list)
+ replaced_in_queue: list[tuple] = []
+ dropped_in_queue: list = []
+ queued: list = []
+ handled_in_queue: list = []
+
+ for record in plan.active_records:
+ if record.logical_key in processed_keys:
+ execution_handled_records.append(
+ ExecutionControlHandledRecord(
+ record=record,
+ reason="duplicate_candidate_record",
+ )
+ )
+ continue
+ processed_keys.add(record.logical_key)
+
+ intent = record.intent
+ instrument = intent.instrument
+
+ if record.origin == CandidateIntentOrigin.GENERATED:
+ to_queue_before = len(to_queue_by_instr[instrument])
+ handled_before = len(handled_in_queue)
+ continue_to_sendability, reject_reason = (
+ execution_control.route_pre_submission_lifecycle_and_inflight(
+ intent,
+ state=state,
+ to_queue_by_instr=to_queue_by_instr,
+ replaced_in_queue=replaced_in_queue,
+ dropped_in_queue=dropped_in_queue,
+ queued=queued,
+ handled_in_queue=handled_in_queue,
+ float_equal=_float_equal,
+ )
+ )
+ if not continue_to_sendability:
+ to_queue_after = len(to_queue_by_instr[instrument])
+ handled_after = len(handled_in_queue)
+ if reject_reason is not None:
+ blocked_records.append(
+ ExecutionControlBlockedRecord(
+ record=record,
+ reason=reject_reason,
+ )
+ )
+ continue
+ if to_queue_after > to_queue_before:
+ blocked_records.append(
+ ExecutionControlBlockedRecord(
+ record=record,
+ reason="inflight",
+ )
+ )
+ continue
+ if handled_after > handled_before:
+ execution_handled_records.append(
+ ExecutionControlHandledRecord(
+ record=record,
+ reason="queue_local_handled",
+ )
+ )
+ continue
+ execution_handled_records.append(
+ ExecutionControlHandledRecord(
+ record=record,
+ reason="handled",
+ )
+ )
+ continue
+
+ rate_result = execution_control.route_after_policy_rate_limit(
+ intent,
+ now_ts_ns_local=context.now_ts_ns_local,
+ max_orders_per_sec=context.max_orders_per_sec,
+ max_cancels_per_sec=context.max_cancels_per_sec,
+ )
+ if rate_result.stage_to_queue:
+ to_queue_by_instr[instrument].append(intent)
+ blocked_records.append(
+ ExecutionControlBlockedRecord(
+ record=record,
+ reason="rate_limit",
+ scheduling_obligation=rate_result.scheduling_obligation,
+ )
+ )
+ if rate_result.scheduling_obligation is not None:
+ obligations.append(rate_result.scheduling_obligation)
+ continue
+
+ dispatchable_records.append(ExecutionControlDispatchableRecord(record=record))
+ continue
+
+ detached = state.pop_queued_intents_for_order(
+ intent.instrument,
+ intent.client_order_id,
+ )
+ detached_intents = [queued_item.intent for queued_item in detached]
+ if not detached_intents:
+ execution_handled_records.append(
+ ExecutionControlHandledRecord(
+ record=record,
+ reason="queued_record_missing",
+ )
+ )
+ continue
+
+ if intent.intent_type in ("new", "replace") and state.has_inflight(
+ intent.instrument, intent.client_order_id
+ ):
+ state.merge_intents_into_queue(
+ instrument=intent.instrument,
+ intents=detached_intents,
+ )
+ blocked_records.append(
+ ExecutionControlBlockedRecord(
+ record=record,
+ reason="inflight",
+ )
+ )
+ continue
+
+ rate_result = execution_control.route_after_policy_rate_limit(
+ intent,
+ now_ts_ns_local=context.now_ts_ns_local,
+ max_orders_per_sec=context.max_orders_per_sec,
+ max_cancels_per_sec=context.max_cancels_per_sec,
+ )
+ if rate_result.stage_to_queue:
+ state.merge_intents_into_queue(
+ instrument=intent.instrument,
+ intents=detached_intents,
+ )
+ blocked_records.append(
+ ExecutionControlBlockedRecord(
+ record=record,
+ reason="rate_limit",
+ scheduling_obligation=rate_result.scheduling_obligation,
+ )
+ )
+ if rate_result.scheduling_obligation is not None:
+ obligations.append(rate_result.scheduling_obligation)
+ continue
+
+ dispatchable_records.append(ExecutionControlDispatchableRecord(record=record))
+
+ execution_control.merge_to_queue_per_instrument(
+ state=state,
+ to_queue_by_instr=to_queue_by_instr,
+ queued=queued,
+ replaced_in_queue=replaced_in_queue,
+ dropped_in_queue=dropped_in_queue,
+ )
+
+ queued_effective_records = tuple(
+ record
+ for record in plan.active_records
+ if _record_is_currently_queued(state, record)
+ )
+ control_scheduling_obligation = _select_effective_control_scheduling_obligation(
+ obligations
+ )
+ decision = ExecutionControlDecision(
+ queued_effective_intents=tuple(
+ record.intent for record in queued_effective_records
+ ),
+ dispatchable_intents=tuple(
+ item.record.intent for item in dispatchable_records
+ ),
+ execution_handled_intents=tuple(
+ item.record.intent for item in execution_handled_records
+ ),
+ control_scheduling_obligation=control_scheduling_obligation,
+ )
+
+ return ExecutionControlApplyResult(
+ queued_effective_records=queued_effective_records,
+ dispatchable_records=tuple(dispatchable_records),
+ execution_handled_records=tuple(execution_handled_records),
+ blocked_records=tuple(blocked_records),
+ control_scheduling_obligation=control_scheduling_obligation,
+ execution_control_decision=decision,
+ )
diff --git a/tradingchassis_core/core/domain/execution_control_decision.py b/tradingchassis_core/core/domain/execution_control_decision.py
new file mode 100644
index 0000000..e40ff8e
--- /dev/null
+++ b/tradingchassis_core/core/domain/execution_control_decision.py
@@ -0,0 +1,38 @@
+"""Core-owned Execution Control decision model."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from tradingchassis_core.core.domain.types import OrderIntent
+from tradingchassis_core.core.execution_control.types import ControlSchedulingObligation
+
+
+@dataclass(frozen=True, slots=True)
+class ExecutionControlDecision:
+ """Immutable non-canonical Execution Control outcome."""
+
+ queued_effective_intents: tuple[OrderIntent, ...] = ()
+ dispatchable_intents: tuple[OrderIntent, ...] = ()
+ execution_handled_intents: tuple[OrderIntent, ...] = ()
+ control_scheduling_obligation: ControlSchedulingObligation | None = None
+
+ def __post_init__(self) -> None:
+ if not isinstance(self.queued_effective_intents, tuple):
+ object.__setattr__(
+ self,
+ "queued_effective_intents",
+ tuple(self.queued_effective_intents),
+ )
+ if not isinstance(self.dispatchable_intents, tuple):
+ object.__setattr__(
+ self,
+ "dispatchable_intents",
+ tuple(self.dispatchable_intents),
+ )
+ if not isinstance(self.execution_handled_intents, tuple):
+ object.__setattr__(
+ self,
+ "execution_handled_intents",
+ tuple(self.execution_handled_intents),
+ )
diff --git a/tradingchassis_core/core/domain/execution_control_plan.py b/tradingchassis_core/core/domain/execution_control_plan.py
new file mode 100644
index 0000000..4b000d4
--- /dev/null
+++ b/tradingchassis_core/core/domain/execution_control_plan.py
@@ -0,0 +1,96 @@
+"""Pure, non-canonical Execution Control candidate planning scaffolds."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+
+from tradingchassis_core.core.domain.candidate_intent import CandidateIntentRecord
+from tradingchassis_core.core.domain.execution_control_decision import (
+ ExecutionControlDecision,
+)
+
+
+@dataclass(frozen=True, slots=True)
+class ExecutionControlCandidateInput:
+ """Policy-admitted candidate records for capture-only Execution Control planning."""
+
+ accepted_generated: tuple[CandidateIntentRecord, ...] = ()
+ passthrough_queued: tuple[CandidateIntentRecord, ...] = ()
+
+ def __post_init__(self) -> None:
+ if not isinstance(self.accepted_generated, tuple):
+ object.__setattr__(
+ self,
+ "accepted_generated",
+ tuple(self.accepted_generated),
+ )
+ if not isinstance(self.passthrough_queued, tuple):
+ object.__setattr__(
+ self,
+ "passthrough_queued",
+ tuple(self.passthrough_queued),
+ )
+
+
+@dataclass(frozen=True, slots=True)
+class ExecutionControlPlan:
+ """Capture-only Execution Control candidate planning result."""
+
+ active_records: tuple[CandidateIntentRecord, ...] = ()
+ queued_effective_records: tuple[CandidateIntentRecord, ...] = ()
+ dispatchable_records: tuple[CandidateIntentRecord, ...] = ()
+ execution_handled_records: tuple[CandidateIntentRecord, ...] = ()
+ execution_control_decision: ExecutionControlDecision = field(
+ default_factory=ExecutionControlDecision
+ )
+
+ def __post_init__(self) -> None:
+ if not isinstance(self.active_records, tuple):
+ object.__setattr__(self, "active_records", tuple(self.active_records))
+ if not isinstance(self.queued_effective_records, tuple):
+ object.__setattr__(
+ self,
+ "queued_effective_records",
+ tuple(self.queued_effective_records),
+ )
+ if not isinstance(self.dispatchable_records, tuple):
+ object.__setattr__(
+ self,
+ "dispatchable_records",
+ tuple(self.dispatchable_records),
+ )
+ if not isinstance(self.execution_handled_records, tuple):
+ object.__setattr__(
+ self,
+ "execution_handled_records",
+ tuple(self.execution_handled_records),
+ )
+
+
+def plan_execution_control_candidates(
+ planning_input: ExecutionControlCandidateInput,
+) -> ExecutionControlPlan:
+ """Build a deterministic, side-effect-free Execution Control plan projection."""
+
+ active_records = (
+ tuple(planning_input.accepted_generated)
+ + tuple(planning_input.passthrough_queued)
+ )
+ queued_effective_records = active_records
+ dispatchable_records: tuple[CandidateIntentRecord, ...] = ()
+ execution_handled_records: tuple[CandidateIntentRecord, ...] = ()
+
+ return ExecutionControlPlan(
+ active_records=active_records,
+ queued_effective_records=queued_effective_records,
+ dispatchable_records=dispatchable_records,
+ execution_handled_records=execution_handled_records,
+ execution_control_decision=ExecutionControlDecision(
+ queued_effective_intents=tuple(
+ record.intent for record in queued_effective_records
+ ),
+ dispatchable_intents=(),
+ execution_handled_intents=(),
+ control_scheduling_obligation=None,
+ ),
+ )
diff --git a/tradingchassis_core/core/domain/intent_combination.py b/tradingchassis_core/core/domain/intent_combination.py
new file mode 100644
index 0000000..5f575f7
--- /dev/null
+++ b/tradingchassis_core/core/domain/intent_combination.py
@@ -0,0 +1,100 @@
+"""Pure helper for Core-step candidate intent combination."""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+from tradingchassis_core.core.domain.candidate_intent import (
+ CandidateIntentOrigin,
+ CandidateIntentRecord,
+)
+from tradingchassis_core.core.domain.types import OrderIntent
+
+
+def _logical_key(intent: OrderIntent) -> str:
+ return f"order:{intent.client_order_id}"
+
+
+def _intent_priority(intent: OrderIntent) -> int:
+ if intent.intent_type == "cancel":
+ return 0
+ if intent.intent_type == "replace":
+ return 1
+ if intent.intent_type == "new":
+ return 2
+ return 9
+
+
+def _dominance_rank(intent: OrderIntent) -> int:
+ if intent.intent_type == "cancel":
+ return 3
+ if intent.intent_type == "replace":
+ return 2
+ if intent.intent_type == "new":
+ return 1
+ 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],
+ queued_intents: Sequence[OrderIntent],
+) -> tuple[CandidateIntentRecord, ...]:
+ """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.
+ """
+ merged: list[tuple[OrderIntent, CandidateIntentOrigin]] = [
+ *((intent, CandidateIntentOrigin.QUEUED) for intent in queued_intents),
+ *((intent, CandidateIntentOrigin.GENERATED) for intent in generated_intents),
+ ]
+ # key -> winning record
+ effective_by_key: dict[str, CandidateIntentRecord] = {}
+
+ for merge_index, (intent, origin) in enumerate(merged):
+ key = _logical_key(intent)
+ incoming = CandidateIntentRecord(
+ intent=intent,
+ origin=origin,
+ logical_key=key,
+ merge_index=merge_index,
+ priority=_intent_priority(intent),
+ )
+ existing = effective_by_key.get(key)
+ if existing is None:
+ effective_by_key[key] = incoming
+ continue
+
+ incoming_rank = _dominance_rank(incoming.intent)
+ existing_rank = _dominance_rank(existing.intent)
+ if incoming_rank > existing_rank:
+ effective_by_key[key] = incoming
+ continue
+ if incoming_rank < existing_rank:
+ continue
+
+ # Same-type conflict: latest in deterministic merge order wins.
+ effective_by_key[key] = incoming
+
+ ordered = sorted(
+ effective_by_key.values(),
+ key=lambda item: (item.priority, item.merge_index, item.logical_key),
+ )
+ return tuple(ordered)
diff --git a/tradingchassis_core/core/domain/order_lifecycle.py b/tradingchassis_core/core/domain/order_lifecycle.py
deleted file mode 100644
index 9a2eb1b..0000000
--- a/tradingchassis_core/core/domain/order_lifecycle.py
+++ /dev/null
@@ -1,63 +0,0 @@
-"""
-Canonical internal order lifecycle policy.
-
-This module defines a lightweight, internal-only lifecycle policy used by the
-canonical order projection. Compatibility order states are normalized into
-canonical lifecycle candidates before transition validation.
-"""
-
-from __future__ import annotations
-
-CANONICAL_ORDER_STATES: frozenset[str] = frozenset(
- {
- "submitted",
- "accepted",
- "partially_filled",
- "filled",
- "canceled",
- "rejected",
- }
-)
-
-CANONICAL_TERMINAL_ORDER_STATES: frozenset[str] = frozenset(
- {
- "filled",
- "canceled",
- "rejected",
- }
-)
-
-CANONICAL_ALLOWED_TRANSITIONS: dict[str, frozenset[str]] = {
- "submitted": frozenset({"accepted", "rejected"}),
- "accepted": frozenset({"partially_filled", "filled", "canceled"}),
- "partially_filled": frozenset({"partially_filled", "filled", "canceled"}),
- "filled": frozenset(),
- "canceled": frozenset(),
- "rejected": frozenset(),
-}
-
-_COMPAT_TO_CANONICAL: dict[str, str | None] = {
- "pending_new": None,
- "accepted": "accepted",
- "working": "accepted",
- "partially_filled": "partially_filled",
- "filled": "filled",
- "canceled": "canceled",
- "rejected": "rejected",
- "replaced": None,
- # Keep "expired" as compatibility/deferred for this slice.
- "expired": None,
-}
-
-
-def normalize_compatibility_state_to_canonical(state_type: str) -> str | None:
- """Map compatibility state values to canonical lifecycle candidates."""
- return _COMPAT_TO_CANONICAL.get(state_type)
-
-
-def is_valid_canonical_order_transition(prev_state: str, next_state: str) -> bool:
- """Return True when prev_state -> next_state is allowed canonically."""
- allowed = CANONICAL_ALLOWED_TRANSITIONS.get(prev_state)
- if allowed is None:
- return False
- return next_state in allowed
diff --git a/tradingchassis_core/core/domain/order_state_machine.py b/tradingchassis_core/core/domain/order_state_machine.py
deleted file mode 100644
index e7463c2..0000000
--- a/tradingchassis_core/core/domain/order_state_machine.py
+++ /dev/null
@@ -1,83 +0,0 @@
-"""
-Order lifecycle state machine definitions.
-
-This module defines the canonical order states and the allowed transitions
-between them. It is intentionally passive and validation-only.
-
-The state machine is snapshot-driven and designed for observability,
-debugging, and research instrumentation. It must NOT enforce behavior
-or raise exceptions in production paths.
-"""
-
-from __future__ import annotations
-
-# Terminal order states: once reached, the order is considered complete.
-ORDER_TERMINAL_STATES: frozenset[str] = frozenset(
- {
- "filled",
- "canceled",
- "expired",
- "rejected",
- }
-)
-
-
-# Allowed order state transitions.
-#
-# Key : previous state (or None if order was not previously observed)
-# Value : set of allowed next states
-#
-# Notes:
-# - The state machine is best-effort and snapshot-driven.
-# - Repeated states (e.g. partially_filled -> partially_filled) are allowed.
-# - This definition is intentionally conservative and venue-agnostic.
-ORDER_ALLOWED_TRANSITIONS: dict[str | None, frozenset[str]] = {
- None: frozenset({"pending_new"}),
-
- "pending_new": frozenset(
- {
- "accepted",
- "rejected",
- }
- ),
-
- "accepted": frozenset(
- {
- "working",
- "canceled",
- "rejected",
- }
- ),
-
- "working": frozenset(
- {
- "working",
- "partially_filled",
- "filled",
- "canceled",
- "replaced",
- }
- ),
-
- "partially_filled": frozenset(
- {
- "partially_filled",
- "filled",
- "canceled",
- "replaced",
- }
- ),
-}
-
-
-def is_terminal_state(state: str) -> bool:
- """Return True if the given state is terminal."""
- return state in ORDER_TERMINAL_STATES
-
-
-def is_valid_transition(prev_state: str | None, next_state: str) -> bool:
- """Return True if the transition prev_state -> next_state is allowed."""
- allowed = ORDER_ALLOWED_TRANSITIONS.get(prev_state)
- if allowed is None:
- return False
- return next_state in allowed
diff --git a/tradingchassis_core/core/domain/policy_risk_decision.py b/tradingchassis_core/core/domain/policy_risk_decision.py
new file mode 100644
index 0000000..19e364e
--- /dev/null
+++ b/tradingchassis_core/core/domain/policy_risk_decision.py
@@ -0,0 +1,120 @@
+"""Core-owned policy-risk decision model and policy admission helpers."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import TYPE_CHECKING, Protocol, Sequence
+
+from tradingchassis_core.core.domain.candidate_intent import (
+ CandidateIntentOrigin,
+ CandidateIntentRecord,
+)
+from tradingchassis_core.core.domain.types import OrderIntent
+
+if TYPE_CHECKING:
+ from tradingchassis_core.core.domain.state import StrategyState
+
+
+class PolicyIntentEvaluator(Protocol):
+ """Side-effect-safe policy evaluator contract for one candidate intent."""
+
+ def evaluate_policy_intent(
+ self,
+ *,
+ intent: OrderIntent,
+ state: StrategyState,
+ now_ts_ns_local: int,
+ ) -> tuple[bool, str | None]:
+ """Return (accepted, reason_if_rejected)."""
+
+
+@dataclass(frozen=True, slots=True)
+class PolicyRiskDecision:
+ """Immutable non-canonical policy admissibility projection."""
+
+ accepted_intents: tuple[OrderIntent, ...] = ()
+ rejected_intents: tuple[OrderIntent, ...] = ()
+
+ def __post_init__(self) -> None:
+ if not isinstance(self.accepted_intents, tuple):
+ object.__setattr__(self, "accepted_intents", tuple(self.accepted_intents))
+ if not isinstance(self.rejected_intents, tuple):
+ object.__setattr__(self, "rejected_intents", tuple(self.rejected_intents))
+
+
+@dataclass(frozen=True, slots=True)
+class PolicyRejectedCandidate:
+ """Generated-origin candidate denied by policy with preserved reason."""
+
+ record: CandidateIntentRecord
+ reason: str
+
+
+@dataclass(frozen=True, slots=True)
+class PolicyAdmissionResult:
+ """Result of side-effect-safe policy admission over candidate records."""
+
+ accepted_generated: tuple[CandidateIntentRecord, ...] = ()
+ rejected_generated: tuple[PolicyRejectedCandidate, ...] = ()
+ passthrough_queued: tuple[CandidateIntentRecord, ...] = ()
+ policy_risk_decision: PolicyRiskDecision = field(default_factory=PolicyRiskDecision)
+
+ def __post_init__(self) -> None:
+ if not isinstance(self.accepted_generated, tuple):
+ object.__setattr__(self, "accepted_generated", tuple(self.accepted_generated))
+ if not isinstance(self.rejected_generated, tuple):
+ object.__setattr__(self, "rejected_generated", tuple(self.rejected_generated))
+ if not isinstance(self.passthrough_queued, tuple):
+ object.__setattr__(self, "passthrough_queued", tuple(self.passthrough_queued))
+
+
+def apply_policy_to_candidate_records(
+ candidate_records: Sequence[CandidateIntentRecord],
+ *,
+ state: StrategyState,
+ now_ts_ns_local: int,
+ policy_evaluator: PolicyIntentEvaluator,
+) -> PolicyAdmissionResult:
+ """Apply policy admission to generated-origin candidates only."""
+
+ accepted_generated: list[CandidateIntentRecord] = []
+ rejected_generated: list[PolicyRejectedCandidate] = []
+ passthrough_queued: list[CandidateIntentRecord] = []
+
+ accepted_intents: list[OrderIntent] = []
+ rejected_intents: list[OrderIntent] = []
+
+ for record in candidate_records:
+ if record.origin == CandidateIntentOrigin.QUEUED:
+ passthrough_queued.append(record)
+ continue
+ if record.origin != CandidateIntentOrigin.GENERATED:
+ raise ValueError(f"Unsupported CandidateIntentOrigin: {record.origin!r}")
+
+ accepted, reason = policy_evaluator.evaluate_policy_intent(
+ intent=record.intent,
+ state=state,
+ now_ts_ns_local=now_ts_ns_local,
+ )
+ if accepted:
+ accepted_generated.append(record)
+ accepted_intents.append(record.intent)
+ continue
+
+ rejected_generated.append(
+ PolicyRejectedCandidate(
+ record=record,
+ reason=reason or "policy_rejected",
+ )
+ )
+ rejected_intents.append(record.intent)
+
+ return PolicyAdmissionResult(
+ accepted_generated=tuple(accepted_generated),
+ rejected_generated=tuple(rejected_generated),
+ passthrough_queued=tuple(passthrough_queued),
+ policy_risk_decision=PolicyRiskDecision(
+ accepted_intents=tuple(accepted_intents),
+ rejected_intents=tuple(rejected_intents),
+ ),
+ )
diff --git a/tradingchassis_core/core/domain/processing.py b/tradingchassis_core/core/domain/processing.py
index 85ac603..d15c8a0 100644
--- a/tradingchassis_core/core/domain/processing.py
+++ b/tradingchassis_core/core/domain/processing.py
@@ -1,7 +1,7 @@
-"""Minimal canonical event processing boundary for core.
+"""Minimal canonical Event processing boundary for core.
This module introduces a narrow, docs-aligned processing boundary for current
-canonical event candidates. For these candidates, ``process_canonical_event``
+canonical Event candidates. For these candidates, ``process_canonical_event``
is the preferred top-level canonical state-advance entrypoint in core.
This module is intentionally small:
@@ -29,6 +29,7 @@
ControlTimeEvent,
FillEvent,
MarketEvent,
+ OrderExecutionFeedbackEvent,
OrderSubmittedEvent,
)
@@ -99,7 +100,7 @@ def process_canonical_event(
position: ProcessingPosition | None = None,
configuration: CoreConfiguration | None = None,
) -> None:
- """Process a canonical event candidate via existing state reducers.
+ """Process a canonical Event candidate via existing state reducers.
Preferred usage for the current slice:
- use this function as the top-level canonical ingestion boundary for
@@ -110,6 +111,7 @@ def process_canonical_event(
- ``MarketEvent`` (category: ``market``)
- ``OrderSubmittedEvent`` (category: ``intent_related``)
- ``FillEvent`` (category: ``execution``)
+ - ``OrderExecutionFeedbackEvent`` (category: ``execution``)
- ``ControlTimeEvent`` (category: ``control``)
``ProcessingPosition`` is accepted as Processing Order metadata at this
@@ -121,7 +123,7 @@ def process_canonical_event(
"""
record_type = type(event)
if not is_canonical_stream_candidate_type(record_type):
- raise TypeError(f"Unsupported non-canonical event type: {record_type.__name__}")
+ raise TypeError(f"Unsupported non-canonical Event type: {record_type.__name__}")
category = canonical_category_for_type(record_type)
@@ -179,6 +181,15 @@ def process_canonical_event(
state.apply_fill_event(event)
return
+ if (
+ category == CanonicalEventCategory.EXECUTION
+ and isinstance(event, OrderExecutionFeedbackEvent)
+ ):
+ if position is not None:
+ state._advance_processing_position(position)
+ state.apply_order_execution_feedback_event(event)
+ return
+
if (
category == CanonicalEventCategory.INTENT_RELATED
and isinstance(event, OrderSubmittedEvent)
@@ -195,7 +206,7 @@ def process_canonical_event(
return
raise TypeError(
- "Unsupported canonical event candidate for this processing boundary: "
+ "Unsupported canonical Event candidate for this processing boundary: "
f"{record_type.__name__}"
)
diff --git a/tradingchassis_core/core/domain/processing_order.py b/tradingchassis_core/core/domain/processing_order.py
index dad7cc1..8872b8b 100644
--- a/tradingchassis_core/core/domain/processing_order.py
+++ b/tradingchassis_core/core/domain/processing_order.py
@@ -22,7 +22,7 @@ def __post_init__(self) -> None:
@dataclass(frozen=True, slots=True)
class EventStreamEntry:
- """Minimal envelope for canonical event processing-order input.
+ """Minimal envelope for canonical Event processing-order input.
This value object intentionally carries only:
- the causal processing-order position; and
diff --git a/tradingchassis_core/core/domain/processing_step.py b/tradingchassis_core/core/domain/processing_step.py
new file mode 100644
index 0000000..711c957
--- /dev/null
+++ b/tradingchassis_core/core/domain/processing_step.py
@@ -0,0 +1,367 @@
+"""Deterministic Core step orchestration over canonical reducer inputs."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Protocol, Sequence
+
+from tradingchassis_core.core.domain.configuration import CoreConfiguration
+from tradingchassis_core.core.domain.execution_control_apply import (
+ ExecutionControlApplyContext,
+ apply_execution_control_plan,
+)
+from tradingchassis_core.core.domain.execution_control_decision import (
+ ExecutionControlDecision,
+)
+from tradingchassis_core.core.domain.execution_control_plan import (
+ ExecutionControlCandidateInput,
+ plan_execution_control_candidates,
+)
+from tradingchassis_core.core.domain.intent_combination import (
+ combine_candidate_intent_records,
+)
+from tradingchassis_core.core.domain.policy_risk_decision import (
+ PolicyAdmissionResult,
+ PolicyIntentEvaluator,
+ apply_policy_to_candidate_records,
+)
+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.step_decision import CoreStepDecision
+from tradingchassis_core.core.domain.step_result import CoreStepResult
+from tradingchassis_core.core.domain.types import OrderIntent
+
+if TYPE_CHECKING:
+ from tradingchassis_core.core.execution_control.execution_control import ExecutionControl
+
+
+@dataclass(frozen=True, slots=True)
+class CoreStepStrategyContext:
+ """Deterministic Strategy-evaluation context for one Core step."""
+
+ state: StrategyState
+ event: object
+ position: ProcessingPosition
+ configuration: CoreConfiguration | None = None
+
+
+class CoreStepStrategyEvaluator(Protocol):
+ """Core-owned Strategy evaluation protocol for unified step semantics."""
+
+ def evaluate(self, context: CoreStepStrategyContext) -> Sequence[OrderIntent]:
+ """Evaluate Strategy once for the provided step context."""
+
+
+@dataclass(frozen=True, slots=True)
+class CoreWakeupStrategyContext:
+ """Deterministic Strategy-evaluation context for one Core wakeup batch."""
+
+ state: StrategyState
+ entries: tuple[EventStreamEntry, ...]
+ configuration: CoreConfiguration | None = None
+ last_position: ProcessingPosition | None = None
+
+
+class CoreWakeupStrategyEvaluator(Protocol):
+ """Core-owned Strategy evaluation protocol for one wakeup batch."""
+
+ def evaluate(self, context: CoreWakeupStrategyContext) -> Sequence[OrderIntent]:
+ """Evaluate Strategy once after all wakeup entries are reduced."""
+
+
+@dataclass(frozen=True, slots=True)
+class CorePolicyAdmissionContext:
+ """Optional side-effect-safe policy admission capture context."""
+
+ policy_evaluator: PolicyIntentEvaluator
+ now_ts_ns_local: int
+
+
+@dataclass(frozen=True, slots=True)
+class CoreExecutionControlApplyContext:
+ """Optional mutable Execution Control apply context for one Core step."""
+
+ execution_control: ExecutionControl
+ now_ts_ns_local: int
+ max_orders_per_sec: float | None = None
+ max_cancels_per_sec: float | None = None
+ activate_dispatchable_outputs: bool = False
+
+
+@dataclass(frozen=True, slots=True)
+class CoreWakeupReductionResult:
+ """Non-canonical reduction-phase output for one runtime wakeup."""
+
+ entries: tuple[EventStreamEntry, ...] = ()
+ generated_intents: tuple[OrderIntent, ...] = ()
+
+ def __post_init__(self) -> None:
+ if not isinstance(self.entries, tuple):
+ object.__setattr__(self, "entries", tuple(self.entries))
+ if not isinstance(self.generated_intents, tuple):
+ object.__setattr__(self, "generated_intents", tuple(self.generated_intents))
+
+
+def _resolve_candidate_instrument(*, entry: EventStreamEntry) -> str | None:
+ event_instrument = getattr(entry.event, "instrument", None)
+ if isinstance(event_instrument, str):
+ return event_instrument
+ return None
+
+
+def _to_core_step_decision(
+ *,
+ policy_result: PolicyAdmissionResult,
+ execution_control_decision: ExecutionControlDecision,
+) -> CoreStepDecision:
+ return CoreStepDecision(
+ policy_rejected_intents=tuple(
+ rejected.record.intent for rejected in policy_result.rejected_generated
+ ),
+ policy_risk_decision=policy_result.policy_risk_decision,
+ execution_control_decision=execution_control_decision,
+ queued_effective_intents=execution_control_decision.queued_effective_intents,
+ dispatchable_intents=execution_control_decision.dispatchable_intents,
+ execution_handled_intents=execution_control_decision.execution_handled_intents,
+ control_scheduling_obligation=execution_control_decision.control_scheduling_obligation,
+ )
+
+
+def run_core_step(
+ state: StrategyState,
+ entry: EventStreamEntry,
+ *,
+ configuration: CoreConfiguration | None = None,
+ policy_admission_context: CorePolicyAdmissionContext | None = None,
+ execution_control_apply_context: CoreExecutionControlApplyContext | None = None,
+ strategy_evaluator: CoreStepStrategyEvaluator | None = None,
+) -> CoreStepResult:
+ """Run one deterministic Core step."""
+ if execution_control_apply_context is not None and policy_admission_context is None:
+ raise ValueError(
+ "execution_control_apply_context requires policy_admission_context"
+ )
+
+ process_event_entry(state, entry, configuration=configuration)
+
+ generated_intents: tuple[OrderIntent, ...] = ()
+ if strategy_evaluator is not None:
+ strategy_context = CoreStepStrategyContext(
+ state=state,
+ event=entry.event,
+ position=entry.position,
+ configuration=configuration,
+ )
+ generated_intents = tuple(strategy_evaluator.evaluate(strategy_context))
+
+ queued_instrument = _resolve_candidate_instrument(entry=entry)
+ queued_snapshot = state.queued_intents_snapshot(queued_instrument)
+ candidate_intent_records = combine_candidate_intent_records(
+ generated_intents=generated_intents,
+ queued_intents=queued_snapshot,
+ )
+ candidate_intents = tuple(record.intent for record in candidate_intent_records)
+
+ if policy_admission_context is None:
+ return CoreStepResult(
+ generated_intents=generated_intents,
+ candidate_intent_records=candidate_intent_records,
+ candidate_intents=candidate_intents,
+ )
+
+ policy_result = apply_policy_to_candidate_records(
+ candidate_intent_records,
+ state=state,
+ now_ts_ns_local=policy_admission_context.now_ts_ns_local,
+ policy_evaluator=policy_admission_context.policy_evaluator,
+ )
+ execution_control_plan = plan_execution_control_candidates(
+ ExecutionControlCandidateInput(
+ accepted_generated=policy_result.accepted_generated,
+ passthrough_queued=policy_result.passthrough_queued,
+ )
+ )
+
+ apply_result = None
+ if execution_control_apply_context is not None:
+ apply_result = apply_execution_control_plan(
+ execution_control_plan,
+ ExecutionControlApplyContext(
+ state=state,
+ execution_control=execution_control_apply_context.execution_control,
+ now_ts_ns_local=execution_control_apply_context.now_ts_ns_local,
+ max_orders_per_sec=execution_control_apply_context.max_orders_per_sec,
+ max_cancels_per_sec=execution_control_apply_context.max_cancels_per_sec,
+ ),
+ )
+
+ effective_execution_control_decision = (
+ execution_control_plan.execution_control_decision
+ if apply_result is None
+ else apply_result.execution_control_decision
+ )
+ core_step_decision = _to_core_step_decision(
+ policy_result=policy_result,
+ execution_control_decision=effective_execution_control_decision,
+ )
+
+ dispatchable_intents: tuple[OrderIntent, ...] = ()
+ control_scheduling_obligation = None
+ if apply_result is not None:
+ control_scheduling_obligation = apply_result.control_scheduling_obligation
+ if (
+ execution_control_apply_context is not None
+ and execution_control_apply_context.activate_dispatchable_outputs
+ ):
+ dispatchable_intents = tuple(
+ record.record.intent for record in apply_result.dispatchable_records
+ )
+
+ return CoreStepResult(
+ generated_intents=generated_intents,
+ candidate_intent_records=candidate_intent_records,
+ candidate_intents=candidate_intents,
+ dispatchable_intents=dispatchable_intents,
+ control_scheduling_obligation=control_scheduling_obligation,
+ core_step_decision=core_step_decision,
+ )
+
+
+def run_core_wakeup_reduction(
+ state: StrategyState,
+ entries: Sequence[EventStreamEntry],
+ *,
+ configuration: CoreConfiguration | None = None,
+ wakeup_strategy_evaluator: CoreWakeupStrategyEvaluator | None = None,
+) -> CoreWakeupReductionResult:
+ """Reduce multiple canonical entries in order for one runtime wakeup."""
+ entries_tuple = tuple(entries)
+ for entry in entries_tuple:
+ process_event_entry(state, entry, configuration=configuration)
+
+ generated_intents: tuple[OrderIntent, ...] = ()
+ if wakeup_strategy_evaluator is not None:
+ last_position = entries_tuple[-1].position if entries_tuple else None
+ wakeup_context = CoreWakeupStrategyContext(
+ state=state,
+ entries=entries_tuple,
+ configuration=configuration,
+ last_position=last_position,
+ )
+ generated_intents = tuple(wakeup_strategy_evaluator.evaluate(wakeup_context))
+
+ return CoreWakeupReductionResult(
+ entries=entries_tuple,
+ generated_intents=generated_intents,
+ )
+
+
+def run_core_wakeup_decision(
+ state: StrategyState,
+ reduction: CoreWakeupReductionResult,
+ *,
+ queued_instrument: str | None = None,
+ policy_admission_context: CorePolicyAdmissionContext | None = None,
+ execution_control_apply_context: CoreExecutionControlApplyContext | None = None,
+) -> CoreStepResult:
+ """Run one wakeup-level candidate/policy/Execution Control decision phase."""
+
+ if execution_control_apply_context is not None and policy_admission_context is None:
+ raise ValueError(
+ "execution_control_apply_context requires policy_admission_context"
+ )
+
+ queued_snapshot = state.queued_intents_snapshot(queued_instrument)
+ candidate_intent_records = combine_candidate_intent_records(
+ generated_intents=reduction.generated_intents,
+ queued_intents=queued_snapshot,
+ )
+ candidate_intents = tuple(record.intent for record in candidate_intent_records)
+
+ if policy_admission_context is None:
+ return CoreStepResult(
+ generated_intents=reduction.generated_intents,
+ candidate_intent_records=candidate_intent_records,
+ candidate_intents=candidate_intents,
+ )
+
+ policy_result = apply_policy_to_candidate_records(
+ candidate_intent_records,
+ state=state,
+ now_ts_ns_local=policy_admission_context.now_ts_ns_local,
+ policy_evaluator=policy_admission_context.policy_evaluator,
+ )
+ execution_control_plan = plan_execution_control_candidates(
+ ExecutionControlCandidateInput(
+ accepted_generated=policy_result.accepted_generated,
+ passthrough_queued=policy_result.passthrough_queued,
+ )
+ )
+ apply_result = None
+ if execution_control_apply_context is not None:
+ apply_result = apply_execution_control_plan(
+ execution_control_plan,
+ ExecutionControlApplyContext(
+ state=state,
+ execution_control=execution_control_apply_context.execution_control,
+ now_ts_ns_local=execution_control_apply_context.now_ts_ns_local,
+ max_orders_per_sec=execution_control_apply_context.max_orders_per_sec,
+ max_cancels_per_sec=execution_control_apply_context.max_cancels_per_sec,
+ ),
+ )
+ effective_execution_control_decision = (
+ execution_control_plan.execution_control_decision
+ if apply_result is None
+ else apply_result.execution_control_decision
+ )
+ core_step_decision = _to_core_step_decision(
+ policy_result=policy_result,
+ execution_control_decision=effective_execution_control_decision,
+ )
+ dispatchable_intents: tuple[OrderIntent, ...] = ()
+ control_scheduling_obligation = None
+ if apply_result is not None:
+ control_scheduling_obligation = apply_result.control_scheduling_obligation
+ if (
+ execution_control_apply_context is not None
+ and execution_control_apply_context.activate_dispatchable_outputs
+ ):
+ dispatchable_intents = tuple(
+ record.record.intent for record in apply_result.dispatchable_records
+ )
+ return CoreStepResult(
+ generated_intents=reduction.generated_intents,
+ candidate_intent_records=candidate_intent_records,
+ candidate_intents=candidate_intents,
+ dispatchable_intents=dispatchable_intents,
+ control_scheduling_obligation=control_scheduling_obligation,
+ core_step_decision=core_step_decision,
+ )
+
+
+def run_core_wakeup_step(
+ state: StrategyState,
+ entries: Sequence[EventStreamEntry],
+ *,
+ configuration: CoreConfiguration | None = None,
+ wakeup_strategy_evaluator: CoreWakeupStrategyEvaluator | None = None,
+ queued_instrument: str | None = None,
+ policy_admission_context: CorePolicyAdmissionContext | None = None,
+ execution_control_apply_context: CoreExecutionControlApplyContext | None = None,
+) -> CoreStepResult:
+ """Convenience wrapper for reduction + wakeup-level decision/apply."""
+
+ reduction = run_core_wakeup_reduction(
+ state,
+ entries,
+ configuration=configuration,
+ wakeup_strategy_evaluator=wakeup_strategy_evaluator,
+ )
+ return run_core_wakeup_decision(
+ state,
+ reduction,
+ queued_instrument=queued_instrument,
+ policy_admission_context=policy_admission_context,
+ execution_control_apply_context=execution_control_apply_context,
+ )
diff --git a/tradingchassis_core/core/domain/state.py b/tradingchassis_core/core/domain/state.py
index 40de89d..6aab540 100644
--- a/tradingchassis_core/core/domain/state.py
+++ b/tradingchassis_core/core/domain/state.py
@@ -1,97 +1,33 @@
-"""Runtime strategy state management.
+"""Deterministic Core Strategy state.
-This module maintains best-effort market, account, order, and queue state
-derived from venue snapshots and events. Internal records in this module are
-derived-state structures, not canonical Event Stream records.
-
-It is intentionally stateful and optimized for correctness and determinism
-rather than minimal complexity.
+This state container keeps canonical reducer-owned data and Execution Control
+supporting structures (Queue + inflight tracking). Runtime snapshot parsing and
+venue lifecycle adaptation are intentionally out of scope for Core.
"""
-# pylint: disable=line-too-long,too-many-instance-attributes,too-many-public-methods
-# pylint: disable=missing-function-docstring,too-many-locals,too-many-arguments
-# pylint: disable=too-many-positional-arguments,too-many-return-statements
-# pylint: disable=too-many-boolean-expressions
from __future__ import annotations
from collections import deque
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Callable, Iterable
+from typing import TYPE_CHECKING, Iterable
-from tradingchassis_core.core.domain.order_lifecycle import (
- is_valid_canonical_order_transition,
- normalize_compatibility_state_to_canonical,
-)
-from tradingchassis_core.core.domain.order_state_machine import is_valid_transition
from tradingchassis_core.core.domain.processing_order import ProcessingPosition
-from tradingchassis_core.core.domain.slots import SlotKey, stable_slot_order_id
-from tradingchassis_core.core.domain.types import OrderStateEvent
-from tradingchassis_core.core.events.events import (
- DerivedFillEvent,
- DerivedPnLEvent,
- ExposureDerivedEvent,
- OrderStateTransitionEvent,
-)
if TYPE_CHECKING:
from tradingchassis_core.core.domain.types import (
ControlTimeEvent,
FillEvent,
NewOrderIntent,
+ OrderExecutionFeedbackEvent,
OrderIntent,
OrderSubmittedEvent,
)
from tradingchassis_core.core.events.event_bus import EventBus
-# ---------------------------------------------------------------------------
-# Internal state models
-#
-# These models are intentionally NOT part of the JSON-schema "source of truth".
-# They exist to hold runtime state derived from hftbacktest snapshots/events.
-# ---------------------------------------------------------------------------
-
-TERMINAL_ORDER_STATES: set[str] = {"filled", "canceled", "expired", "rejected"}
-
-_UNKNOWN_CCY: str = "UNKNOWN"
-_DEFAULT_QTY_UNIT: str = "contracts"
-
-
-@dataclass(slots=True)
-class OrderSnapshot:
- """Best-effort compatibility order projection.
-
- This snapshot-facing structure supports compatibility ingestion/projection
- flows and is not canonical lifecycle authority.
- """
-
- instrument: str
- client_order_id: str
-
- ts_ns_exch: int
- ts_ns_local: int
-
- order_type: str
- time_in_force: str
- state_type: str
-
- side: str
-
- intended_price: float
- filled_price: float
-
- intended_qty: float
- cum_filled_qty: float
- remaining_qty: float
-
- # Best-effort request marker from hftbacktest snapshots.
- # Convention: 0 indicates no in-flight request.
- req: int = 0
-
-
@dataclass(slots=True)
class QueuedIntent:
- """An intent stored for later sending (data only, no policy)."""
+ """An intent stored for later sending (data-only Queue)."""
intent: OrderIntent
queued_at_ts_ns: int
@@ -107,33 +43,17 @@ class InflightInfo:
ts_sent_ns_local: int
-@dataclass(slots=True)
-class CanonicalOrderProjection:
- """Internal canonical order lifecycle projection."""
-
- instrument: str
- client_order_id: str
- state: str
- submitted_ts_ns_local: int
- updated_ts_ns_local: int
-
-
@dataclass(slots=True)
class MarketState:
"""Best-effort market snapshot needed for risk checks."""
- # Receipt (local) time is the strategy time axis.
last_ts_ns_local: int = 0
- # Venue time is used as a tie-breaker for replacement-style updates.
last_ts_ns_exch: int = 0
-
best_bid: float = 0.0
best_ask: float = 0.0
mid: float = 0.0
-
best_bid_qty: float = 0.0
best_ask_qty: float = 0.0
-
tick_size: float = 0.0
lot_size: float = 0.0
contract_size: float = 1.0
@@ -141,7 +61,7 @@ class MarketState:
@dataclass(slots=True)
class AccountState:
- """Best-effort account values from hftbacktest state_values()."""
+ """Best-effort account values from canonical execution feedback."""
position: float = 0.0
balance: float = 0.0
@@ -149,58 +69,64 @@ class AccountState:
trading_volume: float = 0.0
trading_value: float = 0.0
num_trades: int = 0
-
equity: float = 0.0
initial_equity: float = 0.0
realized_pnl: float = 0.0
+@dataclass(slots=True)
+class WorkingOrder:
+ """Canonical in-memory view of an active order."""
+
+ 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
+
+
+@dataclass(slots=True)
+class CanonicalOrderProjection:
+ """Internal canonical order lifecycle projection."""
+
+ instrument: str
+ client_order_id: str
+ state: str
+ submitted_ts_ns_local: int
+ updated_ts_ns_local: int
+ side: str | None = None
+ intended_price: float | None = None
+ intended_qty: float | None = None
+
+
class StrategyState:
- """High-level strategy state keyed by instrument."""
+ """High-level deterministic Strategy state keyed by instrument."""
def __init__(self, event_bus: EventBus) -> None:
self._event_bus = event_bus
self.market: dict[str, MarketState] = {}
self.account: dict[str, AccountState] = {}
- self.orders: dict[str, dict[str, OrderSnapshot]] = {}
- # Accumulates OrderStateEvents since the last consumer pop.
- # Used to surface edge-events such as "replaced" to strategies.
- self.order_events: dict[str, deque[OrderStateEvent]] = {}
+ self.orders: dict[str, dict[str, WorkingOrder]] = {}
self.fills: dict[str, deque[FillEvent]] = {}
- # Best-effort idempotence for fill deltas.
- # Tracks last observed cumulative filled quantity per (instrument, client_order_id).
self.fill_cum_qty: dict[str, dict[str, float]] = {}
self.queued_intents: dict[str, deque[QueuedIntent]] = {}
-
self.inflight: dict[str, dict[str, InflightInfo]] = {}
- # Internal canonical lifecycle projection keyed by (instrument, client_order_id).
- # This projection is intentionally separate from compatibility snapshots.
self.canonical_orders: dict[tuple[str, str], CanonicalOrderProjection] = {}
-
- # Best-effort tracking of last sent intent per (instrument, client_order_id).
- # Mapping: instrument -> client_order_id -> (ts_ns_local, intent_type)
self.last_sent_intents: dict[str, dict[str, tuple[int, str]]] = {}
-
- # Rolling equity series for rolling-loss checks.
- # Stores (ts_ns_local, total_equity).
self.rolling_equity: deque[tuple[int, float]] = deque()
self._last_realized_pnl: dict[str, float] = {}
self._last_exposure: dict[str, float] = {}
-
- # Canonical monotone simulation time (local/receipt axis).
- # This is the single time reference used for gating and risk decisions.
self.last_ts_ns_local: int = 0
-
- # Migration-step Processing Order cursor metadata.
- # Private: boundary-owned by process_canonical_event only.
self._last_processing_position_index: int | None = None
- # ---- Timestamp ----
def update_timestamp(self, ts_ns_local: int) -> None:
- # Monotone simulation time: never regress.
- # Using max() here makes the policy explicit.
self.last_ts_ns_local = max(self.last_ts_ns_local, ts_ns_local)
@property
@@ -209,32 +135,17 @@ def sim_ts_ns_local(self) -> int:
return self.last_ts_ns_local
def _advance_processing_position(self, position: ProcessingPosition) -> None:
- """Advance private Processing Order cursor for positioned canonical events."""
last = self._last_processing_position_index
next_index = position.index
-
if last is not None and next_index <= last:
raise ValueError(
"Non-monotonic ProcessingPosition index: "
f"received {next_index} after {last}."
)
-
self._last_processing_position_index = next_index
def mark_intent_sent(self, instrument: str, client_order_id: str, intent_type: str) -> None:
- """Record that an intent was sent to the execution layer.
-
- This is used for best-effort inflight handling. hftbacktest provides snapshots
- (status/req) rather than explicit ACK events, so inflight is cleared heuristically
- as soon as subsequent snapshots indicate completion.
-
- Compatibility boundary:
- - This mutates internal execution-control tracking only.
- - For ``intent_type == "new"``, it seeds an internal canonical order
- projection at ``submitted`` as sidecar projection state.
- - It does not create a canonical Event Stream record.
- """
-
+ """Record that an intent was sent to the execution layer."""
bucket = self.last_sent_intents.get(instrument)
if bucket is None:
bucket = {}
@@ -247,7 +158,6 @@ def mark_intent_sent(self, instrument: str, client_order_id: str, intent_type: s
if inflight_bucket is None:
inflight_bucket = {}
self.inflight[instrument] = inflight_bucket
-
inflight_bucket[client_order_id] = InflightInfo(action=intent_type, ts_sent_ns_local=ts_now)
if intent_type != "new":
@@ -256,7 +166,6 @@ def mark_intent_sent(self, instrument: str, client_order_id: str, intent_type: s
key = (instrument, client_order_id)
if key in self.canonical_orders:
return
-
self.canonical_orders[key] = CanonicalOrderProjection(
instrument=instrument,
client_order_id=client_order_id,
@@ -266,37 +175,46 @@ def mark_intent_sent(self, instrument: str, client_order_id: str, intent_type: s
)
def apply_order_submitted_event(self, event: OrderSubmittedEvent) -> None:
- """Reduce canonical dispatch-time submitted entry into lifecycle projection.
-
- This reducer updates only internal canonical lifecycle projection state.
- It intentionally does not mutate compatibility snapshot orders, inflight
- bookkeeping, or last-sent tracking.
- """
+ """Reduce canonical submitted-entry into active-order projections."""
+ self.update_timestamp(event.ts_ns_local_dispatch)
key = (event.instrument, event.client_order_id)
projection = self.canonical_orders.get(key)
if projection is None:
- self.canonical_orders[key] = CanonicalOrderProjection(
+ projection = CanonicalOrderProjection(
instrument=event.instrument,
client_order_id=event.client_order_id,
state="submitted",
submitted_ts_ns_local=event.ts_ns_local_dispatch,
updated_ts_ns_local=event.ts_ns_local_dispatch,
)
- return
+ self.canonical_orders[key] = projection
- # Idempotent submitted-entry behavior:
- # - If already submitted, keep existing projection unchanged.
- # - If already beyond submitted, do not regress lifecycle state.
- return
+ projection.state = "submitted"
+ projection.updated_ts_ns_local = max(
+ projection.updated_ts_ns_local, event.ts_ns_local_dispatch
+ )
+ projection.side = event.side
+ projection.intended_price = event.intended_price.value
+ projection.intended_qty = event.intended_qty.value
- def apply_control_time_event(self, event: ControlTimeEvent) -> None:
- """Reduce canonical control-time event at boundary without state mutation.
+ order_bucket = self.orders.setdefault(event.instrument, {})
+ order_bucket[event.client_order_id] = WorkingOrder(
+ instrument=event.instrument,
+ client_order_id=event.client_order_id,
+ side=event.side,
+ intended_price=event.intended_price.value,
+ intended_qty=event.intended_qty.value,
+ cum_filled_qty=0.0,
+ remaining_qty=event.intended_qty.value,
+ state="submitted",
+ submitted_ts_ns_local=event.ts_ns_local_dispatch,
+ updated_ts_ns_local=event.ts_ns_local_dispatch,
+ )
+ self._clear_inflight(event.instrument, event.client_order_id)
- This first core-only slice intentionally keeps ControlTimeEvent reduction
- as a no-op for queue/rate/inflight and compatibility projections.
- """
- _ = event
- return
+ 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)
def _clear_inflight(self, instrument: str, client_order_id: str) -> None:
inflight_bucket = self.inflight.get(instrument)
@@ -305,67 +223,11 @@ def _clear_inflight(self, instrument: str, client_order_id: str) -> None:
inflight_bucket.pop(client_order_id, None)
def has_inflight(self, instrument: str, client_order_id: str) -> bool:
- """Return True if an order id currently has an inflight marker."""
inflight_bucket = self.inflight.get(instrument)
if inflight_bucket is None:
return False
return client_order_id in inflight_bucket
- def _maybe_clear_inflight_from_snapshot(self, event: OrderStateEvent) -> None:
- """Heuristically clear inflight markers based on snapshot state.
-
- This avoids leaking inflight entries when orders progress from pending to
- working/terminal states.
- """
-
- # NOTE:
- # Inflight clearing is heuristic because the venue provides snapshots,
- # not explicit ACK / completion events.
- # This is sufficient for backtest and research purposes, but does NOT
- # guarantee exact ACK ordering as in a live FIX/WebSocket venue.
-
- inflight_bucket = self.inflight.get(event.instrument)
- if inflight_bucket is None:
- return
-
- info = inflight_bucket.get(event.client_order_id)
- if info is None:
- return
-
- if event.ts_ns_local < info.ts_sent_ns_local:
- return
-
- req_val = 0
- if isinstance(event.raw, dict):
- req_val = event.raw.get("req", 0)
-
- # If the snapshot indicates no active request, clear inflight as soon
- # as the snapshot is at or after the send time.
- if req_val == 0:
- if event.state_type in TERMINAL_ORDER_STATES or event.state_type == "rejected":
- self._clear_inflight(event.instrument, event.client_order_id)
- return
-
- if event.state_type in ("accepted", "working", "partially_filled"):
- self._clear_inflight(event.instrument, event.client_order_id)
- return
-
- if event.state_type in TERMINAL_ORDER_STATES or event.state_type == "rejected":
- self._clear_inflight(event.instrument, event.client_order_id)
- return
-
- if info.action == "new" and event.state_type in ("accepted", "working", "partially_filled"):
- self._clear_inflight(event.instrument, event.client_order_id)
- return
-
- if info.action == "cancel" and event.state_type == "canceled":
- self._clear_inflight(event.instrument, event.client_order_id)
- return
-
- if info.action == "replace" and event.state_type in ("working", "partially_filled"):
- self._clear_inflight(event.instrument, event.client_order_id)
-
- # ---- Market ----
def update_market(
self,
instrument: str,
@@ -380,29 +242,16 @@ def update_market(
ts_ns_local: int,
ts_ns_exch: int,
) -> None:
- """Low-level market reducer primitive.
-
- This method applies a market snapshot update directly to internal state.
- It is intentionally preserved for compatibility and reducer-level tests.
- For canonical candidates, prefer ``process_canonical_event`` as the
- top-level canonical ingestion boundary.
- """
m = self.market.get(instrument)
if m is None:
m = MarketState()
self.market[instrument] = m
-
- # Replacement-style update policy:
- # - primary sort key: receipt time (local)
- # - tie-breaker: venue time (best-effort)
if ts_ns_local < m.last_ts_ns_local:
return
if ts_ns_local == m.last_ts_ns_local and ts_ns_exch <= m.last_ts_ns_exch:
return
-
m.last_ts_ns_local = ts_ns_local
m.last_ts_ns_exch = ts_ns_exch
-
m.best_bid = best_bid
m.best_ask = best_ask
m.best_bid_qty = best_bid_qty
@@ -410,11 +259,8 @@ def update_market(
m.tick_size = tick_size
m.lot_size = lot_size
m.contract_size = contract_size
-
- if m.best_bid > 0.0 and m.best_ask > 0.0:
- m.mid = 0.5 * (m.best_bid + m.best_ask)
- else:
- m.mid = 0.0
+ m.mid = 0.5 * (m.best_bid + m.best_ask) if m.best_bid > 0.0 and m.best_ask > 0.0 else 0.0
+ self.update_timestamp(ts_ns_local)
def _update_market_from_positioned_canonical_event(
self,
@@ -430,20 +276,12 @@ def _update_market_from_positioned_canonical_event(
ts_ns_local: int,
ts_ns_exch: int,
) -> None:
- """Apply market update for a positioned canonical event.
-
- This helper intentionally bypasses timestamp replacement no-op rules.
- It is valid only when called after canonical boundary ProcessingPosition
- monotonicity validation has accepted the event as the next causal input.
- """
m = self.market.get(instrument)
if m is None:
m = MarketState()
self.market[instrument] = m
-
m.last_ts_ns_local = ts_ns_local
m.last_ts_ns_exch = ts_ns_exch
-
m.best_bid = best_bid
m.best_ask = best_ask
m.best_bid_qty = best_bid_qty
@@ -451,11 +289,8 @@ def _update_market_from_positioned_canonical_event(
m.tick_size = tick_size
m.lot_size = lot_size
m.contract_size = contract_size
-
- if m.best_bid > 0.0 and m.best_ask > 0.0:
- m.mid = 0.5 * (m.best_bid + m.best_ask)
- else:
- m.mid = 0.0
+ m.mid = 0.5 * (m.best_bid + m.best_ask) if m.best_bid > 0.0 and m.best_ask > 0.0 else 0.0
+ self.update_timestamp(ts_ns_local)
def get_mid(self, instrument: str) -> float:
m = self.market.get(instrument)
@@ -473,7 +308,6 @@ def get_lot_size(self, instrument: str) -> float:
m = self.market.get(instrument)
return 0.0 if m is None else m.lot_size
- # ---- Account ----
def update_account(
self,
instrument: str,
@@ -488,7 +322,6 @@ def update_account(
if a is None:
a = AccountState()
self.account[instrument] = a
-
a.position = position
a.balance = balance
a.fee = fee
@@ -498,67 +331,22 @@ def update_account(
mid = self.get_mid(instrument)
a.equity = a.balance + a.position * mid * self.get_contract_size(instrument)
-
if a.initial_equity == 0.0 and mid > 0.0:
a.initial_equity = a.equity
-
a.realized_pnl = (a.equity - a.initial_equity) if a.initial_equity != 0.0 else 0.0
-
self._update_rolling_equity(ts_ns_local=self.last_ts_ns_local)
- # ---- Derived Realized PnL detection ----
- last = self._last_realized_pnl.get(instrument)
- cur = a.realized_pnl
-
- if last is None:
- # First observation: initialize baseline, no event
- self._last_realized_pnl[instrument] = cur
- elif cur != last:
- self._event_bus.emit(
- DerivedPnLEvent(
- ts_ns_local=self.last_ts_ns_local,
- instrument=instrument,
- delta_pnl=cur - last,
- cum_realized_pnl=cur,
- )
- )
- self._last_realized_pnl[instrument] = cur
-
- # ---- Derived Exposure detection ----
- mid = self.get_mid(instrument)
- contract_size = self.get_contract_size(instrument)
- exposure = a.position * mid * contract_size
-
- last_exposure = self._last_exposure.get(instrument)
-
- if last_exposure is None:
- # First observation establishes baseline
- self._last_exposure[instrument] = exposure
- elif exposure != last_exposure:
- self._event_bus.emit(
- ExposureDerivedEvent(
- ts_ns_local=self.last_ts_ns_local,
- instrument=instrument,
- exposure=exposure,
- delta_exposure=exposure - last_exposure,
- )
- )
- self._last_exposure[instrument] = exposure
-
def _update_rolling_equity(self, *, ts_ns_local: int) -> None:
if ts_ns_local <= 0:
return
-
total_equity = sum(x.equity for x in self.account.values())
dq = self.rolling_equity
-
if dq:
last_ts, last_eq = dq[-1]
if ts_ns_local < last_ts:
return
if ts_ns_local == last_ts and total_equity == last_eq:
return
-
dq.append((ts_ns_local, total_equity))
def get_total_equity(self) -> float:
@@ -567,542 +355,127 @@ def get_total_equity(self) -> float:
def get_rolling_loss(self, *, now_ts_ns_local: int, window_ns: int) -> float | None:
if window_ns <= 0:
return None
-
dq = self.rolling_equity
if not dq:
dq.append((now_ts_ns_local, self.get_total_equity()))
return 0.0
-
cutoff = now_ts_ns_local - window_ns
while len(dq) > 1 and dq[0][0] < cutoff:
dq.popleft()
-
start_ts, start_eq = dq[0]
if start_ts > now_ts_ns_local:
return None
-
cur_eq = self.get_total_equity()
return float(cur_eq - start_eq)
def get_total_pnl(self) -> float:
return float(sum(a.realized_pnl for a in self.account.values()))
- # ---- Orders ----
- @staticmethod
- def _should_drop_transition_update(cur: OrderSnapshot, event: OrderStateEvent) -> bool:
- """Return True if a late transition-style update should be ignored.
-
- Transition updates (live-style) must not be dropped purely because they
- arrived late. They should only be dropped if they are idempotent and do
- not advance the current snapshot.
-
- Accepted as progress:
- - terminal state applied when current state is non-terminal
- - increased cumulative filled quantity
- - decreased remaining quantity
- - clearing a request marker (req!=0 -> req==0)
- """
-
- if event.state_type in TERMINAL_ORDER_STATES and cur.state_type not in TERMINAL_ORDER_STATES:
- return False
-
- event_cum = 0.0
- if event.cum_filled_qty is not None:
- event_cum = event.cum_filled_qty.value
-
- if event_cum > cur.cum_filled_qty:
- return False
-
- event_remaining = 0.0
- if event.remaining_qty is not None:
- event_remaining = event.remaining_qty.value
- else:
- intended_qty = event.intended_qty.value
- event_remaining = max(0.0, intended_qty - event_cum)
-
- if event_remaining < cur.remaining_qty:
- return False
-
- current_req = 0
- if isinstance(event.raw, dict):
- try:
- current_req = event.raw["req"] # type: ignore[arg-type]
- except KeyError:
- current_req = 0
-
- if cur.req != current_req and current_req == 0:
- return False
-
- # Idempotent late update: ignore when it is strictly older.
- if event.ts_ns_local < cur.ts_ns_local:
- return True
- if event.ts_ns_local == cur.ts_ns_local and event.ts_ns_exch < cur.ts_ns_exch:
- return True
-
- # Equal/newer but not progressing: keep latest by timestamp.
- return False
-
- def apply_order_state_event(self, event: OrderStateEvent) -> None:
- """Reduce compatibility execution-feedback into snapshot-facing state.
-
- This is the compatibility reducer path for ``OrderStateEvent`` records.
- It is not canonical Event Stream processing. Internal canonical-order
- projection updates performed here are sidecar projection logic used to
- keep compatibility pathways aligned with submitted-boundary semantics.
- """
- self._advance_canonical_order_projection(event)
-
- events_bucket = self.order_events.setdefault(event.instrument, deque())
- bucket = self.orders.setdefault(event.instrument, {})
- cur = bucket.get(event.client_order_id)
-
- raw_dict: dict[str, object | None] = event.raw if isinstance(event.raw, dict) else None
-
- current_req = 0
- source = "transition"
- if raw_dict is not None:
- try:
- current_req = raw_dict["req"] # type: ignore[arg-type]
- except KeyError:
- current_req = 0
-
- try:
- source = raw_dict["source"] # type: ignore[arg-type]
- except KeyError:
- source = "transition"
-
- prev_req = cur.req if cur is not None else 0
-
- inflight_bucket = self.inflight.get(event.instrument)
- inflight_info = None if inflight_bucket is None else inflight_bucket.get(event.client_order_id)
-
- if (
- inflight_info is not None
- and inflight_info.action == "replace"
- and prev_req != 0
- and current_req == 0
- and event.state_type not in TERMINAL_ORDER_STATES
- and event.state_type != "rejected"
- ):
- replaced_event = event.model_copy(update={"state_type": "replaced"})
- events_bucket.append(replaced_event)
-
- if cur is not None:
- # Late-update policy
- #
- # - Replacement-style events ("snapshot") may be safely treated as
- # overwrites. Older snapshots must not overwrite newer snapshots.
- # - Transition-style events ("transition") should not be dropped
- # purely because they are late. A late terminal update or a late
- # fill progression must still be applied.
- is_snapshot = source == "snapshot"
-
- if is_snapshot:
- if event.ts_ns_local < cur.ts_ns_local:
- return
- if event.ts_ns_local == cur.ts_ns_local and event.ts_ns_exch < cur.ts_ns_exch:
- return
- else:
- if self._should_drop_transition_update(cur, event):
- return
-
- # Treat 'replaced' as an edge event. The order identity remains and the
- # snapshot should continue to exist in state.
- if event.state_type == "replaced":
- effective_state = "working" if cur is None else cur.state_type
- event = event.model_copy(update={"state_type": effective_state})
-
- # Order lifecycle state transition validation (observability only)
- prev_state: str | None = None if cur is None else cur.state_type
- next_state: str = event.state_type
-
- if not is_valid_transition(prev_state, next_state):
- self._event_bus.emit(
- OrderStateTransitionEvent(
- ts_ns_local=event.ts_ns_local,
- instrument=event.instrument,
- client_order_id=event.client_order_id,
- prev_state=prev_state,
- next_state=next_state,
- )
- )
-
- # Derived Fill detection (snapshot-based)
- if (
- cur is not None
- and event.cum_filled_qty is not None
- ):
- prev_cum = cur.cum_filled_qty
- new_cum = event.cum_filled_qty.value
-
- if new_cum > prev_cum:
- self._event_bus.emit(
- DerivedFillEvent(
- ts_ns_local=event.ts_ns_local,
- instrument=event.instrument,
- client_order_id=event.client_order_id,
- side=event.side,
- delta_qty=new_cum - prev_cum,
- cum_qty=new_cum,
- price=(
- event.filled_price.value
- if event.filled_price is not None
- else None
- ),
- )
- )
-
- events_bucket.append(event)
-
- intended_price = event.intended_price.value
- filled_price = event.filled_price.value if event.filled_price is not None else 0.0
-
- intended_qty = event.intended_qty.value
- cum_filled_qty = event.cum_filled_qty.value if event.cum_filled_qty is not None else 0.0
- remaining_qty = (
- event.remaining_qty.value
- if event.remaining_qty is not None
- else max(0.0, intended_qty - cum_filled_qty)
- )
-
- snap = OrderSnapshot(
- instrument=event.instrument,
- client_order_id=event.client_order_id,
- ts_ns_exch=event.ts_ns_exch,
- ts_ns_local=event.ts_ns_local,
- order_type=event.order_type,
- time_in_force=event.time_in_force,
- state_type=event.state_type,
- side=event.side,
- intended_price=intended_price,
- filled_price=filled_price,
- intended_qty=intended_qty,
- cum_filled_qty=cum_filled_qty,
- remaining_qty=remaining_qty,
- req=current_req,
- )
-
- # Clear inflight heuristically before any early return.
- self._maybe_clear_inflight_from_snapshot(event)
-
- if snap.state_type in TERMINAL_ORDER_STATES:
- bucket.pop(event.client_order_id, None)
- self._clear_inflight(event.instrument, event.client_order_id)
- last_bucket = self.last_sent_intents.get(event.instrument)
- if last_bucket is not None:
- last_bucket.pop(event.client_order_id, None)
- return
-
- bucket[event.client_order_id] = snap
-
- def _advance_canonical_order_projection(self, event: OrderStateEvent) -> None:
- key = (event.instrument, event.client_order_id)
- projection = self.canonical_orders.get(key)
- if projection is None:
- return
-
- next_canonical_state = normalize_compatibility_state_to_canonical(event.state_type)
- if next_canonical_state is None:
- return
- if event.ts_ns_local < projection.updated_ts_ns_local:
- return
- if not is_valid_canonical_order_transition(projection.state, next_canonical_state):
- return
-
- projection.state = next_canonical_state
- projection.updated_ts_ns_local = event.ts_ns_local
-
- # ---- Fills ----
-
- # NOTE:
- # Currently unused.
- # hftbacktest does not emit explicit FillEvent deltas; fills are inferred
- # indirectly from order state snapshots instead.
- # This method is reserved for event-driven backends or live trading venues
- # that provide fill-level events.
def apply_fill_event(self, event: FillEvent, *, max_keep: int = 10_000) -> None:
- """Low-level fill reducer primitive.
-
- This method applies fill deltas directly to internal state and emits the
- fill event on the bus. It remains available for compatibility and
- reducer-level parity testing. For canonical candidates, prefer
- ``process_canonical_event`` as the top-level canonical ingestion
- boundary.
- """
+ """Reduce canonical fill deltas into fill and active-order projections."""
+ self.update_timestamp(event.ts_ns_local)
instrument = event.instrument
client_order_id = event.client_order_id
- bucket = self.fill_cum_qty[instrument] if instrument in self.fill_cum_qty else None
- if bucket is None:
- bucket = {}
- self.fill_cum_qty[instrument] = bucket
-
+ bucket = self.fill_cum_qty.setdefault(instrument, {})
cum_qty = event.cum_filled_qty.value
- last_cum: float = bucket[client_order_id] if client_order_id in bucket else None
- if last_cum is not None:
- # Fill events are deltas. Duplicates commonly repeat the same cumulative filled.
- # Late/out-of-order fills can arrive with a smaller cumulative filled.
- # Both cases should be idempotent no-ops.
- if cum_qty <= last_cum + 1e-12:
- return
-
+ last_cum = bucket.get(client_order_id)
+ if last_cum is not None and cum_qty <= last_cum + 1e-12:
+ return
bucket[client_order_id] = cum_qty
- dq = self.fills[instrument] if instrument in self.fills else None
- if dq is None:
- dq = deque()
- self.fills[instrument] = dq
-
+ dq = self.fills.setdefault(instrument, deque())
dq.append(event)
while len(dq) > max_keep:
dq.popleft()
- self._event_bus.emit(event)
-
- def ingest_order_snapshots(self, instrument: str, orders_snapshot_iter: Iterable[object]) -> None:
- """Ingest hftbacktest order snapshots and reduce them into internal state.
-
- hftbacktest provides *snapshots* (not deltas). We translate each snapshot into
- an OrderStateEvent (snapshot) and feed it into apply_order_state_event().
-
- This is an adapter/materialization path for compatibility snapshot
- ingestion. It is intentionally separate from canonical ingestion.
- """
-
- def map_status(status: int, req: int, client_order_id: str) -> str:
- """Best-effort mapping from hftbacktest (status, req) to schema state.
-
- Design: terminal status wins. If a request marker is present (req!=0),
- treat this as "in-flight". In that case, "pending_new" is used only
- for in-flight NEW actions; all other in-flight actions map to "accepted".
- """
-
- if status == 3:
- return "filled"
- if status == 4:
- return "canceled"
- if status == 5:
- return "expired"
-
- if req != 0:
- inflight_bucket = self.inflight.get(instrument)
- inflight_info = None if inflight_bucket is None else inflight_bucket.get(client_order_id)
- if inflight_info is not None and inflight_info.action == "new":
- return "pending_new"
- return "accepted"
-
- if status == 0:
- return "accepted"
- if status == 1:
- return "working"
- if status == 2:
- return "partially_filled"
-
- return "rejected"
-
- # hftbacktest "values()" often returns a custom iterator with has_next/get
- if hasattr(orders_snapshot_iter, "has_next") and hasattr(orders_snapshot_iter, "get"):
- it = orders_snapshot_iter
-
- def _next() -> object | None:
- return it.get() if it.has_next() else None
-
- while True:
- o = _next()
- if o is None:
- break
- self._ingest_one_hft_order(instrument, o, map_status)
- return
+ order_bucket = self.orders.get(instrument)
+ if order_bucket is not None:
+ working = order_bucket.get(client_order_id)
+ if working is not None:
+ working.cum_filled_qty = cum_qty
+ if event.remaining_qty is not None:
+ working.remaining_qty = event.remaining_qty.value
+ else:
+ working.remaining_qty = max(0.0, working.intended_qty - cum_qty)
+ working.state = "filled" if working.remaining_qty <= 1e-12 else "partially_filled"
+ working.updated_ts_ns_local = event.ts_ns_local
+ if working.state == "filled":
+ order_bucket.pop(client_order_id, None)
+ self._clear_inflight(instrument, client_order_id)
+
+ projection = self.canonical_orders.get((instrument, client_order_id))
+ if projection is not None and event.ts_ns_local >= projection.updated_ts_ns_local:
+ if event.remaining_qty is not None and event.remaining_qty.value <= 1e-12:
+ projection.state = "filled"
+ else:
+ projection.state = "partially_filled"
+ projection.updated_ts_ns_local = event.ts_ns_local
- # Otherwise assume a normal Python iterable
- for o in orders_snapshot_iter:
- self._ingest_one_hft_order(instrument, o, map_status)
+ self._event_bus.emit(event)
- def _ingest_one_hft_order(
+ def apply_order_execution_feedback_event(
self,
- instrument: str,
- o: object,
- map_status: Callable[[int, int, int], str],
+ event: OrderExecutionFeedbackEvent,
) -> None:
- """Translate a single hftbacktest order snapshot object into an OrderStateEvent."""
-
- # --- Map primitive enums to your schema enums ---
- order_type: str = "limit" if o.order_type == 0 else "market"
-
- # hftbacktest typically uses BUY=1, SELL=-1
- side: str = "buy" if o.side == 1 else "sell"
-
- tif: int = o.time_in_force
- if tif == 0:
- time_in_force = "GTC"
- elif tif == 1:
- time_in_force = "IOC"
- elif tif == 2:
- time_in_force = "FOK"
- elif tif == 3:
- time_in_force = "POST_ONLY"
- else:
- time_in_force = "GTC"
-
- req_val: int = o.req
-
- state_type: str = map_status(o.status, req_val, o.order_id)
-
- # --- Prices / quantities (schema requires structured objects) ---
- intended_price: dict[str, str | float] = {"currency": _UNKNOWN_CCY, "value": o.price}
-
- exec_price: float = o.exec_price
- filled_price: dict[str, str | float] = None if exec_price <= 0.0 else {"currency": _UNKNOWN_CCY, "value": exec_price}
-
- intended_qty: dict[str, str | float] = {"value": o.qty, "unit": _DEFAULT_QTY_UNIT}
-
- exec_qty: float = o.exec_qty
- cum_filled_qty: dict[str, str | float] = None if exec_qty <= 0.0 else {"value": exec_qty, "unit": _DEFAULT_QTY_UNIT}
-
- leaves_qty: float = o.leaves_qty
- remaining_qty: dict[str, str | float] = {"value": leaves_qty, "unit": _DEFAULT_QTY_UNIT}
-
- event = OrderStateEvent(
- ts_ns_exch=o.exch_timestamp,
- ts_ns_local=o.local_timestamp,
- instrument=instrument,
- client_order_id=str(o.order_id),
- order_type=order_type,
- state_type=state_type,
- side=side,
- intended_price=intended_price,
- filled_price=filled_price,
- intended_qty=intended_qty,
- cum_filled_qty=cum_filled_qty,
- remaining_qty=remaining_qty,
- time_in_force=time_in_force,
- reason=None,
- raw={"status": o.status, "req": req_val, "source": "snapshot"},
+ """Reduce canonical execution feedback into account state only."""
+ self.update_timestamp(event.ts_ns_local_feedback)
+ self.update_account(
+ instrument=event.instrument,
+ position=event.position,
+ balance=event.balance,
+ fee=event.fee,
+ trading_volume=event.trading_volume,
+ trading_value=event.trading_value,
+ num_trades=event.num_trades,
)
- self.apply_order_state_event(event)
-
- def get_orders(self, instrument: str) -> dict[str, OrderSnapshot]:
- """Return active order snapshots for an instrument (read-only view)."""
- return self.orders.get(instrument, {})
-
- # NOTE:
- # Currently unused.
- # OrderStateEvents are accumulated for observability (e.g. replaced, invalid
- # transitions), but no strategy currently consumes edge-events explicitly.
- # This hook is reserved for strategies that require order lifecycle events.
- def pop_order_events(self, instrument: str) -> list[OrderStateEvent]:
- """Return and clear accumulated OrderStateEvents for an instrument.
-
- The engine calls the strategy without passing a per-event stream. Strategies
- that need edge-events (e.g. replaced) can consume them via this method.
- """
-
- dq = self.order_events.get(instrument)
- if dq is None or not dq:
- return []
-
- out: list[OrderStateEvent] = list(dq)
- dq.clear()
- return out
-
- def get_working_order_snapshot(self, instrument: str, client_order_id: str) -> OrderSnapshot | None:
- """Return an active order snapshot for an order id.
-
- Returns None if no active snapshot exists.
- """
-
+ def get_working_order_snapshot(self, instrument: str, client_order_id: str) -> WorkingOrder | None:
bucket = self.orders.get(instrument)
if bucket is None:
return None
return bucket.get(client_order_id)
- # ---- Slot helpers (multi-level quoting) ----
- def slot_client_order_id(self, slot: SlotKey, namespace: str) -> str:
- """Return the stable client_order_id for a slot."""
- return stable_slot_order_id(slot, namespace=namespace)
-
- def is_slot_busy(self, slot: SlotKey, namespace: str) -> bool:
- """Return True if a slot is busy in queued ∪ working."""
-
- client_order_id = self.slot_client_order_id(slot, namespace=namespace)
- return self.is_order_id_busy(slot.instrument, client_order_id)
-
- # ---- Queue / existence helpers (C1) ----
-
- # NOTE:
- # Currently unused.
- # SlotKey helpers are intended for slot-based / multi-level market making
- # strategies with deterministic order identifiers.
- # No slot-based strategy is implemented at this time.
- def slot_key(self, instrument: str, side: str, level_index: int) -> SlotKey:
- """Create a SlotKey for a given instrument/side/level."""
-
- return SlotKey(instrument=str(instrument), side=str(side), level_index=int(level_index))
-
- # NOTE:
- # Currently unused.
- # Deterministic slot-based order IDs are reserved for slot-driven quoting
- # strategies. Current strategies generate order IDs explicitly.
- def slot_order_id(self, slot: SlotKey, namespace: str) -> str:
- """Return the deterministic client_order_id for a slot."""
-
- return stable_slot_order_id(slot, namespace=namespace)
-
- def is_order_id_busy(self, instrument: str, client_order_id: str) -> bool:
- """Return True if an order id exists in queued ∪ working."""
-
- return bool(
- self.has_working_order(instrument, client_order_id)
- or self.has_queued_intent(instrument, client_order_id)
- )
-
- # NOTE:
- # Currently unused.
- # Slot occupancy checks are only relevant for slot-based strategies
- # that enforce one active order per slot.
- def is_slot_key_busy(self, slot: SlotKey, namespace: str) -> bool:
- """Return True if a slot has a working order or queued intent."""
-
- order_id = stable_slot_order_id(slot, namespace=namespace)
- return self.is_order_id_busy(slot.instrument, order_id)
-
def has_working_order(self, instrument: str, client_order_id: str) -> bool:
- """Check whether an active (non-terminal) order exists in working state."""
bucket = self.orders.get(instrument)
if bucket is None:
return False
return client_order_id in bucket
def has_queued_intent(self, instrument: str, client_order_id: str) -> bool:
- """Check whether any queued intent exists for the given order id."""
q = self.queued_intents.get(instrument)
if q is None:
return False
key = f"order:{client_order_id}"
return any(qi.logical_key == key for qi in q)
+ def queued_intents_snapshot(self, instrument: str | None = None) -> tuple[OrderIntent, ...]:
+ if instrument is not None:
+ q = self.queued_intents.get(instrument)
+ if q is None:
+ return ()
+ return tuple(qi.intent for qi in q)
+ snapshots: list[OrderIntent] = []
+ for instrument_key in sorted(self.queued_intents):
+ snapshots.extend(qi.intent for qi in self.queued_intents[instrument_key])
+ return tuple(snapshots)
+
def pop_queued_intents_for_order(self, instrument: str, client_order_id: str) -> list[QueuedIntent]:
- """Remove and return all queued intents for the given order id."""
q = self.queued_intents.get(instrument)
if q is None or not q:
return []
-
key = f"order:{client_order_id}"
removed: list[QueuedIntent] = []
kept: deque[QueuedIntent] = deque()
-
for qi in q:
if qi.logical_key == key:
removed.append(qi)
else:
kept.append(qi)
-
self.queued_intents[instrument] = kept
return removed
def find_queued_new_intent(self, instrument: str, client_order_id: str) -> NewOrderIntent | None:
- """Return the queued NEW intent for the given order id, if present."""
q = self.queued_intents.get(instrument)
if q is None:
return None
@@ -1113,7 +486,6 @@ def find_queued_new_intent(self, instrument: str, client_order_id: str) -> NewOr
return None
def _intent_priority(self, intent: OrderIntent) -> int:
- """Lower number means higher priority for flushing."""
if intent.intent_type == "cancel":
return 0
if intent.intent_type == "replace":
@@ -1123,12 +495,6 @@ def _intent_priority(self, intent: OrderIntent) -> int:
return 9
def _compute_logical_key(self, intent: OrderIntent) -> str:
- """Compute a stable key for queue replacement/deduplication.
-
- Contract:
- - All order lifecycle operations are keyed by client_order_id.
- - flags/target identifiers are intentionally not supported.
- """
return f"order:{intent.client_order_id}"
def merge_intents_into_queue(
@@ -1136,45 +502,26 @@ def merge_intents_into_queue(
instrument: str,
intents: Iterable[OrderIntent],
) -> tuple[list[OrderIntent], list[tuple[OrderIntent, OrderIntent]], list[OrderIntent]]:
- """Merge intents into the outbox queue with replacement semantics.
-
- This is OUTBOX DATA management only (no policy). The Risk/Gate decides what goes here.
-
- Replacement rules per logical_key:
- - CANCEL dominates:
- remove any queued NEW/REPLACE for that key and queue only CANCEL.
- if CANCEL already queued, additional CANCEL replaces the older CANCEL (keep latest).
- - REPLACE replaces queued NEW/REPLACE for that key.
- if CANCEL is queued, the REPLACE is dropped (cancel dominates).
- - NEW replaces queued NEW for that key.
- if REPLACE or CANCEL is queued, the NEW is dropped (new is obsolete).
- """
q: deque[QueuedIntent] = self.queued_intents.setdefault(instrument, deque())
-
queued: list[OrderIntent] = []
replaced_in_queue: list[tuple[OrderIntent, OrderIntent]] = []
dropped: list[OrderIntent] = []
- # Helper: find all queued entries matching key
def _matching_entries(key: str) -> list[QueuedIntent]:
return [qi for qi in q if qi.logical_key == key]
for intent in intents:
key = self._compute_logical_key(intent)
prio = self._intent_priority(intent)
-
matches = _matching_entries(key)
-
has_cancel = any(qi.intent.intent_type == "cancel" for qi in matches)
has_replace = any(qi.intent.intent_type == "replace" for qi in matches)
has_new = any(qi.intent.intent_type == "new" for qi in matches)
if intent.intent_type == "cancel":
- # Remove all existing entries for the key (including older cancel) and keep latest cancel only.
for qi in list(matches):
q.remove(qi)
replaced_in_queue.append((qi.intent, intent))
-
q.append(
QueuedIntent(
intent=intent,
@@ -1187,17 +534,13 @@ def _matching_entries(key: str) -> list[QueuedIntent]:
continue
if intent.intent_type == "replace":
- # If a cancel is already queued, replace is obsolete.
if has_cancel:
dropped.append(intent)
continue
-
- # Remove queued NEW/REPLACE for that key, keep only latest replace.
for qi in list(matches):
if qi.intent.intent_type in ("new", "replace"):
q.remove(qi)
replaced_in_queue.append((qi.intent, intent))
-
q.append(
QueuedIntent(
intent=intent,
@@ -1210,18 +553,14 @@ def _matching_entries(key: str) -> list[QueuedIntent]:
continue
if intent.intent_type == "new":
- # If cancel or replace is already queued, new is obsolete.
if has_cancel or has_replace:
dropped.append(intent)
continue
-
- # Replace only queued NEW for that key (keep latest new).
if has_new:
for qi in list(matches):
if qi.intent.intent_type == "new":
q.remove(qi)
replaced_in_queue.append((qi.intent, intent))
-
q.append(
QueuedIntent(
intent=intent,
@@ -1233,7 +572,6 @@ def _matching_entries(key: str) -> list[QueuedIntent]:
queued.append(intent)
continue
- # Unknown intent types are dropped to avoid silent weirdness.
dropped.append(intent)
return queued, replaced_in_queue, dropped
@@ -1244,48 +582,29 @@ def pop_queued_intents(
*,
max_items: int | None = None,
) -> list[OrderIntent]:
- """Pop flush candidates from the outbox queue.
-
- Ordering:
- - priority (cancel -> replace -> new)
- - FIFO by queued_at_ts_ns within same priority
-
- This function removes the selected items from the queue and returns their intents.
- Gate may decide to re-queue them again if still rate-limited.
- """
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))
- # Inflight gating: do not emit intents for order ids that currently have
- # an outbound request in flight. This keeps the queue stable and avoids
- # sending replace storms while ACKs are pending.
filtered: list[QueuedIntent] = []
for qi in items:
if self.has_inflight(instrument, qi.intent.client_order_id):
continue
filtered.append(qi)
- if max_items is None:
- selected = filtered
- else:
- if max_items <= 0:
- return []
- selected = filtered[:max_items]
+ 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/domain/step_decision.py b/tradingchassis_core/core/domain/step_decision.py
new file mode 100644
index 0000000..e06fdec
--- /dev/null
+++ b/tradingchassis_core/core/domain/step_decision.py
@@ -0,0 +1,51 @@
+"""Core-owned non-canonical decision scaffold for run_core_step results."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from tradingchassis_core.core.domain.execution_control_decision import (
+ ExecutionControlDecision,
+)
+from tradingchassis_core.core.domain.policy_risk_decision import PolicyRiskDecision
+from tradingchassis_core.core.domain.types import OrderIntent
+from tradingchassis_core.core.execution_control.types import ControlSchedulingObligation
+
+
+@dataclass(frozen=True, slots=True)
+class CoreStepDecision:
+ """Immutable scaffold decision model for integrated Core-step semantics."""
+
+ policy_rejected_intents: tuple[OrderIntent, ...] = ()
+ policy_risk_decision: PolicyRiskDecision | None = None
+ execution_control_decision: ExecutionControlDecision | None = None
+ queued_effective_intents: tuple[OrderIntent, ...] = ()
+ dispatchable_intents: tuple[OrderIntent, ...] = ()
+ execution_handled_intents: tuple[OrderIntent, ...] = ()
+ control_scheduling_obligation: ControlSchedulingObligation | None = None
+
+ def __post_init__(self) -> None:
+ if not isinstance(self.policy_rejected_intents, tuple):
+ object.__setattr__(
+ self,
+ "policy_rejected_intents",
+ tuple(self.policy_rejected_intents),
+ )
+ if not isinstance(self.queued_effective_intents, tuple):
+ object.__setattr__(
+ self,
+ "queued_effective_intents",
+ tuple(self.queued_effective_intents),
+ )
+ if not isinstance(self.dispatchable_intents, tuple):
+ object.__setattr__(
+ self,
+ "dispatchable_intents",
+ tuple(self.dispatchable_intents),
+ )
+ if not isinstance(self.execution_handled_intents, tuple):
+ object.__setattr__(
+ self,
+ "execution_handled_intents",
+ tuple(self.execution_handled_intents),
+ )
diff --git a/tradingchassis_core/core/domain/step_result.py b/tradingchassis_core/core/domain/step_result.py
new file mode 100644
index 0000000..1d1e1d4
--- /dev/null
+++ b/tradingchassis_core/core/domain/step_result.py
@@ -0,0 +1,60 @@
+"""Core step result contract model."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from tradingchassis_core.core.domain.candidate_intent import CandidateIntentRecord
+from tradingchassis_core.core.domain.step_decision import CoreStepDecision
+from tradingchassis_core.core.domain.types import OrderIntent
+from tradingchassis_core.core.execution_control.types import ControlSchedulingObligation
+
+
+@dataclass(frozen=True, slots=True)
+class CoreStepResult:
+ """Immutable result object for deterministic Core step APIs.
+
+ ``control_scheduling_obligation`` is set only when Execution Control apply
+ defers for **rate limits** (time-dependent). It is ``None`` for inflight-only
+ deferral and other cases without a Core-derived wake time. Only injected
+ ``ControlTimeEvent`` values are canonical stream input for control time.
+ """
+
+ generated_intents: tuple[OrderIntent, ...] = ()
+ candidate_intent_records: tuple[CandidateIntentRecord, ...] = ()
+ candidate_intents: tuple[OrderIntent, ...] = ()
+ dispatchable_intents: tuple[OrderIntent, ...] = ()
+ control_scheduling_obligation: ControlSchedulingObligation | None = None
+ core_step_decision: CoreStepDecision | None = None
+
+ def __post_init__(self) -> None:
+ if not isinstance(self.generated_intents, tuple):
+ object.__setattr__(
+ self,
+ "generated_intents",
+ tuple(self.generated_intents),
+ )
+ if not isinstance(self.candidate_intent_records, tuple):
+ object.__setattr__(
+ self,
+ "candidate_intent_records",
+ tuple(self.candidate_intent_records),
+ )
+ if not isinstance(self.candidate_intents, tuple):
+ object.__setattr__(
+ self,
+ "candidate_intents",
+ tuple(self.candidate_intents),
+ )
+ if self.candidate_intent_records:
+ object.__setattr__(
+ self,
+ "candidate_intents",
+ tuple(record.intent for record in self.candidate_intent_records),
+ )
+ if not isinstance(self.dispatchable_intents, tuple):
+ object.__setattr__(
+ self,
+ "dispatchable_intents",
+ tuple(self.dispatchable_intents),
+ )
diff --git a/tradingchassis_core/core/domain/types.py b/tradingchassis_core/core/domain/types.py
index a368d3e..f3261cc 100644
--- a/tradingchassis_core/core/domain/types.py
+++ b/tradingchassis_core/core/domain/types.py
@@ -1,60 +1,37 @@
-"""Core shared data models and schemas.
+"""Core shared data models.
-This module defines Pydantic models used across the system for market data,
-order intents, risk constraints, and execution feedback.
-
-Semantic notes for this refactor slice:
-- ``MarketEvent`` is a canonical Market Event candidate.
-- ``FillEvent`` is tracked as a canonical Execution Event candidate.
-- ``OrderStateEvent`` remains a compatibility execution-feedback /
- snapshot-materialization record for now.
-
-These models are treated as schema definitions and intentionally prioritize
-structural clarity over minimal class size.
+Pydantic models in this module are the source of truth for Core contracts.
"""
# pylint: disable=line-too-long,missing-class-docstring,missing-function-docstring
from __future__ import annotations
-from typing import Annotated, Any, Literal
+from typing import Annotated, Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator
-# ---------------------------------------------------------------------------
-# Common models
-# ---------------------------------------------------------------------------
-
class Money(BaseModel):
currency: str = Field(..., min_length=1)
- amount: float = Field(...,)
-
+ amount: float = Field(...)
model_config = ConfigDict(extra="forbid")
class Price(BaseModel):
currency: str = Field(..., min_length=1)
value: float = Field(..., ge=0)
-
model_config = ConfigDict(extra="forbid")
class Quantity(BaseModel):
value: float = Field(..., ge=0)
- unit: str = Field(..., min_length=1) # e.g. "shares", "contracts", "BTC"
-
+ unit: str = Field(..., min_length=1)
model_config = ConfigDict(extra="forbid")
-# ---------------------------------------------------------------------------
-# Market data models (MarketEvent + payloads)
-# ---------------------------------------------------------------------------
-
-
class BookLevel(BaseModel):
price: Price
quantity: Quantity
-
model_config = ConfigDict(extra="forbid")
@@ -62,9 +39,7 @@ class BookPayload(BaseModel):
book_type: Literal["snapshot", "delta"]
bids: list[BookLevel]
asks: list[BookLevel]
- # depth is optional in the JSON schema, but must be >= 0 when present
depth: int | None = Field(default=None, ge=0)
-
model_config = ConfigDict(extra="forbid")
@@ -73,29 +48,20 @@ class TradePayload(BaseModel):
price: Price
quantity: Quantity
trade_id: str | None = Field(default=None, min_length=1)
-
model_config = ConfigDict(extra="forbid")
class MarketEvent(BaseModel):
ts_ns_exch: int = Field(..., gt=0)
ts_ns_local: int = Field(..., gt=0)
-
instrument: str = Field(..., min_length=1)
event_type: Literal["book", "trade"]
-
book: BookPayload | None = None
trade: TradePayload | None = None
-
model_config = ConfigDict(extra="forbid")
@model_validator(mode="after")
def validate_payload_for_event_type(self) -> MarketEvent:
- """
- Enforce the conditional requirements from the JSON schema:
- - If event_type == "book": book must be present, trade must be None
- - If event_type == "trade": trade must be present, book must be None
- """
if self.event_type == "book":
if self.book is None:
raise ValueError("book payload is required when event_type is 'book'")
@@ -115,146 +81,49 @@ def is_trade(self) -> bool:
return self.event_type == "trade"
-# ---------------------------------------------------------------------------
-# Order intent models (discriminated union)
-# ---------------------------------------------------------------------------
-
-
TimeInForce = Literal["GTC", "IOC", "FOK", "POST_ONLY"]
OrderType = Literal["limit", "market"]
Side = Literal["buy", "sell"]
class OrderIntentBase(BaseModel):
- """
- Base fields shared by all order intents.
-
- Notes:
- - client_order_id maps to the execution binding's order_id and is used for new/cancel/replace.
- - intents_correlation_id is optional and can be used to link multiple intents together.
- """
-
- ts_ns_local: int = Field(
- ...,
- gt=0,
- description="Local intent timestamp in nanoseconds since Unix epoch.",
- )
- instrument: str = Field(
- ...,
- min_length=1,
- description="Instrument identifier used for routing/execution binding (e.g., symbol, asset code).",
- )
- client_order_id: str = Field(
- ...,
- min_length=1,
- description=(
- "Order identifier (maps to the execution binding's order_id). "
- "Used for new/cancel/replace. Must be unique while an order with the same ID exists."
- ),
- )
- intents_correlation_id: str | None = Field(
- default=None,
- min_length=1,
- description=(
- "Optional correlation identifier to link multiple intents "
- "(e.g., decision bundles) across the order lifecycle."
- ),
- )
-
+ ts_ns_local: int = Field(..., gt=0)
+ instrument: str = Field(..., min_length=1)
+ client_order_id: str = Field(..., min_length=1)
+ intents_correlation_id: str | None = Field(default=None, min_length=1)
model_config = ConfigDict(extra="forbid")
class NewOrderIntent(OrderIntentBase):
- """
- Create a new order.
-
- Important:
- - intended_price is required for both limit and market orders to match the execution binding signature.
- """
-
- intent_type: Literal["new"] = Field(
- "new",
- description="Intent type describing the order lifecycle action.",
- )
-
- side: Side = Field(..., description="Order side.")
- order_type: OrderType = Field(..., description="Order type.")
- intended_qty: Quantity = Field(
- ...,
- description="Intended total order quantity.",
- )
- intended_price: Price = Field(
- ...,
- description=(
- "Intended order price. Required for both limit and market orders "
- "to match the execution binding signature."
- ),
- )
- time_in_force: TimeInForce = Field(
- ...,
- description="Time in force. Required for new intents.",
- )
+ intent_type: Literal["new"] = Field("new")
+ side: Side = Field(...)
+ order_type: OrderType = Field(...)
+ intended_qty: Quantity = Field(...)
+ intended_price: Price = Field(...)
+ time_in_force: TimeInForce = Field(...)
class CancelOrderIntent(OrderIntentBase):
- """
- Cancel an existing order identified by client_order_id.
+ intent_type: Literal["cancel"] = Field("cancel")
- This intent deliberately forbids order-creation fields (side, order_type, qty, price, tif).
- """
- intent_type: Literal["cancel"] = Field(
- "cancel",
- description="Intent type describing the order lifecycle action.",
- )
+class ReplaceOrderIntent(OrderIntentBase):
+ intent_type: Literal["replace"] = Field("replace")
+ side: Side = Field(...)
+ order_type: Literal["limit"] = Field("limit")
+ intended_qty: Quantity = Field(...)
+ intended_price: Price = Field(...)
-class ReplaceOrderIntent(OrderIntentBase):
- """
- Modify an existing order (limit-only). The order ID remains the same (client_order_id).
-
- Notes:
- - order_type is constrained to 'limit'.
- - time_in_force is intentionally not present here because the execution binding does not support modifying it.
- - intended_qty is the new TOTAL quantity (not a delta).
- """
-
- intent_type: Literal["replace"] = Field(
- "replace",
- description="Intent type describing the order lifecycle action.",
- )
-
- side: Side = Field(..., description="Order side.")
- order_type: Literal["limit"] = Field(
- "limit",
- description="Order type. For replace intents this must be 'limit'.",
- )
- intended_qty: Quantity = Field(
- ...,
- description="Intended total order quantity (new total quantity, not a delta).",
- )
- intended_price: Price = Field(
- ...,
- description="Intended order price.",
- )
-
-
-# Discriminated union: Pydantic will select the correct model based on intent_type.
OrderIntent = Annotated[
NewOrderIntent | CancelOrderIntent | ReplaceOrderIntent,
Field(discriminator="intent_type"),
]
-# ---------------------------------------------------------------------------
-# Risk constraints models
-# ---------------------------------------------------------------------------
-
-
class PositionLimits(BaseModel):
currency: str = Field(..., min_length=1)
max_position: float | None = Field(default=None, ge=0)
-
model_config = ConfigDict(extra="forbid")
@@ -262,7 +131,6 @@ class NotionalLimits(BaseModel):
currency: str = Field(..., min_length=1)
max_gross_notional: float | None = Field(default=None, ge=0)
max_single_order_notional: float | None = Field(default=None, ge=0)
-
model_config = ConfigDict(extra="forbid")
@@ -271,14 +139,12 @@ class QuoteLimits(BaseModel):
max_gross_quote_notional: float | None = Field(default=None, ge=0)
max_net_quote_notional: float | None = None
max_active_quotes: int | None = Field(default=None, ge=0)
-
model_config = ConfigDict(extra="forbid")
class OrderRateLimits(BaseModel):
max_orders_per_second: float | None = Field(default=None, ge=0)
max_cancels_per_second: float | None = Field(default=None, ge=0)
-
model_config = ConfigDict(extra="forbid")
@@ -287,7 +153,6 @@ class MaxLoss(BaseModel):
max_drawdown: float = Field(..., lt=0)
rolling_loss: float | None = Field(default=None, lt=0)
rolling_loss_window: float | None = Field(default=None, gt=0)
-
model_config = ConfigDict(extra="forbid")
@@ -295,88 +160,68 @@ class RiskConstraints(BaseModel):
ts_ns_local: int = Field(..., gt=0)
scope: str = Field(..., min_length=1)
trading_enabled: bool
-
position_limits: PositionLimits | None = None
notional_limits: NotionalLimits | None = None
quote_limits: QuoteLimits | None = None
order_rate_limits: OrderRateLimits | None = None
max_loss: MaxLoss | None = None
-
extra: dict[str, str | float | bool | None] = Field(default_factory=dict)
-
model_config = ConfigDict(extra="forbid")
-# ---------------------------------------------------------------------------
-# FillEvent model (delta event)
-# ---------------------------------------------------------------------------
-
-
class FillEvent(BaseModel):
ts_ns_exch: int = Field(..., gt=0)
ts_ns_local: int = Field(..., gt=0)
-
instrument: str = Field(..., min_length=1)
client_order_id: str = Field(..., min_length=1)
-
side: Literal["buy", "sell"]
intended_price: Price | None = None
-
filled_price: Price
intended_qty: Quantity | None = None
-
cum_filled_qty: Quantity
remaining_qty: Quantity | None = None
-
time_in_force: Literal["GTC", "IOC", "FOK", "POST_ONLY"]
liquidity_flag: Literal["maker", "taker", "unknown"]
-
fee: Money | None = None
-
model_config = ConfigDict(extra="forbid")
-# ---------------------------------------------------------------------------
-# OrderSubmittedEvent model (dispatch-time submitted boundary event)
-# ---------------------------------------------------------------------------
+class OrderExecutionFeedbackEvent(BaseModel):
+ ts_ns_local_feedback: int = Field(..., gt=0)
+ instrument: str = Field(..., min_length=1)
+ position: float
+ balance: float
+ fee: float
+ trading_volume: float
+ trading_value: float
+ num_trades: int
+ runtime_correlation: dict[str, str | int | float | bool | None] | None = None
+ model_config = ConfigDict(extra="forbid")
class OrderSubmittedEvent(BaseModel):
ts_ns_local_dispatch: int = Field(..., gt=0)
-
instrument: str = Field(..., min_length=1)
client_order_id: str = Field(..., min_length=1)
-
side: Literal["buy", "sell"]
order_type: Literal["limit", "market"]
-
intended_price: Price
intended_qty: Quantity
time_in_force: Literal["GTC", "IOC", "FOK", "POST_ONLY"]
-
intent_correlation_id: str | None = Field(default=None, min_length=1)
dispatch_attempt_id: str | None = Field(default=None, min_length=1)
runtime_correlation: dict[str, str | int | float | bool | None] | None = None
-
model_config = ConfigDict(extra="forbid")
-# ---------------------------------------------------------------------------
-# ControlTimeEvent model (runtime-realized control-time canonical event)
-# ---------------------------------------------------------------------------
-
-
class ControlTimeEvent(BaseModel):
ts_ns_local_control: int = Field(..., gt=0)
reason: str = Field(..., min_length=1)
-
due_ts_ns_local: int | None = Field(default=None, gt=0)
realized_ts_ns_local: int | None = Field(default=None, gt=0)
-
obligation_reason: str | None = Field(default=None, min_length=1)
obligation_due_ts_ns_local: int | None = Field(default=None, gt=0)
runtime_correlation: dict[str, str | int | float | bool | None] | None = None
-
model_config = ConfigDict(extra="forbid")
@model_validator(mode="after")
@@ -386,51 +231,3 @@ def validate_due_or_realized_present(self) -> ControlTimeEvent:
"at least one of due_ts_ns_local or realized_ts_ns_local is required"
)
return self
-
-
-# ---------------------------------------------------------------------------
-# OrderStateEvent model (snapshot event)
-# ---------------------------------------------------------------------------
-
-
-class OrderStateEvent(BaseModel):
- """Compatibility execution-feedback / snapshot-materialization record.
-
- ``OrderStateEvent`` remains non-canonical in this slice. It exists for
- compatibility ingestion/projection flows and must not be interpreted as a
- canonical Event Stream record.
- """
- ts_ns_exch: int = Field(..., gt=0)
- ts_ns_local: int = Field(..., gt=0)
-
- instrument: str = Field(..., min_length=1)
- client_order_id: str = Field(..., min_length=1)
-
- order_type: Literal["limit", "market"]
- state_type: Literal[
- "pending_new",
- "accepted",
- "working",
- "partially_filled",
- "filled",
- "canceled",
- "expired",
- "rejected",
- "replaced",
- ]
-
- side: Literal["buy", "sell"]
- intended_price: Price
-
- filled_price: Price | None = None
- intended_qty: Quantity
-
- cum_filled_qty: Quantity | None = None
- remaining_qty: Quantity | None = None
-
- time_in_force: Literal["GTC", "IOC", "FOK", "POST_ONLY"]
-
- reason: str | None = Field(default=None, min_length=1)
- raw: dict[str, Any | None] = None
-
- model_config = ConfigDict(extra="forbid")
diff --git a/tradingchassis_core/core/events/event_bus.py b/tradingchassis_core/core/events/event_bus.py
index 5cf493d..59eb396 100644
--- a/tradingchassis_core/core/events/event_bus.py
+++ b/tradingchassis_core/core/events/event_bus.py
@@ -25,7 +25,7 @@ def register(self, sink: EventSink) -> None:
self._sinks.append(sink)
def emit(self, event: Any) -> None:
- """Emit an event to all sinks."""
+ """Emit an Event to all sinks."""
for sink in self._sinks:
sink.on_event(event)
diff --git a/tradingchassis_core/core/events/event_sink.py b/tradingchassis_core/core/events/event_sink.py
index 49a8b3d..3040040 100644
--- a/tradingchassis_core/core/events/event_sink.py
+++ b/tradingchassis_core/core/events/event_sink.py
@@ -10,4 +10,4 @@
class EventSink(Protocol):
def on_event(self, event: Any) -> None:
- """Consume a domain event."""
+ """Consume a domain Event."""
diff --git a/tradingchassis_core/core/events/events.py b/tradingchassis_core/core/events/events.py
index 4255fa5..36d8155 100644
--- a/tradingchassis_core/core/events/events.py
+++ b/tradingchassis_core/core/events/events.py
@@ -1,14 +1,5 @@
-"""Non-canonical telemetry and compatibility event records.
+"""Non-canonical telemetry records."""
-This module is intentionally separate from canonical Event Stream candidates.
-Records defined here are used for observability and compatibility projections:
-
-- telemetry / observability records (e.g. risk summaries, derived metrics)
-- compatibility projection artifacts (e.g. inferred fill deltas)
-
-These records are transport payloads for local sinks and must not be interpreted
-as canonical Event Stream semantics by default.
-"""
from __future__ import annotations
from dataclasses import dataclass
@@ -16,10 +7,8 @@
@dataclass(slots=True)
class OrderStateTransitionEvent:
- """Observability payload for invalid/edge order-state transitions.
+ """Observability payload for unexpected order-state transitions."""
- Telemetry only; not a canonical Event Stream record.
- """
ts_ns_local: int
instrument: str
client_order_id: str
@@ -27,62 +16,31 @@ class OrderStateTransitionEvent:
next_state: str
-@dataclass(slots=True)
-class DerivedFillEvent:
- """Inferred compatibility projection artifact.
-
- This record is derived from snapshot progression and is not a canonical
- ``FillEvent`` or canonical Event Stream record.
- """
- ts_ns_local: int
- instrument: str
- client_order_id: str
-
- side: str
-
- delta_qty: float
- cum_qty: float
-
- price: float | None
-
-
@dataclass(slots=True)
class DerivedPnLEvent:
- """Observability payload for derived realized-PnL changes.
+ """Observability payload for derived realized-PnL changes."""
- Telemetry only; not a canonical Event Stream record.
- """
ts_ns_local: int
instrument: str
-
delta_pnl: float
cum_realized_pnl: float
@dataclass(slots=True)
class ExposureDerivedEvent:
- """Observability payload for derived exposure changes.
+ """Observability payload for derived exposure changes."""
- Telemetry only; not a canonical Event Stream record.
- """
ts_ns_local: int
instrument: str
-
exposure: float
delta_exposure: float
@dataclass(slots=True)
class RiskDecisionEvent:
- """Observability payload summarizing risk/gate outcomes.
+ """Observability payload summarizing policy-risk outcomes."""
- Telemetry only; not a canonical Event Stream record.
- """
ts_ns_local: int
-
accepted: int
- queued: int
rejected: int
- handled: int
-
reject_reasons: dict[str, int]
diff --git a/tradingchassis_core/core/execution_control/__init__.py b/tradingchassis_core/core/execution_control/__init__.py
index 1a3be4a..7f123df 100644
--- a/tradingchassis_core/core/execution_control/__init__.py
+++ b/tradingchassis_core/core/execution_control/__init__.py
@@ -1,8 +1,12 @@
"""Execution control (internal).
-This package intentionally hosts internal components that govern queue admission,
-inflight gating, and timing/rate limiting, while keeping RiskEngine focused on
-policy decisions.
+This package hosts internal components that govern Queue admission, inflight
+gating, and rate limiting. Policy admission stays in the Risk Engine / domain
+layer.
+
+``ControlSchedulingObligation`` (in ``types``) is a non-canonical scheduling hint
+for **rate-limit** deferral only in the current Core slice; **inflight** deferral
+does not emit that obligation by default. See ``docs/flows/control-time-and-scheduling.md``.
"""
from tradingchassis_core.core.execution_control.execution_control import ExecutionControl
diff --git a/tradingchassis_core/core/execution_control/execution_control.py b/tradingchassis_core/core/execution_control/execution_control.py
index 66f47e4..b7de265 100644
--- a/tradingchassis_core/core/execution_control/execution_control.py
+++ b/tradingchassis_core/core/execution_control/execution_control.py
@@ -1,10 +1,12 @@
"""Execution control (internal extraction from RiskEngine).
Owns:
-- token bucket rate limiting state & math
-- inflight gating that routes NEW/REPLACE to queue
-- queue admission via StrategyState.merge_intents_into_queue(...)
-- queue-only local handling for certain CANCEL/REPLACE cases
+- token bucket rate limiting state & math (time-dependent deferral may surface
+ a ``ControlSchedulingObligation`` from the apply stage; see docs on control time)
+- inflight gating that routes NEW/REPLACE to Queue (feedback-dependent; no
+ scheduling obligation by default)
+- Queue admission via StrategyState.merge_intents_into_queue(...)
+- Queue-only local handling for certain CANCEL/REPLACE cases
"""
from __future__ import annotations
@@ -15,7 +17,7 @@
from typing import TYPE_CHECKING, Callable
from tradingchassis_core.core.domain.reject_reasons import RejectReason
-from tradingchassis_core.core.domain.types import NewOrderIntent, OrderIntent
+from tradingchassis_core.core.domain.types import NewOrderIntent, OrderIntent, ReplaceOrderIntent
from tradingchassis_core.core.execution_control.types import ControlSchedulingObligation
if TYPE_CHECKING:
@@ -101,8 +103,10 @@ def route_after_policy_rate_limit(
accept_now=False,
stage_to_queue=True,
scheduling_obligation=ControlSchedulingObligation(
- ts_ns_local=wake_ts,
+ due_ts_ns_local=wake_ts,
reason="rate_limit",
+ scope_key=f"instrument:{it.instrument}",
+ source="execution_control_rate_limit",
),
)
return _RateRoutingResult(
@@ -120,8 +124,10 @@ def route_after_policy_rate_limit(
accept_now=False,
stage_to_queue=True,
scheduling_obligation=ControlSchedulingObligation(
- ts_ns_local=wake_ts,
+ due_ts_ns_local=wake_ts,
reason="rate_limit",
+ scope_key=f"instrument:{it.instrument}",
+ source="execution_control_rate_limit",
),
)
@@ -165,6 +171,8 @@ def route_pre_submission_lifecycle_and_inflight(
has_queued = state.has_queued_intent(it.instrument, it.client_order_id)
if it.intent_type == "replace":
+ if not isinstance(it, ReplaceOrderIntent):
+ return False, RejectReason.INVALID_QTY
if has_working:
working = state.get_working_order_snapshot(it.instrument, it.client_order_id)
if working is not None:
@@ -206,6 +214,8 @@ def route_pre_submission_lifecycle_and_inflight(
return False, RejectReason.ORDER_NOT_FOUND
if it.intent_type == "replace":
+ if not isinstance(it, ReplaceOrderIntent):
+ return False, RejectReason.INVALID_QTY
if not has_working:
queued_new = state.find_queued_new_intent(it.instrument, it.client_order_id)
if queued_new is None:
@@ -251,7 +261,7 @@ def handle_cancel_against_queued_only_state(
def handle_replace_against_queued_new(
self,
- it: OrderIntent,
+ it: ReplaceOrderIntent,
*,
state: StrategyState,
queued_new: NewOrderIntent,
@@ -260,12 +270,13 @@ def handle_replace_against_queued_new(
queued: list[OrderIntent],
handled_in_queue: list[OrderIntent],
) -> None:
- """REPLACE acting on queued NEW: transform into updated NEW in the queue."""
+ """REPLACE acting on queued NEW: transform into updated NEW in the Queue."""
removed = state.pop_queued_intents_for_order(it.instrument, it.client_order_id)
for qi in removed:
replaced_in_queue.append((qi.intent, it))
updated_new = NewOrderIntent(
+ intent_type="new",
ts_ns_local=it.ts_ns_local,
instrument=it.instrument,
client_order_id=it.client_order_id,
@@ -290,7 +301,7 @@ def handle_replace_against_queued_new(
@staticmethod
def is_replace_noop_against_working(
*,
- replace_intent: OrderIntent,
+ replace_intent: ReplaceOrderIntent,
working_intended_price: float,
working_intended_qty: float,
float_equal: Callable[[float, float], bool],
@@ -302,7 +313,7 @@ def is_replace_noop_against_working(
@staticmethod
def is_replace_noop_against_queued_new(
*,
- replace_intent: OrderIntent,
+ replace_intent: ReplaceOrderIntent,
queued_new: NewOrderIntent,
float_equal: Callable[[float, float], bool],
) -> bool:
diff --git a/tradingchassis_core/core/execution_control/types.py b/tradingchassis_core/core/execution_control/types.py
index 7d15a32..ae44282 100644
--- a/tradingchassis_core/core/execution_control/types.py
+++ b/tradingchassis_core/core/execution_control/types.py
@@ -16,6 +16,25 @@ class ControlSchedulingObligation:
This is a derived control signal (not an Event) and does not mutate State.
"""
- ts_ns_local: int
+ due_ts_ns_local: int
reason: str
+ scope_key: str
+ source: str
+ obligation_key: str = ""
+
+ def __post_init__(self) -> None:
+ if self.obligation_key:
+ return
+ object.__setattr__(
+ self,
+ "obligation_key",
+ (
+ f"{self.source}|{self.scope_key}|{self.reason}|{self.due_ts_ns_local}"
+ ),
+ )
+
+ @property
+ def ts_ns_local(self) -> int:
+ """Compatibility alias for pre-16F callers/tests."""
+ return self.due_ts_ns_local
diff --git a/tradingchassis_core/core/ports/__init__.py b/tradingchassis_core/core/ports/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tradingchassis_core/core/ports/engine_context.py b/tradingchassis_core/core/ports/engine_context.py
deleted file mode 100644
index 57d0b83..0000000
--- a/tradingchassis_core/core/ports/engine_context.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from __future__ import annotations
-
-from typing import Protocol
-
-
-class EngineContext(Protocol):
- """Read-only execution context exposed to strategies.
-
- This context intentionally hides engine / runtime specifics.
- """
-
- @property
- def tick_size(self) -> float:
- """Minimum price increment."""
diff --git a/tradingchassis_core/core/ports/venue_adapter.py b/tradingchassis_core/core/ports/venue_adapter.py
deleted file mode 100644
index 2bdf756..0000000
--- a/tradingchassis_core/core/ports/venue_adapter.py
+++ /dev/null
@@ -1,35 +0,0 @@
-"""Venue adapter protocol for strategy execution.
-
-This module defines the abstract venue-facing boundary used by the strategy
-loop. Concrete implementations adapt specific backtest or live venues to
-this protocol.
-"""
-
-from __future__ import annotations
-
-from typing import Any, Protocol
-
-
-class VenueAdapter(Protocol):
- """Venue-facing feed boundary.
-
- The strategy loop must not depend on venue-specific APIs.
-
- Snapshot objects are intentionally typed as Any: they are only consumed by
- venue-specific translation code, not by strategy/risk/state layers.
- """
-
- def wait_next(self, *, timeout_ns: int, include_order_resp: bool) -> int:
- """Block until the next wakeup, returning a venue-defined rc code."""
-
- def current_timestamp_ns(self) -> int:
- """Return the venue local/receipt timestamp axis in ns."""
-
- def read_market_snapshot(self) -> Any:
- """Return the current market snapshot object (venue-specific)."""
-
- def read_orders_snapshot(self) -> tuple[Any, Any]:
- """Return a tuple (state_values, orders) (venue-specific)."""
-
- def record(self, recorder: Any) -> None:
- """Record the current venue state into the recorder (if supported)."""
diff --git a/tradingchassis_core/core/ports/venue_policy.py b/tradingchassis_core/core/risk/execution_constraints_policy.py
similarity index 61%
rename from tradingchassis_core/core/ports/venue_policy.py
rename to tradingchassis_core/core/risk/execution_constraints_policy.py
index 1640b98..5a08003 100644
--- a/tradingchassis_core/core/ports/venue_policy.py
+++ b/tradingchassis_core/core/risk/execution_constraints_policy.py
@@ -1,9 +1,7 @@
-"""Venue policy normalization and validation logic.
+"""Execution-constraint normalization and validation logic.
-This module applies minimal, venue-agnostic constraints to order intents,
-such as tick/lot rounding, post-only enforcement, and minimum notional checks.
-The logic is intentionally explicit and branch-heavy to preserve correctness
-and debuggability.
+This module applies instrument/execution constraints to order intents, such as
+tick/lot rounding, post-only enforcement, and minimum notional checks.
"""
from __future__ import annotations
@@ -13,7 +11,13 @@
from typing import TYPE_CHECKING
from tradingchassis_core.core.domain.reject_reasons import RejectReason
-from tradingchassis_core.core.domain.types import NewOrderIntent, OrderIntent
+from tradingchassis_core.core.domain.types import (
+ NewOrderIntent,
+ OrderIntent,
+ Price,
+ Quantity,
+ ReplaceOrderIntent,
+)
if TYPE_CHECKING:
from tradingchassis_core.core.domain.state import StrategyState
@@ -21,27 +25,15 @@
@dataclass(slots=True)
class NormalizationOutcome:
- """Result of venue policy normalization."""
+ """Result of execution-constraint normalization."""
normalized: OrderIntent | None
reject_reason: str | None
dropped: bool
-class VenuePolicy:
- """Minimal venue policy layer.
-
- Scope (kept intentionally small):
- - tick rounding for limit prices
- - lot rounding for intended quantity
-
- Venue-specific constraints can be enabled in a minimal form:
- - post-only crossing checks (best-effort using top-of-book from state)
- - min-notional checks
-
- The policy remains best-effort and intentionally avoids venue-specific
- edge cases (self-trade prevention, reduce-only, advanced price sliding).
- """
+class ExecutionConstraintsPolicy:
+ """Minimal instrument/execution constraints layer."""
def __init__(
self,
@@ -50,30 +42,25 @@ def __init__(
post_only_mode: str = "reject",
) -> None:
self._min_order_notional = float(min_order_notional)
-
mode = str(post_only_mode)
if mode not in {"reject", "drop"}:
raise ValueError(f"Invalid post_only_mode: {mode}")
self._post_only_mode = mode
- # pylint: disable=too-many-return-statements
def normalize_intent(self, intent: OrderIntent, state: StrategyState) -> NormalizationOutcome:
- """Normalize an intent according to venue constraints.
-
- Returns:
- NormalizationOutcome with one of:
- - normalized != None: normalized intent
- - reject_reason != None: hard reject
- - dropped == True: no-op intent (e.g. qty rounds to 0)
- """
-
if intent.intent_type == "cancel":
return NormalizationOutcome(normalized=intent, reject_reason=None, dropped=False)
+ if not isinstance(intent, (NewOrderIntent, ReplaceOrderIntent)):
+ return NormalizationOutcome(
+ normalized=None,
+ reject_reason=RejectReason.INVALID_QTY,
+ dropped=False,
+ )
+
tick_size = state.get_tick_size(intent.instrument)
lot_size = state.get_lot_size(intent.instrument)
-
- qty = 0.0 if intent.intended_qty is None else float(intent.intended_qty.value)
+ qty = float(intent.intended_qty.value)
qty_norm = self._round_qty(qty, lot_size)
if qty_norm <= 0.0:
return NormalizationOutcome(normalized=None, reject_reason=None, dropped=True)
@@ -86,19 +73,19 @@ def normalize_intent(self, intent: OrderIntent, state: StrategyState) -> Normali
reject_reason=RejectReason.INVALID_LIMIT_PRICE,
dropped=False,
)
-
- px = float(intent.intended_price.value)
- px_norm = self._round_price(px, tick_size, side=intent.side)
+ px_norm = self._round_price(
+ float(intent.intended_price.value), tick_size, side=intent.side
+ )
if px_norm is None or px_norm <= 0.0:
return NormalizationOutcome(
normalized=None,
reject_reason=RejectReason.INVALID_LIMIT_PRICE,
dropped=False,
)
-
- post_only_outcome = self._enforce_post_only(intent, state, px_norm)
- if post_only_outcome is not None:
- return post_only_outcome
+ if isinstance(intent, NewOrderIntent):
+ post_only_outcome = self._enforce_post_only(intent, state, px_norm)
+ if post_only_outcome is not None:
+ return post_only_outcome
min_notional_outcome = self._enforce_min_notional(intent, state, qty_norm, px_norm)
if min_notional_outcome is not None:
@@ -110,47 +97,32 @@ def normalize_intent(self, intent: OrderIntent, state: StrategyState) -> Normali
reject_reason=None,
dropped=False,
)
-
- # replace
return NormalizationOutcome(
normalized=self._clone_replace(intent, qty_norm, px_norm),
reject_reason=None,
dropped=False,
)
- # pylint: disable=too-many-return-statements
def _enforce_post_only(
self,
- intent: OrderIntent,
+ intent: NewOrderIntent,
state: StrategyState,
px_norm: float,
) -> NormalizationOutcome | None:
- if intent.intent_type != "new":
- return None
if intent.time_in_force != "POST_ONLY":
return None
-
market = state.market[intent.instrument] if intent.instrument in state.market else None
if market is None:
return None
-
best_bid = float(market.best_bid)
best_ask = float(market.best_ask)
if best_bid <= 0.0 or best_ask <= 0.0:
return None
-
- would_cross = False
- if intent.side == "buy":
- would_cross = px_norm >= best_ask
- else:
- would_cross = px_norm <= best_bid
-
+ would_cross = px_norm >= best_ask if intent.side == "buy" else px_norm <= best_bid
if not would_cross:
return None
-
if self._post_only_mode == "drop":
return NormalizationOutcome(normalized=None, reject_reason=None, dropped=True)
-
return NormalizationOutcome(
normalized=None,
reject_reason=RejectReason.POST_ONLY_WOULD_TRADE,
@@ -159,30 +131,25 @@ def _enforce_post_only(
def _enforce_min_notional(
self,
- intent: OrderIntent,
+ intent: NewOrderIntent | ReplaceOrderIntent,
state: StrategyState,
qty_norm: float,
px_norm: float | None,
) -> NormalizationOutcome | None:
if self._min_order_notional <= 0.0:
return None
-
price = px_norm
if intent.order_type == "market":
mid = float(state.get_mid(intent.instrument))
if mid <= 0.0:
return None
price = mid
-
if price is None or price <= 0.0:
return None
-
contract_size = float(state.get_contract_size(intent.instrument))
notional = float(price) * float(qty_norm) * contract_size
-
if notional + 1e-12 >= self._min_order_notional:
return None
-
return NormalizationOutcome(
normalized=None,
reject_reason=RejectReason.MIN_NOTIONAL,
@@ -203,44 +170,39 @@ def _round_price(price: float, tick_size: float, *, side: str) -> float | None:
return None
if tick_size <= 0.0:
return float(price)
-
ticks = price / tick_size
- if side == "buy":
- rounded = math.floor(ticks) * tick_size
- else:
- rounded = math.ceil(ticks) * tick_size
+ rounded = math.floor(ticks) * tick_size if side == "buy" else math.ceil(ticks) * tick_size
return float(rounded)
@staticmethod
- def _clone_new(intent: OrderIntent, qty: float, px: float | None) -> NewOrderIntent:
- qty_unit = "contracts" if intent.intended_qty is None else intent.intended_qty.unit
- price_ccy = "UNKNOWN" if intent.intended_price is None else intent.intended_price.currency
-
+ def _clone_new(intent: NewOrderIntent, qty: float, px: float | None) -> NewOrderIntent:
+ price = intent.intended_price.value if px is None else px
return NewOrderIntent(
+ intent_type="new",
ts_ns_local=intent.ts_ns_local,
instrument=intent.instrument,
client_order_id=intent.client_order_id,
intents_correlation_id=intent.intents_correlation_id,
side=intent.side,
order_type=intent.order_type,
- intended_qty={"unit": qty_unit, "value": qty},
- intended_price=None
- if px is None
- else {"currency": price_ccy, "value": px},
+ intended_qty=Quantity(unit=intent.intended_qty.unit, value=qty),
+ intended_price=Price(currency=intent.intended_price.currency, value=price),
time_in_force=intent.time_in_force,
)
@staticmethod
- def _clone_replace(intent: OrderIntent, qty: float, px: float | None) -> OrderIntent:
- # ReplaceOrderIntent shares the same field names as OrderIntent for the used fields.
- payload = intent.model_dump()
- qty_unit = "contracts" if intent.intended_qty is None else intent.intended_qty.unit
- payload["intended_qty"] = {"unit": qty_unit, "value": qty}
-
- price_ccy = "UNKNOWN" if intent.intended_price is None else intent.intended_price.currency
- payload["intended_price"] = (
- None
- if px is None
- else {"currency": price_ccy, "value": px}
+ def _clone_replace(
+ intent: ReplaceOrderIntent, qty: float, px: float | None
+ ) -> ReplaceOrderIntent:
+ price = intent.intended_price.value if px is None else px
+ return ReplaceOrderIntent(
+ intent_type="replace",
+ ts_ns_local=intent.ts_ns_local,
+ instrument=intent.instrument,
+ client_order_id=intent.client_order_id,
+ intents_correlation_id=intent.intents_correlation_id,
+ side=intent.side,
+ order_type=intent.order_type,
+ intended_qty=Quantity(unit=intent.intended_qty.unit, value=qty),
+ intended_price=Price(currency=intent.intended_price.currency, value=price),
)
- return type(intent).model_validate(payload)
diff --git a/tradingchassis_core/core/risk/risk_config.py b/tradingchassis_core/core/risk/risk_config.py
index 7d8ac54..4b2c3f3 100644
--- a/tradingchassis_core/core/risk/risk_config.py
+++ b/tradingchassis_core/core/risk/risk_config.py
@@ -1,4 +1,4 @@
-"""Risk configuration model for backtest and live trading engines."""
+"""Typed deterministic risk configuration model."""
from __future__ import annotations
@@ -16,56 +16,21 @@
class RiskConfig(BaseModel):
- """Structured-only risk configuration."""
+ """Structured risk configuration used by Core policy evaluation."""
scope: str = Field(..., min_length=1)
trading_enabled: bool = True
-
- # Mirrors types.py RiskConstraints fields (types.py is the source of truth)
position_limits: PositionLimits | None = None
notional_limits: NotionalLimits | None = None
quote_limits: QuoteLimits | None = None
order_rate_limits: OrderRateLimits | None = None
max_loss: MaxLoss | None = None
-
- # Optional additional config fields (kept separate from RiskConstraints.extra)
extra: dict[str, Any] = Field(default_factory=dict)
model_config = ConfigDict(extra="forbid")
- @classmethod
- def from_json_obj(cls, risk_obj: dict[str, Any]) -> RiskConfig:
- """Create a RiskConfig instance from a JSON-compatible object."""
- return cls.model_validate(risk_obj)
-
@model_validator(mode="after")
def validate_consistency(self) -> RiskConfig:
- """Validate internal consistency of the risk configuration."""
if self.notional_limits is None:
raise ValueError("notional_limits is required")
return self
-
- @property
- def params(self) -> dict[str, Any]:
- """Return engine-compatible flat risk parameters."""
- return self.to_engine_params()
-
- def to_engine_params(self) -> dict[str, Any]:
- """Convert the structured configuration into flat engine parameters."""
- params: dict[str, Any] = {}
-
- if self.position_limits is not None:
- params["position_limits"] = self.position_limits
- if self.notional_limits is not None:
- params["notional_limits"] = self.notional_limits
- if self.quote_limits is not None:
- params["quote_limits"] = self.quote_limits
- if self.order_rate_limits is not None:
- params["order_rate_limits"] = self.order_rate_limits
- if self.max_loss is not None:
- params["max_loss"] = self.max_loss
-
- if self.extra:
- params.update(self.extra)
-
- return params
diff --git a/tradingchassis_core/core/risk/risk_engine.py b/tradingchassis_core/core/risk/risk_engine.py
index 20d0d17..7d7f7da 100644
--- a/tradingchassis_core/core/risk/risk_engine.py
+++ b/tradingchassis_core/core/risk/risk_engine.py
@@ -1,166 +1,97 @@
-"""Risk engine implementing hard risk checks and intent gating."""
+"""Policy-risk evaluator used by deterministic Core policy admission."""
from __future__ import annotations
-from collections import defaultdict
-from dataclasses import dataclass
from typing import TYPE_CHECKING
from tradingchassis_core.core.domain.reject_reasons import RejectReason
from tradingchassis_core.core.domain.types import OrderIntent, RiskConstraints
-from tradingchassis_core.core.events.events import RiskDecisionEvent
-from tradingchassis_core.core.execution_control import ExecutionControl
-from tradingchassis_core.core.ports.venue_policy import VenuePolicy
+from tradingchassis_core.core.risk.execution_constraints_policy import (
+ ExecutionConstraintsPolicy,
+)
from tradingchassis_core.core.risk.risk_policy import RiskPolicy
if TYPE_CHECKING:
- from risk.risk_config import RiskConfig
-
from tradingchassis_core.core.domain.state import StrategyState
- from tradingchassis_core.core.events.event_bus import EventBus
-
-
-# ---------------------------------------------------------------------------
-# Gate decision models (internal, not part of JSON schema)
-# ---------------------------------------------------------------------------
-
-
-@dataclass(slots=True)
-class RejectedIntent:
- intent: OrderIntent
- reason: str
-
-
-@dataclass(slots=True)
-class GateDecision:
- """Result of the hard risk/gate layer.
-
- Compatibility decision contract consumed by strategy/runtime orchestration.
- This is not an Event and not a canonical Event Stream record.
-
- - accepted_now: intents that may be sent immediately
- - queued: intents that were enqueued into StrategyState.queue (data-only)
- - rejected: hard rejects with reasons
- - replaced_in_queue: (old, new) pairs when queue replacement happened
- - dropped_in_queue: intents that were dropped during queue merge (e.g. superseded)
- - handled_in_queue: intents that were fully handled locally in the queue layer
- (e.g. cancel/replace acting only on queued state) and must not be sent
- - next_send_ts_ns_local: earliest local timestamp where it makes sense to wake up
- to try flushing the queue (best-effort)
- """
-
- ts_ns_local: int
- accepted_now: list[OrderIntent]
- queued: list[OrderIntent]
- rejected: list[RejectedIntent]
- replaced_in_queue: list[tuple[OrderIntent, OrderIntent]]
- dropped_in_queue: list[OrderIntent]
- handled_in_queue: list[OrderIntent]
- # Populated by the runner after outbound execution.
- execution_rejected: list[RejectedIntent]
- next_send_ts_ns_local: int | None
+ from tradingchassis_core.core.risk.risk_config import RiskConfig
class RiskEngine:
- """Hard risk and intent gating engine."""
-
- # pylint: disable=too-many-instance-attributes
- """Hard risk + gate layer.
+ """Policy-only evaluator.
- This layer is allowed to:
- - hard reject invalid / risk-breaching intents
- - queue intents that should be sent later (rate limits, budgets)
- - accept intents for immediate sending
-
- It must NOT submit orders itself.
+ This component is intentionally side-effect-free for the CoreStep policy phase:
+ it does not mutate Queue/rate/inflight state and does not perform Execution Control.
"""
- def __init__(self, risk_cfg: RiskConfig, event_bus: EventBus) -> None:
+ def __init__(self, risk_cfg: RiskConfig) -> None:
self.risk_cfg = risk_cfg
- self._event_bus = event_bus
- venue_policy_cfg = self._parse_venue_policy_config(risk_cfg)
- self._venue_policy = VenuePolicy(
- min_order_notional=venue_policy_cfg["min_order_notional"],
- post_only_mode=venue_policy_cfg["post_only_mode"],
+ min_order_notional, post_only_mode = self._parse_execution_constraints_config(risk_cfg)
+ self._constraints_policy = ExecutionConstraintsPolicy(
+ min_order_notional=min_order_notional,
+ post_only_mode=post_only_mode,
)
-
- self._risk_policy = RiskPolicy(venue_policy=self._venue_policy)
-
- # Internal execution-control component owns rate state and queue admission logic.
- # RiskEngine must own a single instance to preserve state lifetime semantics.
- self._execution_control = ExecutionControl()
+ self._risk_policy = RiskPolicy(constraints_policy=self._constraints_policy)
@staticmethod
- def _parse_venue_policy_config(risk_cfg: RiskConfig) -> dict[str, object]:
- cfg: dict[str, object] = {
- "min_order_notional": 0.0,
- "post_only_mode": "reject",
- }
+ def _parse_execution_constraints_config(risk_cfg: RiskConfig) -> tuple[float, str]:
+ min_order_notional = 0.0
+ post_only_mode = "reject"
extra = risk_cfg.extra
if not isinstance(extra, dict):
- return cfg
+ return min_order_notional, post_only_mode
- # Preferred form: extra["venue_policy"] is a nested dict.
- # RiskConstraints.extra requires flat scalar values, therefore
- # nested config must be normalized before being exposed as constraints.
- vp = extra["venue_policy"] if "venue_policy" in extra else None
- if isinstance(vp, dict):
- if "min_order_notional" in vp:
+ constraints = (
+ extra["execution_constraints"]
+ if "execution_constraints" in extra
+ else None
+ )
+ if isinstance(constraints, dict):
+ if "min_order_notional" in constraints:
try:
- cfg["min_order_notional"] = float(vp["min_order_notional"])
+ min_order_notional = float(constraints["min_order_notional"])
except (TypeError, ValueError):
pass
-
- if "post_only_mode" in vp:
- mode = str(vp["post_only_mode"])
+ if "post_only_mode" in constraints:
+ mode = str(constraints["post_only_mode"])
if mode in {"reject", "drop"}:
- cfg["post_only_mode"] = mode
-
- return cfg
+ post_only_mode = mode
+ return min_order_notional, post_only_mode
- # Backwards/alternative form: flattened keys.
- if "venue_policy_min_order_notional" in extra:
+ if "execution_constraints_min_order_notional" in extra:
try:
- cfg["min_order_notional"] = float(
- extra["venue_policy_min_order_notional"]
- )
+ min_order_notional = float(
+ extra["execution_constraints_min_order_notional"]
+ )
except (TypeError, ValueError):
pass
- if "venue_policy_post_only_mode" in extra:
- mode = str(extra["venue_policy_post_only_mode"])
+ if "execution_constraints_post_only_mode" in extra:
+ mode = str(extra["execution_constraints_post_only_mode"])
if mode in {"reject", "drop"}:
- cfg["post_only_mode"] = mode
+ post_only_mode = mode
- return cfg
+ return min_order_notional, post_only_mode
@staticmethod
- def _constraints_extra(extra: object) -> dict[str, object]:
- """Normalize RiskConfig.extra for RiskConstraints.
-
- RiskConstraints.extra is defined as a flat mapping of scalar values
- (str/float/bool/None). Nested dicts are not allowed.
-
- The normalization keeps the original mapping intact on RiskConfig,
- but produces a flattened mapping for strategy constraints.
- """
-
+ def _constraints_extra(extra: object) -> dict[str, str | float | bool | None]:
if not isinstance(extra, dict):
return {}
- normalized: dict[str, object] = {}
+ normalized: dict[str, str | float | bool | None] = {}
for key, value in extra.items():
- if key == "venue_policy" and isinstance(value, dict):
- # Flatten nested venue policy config.
+ if key == "execution_constraints" and isinstance(value, dict):
if "min_order_notional" in value:
- normalized["venue_policy_min_order_notional"] = value["min_order_notional"]
+ normalized["execution_constraints_min_order_notional"] = value[
+ "min_order_notional"
+ ]
if "post_only_mode" in value:
- normalized["venue_policy_post_only_mode"] = value["post_only_mode"]
+ normalized["execution_constraints_post_only_mode"] = value[
+ "post_only_mode"
+ ]
continue
- # Only keep scalar values to match the RiskConstraints schema.
if value is None or isinstance(value, (str, float, bool)):
normalized[key] = value
elif isinstance(value, int):
@@ -168,17 +99,8 @@ def _constraints_extra(extra: object) -> dict[str, object]:
return normalized
- @staticmethod
- def _float_equal(a: float, b: float) -> bool:
- """Best-effort float equality for normalized values."""
- return abs(a - b) <= 1e-12
-
- # ---------------------------------------------------------------------
- # Soft constraints for strategy
- # ---------------------------------------------------------------------
-
def build_constraints(self, current_timestamp_ns_local: int) -> RiskConstraints:
- """Build RiskConstraints handed to the strategy."""
+ """Build RiskConstraints handed to Strategy evaluation."""
extra = self._constraints_extra(self.risk_cfg.extra)
return RiskConstraints(
ts_ns_local=current_timestamp_ns_local,
@@ -192,81 +114,28 @@ def build_constraints(self, current_timestamp_ns_local: int) -> RiskConstraints:
extra=extra,
)
- # ---------------------------------------------------------------------
- # Hard gate decision
- # ---------------------------------------------------------------------
-
- # pylint: disable=too-many-locals,too-many-branches,too-many-statements
- def decide_intents(
+ def evaluate_policy_intent(
self,
- raw_intents: list[OrderIntent],
+ *,
+ intent: OrderIntent,
state: StrategyState,
now_ts_ns_local: int,
- ) -> GateDecision:
- """Hard gate decision.
-
- - Hard rejects: never send (risk breach / invalid)
- - Queue: send later (rate limits / local budgets)
- - Accept: send now
-
- NOTE: This implementation *does* enqueue queued intents into StrategyState
- (data-only queue) by calling state.merge_intents_into_queue().
- """
+ ) -> tuple[bool, str | None]:
+ """Evaluate one intent with policy-only checks and no side effects."""
- accepted_now: list[OrderIntent] = []
- to_queue_by_instr: defaultdict[str, list[OrderIntent]] = defaultdict(list)
- rejected: list[RejectedIntent] = []
- replaced_in_queue: list[tuple[OrderIntent, OrderIntent]] = []
- dropped_in_queue: list[OrderIntent] = []
- handled_in_queue: list[OrderIntent] = []
+ raw_intents = [intent]
- # Intents that ended up queued due to rate limits or due to queue-only handling.
- queued: list[OrderIntent] = []
- next_send_ts: int | None = None
-
- # counters for RiskDecisionEvent
- reject_counts: dict[str, int] = {}
- def _count_reject(reason: str) -> None:
- reject_counts[reason] = reject_counts.get(reason, 0) + 1
-
- # --- Trading enabled gate ---
triggered, policy_accepted, policy_rejected = self._risk_policy.trading_enabled_gate(
trading_enabled=self.risk_cfg.trading_enabled,
raw_intents=raw_intents,
)
if triggered:
- accepted_now.extend(policy_accepted)
- for it, reason in policy_rejected:
- rejected.append(RejectedIntent(it, reason))
- _count_reject(reason)
-
- decision = GateDecision(
- ts_ns_local=now_ts_ns_local,
- accepted_now=accepted_now,
- queued=[],
- rejected=rejected,
- replaced_in_queue=[],
- dropped_in_queue=[],
- handled_in_queue=[],
- execution_rejected=[],
- next_send_ts_ns_local=None,
- )
+ if policy_accepted:
+ return True, None
+ if policy_rejected:
+ return False, policy_rejected[0][1]
+ return False, RejectReason.TRADING_DISABLED
- # emit summary
- self._event_bus.emit(
- RiskDecisionEvent(
- ts_ns_local=now_ts_ns_local,
- accepted=len(accepted_now),
- queued=0,
- rejected=len(rejected),
- handled=len(handled_in_queue),
- reject_reasons=reject_counts,
- )
- )
-
- return decision
-
- # --- Max loss (portfolio drawdown kill-switch) ---
triggered, policy_accepted, policy_rejected = self._risk_policy.max_loss_gate(
max_loss_cfg=self.risk_cfg.max_loss,
raw_intents=raw_intents,
@@ -274,170 +143,53 @@ def _count_reject(reason: str) -> None:
now_ts_ns_local=now_ts_ns_local,
)
if triggered:
- accepted_now.extend(policy_accepted)
- for it, reason in policy_rejected:
- rejected.append(RejectedIntent(it, reason))
- _count_reject(reason)
-
- decision = GateDecision(
- ts_ns_local=now_ts_ns_local,
- accepted_now=accepted_now,
- queued=[],
- rejected=rejected,
- replaced_in_queue=[],
- dropped_in_queue=[],
- handled_in_queue=[],
- execution_rejected=[],
- next_send_ts_ns_local=None,
- )
-
- self._event_bus.emit(
- RiskDecisionEvent(
- ts_ns_local=now_ts_ns_local,
- accepted=len(accepted_now),
- queued=0,
- rejected=len(rejected),
- handled=len(handled_in_queue),
- reject_reasons=reject_counts,
- )
- )
-
- return decision
-
- # --- Rate limits (per second, local time) ---
- rate_cfg = self.risk_cfg.order_rate_limits
- max_orders_per_sec = None if rate_cfg is None else rate_cfg.max_orders_per_second
- max_cancels_per_sec = None if rate_cfg is None else rate_cfg.max_cancels_per_second
+ if policy_accepted:
+ return True, None
+ if policy_rejected:
+ return False, policy_rejected[0][1]
+ return False, RejectReason.MAX_LOSS_DRAWDOWN
+
+ norm = self._risk_policy.normalize_intent(intent, state)
+ if norm.reject_reason is not None:
+ return False, norm.reject_reason
+ if norm.dropped:
+ return False, "dropped_by_policy"
+ if norm.normalized is None:
+ return False, RejectReason.INVALID_QTY
+
+ normalized_intent = norm.normalized
+
+ ok, reason = self._risk_policy.validate_intent(normalized_intent, state)
+ if not ok:
+ return False, reason
- # --- Position / notional limits ---
pos_cfg = self.risk_cfg.position_limits
max_pos = None if (pos_cfg is None or pos_cfg.max_position is None) else pos_cfg.max_position
notional_cfg = self.risk_cfg.notional_limits
- max_gross_notional = notional_cfg.max_gross_notional
- max_single_order_notional = notional_cfg.max_single_order_notional
+ max_gross_notional = (
+ None if notional_cfg is None else notional_cfg.max_gross_notional
+ )
+ max_single_order_notional = (
+ None if notional_cfg is None else notional_cfg.max_single_order_notional
+ )
quote_cfg = self.risk_cfg.quote_limits
-
quote_book = None
if quote_cfg is not None:
quote_book = self._risk_policy.quote_book_global(state)
-
- # Base portfolio gross notional (best-effort)
base_gross_notional = self._risk_policy.portfolio_gross_notional(state)
- # -----------------------------------------------------------------
- # Per-intent decision
- # -----------------------------------------------------------------
- for it in raw_intents:
- norm = self._risk_policy.normalize_intent(it, state)
- if norm.reject_reason is not None:
- rejected.append(RejectedIntent(it, norm.reject_reason))
- _count_reject(norm.reject_reason)
- continue
- if norm.dropped:
- handled_in_queue.append(it)
- continue
- if norm.normalized is None:
- rejected.append(RejectedIntent(it, RejectReason.INVALID_QTY))
- _count_reject(RejectReason.INVALID_QTY)
- continue
-
- it = norm.normalized
- # 0) Pre-submission lifecycle / identity / inflight routing compatibility handling.
- continue_to_policy, lifecycle_reject_reason = (
- self._execution_control.route_pre_submission_lifecycle_and_inflight(
- it,
- state=state,
- to_queue_by_instr=to_queue_by_instr,
- replaced_in_queue=replaced_in_queue,
- dropped_in_queue=dropped_in_queue,
- queued=queued,
- handled_in_queue=handled_in_queue,
- float_equal=self._float_equal,
- )
- )
- if not continue_to_policy:
- if lifecycle_reject_reason is not None:
- rejected.append(RejectedIntent(it, lifecycle_reject_reason))
- _count_reject(lifecycle_reject_reason)
- continue
-
- # 1) Outbound hygiene validation (hard reject)
- ok, reason = self._risk_policy.validate_intent(it, state)
- if not ok:
- rejected.append(RejectedIntent(it, reason))
- _count_reject(reason)
- continue
-
- # 2) Hard risk checks (hard reject)
- ok, reason = self._risk_policy.hard_checks(
- it,
- state,
- max_pos=max_pos,
- max_single_order_notional=max_single_order_notional,
- max_gross_notional=max_gross_notional,
- base_gross_notional=base_gross_notional,
- quote_cfg=quote_cfg,
- quote_book=quote_book,
- )
- if not ok:
- rejected.append(RejectedIntent(it, reason))
- _count_reject(reason)
- continue
-
- # 3) Rate limiting -> queue (soft, not reject)
- rate_result = self._execution_control.route_after_policy_rate_limit(
- it,
- now_ts_ns_local=now_ts_ns_local,
- max_orders_per_sec=max_orders_per_sec,
- max_cancels_per_sec=max_cancels_per_sec,
- )
- if rate_result.stage_to_queue:
- to_queue_by_instr[it.instrument].append(it)
- obligation = rate_result.scheduling_obligation
- if obligation is not None:
- next_send_ts = (
- obligation.ts_ns_local
- if next_send_ts is None
- else min(next_send_ts, obligation.ts_ns_local)
- )
- continue
-
- accepted_now.append(it)
-
- # -----------------------------------------------------------------
- # Queue merge per instrument (replacement rules live in StrategyState)
- # -----------------------------------------------------------------
- self._execution_control.merge_to_queue_per_instrument(
- state=state,
- to_queue_by_instr=to_queue_by_instr,
- queued=queued,
- replaced_in_queue=replaced_in_queue,
- dropped_in_queue=dropped_in_queue,
- )
-
- decision = GateDecision(
- ts_ns_local=now_ts_ns_local,
- accepted_now=accepted_now,
- queued=queued,
- rejected=rejected,
- replaced_in_queue=replaced_in_queue,
- dropped_in_queue=dropped_in_queue,
- handled_in_queue=handled_in_queue,
- execution_rejected=[],
- next_send_ts_ns_local=next_send_ts,
- )
-
- self._event_bus.emit(
- RiskDecisionEvent(
- ts_ns_local=now_ts_ns_local,
- accepted=len(accepted_now),
- queued=len(queued),
- rejected=len(rejected),
- handled=len(handled_in_queue),
- reject_reasons=reject_counts,
- )
+ ok, reason = self._risk_policy.hard_checks(
+ normalized_intent,
+ state,
+ max_pos=max_pos,
+ max_single_order_notional=max_single_order_notional,
+ max_gross_notional=max_gross_notional,
+ base_gross_notional=base_gross_notional,
+ quote_cfg=quote_cfg,
+ quote_book=quote_book,
)
-
- return decision
+ if not ok:
+ return False, reason
+ return True, None
diff --git a/tradingchassis_core/core/risk/risk_policy.py b/tradingchassis_core/core/risk/risk_policy.py
index 4e17f31..0356d6e 100644
--- a/tradingchassis_core/core/risk/risk_policy.py
+++ b/tradingchassis_core/core/risk/risk_policy.py
@@ -2,7 +2,7 @@
This module is intentionally internal and behavior-preserving:
- It contains only policy checks (validation, kill-switches, hard limits).
-- It does not perform queue admission, rate limiting, or inflight gating.
+- It does not perform Queue admission, rate limiting, or inflight gating.
"""
from __future__ import annotations
@@ -10,8 +10,11 @@
from typing import TYPE_CHECKING
from tradingchassis_core.core.domain.reject_reasons import RejectReason
-from tradingchassis_core.core.domain.types import OrderIntent
-from tradingchassis_core.core.ports.venue_policy import NormalizationOutcome, VenuePolicy
+from tradingchassis_core.core.domain.types import NewOrderIntent, OrderIntent, ReplaceOrderIntent
+from tradingchassis_core.core.risk.execution_constraints_policy import (
+ ExecutionConstraintsPolicy,
+ NormalizationOutcome,
+)
if TYPE_CHECKING:
from tradingchassis_core.core.domain.state import StrategyState
@@ -21,8 +24,8 @@
class RiskPolicy:
"""Pure policy layer used by RiskEngine."""
- def __init__(self, *, venue_policy: VenuePolicy) -> None:
- self._venue_policy = venue_policy
+ def __init__(self, *, constraints_policy: ExecutionConstraintsPolicy) -> None:
+ self._constraints_policy = constraints_policy
def trading_enabled_gate(
self,
@@ -66,10 +69,11 @@ def max_loss_gate(
pnl = state.get_total_pnl()
if pnl <= max_loss_cfg.max_drawdown:
- return True, self._accept_cancels_reject_others(
+ accepted_now, rejected = self._accept_cancels_reject_others(
raw_intents,
RejectReason.MAX_LOSS_DRAWDOWN,
)
+ return True, accepted_now, rejected
# Rolling loss kill-switch (equity change over a fixed window)
if max_loss_cfg.rolling_loss is not None and max_loss_cfg.rolling_loss_window is not None:
@@ -79,10 +83,11 @@ def max_loss_gate(
window_ns=window_ns,
)
if rolling is not None and rolling <= max_loss_cfg.rolling_loss:
- return True, self._accept_cancels_reject_others(
+ accepted_now, rejected = self._accept_cancels_reject_others(
raw_intents,
RejectReason.MAX_LOSS_ROLLING,
)
+ return True, accepted_now, rejected
return False, [], []
@@ -101,7 +106,7 @@ def _accept_cancels_reject_others(
return accepted_now, rejected
def normalize_intent(self, it: OrderIntent, state: StrategyState) -> NormalizationOutcome:
- return self._venue_policy.normalize_intent(it, state)
+ return self._constraints_policy.normalize_intent(it, state)
def validate_intent(self, it: OrderIntent, state: StrategyState) -> tuple[bool, str]:
"""Outbound intent sanity.
@@ -142,7 +147,7 @@ def hard_checks(
max_gross_notional: float | None,
base_gross_notional: float | None,
quote_cfg: QuoteLimits | None,
- quote_book: dict[tuple[str, str | None, tuple[float, float]]],
+ quote_book: dict[tuple[str, str], tuple[float, float]] | None,
) -> tuple[bool, str]:
"""Apply hard risk checks. Returns (ok, reason)."""
@@ -207,6 +212,8 @@ def hard_checks(
return True, "OK"
def intent_price(self, it: OrderIntent, state: StrategyState) -> float | None:
+ if not isinstance(it, (NewOrderIntent, ReplaceOrderIntent)):
+ return None
if it.order_type == "limit":
return None if it.intended_price is None else it.intended_price.value
mid = state.get_mid(it.instrument)
diff --git a/tradingchassis_core/core/schemas/common.schema.json b/tradingchassis_core/core/schemas/common.schema.json
deleted file mode 100644
index ca8539e..0000000
--- a/tradingchassis_core/core/schemas/common.schema.json
+++ /dev/null
@@ -1,52 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "$id": "https://schemas.example.com/common.schema.json",
- "definitions": {
- "Money": {
- "type": "object",
- "description": "Monetary amount with currency.",
- "required": ["currency", "amount"],
- "properties": {
- "currency": {
- "type": "string",
- "minLength": 1,
- "description": "Currency code (e.g. USD, EUR)."
- },
- "amount": {
- "type": "number",
- "description": "Positive or negative monetary amount."
- }
- },
- "additionalProperties": false
- },
-
- "Price": {
- "type": "object",
- "description": "Price or rate with currency (e.g. price per unit).",
- "required": ["currency", "value"],
- "properties": {
- "currency": {
- "type": "string",
- "minLength": 1,
- "description": "Currency code (e.g. USD, EUR)."
- },
- "value": {
- "type": "number",
- "minimum": 0,
- "description": "Price value in quote currency. Price can be 0 as a placeholder."
- }
- },
- "additionalProperties": false
- },
-
- "Quantity": {
- "type": "object",
- "required": ["value", "unit"],
- "properties": {
- "value": { "type": "number", "minimum": 0, "description": "Value can be 0 for remaining quantity." },
- "unit": { "type": "string", "minLength": 1 }
- },
- "additionalProperties": false
- }
- }
-}
diff --git a/tradingchassis_core/core/schemas/fill_event.schema.json b/tradingchassis_core/core/schemas/fill_event.schema.json
deleted file mode 100644
index dd444cf..0000000
--- a/tradingchassis_core/core/schemas/fill_event.schema.json
+++ /dev/null
@@ -1,79 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "$id": "https://example.com/schemas/fill_event.schema.json",
- "title": "FillEvent",
- "type": "object",
- "required": [
- "ts_ns_exch",
- "ts_ns_local",
- "instrument",
- "client_order_id",
- "side",
- "filled_price",
- "cum_filled_qty",
- "time_in_force",
- "liquidity_flag"
- ],
- "properties": {
- "ts_ns_exch": {
- "description": "Venue fill event timestamp in nanoseconds since Unix epoch.",
- "type": "integer",
- "exclusiveMinimum": 0
- },
- "ts_ns_local": {
- "description": "Local fill event timestamp in nanoseconds since Unix epoch.",
- "type": "integer",
- "exclusiveMinimum": 0
- },
- "instrument": {
- "description": "Instrument identifier.",
- "type": "string",
- "minLength": 1
- },
- "client_order_id": {
- "description": "Client-assigned order ID (if used).",
- "type": "string",
- "minLength": 1
- },
- "side": {
- "description": "Side of the filled order from client perspective.",
- "type": "string",
- "enum": ["buy", "sell"]
- },
- "intended_price": {
- "description": "Intended price for the order.",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Price"
- },
- "filled_price": {
- "description": "Filled price for the order.",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Price"
- },
- "intended_qty": {
- "description": "Intended quantity of this order in underlying units.",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Quantity"
- },
- "cum_filled_qty": {
- "description": "Cumulative filled quantity of this order in underlying units at this state.",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Quantity"
- },
- "remaining_qty": {
- "description": "Remaining open quantity after this state.",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Quantity"
- },
- "time_in_force": {
- "description": "Time-in-force instruction (venue-specific mapping).",
- "type": "string",
- "enum": ["GTC", "IOC", "FOK", "POST_ONLY"]
- },
- "liquidity_flag": {
- "description": "Indicates whether the fill was maker or taker.",
- "type": "string",
- "enum": ["maker", "taker", "unknown"]
- },
- "fee": {
- "description": "Transaction fee or rebate. Negative = fee paid, positive = rebate.",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Money"
- }
- },
- "additionalProperties": false
-}
diff --git a/tradingchassis_core/core/schemas/market_event.schema.json b/tradingchassis_core/core/schemas/market_event.schema.json
deleted file mode 100644
index 6b74db0..0000000
--- a/tradingchassis_core/core/schemas/market_event.schema.json
+++ /dev/null
@@ -1,121 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "$id": "https://example.com/schemas/market_event.schema.json",
- "title": "MarketEvent",
- "type": "object",
- "required": [
- "ts_ns_exch",
- "ts_ns_local",
- "instrument",
- "event_type"
- ],
- "properties": {
- "ts_ns_exch": {
- "description": "Venue event timestamp in nanoseconds since Unix epoch.",
- "type": "integer",
- "exclusiveMinimum": 0
- },
- "ts_ns_local": {
- "description": "Local event timestamp in nanoseconds since Unix epoch.",
- "type": "integer",
- "exclusiveMinimum": 0
- },
- "instrument": {
- "description": "Instrument identifier, venue-specific.",
- "type": "string",
- "minLength": 1
- },
- "event_type": {
- "description": "Type of market event.",
- "type": "string",
- "enum": ["book", "trade"]
- },
- "book": {
- "description": "Payload for order book events.",
- "type": "object",
- "required": ["book_type", "bids", "asks"],
- "properties": {
- "book_type": {
- "description": "Whether this is a full snapshot or an incremental update.",
- "type": "string",
- "enum": ["snapshot", "delta"]
- },
- "bids": {
- "description": "Bid levels, highest price first.",
- "type": "array",
- "items": {
- "type": "object",
- "required": ["price", "quantity"],
- "properties": {
- "price": { "$ref": "https://schemas.example.com/common.schema.json#/definitions/Price" },
- "quantity": { "$ref": "https://schemas.example.com/common.schema.json#/definitions/Quantity" }
- },
- "additionalProperties": false
- }
- },
- "asks": {
- "description": "Ask levels, lowest price first.",
- "type": "array",
- "items": {
- "type": "object",
- "required": ["price", "quantity"],
- "properties": {
- "price": { "$ref": "https://schemas.example.com/common.schema.json#/definitions/Price" },
- "quantity": { "$ref": "https://schemas.example.com/common.schema.json#/definitions/Quantity" }
- },
- "additionalProperties": false
- }
- },
- "depth": {
- "description": "Depth of the book represented by this event (number of levels).",
- "type": "integer",
- "minimum": 0
- }
- },
- "additionalProperties": false
- },
- "trade": {
- "description": "Payload for trade events.",
- "type": "object",
- "required": ["side", "price", "quantity"],
- "properties": {
- "side": {
- "description": "Taker side of the trade.",
- "type": "string",
- "enum": ["buy", "sell"]
- },
- "price": {
- "description": "Trade price.",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Price"
- },
- "quantity": {
- "description": "Trade quantity in contract units.",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Quantity"
- },
- "trade_id": {
- "description": "Venue-provided trade identifier, if available.",
- "type": "string",
- "minLength": 1
- }
- },
- "additionalProperties": false
- }
- },
- "allOf": [
- {
- "if": { "properties": { "event_type": { "const": "book" } } },
- "then": {
- "required": ["book"],
- "not": { "required": ["trade"] }
- }
- },
- {
- "if": { "properties": { "event_type": { "const": "trade" } } },
- "then": {
- "required": ["trade"],
- "not": { "required": ["book"] }
- }
- }
- ],
- "additionalProperties": false
-}
diff --git a/tradingchassis_core/core/schemas/order_intent.schema.json b/tradingchassis_core/core/schemas/order_intent.schema.json
deleted file mode 100644
index 9b4522e..0000000
--- a/tradingchassis_core/core/schemas/order_intent.schema.json
+++ /dev/null
@@ -1,187 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "$id": "https://example.com/schemas/order_intent.schema.json",
- "title": "OrderIntent",
- "type": "object",
- "description": "Schema for an order intent used as a single source of truth across strategy, validation, and (backtest) execution bindings. The schema models intent-specific requirements via oneOf (new/cancel/replace).",
- "required": [
- "ts_ns_local",
- "instrument",
- "client_order_id",
- "intent_type"
- ],
- "properties": {
- "ts_ns_local": {
- "type": "integer",
- "description": "Local intent timestamp in nanoseconds since Unix epoch.",
- "exclusiveMinimum": 0
- },
- "instrument": {
- "type": "string",
- "description": "Instrument identifier used for routing and execution binding (e.g., symbol, asset code). Required for all intents, including cancel.",
- "minLength": 1
- },
- "client_order_id": {
- "type": "string",
- "description": "Order identifier (maps to hftbacktest 'order_id'). Used for new/cancel/replace. Must be unique while an order with the same ID exists.",
- "minLength": 1
- },
- "intent_type": {
- "type": "string",
- "enum": [
- "new",
- "cancel",
- "replace"
- ],
- "description": "Intent type describing the order lifecycle action."
- },
- "intents_correlation_id": {
- "type": "string",
- "description": "Optional correlation identifier to link multiple intents (e.g., decision bundles) across the order lifecycle.",
- "minLength": 1
- },
- "order_type": {
- "type": "string",
- "enum": [
- "limit",
- "market"
- ],
- "description": "Order type. For replace intents this must be 'limit'."
- },
- "side": {
- "type": "string",
- "enum": [
- "buy",
- "sell"
- ],
- "description": "Order side."
- },
- "intended_price": {
- "description": "Intended order price. Required for both limit and market orders to match the execution binding signature.",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Price"
- },
- "intended_qty": {
- "description": "Intended total order quantity. For replace intents, this is the new total quantity (not a delta).",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Quantity"
- },
- "time_in_force": {
- "type": "string",
- "enum": [
- "GTC",
- "IOC",
- "FOK",
- "POST_ONLY"
- ],
- "description": "Time in force. Required for new intents. Not allowed for replace intents because the binding does not support modifying it."
- }
- },
- "oneOf": [
- {
- "title": "NewOrderIntent",
- "description": "Create a new order.",
- "type": "object",
- "properties": {
- "intent_type": {
- "const": "new"
- }
- },
- "required": [
- "ts_ns_local",
- "instrument",
- "client_order_id",
- "intent_type",
- "side",
- "order_type",
- "intended_qty",
- "intended_price",
- "time_in_force"
- ]
- },
- {
- "title": "CancelOrderIntent",
- "description": "Cancel an existing order identified by client_order_id.",
- "type": "object",
- "properties": {
- "intent_type": {
- "const": "cancel"
- }
- },
- "required": [
- "ts_ns_local",
- "instrument",
- "client_order_id",
- "intent_type"
- ],
- "allOf": [
- {
- "not": {
- "required": [
- "side"
- ]
- }
- },
- {
- "not": {
- "required": [
- "order_type"
- ]
- }
- },
- {
- "not": {
- "required": [
- "intended_qty"
- ]
- }
- },
- {
- "not": {
- "required": [
- "intended_price"
- ]
- }
- },
- {
- "not": {
- "required": [
- "time_in_force"
- ]
- }
- }
- ]
- },
- {
- "title": "ReplaceOrderIntent",
- "description": "Modify an existing order (limit-only). The order ID remains the same (client_order_id). Time-in-force cannot be modified.",
- "type": "object",
- "properties": {
- "intent_type": {
- "const": "replace"
- },
- "order_type": {
- "const": "limit"
- }
- },
- "required": [
- "ts_ns_local",
- "instrument",
- "client_order_id",
- "intent_type",
- "side",
- "order_type",
- "intended_qty",
- "intended_price"
- ],
- "allOf": [
- {
- "not": {
- "required": [
- "time_in_force"
- ]
- }
- }
- ]
- }
- ],
- "additionalProperties": false
-}
diff --git a/tradingchassis_core/core/schemas/order_state_event.schema.json b/tradingchassis_core/core/schemas/order_state_event.schema.json
deleted file mode 100644
index 41149e1..0000000
--- a/tradingchassis_core/core/schemas/order_state_event.schema.json
+++ /dev/null
@@ -1,100 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "$id": "https://example.com/schemas/order_state_event.schema.json",
- "title": "OrderStateEvent",
- "type": "object",
- "required": [
- "ts_ns_exch",
- "ts_ns_local",
- "instrument",
- "client_order_id",
- "order_type",
- "state_type",
- "side",
- "intended_price",
- "intended_qty",
- "time_in_force"
- ],
- "properties": {
- "ts_ns_exch": {
- "description": "Venue state event timestamp in nanoseconds since Unix epoch.",
- "type": "integer",
- "exclusiveMinimum": 0
- },
- "ts_ns_local": {
- "description": "Local state event timestamp in nanoseconds since Unix epoch.",
- "type": "integer",
- "exclusiveMinimum": 0
- },
- "instrument": {
- "description": "Instrument identifier.",
- "type": "string",
- "minLength": 1
- },
- "client_order_id": {
- "description": "Client-assigned order ID (if used).",
- "type": "string",
- "minLength": 1
- },
- "order_type": {
- "description": "Order type indicating how the order was intended to be executed.",
- "type": "string",
- "enum": ["limit", "market"]
- },
- "state_type": {
- "description": "Lifecycle state of the order.",
- "type": "string",
- "enum": [
- "pending_new",
- "accepted",
- "working",
- "partially_filled",
- "filled",
- "canceled",
- "expired",
- "rejected",
- "replaced"
- ]
- },
- "side": {
- "description": "Side of the filled order from client perspective.",
- "type": "string",
- "enum": ["buy", "sell"]
- },
- "intended_price": {
- "description": "Intended price for the order.",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Price"
- },
- "filled_price": {
- "description": "Filled price for the order.",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Price"
- },
- "intended_qty": {
- "description": "Intended quantity of this order in underlying units.",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Quantity"
- },
- "cum_filled_qty": {
- "description": "Cumulative filled quantity of this order in underlying units at this state.",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Quantity"
- },
- "remaining_qty": {
- "description": "Remaining open quantity after this state.",
- "$ref": "https://schemas.example.com/common.schema.json#/definitions/Quantity"
- },
- "time_in_force": {
- "description": "Time-in-force instruction (venue-specific mapping).",
- "type": "string",
- "enum": ["GTC", "IOC", "FOK", "POST_ONLY"]
- },
- "reason": {
- "description": "Optional reason or error code for this state change.",
- "type": "string",
- "minLength": 1
- },
- "raw": {
- "description": "Optional raw payload from venue or OMS for debugging.",
- "type": "object"
- }
- },
- "additionalProperties": false
-}
diff --git a/tradingchassis_core/core/schemas/risk_constraints.schema.json b/tradingchassis_core/core/schemas/risk_constraints.schema.json
deleted file mode 100644
index 8336204..0000000
--- a/tradingchassis_core/core/schemas/risk_constraints.schema.json
+++ /dev/null
@@ -1,166 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "$id": "https://example.com/schemas/risk_constraints.schema.json",
- "title": "RiskConstraints",
- "type": "object",
- "required": [
- "ts_ns_local",
- "scope",
- "trading_enabled"
- ],
- "properties": {
- "ts_ns_local": {
- "description": "Local risk state timestamp in nanoseconds since Unix epoch.",
- "type": "integer",
- "exclusiveMinimum": 0
- },
- "scope": {
- "description": "Scope for which these constraints apply (e.g. strategy name, account).",
- "type": "string",
- "minLength": 1
- },
- "trading_enabled": {
- "description": "Global on/off flag. If false, no new risk-increasing orders should be sent.",
- "type": "boolean"
- },
-
- "position_limits": {
- "description": "Limits expressed in underlying units (e.g. shares, contracts). Position value is interpreted in the given currency context.",
- "type": "object",
- "required": ["currency"],
- "properties": {
- "currency": {
- "description": "Currency context for evaluating position limits (e.g. 'USD', 'EUR')",
- "type": "string",
- "minLength": 1
- },
- "max_position": {
- "description": "Maximum allowed absolute position in underlying units (applies to the net position if the venue auto-nets).",
- "type": "number",
- "minimum": 0
- }
- },
- "additionalProperties": false
- },
-
- "notional_limits": {
- "description": "Limits expressed in monetary notional (exposure) in a given currency.",
- "type": "object",
- "required": [
- "currency"
- ],
- "properties": {
- "currency": {
- "description": "Currency identifier used for notional limits (e.g. 'USD', 'EUR', 'SimDollar'). No specific format enforced.",
- "type": "string",
- "minLength": 1
- },
- "max_gross_notional": {
- "description": "Maximum allowed gross notional exposure (sum of absolute notionals across instruments) in the given currency.",
- "type": "number",
- "minimum": 0
- },
- "max_single_order_notional": {
- "description": "Maximum allowed notional for a single new order in the given currency.",
- "type": "number",
- "minimum": 0
- }
- },
- "additionalProperties": false
- },
-
- "quote_limits": {
- "description": "Limits on outstanding quote exposure, expressed in a monetary currency.",
- "type": "object",
- "required": [
- "currency"
- ],
- "properties": {
- "currency": {
- "description": "Currency identifier used for quote-notional limits (e.g. 'USD', 'EUR', 'SimDollar'). No specific format enforced.",
- "type": "string",
- "minLength": 1
- },
- "max_gross_quote_notional": {
- "description": "Maximum gross notional of all active quotes (sum of absolute notional of all bid/ask quotes) in the given currency.",
- "type": "number",
- "minimum": 0
- },
- "max_net_quote_notional": {
- "description": "Maximum net notional of active quotes (bid notional - ask notional).",
- "type": "number"
- },
- "max_active_quotes": {
- "description": "Maximum number of active quotes across all instruments.",
- "type": "integer",
- "minimum": 0
- }
- },
- "additionalProperties": false
- },
-
- "order_rate_limits": {
- "description": "Rate limits for order and cancel activity.",
- "type": "object",
- "properties": {
- "max_orders_per_second": {
- "description": "Maximum number of new orders per second.",
- "type": "number",
- "minimum": 0
- },
- "max_cancels_per_second": {
- "description": "Maximum number of cancel requests per second.",
- "type": "number",
- "minimum": 0
- }
- },
- "additionalProperties": false
- },
-
- "max_loss": {
- "description": "Maximum allowed realized + unrealized losses.",
- "type": "object",
- "required": [
- "currency",
- "max_drawdown"
- ],
- "properties": {
- "currency": {
- "description": "Currency identifier used for the daily loss limit (e.g. 'USD', 'EUR', 'SimDollar'). No specific format enforced.",
- "type": "string",
- "minLength": 1
- },
- "max_drawdown": {
- "description": "Maximum permitted portfolio loss measured from the most recent equity peak (peak-to-trough drawdown limit).",
- "type": "number",
- "exclusiveMaximum": 0
- },
- "rolling_loss": {
- "description": "Maximum permitted loss within a rolling time window; used as an additional regime/bug protection alongside the max_drawdown limit.",
- "type": "number",
- "exclusiveMaximum": 0
- },
- "rolling_loss_window": {
- "description": "Size of rolling loss measurement window in minutes, defining how far back losses are accumulated for the rolling_loss check.",
- "type": "number",
- "exclusiveMinimum": 0
- }
- },
- "additionalProperties": false
- },
-
- "extra": {
- "description": "Extension point for additional risk parameters.",
- "type": "object",
- "additionalProperties": {
- "type": [
- "string",
- "number",
- "boolean",
- "null"
- ]
- }
- }
- },
- "additionalProperties": false
-}
diff --git a/tradingchassis_core/strategies/__init__.py b/tradingchassis_core/strategies/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tradingchassis_core/strategies/base.py b/tradingchassis_core/strategies/base.py
deleted file mode 100644
index e6389e8..0000000
--- a/tradingchassis_core/strategies/base.py
+++ /dev/null
@@ -1,56 +0,0 @@
-"""Base strategy interface.
-
-This module defines the Strategy protocol used by the backtest and
-live execution engines. Concrete strategies implement this interface and are
-driven exclusively by venue wakeups and risk engine decisions.
-"""
-
-from __future__ import annotations
-
-from abc import ABC, abstractmethod
-from typing import TYPE_CHECKING
-
-if TYPE_CHECKING:
- from tradingchassis_core.core.domain.state import StrategyState
- from tradingchassis_core.core.domain.types import MarketEvent, OrderIntent, RiskConstraints
- from tradingchassis_core.core.ports.engine_context import EngineContext
- from tradingchassis_core.core.risk.risk_engine import GateDecision
-
-
-class Strategy(ABC):
- """Strategy protocol implemented by all concrete strategies.
-
- The strategy is triggered by two event sources:
- - Feed events (rc=2): market data changes such as book/trade updates.
- - Order updates (rc=3): order responses / fills / cancels reflected in state snapshots.
-
- The strategy must NOT assume that created intents are already live in the market.
- Live state must be derived from StrategyState order snapshots.
- """
-
- @abstractmethod
- def on_feed(
- self,
- state: StrategyState,
- event: MarketEvent,
- engine_cfg: EngineContext,
- constraints: RiskConstraints,
- ) -> list[OrderIntent]:
- """Handle a feed wakeup (rc=2) and produce zero or more raw OrderIntents."""
-
- @abstractmethod
- def on_order_update(
- self,
- state: StrategyState,
- engine_cfg: EngineContext,
- constraints: RiskConstraints,
- ) -> list[OrderIntent]:
- """Handle an order update wakeup (rc=3) and produce zero or more raw OrderIntents.
-
- This hook is used for live-like behavior, e.g. reacting to fills, rejects,
- or cancels without waiting for the next market data tick.
- """
-
- @abstractmethod
- def on_risk_decision(self, decision: GateDecision) -> None:
- """Receive GateDecision feedback (accepted, queued, rejected with reasons)."""
diff --git a/tradingchassis_core/strategies/strategy_config.py b/tradingchassis_core/strategies/strategy_config.py
deleted file mode 100644
index 3fe9e62..0000000
--- a/tradingchassis_core/strategies/strategy_config.py
+++ /dev/null
@@ -1,70 +0,0 @@
-"""Strategy configuration model.
-
-This module defines the StrategyConfig schema used to parse and normalize
-strategy-related configuration from JSON into engine-consumable parameters.
-"""
-
-from __future__ import annotations
-
-from typing import Any
-
-from pydantic import BaseModel, ConfigDict, Field, model_validator
-
-
-class StrategyConfig(BaseModel):
- """Strategy config that collects arbitrary extra keys into ``params``.
-
- JSON example:
- "strategy": {
- "class_path": "my_strategies.debug:DebugStrategy",
- "spread": 50.0,
- "size": 0.0001
- }
-
- Result:
- class_path="my_strategies.debug:DebugStrategy"
- params={"spread": 50.0, "size": 0.0001}
- """
-
- class_path: str = Field(..., min_length=1)
-
- params: dict[str, Any] = Field(default_factory=dict)
-
- model_config = ConfigDict(extra="allow")
-
- @model_validator(mode="before")
- @classmethod
- def _collect_extras_into_params(cls, data: Any) -> Any:
- """Collect unknown top-level keys into the ``params`` mapping.
-
- This allows flat JSON strategy configuration without requiring
- a nested "params" object.
- """
- if not isinstance(data, dict):
- return data
-
- d = dict(data)
-
- explicit_params = d.pop("params", None)
-
- reserved = {"class_path"}
-
- extras = {k: v for k, v in d.items() if k not in reserved}
-
- for k in extras.keys():
- d.pop(k, None)
-
- merged: dict[str, Any] = {}
- if isinstance(explicit_params, dict):
- merged.update(explicit_params)
- merged.update(extras)
-
- d["params"] = merged
- return d
-
- def to_engine_params(self) -> dict[str, Any]:
- """Return a shallow copy of strategy parameters.
-
- The engine must not mutate configuration state.
- """
- return dict(self.params)