mercury-architecture-snapshot
Β· 3 min read
Minimal plan that actually works and stays smallβ
-
Position queue
- Keep
KAIROS.position-executiononly (scheduler + execution). - Keep
KAIROS.dynamic-tpsl(independent of sync loop).
- Keep
-
Public runtime surface (4 files, small)
-
position-execution.consumer.ts
- Handles
command.schedule_positionsβ evenly enqueuecommand.sync_position. - Handles
command.sync_positionβ calls orchestrator; tiny effect mapping only if needed. - trading-context.orchestrator.ts
- Derive diagnostics via
derive-sync.tsβ choose phase β call adapter method.
- Derive diagnostics via
- TradingAdapter + two adapters with identical interface
- live-context.adapter.ts implements:
- syncCreation(position)
- syncPromotion(position)
- syncOngoing(position)
- Internals: reconcile(position) does everything (TP diff/cancel/create, SL ensure/update, trailing SL).
- shadow-context.adapter.ts implements the same 3 methods
- Internals: simulateTriggers+fill via OrderService.
- live-context.adapter.ts implements:
- Handles
-
Events (minimal, useful)
- QueueEventService publishes
event.position_partially_closed/closed/status_changedtoKAIROS.position-execution. - order-events.consumer.ts enqueues
command.sync_position { positionId }immediately.
- QueueEventService publishes
What to delete/replace to deflate the bloatβ
- Delete: position-sync.engine.ts
- Delete: base-trading-context.ts
- Replace: live-trading-context.ts β live-context.adapter.ts (β€250 LOC; reconcile-centric)
- Replace: shadow-trading-context.ts β shadow-context.adapter.ts (β€200 LOC; simulate+fill)
Tiny interfaces (one adapter interface for both)β
type TradingPhase = 'creation' | 'promotion' | 'ongoing';
interface TradingAdapter {
getType(): 'LIVE' | 'SHADOW';
syncCreation(position: Position): Promise<string[]>;
syncPromotion(position: Position): Promise<string[]>;
syncOngoing(position: Position): Promise<string[]>;
}
- orchestrator
- phase = deriveSyncDiagnostics(position, orders).phase
- adapter = factory.for(position)
- call adapter.syncCreation|syncPromotion|syncOngoing
- persist only lastSyncUpdate
Why this stays small and testableβ
- One public method per phase per adapter. All βhowβ lives inside private reconcile()/simulate().
- No engine, no base class. One interface = zero drift.
- External-only mocking: mock Bybit client; use real DB/OrderService.
- Tests:
- derive-sync.spec.ts (pure)
- live adapter βreconcileβ spec (Bybit mocked)
- live-sync-positions.e2e-spec.ts (happy path)
- order-events.consumer.spec.ts (enqueues immediate sync)
Migration steps (safe and quick) - β ALL COMPLETED!β
- Add TradingAdapter, trading-context.orchestrator.ts. β Done
- Add live-context.adapter.ts (copy only needed logic; fold createTPOrders/updateSL/etc. into private reconcile()). β Done (359 LOC, 6 methods)
- Add shadow-context.adapter.ts (simulateTriggers+fill). β Done (195 LOC, 5 methods)
- Update trading-context.factory.ts to return new adapters. β Done (new methods + orchestrator integration)
- Replace KairosEventService with queue-event.service.ts in OrderService/PositionService. β Done (OrderService.fillOrder() β publishOrderFilledEvent() + order-filled.consumer)
- Add order-events.consumer.ts to enqueue immediate sync on events. β Done
- Change position-sync.consumer.ts to call orchestrator. β Done (ΠΏΠ°ΡΠ°ΡΠ½ΡΠΉ engine β orchestrator.syncPosition())
- Delete engine + base; remove old methods/tests referencing them. β Done (DELETED: engine + base + live-context 1624 lines + shadow-context 485 lines = 2109+ lines ΠΏΠ°ΡΠ°ΡΡ)
- Keep scheduler behavior; run it on
KAIROS.position-execution(same bus as execution). β Done (already using same queue)
BONUS CLEANUP STEPS - β COMPLETED!β
- Consolidate consumers: 6 separate consumers β 2 unified consumers per queue. β Done (PositionExecutionConsumer + DynamicTpslConsumer)
- Delete METRICS queue: move metrics to position execution events. β Done (metrics now trigger on position closures)
- Delete TransactionManager abstraction: direct account.updateBalance() calls. β Done (removed unnecessary wrapper layer)
- Delete OrderService: inline order creation + publishOrderFilledEvent pattern. β Done (500+ lines β direct EntityManager calls)
- Remove EventEmitter2 completely: full queue-based architecture. β Done (zero @OnEvent decorators remaining)
- Delete useless tests: trailing-sl-real.e2e-spec.ts didn't test our logic. β Done (cleaned up test noise)
π MIGRATION COMPLETE! π
Result: minute reconcile baseline + immediate sync on events, tiny surface, no 1500-line files, easy to extend.
Architecture Stats:
- DELETED: 3500+ lines ΠΏΠ°ΡΠ°ΡΠ½ΠΎΠ³ΠΎ Π³ΠΎΠ²Π½Π° (engine + base + contexts + services + consumers)
- CREATED: ~600 lines ΡΠΈΡΡΠΎΠΉ Π°ΡΡ ΠΈΡΠ΅ΠΊΡΡΡΡ (adapters + single consumer + queue services)
- NET: -2900 lines, +event-driven architecture, +testability
π₯ FINAL ARCHITECTURE CLEANUP:
- β KairosEventService (295 lines) β DELETED
- β KairosEventConsumer β DELETED
- β TransactionManagerService β DELETED (replaced with direct account.updateBalance() calls)
- β TransactionManagerConsumer β DELETED (balance updates moved to OrderFilledConsumer)
- β OrderService (500+ lines) β DELETED (replaced with inline code in shadow adapter)
- β OrderService.fillOrder() β publishOrderFilledEvent() + OrderFilledConsumer
- β 6 separate consumers β 2 unified consumers (PositionExecutionConsumer + DynamicTpslConsumer)
- β METRICS queue β DELETED (metrics moved to position execution events)
- β All @OnEvent decorators β Queue consumers
- β EventEmitter2 dependencies β REMOVED
- β Useless trailing-sl-real.e2e-spec.ts test β DELETED
Proposed test set (minimal, no legacy)β
-
Keep (rename if noted)
- derive-sync.spec.ts
- position-scheduler.service.spec.ts
- position-distribution.util.spec.ts
- trailing-stop-loss.utils.spec.ts
- pnl-calculator.service.spec.ts
- position-query.service.spec.ts (optional)
- tests/bullmq-patterns.spec.ts (optional)
-
Modify
- order.service.events.spec.ts β order.service.queue-events.spec.ts
- Assert QueueEventService publishes to KAIROS.position-execution; remove EventEmitter2.
- live-sync-positions.e2e-spec.ts
- Assert orchestrator + TradingAdapter flow; drop engine/base deps.
- position.e2e-spec.ts
- Align with orchestrator/adapter, queue names, and immediate sync on events.
- order.service.events.spec.ts β order.service.queue-events.spec.ts
-
Delete DONE
- position-sync.engine.spec.ts
- trading-context.spec.ts
- live-trading-context.spec.ts
- live-trading-context.e2e-spec.ts
- Any tests that depend on EventEmitter2-based KairosEventService
-
Add (new, small)
- live-context.adapter.reconcile.spec.ts
- Mocks Bybit adapter; verifies: TP diff/cancel/create, SL ensure/update, trailing SL path.
- shadow-context.adapter.simulate.spec.ts
- Verifies simulateTriggers leads to fills via OrderService.
- trading-context.orchestrator.spec.ts
- Given diagnostics phase, calls the correct adapter method; persists lastSyncUpdate.
- order-events.consumer.spec.ts
- On position_closed/partially_closed, enqueues command.sync_position for that position.
- queue-event.service.spec.ts
- Publishes event.* to KAIROS.position-execution with stable jobId and metadata.
- live-context.adapter.reconcile.spec.ts
This yields a lean suite: pure diagnostics, one unit per adapter, one orchestrator unit, one scheduler unit, one event-to-sync bridge, plus 1-2 e2e happy paths.