PLUTUS · ENGINE ROOM // PAPER ONLY · NO LIVE CAPITAL
ENGINEERED BY LEOPARD DATA

System Architecture

One long-lived process runs the whole fleet in phases, a handful of libraries share the plumbing, and the line between what is built and what is only drawn is stated plainly.

One process that trades

Plutus is a scheduled batch engine, not a low-latency trading system. The morning cycle has about fifteen minutes of wall clock to do a few seconds of real work for the whole fleet, and that one fact governs every choice below. Concurrency exists to keep I/O from serializing and to keep backtests tractable, never to shave microseconds off a decision path that has minutes to spare.

The engine is a single BackgroundService in Plutus.Trading.Console. It never shuts down, because the IB Gateway session it owns is expensive to re-establish; a PeriodicTimer fires the morning cycle at 06:45 ET and reduced cycles each hour after that. There is no thread per account, no thread per strategy, and no actor mailboxes. There is exactly one place to look when asking what the machine is doing right now.

Three rules beat performance every time, in this order: determinism (the same inputs must produce the same outputs, or the frozen-baseline gate and the whole backtest-to-promotion pipeline are theatre), resumability (a deploy, crash or VM restart at any instant resumes without repeating or skipping work), and isolation (one account's failure halts that account, never the fleet). Throughput is fourth, and it is never allowed to buy itself with the first three.

Runtime topology

ProcessHostRole
Plutus.Trading.ConsoleTrader VM, Standard_B2s, Ubuntu, systemdThe engine; the only thing that talks to brokers
IB Gateway (JVM)Same VMBroker session; auto-restart and daily re-auth
Plutus.AdminWebApp Service B1Human control plane; writes DaemonCommands, reads journals
Plutus.Api.AzureFunctionsFunction App B1HTTP API over the database
MySQL Flexible Server B1msPrivate VNetAll state; one vCore, the scarcest resource in the estate

The B2s has 2 vCPU and 4 GB. The Gateway's JVM takes 1 to 1.5 GB and the OS about half a gigabyte, so the daemon plans against roughly 1.5 usable vCPU and 2 GB. Memory is not the constraint: a trailing two-year bar cache for 520 symbols is about 20 MB, and the full 25-year history about 210 MB. CPU credits are the constraint. B-series machines bank burst credits and throttle to about 40 percent baseline when they run out, so a long CPU burn can degrade the trading path hours later. The rule that follows: heavy backtests never run during market hours.

Read wide, compute parallel, write batched

Each cycle is a sequence of phases, and each phase picks the concurrency that fits its work.

PhaseNatureConcurrency
ReconcileI/O, broker and databaseSequential over in-flight orders; ordering matters, volume is tiny
PullNetwork I/OAsync, about 8 in flight, per universe; one pull feeds all accounts
AssembleCPU, smallSingle-threaded, once, then frozen immutable
DecideCPU, pureParallel.ForEachAsync over accounts, MaxDoP 2 to 4
ExecuteI/OPaper in parallel; live serialized through one channel
JournalDatabase writesBatched, few connections

The pattern in one line: bulk-read, then parallel pure compute, then bulk-write. Before the fan-out, all accounts' configs, bindings, positions and caps load in about four bulk queries and are partitioned in memory. Forty accounts each issuing their own three queries would be 120 round-trips against a one-vCore server; the bulk form is four. Inside the fan-out, each account task receives the one immutable MarketState plus its own pre-loaded state and touches no DbContext at all. Results come back as in-memory records and are written in batched multi-row inserts, a handful of transactions rather than thousands of autocommits.

The account is the unit because the domain already makes it one: capital, risk caps, cash floor and the allocation model all compose within an account, and strategies inside an account cannot run independently, since the allocation model must see every bound strategy's proposal together. Parallelism may change wall-clock and never results, which holds only while accounts do not interact within a cycle.

DaemonCommands is polled between phases and before each account in the fan-out, so a halt or kill switch lands within one account's compute rather than at the end of a cycle. Shutdown is cooperative but never assumed: a SIGKILL mid-phase is a supported case, not an incident. The engine's day in full.

Inside the engineComponents inside Plutus.Trading.Console
The target design of the one process that trades. Today the project holds Program.cs and a TradingDaemonService skeleton; the boxes are what fills it in.

The projects

The solution mirrors Gmi.Ai on purpose, so anyone fluent in one repo is fluent in both. What exists today, and what each piece is for:

  • Plutus.ClassLibrary — the frozen contracts: IStrategy, MarketState, TargetWeights, Maybe<T>, and the DocumentSerializer every JSON document passes through.
  • Plutus.Strategies — the strategy classes and their config records. It references only the ClassLibrary; it cannot see a database, a broker or a clock. Why that matters.
  • Plutus.MarketData — the blob bar store, the market calendar interface, and the path conventions for bars and corporate actions.
  • Plutus.Backtester — the point-in-time assembler, the day loop, the book, the fill simulator, the tear sheet builder, and the EF-backed run store.
  • Plutus.BlobStorageIArtifactStore and its Azure implementation.
  • Plutus.RestClient — typed clients for FMP, Claude, SendGrid and Twilio.
  • Plutus.DataAccess — EF Core with Pomelo, PlutusDbContext, and the entities that exist so far.
  • Plutus.Services — business logic; today, the news archive service.
  • The consoles — Trading (the daemon), Backtester, NewsArchiver, EmailSender and TextSender; the last two are ports of the GMI senders.
  • Plutus.AdminWeb, Plutus.Technology (this site), Plutus.Api.AzureFunctions, and Plutus.Pulumi.Infrastructure.Prod, the estate as C#.
Projects in the solution and their dependencies
Runnable hosts on top, shared libraries below. Everything reaches the database through DataAccess and the outside world through RestClient.

Built versus designed

The gap between design maturity and code maturity is the defining fact of the project. Twenty-four design documents preceded any engine. As of today the built half is: the news archiver and its blob layout (Gate 1 passed); the contracts, the backtester and three strategies with twelve contract tests (Gate 2 passed); the 124-table schema loaded into a local MySQL 8 with its vocabularies and the measured market calendar; and one storage account in Azure.

The designed-only half is most of the diagram above. The daemon's RunCycleAsync reads configuration and pending orders and then reaches a TODO where the system goes. The resolver engine, the probability engine, the news pipeline, the gauntlet, the admin UI and the IBKR channel are specified and unbuilt. The entity layer covers six tables. The Pulumi estate is coded but gated behind a deployEstate flag, so the VM and the database wait for Phase 4. The build log records each gate as it passes.

Facts
  • One long-lived process; one loop
  • ~15 min of wall clock for the morning cycle
  • 4 bulk queries before the fan-out, not 120
  • MaxDoP 2–4 across accounts in Decide
  • 1 vCore of MySQL, the scarcest resource
  • ~930 lines of engine C# when the plan was written; the backtester has since been built
The priority order
1Determinism
same inputs, same outputs
rule
2Resumability
SIGKILL mid-phase is supported
rule
3Isolation
halt the account, not the fleet
rule
4Throughput
never bought with 1–3
last