Skip to main content
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

1

Position Sizing

Calculate safe position sizes based on portfolio value and risk per trade
2

Exposure Limits

Enforce maximum position sizes and concentration limits per symbol
3

Cash Validation

Verify sufficient available cash before opening positions
4

Stop-Loss Requirements

Require stop-loss levels for all positions to limit downside
5

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 / 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 / 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 + 500 + 200200 - 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(risk:48,000 (risk: 2,000)
  • Target: 56,000(reward:56,000 (reward: 6,000)
  • Risk-Reward: 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,00012,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

Build docs developers (and LLMs) love