Domain Models and Lifecycle Binding¶
This guide explains how to use PyCharter contracts as domain entity models with an optional lifecycle binding: a convention that links an entity type to a state machine (e.g. for order management or trading sessions) without PyCharter depending on any FSM engine. When you use an FSM engine such as PyStator, you can resolve the contract, read the binding, validate payloads, and call the engine seamlessly.
Table of Contents¶
- What is a domain entity contract?
- The lifecycle convention
- Reading the binding in code
- Plugging into PyStator
- Seed domain contracts
What is a domain entity contract?¶
A domain entity contract in PyCharter is a data contract that describes a "noun" in your domain (e.g. Order, TradingSession, RebalanceRun). It has:
- A schema: the shape of the entity (id, status, and other fields).
- Optional lifecycle metadata: which state machine governs this entity’s lifecycle (e.g.
order_lifecycle,intraday_signal_lifecycle,optimization_cycle).
The contract does not store entity instances; it only stores the definition. Entity state (current FSM state per instance) lives in your application or in an FSM engine such as PyStator.
The lifecycle convention¶
A domain entity contract may include an optional lifecycle block in metadata at the root (metadata.lifecycle) or under governance_rules (metadata.governance_rules.lifecycle). PyCharter reads either location. The following fields align with FSM engines such as PyStator (machine name/version, entity id and state field mapping):
| Key | Required | Description |
|---|---|---|
state_machine_name |
Yes | Name of the FSM that governs this entity (e.g. order_lifecycle). Must match the FSM’s meta.machine_name (e.g. PyStator trading_machines). |
machine_version |
No | FSM machine version (e.g. "1.0.0"). If omitted, the engine typically uses the latest version. PyStator API accepts machine_version in the request body. |
state_field |
No | Name of the field on the entity that holds the current FSM state. Default: "status". Schema enum for this field should match the FSM state names. |
entity_id_field |
No | Name of the field on the entity used as entity_id in FSM APIs (e.g. order_id, cycle_id). Used when calling e.g. POST /entities/{entity_id}/events. |
Example in YAML (root-level lifecycle, as in the metadata template):
lifecycle:
state_machine_name: order_lifecycle
machine_version: "1.0.0"
state_field: status
entity_id_field: order_id
For the app and the FSM engine to stay in sync, the schema’s status (or state_field) enum should match the state names of the corresponding FSM. When the engine returns a new state, write it back to the entity’s state field. Use entity_id_field to know which field value to pass as entity_id to the FSM API.
Reading the binding in code¶
PyCharter provides engine-agnostic helpers that operate on in-memory dicts (no FSM dependency).
get_lifecycle_binding(metadata)¶
Returns the full lifecycle dict from metadata.lifecycle (root) or metadata["governance_rules"]["lifecycle"] if present and valid (must contain state_machine_name); otherwise None. The dict may include machine_version and entity_id_field when defined in metadata.
from pycharter import get_lifecycle_binding
metadata = {
"lifecycle": {
"state_machine_name": "order_lifecycle",
"machine_version": "1.0.0",
"state_field": "status",
"entity_id_field": "order_id",
}
}
binding = get_lifecycle_binding(metadata)
# binding has state_machine_name, state_field, machine_version, entity_id_field
get_domain_entity_info(contract_dict)¶
Given a full contract dict (e.g. from a store or build_contract), returns state_machine_name, state_field (default "status"), and when present in metadata: machine_version, entity_id_field. Use entity_id = record[info["entity_id_field"]] when calling the FSM API, and pass machine_version if you need to pin the FSM version.
from pycharter import get_domain_entity_info
contract = {
"metadata": {
"lifecycle": {
"state_machine_name": "order_lifecycle",
"entity_id_field": "order_id",
}
},
"schema": { ... },
}
info = get_domain_entity_info(contract)
# info == {"state_machine_name": "order_lifecycle", "state_field": "status", "entity_id_field": "order_id"}
Plugging into PyStator¶
When both PyCharter and PyStator are used, a typical flow is:
- Resolve the domain contract from PyCharter (by name/version from your store or API).
- Read the lifecycle binding with
get_lifecycle_binding(metadata)orget_domain_entity_info(contract_dict)to getstate_machine_name,state_field, and optionallymachine_versionandentity_id_field. - Validate the payload (e.g. order or event context) with PyCharter’s
Validatoragainst the contract’s schema. - Call PyStator with:
entity_id: from your record (e.g.record[info["entity_id_field"]]whenentity_id_fieldis set),machine_name:info["state_machine_name"](must match PyStatormeta.machine_name),- optional
machine_version:info.get("machine_version")to pin FSM version, trigger: event name,context: validated payload or subset.
Example (conceptual):
# 1. Load contract from store or file
contract = build_contract_from_store(store, "order_domain", "1.0.0")
# 2. Get lifecycle binding (no PyStator import in PyCharter)
info = get_domain_entity_info(contract)
if not info:
raise ValueError("Contract has no lifecycle binding")
# 3. Validate payload
validator = Validator.from_dict(contract["schema"], ...)
validator.validate(payload)
# 4. Call PyStator (your code or HTTP client)
# entity_id = payload.get(info.get("entity_id_field", "id"))
# POST /entities/{entity_id}/events
# body: { "trigger": "exchange_ack", "machine_name": info["state_machine_name"],
# "machine_version": info.get("machine_version"), "context": payload }
A runnable example is provided under examples/domain_lifecycle_pystator_example.py (or in docs examples).
Seed domain contracts¶
The PyCharter seed data includes domain contracts for intraday trading and weekly portfolio optimization. Each binds to a PyStator FSM by name (see pystator seed: trading_machines.yaml).
| Contract name | Purpose | FSM (state_machine_name) |
|---|---|---|
order_domain |
Single order lifecycle (intraday or rebalance) | order_lifecycle |
intraday_signal_domain |
One signal: detect → validate → submit → fill/cancel | intraday_signal_lifecycle |
intraday_session_domain |
Trading session: pre_market → … → post_market | intraday_trading_session |
portfolio_domain |
Portfolio lifecycle: monitoring → rebalancing → review / suspended | portfolio_lifecycle |
optimization_cycle_domain |
One optimization run: data_collection → optimizing → executing → completed | optimization_cycle |
flight_domain |
Flight lifecycle: scheduled → boarding → departed → in_flight → arrived / cancelled | flight_lifecycle |
After running pycharter db seed, these contracts and their schemas, metadata (with lifecycle), coercion/validation rules, and optionally ontologies (PostgreSQL Wiki) are loaded. Use them to validate entity payloads and to obtain state_machine_name when calling PyStator.
See also¶
- Concepts – Data contracts, validation pipeline, metadata store.
- Contract Builder API – Building and resolving contracts.
- Example:
examples/domain_lifecycle_pystator_example.py(if present in the repo) – Load contract → lifecycle → validate → call PyStator.