Production runtime for a DAG of interconnected trading agents — TypeScript + NestJS, hexagonal architecture, tick-driven. Agents are nodes in a compile-time-type-checked graph: signal producers feed decision agents, decision agents emit trade intents, executors act — with staleness gates, circuit breakers, idempotent order submission, and an append-only event log underneath. Runs against Hyperliquid testnet.
Maestro is the production half of a two-system design. The research half — rumpy, a private Rust monorepo — trains the models and owns the strategy; its execution layer is public at rumpy-execution. Maestro runs the live side: it collects market data, materializes features, invokes the trained models and the Rust execution solver as subprocesses, and turns target weights into orders. Every domain event it emits is appended to an event log that rumpy consumes offline for the next research cycle — the two systems form a loop.
flowchart LR
RUMPY["rumpy (private)<br/>research · training ·<br/>backtesting"] -->|"models + solver<br/>artifacts"| M["maestro<br/>production runtime"]
M -->|"append-only<br/>event log"| RUMPY
M <-->|"orders · fills ·<br/>market data"| HL["Hyperliquid<br/>(testnet)"]
One process, one graph, one pass per tick:
flowchart TB
subgraph SRC["Tick sources — merged into one AsyncIterable<Tick>"]
direction LR
CRON["cron"]
REC["reconciler"]
RSV["ws · data-gate · manual<br/>(reserved type-union slots, unwired)"]
end
SCHED["SchedulerPort — merge(cron, reconciler)<br/>(switch/gate combinators: library surface)"]
PIPE["signal pipeline (pre-tick, not agents)<br/>OHLCV ingestion → feature compute → model scoring →<br/>alpha build → execution solve (Rust subprocess)"]
CRON --> SCHED
REC --> SCHED
SCHED --> PIPE
subgraph DAG["Agent DAG — one forward pass per tick"]
SA["source agents"]
SIG["signal agents"]
DEC["decision agents<br/>→ TradeIntents"]
VAL["validate — pass-through stub<br/>(risk/margin checks: planned PR)"]
EXE["executor agents<br/>deterministic clOrdId"]
SA --> SIG --> DEC --> VAL --> EXE
end
PIPE --> DAG
subgraph PORTS["Ports → swappable adapters"]
direction LR
XP["ExecutorPort<br/>AccountPort"]
DP["DataSourcePort<br/>FeatureReaderPort"]
MP["ModelRegistryPort<br/>StateStorePort"]
NP["NotifierPort"]
end
DAG --> PORTS
PORTS --> EXT["Hyperliquid · data feeds ·<br/>Telegram · model storage"]
DAG -->|"every DomainEvent"| SINK["EventSinkPort — append-only JSONL<br/>flush → fdatasync group commit<br/>(loss bound: one tick)"]
Each tick carries a UUIDv7 id (monotonic counter + clock-regression guard), a logicalTime from ClockPort (LiveClock in production; a SimulatedClock exists but no replay mode is wired yet), and ts_event vs ts_init for late-arrival detection. Duplicate tick ids are dropped by a 5-minute-TTL dedup before any phase runs.
- Pre-tick pipeline — infrastructure (data ingestion, feature computation, model scoring, the Rust execution solve) runs before the graph, gated on data freshness. This mirrors the convention in LEAN, NautilusTrader, Freqtrade, and Hummingbot: data preparation is a separate phase, not an agent.
- Decide — the executor walks the DAG in topological order,
Promise.allper depth level. Every agent input carries anasOftimestamp; if an upstream output is older than its declared staleness budget, the agent is skipped — anAgentSkippedEventis emitted and the skip cascades downstream — unless the agent opted into a TTL-boundedlastGoodOutputfallback. Failures count against a circuit breaker (5 failures/60s → open 30s → half-open probe). - Validate — currently a deliberate pass-through: the phase boundary exists in the executor, but risk/position/margin checks are a planned PR (the code says so in a comment rather than pretending). Intents flow through unchecked today.
- Execute — orders go out sequentially with a deterministic
clOrdId = SHA-256(agentId | tickId | intentSeq)truncated to 128 bits. The primary re-execution guard is the in-process tick dedup; the deterministic cloid makes any residual duplicate identifiable venue-side. - Publish — every domain event from the tick is appended to the JSONL event sink, flushed, then fdatasync'd (the Postgres/EventStoreDB group-commit pattern). Agents never read the log; it exists for offline research.
| Path | Responsibility |
|---|---|
src/modules/scheduler/, src/libs/scheduler/ |
Cron + reconciler tick sources composed via merge; switch/gate combinators and the ws/data-gate/manual source types are library surface, unwired in production |
src/libs/agents/ |
BaseAgent contract, GraphBuilder (generics enforce typed edges at compile time), DagExecutor, circuit breaker, staleness gate |
src/modules/runtime/ |
RuntimeModule/RuntimeService — graph assembly, tick-source wiring, pre-tick hook registration |
src/modules/agents/ |
Concrete signal and decision agents (model scoring, portfolio execution) |
src/modules/signal-pipeline/ |
Pre-tick infrastructure: OHLCV → features → model scores → alpha → execution solve, freshness-gated |
src/modules/exchanges/hyperliquid/ |
Venue adapter: REST + WS transports, wallet signing, order lifecycle |
src/modules/execution-compute/ |
Spawns the Rust rumpy-execution solver; weights parquet in/out |
src/modules/model-registry/, feature-* |
Model artifact resolution and feature parquet access |
src/modules/event-sink/ |
Append-only JSONL domain-event log with group-commit durability |
src/modules/state-store/, snapshot/, intent-snapshots/ |
Positions, order state, and point-in-time snapshots |
src/modules/data-collection/, data-ingestion/ |
L2 book and trades collectors (WS + REST polling; liquidations captured as a flag on trade rows) behind an outbound SOCKS proxy pool |
src/modules/notifier/ |
Telegram lifecycle and fill notifications |
src/configs/ |
Zod-validated YAML + env config with fail-closed boot checks and secret redaction |
src/libs/ |
DDD tactical layer (Entity, AggregateRoot, DomainEvent), ports, transports |
-
Typed edges, checked at compile time.
GraphBuilder's generics make it a type error to wire an agent to an upstream output it doesn't accept — DAG mistakes are caught at build, not at 3am. Rejected: runtime wiring with string keys. -
Infrastructure is not an agent. Ingestion, feature computation, and model scoring materialize artifacts in a pre-tick phase; only signal/decision/executor logic lives in the graph. Adopted after surveying how LEAN, NautilusTrader, Freqtrade, and Hummingbot all separate data preparation from strategy. Rejected: modeling pipeline steps as DAG nodes — it entangles retry/freshness semantics with trading logic.
-
Fail closed, everywhere. Stale input → the agent is skipped (or serves a TTL-bounded last-good output where it explicitly opted in) — never a trade on old data. Missing proxy credentials with endpoints configured → refuse to boot.
HL_MASTER_PRIVATE_KEYset on a mainnet host → refuse to boot (assertRuntimeSafe). Config is Zod-validated with secrets redacted from logs by an explicit redaction path list. Rejected: permissive defaults and warning logs nobody reads. -
The event log is the product. Every domain event appends to JSONL with flush + fdatasync group commit — worst-case loss on power failure is one tick. The log is write-only from Maestro's perspective; rumpy consumes it offline, closing the research loop. Rejected: database-first event storage — a single-writer append-only file with group commit is simpler and its durability bound is explicit.
-
Architecture rules as executable checks. The hexagonal/layering boundaries aren't a convention document — dependency-cruiser rules (
npm run deps:validate) enforce them in CI; a DDD-compliance script tracks known violations in an explicit allowlist whose own header says each alignment PR removes entries and the file dies when empty; andcoding-standards.invariants.spec.tsenforces code-level invariants (every fire-and-forget async site needs a.catchor an explicit safe-void annotation) inside the test suite. Rejected: code-review vigilance.
93 suites, 994 tests, verified green from this tree:
npm ci
npm test # unit + invariant suites
npm run deps:validate # architecture layering rules (also in CI)Integration scripts (scripts/test-integration-*.ts) exercise live-adapter paths (model registry, transports, venue round-trips) against testnet and are run per-PR rather than in the default suite.
- Testnet. The deployment trades Hyperliquid testnet (the config default); master-wallet signing on mainnet is refused at boot (
assertRuntimeSafe). Mainnet access today is read-only market-data collection. - The research loop's other half (training, backtesting, strategy) is private — see the
rumpy-executionREADME for the published slice of it. - Development happens on
staging(the default working branch);mainmirrors it at release points. History is ~100 research-gated PRs across 400+ commits — eachprs/PR-*.mdfile carries the research, design, and verification criteria for its change. - Scaffolded from domain-driven-hexagon (MIT); the DDD tactical layer in
src/libs/derives from it.
MIT