Risk controls ensure the trading system operates within safe parameters, preventing excessive losses and maintaining portfolio health through position sizing, exposure limits, and validation checks.
Risk Management Layers
Position Sizing
Calculate safe position sizes based on portfolio value and risk per trade
Exposure Limits
Enforce maximum position sizes and concentration limits per symbol
Cash Validation
Verify sufficient available cash before opening positions
Stop-Loss Requirements
Require stop-loss levels for all positions to limit downside
Cooldown Periods
Prevent rapid re-entry after closing losing positions
Position Sizing
Position sizing determines how much capital to allocate to each trade based on portfolio value and risk tolerance.
Risk-Based Sizing
Calculate position size based on risk per trade as a percentage of portfolio value.
function calculatePositionSize (
portfolioValue : number ,
riskPercentage : number ,
entryPrice : number ,
stopLoss : number ,
) : number {
const riskAmount = portfolioValue * ( riskPercentage / 100 );
const priceRisk = Math . abs ( entryPrice - stopLoss );
const positionSize = riskAmount / priceRisk ;
return positionSize ;
}
Example :
Portfolio: $10,000
Risk per trade: 2% ($200)
Entry: $50,000
Stop-loss: $48,000
Price risk: $2,000 per BTC
Position size: 200 / 200 / 200/ 2,000 = 0.1 BTC
This ensures that if the stop-loss is hit, the maximum loss is exactly 2% of portfolio value, regardless of the distance to the stop.
Fixed Percentage Sizing
Allocate a fixed percentage of portfolio value to each position.
function calculateFixedPercentageSize (
portfolioValue : number ,
allocationPercentage : number ,
entryPrice : number ,
) : number {
const notional = portfolioValue * ( allocationPercentage / 100 );
const positionSize = notional / entryPrice ;
return positionSize ;
}
Example :
Portfolio: $10,000
Allocation: 10%
Entry: $50,000
Notional: $1,000
Position size: 1 , 000 / 1,000 / 1 , 000/ 50,000 = 0.02 BTC
Fixed percentage sizing can lead to large losses if stops are wide. Risk-based sizing is generally safer.
Kelly Criterion
Optimal position sizing based on historical win rate and average win/loss ratio.
function calculateKellySize (
winRate : number , // Probability of winning (0-1)
avgWin : number , // Average win amount
avgLoss : number , // Average loss amount (positive)
) : number {
const winLossRatio = avgWin / avgLoss ;
const kellyPercent = ( winRate * winLossRatio - ( 1 - winRate )) / winLossRatio ;
return Math . max ( 0 , kellyPercent ); // Never negative
}
Example :
Win rate: 60% (0.6)
Average win: $300
Average loss: $150
Win/loss ratio: 2.0
Kelly %: (0.6 × 2.0 - 0.4) / 2.0 = 40%
Kelly Criterion can suggest aggressive position sizes. Many traders use fractional Kelly (e.g., half Kelly) for more conservative sizing.
Exposure Limits
Exposure limits prevent over-concentration in any single asset or direction.
Per-Symbol Limits
Restrict maximum position size per symbol as a percentage of portfolio value.
const MAX_POSITION_SIZE_PERCENT = 20 ; // Max 20% of portfolio in any symbol
function validatePositionSize (
symbol : string ,
requestedNotional : number ,
existingPositions : Position [],
portfolioValue : number ,
) : { allowed : boolean ; reason ?: string } {
// Calculate existing exposure for this symbol
const existingPosition = existingPositions . find ( p => p . symbol === symbol );
const existingNotional = existingPosition
? existingPosition . quantity * existingPosition . markPrice
: 0 ;
// Calculate total exposure after this trade
const totalNotional = existingNotional + requestedNotional ;
const exposurePercent = ( totalNotional / portfolioValue ) * 100 ;
if ( exposurePercent > MAX_POSITION_SIZE_PERCENT ) {
return {
allowed: false ,
reason: `Position would exceed max ${ MAX_POSITION_SIZE_PERCENT } % exposure for ${ symbol } ( ${ exposurePercent . toFixed ( 1 ) } %)` ,
};
}
return { allowed: true };
}
Directional Limits
Limit total exposure in one direction (LONG or SHORT) across all positions.
const MAX_DIRECTIONAL_EXPOSURE = 60 ; // Max 60% net long or short
function validateDirectionalExposure (
side : "LONG" | "SHORT" ,
requestedNotional : number ,
existingPositions : Position [],
portfolioValue : number ,
) : { allowed : boolean ; reason ?: string } {
// Calculate current net exposure
let longNotional = 0 ;
let shortNotional = 0 ;
for ( const position of existingPositions ) {
const notional = position . quantity * position . markPrice ;
if ( position . side === "LONG" ) {
longNotional += notional ;
} else {
shortNotional += notional ;
}
}
// Add requested position
if ( side === "LONG" ) {
longNotional += requestedNotional ;
} else {
shortNotional += requestedNotional ;
}
const netExposure = longNotional - shortNotional ;
const netExposurePercent = ( Math . abs ( netExposure ) / portfolioValue ) * 100 ;
if ( netExposurePercent > MAX_DIRECTIONAL_EXPOSURE ) {
return {
allowed: false ,
reason: `Directional exposure would exceed ${ MAX_DIRECTIONAL_EXPOSURE } % ( ${ netExposurePercent . toFixed ( 1 ) } %)` ,
};
}
return { allowed: true };
}
Correlation Limits
Prevent over-concentration in correlated assets (e.g., multiple altcoins).
Correlation limits are not currently implemented but are recommended for production systems. Track asset correlations and limit total exposure to highly correlated instruments.
Cash Validation
Before opening a position, the system verifies sufficient available cash considering existing positions and margin requirements.
hasSufficientCash (
symbol : string ,
side : OrderSide ,
execution : OrderExecution ,
leverage : number | undefined ,
): boolean {
const position = this . positions . get ( symbol );
const currentSide = position ?. side ?? null ;
const orderSide = side === "buy" ? "LONG" : "SHORT" ;
const notional = execution . totalQuantity * execution . averagePrice ;
// If closing or reducing position, no cash needed
if ( currentSide && currentSide !== orderSide ) {
return true ;
}
// Calculate required cash (with leverage)
const effectiveLeverage = leverage ?? 1 ;
const requiredCash = notional / effectiveLeverage ;
// Check available cash
const snapshot = this . getSnapshot ();
const available = snapshot . availableCash ;
if ( available < requiredCash ) {
console . warn (
`[AccountState] Insufficient cash: need= ${ requiredCash . toFixed ( 2 ) } , have= ${ available . toFixed ( 2 ) } ` ,
);
return false ;
}
return true ;
}
Location : src/server/features/simulator/accountState.ts
Available Cash Calculation :
availableCash = totalCash + totalUnrealizedPnl - totalMarginUsed
Where:
totalCash: Initial capital + realized P&L
totalUnrealizedPnl: Sum of unrealized P&L across all positions
totalMarginUsed: Sum of margin reserved for open positions
Margin Used (per position):
marginUsed = ( quantity × entryPrice ) / leverage
Example :
Initial capital: $10,000
Realized P&L: +$500
Unrealized P&L: +$200 (from open positions)
Margin used: $3,000 (3 positions with 3x leverage)
Available cash : 10 , 000 + 10,000 + 10 , 000 + 500 + 200 − 200 - 200 − 3,000 = $7,700
The system rejects orders that would exceed available cash to prevent over-leveraging and forced liquidations.
Stop-Loss Requirements
All positions should have a stop-loss level defined in the exit plan to limit downside risk.
Mandatory Stops
While the system doesn’t enforce mandatory stop-losses (to allow flexibility), best practices recommend requiring stops for all positions.
function validateExitPlan (
side : "LONG" | "SHORT" ,
entryPrice : number ,
exitPlan : ExitPlan | null ,
) : { valid : boolean ; warnings : string [] } {
const warnings : string [] = [];
// Check for stop-loss
if ( ! exitPlan ?. stop ) {
warnings . push ( "No stop-loss defined - position has unlimited downside risk" );
} else {
// Validate stop-loss direction
const isLong = side === "LONG" ;
const stopBelowEntry = exitPlan . stop < entryPrice ;
if ( isLong && ! stopBelowEntry ) {
warnings . push ( "LONG stop-loss should be below entry price" );
} else if ( ! isLong && stopBelowEntry ) {
warnings . push ( "SHORT stop-loss should be above entry price" );
}
}
// Check for take-profit
if ( ! exitPlan ?. target ) {
warnings . push ( "No take-profit defined - consider setting a profit target" );
}
return {
valid: warnings . length === 0 ,
warnings ,
};
}
Risk-Reward Ratio
Calculate and validate risk-reward ratios before opening positions.
function calculateRiskReward (
side : "LONG" | "SHORT" ,
entryPrice : number ,
stopLoss : number ,
target : number ,
) : number {
const isLong = side === "LONG" ;
const risk = Math . abs ( entryPrice - stopLoss );
const reward = isLong
? target - entryPrice
: entryPrice - target ;
return reward / risk ;
}
Example (LONG) :
Entry: $50,000
Stop: 48 , 000 ( r i s k : 48,000 (risk: 48 , 000 ( r i s k : 2,000)
Target: 56 , 000 ( r e w a r d : 56,000 (reward: 56 , 000 ( re w a r d : 6,000)
Risk-Reward: 6 , 000 / 6,000 / 6 , 000/ 2,000 = 3:1
A minimum risk-reward ratio of 2:1 or 3:1 is recommended to ensure winners outweigh losers over time.
Cooldown Periods
Cooldown periods prevent rapid re-entry into the same symbol after a stop-loss, reducing emotional trading and overtrading.
Setting Cooldowns
Cooldowns are stored in the exitPlan.cooldownUntil field when a position is closed via stop-loss.
// When closing a position via stop-loss trigger
if ( closeTrigger === "STOP" ) {
const cooldownHours = 4 ; // 4-hour cooldown after stop-loss
const cooldownUntil = new Date (
Date . now () + cooldownHours * 60 * 60 * 1000
). toISOString ();
await closeOrder ({
orderId: dbOrder . id ,
exitPrice: actualExitPrice . toString (),
realizedPnl: pnl . toString (),
closeTrigger: "STOP" ,
});
// Store cooldown in a separate tracking table or in-memory cache
await setCooldown ({
modelId: account . id ,
symbol ,
cooldownUntil ,
});
}
Enforcing Cooldowns
Check cooldown status before opening new positions.
function isInCooldown (
symbol : string ,
cooldowns : Map < string , string >,
) : boolean {
const cooldownUntil = cooldowns . get ( symbol );
if ( ! cooldownUntil ) return false ;
const cooldownExpiry = new Date ( cooldownUntil ). getTime ();
const now = Date . now ();
return now < cooldownExpiry ;
}
Cooldowns help prevent revenge trading and give time for market conditions to stabilize after a loss.
Session-Based Limits
Session-based limits prevent excessive activity within a single trading cycle.
Action Count Limits
Limit the number of actions (opens/closes) per symbol per session.
// Track per-symbol action counts for session limits
const symbolActionCounts = new Map < string , number >();
// In createPositionTool handler:
const MAX_ACTIONS_PER_SYMBOL = 2 ; // Max 2 position changes per symbol per session
for ( const request of positions ) {
const currentCount = symbolActionCounts . get ( request . symbol ) ?? 0 ;
if ( currentCount >= MAX_ACTIONS_PER_SYMBOL ) {
results . push ({
symbol: request . symbol ,
side: request . side ,
quantity: request . quantity ,
leverage: request . leverage ,
success: false ,
error: `Exceeded max ${ MAX_ACTIONS_PER_SYMBOL } actions for ${ request . symbol } this session` ,
});
continue ;
}
// Execute position creation...
// Increment action count
symbolActionCounts . set ( request . symbol , currentCount + 1 );
}
Location : src/server/features/trading/tradeExecutor.ts:110
Flip Prevention
Prevent flipping from LONG to SHORT (or vice versa) on the same symbol within a single session.
const closedPositionCooldowns = new Map <
string ,
{ side : "LONG" | "SHORT" ; cooldownUntil : string }
> ();
// After closing a position:
closedPositionCooldowns . set ( symbol , {
side: closedPosition . side ,
cooldownUntil: new Date ( Date . now () + 5 * 60 * 1000 ). toISOString (), // 5-min cooldown
});
// Before opening a new position:
const recentClose = closedPositionCooldowns . get ( request . symbol );
if ( recentClose && recentClose . side !== request . side ) {
const cooldownExpiry = new Date ( recentClose . cooldownUntil ). getTime ();
if ( Date . now () < cooldownExpiry ) {
return {
success: false ,
error: `Cannot flip from ${ recentClose . side } to ${ request . side } within 5 minutes` ,
};
}
}
Flip prevention reduces whipsaw losses from rapidly reversing positions based on short-term price movements.
Risk Metrics
Track portfolio-level risk metrics to monitor overall health.
Maximum Drawdown
Track the largest peak-to-trough decline in portfolio value.
function calculateMaxDrawdown ( portfolioHistory : number []) : number {
let peak = portfolioHistory [ 0 ];
let maxDrawdown = 0 ;
for ( const value of portfolioHistory ) {
if ( value > peak ) {
peak = value ;
}
const drawdown = ( peak - value ) / peak ;
if ( drawdown > maxDrawdown ) {
maxDrawdown = drawdown ;
}
}
return maxDrawdown * 100 ; // Return as percentage
}
Example :
Peak: $12,000
Trough: $9,000
Drawdown: (12 , 000 − 12,000 - 12 , 000 − 9,000) / $12,000 = 25%
Current Drawdown
Calculate current drawdown from recent peak.
function calculateCurrentDrawdown (
currentValue : number ,
recentPeak : number ,
) : number {
if ( currentValue >= recentPeak ) return 0 ;
return (( recentPeak - currentValue ) / recentPeak ) * 100 ;
}
If current drawdown exceeds a threshold (e.g., 20%), consider reducing position sizes or pausing trading until conditions improve.
Sharpe Ratio
Measure risk-adjusted returns.
function calculateSharpeRatio (
returns : number [],
riskFreeRate : number = 0 ,
) : number {
if ( returns . length === 0 ) return 0 ;
const avgReturn = returns . reduce (( sum , r ) => sum + r , 0 ) / returns . length ;
const variance = returns . reduce (
( sum , r ) => sum + Math . pow ( r - avgReturn , 2 ),
0 ,
) / returns . length ;
const stdDev = Math . sqrt ( variance );
if ( stdDev === 0 ) return 0 ;
return ( avgReturn - riskFreeRate ) / stdDev ;
}
Interpretation :
Sharpe > 1.0: Good risk-adjusted returns
Sharpe > 2.0: Excellent risk-adjusted returns
Sharpe < 0: Losing money on a risk-adjusted basis
Best Practices
Risk Per Trade Limit risk per trade to 1-2% of portfolio value. This ensures you can withstand multiple consecutive losses without significant drawdown.
Diversification Don’t put all capital in one symbol. Spread risk across multiple uncorrelated assets.
Stop-Loss Always Every position should have a predefined stop-loss. Never let losses run indefinitely.
Monitor Drawdown Track current and maximum drawdown. Reduce position sizes during drawdown periods.
Next Steps
Position Management Learn about position tracking and exit plans
Portfolio Analytics Explore performance metrics and analytics