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
| Process | Host | Role |
|---|---|---|
Plutus.Trading.Console | Trader VM, Standard_B2s, Ubuntu, systemd | The engine; the only thing that talks to brokers |
| IB Gateway (JVM) | Same VM | Broker session; auto-restart and daily re-auth |
Plutus.AdminWeb | App Service B1 | Human control plane; writes DaemonCommands, reads journals |
Plutus.Api.AzureFunctions | Function App B1 | HTTP API over the database |
| MySQL Flexible Server B1ms | Private VNet | All 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.
| Phase | Nature | Concurrency |
|---|---|---|
| Reconcile | I/O, broker and database | Sequential over in-flight orders; ordering matters, volume is tiny |
| Pull | Network I/O | Async, about 8 in flight, per universe; one pull feeds all accounts |
| Assemble | CPU, small | Single-threaded, once, then frozen immutable |
| Decide | CPU, pure | Parallel.ForEachAsync over accounts, MaxDoP 2 to 4 |
| Execute | I/O | Paper in parallel; live serialized through one channel |
| Journal | Database writes | Batched, 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.

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 theDocumentSerializerevery 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.BlobStorage —
IArtifactStoreand 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#.

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.
- 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
same inputs, same outputsrule
SIGKILL mid-phase is supportedrule
halt the account, not the fleetrule
never bought with 1–3last