/** * Risk Management Layer * * Comprehensive risk controls for trading systems: * - Position limits (per-asset and portfolio) * - Stop-loss orders (fixed, trailing, volatility-based) * - Circuit breakers (drawdown, loss rate, volatility) * - Exposure management * - Correlation risk * - Leverage control */ // Risk Management Configuration const riskConfig = { // Position limits positions: { maxPositionSize: 0.10, // Max 10% per position maxPositionValue: 50000, // Max $50k per position minPositionSize: 0.01, // Min 1% position maxOpenPositions: 20, // Max concurrent positions maxSectorExposure: 0.30, // Max 30% per sector maxCorrelatedExposure: 0.40 // Max 40% in correlated assets }, // Portfolio limits portfolio: { maxLongExposure: 1.0, // Max 100% long maxShortExposure: 0.5, // Max 50% short maxGrossExposure: 1.5, // Max 150% gross maxNetExposure: 1.0, // Max 100% net maxLeverage: 2.0, // Max 2x leverage minCashReserve: 0.05 // Keep 5% cash }, // Stop-loss settings stopLoss: { defaultType: 'trailing', // fixed, trailing, volatility fixedPercent: 0.05, // 5% fixed stop trailingPercent: 0.03, // 3% trailing stop volatilityMultiplier: 2.0, // 2x ATR for vol stop maxLossPerTrade: 0.02, // Max 2% loss per trade maxDailyLoss: 0.05 // Max 5% daily loss }, // Circuit breakers circuitBreakers: { drawdownThreshold: 0.10, // 10% drawdown triggers drawdownCooldown: 86400000, // 24h cooldown lossRateThreshold: 0.70, // 70% loss rate in window lossRateWindow: 20, // 20 trade window volatilityThreshold: 0.04, // 4% daily vol threshold volatilityMultiplier: 3.0, // 3x normal vol consecutiveLosses: 5 // 5 consecutive losses }, // Risk scoring scoring: { updateFrequency: 60000, // Update every minute historyWindow: 252, // 1 year of daily data correlationThreshold: 0.7 // High correlation threshold } }; /** * Stop-Loss Manager */ class StopLossManager { constructor(config = riskConfig.stopLoss) { this.config = config; this.stops = new Map(); // symbol -> stop config this.volatility = new Map(); // symbol -> ATR } // Set stop-loss for a position setStop(symbol, entryPrice, type = null, params = {}) { const stopType = type || this.config.defaultType; let stopPrice; switch (stopType) { case 'fixed': stopPrice = entryPrice * (1 - this.config.fixedPercent); break; case 'trailing': stopPrice = entryPrice * (1 - this.config.trailingPercent); break; case 'volatility': const atr = this.volatility.get(symbol) || entryPrice * 0.02; stopPrice = entryPrice - (atr * this.config.volatilityMultiplier); break; default: stopPrice = entryPrice * (1 - this.config.fixedPercent); } this.stops.set(symbol, { type: stopType, entryPrice, stopPrice, highWaterMark: entryPrice, params, createdAt: Date.now() }); return this.stops.get(symbol); } // Update trailing stop with new price updateTrailingStop(symbol, currentPrice) { const stop = this.stops.get(symbol); if (!stop || stop.type !== 'trailing') return null; if (currentPrice > stop.highWaterMark) { stop.highWaterMark = currentPrice; stop.stopPrice = currentPrice * (1 - this.config.trailingPercent); } return stop; } // Check if stop is triggered checkStop(symbol, currentPrice) { const stop = this.stops.get(symbol); if (!stop) return { triggered: false }; // Update trailing stop first if (stop.type === 'trailing') { this.updateTrailingStop(symbol, currentPrice); } const triggered = currentPrice <= stop.stopPrice; return { triggered, stopPrice: stop.stopPrice, currentPrice, loss: triggered ? (stop.entryPrice - currentPrice) / stop.entryPrice : 0, type: stop.type }; } // Set volatility for volatility-based stops setVolatility(symbol, atr) { this.volatility.set(symbol, atr); } // Remove stop removeStop(symbol) { this.stops.delete(symbol); } // Get all active stops getActiveStops() { return Object.fromEntries(this.stops); } } /** * Circuit Breaker System */ class CircuitBreaker { constructor(config = riskConfig.circuitBreakers) { this.config = config; this.state = { isTripped: false, tripReason: null, tripTime: null, cooldownUntil: null }; // Tracking data this.peakEquity = 0; this.currentEquity = 0; this.consecutiveLosses = 0; // Optimized: Use ring buffers instead of arrays with shift/slice const tradeWindowSize = config.lossRateWindow * 2; this._tradeBuffer = new Array(tradeWindowSize); this._tradeIndex = 0; this._tradeCount = 0; this._tradeLossCount = 0; // Track losses incrementally this._volBuffer = new Array(20); this._volIndex = 0; this._volCount = 0; this._volSum = 0; // Running sum for O(1) average } // Update with new equity value updateEquity(equity) { this.currentEquity = equity; if (equity > this.peakEquity) { this.peakEquity = equity; } // Check drawdown breaker const drawdown = (this.peakEquity - equity) / this.peakEquity; if (drawdown >= this.config.drawdownThreshold) { this.trip('drawdown', `Drawdown ${(drawdown * 100).toFixed(1)}% exceeds threshold`); } } // Optimized: Record trade with O(1) ring buffer recordTrade(profit) { const bufferSize = this._tradeBuffer.length; const windowSize = this.config.lossRateWindow; // If overwriting an old trade, adjust loss count if (this._tradeCount >= bufferSize) { const oldTrade = this._tradeBuffer[this._tradeIndex]; if (oldTrade && oldTrade.profit < 0) { this._tradeLossCount--; } } // Add new trade this._tradeBuffer[this._tradeIndex] = { profit, timestamp: Date.now() }; if (profit < 0) this._tradeLossCount++; this._tradeIndex = (this._tradeIndex + 1) % bufferSize; if (this._tradeCount < bufferSize) this._tradeCount++; // Update consecutive losses if (profit < 0) { this.consecutiveLosses++; } else { this.consecutiveLosses = 0; } // Check loss rate breaker (O(1) using tracked count) if (this._tradeCount >= windowSize) { // Count losses in recent window let recentLosses = 0; const startIdx = (this._tradeIndex - windowSize + bufferSize) % bufferSize; for (let i = 0; i < windowSize; i++) { const idx = (startIdx + i) % bufferSize; if (this._tradeBuffer[idx] && this._tradeBuffer[idx].profit < 0) { recentLosses++; } } const lossRate = recentLosses / windowSize; if (lossRate >= this.config.lossRateThreshold) { this.trip('lossRate', `Loss rate ${(lossRate * 100).toFixed(1)}% exceeds threshold`); } } // Check consecutive losses breaker if (this.consecutiveLosses >= this.config.consecutiveLosses) { this.trip('consecutiveLosses', `${this.consecutiveLosses} consecutive losses`); } } // Optimized: Update volatility with O(1) ring buffer and running sum updateVolatility(dailyReturn) { const absReturn = Math.abs(dailyReturn); const bufferSize = this._volBuffer.length; // If overwriting old value, subtract from running sum if (this._volCount >= bufferSize) { this._volSum -= this._volBuffer[this._volIndex]; } // Add new value this._volBuffer[this._volIndex] = absReturn; this._volSum += absReturn; this._volIndex = (this._volIndex + 1) % bufferSize; if (this._volCount < bufferSize) this._volCount++; // Check volatility spike (O(1) using running sum) if (this._volCount >= 5) { const avgVol = (this._volSum - absReturn) / (this._volCount - 1); const currentVol = absReturn; if (currentVol > avgVol * this.config.volatilityMultiplier || currentVol > this.config.volatilityThreshold) { this.trip('volatility', `Volatility spike: ${(currentVol * 100).toFixed(2)}%`); } } } // Trip the circuit breaker trip(reason, message) { if (this.state.isTripped) return; // Already tripped this.state = { isTripped: true, tripReason: reason, tripMessage: message, tripTime: Date.now(), cooldownUntil: Date.now() + this.config.drawdownCooldown }; console.warn(`🔴 CIRCUIT BREAKER TRIPPED: ${message}`); } // Check if trading is allowed canTrade() { if (!this.state.isTripped) return { allowed: true }; // Check if cooldown has passed if (Date.now() >= this.state.cooldownUntil) { this.reset(); return { allowed: true }; } return { allowed: false, reason: this.state.tripReason, message: this.state.tripMessage, cooldownRemaining: this.state.cooldownUntil - Date.now() }; } // Reset circuit breaker reset() { this.state = { isTripped: false, tripReason: null, tripTime: null, cooldownUntil: null }; this.consecutiveLosses = 0; console.log('🟢 Circuit breaker reset'); } // Force reset (manual override) forceReset() { this.reset(); this.peakEquity = this.currentEquity; // Reset ring buffers this._tradeIndex = 0; this._tradeCount = 0; this._tradeLossCount = 0; this._volIndex = 0; this._volCount = 0; this._volSum = 0; } getState() { return { ...this.state, drawdown: this.peakEquity > 0 ? (this.peakEquity - this.currentEquity) / this.peakEquity : 0, consecutiveLosses: this.consecutiveLosses, recentLossRate: this.calculateRecentLossRate() }; } // Optimized: O(windowSize) but only called for reporting calculateRecentLossRate() { const windowSize = this.config.lossRateWindow; const count = Math.min(this._tradeCount, windowSize); if (count === 0) return 0; let losses = 0; const bufferSize = this._tradeBuffer.length; const startIdx = (this._tradeIndex - count + bufferSize) % bufferSize; for (let i = 0; i < count; i++) { const idx = (startIdx + i) % bufferSize; if (this._tradeBuffer[idx] && this._tradeBuffer[idx].profit < 0) { losses++; } } return losses / count; } } /** * Position Limit Manager */ class PositionLimitManager { constructor(config = riskConfig.positions) { this.config = config; this.positions = new Map(); this.sectors = new Map(); // symbol -> sector mapping } // Set sector for a symbol setSector(symbol, sector) { this.sectors.set(symbol, sector); } // Check if position size is allowed checkPositionSize(symbol, proposedSize, portfolioValue) { const sizePercent = proposedSize / portfolioValue; const violations = []; // Check max position size if (sizePercent > this.config.maxPositionSize) { violations.push({ type: 'maxPositionSize', message: `Position ${(sizePercent * 100).toFixed(1)}% exceeds max ${(this.config.maxPositionSize * 100)}%`, limit: this.config.maxPositionSize * portfolioValue }); } // Check max position value if (proposedSize > this.config.maxPositionValue) { violations.push({ type: 'maxPositionValue', message: `Position $${proposedSize.toFixed(0)} exceeds max $${this.config.maxPositionValue}`, limit: this.config.maxPositionValue }); } // Check min position size if (sizePercent < this.config.minPositionSize && proposedSize > 0) { violations.push({ type: 'minPositionSize', message: `Position ${(sizePercent * 100).toFixed(1)}% below min ${(this.config.minPositionSize * 100)}%`, limit: this.config.minPositionSize * portfolioValue }); } return { allowed: violations.length === 0, violations, adjustedSize: this.adjustPositionSize(proposedSize, portfolioValue) }; } // Adjust position size to comply with limits adjustPositionSize(proposedSize, portfolioValue) { let adjusted = proposedSize; // Apply max position size const maxByPercent = portfolioValue * this.config.maxPositionSize; adjusted = Math.min(adjusted, maxByPercent); // Apply max position value adjusted = Math.min(adjusted, this.config.maxPositionValue); return adjusted; } // Check sector exposure checkSectorExposure(symbol, proposedSize, currentPositions, portfolioValue) { const sector = this.sectors.get(symbol); if (!sector) return { allowed: true }; // Calculate current sector exposure let sectorExposure = 0; for (const [sym, pos] of Object.entries(currentPositions)) { if (this.sectors.get(sym) === sector) { sectorExposure += Math.abs(pos.value || 0); } } const totalSectorExposure = (sectorExposure + proposedSize) / portfolioValue; if (totalSectorExposure > this.config.maxSectorExposure) { return { allowed: false, message: `Sector ${sector} exposure ${(totalSectorExposure * 100).toFixed(1)}% exceeds max ${(this.config.maxSectorExposure * 100)}%`, currentExposure: sectorExposure, maxAllowed: this.config.maxSectorExposure * portfolioValue - sectorExposure }; } return { allowed: true, sectorExposure: totalSectorExposure }; } // Check number of open positions checkPositionCount(currentPositions) { const count = Object.keys(currentPositions).filter(s => currentPositions[s].quantity !== 0).length; if (count >= this.config.maxOpenPositions) { return { allowed: false, message: `Max open positions (${this.config.maxOpenPositions}) reached`, currentCount: count }; } return { allowed: true, currentCount: count }; } } /** * Exposure Manager */ class ExposureManager { constructor(config = riskConfig.portfolio) { this.config = config; } // Calculate portfolio exposure calculateExposure(positions, portfolioValue) { let longExposure = 0; let shortExposure = 0; for (const pos of Object.values(positions)) { const value = pos.value || (pos.quantity * pos.price) || 0; if (value > 0) { longExposure += value; } else { shortExposure += Math.abs(value); } } const grossExposure = longExposure + shortExposure; const netExposure = longExposure - shortExposure; return { long: longExposure / portfolioValue, short: shortExposure / portfolioValue, gross: grossExposure / portfolioValue, net: netExposure / portfolioValue, leverage: grossExposure / portfolioValue, longValue: longExposure, shortValue: shortExposure }; } // Check if trade would violate exposure limits checkExposure(proposedTrade, currentPositions, portfolioValue) { // Simulate new exposure const newPositions = { ...currentPositions }; const symbol = proposedTrade.symbol; const value = proposedTrade.value || (proposedTrade.quantity * proposedTrade.price); const side = proposedTrade.side; newPositions[symbol] = { ...newPositions[symbol], value: (newPositions[symbol]?.value || 0) + (side === 'buy' ? value : -value) }; const exposure = this.calculateExposure(newPositions, portfolioValue); const violations = []; if (exposure.long > this.config.maxLongExposure) { violations.push({ type: 'maxLongExposure', message: `Long exposure ${(exposure.long * 100).toFixed(1)}% exceeds max ${(this.config.maxLongExposure * 100)}%` }); } if (exposure.short > this.config.maxShortExposure) { violations.push({ type: 'maxShortExposure', message: `Short exposure ${(exposure.short * 100).toFixed(1)}% exceeds max ${(this.config.maxShortExposure * 100)}%` }); } if (exposure.gross > this.config.maxGrossExposure) { violations.push({ type: 'maxGrossExposure', message: `Gross exposure ${(exposure.gross * 100).toFixed(1)}% exceeds max ${(this.config.maxGrossExposure * 100)}%` }); } if (exposure.leverage > this.config.maxLeverage) { violations.push({ type: 'maxLeverage', message: `Leverage ${exposure.leverage.toFixed(2)}x exceeds max ${this.config.maxLeverage}x` }); } return { allowed: violations.length === 0, violations, currentExposure: this.calculateExposure(currentPositions, portfolioValue), projectedExposure: exposure }; } // Check cash reserve checkCashReserve(cash, portfolioValue) { const cashPercent = cash / portfolioValue; if (cashPercent < this.config.minCashReserve) { return { allowed: false, message: `Cash reserve ${(cashPercent * 100).toFixed(1)}% below min ${(this.config.minCashReserve * 100)}%`, required: this.config.minCashReserve * portfolioValue }; } return { allowed: true, cashPercent }; } } /** * Risk Manager - Main integration class */ class RiskManager { constructor(config = riskConfig) { this.config = config; this.stopLossManager = new StopLossManager(config.stopLoss); this.circuitBreaker = new CircuitBreaker(config.circuitBreakers); this.positionLimits = new PositionLimitManager(config.positions); this.exposureManager = new ExposureManager(config.portfolio); // State this.blockedSymbols = new Set(); this.dailyLoss = 0; this.dailyStartEquity = 0; } // Initialize for trading day startDay(equity) { this.dailyStartEquity = equity; this.dailyLoss = 0; } // Main check - can this trade be executed? canTrade(symbol, trade, portfolio) { const results = { allowed: true, checks: {}, warnings: [], adjustments: {} }; // Check circuit breaker const circuitCheck = this.circuitBreaker.canTrade(); results.checks.circuitBreaker = circuitCheck; if (!circuitCheck.allowed) { results.allowed = false; return results; } // Check if symbol is blocked if (this.blockedSymbols.has(symbol)) { results.allowed = false; results.checks.blocked = { allowed: false, message: `Symbol ${symbol} is blocked` }; return results; } // Check position limits const positionCheck = this.positionLimits.checkPositionSize( symbol, trade.value, portfolio.equity ); results.checks.positionSize = positionCheck; if (!positionCheck.allowed) { results.warnings.push(...positionCheck.violations.map(v => v.message)); results.adjustments.size = positionCheck.adjustedSize; } // Check position count const countCheck = this.positionLimits.checkPositionCount(portfolio.positions); results.checks.positionCount = countCheck; if (!countCheck.allowed) { results.allowed = false; return results; } // Check sector exposure const sectorCheck = this.positionLimits.checkSectorExposure( symbol, trade.value, portfolio.positions, portfolio.equity ); results.checks.sectorExposure = sectorCheck; if (!sectorCheck.allowed) { results.warnings.push(sectorCheck.message); } // Check portfolio exposure const exposureCheck = this.exposureManager.checkExposure( trade, portfolio.positions, portfolio.equity ); results.checks.exposure = exposureCheck; if (!exposureCheck.allowed) { results.allowed = false; return results; } // Check cash reserve const cashAfterTrade = portfolio.cash - trade.value; const cashCheck = this.exposureManager.checkCashReserve(cashAfterTrade, portfolio.equity); results.checks.cashReserve = cashCheck; if (!cashCheck.allowed) { results.warnings.push(cashCheck.message); } // Check daily loss limit const dailyLossCheck = this.checkDailyLoss(portfolio.equity); results.checks.dailyLoss = dailyLossCheck; if (!dailyLossCheck.allowed) { results.allowed = false; return results; } return results; } // Check daily loss limit checkDailyLoss(currentEquity) { if (this.dailyStartEquity === 0) return { allowed: true }; const dailyReturn = (currentEquity - this.dailyStartEquity) / this.dailyStartEquity; if (dailyReturn < -this.config.stopLoss.maxDailyLoss) { return { allowed: false, message: `Daily loss ${(Math.abs(dailyReturn) * 100).toFixed(1)}% exceeds max ${(this.config.stopLoss.maxDailyLoss * 100)}%`, dailyLoss: dailyReturn }; } return { allowed: true, dailyLoss: dailyReturn }; } // Set stop-loss for a position setStopLoss(symbol, entryPrice, type, params) { return this.stopLossManager.setStop(symbol, entryPrice, type, params); } // Check all stops checkAllStops(prices) { const triggered = []; for (const [symbol, price] of Object.entries(prices)) { const check = this.stopLossManager.checkStop(symbol, price); if (check.triggered) { triggered.push({ symbol, ...check }); } } return triggered; } // Update circuit breaker with equity updateEquity(equity) { this.circuitBreaker.updateEquity(equity); } // Record trade for circuit breaker recordTrade(profit) { this.circuitBreaker.recordTrade(profit); } // Block a symbol blockSymbol(symbol, reason) { this.blockedSymbols.add(symbol); console.warn(`🚫 Symbol ${symbol} blocked: ${reason}`); } // Unblock a symbol unblockSymbol(symbol) { this.blockedSymbols.delete(symbol); } // Get full risk report getRiskReport(portfolio) { const exposure = this.exposureManager.calculateExposure(portfolio.positions, portfolio.equity); return { circuitBreaker: this.circuitBreaker.getState(), exposure, stops: this.stopLossManager.getActiveStops(), blockedSymbols: [...this.blockedSymbols], dailyLoss: this.checkDailyLoss(portfolio.equity), limits: { maxPositionSize: this.config.positions.maxPositionSize, maxLeverage: this.config.portfolio.maxLeverage, maxDrawdown: this.config.circuitBreakers.drawdownThreshold } }; } } // Exports export { RiskManager, StopLossManager, CircuitBreaker, PositionLimitManager, ExposureManager, riskConfig }; // Demo if run directly const isMainModule = import.meta.url === `file://${process.argv[1]}`; if (isMainModule) { console.log('══════════════════════════════════════════════════════════════════════'); console.log('RISK MANAGEMENT LAYER'); console.log('══════════════════════════════════════════════════════════════════════\n'); const riskManager = new RiskManager(); // Initialize for trading day const portfolio = { equity: 100000, cash: 50000, positions: { AAPL: { quantity: 100, price: 150, value: 15000 }, MSFT: { quantity: 50, price: 300, value: 15000 } } }; riskManager.startDay(portfolio.equity); console.log('1. Portfolio Status:'); console.log('──────────────────────────────────────────────────────────────────────'); console.log(` Equity: $${portfolio.equity.toLocaleString()}`); console.log(` Cash: $${portfolio.cash.toLocaleString()}`); console.log(` Positions: ${Object.keys(portfolio.positions).length}`); console.log(); console.log('2. Trade Check - Buy $20,000 GOOGL:'); console.log('──────────────────────────────────────────────────────────────────────'); const trade1 = { symbol: 'GOOGL', side: 'buy', value: 20000, quantity: 100, price: 200 }; const check1 = riskManager.canTrade('GOOGL', trade1, portfolio); console.log(` Allowed: ${check1.allowed ? '✓ Yes' : '✗ No'}`); if (check1.warnings.length > 0) { console.log(` Warnings: ${check1.warnings.join(', ')}`); } console.log(); console.log('3. Trade Check - Buy $60,000 TSLA (exceeds limits):'); console.log('──────────────────────────────────────────────────────────────────────'); const trade2 = { symbol: 'TSLA', side: 'buy', value: 60000, quantity: 300, price: 200 }; const check2 = riskManager.canTrade('TSLA', trade2, portfolio); console.log(` Allowed: ${check2.allowed ? '✓ Yes' : '✗ No'}`); if (check2.checks.positionSize?.violations) { for (const v of check2.checks.positionSize.violations) { console.log(` Violation: ${v.message}`); } } if (check2.adjustments.size) { console.log(` Adjusted Size: $${check2.adjustments.size.toLocaleString()}`); } console.log(); console.log('4. Stop-Loss Management:'); console.log('──────────────────────────────────────────────────────────────────────'); const stop = riskManager.setStopLoss('AAPL', 150, 'trailing'); console.log(` AAPL trailing stop set at $${stop.stopPrice.toFixed(2)}`); // Simulate price movement riskManager.stopLossManager.updateTrailingStop('AAPL', 160); // Price went up const updatedStop = riskManager.stopLossManager.stops.get('AAPL'); console.log(` After price rise to $160: stop at $${updatedStop.stopPrice.toFixed(2)}`); const stopCheck = riskManager.stopLossManager.checkStop('AAPL', 145); // Price dropped console.log(` Check at $145: ${stopCheck.triggered ? '🔴 TRIGGERED' : '🟢 OK'}`); console.log(); console.log('5. Circuit Breaker Test:'); console.log('──────────────────────────────────────────────────────────────────────'); // Simulate losses for (let i = 0; i < 4; i++) { riskManager.recordTrade(-500); } console.log(` 4 losing trades recorded`); console.log(` Consecutive losses: ${riskManager.circuitBreaker.consecutiveLosses}`); riskManager.recordTrade(-500); // 5th loss const cbState = riskManager.circuitBreaker.getState(); console.log(` 5th loss recorded`); console.log(` Circuit breaker: ${cbState.isTripped ? '🔴 TRIPPED' : '🟢 OK'}`); if (cbState.isTripped) { console.log(` Reason: ${cbState.tripMessage}`); } console.log(); console.log('6. Risk Report:'); console.log('──────────────────────────────────────────────────────────────────────'); riskManager.circuitBreaker.forceReset(); // Reset for demo const report = riskManager.getRiskReport(portfolio); console.log(` Long Exposure: ${(report.exposure.long * 100).toFixed(1)}%`); console.log(` Short Exposure: ${(report.exposure.short * 100).toFixed(1)}%`); console.log(` Gross Exposure: ${(report.exposure.gross * 100).toFixed(1)}%`); console.log(` Leverage: ${report.exposure.leverage.toFixed(2)}x`); console.log(` Circuit Breaker: ${report.circuitBreaker.isTripped ? 'TRIPPED' : 'OK'}`); console.log(` Active Stops: ${Object.keys(report.stops).length}`); console.log(); console.log('══════════════════════════════════════════════════════════════════════'); console.log('Risk management layer ready'); console.log('══════════════════════════════════════════════════════════════════════'); }