Skip to main content
Glama
Ademscodeisnotsobad

Quant Companion MCP

risk.ts5.91 kB
/** * Risk Management Module * * Applies risk limits to strategy decisions. * Pure functions - no side effects or external I/O. */ import { StrategyContext, StrategyDecision, RiskLimits, RiskState, RiskAdjustedDecision, DEFAULT_RISK_LIMITS, } from "./types"; /** * Create initial risk state. */ export function createRiskState(initialEquity: number): RiskState { return { peakEquity: initialEquity, dailyPnl: 0, dayStartEquity: initialEquity, currentDate: undefined, }; } /** * Update risk state with new equity value. * Call this after each bar to track peak equity and daily PnL. */ export function updateRiskState( state: RiskState, newEquity: number, date: Date ): RiskState { const newState = { ...state }; // Update peak equity if (newEquity > newState.peakEquity) { newState.peakEquity = newEquity; } // Check for day change - reset daily PnL const currentDateStr = date.toISOString().split("T")[0]; const prevDateStr = newState.currentDate?.toISOString().split("T")[0]; if (currentDateStr !== prevDateStr) { // New day - reset daily tracking newState.dayStartEquity = newEquity; newState.dailyPnl = 0; newState.currentDate = date; } else { // Same day - update daily PnL newState.dailyPnl = newEquity - newState.dayStartEquity; } return newState; } /** * Calculate current drawdown from peak. */ export function calculateDrawdown(currentEquity: number, peakEquity: number): number { if (peakEquity <= 0) return 0; return (peakEquity - currentEquity) / peakEquity; } /** * Calculate daily loss as fraction of day start equity. */ export function calculateDailyLoss(state: RiskState): number { if (state.dayStartEquity <= 0) return 0; if (state.dailyPnl >= 0) return 0; return Math.abs(state.dailyPnl) / state.dayStartEquity; } /** * Calculate notional exposure for a position. */ export function calculateNotional(shares: number, price: number): number { return Math.abs(shares) * price; } /** * Calculate max shares allowed by notional limit. */ export function maxSharesByNotional( equity: number, price: number, maxNotionalFraction: number ): number { if (price <= 0) return 0; const maxNotional = equity * maxNotionalFraction; return Math.floor(maxNotional / price); } /** * Apply risk management to a strategy decision. * * This function: * 1. Checks drawdown limits * 2. Checks daily loss limits * 3. Caps position size by notional limits * 4. Returns adjusted decision with annotations */ export function applyRiskManagement( decision: StrategyDecision, context: StrategyContext, limits: RiskLimits, state: RiskState ): RiskAdjustedDecision { const annotations: string[] = []; let targetShares = decision.targetPositionShares; let wasAdjusted = false; let reason = decision.reason; // 1. Check portfolio drawdown const drawdown = calculateDrawdown(context.equity, state.peakEquity); if (drawdown > limits.maxPortfolioDrawdown) { if (targetShares !== 0) { targetShares = 0; wasAdjusted = true; reason = `forced flat: max drawdown exceeded (${(drawdown * 100).toFixed(1)}% > ${(limits.maxPortfolioDrawdown * 100).toFixed(1)}%)`; annotations.push(reason); } } // 2. Check daily loss limit const dailyLoss = calculateDailyLoss(state); if (dailyLoss > limits.maxDailyLoss) { if (targetShares !== 0) { targetShares = 0; wasAdjusted = true; reason = `forced flat: daily loss limit hit (${(dailyLoss * 100).toFixed(1)}% > ${(limits.maxDailyLoss * 100).toFixed(1)}%)`; annotations.push(reason); } } // 3. Cap by notional limit (only if not already forced flat) if (targetShares !== 0) { const maxShares = maxSharesByNotional( context.equity, context.price, limits.maxNotionalPerSymbol ); const absTarget = Math.abs(targetShares); if (absTarget > maxShares) { const sign = targetShares > 0 ? 1 : -1; targetShares = sign * maxShares; wasAdjusted = true; const annotation = `capped position: notional limit (${absTarget} → ${maxShares} shares)`; annotations.push(annotation); reason = `${decision.reason} [${annotation}]`; } } // 4. Check gross exposure limit if (targetShares !== 0) { const proposedNotional = calculateNotional(targetShares, context.price); const maxNotional = context.equity * limits.maxGrossExposure; if (proposedNotional > maxNotional) { const maxByExposure = Math.floor(maxNotional / context.price); const sign = targetShares > 0 ? 1 : -1; targetShares = sign * maxByExposure; wasAdjusted = true; const annotation = `capped by gross exposure limit`; annotations.push(annotation); } } return { targetPositionShares: targetShares, confidence: decision.confidence, reason, originalDecision: decision, wasAdjusted, riskAnnotations: annotations, }; } /** * Validate risk limits configuration. */ export function validateRiskLimits(limits: RiskLimits): string[] { const errors: string[] = []; if (limits.maxNotionalPerSymbol <= 0 || limits.maxNotionalPerSymbol > 1) { errors.push("maxNotionalPerSymbol must be between 0 and 1"); } if (limits.maxPortfolioDrawdown <= 0 || limits.maxPortfolioDrawdown > 1) { errors.push("maxPortfolioDrawdown must be between 0 and 1"); } if (limits.maxDailyLoss <= 0 || limits.maxDailyLoss > 1) { errors.push("maxDailyLoss must be between 0 and 1"); } if (limits.maxGrossExposure <= 0) { errors.push("maxGrossExposure must be positive"); } return errors; } /** * Create risk limits with partial overrides. */ export function createRiskLimits(overrides: Partial<RiskLimits> = {}): RiskLimits { return { ...DEFAULT_RISK_LIMITS, ...overrides, }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Ademscodeisnotsobad/Quant-Companion-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server