Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

412 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Maestro

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.

Where this fits

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)"]
Loading

Architecture

One process, one graph, one pass per tick:

flowchart TB
    subgraph SRC["Tick sources — merged into one AsyncIterable&lt;Tick&gt;"]
        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)"]
Loading

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.

What happens on a tick

  1. 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.
  2. Decide — the executor walks the DAG in topological order, Promise.all per depth level. Every agent input carries an asOf timestamp; if an upstream output is older than its declared staleness budget, the agent is skipped — an AgentSkippedEvent is emitted and the skip cascades downstream — unless the agent opted into a TTL-bounded lastGoodOutput fallback. Failures count against a circuit breaker (5 failures/60s → open 30s → half-open probe).
  3. 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.
  4. 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.
  5. 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.

Component map

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

Key design decisions

  1. 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.

  2. 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.

  3. 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_KEY set 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.

  4. 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.

  5. 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; and coding-standards.invariants.spec.ts enforces code-level invariants (every fire-and-forget async site needs a .catch or an explicit safe-void annotation) inside the test suite. Rejected: code-review vigilance.

Testing

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.

Status — honest scope

  • 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-execution README for the published slice of it.
  • Development happens on staging (the default working branch); main mirrors it at release points. History is ~100 research-gated PRs across 400+ commits — each prs/PR-*.md file 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.

License

MIT

About

Production runtime for a DAG of interconnected trading agents — TypeScript/NestJS, hexagonal, tick-driven. Compile-time-typed DAG wiring, staleness-gated signals, idempotent execution, append-only event log. Hyperliquid testnet.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages