[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()