← All case studies

Case Study

Rebuilding the Signal to Alpha engine

Splitting strategy evaluation from execution in a live system, without stopping it

Next.js 15FastAPIPostgreSQLSupabase AuthSchwab APIWebull APIClaude APIAPSchedulerasyncpgDiscord.pyRechartsDocker

The first version of the Signal to Alpha engine ran one operating-system subprocess per connected brokerage account. Each subprocess independently evaluated the full strategy on every incoming price bar. It worked, and it could not scale.

This is the account of replacing that design with a single event-driven process while the system stayed running — why the original shape had to change, how the migration was sequenced so it could be abandoned at any point, and what the new coupling surface bought.

The Challenge

The original architecture had four compounding problems:

  • Strategy evaluation cost scaled with the number of subscribers, not the number of strategies — the same indicator math ran once per account over identical market data
  • Memory grew linearly with connected accounts, because each one carried a full Python process
  • Strategy selection and order execution lived in one module, so changing how a trade was chosen risked changing how it was filled
  • Every subscriber independently re-derived the same signal, which meant the system had no single place where a signal existed as a fact

The last point is the one that mattered most. Without a durable, shared notion of "the strategy said this at this moment," there was nothing to audit, no way to fan one decision out to several accounts, and no way to reason about whether two accounts had acted on the same information.

What I Built

A single asyncio process housing cooperating components that communicate over a typed message bus.

  • A strategy runner that is deliberately user-agnostic — it consumes one price bar, evaluates the strategy once, and publishes an immutable signal
  • A message bus declared as a Protocol, with an in-process implementation and a Redis implementation selected by environment variable
  • A distributor that fans each published signal out to every enabled subscriber and records the delivery
  • Per-subscriber executor coroutines for paper and live modes, which react to signals and never re-evaluate the strategy
  • A uniqueness constraint on signal deliveries, so a redelivered message cannot execute twice
  • Facades exposing the original engine's risk gates and exit logic, so both engines run the same risk code rather than two copies of it

Strategy logic now executes exactly once per bar regardless of how many subscribers are attached. The worker memory allocation dropped from 512Mi to 256Mi, because the subprocess fan-out it was sized for no longer exists.

How It Works

The migration shipped as nine dependency-ordered pull requests across four waves. The ordering was the point: each wave had to leave the system in a working state, and the legacy path had to stay live and test-covered until the replacement had earned the traffic.

  1. 01Freeze the contract first — the immutable signal shape and the bus abstraction, before anything produced or consumed them
  2. 02Build each component in isolation behind that contract: runner, distributor, paper executor, live executor
  3. 03Assemble them with a single wiring class and an end-to-end test driving one full bar → signal → delivery → execution cycle
  4. 04Expose the new engine through its own API routes and interface, still alongside the old path
  5. 05Repoint the deployment at the new process and retire the old one from production, keeping its source as a tested reference

Freezing the contract before writing producers or consumers is what made the middle steps independently reviewable. Each component could be built and tested against a shape that was already settled, rather than against a moving target.

Key Engineering Decisions

Make the bus a Protocol, not a class

The runners and the executors are coupled through exactly one thing: a typed message crossing a bus interface. The interface is a Protocol with an in-process implementation for a single deployment and a Redis implementation behind an environment variable.

The immediate benefit is not the Redis path, which a single-process deployment does not need. It is that the narrow interface forced the question of what a signal actually is, and prevented the executors from reaching back into runner internals. Scaling across multiple processes later becomes a configuration change rather than a redesign.

Facade the risk logic instead of reimplementing it

The original engine's entry gates and exit management were not rewritten for the new architecture. They were exposed through facades that the new executor calls.

Reimplementing them would have been cleaner to read and considerably more dangerous. Two implementations of "should this position close" is two answers to a question that must have exactly one — and the divergence would surface as a position that one engine would have exited and the other held.

Make duplicate execution impossible at the database, not in the code

Message buses redeliver. Retries happen. Guarding against double execution with application logic means trusting that every future code path remembers to check.

Signal deliveries carry a uniqueness constraint across the signal, the subscriber, and the mode. A duplicate delivery fails at the write rather than proceeding to place a second order. This sits alongside idempotent broker order identifiers, so a retry at the broker boundary cannot double-fill either.

Keep the old path running until the new one earned the traffic

The legacy supervisor stayed live and covered by its tests through every wave, and was retired from production only in the final step. Its source remains as a reference implementation rather than being deleted.

The cost is carrying two paths for the duration. The benefit is that every intermediate state was shippable, and abandoning the migration at any wave would have left a working system.

What Production Revealed

Three things the migration surfaced that were not visible from the design:

Verify "no new failures" rather than asserting it

The claim that the migration introduced no regressions was established by stashing the change and comparing suite results against the baseline, not by observing that the tests looked fine. The suite has known ordering-sensitive failures that pass when run per-directory, which means a raw pass count would have been misleading in both directions.

A narrow interface exposes what was previously implicit

Several pieces of state that the old design carried incidentally — because the evaluating process was also the executing process — had to become explicit fields on the signal or on persisted position state. The bus did not create that coupling; it revealed it.

Position state has to survive the process

Multi-stage exits mean a position is partway through a plan at any given moment. That state is persisted rather than held in memory, including the exit policy the position was opened under, so a restart resumes the plan instead of losing it — and a later change to exit rules cannot retroactively alter a position already in flight.

Result

Strategy evaluation now runs once per bar regardless of subscriber count, and the system has a single durable record of what each strategy decided and when.

The engine runs as one process instead of one per account, at half the previous memory allocation.

Signal delivery is idempotent at the database level, and both the new and legacy engines share one implementation of the risk and exit logic.

The migration shipped across nine pull requests with the legacy path live throughout, and introduced no new test failures against a stashed baseline comparison.

What it demonstrates

Event-driven architectureIncremental migration of a running systemInterface design and decouplingIdempotency and exactly-once executionConcurrency with asyncioRisk-sensitive change management

The architecture described here is deliberately unremarkable — publish/subscribe decoupling and staged replacement of a legacy path are well-established practice. What the project required was applying them to a system that moves real money and could not be stopped, which puts the interesting work in the sequencing and the failure paths rather than in the pattern choice.

Discuss a project like this