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.
The RiskManager (ot_module/risk_manager.py, ~2,040 lines) acts as the safety firewall for all trading operations. It operates between signal generation and order execution, evaluating both global account metrics and asset-specific performance windows. Every signal must pass through 11 sequential stages of validation before a single cent of capital is committed.
The Signal Assessment Pipeline
Whenever a strategy yields a trading signal, the TradingController dispatches it to the RiskManager's ssess_signal() method (lines 1284–1793) before executing any orders.
Method Signature
Sources:Returns a tuple of (approved: bool, quantity: float | None, initial_base_risk_usd: float, reject_reason: str | None).
Stage-by-Stage Breakdown
Stage 0 — Symbol Blacklist Pre-Check (lines 1297–1303)
Calls is_symbol_trading_allowed(signal.symbol) which performs an on-the-fly database lookup:
- Checks the global _is_trading_allowed flag (set by _check_risk_limits()).
- Fetches the user's AppConfig from PostgreSQL and iterates isk_management.blacklist.coins[].
- Supports permanent blacklist entries (until is None) and temporary entries (ISO-8601 until datetime).
- Telegram alerts are throttled to once per 10 minutes per symbol via _notify_blacklist() (lines 1027–1048).
Stage 1 — Balance & Risk Base (lines 1305–1357)
Depending on the trading mode:
| Mode | Executor | Risk Config Source |
|---|---|---|
| paper | self.paper_executor | paper_risk_per_trade, paper_max_stop_distance_pct |
| live | self.live_executor (or override) | live_risk_per_trade, live_max_stop_distance_pct |
In live mode, calls self.update_balance() (lines 570–634) which includes an anomaly detection safeguard — if the new balance drops >90% from the previous reading (and was previously > and now < .50), it treats this as a temporary API glitch and skips the update.
Stage 2 — Base Risk USD Calculation (lines 1362–1383)
Sources:The signal can override the default risk-per-trade percentage via signal.risk_pct, or specify a fixed USD amount via signal.risk_usd.
Stage 3 — Global Risk Flag (lines 1385–1389)
If self._is_trading_allowed is False (set by _check_risk_limits), the signal is immediately rejected with "GLOBAL_RISK_LIMIT".
Stage 4 — Balance Freshness (lines 1391–1395)
If the balance update failed, rejects with "BALANCE_UPDATE_FAILED".
Stage 5 — Dynamic Strategy/Symbol Multiplier (lines 1398–1433)
If strategy-symbol adjustment is enabled, the manager looks up SymbolStrategyPerformanceStats for the (symbol, strategy_name) pair and reads the current_risk_multiplier_index:
Sources:The multiplier list from config (STRATEGY_SYMBOL_RISK_MULTIPLIERS): [0.1, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0]
| Index | Multiplier | Meaning |
|---|---|---|
| 0 | 0.1 | 90% risk reduction (severe drawdown) |
| 2 | 0.5 | 50% risk reduction |
| 4 | 1.0 | Default (no adjustment) |
| 6 | 1.5 | 50% risk increase (hot streak) |
| 8 | 2.0 | Double risk (high confidence) |
Stage 6 — Max Position Nominal (lines 1435–1445)
Sources:Config search order: MAX_REAL_POSITION_SIZE_PCT_BALANCE → BACKTEST_MAX_POSITION_SIZE_PCT_BALANCE → .50 (default 50% of balance).
Stage 7 — Entry Price & SL Distance (lines 1447–1533)
Validates the entry price from signal.trigger_price ?? signal.entry_price. Then branches:
No-Stop-Loss mode (lines 1466–1488): If signal.stop_loss is None (DCA/Grid strategies), sizes as:
Sources:Standard SL mode (lines 1489–1548): Validates SL is on the correct side ("SL_WRONG_SIDE"), distance is non-zero ("ZERO_SL_DISTANCE"), distance ≤ max allowed ("SL_TOO_FAR"), and distance ≥ min allowed ("SL_TOO_CLOSE").
Sources:Stage 8 — Reward/Risk Check (lines 1550–1615)
Skipped if stop_loss is None (no-stop mode) or skip_min_rr_for_dca_grid is set. Derives TP from signal.take_profit, falling back to the farthest signal.partial_targets. Rejects with "LOW_RR" if the ratio is below min_rr_ratio (default: 1.0).
Stage 9 — Exchange Lot Filters (lines 1617–1737)
Applies exchange-specific precision rules:
- stepSize truncation: quantity = floor(quantity / stepSize) * stepSize (lines 1637–1643).
- maxQty clamp: Caps at exchange max (lines 1646–1657).
- minQty adjustment: If below minimum, attempts adjustment within risk/notional limits, else "MIN_QTY_VIOLATION" (lines 1659–1679).
- minNotional: Ensures order value ≥ exchange minimum (lines 1689–1730).
Stage 10 — Dollar R/R Check (lines 1739–1780)
Only if config.RISK_MANAGER_MIN_DOLLAR_RR_RATIO is set:
Sources:Rejects if below threshold with "LOW_DOLLAR_RR".
Stage 11 — Return (lines 1782–1793)
Returns (True, final_quantity, initial_base_risk_usd, None) for approval.
Global Account Limits (_check_risk_limits)
The _check_risk_limits() method (lines 1231–1282) performs synchronous checks against global account parameters:
| Check | Formula | Default Threshold | Effect |
|---|---|---|---|
| Minimum Balance | Balance < min_balance_threshold | Trading disabled | |
| Max Drawdown | bs(today_pnl / start_balance) >= max_drawdown_threshold | 20% | Trading disabled |
| Daily Max Loss | bs(today_pnl / start_balance) >= daily_max_loss_threshold | 5% | Trading disabled |
| Consecutive Losses | consecutive_losses >= max_consecutive_losses | 10 | Trading disabled |
Any failure triggers a Telegram alert via syncio.run_coroutine_threadsafe and sets self._is_trading_allowed = False. Recovery is automatic — if all checks pass on a subsequent call, trading is re-enabled.
Daily Stats Reset (lines 648–670)
Detects UTC day change at 0:01 UTC and resets start_of_day_balance, oday_pnl, consecutive_losses:
Sources:Automatic Blacklisting
To prevent a strategy from "revenge-trading" or blowing an account during anomalies, the RiskManager can dynamically restrict trading on specific pairs.
Auto-Blacklist Rules (_check_and_apply_auto_blacklist_rules, lines 1050–1118)
Loaded from isk_management.blacklist.autoRules[]. Each rule has:
| Field | Type | Description |
|---|---|---|
| enabled | ool | Whether the rule is active |
| consecutiveStops | int | Number of consecutive stop-loss hits to trigger |
| withinPeriod | str | Time window: "15m", "1h", "4h", or None (all-time) |
| duration | str | Blacklist duration: "1h", "4h", "8h", "end_of_day", or "permanent" |
Auto-Add to Blacklist (_auto_add_to_blacklist, lines 1120–1229)
Computes until_dt based on duration, reads current config from DB, checks if symbol already exists, creates/updates entry with symbol, until, eason, ddedAt, then persists via crud.update_config_section + db_session.commit().
Dynamic Risk Multipliers (Lot Sizing)
DepthSight does not rely solely on flat position sizing. The engine adapts risk based on the recent success of each strategy/symbol combination using a rolling window.
The Rolling Performance Window
SymbolStrategyPerformanceStats (dataclass, lines 35–49) tracks trade metrics over a configurable window (default: last 20 trades):
| Field | Description |
|---|---|
| rade_results_buffer | deque of (pnl_usd, initial_risk_usd) pairs |
| current_pnl_sum_usd | Sum of PnL in the current window |
| sum_initial_risk_usd_in_window | Total capital risked in the window |
| current_wins_in_window | Win count in the window |
| current_trades_in_window | Total trades in the window |
| current_consecutive_losses | Running consecutive loss counter |
| current_risk_multiplier_index | Index into the multipliers array (0-8) |
Performance Update Flow (update_symbol_strategy_performance, lines 710–771)
Called after each trade closes:
- Sliding window maintenance — if buffer is full, subtracts oldest entry from running sums.
- Appends new (pnl_usd, initial_risk_usd).
- Recalculates current_pnl_sum_usd, current_wins_in_window, current_trades_in_window.
- Counts consecutive losses/wins from the end of the buffer.
- Calls _check_and_adjust_risk_for_symbol_strategy().
Adjustment Logic (_check_and_adjust_risk_for_symbol_strategy, lines 773–911)
Risk Reduction — triggers when current_risk_multiplier_index > 0:
| Trigger | Condition |
|---|---|
| PnL threshold | pnl_pct_in_window < _strategy_symbol_pnl_thresh_pct (default: -10%) |
| Win rate threshold | win_rate_in_window < _strategy_symbol_wr_thresh_pct (default: 30%) |
| Consecutive losses | current_consecutive_losses >= _strategy_symbol_max_consec_loss (default: 5) |
Risk Improvement — only after cooldown period (default: 3600s):
- Recovery consecutive wins ≥ _strategy_symbol_rec_consec_wins (default: 3)
- OR PnL recovery: pnl_pct_in_window > _strategy_symbol_rec_pnl_thresh_pct (default: 50%)
DB Persistence
Performance data is serialized to the symbol_strategy_performance table:
- Load from DB: _load_performance_from_db() (lines 1795–1852) — deserializes JSON buffer, recalculates derivatives.
- Save to DB: _save_performance_to_db() (lines 1854–1891) — serializes buffer to JSON, stores index, timestamps, totals.
Position Sizing for Scale-In (calculate_scaled_in_quantity)
For scaling into existing positions (lines 1997–2036):
Sources:Passes through _adjust_and_round_quantity() (lines 1909–1995) which applies stepSize, minQty, maxQty, minNotional exchange filters.
Complete Rejection Reasons Reference
| Rejection Code | Stage | Description |
|---|---|---|
| SYMBOL_BLACKLISTED | 0 | Symbol is on manual or auto-blacklist |
| PAPER_BALANCE_FETCH_FAILED | 1 | Paper trading balance unavailable |
| ZERO_BALANCE | 1 | Account balance is zero |
| GLOBAL_RISK_LIMIT | 3 | Daily loss, drawdown, or consecutive loss limit hit |
| BALANCE_UPDATE_FAILED | 4 | Live balance fetch failed |
| ZERO_RISK | 5 | Dynamic multiplier reduced risk to zero |
| INVALID_PRICE | 7 | Entry price is zero or NaN |
| INVALID_SL | 7 | Stop loss is not a valid number |
| SL_WRONG_SIDE | 7 | SL is on the wrong side of entry (e.g., SL above entry for long) |
| ZERO_SL_DISTANCE | 7 | SL distance is zero |
| SL_TOO_FAR | 7 | SL exceeds maximum stop distance % |
| SL_TOO_CLOSE | 7 | SL below minimum stop distance |
| NO_TP_FOR_RR | 8 | No take profit defined for R/R check |
| TP_NOT_PROFITABLE | 8 | TP is on the wrong side of entry |
| ZERO_SL_FOR_RR | 8 | Zero SL distance for R/R calculation |
| LOW_RR | 8 | Reward/Risk ratio below minimum threshold |
| MIN_QTY_VIOLATION | 9 | Position quantity below exchange minimum |
| ZERO_QTY_FINAL | 9 | Final calculated quantity is zero |
| MIN_NOTIONAL_VIOLATION | 9 | Order value below exchange minimum notional |
| LOW_DOLLAR_RR | 10 | Dollar-based R/R below configured minimum |
Trading Controller Lifecycle
Detailed analysis of the initialization, state management, position lifecycle, event processing loop, and graceful shutdown inside the core TradingController.
Trade Mining Overview
How DepthSight nodes mint $DEPTH by submitting anonymous trade telemetry — node identity, scoring, mining epochs and reward distribution.