Trading Engine

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.

⏱️ 10 min read📊 Level: Advanced

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.

Rendering diagram...

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:

ModeExecutorRisk Config Source
paperself.paper_executorpaper_risk_per_trade, paper_max_stop_distance_pct
liveself.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]

IndexMultiplierMeaning
00.190% risk reduction (severe drawdown)
20.550% risk reduction
41.0Default (no adjustment)
61.550% risk increase (hot streak)
82.0Double 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:

  1. stepSize truncation: quantity = floor(quantity / stepSize) * stepSize (lines 1637–1643).
  2. maxQty clamp: Caps at exchange max (lines 1646–1657).
  3. minQty adjustment: If below minimum, attempts adjustment within risk/notional limits, else "MIN_QTY_VIOLATION" (lines 1659–1679).
  4. 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:

CheckFormulaDefault ThresholdEffect
Minimum BalanceBalance < min_balance_thresholdTrading disabled
Max Drawdownbs(today_pnl / start_balance) >= max_drawdown_threshold20%Trading disabled
Daily Max Lossbs(today_pnl / start_balance) >= daily_max_loss_threshold5%Trading disabled
Consecutive Lossesconsecutive_losses >= max_consecutive_losses10Trading 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:

FieldTypeDescription
enabledoolWhether the rule is active
consecutiveStopsintNumber of consecutive stop-loss hits to trigger
withinPeriodstrTime window: "15m", "1h", "4h", or None (all-time)
durationstrBlacklist duration: "1h", "4h", "8h", "end_of_day", or "permanent"
Sources:

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):

FieldDescription
rade_results_bufferdeque of (pnl_usd, initial_risk_usd) pairs
current_pnl_sum_usdSum of PnL in the current window
sum_initial_risk_usd_in_windowTotal capital risked in the window
current_wins_in_windowWin count in the window
current_trades_in_windowTotal trades in the window
current_consecutive_lossesRunning consecutive loss counter
current_risk_multiplier_indexIndex into the multipliers array (0-8)

Performance Update Flow (update_symbol_strategy_performance, lines 710–771)

Called after each trade closes:

  1. Sliding window maintenance — if buffer is full, subtracts oldest entry from running sums.
  2. Appends new (pnl_usd, initial_risk_usd).
  3. Recalculates current_pnl_sum_usd, current_wins_in_window, current_trades_in_window.
  4. Counts consecutive losses/wins from the end of the buffer.
  5. 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:

TriggerCondition
PnL thresholdpnl_pct_in_window < _strategy_symbol_pnl_thresh_pct (default: -10%)
Win rate thresholdwin_rate_in_window < _strategy_symbol_wr_thresh_pct (default: 30%)
Consecutive lossescurrent_consecutive_losses >= _strategy_symbol_max_consec_loss (default: 5)
Sources:

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%)
Sources:

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 CodeStageDescription
SYMBOL_BLACKLISTED0Symbol is on manual or auto-blacklist
PAPER_BALANCE_FETCH_FAILED1Paper trading balance unavailable
ZERO_BALANCE1Account balance is zero
GLOBAL_RISK_LIMIT3Daily loss, drawdown, or consecutive loss limit hit
BALANCE_UPDATE_FAILED4Live balance fetch failed
ZERO_RISK5Dynamic multiplier reduced risk to zero
INVALID_PRICE7Entry price is zero or NaN
INVALID_SL7Stop loss is not a valid number
SL_WRONG_SIDE7SL is on the wrong side of entry (e.g., SL above entry for long)
ZERO_SL_DISTANCE7SL distance is zero
SL_TOO_FAR7SL exceeds maximum stop distance %
SL_TOO_CLOSE7SL below minimum stop distance
NO_TP_FOR_RR8No take profit defined for R/R check
TP_NOT_PROFITABLE8TP is on the wrong side of entry
ZERO_SL_FOR_RR8Zero SL distance for R/R calculation
LOW_RR8Reward/Risk ratio below minimum threshold
MIN_QTY_VIOLATION9Position quantity below exchange minimum
ZERO_QTY_FINAL9Final calculated quantity is zero
MIN_NOTIONAL_VIOLATION9Order value below exchange minimum notional
LOW_DOLLAR_RR10Dollar-based R/R below configured minimum