How the Signal Router Combines Multiple Data Sources into Edge Signals for Trading
The Signal Router aggregates predictions from the TradingSignal bus, enriches them with real-time market features and optional ML confidence scoring, then validates and executes orders through a serial pipeline that enforces risk controls like cooldowns and daily loss limits.
The signal router serves as the central intelligence hub in the alsk1992/CloddsBot repository, transforming raw market predictions into executable edge signals for the trading layer. By fusing data from prediction models, live order books, machine learning classifiers, and dynamic risk configurations, it ensures only validated, properly-sized orders reach the execution service.
Architecture of the Signal Router
At its core, the router is implemented in src/signal-router/router.ts and exposes an EventEmitter-based API that includes start, stop, and getStats methods. The createSignalRouter factory function receives dependencies including the ExecutionService, SignalRouterConfig, optional SmartRouter, and optional MLSignalModel. This dependency injection pattern allows the router to remain stateless while coordinating between disparate data sources.
The Nine-Stage Data Integration Pipeline
The router processes every incoming signal through a strict nine-stage pipeline, pulling from different data sources at each step to build a complete execution context.
1. Signal Intake from the Trading Bus
The router registers a listener on the SignalBus via signalBus.onSignal upon initialization. When prediction models or market-making bots publish signals, the router immediately enqueues them in the signalQueue for serial processing. This intake mechanism ensures that high-frequency signals do not overwhelm the execution layer.
2. Real-Time Feature Enrichment
For each dequeued signal, the router calls getMarketFeatures(platform, marketId, outcomeId) from the feature-engineering service (src/services/feature-engineering/index.ts). This function fetches the latest order-book depth, liquidity scores, spread percentages, and tick data. The resulting metrics populate a features object that travels with the signal through subsequent stages.
3. ML Confidence Modulation
When an mlModel is supplied, the router converts the raw feature set into a training-compatible structure using combinedToMarketFeatures(features) from src/ml-pipeline/trainer.js. It then queries the model via mlModel.predict. The prediction result performs dual duty: it can reject signals where ML confidence strongly disagrees with the raw signal, and it rescales signal strength using the formula signal.strength *= (0.5 + 0.5 * mlSignal.confidence). This modulation turns raw predictions into weighted edge signals based on historical model accuracy.
4. Smart Router Integration
If cfg.useSmartRouter is enabled, the prepared order request is handed to the SmartRouter for additional aggregation or routing logic before reaching the execution service. This layer allows for complex order splitting across multiple venues or liquidity pools.
5. Risk Validation and Filtering
The shouldRoute function validates the enriched signal against guardrails defined in SignalRouterConfig. It checks signal type filters, neutral direction constraints, minimum strength thresholds, platform whitelists, market exclusion lists, per-market cooldowns (marketCooldowns), daily P&L limits (maxDailyLoss), and maximum concurrent position counts. Signals failing any check are logged as rejected with a specific skip reason recorded for audit trails.
6. Price Selection from Market Data
Using the order-book data retrieved in stage two, the router selects the best ask for buy orders and the best bid for sell orders. If only tick data is available, it falls back to the tick price. Signals lacking usable price data are rejected at this stage to prevent malformed orders.
7. Dynamic Position Sizing
The final order size is computed as defaultSizeUsd * signal.strength when strengthScaling is enabled in configuration. The result is capped to maxSizeUsd and rounded to a minimum of $1. This ensures position sizes remain proportional to signal confidence while respecting account risk limits.
8. Order Execution
For live trades (when dryRun is false), the router constructs an orderRequest and calls the appropriate method on the injected ExecutionService—such as execution.makerBuy or execution.marketSell. The order mode (maker, market, or limit) determines the specific execution path. Successful executions emit an executed event containing the orderId and market details; failures emit a failed event with error context.
9. Post-Execution State Management
After successful execution, the router places the market key on a cooldown map using marketCooldowns.set to prevent immediate re-entry. It updates the openPositions tracking map, increments daily statistics, and appends the execution record to recentExecutions. This housekeeping ensures that subsequent signals respect the updated portfolio state.
Serial Queue Processing
Signals are processed one-by-one via the drainQueue mechanism to avoid concurrent fan-out. This serial approach guarantees that each market’s cooldown timers and position limits are evaluated against the most current state, preventing race conditions that could lead to over-leveraging or duplicate orders.
Configuration and Runtime Control
The router supports runtime reconfiguration through the updateConfig method, allowing operators to toggle dryRun mode, adjust strength thresholds, enable or disable the smart router, or modify size limits without restarting the bot. When dryRun is active, the router emits dry_run events instead of calling execution methods, logging how the signal would have been executed—essential for back-testing and safe deployment.
Practical Implementation Example
// Create a router instance (in gateway/index.ts)
import { createSignalRouter } from './signal-router';
const router = createSignalRouter(
executionService, // ExecutionService instance
routerConfig, // SignalRouterConfig from config file
smartRouter, // Optional SmartRouter
mlModel, // Optional MLSignalModel
);
// Start the router with the signal bus (provided by the ML pipeline)
router.start(signalBus);
// Listen for execution events (useful for logging or UI updates)
router.on('executed', (exec) => {
console.log(`Order ${exec.orderId} executed for ${exec.signal.marketId}`);
});
router.on('dry_run', (exec) => {
console.log(`Dry-run: would have placed ${exec.orderSize} USD @ ${exec.orderPrice}`);
});
Summary
- The signal router in
src/signal-router/router.tsorchestrates nine distinct stages to combine prediction signals, market features, and ML insights into executable trades. - It enforces strict risk controls through the
shouldRoutevalidator, checking cooldowns, daily loss limits, and position counts before execution. - ML confidence modulates signal strength dynamically, allowing the router to scale position size based on model agreement.
- Serial queue processing via
drainQueueprevents concurrent execution errors and ensures accurate state management. - Runtime configuration updates and dry-run mode enable safe testing and operational flexibility without system restarts.
Frequently Asked Questions
How does the signal router handle conflicting signals from multiple data sources?
The router resolves conflicts through sequential validation and ML modulation. Raw signals from the TradingSignal bus are enriched with market features, then optionally scored by an ML model. If the ML prediction strongly disagrees with the raw signal direction, the router rejects the trade. This hierarchy ensures that algorithmic predictions are weighted against statistical model confidence before reaching the execution layer.
What happens when the ML model confidence is low?
When mlModel.predict returns low confidence, the router applies the rescaling formula signal.strength *= (0.5 + 0.5 * mlSignal.confidence), which reduces the position size proportionally. In extreme cases where confidence indicates a likely false positive, the signal may be rejected entirely during the validation stage, preventing low-probability trades from reaching the market.
Can the router operate in simulation mode before live trading?
Yes. The router defaults to dryRun mode, where it calculates order prices and sizes but emits dry_run events instead of calling ExecutionService methods. This mode allows strategy validation against historical or real-time data without capital risk. Operators can toggle live trading via updateConfig or by modifying the initial SignalRouterConfig passed to createSignalRouter.
Which file contains the core routing logic?
The primary implementation resides in src/signal-router/router.ts, which contains the createSignalRouter factory and the drainQueue processing loop. Type definitions for configurations and execution events are located in src/signal-router/types.ts, while the feature enrichment logic used by the router is implemented in src/services/feature-engineering/index.ts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →