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 corepyactuator.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 serverpyactuator 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.
  • 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.
  • CLIpyactuator 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 depsalpaca-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)
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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyactuator-0.0.15.tar.gz (1.4 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyactuator-0.0.15-py3-none-any.whl (1.9 MB view details)

Uploaded Python 3

File details

Details for the file pyactuator-0.0.15.tar.gz.

File metadata

  • Download URL: pyactuator-0.0.15.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

Hashes for pyactuator-0.0.15.tar.gz
Algorithm Hash digest
SHA256 9ab033707811fb5c076a9545a97f06157c47340eaccdc0a9f3b5eb6aa1e27446
MD5 101120dda8107a2a48ae361a91d60c1c
BLAKE2b-256 24f5abd80505ef698be7f83d324bd72a7a7ee25f23a9a0436e7bf6ca84b185d2

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyactuator-0.0.15.tar.gz:

Publisher: publish.yml on optophi/pyactuator

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyactuator-0.0.15-py3-none-any.whl.

File metadata

  • Download URL: pyactuator-0.0.15-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

Hashes for pyactuator-0.0.15-py3-none-any.whl
Algorithm Hash digest
SHA256 fdff4e0ba1d44ad0735c8e9150edcbbad9602e63553e03dc3d52af70f789c6d7
MD5 7cd3bf283902c79ecb504e60a6fce7b0
BLAKE2b-256 d2aa157ab2188fec38d2ff539eab435558cd89851569c1290c5694fdef3b1684

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyactuator-0.0.15-py3-none-any.whl:

Publisher: publish.yml on optophi/pyactuator

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.0.16

2 files

This release

0.0.15 This release

2 files

0.0.14

2 files

0.0.13

2 files

0.0.12

2 files

0.0.11

2 files

0.0.10

2 files

0.0.9

2 files

0.0.8

2 files

0.0.7

2 files

0.0.6

2 files

0.0.5

2 files

0.0.4

1 file

0.0.3

1 file

0.0.2

1 file

0.0.1

1 file

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