Skip to main content

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), and research. See Domains Overview.
  • Stateless core — pyactuator.core ships 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, and pyactuator.testing wires 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 serve offers a tool profile's operations to Claude Desktop, Claude Code and any other MCP client, each call validated, governed and audited. See MCP server.
  • Operation plugins — installed packages can add their own operations to the catalog, available through invoke(), MCP and PyStator actions. See Publish your own operation.
  • Deployment manifest — one pyactuator.yaml declares the agents sharing a runtime: each one's tool profile, credentials and policy. pyactuator preflight --strict checks it before a deploy. See Deployment manifest.
  • CLI — pyactuator run for unified YAML/JSON configs, pyactuator tools list for valid operation names, plus api, ui, worker, and docs helpers.
  • HTTP API — domain-scoped FastAPI routers under /api/v1/<domain>/..., sharing a single AdapterRegistry.
  • Optional deps — alpaca-py, httpx, google-auth, jinja2, markdown-it-py, and reportlab are all optional extras, so you install only the vendors you actuate. The base install pulls duckdb + pyarrow for the analytics, datasets, data-source, and charts domains.
  • Backwards compatible — existing from pyactuator import OrderRequest imports 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 / Calendar)
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 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.

Metadata

Release files for pyactuator 0.0.17

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyactuator 0.0.17
File Size Uploaded Provenance Actions
pyactuator-0.0.17.tar.gz 2.2 MB With provenance Details for pyactuator-0.0.17.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pyactuator 0.0.17
File Interpreter ABI Platform Actions
pyactuator-0.0.17-py3-none-any.whl Python 3 none any Details for pyactuator-0.0.17-py3-none-any.whl

Total release size: 4.5 MB

Release files / pyactuator-0.0.17.tar.gz

Download URL pyactuator-0.0.17.tar.gz
Size 2.2 MB
Tags Source
SHA-256 checksum
How to use checksums
4f578c282b8c3a5bce41efac13c9891c1c9e2a0369125ee3eebfee1e3669e688
BLAKE2b-256 checksum
How to use checksums
bf48557415d71c535cea94eea34b2c774e8e1113b40bcd25693c1830d82576d2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / pyactuator-0.0.17-py3-none-any.whl

Download URL pyactuator-0.0.17-py3-none-any.whl
Size 2.2 MB
Tags Python 3
SHA-256 checksum
How to use checksums
a9e77b4c929055d8b7668c6ca23ce041bb58b43a01db9c328d9d5eeb71974a48
BLAKE2b-256 checksum
How to use checksums
d74f6f408dc32454f948cfec1d20f664c911efe411c9fbe96ad5c2fb356eefe0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Security for pyactuator 0.0.17

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for pyactuator-0.0.17.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page