[WITH CODE] Risk Engine: Contract sizing
How many contracts should you trade to avoid blowing up your account?
Table of content:
Introduction.
Structural reframing.
Tail loss modeling.
Risk-responsive leverage allocation.
Exposure and contract sizing logic.
Introduction
Algorithmic trading systems can give you this sleek, high-tech confidence—like the robots have everything under control. They’re fast, precise, and backtested to death, right? But that’s where the trap snaps shut. When your risk metrics are built on things like standard deviation or recent drawdowns, you’re basically judging a hurricane by the breeze in your backyard. Sure, those stats tell you how volatile things usually are, but they completely ignore the financial equivalent of asteroid strikes—the rare, catastrophic events that actually blow up portfolios.
The problem is, most strategies get optimized for smooth, sexy equity curves and fat Sharpe ratios. It’s like training for a marathon on a treadmill: you look great until you hit real hills, potholes, or a sudden thunderstorm. Backtests love historical data, but history doesn’t repeat—it just rhymes. And when it does, it’s usually in ways your model never saw coming, like a liquidity drought or every “uncorrelated” asset nosediving at once.
Then there’s leverage. Traders get cocky here. They’ll size positions based on calm markets, not even factoring in how liquidity evaporates when everyone’s panicking. You might think you’re holding a diversified portfolio, but under stress, everything can turn into the same trade—like everyone suddenly realizing they’re all wearing the same outfit to a party.
So how do you fix this? Start by ditching the fairy-tale math. Use metrics that stare into the abyss, like Expected Shortfall, which focuses on how bad the worst days could get. Stress-test your strategy against synthetic disasters—not just 2008 or March 2020, but nightmare scenarios that haven’t happened yet. What if rates spike and oil crashes and Twitter erupts into a meme-stock frenzy?
And for the love of volatility, stop assuming markets behave normally. Build your models with fat tails—those thick, ugly edges of the distribution where Black Swans lurk. Use regime-switching models that sense when the market’s mood shifts from chill to unhinged. Adjust leverage dynamically: throttle it back when the VIX is screaming, and dial it up when complacency returns. Assume your models will break, because they will. The goal isn’t to predict chaos—it’s to survive it. Real robustness isn’t avoiding storms; it’s building a ship that won’t sink when the waves get stupid.
Risk creeps in silently, not with a bang but with the quiet accumulation of leverage during calm markets. It’s a slow burn—position sizes grow, correlations compress, and volatility flatlines, lulling models into complacency. But when the storm hits—when volatility spikes, liquidity evaporates, and "diversified" assets collapse into a single toxic trade—the damage is irreversible. Losses compound. Margins blow out. Exit strategies fail. The system, backtested to perfection, becomes its own enemy.
This isn’t a flaw in math; it’s a flaw in philosophy. Traditional risk frameworks treat markets as static, assuming tomorrow’s risks mirror today’s. They rely on volatility, a metric blind to asymmetry, equating upside swings with catastrophic downsides. But true danger isn’t in the noise—it’s in the silence before the tail strike. Volatility doesn’t measure risk; it mislabels it.
Enter Conditional Value at Risk. Unlike volatility, CVaR stares unflinchingly at the tails—the 5% of days where portfolios don’t just dip but implode. It answers the critical question: If things go horribly wrong, how wrong will they go?
If you want more info about this, check:
Yet most models ignore this. They’re polished, precise, and catastrophically fragile.
The reckoning arrives abruptly: a flash crash, a rate shock, a liquidity black hole. The portfolio hemorrhages. Stop-losses lag. Leverage magnifies losses. The model, technically “correct,” fails because it mistook symmetry for safety. This moment forces a brutal truth: leverage cannot be static. It must adapt, not to average conditions, but to the market’s worst moods.
Thus, the paradigm shifts: Exposure must derive from tail risk, not capital.
This demands rebuilding the risk engine from the ground up:
Scale positions inversely to CVaR. When tail risks swell—e.g., VIX term structure inverts, credit spreads snap—slash exposure.
Adjust risk budgets in real time.
Treat liquidity as a decaying asset. Reduce position sizes in instruments prone to bid-ask explosions.
Simulate not just historical crises, but unseen ones.
But this engine isn’t glamorous. It rejects “optimal” leverage for robust leverage. It accepts lower returns in calm markets to avoid annihilation in storms. It trades elegance for survival.
Structural reframing
To remedy the core vulnerability identified earlier—the mismatch between static exposure and dynamic market risk—we introduce a modular architecture centered on CVar. This architecture does not merely replace existing sizing heuristics; it fundamentally redefines the sequence by which exposure is calculated, risk is interpreted, and trades are executed.
CVaR, measures the average loss in the worst-case quantile beyond a predefined confidence level. Where VaR cuts off the tail and says this is where losses start, CVaR ventures into the abyss and calculates the mean depth of those tail events. This makes it a far superior risk measure for systems that operate in high-stakes, fast-changing environments.
The engine is structured into three tightly interconnected pillars:
Tail risk quantification: A module that calculates CVaR from recent returns and maintains adaptive windows to reflect the most relevant market behavior.
Leverage scaling: A mathematical map that adjusts trader-intended leverage downward or upward depending on the current CVaR relative to a target risk level.
Contract sizing and execution engine: A translation layer that takes adjusted exposure and converts it into minimum-executable positions, while respecting asset-specific constraints like nominal contract value or minimum lot size.
Each module is driven by its predecessor and influences the next. Tail risk computation is not an endpoint—it is the beginning of a logic cascade that transforms a single CVaR number into actionable, deployable orders. As a result, position sizing becomes reflexive: it adjusts in real time to the breathing patterns of market stress.
The workflow is this:
This is a radical shift in philosophy. Traditionally, leverage was set based on capital, expected volatility, or worst-case drawdown observed during backtests. In this new architecture, leverage is set by the magnitude of plausible disaster. Instead of assuming the world will resemble the past, the system stays paranoid—it expects the worst and sizes for it.
No architecture survives first contact with markets without compromise. As we build this CVaR-based engine, we must confront a set of interlocking practical and theoretical hurdles that complicate the pure theory:
Brokers also impose caps on leverage and may reject orders that violate margin requirements. This requires the contract sizing logic to be highly aware of execution rules, rounding precision, and margin buffer logic.
Even a short delay between CVaR calculation and order execution can be fatal. The market regime may change between measurement and trade. To mitigate this, the engine should monitor volatility indicators and apply early-warning signals to downscale leverage proactively.
Forex, indices, stocks, and ETFs each behave differently in terms of liquidity, nominal size, and regulatory limits. The engine must adapt its parameters dynamically across asset types, creating abstract interfaces that unify position sizing while respecting domain-specific boundaries—adjust this for your broker conditions.
These constraints are not obstacles—they are design features. They force the risk engine to become robust, not just in theory but in operation. And they ensure that the engine does not merely function in lab conditions, but also in live trading.
Keep in mind that this framework has two layers:
Today we're focusing on the second item, "number of contracts." But you can find the first one “Capital allocation” here:
Tail loss modeling
In traditional models, risk is often measured by volatility—a symmetric metric that assumes losses and gains are equally undesirable. This assumption is fundamentally flawed for live trading. While gains compound and benefit portfolios, losses have a nonlinear and often catastrophic impact. Volatility merely tells us how much something moves; it doesn't tell us how dangerous those movements are.
CVaR focuses specifically on the tail—the area of the return distribution where the worst outcomes live. More formally, for a given confidence level α∈(0,1), CVaR is defined as:
Where L is the loss variable and VaRα is the Value at Risk at the same confidence level. This means CVaR tells us the average loss in the worst 1−α proportion of cases:
This distinction is crucial. VaR gives us a threshold; CVaR tells us what happens beyond that threshold. And in trading, those beyond-threshold events are what truly kill portfolios.
To compute CVaR in a trading system, we use recent return data to simulate what would happen if positions were sized as they are now. Then we calculate the loss distribution and identify the average loss among the worst outcomes. But here we introduce and different path by bootstraping the data:
class CVarCalculator:
"""
Calculates Conditional Value at Risk (Expected Shortfall) from a series of returns.
Supports bootstrapped smoothing to mitigate instability.
"""
def __init__(self, confidence_level: float = 0.95, bootstrap_samples: int = 0):
if not 0 < confidence_level < 1:
raise ValueError("Confidence level must be between 0 and 1.")
_validate_positive("Bootstrap samples", bootstrap_samples if bootstrap_samples is not None else 0)
self.confidence_level = confidence_level
self.bootstrap_samples = bootstrap_samples
def calculate(self, returns: np.ndarray) -> float:
returns = np.asarray(returns, dtype=float)
if returns.size == 0:
raise ValueError("The return series is empty.")
# Positive losses as negative returns
losses = -returns
alpha = 1 - self.confidence_level
if self.bootstrap_samples and self.bootstrap_samples > 1:
# Bootstrapped CVaR to smooth estimation
boot_cvars = []
for _ in range(self.bootstrap_samples):
sample = np.random.choice(losses, size=losses.size, replace=True)
var_thresh = np.percentile(sample, alpha * 100)
tail = sample[sample >= var_thresh]
boot_cvars.append(np.mean(tail) if tail.size else 0.0)
return float(np.mean(boot_cvars))
else:
# Standard CVaR
var_threshold = np.percentile(losses, alpha * 100)
tail_losses = losses[losses >= var_threshold]
return float(np.mean(tail_losses)) if tail_losses.size else 0.0
This class forms the backbone of the risk engine. It translates noisy, asymmetric financial data into a single, interpretable signa.
One of the key strengths of CVaR lies in its coherence. Unlike VaR, which fails subadditivity, CVaR is a coherent risk measure. This means it satisfies desirable mathematical properties such as:
Monotonicity: If one portfolio always produces worse outcomes than another, it should have a higher CVaR.
Translation invariance: Adding a fixed amount to all outcomes reduces CVaR by that amount.
Subadditivity: Diversification reduces risk; CVaR accounts for that.
Positive homogeneity: Doubling the position doubles the risk.
These properties make CVaR suitable not just as a metric, but as the foundation for a scalable risk allocation system. It adapts well to leverage-based systems, works across multiple assets, and naturally respects diversification.
A picture is worth a thousand words:
Risk-responsive leverage allocation
At the core of responsible position sizing lies the transformation of a trader’s desired leverage into one that respects risk exposure—particularly the conditional risk defined by CVaR. In traditional systems, leverage is often an arbitrary number chosen to increase capital efficiency or target a desired level of volatility. In our system, leverage is not an input—it’s an output.
We begin with a trader-defined leverage, ℓtrader, which represents the maximum exposure the trader is willing to take in the absence of stress. To scale this according to tail risk, we introduce a target CVaR, CVaRtarget, and adjust the leverage based on the observed CVaRactual.
The formula is simple, but beautiful:
This creates an inverse relationship: when actual risk is high, leverage decreases. When risk is low, leverage increases—though within predefined limits.
We then apply a hard cap based on broker or regulatory constraints:
This formulation allows the system to breathe with the market. In calm periods, leverage expands; in turbulent ones, it contracts. Unlike static exposure methods, this approach dynamically adapts to the shape and severity of the return distribution:
Here's how the logic is encoded in Python:
class RiskAdjuster:
"""
Adjusts trader leverage based on actual CVaR, target CVaR,
with optional floor on CVaR and dampening regularization.
"""
def __init__(
self,
confidence_level: float = 0.95,
cvar_target: float = 0.01,
bootstrap_samples: int = 0,
cvar_floor: float = None,
reg_factor: float = 1.0
):
_validate_positive("Target CVaR", cvar_target)
if cvar_floor is not None and cvar_floor < 0:
raise ValueError(f"CVaR floor must be non-negative. Given: {cvar_floor}")
if reg_factor < 1:
raise ValueError(f"Regularization factor must be >=1. Given: {reg_factor}")
self.cvar_target = cvar_target
self.cvar_floor = cvar_floor
self.reg_factor = reg_factor
self.calculator = CVarCalculator(confidence_level, bootstrap_samples)
def actual_cvar(self, returns: np.ndarray) -> float:
raw_cvar = self.calculator.calculate(returns)
# Ensure floor
if self.cvar_floor is not None:
return max(raw_cvar, self.cvar_floor)
return raw_cvar
def adjust_leverage(self, trader_leverage: float, actual_cvar: float) -> float:
"""
Calculates leverage adjustment:
raw = trader_leverage * (cvar_target / actual_cvar) if actual_cvar > 0 else trader_leverage
Then dampen: trader_leverage + (raw - trader_leverage) / reg_factor
"""
_validate_positive("Trader leverage", trader_leverage)
if actual_cvar <= 0:
return trader_leverage
raw_leverage = trader_leverage * (self.cvar_target / actual_cvar)
# Dampening towards trader leverage
adjusted = trader_leverage + (raw_leverage - trader_leverage) / self.reg_factor
return float(adjusted)
This method ensures that when tail risk is low, leverage expands, but when risk climbs, leverage is automatically curtailed to preserve capital. Indeed, it creates a form of volatility targeting, but one that focuses on tail events rather than average dispersion.
Exposure and contract sizing logic
At this point, the system has already calculated a risk-adjusted leverage level based on CVaR. But theory meets practice when this leverage needs to be implemented as tradable units—actual contracts or shares. This step requires understanding the unique structure of each asset class and translating exposure into discrete, executable orders.
Every asset type—be it forex, indices, equities, or ETFs—comes with a distinct market structure that defines its contract size (or nominal value), minimum tradable lot, and allowable leverage. These constraints are not soft—they are enforced by brokers and exchanges. Any attempt to trade outside these rules results in rejected orders or, worse, unexpected margin calls.
The objective, then, is to compute how many units of an asset should be traded given:
Capital C.
Risk-adjusted leverage ℓfinal.
Contract nominal value N
Minimum trading lot μ.
We begin by calculating the effective exposure:
This gives us the amount of capital to be deployed under the current leverage constraint. From there, the raw number of contracts is:
However, contracts must be bought or sold in fixed-size blocks—commonly known as lots. Therefore, we floor this value to the nearest allowable multiple of μ:
This ensures compliance with execution rules and avoids order rejection. And once again, the implementation is based on:
class ContractSizer:
"""
Calculates nominal volume, max leverage, and minimum lot size per asset.
"""
@staticmethod
def get_asset_params(asset_type: str, price: float):
t = asset_type.lower()
_validate_positive("Asset price", price)
if t == 'forex':
return 100_000.0, 30.0, 0.01
elif t == 'indice':
return 10.0 * price, 20.0, 0.01
elif t in ('stock', 'etf', 'stock/etf'):
return price, 5.0, 1.0
else:
raise ValueError("Unsupported asset type. Use 'forex', 'indice', or 'stock/etf'.")
@staticmethod
def size_contracts(capital: float, leverage: float, nominal: float, min_lot: float):
_validate_positive("Capital", capital)
exposure = capital * leverage
raw = exposure / nominal
floored = np.floor(raw / min_lot) * min_lot
final = floored if floored >= min_lot else min_lot
return float(raw), float(final)
This class produces two outputs, the eheoretical exposure in raw units and the rounded, valid number of contracts to execute. It provides the necessary bridge between the world of financial theory and the realities of brokerage APIs, tick sizes, and lot minimums. Different asset types require different assumptions:
Embedding this differentiation ensures that contract sizing remains coherent across a diverse portfolio.
Here you can check an example of output, the last row Final Contracts is what you would use for your order engine:
Systems offline—mission paused, team. Stellar data runs, laser-focus analysis, zero fluff—pure algorithmic triumph. Let that iterate.
Now power down. Purge the buffers, recalibrate your priors, and let stochastic pulses guide your next breakthrough. Code tighter, hedge sharper, and always—always—push the frontier. Stay nimble, stay hungry, and forever—stay quanty! 🚀
PS: How often do you update or refactor your trading codebase?
Appendix
Full code here:
import numpy as np
def _validate_positive(name: str, value: float):
if value is None or value <= 0:
raise ValueError(f"{name} must be positive. Given value: {value}")
class CVarCalculator:
"""
Calculates Conditional Value at Risk (Expected Shortfall) from a series of returns.
Supports bootstrapped smoothing to mitigate instability.
"""
def __init__(self, confidence_level: float = 0.95, bootstrap_samples: int = 0):
if not 0 < confidence_level < 1:
raise ValueError("Confidence level must be between 0 and 1.")
_validate_positive("Bootstrap samples", bootstrap_samples if bootstrap_samples is not None else 0)
self.confidence_level = confidence_level
self.bootstrap_samples = bootstrap_samples
def calculate(self, returns: np.ndarray) -> float:
returns = np.asarray(returns, dtype=float)
if returns.size == 0:
raise ValueError("The return series is empty.")
# Positive losses as negative returns
losses = -returns
alpha = 1 - self.confidence_level
if self.bootstrap_samples and self.bootstrap_samples > 1:
# Bootstrapped CVaR to smooth estimation
boot_cvars = []
for _ in range(self.bootstrap_samples):
sample = np.random.choice(losses, size=losses.size, replace=True)
var_thresh = np.percentile(sample, alpha * 100)
tail = sample[sample >= var_thresh]
boot_cvars.append(np.mean(tail) if tail.size else 0.0)
return float(np.mean(boot_cvars))
else:
# Standard CVaR
var_threshold = np.percentile(losses, alpha * 100)
tail_losses = losses[losses >= var_threshold]
return float(np.mean(tail_losses)) if tail_losses.size else 0.0
class RiskAdjuster:
"""
Adjusts trader leverage based on actual CVaR, target CVaR,
with optional floor on CVaR and dampening regularization.
"""
def __init__(
self,
confidence_level: float = 0.95,
cvar_target: float = 0.01,
bootstrap_samples: int = 0,
cvar_floor: float = None,
reg_factor: float = 1.0
):
_validate_positive("Target CVaR", cvar_target)
if cvar_floor is not None and cvar_floor < 0:
raise ValueError(f"CVaR floor must be non-negative. Given: {cvar_floor}")
if reg_factor < 1:
raise ValueError(f"Regularization factor must be >=1. Given: {reg_factor}")
self.cvar_target = cvar_target
self.cvar_floor = cvar_floor
self.reg_factor = reg_factor
self.calculator = CVarCalculator(confidence_level, bootstrap_samples)
def actual_cvar(self, returns: np.ndarray) -> float:
raw_cvar = self.calculator.calculate(returns)
# Ensure floor
if self.cvar_floor is not None:
return max(raw_cvar, self.cvar_floor)
return raw_cvar
def adjust_leverage(self, trader_leverage: float, actual_cvar: float) -> float:
"""
Calculates leverage adjustment:
raw = trader_leverage * (cvar_target / actual_cvar) if actual_cvar > 0 else trader_leverage
Then dampen: trader_leverage + (raw - trader_leverage) / reg_factor
"""
_validate_positive("Trader leverage", trader_leverage)
if actual_cvar <= 0:
return trader_leverage
raw_leverage = trader_leverage * (self.cvar_target / actual_cvar)
# Dampening towards trader leverage
adjusted = trader_leverage + (raw_leverage - trader_leverage) / self.reg_factor
return float(adjusted)
class ContractSizer:
"""
Calculates nominal volume, max leverage, and minimum lot size per asset.
"""
@staticmethod
def get_asset_params(asset_type: str, price: float):
t = asset_type.lower()
_validate_positive("Asset price", price)
if t == 'forex':
return 100_000.0, 30.0, 0.01
elif t == 'indice':
return 10.0 * price, 20.0, 0.01
elif t in ('stock', 'etf', 'stock/etf'):
return price, 5.0, 1.0
else:
raise ValueError("Unsupported asset type. Use 'forex', 'indice', or 'stock/etf'.")
@staticmethod
def size_contracts(capital: float, leverage: float, nominal: float, min_lot: float):
_validate_positive("Capital", capital)
exposure = capital * leverage
raw = exposure / nominal
floored = np.floor(raw / min_lot) * min_lot
final = floored if floored >= min_lot else min_lot
return float(raw), float(final)
class RiskEngineCVaR:
"""
Risk engine: computes CVaR with smoothing, adjusts leverage with floor and dampening,
applies market constraints, and sizes contracts.
"""
def __init__(
self,
returns_series: np.ndarray,
asset_type: str,
price: float,
capital: float,
lev_trader: float,
confidence_level: float = 0.95,
cvar_target: float = 0.01,
bootstrap_samples: int = 0,
cvar_floor: float = None,
reg_factor: float = 1.0,
margin_buffer: float = 0.05
):
# Validate inputs
for name, v in (("capital", capital), ("lev_trader", lev_trader), ("margin_buffer", margin_buffer)):
if name == "margin_buffer":
if not 0 <= v < 1:
raise ValueError("Margin buffer must be between 0 and 1.")
else:
_validate_positive(name, v)
self.capital = capital
self.price = price
self.asset_type = asset_type
self.lev_trader = lev_trader
self.returns = np.asarray(returns_series, dtype=float)
# Compute actual CVaR
self.adjuster = RiskAdjuster(
confidence_level,
cvar_target,
bootstrap_samples,
cvar_floor,
reg_factor
)
self.cvar_actual = self.adjuster.actual_cvar(self.returns)
self.cvar_target = cvar_target
# Asset params and market caps
self.nominal, self.max_leverage, self.min_lot = ContractSizer.get_asset_params(
asset_type, price
)
# Raw and dampened leverage
raw_lev = self.adjuster.adjust_leverage(lev_trader, self.cvar_actual)
# Enforce margin buffer: reduce max leverage
max_allowed = self.max_leverage * (1 - margin_buffer)
self.raw_leverage = raw_lev
self.leverage = float(min(raw_lev, max_allowed))
# Contract sizing
self.raw_contracts, self.contracts = ContractSizer.size_contracts(
capital, self.leverage, self.nominal, self.min_lot
)
def summary(self) -> dict:
return {
'Actual CVaR (%)': round(self.cvar_actual * 100, 4),
'Target CVaR (%)': round(self.cvar_target * 100, 2),
'Trader Leverage': self.lev_trader,
'Raw Leverage': round(self.raw_leverage, 2),
'Final Leverage': round(self.leverage, 2),
'Asset': self.asset_type,
'Price': self.price,
'Nominal Volume': self.nominal,
'Min Lot': self.min_lot,
'Raw Contracts': round(self.raw_contracts, 4),
'Final Contracts': round(self.contracts, 4),
}
def main():
# Example usage with bootstrapping and dampening
np.random.seed(42)
returns = np.random.normal(loc=0.001, scale=0.02, size=100)
engine = RiskEngineCVaR(
returns_series=returns,
asset_type='indice',
price=5345.0,
capital=10000.0,
lev_trader=2,
confidence_level=0.95,
cvar_target=0.01,
bootstrap_samples=500,
cvar_floor=0.002,
reg_factor=3.0,
margin_buffer=0.1
)
for k, v in engine.summary().items():
print(f"{k}: {v}")
if __name__ == '__main__':
main()