How CloddsBot's Risk Management Engine Calculates VaR, CVaR, and Kelly Sizing
CloddsBot evaluates trade risk in three sequential stages: computing rolling-window VaR and CVaR in src/risk/var.ts, enforcing portfolio-level VaR limits in src/risk/engine.ts, and applying the Kelly criterion with confidence scaling via src/utils/kelly.ts to determine optimal position sizes.
The alsk1992/CloddsBot repository implements a quantitative risk management subsystem that protects capital through statistical tail-risk measurement and mathematical bet sizing. The engine processes each trade through a structured pipeline that quantifies potential losses using both historical and parametric methods, then optimizes allocations using the Kelly formula adjusted for forecast confidence.
Stage 1: VaR and CVaR Calculation in src/risk/var.ts
The foundation of CloddsBot's risk assessment lies in the createVaRCalculator factory function. This module maintains a rolling window of P&L observations to calculate Value at Risk (VaR) and Conditional Value at Risk (CVaR), also known as Expected Shortfall.
Rolling Window Statistics
By default, the calculator initializes a bounded array (window) with a windowSize of 100 observations. As trades complete, the addObservation() method appends records containing pnlUsd, pnlPct, and timestamps (lines 85-92). The computeStats helper (lines 94-99) computes the arithmetic mean and standard deviation (σ) of these windowed values, providing the statistical moments required for parametric VaR.
Historical and Parametric VaR
The calculateAt routine (lines 47-88) generates two distinct VaR estimates at a configurable confidence level (default 0.95):
- Historical VaR: The implementation sorts the window array and selects the loss at the
(1 - confidence)percentile usingsorted[Math.floor((1 - confidence) * len)](lines 64-68). This represents the actual dollar loss that was exceeded in only 5% of historical observations. - Parametric VaR: Assuming normally distributed returns, the
normInvfunction (lines 101-145) calculates the z-score for the confidence level via the inverse CDF. The parametric VaR equals-(mean - z * stdDev)(lines 70-71), estimating tail risk based on the portfolio's volatility characteristics.
CVaR (Expected Shortfall) Implementation
Conditional Value at Risk measures the average severity of losses beyond the VaR threshold. The calculator identifies all tail losses up to the VaR index, averages them using tailLosses.reduce(...) / tailLosses.length, and negates the result (lines 73-76). This figure represents the expected loss given that a tail event has occurred.
All metrics return within a VaRResult object (lines 22-37) containing historicalVaR, parametricVaR, cvar, sample size, and statistical moments.
Stage 2: Portfolio VaR Limit Enforcement in src/risk/engine.ts
After calculating risk metrics, the Risk Engine validates trades against configured limits. The createRiskEngine function initializes with a varLimit parameter in RiskConfig. Before approving any trade, the engine compares the portfolio's current VaR against this limit (lines 162-166). If the calculated tail risk exceeds the threshold, the engine immediately rejects the trade, preventing accumulation of correlated risk exposure during volatile periods.
Stage 3: Kelly Criterion Sizing in src/utils/kelly.ts
Once VaR checks pass, the engine calculates optimal position sizes using the Kelly criterion to maximize logarithmic wealth growth while avoiding ruin.
The Core Formula and Input Validation
The calculateKelly function (lines 16-66) implements the standard formula f = (b × p - q) / b*, where:
- b = net odds (
odds - 1) - p = win probability (
winProb) - q = loss probability (
1 - p)
Input validation (lines 20-26) rejects impossible probabilities or odds ≤ 1, ensuring mathematical validity.
Expected Value Filtering
The calculator verifies positive expected value before proceeding: winProb * b - loseProb > 0 (lines 36-40). If the EV calculation yields zero or negative results—indicating a losing proposition—the function returns zero for all position sizes, preventing the risk engine from approving -EV trades.
Confidence Scaling and Bet Size Calculation
To account for uncertainty in probability estimates, the raw Kelly fraction undergoes confidence adjustment via the optional confidence parameter (lines 44-46), multiplying f* by a scaling factor (typically 0.0 to 1.0).
The final output includes:
- Full Kelly:
bankroll × min(adjustedFraction, 1)(capped at 100% of capital) - Half Kelly: 50% of the full Kelly amount
- Quarter Kelly: 25% of the full Kelly amount
- Recommended Size: Defaults to half-Kelly of the confidence-adjusted fraction, hard-capped at 25% of total bankroll (lines 52-55)
Practical Implementation Examples
Creating a VaR Calculator
import { createVaRCalculator } from './src/risk/var';
// Initialize with 99% confidence and 120-trade window
const varCalc = createVaRCalculator({ windowSize: 120, confidenceLevel: 0.99 });
// Feed P&L observations as trades complete
varCalc.addObservation({ pnlUsd: -12.4, pnlPct: -0.012, timestamp: new Date() });
varCalc.addObservation({ pnlUsd: 8.3, pnlPct: 0.008, timestamp: new Date() });
// Retrieve risk metrics
const result = varCalc.calculate();
console.log('Historical VaR ≈ $' + result.historicalVaR);
console.log('Parametric VaR ≈ $' + result.parametricVaR);
console.log('CVaR (Expected Shortfall) ≈ $' + result.cvar);
Computing Kelly Position Sizes
import { calculateKelly } from './src/utils/kelly';
const kelly = calculateKelly({
winProb: 0.62, // 62% probability of winning
odds: 2.5, // 2.5:1 payout (net odds = 1.5)
bankroll: 10_000, // $10,000 bankroll
confidence: 0.8, // 80% confidence in the edge estimate
});
console.log('Kelly fraction ≈ ' + (kelly.kellyFraction * 100).toFixed(1) + '%');
console.log('Full-Kelly bet ≈ $' + kelly.fullKelly.toFixed(2));
console.log('Recommended (half-Kelly) bet ≈ $' + kelly.recommendedSize.toFixed(2));
Integrating with the Risk Engine
import { createRiskEngine } from './src/risk/engine';
const engine = createRiskEngine(
{ varLimit: 5_000, varConfidence: 0.95, initialBankroll: 20_000 },
{
riskContext: {/* portfolio context */},
kellyCalculator: {/* DynamicKellyCalculator instance */},
},
);
// Record completed trade P&L
engine.recordPnL({ pnlUsd: -150, pnlPct: -0.0075, timestamp: new Date() });
// Validate new trade request
const decision = engine.validateTrade({
userId: 'u123',
platform: 'polymarket',
marketId: 'm456',
side: 'buy',
size: 1_000,
price: 0.55,
estimatedEdge: 0.06,
confidence: 0.85,
});
if (decision.approved) {
console.log('Approved size:', decision.adjustedSize);
} else {
console.warn('Rejected:', decision.reason);
}
Summary
- Three-stage validation: CloddsBot's risk engine first quantifies tail risk via VaR/CVaR, enforces portfolio limits, then applies Kelly sizing for capital allocation.
- Dual VaR methodology: The system calculates both historical percentile losses and parametric normal-distribution estimates to cross-validate risk figures.
- Expected Shortfall: CVaR averages losses beyond the VaR threshold, providing insight into the severity of tail events rather than just their probability.
- Defensive Kelly implementation: The formula includes EV filtering to block negative-expectation bets, confidence scaling for model uncertainty, and hard caps at 25% of bankroll for the recommended size.
- Volatility regime adjustment: Final position sizes are multiplied by
volSnapshot.sizeMultiplier(lines 343-351 inengine.ts) to reduce exposure during high-volatility periods.
Frequently Asked Questions
What confidence level does CloddsBot use for VaR calculations?
The default confidence level is 0.95 (95%), meaning the VaR represents the loss level that is not expected to be exceeded 95% of the time. This is configurable during calculator instantiation via the confidenceLevel parameter or overridden per-call in calculateAt.
How does the Kelly calculator handle negative expected value scenarios?
The calculateKelly function explicitly checks if winProb * b - loseProb is positive (lines 36-40). If the expected value is zero or negative, the function returns a zero-size result for all bet types (fullKelly, halfKelly, recommendedSize), preventing the risk engine from approving mathematically losing propositions.
What is the maximum position size the Kelly algorithm can recommend?
While the raw Kelly fraction f* can theoretically exceed 1.0, the implementation caps the full Kelly at 100% of bankroll (min(adjustedFraction, 1)). The recommended size defaults to half-Kelly and is further constrained to a maximum of 25% of total bankroll (lines 52-55), ensuring no single position can cause catastrophic drawdown.
How does the risk engine adjust Kelly sizing for market volatility?
After retrieving the base Kelly position size, the engine applies a volatility regime multiplier via volSnapshot.sizeMultiplier (lines 343-351 in src/risk/engine.ts). This scaler reduces the recommended position size during high-volatility environments and increases it during stable periods, dynamically adapting the Kelly output to current market conditions before final approval.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →