Status: proposal for implementation, 2026-11-13. Today the repository contains an interactive mockup and documentation, with no backend, database schema, Compose deployment or Python application scaffold. This sketch translates the product vision into an implementable design; it does not claim these components exist. GUIDELINES and the crypto policy remain mandatory.
Build a modular Python application with a shared domain core and separately
runnable API and engine. Use a small uv workspace and build a separate image
for each application component. Start with three Compose services: api,
engine, and one PostgreSQL database. Add a separate research process/image
when running long backtests alongside live trading; CPU-heavy work must not run
on the order-processing event loop.
| Choice | Purpose and boundary |
|---|---|
| HTML, custom CSS, plain JavaScript | Buildless frontend; reusable controls, hub navigation, charts and API requests. Serve it from the API image under the same origin. The mockup remains a separate demo artifact. |
| FastAPI at the HTTP edge | Proposed routing, request validation and resource lifecycle. Pydantic handles DTOs/settings; domain services receive typed immutable values and ports. |
| Python engine | Market ingestion, forward paper runs, live execution and reconciliation, independent of an open browser. One active execution engine per exchange account. |
| PostgreSQL | One transactional source for configurations, durable commands, accounting, orders and evidence. Concurrent API/engine access and atomic capital reservations justify a database service. |
| Optional research worker | Bounded backtest jobs over stored data. No trading credentials, order-submission capability or authority to change real balances. |
Use database-backed work records initially; a broker, cache cluster, frontend build chain and separate web server are not needed for the proposed workload. Pin Python, dependencies and image versions when introducing their executable configuration. No backend dependencies are installed by this document.
Only the API's private ingress is reachable from the LAN. Do not publish database, engine or research ports, or add public ingress. External exchange connectivity is outbound. LAN access still requires authentication, TLS and authorization.
Proposed layout; create packages as their implementation becomes necessary:
packages/
trader-core/src/trader_core/
models.py # IDs, assets, quantities, immutable commands/results
ports.py # capability ABCs, contracts and safe exceptions
strategies/ # pure algorithms and typed parameter definitions
catalog.py # configuration/version rules
runs.py # lifecycle and execution specifications
execution.py # policy/risk decisions and order planning
accounting.py # allocations, reservations and fill application rules
analysis.py # versioned performance calculations
trader-adapters/src/trader_adapters/
postgres/ # transactional persistence and read projections
coinbase/ # account, market and order adapters
simulation.py # explicit fill simulator, never a live gateway
trader-api/src/trader_api/
main.py # create_app(settings), dependency wiring, lifespan
routes/ schemas.py settings.py static/
trader-engine/src/trader_engine/
__main__.py worker.py settings.py
trader-research/src/trader_research/ # add when backtest isolation is needed
tests/
contracts/ fakes/ core/ api/ engine/ e2e/
deployment/ # private Compose configuration and release wiring
The API and workers compose capabilities; domain services import domain models and ports, never SDKs, adapters, FastAPI or Pydantic. Keep algorithms pure: market observations + parameter/state snapshot → decision + next immutable strategy state. No network access, credentials or order submission inside a strategy. Persist state needed for restart; a process-local object is not the authoritative portfolio.
These names identify responsibilities, not a requirement for an interface per
table. Define a port where a capability boundary is useful. API wiring has no
OrderGateway; research wiring has no live gateway or trading key. Engine
paper-run wiring receives a simulator and virtual accounting scope. All external
adapter implementations and meaningful fakes pass the same capability contract
tests, including failures, timeouts and cleanup.
| Capability | Contract that matters |
|---|---|
| Catalog | Save a new immutable configuration version; reject stale edits using an expected revision; resolve an exact algorithm/version. |
| CommandStore | Durable acceptance and explicit claim/completion; at-least-once processing, bounded retry/recovery, stable idempotency key and rejection of key reuse with a different payload. |
| TradingLedger | Complete atomic mutations: fund a run, reserve an intent, apply a unique fill, release a confirmed reservation. Never expose a series of independently committing balance updates. |
| OrderGateway / AccountReader | Role-specific submit/cancel/query and snapshot/history capabilities; distinguish rejection, unavailability and an unknown outcome. |
| MarketFeed / MarketHistory | Timestamped observations with gap/freshness semantics; no fabricated continuity after reconnect. |
| FillSimulator / ResultStore | Reproducible simulated fills and immutable observations labelled by mode, dataset, model and calculation version. |
Use explicit validated constructor arguments and composition roots. Settings
hold secrets as SecretStr; ports return safe domain exceptions. Resources
start explicitly, close idempotently under cancellation, and have bounded waits.
The engine owns its asynchronous loops with TaskGroup; an execution-critical
failure closes the submission gate and exits for controlled recovery.
| Record | Important identity and invariant |
|---|---|
| StrategyDefinition | Strategy ID, implementation version/code revision, parameter schema version, supported universe. Algorithms are registered in code; the browser edits parameters. |
| Configuration / ConfigurationVersion | Configuration ID and name; append-only numbered parameter snapshots. Editing or duplicating never mutates an existing version. |
| Run | Run ID, exact configuration and implementation versions, immutable mode and execution specification, mutable lifecycle. Promotion creates a new run. |
| ExchangeAccount / Asset / Product | Exchange/account identity; asset/network identity and canonical product metadata. A display ticker is not an identity or proof of tradability. |
| Allocation / Reservation / LedgerEntry | Live account, run, asset, quantity, reason and source event. Reservations are a subset of allocations. Paper accounting is a separate scope. |
| Command / OrderIntent / Order / Fill | Stable command and client-order IDs, exchange order/fill IDs, deduplication keys, transitions and reconciliation evidence. |
| Observation / AnalysisResult | Run and actual covered window, market dataset, fees/slippage model, valuation basis and calculation version. |
| PolicyRevision / AssetClassification | Profile, sources, effective/review dates and allow/deny/review decisions; frozen evidence on each execution decision. |
Use deeply immutable, typed Python values (frozen=True, slots=True, tuples,
distinct ID/quantity types). Use Decimal and database exact numeric values for
money, prices and quantities; send decimal strings across JSON. Validate exchange
increments/minimums explicitly. Time values are timezone-aware UTC; reporting
timezone is separate. The mock's JavaScript numbers are demonstration data.
Run mode (backtest, paper, live), lifecycle (starting, running, paused,
blocked, stopped) and evidence availability are separate concepts. New runs
have no metrics until their own observations exist. Store parameter, algorithm,
instrument, policy and simulation versions needed to reproduce results. An
upgrade that cannot resolve an active run's implementation version blocks that
run instead of silently substituting new code.
Use foreign keys, uniqueness constraints and explicit transaction locks for cross-process invariants. Versioned SQL migrations and restore-tested backups are part of the database implementation. Corrected financial records use linked adjustments; do not rewrite historical fills or silently remove discrepancies.
OrderGateway.Proposed HTTP surface: strategy/configuration queries and version creation; run/result queries; paper/backtest requests; live-allocation previews and confirmation; pause/halt/resume/cancel commands; command status; portfolio, activity, connection and policy queries. Begin with same-origin JSON and bounded polling. The browser displays server state; it never computes authoritative free capital, chooses an order route or stores exchange credentials.
The engine is the sole writer of live allocations, reservations, orders and fill-derived accounting. Enforce one active executor per account operationally and through database ownership checks; do not deploy overlapping live engines. Losing database access/ownership blocks new submissions. Local locks coordinate tasks but do not replace transactional cross-process invariants.
For each live decision: evaluate → check policy/risk → reserve quantity including fee exposure and persist an order intent → submit with the persisted client ID → reconcile outcome. Commit intent/reservation before the external request; do not hold a database transaction open over a network call. Replayed commands and fills are idempotent. A fill and its ledger/reservation changes commit atomically; partial fills retain the remaining reservation.
A timeout after submission is unknown, not a rejection. Preserve the client
ID, query/reconcile the existing order and fills, and block conflicting reuse of
funds. Never generate a new order ID merely to retry an unknown result. Coinbase
documents returning the existing order for a reused client_order_id; verify
this behavior in the adapter contract and retain recovery logic rather than
claiming end-to-end exactly-once execution. See Create Order.
On startup or feed interruption, close the execution gate and reconcile account balances, open orders and fills against persisted state. WebSocket observations need gap detection and REST recovery; absence from a feed is not proof of cancellation. See Coinbase's current WebSocket endpoints. Unexplained balances and external/manual orders are visible restricted holdings, not allocatable funds. A dedicated account reduces attribution ambiguity. Persist and reconcile observed exchange events even when they fall outside the permitted trading universe, including unexpected conversions and fee assets. Mark their treatment for review. Preserve undecodable events durably for investigation and block affected execution until their accounting is resolved.
reconciled holdings = free treasury + restricted unallocated holdings
+ sum(run available assets + run order reservations)
With no restrictions, this reduces to the mock's free + allocated view. Value each asset once at one stated snapshot; exchange and run breakdowns are views of the same holdings. Never add reservations or paper balances to total wealth.
Halt requests persist a close-only safety flag with their command; the engine checks it before dispatch. UI distinguishes requested halt, engine-acknowledged halt and orders needing reconciliation. A request can race an in-flight order. Pause/halt never implies cancellation, liquidation, released capital or a fixed loss guarantee. Cancellation is a separate confirmed transition. Resume requires current reconciliation/policy/data checks and preserves individually paused runs. Only the engine can reopen the gate; the API cannot authorize an execution bypass. No best-effort/drop-oldest queues for orders, fills or accounting.
The crypto policy applies at execution, including every route leg and fee asset. Unknown/stale classification blocks live execution. PLN valuation, trading P&L and tax records are distinct. A tax export preserves evidence for review; it does not establish a universal tax exemption.
| Mockup area | Backend responsibility |
|---|---|
| Performance | Read projections over run observations; consistent period, unit, benchmark, fees and formula version; show sample coverage and freshness. |
| Strategy hub / Overview | Versioned strategy catalogue and supported parameter schema. |
| Configurations | Append-only configuration versions and edit-conflict handling. |
| Runs & results | Lifecycle, frozen execution specification, samples, state and capital attribution. |
| Deployment | Preview, explicit confirmation, durable command, authoritative funding and policy/risk gates. |
| Portfolio | Reconciled per-asset ledger, real allocations, reservations and restricted balances. |
| Activity / Rules & connections | Business audit events, command outcomes, sanitized connection health and policy review state. |
Use the same pure algorithm for replay and forward evaluation. Backtests use fixed datasets and explicit fee/slippage/latency assumptions; paper runs use observed markets and simulated fills; live runs use actual fills. Version metric definitions, sampling and annualization; account for deposits/reallocations before presenting returns. Keep heavy calculations out of execution loops. Read projections may be rebuilt; the critical ledger may not be dropped.
Use the self-hosted GitHub runner and Infisical for CI/CD secrets. Build component images tagged with the same reviewed source revision. API database permissions cover configuration, command acceptance and read models; engine permissions cover trading mutations. Research permissions cover market history, research jobs and simulation results. Supply trading keys only to engine wiring, with withdrawals disabled. Secrets never enter frontend assets, images or logs.
Authenticate the single owner with a server-side session, secure/HttpOnly cookies, CSRF protection and same-origin requests over private HTTPS. Configure private TLS ingress explicitly; choose its implementation with the home-network setup. Close the execution gate before deployment, drain/reconcile in-flight work, back up and migrate the database once, then restart with startup reconciliation. A code rollback does not undo exchange transactions or database migrations.
| Goal | Where behavior belongs |
|---|---|
| Add an algorithm | Pure implementation and parameter model in trader_core/strategies; register its version at the composition root. |
| Add an exchange | Role-specific adapters behind existing account/market/order ports; run the same contract suites. |
| Change risk/legal rules | Core decision services and versioned policy data; enforce in engine, expose reason codes to UI. |
| Add a metric | Versioned analysis calculation, read projection and metric definition; never rewrite raw evidence. |
| Add a UI feature | Buildless static UI + typed HTTP DTO/use case; keep money decisions in backend services. |
Before live execution, require observable tests for concurrent allocation, duplicate commands/fills, unknown submission outcomes, partial fills, stale metadata, feed gaps, cancellation races, ownership loss and crash/restart recovery. Test paper/live isolation, real Postgres transactions and no secret leakage. A new external adapter needs the port's shared contract suite; mocks of its internal SDK do not establish that contract. Keep at least one real-adapter end-to-end path with exchange HTTP/WebSocket behavior supplied by a local test server; tests must never place real exchange orders. Verify SIGTERM/cancellation cleanup and database backup restoration.
When the scaffold exists, run frozen uv installation, the configured
pre-commit checks, then pytest/contract/integration tests, plus Compose config
validation. These are proposed gates, not checks currently configured or passed.
Application logs use safe constant messages and structured error types; audit
records are separate from logs. Document low-cardinality metrics for execution
health, feed freshness, pending commands, reconciliation and blocked orders.
Technical references checked for this sketch: FastAPI static files and lifespan, and PostgreSQL transaction locking and constraints. These support implementation mechanisms; the component split and ownership rules above are project design decisions.