Trading Controller Lifecycle
Detailed analysis of the initialization, state management, position lifecycle, event processing loop, and graceful shutdown inside the core TradingController.
The TradingController (ot_module/controller.py, ~13,800 LOC) is the central nervous system of the DepthSight bot execution engine. It orchestrates incoming market data, runs strategy instances, validates every signal through the RiskManager, and dispatches orders to exchange executors — all within a single asynchronous event loop.
Critical Structural Components
When initialized, the constructor (init, lines 440–530) establishes these critical structures to coordinate asynchronous processing and ensure thread safety across hundreds of concurrently running strategies:
| Component | Type | Purpose |
|---|---|---|
| event_queue | syncio.Queue(maxsize=1000) | Central bounded event bus for incoming market data and order updates |
| _event_handler_semaphore | syncio.Semaphore(64) | Limits concurrent event handlers to prevent thread exhaustion |
| _positions_dict_lock | syncio.Lock | Protects structural integrity of the ActivePositionMap dictionary |
| _symbol_locks | Dict[str, asyncio.Lock] | Per-symbol locks ensuring sequential order processing on the same coin |
| _active_positions | ActivePositionMap | Thread-safe, market-aware runtime storage for active positions |
| running_strategy_instances | Dict[str, Tuple[BaseStrategy, dict]] | Maps running strategy config_id to its strategy instance and runtime config |
| executors | Dict[str, Executor] | References to "live" (BinanceExecutor) and "paper" (PaperTradingExecutor) |
| redis_client | redis.asyncio.Redis | Client for streaming state changes, runtime variables, and logs |
ActivePositionMap — Multi-Market Position Storage
The ActivePositionMap is a custom dict subclass that stores active positions using composite keys of the form market_type:SYMBOL (e.g., utures_usdtm:BTCUSDT). It preserves backward compatibility by allowing symbol-only queries when the lookup is unambiguous, and falls back to matching logic when multiple market types share the same ticker symbol. This design is critical for multi-market support (linear futures, spot, coin-margined) on the same asset.
Startup Orchestration ( start() )
The start() method (lines 975–1090) executes a carefully ordered sequence of startup tasks that must complete before any trading activity can occur:
Step 1 — Configuration Loading (lines 980–1020)
Fetches the user's AppConfig from PostgreSQL and applies runtime settings:
Sources:Key operations:
- Risk Limits: Applies max_concurrent_trades, daily_loss_limit, leverage constraints to the RiskManager.
- Notifications: Loads custom Telegram chat IDs for real-time trade and alert delivery.
- Blacklist: Syncs the static blacklist from the database into the controller's runtime memory.
Step 2 — Symbol Selection Config (lines 1022–1040)
Loads the SymbolSelectionConfig which dictates the bot's market scope:
| Mode | Behavior | Use Case |
|---|---|---|
| STATIC | Trades only a fixed whitelist of symbols from strategy config | Conservative, manually curated portfolios |
| DYNAMIC | Selects symbols based on NATR (Normalized ATR) volatility ranking | Adaptive market making across top movers |
| ORACLE | AI-driven symbol selection using ML Oracle regime parameters | Experimental / high-alpha strategies |
The controller iterates through selected symbols and initializes a DataConsumer subscription for each, ensuring market data flows before strategies begin evaluation.
Step 3 — Runtime State Recovery (lines 1042–1060)
After a server restart or process migration, the controller must reconnect to its prior state without losing track of open trades:
Sources:Redis keys used for persistence:
depthsight:state:positions:{user_id}— serialized list of active positionsdepthsight:state:portfolio:{user_id}— portfolio-level stats (daily PnL, balance)depthsight:state:strategies:{user_id}— running strategy instances and their params
Step 4 — Exchange Reconciliation (lines 1062–1090)
Automatically cross-references local DB records with actual exchange positions via CCXTExecutor.fetch_positions(). This critical step handles:
- Orphaned Positions: If the exchange reports an open position that the controller has no record of (e.g., due to a prior crash), it imports the position with a "RECOVERED" flag.
- Stale Entries: If the controller has a position in _active_positions but the exchange reports it as closed, it marks the trade completed and writes the closing record to the database.
- Quantity Discrepancies: If partial fills occurred during downtime, the controller adjusts internal quantity tracking to match exchange reality.
Step 5 — Data Consumer Registration (lines 1092–1110)
The controller registers event listeners with the DataConsumer. For each active symbol:
- Candle close events (type: "CANDLE_CLOSE") — triggers strategy evaluation on each new closed candle.
- Tick events (type: "TICK") — used for intra-candle stop-loss monitoring and tape-reading strategies.
- Order book snapshots — for market-impact-aware execution and paper-trading fill simulation.
Event Processing Loop (_event_loop())
Once started, the controller enters its main asynchronous event loop (lines 1120–1250). This is the heart of all trading activity:
Event Types and Dispatch Logic
| Event Type | Source | Action |
|---|---|---|
| CANDLE_CLOSE | DataConsumer — kline stream | Evaluates strategy entry conditions, triggers signal generation |
| TICK | DataConsumer — aggTrade stream | Updates stop-loss/take-profit tracking, trailing stops |
| ORDER_UPDATE | Exchange user data stream | Matches fill confirmation, updates position state |
| SIGNAL | Strategy instance (internal) | Dispatches to RiskManager for assessment and execution |
| MANUAL_EXIT | REST API / user command | Liquidates specified position |
| REGIME_CHANGE | Oracle ML engine | Triggers risk re-evaluation, possible mass liquidation |
Concurrency Model
The controller uses a triple-lock strategy to prevent race conditions:
- _positions_dict_lock (asyncio.Lock): Guards structural mutations to _active_positions dictionary (adding/removing positions).
- _symbol_locks[symbol] (asyncio.Lock): Per-symbol ordering guarantee. Ensures events for BTCUSDT are processed sequentially, preventing duplicate orders or conflicting state updates on the same asset.
- _event_handler_semaphore (Semaphore 64): Global throttle capping concurrent event handler tasks, preventing memory exhaustion during volatile market conditions when thousands of events arrive per second.
Position Lifecycle (Open — Manage — Close)
Each position traverses a well-defined lifecycle within the controller:
1. Position Entry (_open_position(), lines ~2500–2650)
When a signal passes the RiskManager assessment:
Sources:2. Position Management (_manage_positions(), lines ~3200–3400)
On each CANDLE_CLOSE or TICK event, the controller iterates active positions:
- Stop Loss Check: if low
<=position.stop_loss (longs) or if high>=position.stop_loss (shorts). - Take Profit Check: Same logic applied with TP threshold.
- Partial Targets: Each configured level is checked independently; on hit, (target_qty / total_qty) * 100% of the position is closed.
- Trailing Stop: SL ratchets up (long) or down (short) by configurable % of MFE (Maximum Favorable Excursion).
- Breakeven: After price moves reakeven_activation_pct favorably, SL moves to entry + buffer.
- DCA / Grid: Unfilled grid orders are monitored and filled as price steps through predefined levels.
3. Position Exit (_close_position(), lines ~3800–4000)
Sources:Order Submission Flow (Signal to Order)
The path from strategy signal to live order traverses four distinct layers:
RiskManager Gate
The ssess_signal() call (ot_module/risk_manager.py, lines 1284–1793) performs 11 sequential stages of validation before any capital is committed:
| Stage | Check | Rejection Code |
|---|---|---|
| 0 | Symbol blacklist | SYMBOL_BLACKLISTED |
| 1 | Balance fetch | ZERO_BALANCE |
| 2 | Risk limit check (daily loss, max trades) | GLOBAL_RISK_LIMIT |
| 3 | Dynamic strategy/symbol multiplier | ZERO_RISK |
| 4 | Entry price validation | INVALID_PRICE |
| 5 | Stop-loss validation | SL_WRONG_SIDE, ZERO_SL_DISTANCE |
| 6 | Reward/Risk ratio | LOW_RR |
| 7 | Exchange lot filters (stepSize, minQty) | MIN_QTY_VIOLATION |
| 8 | Min notional check | MIN_NOTIONAL_VIOLATION |
| 9 | Dollar-based R/R check | LOW_DOLLAR_RR |
Exchange Dispatch
Once approved, the controller selects the correct executor and calls place_order(). The CCXTExecutor.place_order() method (ot_module/exchanges/ccxt_executor.py, lines 605–889) handles:
- Symbol normalization: BTCUSDT to BTC/USDT:USDT (futures) or BTC/USDT (spot).
- Order type mapping: Binance STOP_MARKET to CCXT "market" with stopLossPrice param.
- Exchange-specific parameters: OKX uses posSide, Gate.io uses settle: "usdt", Bitget requires hedged: True.
- Rate-limit compliance: Delegated to CCXT's built-in enableRateLimit token bucket.
State Persistence & Redis Publishing
The controller continuously publishes its runtime state to Redis, enabling:
- Real-Time Frontend Updates: The WebSocket server subscribes to
depthsight:events:positions:{user_id}and forwards updates to the browser dashboard. - Crash Recovery: A new controller instance can reconstruct its active position map from Redis snapshots.
- Multi-Process Coordination: The ot_runner.py daemon monitors Redis state keys to decide when to spawn or terminate controller processes.
Shutdown & Cleanup Sequence (stop())
When a stop command is received (via Redis command bus or API), the stop() method (lines ~1400–1550) executes a graceful teardown:
Shutdown Steps
- _running = False: The event loop terminates on its next iteration.
- Queue Drain: Pending events in event_queue are consumed but discarded.
- Position Liquidation (configurable): If liquidate_on_stop=True, sends market orders to close all active positions. Otherwise, positions are left open and their state preserved in Redis.
- Order Cancellation: Calls Executor.cancel_all_open_orders() for pending orders.
- Data Consumer Unsubscription: Removes symbol subscriptions to stop data flow.
- State Finalization: Writes final trade records to PostgreSQL and publishes a terminal state snapshot to Redis.
Error Handling & Recovery
The controller implements multiple layers of error resilience:
Exchange API Failures
When place_order() raises an exception (network error, rate limit, exchange maintenance):
Sources:Position Reconciliation Timer
A background task (lines ~1600–1650) runs every config.POSITION_RECONCILE_INTERVAL (default: 300 seconds) and:
- Fetches open positions from the exchange via CCXTExecutor.get_open_positions().
- Cross-references with _active_positions.
- Repairs any discrepancies — missing positions, quantity mismatches, stale entries.
Graceful Degradation
If Redis becomes unavailable, the controller continues running in a degraded mode:
- State persistence is disabled (no snapshots).
- Trading operations continue using in-memory state only.
- When Redis recovers, a full state sync is triggered automatically.
Strategy and Signal System
Core technical engine for condition evaluations, visual strategy compiling, indicators calculation, and orderbook stop-loss adaptation inside DepthSight.
Dynamic Risk Management
Comprehensive analysis of the 11-stage signal assessment pipeline, automatic blacklisting, dynamic trade size scaling, and portfolio-level risk limits inside the DepthSight RiskManager.