PyActuator
The actuation core of the Optophi family. PyActuator is a stateless, multi-domain Python package for performing side-effecting operations on external systems on behalf of strategy, state-machine, and agent components. Trading is one domain among several — Google Workspace, Comms (Slack / Email), Documents (PDF / Markdown / HTML), and Database (Postgres, SQLite, MongoDB, Redis) are first-class siblings.
Features
- Multi-domain — 15 domains under
pyactuator.domains.*, each with the same shape (types,protocols,errors,adapters/): market data (alpaca,fmp,massive,polymarket,finance), execution (trading), workspace/comms (google,comms,documents), data (database,datasets,data_sources,analytics,charts), andresearch. See Domains Overview. - Stateless core —
pyactuator.coreships envelopes, idempotency, retry, audit, policy, registry, and spec primitives. No I/O. - Adapter pattern — every external system is an adapter behind a
Protocol. Mock adapters ship with every domain, andpyactuator.testingwires them into a registry for your own tests. See Testing. - Bridges — install pyactuator operations into PyStator state
machines (
integrations.pystator) and PyGubernator tool registries (integrations.pygubernator) with one call. - MCP server —
pyactuator mcp serveoffers a tool profile's operations to Claude Desktop, Claude Code and any other MCP client, each call validated, governed and audited. See MCP server. - Deployment manifest — one
pyactuator.yamldeclares the agents sharing a runtime: each one's tool profile, credentials and policy.pyactuator preflight --strictchecks it before a deploy. See Deployment manifest. - CLI —
pyactuator runfor unified YAML/JSON configs,pyactuator tools listfor valid operation names, plusapi,ui,worker, anddocshelpers. - HTTP API — domain-scoped FastAPI routers under
/api/v1/<domain>/..., sharing a singleAdapterRegistry. - Optional deps —
alpaca-py,httpx,google-auth,jinja2,markdown-it-py, andreportlabare all optional extras, so you install only the vendors you actuate. The base install pullsduckdb+pyarrowfor the analytics, datasets, data-source, and charts domains. - Backwards compatible — existing
from pyactuator import OrderRequestimports continue to work as re-exports of the trading domain.
Installation
# Core only (envelopes, registry, mock adapters) — no mandatory dependencies
pip install pyactuator
# Data engine (analytics / datasets / data sources / charts) — DuckDB + PyArrow
pip install pyactuator[analytics]
# Trading with Alpaca broker
pip install pyactuator[trading-alpaca]
# Google Workspace (Drive / Sheets / Docs)
pip install pyactuator[google]
# Slack via webhook or bot token
pip install pyactuator[comms-slack]
# PDF + Markdown rendering
pip install pyactuator[documents-pdf,documents-md]
# Local file read (PDF text extraction + images; pypdf + Pillow)
pip install pyactuator[documents-read]
# MCP server (`pyactuator mcp serve`)
pip install pyactuator[mcp]
# Everything
pip install pyactuator[all]
# Development
pip install -e ".[dev]"
Documentation
Published docs (Material for MkDocs) live at optophi.github.io/pyactuator.
To build or preview locally:
pip install pyactuator[docs] # or: pip install -e ".[dev]" (includes docs tooling)
mkdocs serve
The site configuration mirrors PyStator / PyCharter: mkdocs.yml, mkdocs.optophi.yml for the Optophi.com docs deployment, and docs/ as the documentation root. CI runs mkdocs build --strict when mkdocs.yml is present (see ./scripts/ci.sh).
Quick start: invoke (the config-driven entry point)
pyactuator.sync.invoke / pyactuator.invoke dispatch any catalogued operation
by name against a mock (or real) AdapterRegistry and return the canonical wire
envelope — the recommended entry point when you don't need a specific domain's
typed objects. In async code, await pyactuator.invoke(...); the sync facade is
for synchronous callers and also works from inside a running event loop.
from pyactuator import list_operations
from pyactuator.sync import invoke
# Every operation name, e.g. "pyactuator.trading.place_order",
# "pyactuator.google.sheets_append", "pyactuator.comms.slack_send" ...
print(list_operations())
result = invoke(
"pyactuator.trading.place_order",
{
"symbol": "AAPL",
"side": "buy",
"quantity": "10",
"order_type": "market",
"client_order_id": "my-order-001", # stable idempotency key
},
)
print(result["success"], result["outcome"]) # True SUCCESS
print(result["adapter"]["real"]) # False — a mock served this call
Every envelope carries an adapter block, so a mock success is never mistaken
for a real one. Pass require_real_adapters=True to refuse to run against a mock
(or set PYACTUATOR_STRICT_ACTUATION=1 to make that the deployment default), or
a configured registry= to actuate against real credentials.
Pass a policy_enforcer= to decide before the adapter is called. A denied call
returns error.code: "POLICY_DENIED"; a dry-run decision returns dry_run: true
with success: false, because nothing happened. pyactuator.runner.run,
register_pyactuator_actions, register_pyactuator_tools and the MCP server's
CatalogToolset accept the same argument.
from decimal import Decimal
from pyactuator.core import LimitNotional
from pyactuator.sync import invoke
result = invoke(
"pyactuator.trading.place_order",
{
"symbol": "AAPL",
"side": "buy",
"quantity": "100",
"order_type": "limit",
"limit_price": "250",
"client_order_id": "my-order-002",
},
policy_enforcer=LimitNotional(Decimal("10000")),
)
print(result["success"], result["error"]["code"]) # False POLICY_DENIED
Quick start: Trading (mock broker)
import asyncio
from decimal import Decimal
from pyactuator import OrderRequest, Side, OrderType, TimeInForce
from pyactuator.domains.trading.adapters.mock import MockExecutionClient
async def main() -> None:
client = MockExecutionClient()
response = await client.submit(OrderRequest(
client_order_id="my-order-001",
symbol="AAPL",
side=Side.BUY,
quantity=Decimal("10"),
order_type=OrderType.MARKET,
time_in_force=TimeInForce.DAY,
))
print(response.success, response.external_order_id)
status = await client.get_status(response.external_order_id)
print(status.status) # ExecutionStatus.FILLED
await client.close()
asyncio.run(main())
Quick start: Google Sheets (mock)
import asyncio
from pyactuator.domains.google.types import AppendRowsRequest
from pyactuator.domains.google.adapters.mock import MockSheetsClient
async def main() -> None:
sheets = MockSheetsClient()
# create_spreadsheet takes the title and returns the new spreadsheet id.
spreadsheet_id = await sheets.create_spreadsheet(title="Daily PnL")
response = await sheets.append_rows(AppendRowsRequest(
spreadsheet_id=spreadsheet_id,
range="Sheet1!A1",
values=[["2025-05-07", "AAPL", 1234.56]],
))
print(spreadsheet_id, response.success)
asyncio.run(main())
Quick start: PyStator action
PyActuator's PyStator bridge installs every domain operation as a
kwargs-aware action callable on a pystator.actions.ActionRegistry.
State machines call them by name from YAML, with parameters templated
from the FSM context.
import asyncio
from pystator.actions import ActionExecutor, ActionRegistry, ActionSpec
from pyactuator.core.audit import MemoryAuditWriter
from pyactuator.core.registry import AdapterRegistry
from pyactuator.domains.trading.adapters.mock import MockExecutionClient
from pyactuator.integrations.pystator import register_pyactuator_actions
async def main() -> None:
adapters = AdapterRegistry()
adapters.register("trading", "broker", MockExecutionClient())
audit = MemoryAuditWriter()
registry = ActionRegistry()
register_pyactuator_actions(registry, adapters=adapters, audit_writer=audit)
executor = ActionExecutor(registry)
result = await executor.async_execute_action_spec(
ActionSpec(
name="pyactuator.trading.place_order",
params={
"symbol": "AAPL",
"side": "buy",
"quantity": 1,
"order_type": "market",
"client_order_id": "coid-1",
},
),
context={},
)
# The action's wire envelope is on ``result.result``.
print(result.success, result.result["result"]["external_order_id"])
print("audit records:", len(audit.records))
asyncio.run(main())
Quick start: PyGubernator tool
from pyactuator.core.registry import AdapterRegistry
from pyactuator.domains.trading.adapters.mock import MockExecutionClient
from pyactuator.integrations.pygubernator import (
iter_pyactuator_tool_specs,
register_pyactuator_tools,
)
from pygubernator.tools import ToolRegistry
adapters = AdapterRegistry()
adapters.register("trading", "broker", MockExecutionClient())
tools = ToolRegistry()
register_pyactuator_tools(tools, adapters=adapters)
for spec in iter_pyactuator_tool_specs():
print(spec.name, spec.metadata.side_effects, spec.metadata.idempotent)
Architecture
┌────────────────────────────────────────────────────────────┐
│ Callers │
│ ───────────────────────────────────────────────── │
│ PyStator FSMs PyGubernator agents HTTP clients │
└────────────┬──────────────┬──────────────┬─────────────────┘
│ │ │
actions tools /api/v1/<domain>
│ │ │
┌────────────▼──────────────▼──────────────▼─────────────────┐
│ pyactuator │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ core: envelopes • idempotency • retry • audit │ │
│ │ policy • errors • registry • spec │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌────────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ trading │ │ google │ │ comms │ │ documents │ │
│ │ Alpaca/Mock│ │Drive/Sheets│ │Slack/Email│ │PDF/MD/HTML │ │
│ └────────────┘ └──────────┘ └──────────┘ └─────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ integrations: pystator • pygubernator │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
│
┌────────┴────────┬────────────┬───────────┐
▼ ▼ ▼ ▼
Broker API Google APIs Slack API SMTP / SendGrid
(Alpaca, IB) (Drive/Sheets)
See ARCHITECTURE.md for the full layer model and
docs/guides/ for the per-topic guides:
domains-overview.md— what domains ship today and which surface to call them through.adding-a-domain.md— step-by-step recipe for adding a new domain.bridges.md— PyStator and PyGubernator bridge usage.
Domains shipped today
| Domain | Operations | Adapters |
|---|---|---|
trading |
submit, get_status, cancel, subscribe_fills, get_positions, get_account |
MockExecutionClient, AlpacaExecutionClient |
google |
Sheets append/read, Drive upload, Docs replace text / create | Mock + HTTPX-backed real adapters |
comms |
Slack send (webhook / API), Email send (SMTP / SendGrid) | Mock + real adapters |
documents |
Render templated PDF / Markdown / HTML | MockDocumentRenderer, JinjaMarkdownRenderer, ReportlabPdfRenderer |
Trading: error hierarchy
The trading domain still exposes its full error hierarchy from the
package root (re-exported from pyactuator.domains.trading.errors):
PyActuatorError
├── OrderValidationError # bad OrderRequest fields
├── OrderRejectedError # broker explicitly rejected
├── OrderNotFoundError # get_status / cancel on unknown order
├── BrokerConnectionError # network / timeout (retryable)
└── BrokerAdapterError # adapter misconfiguration
from pyactuator import (
PyActuatorError,
OrderNotFoundError,
BrokerConnectionError,
)
try:
status = await client.get_status("unknown-id")
except OrderNotFoundError:
print("Order does not exist at broker")
except BrokerConnectionError:
print("Transient failure — safe to retry")
except PyActuatorError:
print("Catch-all for any pyactuator error")
Adapter registry
All bridges and the FastAPI app share a single
pyactuator.core.registry.AdapterRegistry keyed on (domain, kind):
from pyactuator.core.registry import AdapterRegistry
from pyactuator.domains.trading.adapters.mock import MockExecutionClient
from pyactuator.domains.comms.adapters.mock import MockSlackClient
adapters = AdapterRegistry()
adapters.register("trading", "broker", MockExecutionClient())
adapters.register("comms", "slack", MockSlackClient())
broker = adapters.resolve("trading", "broker")
slack = adapters.resolve("comms", "slack")
Swapping a real adapter for a mock at startup is a one-liner.
Audit
Every bridge call writes an AuditRecord through an injected
AuditWriter:
from pystator.actions import ActionRegistry
from pyactuator.core.audit import MemoryAuditWriter
from pyactuator.core.registry import AdapterRegistry
from pyactuator.domains.trading.adapters.mock import MockExecutionClient
from pyactuator.integrations.pystator import register_pyactuator_actions
adapters = AdapterRegistry()
adapters.register("trading", "broker", MockExecutionClient())
audit = MemoryAuditWriter()
register_pyactuator_actions(ActionRegistry(), adapters=adapters, audit_writer=audit)
# After actions run, each call has left one record:
for record in audit.records:
print(record.operation, record.success, record.correlation_id)
NullAuditWriter is the default; production deployments wire their own
writer (e.g. backed by a database or a log shipper).
HTTP API
Each domain ships a FastAPI router under /api/v1/<domain>/...:
| Route | Purpose |
|---|---|
POST /api/v1/orders |
Submit an order |
GET /api/v1/orders/{id} |
Order status |
POST /api/v1/google/sheets/append |
Append rows to a sheet |
POST /api/v1/google/drive/upload |
Upload a file to Drive |
POST /api/v1/google/drive/share |
Grant a role on a Drive file to an address |
POST /api/v1/google/docs/replace-text |
Replace text in a Google Doc |
POST /api/v1/comms/slack/send |
Send a Slack message |
POST /api/v1/comms/email/send |
Send an email |
POST /api/v1/documents/render |
Render a templated document |
GET /api/v1/documents/local/roots |
List configured local-read root keys (safe names only) |
POST /api/v1/documents/local/read |
Read txt / csv / pdf / image under allowlisted roots |
POST /api/v1/database/postgres/query |
Postgres read-only SQL |
POST /api/v1/database/sqlite/query |
SQLite read-only SQL |
POST /api/v1/database/mongo/find |
MongoDB find (bounded) |
POST /api/v1/database/redis/string-get |
Redis GET |
Adapters resolve from app.state.adapter_registry. See
pyactuator.api.main for the wiring and pyactuator.api.dependencies.*
for the resolvers.
API reference (trading)
Protocols
| Protocol | Methods |
|---|---|
ExecutionClient |
submit, get_status, cancel, subscribe_fills, close |
BrokerQueryClient |
get_positions, get_position, get_account |
Types
| Type | Purpose |
|---|---|
OrderRequest |
Submit order params (validates on construction) |
OrderResponse |
Submission result (success, external_order_id, status) |
OrderStatus |
Full order status from broker |
Fill |
Execution/fill report |
CancelResponse |
Cancellation result |
Position |
Current position for a symbol |
Account |
Brokerage account info |
Enums
| Enum | Values |
|---|---|
Side |
BUY, SELL |
OrderType |
MARKET, LIMIT, STOP, STOP_LIMIT |
TimeInForce |
DAY, GTC, IOC, FOK, OPG, CLS |
ExecutionStatus |
PENDING_NEW, OPEN, PARTIALLY_FILLED, FILLED, CANCELED, REJECTED, EXPIRED |
Adapters
| Adapter | Install | Protocols |
|---|---|---|
MockExecutionClient |
core | ExecutionClient + BrokerQueryClient |
AlpacaExecutionClient |
pyactuator[trading-alpaca] |
ExecutionClient + BrokerQueryClient |
RetryExecutionClient |
core | wraps any ExecutionClient with retry |
License
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyactuator-0.0.14.tar.gz.
File metadata
- Download URL: pyactuator-0.0.14.tar.gz
- Upload date:
- Size: 1.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3a2f8ec09d4c7dae318891aadaaa22787007227738a1a4e6ac0ed0a40fcfe71c
|
|
| MD5 |
ce21ed7cbefadb942f6ff5f7ebe74f68
|
|
| BLAKE2b-256 |
82661c4404d27d1890efdc837abca9176bfd343fab065eebdd709bb621739d45
|
Provenance
The following attestation bundles were made for pyactuator-0.0.14.tar.gz:
Publisher:
publish.yml on optophi/pyactuator
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyactuator-0.0.14.tar.gz -
Subject digest:
3a2f8ec09d4c7dae318891aadaaa22787007227738a1a4e6ac0ed0a40fcfe71c - Sigstore transparency entry: 2840645167
- Sigstore integration time:
-
Permalink:
optophi/pyactuator@ac42f06e6d733a165d4c4ffb79d5a81e33d5a34b -
Branch / Tag:
refs/tags/v0.0.14 - Owner: https://github.com/optophi
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ac42f06e6d733a165d4c4ffb79d5a81e33d5a34b -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyactuator-0.0.14-py3-none-any.whl.
File metadata
- Download URL: pyactuator-0.0.14-py3-none-any.whl
- Upload date:
- Size: 1.9 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aa91485966e5a58f9024476118a7c88e16bf9819edf3bcdd962115bfb7828f35
|
|
| MD5 |
f2fd0ff397def61e3ab757d32b77328b
|
|
| BLAKE2b-256 |
f52087d7850b3f4e5c7c8c05219d932ee0485ac4f5c056cdf8d1c6ea1010438a
|
Provenance
The following attestation bundles were made for pyactuator-0.0.14-py3-none-any.whl:
Publisher:
publish.yml on optophi/pyactuator
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyactuator-0.0.14-py3-none-any.whl -
Subject digest:
aa91485966e5a58f9024476118a7c88e16bf9819edf3bcdd962115bfb7828f35 - Sigstore transparency entry: 2840645198
- Sigstore integration time:
-
Permalink:
optophi/pyactuator@ac42f06e6d733a165d4c4ffb79d5a81e33d5a34b -
Branch / Tag:
refs/tags/v0.0.14 - Owner: https://github.com/optophi
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ac42f06e6d733a165d4c4ffb79d5a81e33d5a34b -
Trigger Event:
push
-
Statement type: