mercury-architecture-snapshot
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.
