Use when implementing any risk management logic — portfolio-level gates, per-track allocation limits, daily loss limits, drawdown controls, kill switch mechanics, VIX-based halting, macro event blocking, position sizing calculations, or the composite safety check that runs before every trade entry. Triggers on: risk management, position sizing, daily loss limit, drawdown, kill switch, max allocation, portfolio risk, vix gate, risk per trade, margin, buying power, portfolio delta, risk controls, safety check, halt trading, emergency stop, capital allocation, track allocation, trade gate.
You are the Trading Risk Guardian — the last line of defence before any capital is deployed. Your job is to prevent catastrophic losses through layered, independent risk checks. Every single trade must pass through this system. There are no exceptions.
Layer 1: Intelligence Score Gate (external intelligence service)
Layer 2: VIX Gate (market-wide volatility check)
Layer 3: Macro Calendar Gate (FOMC/CPI/NFP blocking)
Layer 4: Kill Switch (manual override — file-based)
Layer 5: Daily P&L Gate (daily loss limit)
Layer 6: Portfolio Drawdown Gate (maximum drawdown — full halt)
Layer 7: Per-Track Allocation Gate (no single strategy over-allocated)
Layer 8: Portfolio Delta Gate (directional exposure limit)
Layer 9: Position-Level Gate (per-symbol checks before entry)
Every layer is independent. Any single layer can block a trade. Failure of one layer does NOT bypass others.
| Track | Strategy | Max Allocation | Max Positions | Position Risk |
|---|---|---|---|---|
| A — Wheel | CSP + CC | 35% of account | 1 per symbol, 5 symbols | 1 contract/$2,300 BP |
| B — Strangle | Short Strangle | 45% of account | 2 simultaneous | 1-2% of account |
| C — 0DTE | Iron Butterfly | 15% of account | 1 per session | 0.5% of account |
| Fallback | Iron Condor | 10% of account | 3 max | 2% of account |
// NEVER exceed these — hard coded, not configurable
const HARD_LIMITS = {
WHEEL: 0.35, // 35% of account value
STRANGLE: 0.45, // 45% of account value
ZEDTE: 0.15, // 15% of account value
FALLBACK: 0.10, // 10% of account value
DAILY_LOSS: 0.05, // -5% in one day → halt for the day
MAX_DRAWDOWN: 0.10, // -10% from peak → full halt
} as const;
// 1 contract per $2,300 of buying power (1 contract = 100 shares)
// Never exceed 35% total Wheel allocation
function wheelContractSize(account: Account, strikePrice: number): number {
const maxAlloc = account.value * HARD_LIMITS.WHEEL;
const currentWheelAlloc = getWheelAllocation(account);
const available = maxAlloc - currentWheelAlloc;
const bpPerContract = strikePrice * 100; // 1 contract = 100 shares × strike
const maxContracts = Math.floor(available / bpPerContract);
return Math.max(0, Math.min(maxContracts, 1)); // Start with 1 contract per symbol
}
// Risk: 2% of account per strangle (3x premium stop defines max loss)
// Position size: (account * 0.02) / (3 × premium_received)
function strangleContracts(account: Account, premiumReceived: number): number {
const riskAmount = account.value * 0.02; // 2% risk
const stopLoss = premiumReceived * 3; // 3x = max loss
const contracts = Math.floor(riskAmount / (stopLoss * 100));
return Math.max(1, Math.min(contracts, 5));
}
// 0.5% of account — keep small, trade frequently
function zeroDteContracts(account: Account, premiumReceived: number): number {
const maxRisk = account.value * 0.005; // 0.5% max risk
const maxLoss = premiumReceived * 100; // 1x premium = max loss for butterfly
return Math.max(1, Math.floor(maxRisk / maxLoss));
}
interface RiskCheckResult {
canTrade: boolean;
reason?: string;
reduceSize?: boolean; // Suggests half-size if cautious but tradeable
}
export class PortfolioRisk {
async check(account: Account): Promise<RiskCheckResult> {
// Layer 4: Kill switch (fastest check first)
if (await isKillSwitchActive()) {
return { canTrade: false, reason: 'KILL_SWITCH_ACTIVE' };
}
// Layer 2: VIX gate
const vix = await getVIX();
if (vix > config.MAX_VIX_TO_TRADE) {
await sendAlert({ level: 'WARNING', subject: `VIX ${vix} — entries suspended`, throttleKey: 'vix-spike' });
return { canTrade: false, reason: `VIX_TOO_HIGH:${vix}` };
}
// Layer 5: Daily P&L gate
const todayPnL = await posRepo.getTodayRealizedPnL();
const dailyLossPct = todayPnL / account.value;
if (dailyLossPct < -HARD_LIMITS.DAILY_LOSS) {
await sendAlert({
level: 'CRITICAL',
subject: `Daily loss limit hit: ${(dailyLossPct * 100).toFixed(2)}%`,
body: `Today P&L: $${todayPnL.toFixed(0)}. All new entries halted for today.`,
throttleKey: 'daily-loss',
});
return { canTrade: false, reason: 'DAILY_LOSS_LIMIT' };
}
// Layer 6: Portfolio drawdown gate
const drawdown = await posRepo.getMaxDrawdown();
if (drawdown > HARD_LIMITS.MAX_DRAWDOWN) {
await sendAlert({
level: 'CRITICAL',
subject: `MAX DRAWDOWN EXCEEDED: ${(drawdown * 100).toFixed(2)}%`,
body: 'Full trading halt. Manual restart required via kill switch.',
throttleKey: 'drawdown',
});
return { canTrade: false, reason: 'MAX_DRAWDOWN_EXCEEDED' };
}
// Layer 7: Per-track allocation gates
const allocs = await this.getAllocations(account);
if (allocs.WHEEL >= HARD_LIMITS.WHEEL) return { canTrade: false, reason: 'WHEEL_OVER_ALLOCATED' };
if (allocs.STRANGLE >= HARD_LIMITS.STRANGLE) return { canTrade: false, reason: 'STRANGLE_OVER_ALLOCATED' };
if (allocs.ZEDTE >= HARD_LIMITS.ZEDTE) return { canTrade: false, reason: 'ZEDTE_OVER_ALLOCATED' };
// Layer 8: Portfolio delta gate (warning but tradeable)
const portfolioDelta = await this.getPortfolioDelta();
if (Math.abs(portfolioDelta) > 0.50) {
await sendAlert({ level: 'WARNING', subject: `Portfolio delta skewed: ${portfolioDelta.toFixed(2)}`, throttleKey: 'delta-skew' });
return { canTrade: true, reduceSize: true, reason: 'DELTA_ELEVATED' };
}
return { canTrade: true };
}
private async getAllocations(account: Account): Promise<Record<string, number>> {
const positions = await posRepo.getAllOpen();
const calcAlloc = (track: string) => {
const trackPositions = positions.filter(p => p.track === track);
const totalBP = trackPositions.reduce((sum, p) => sum + p.buyingPowerUsed, 0);
return totalBP / account.value;
};
return {
WHEEL: calcAlloc('WHEEL'),
STRANGLE: calcAlloc('STRANGLE'),
ZEDTE: calcAlloc('0DTE'),
FALLBACK: calcAlloc('IRON_CONDOR'),
};
}
private async getPortfolioDelta(): Promise<number> {
const positions = await posRepo.getAllOpen();
return positions.reduce((sum, p) => sum + (p.currentDelta * p.contracts * 100), 0);
}
}
// File: .bot_stopped (in working directory)
// To halt: touch .bot_stopped
// To resume: rm .bot_stopped
// Also accessible via API: POST /api/bot/stop | POST /api/bot/start
const KILL_SWITCH_PATH = path.join(process.cwd(), '.bot_stopped');
async function isKillSwitchActive(): Promise<boolean> {
try {
await fs.access(KILL_SWITCH_PATH);
return true; // File exists → bot stopped
} catch {
return false; // File absent → bot running
}
}
async function activateKillSwitch(reason: string): Promise<void> {
await fs.writeFile(KILL_SWITCH_PATH, `Stopped at ${new Date().toISOString()}: ${reason}`);
await sendAlert({ level: 'CRITICAL', subject: `Kill switch activated: ${reason}` });
logger.error('Kill switch activated', { reason });
}
| # | Check | Condition | Action | Alert Level |
|---|---|---|---|---|
| 1 | Kill switch | File .bot_stopped exists | Halt ALL trades | (none — already halted) |
| 2 | VIX gate | VIX > 35 | Skip Tracks B+C; A monitor-only | WARNING |
| 3 | Macro block | FOMC/CPI/NFP window | No new entries | WARNING |
| 4 | Daily loss | P&L < -5% today | Halt until next market open | CRITICAL |
| 5 | Max drawdown | Peak-to-trough > 10% | Full halt — manual restart | CRITICAL |
| 6 | Track A over-alloc | Wheel > 35% | No new Wheel entries | WARNING |
| 7 | Track B over-alloc | Strangle > 45% | No new strangles | WARNING |
| 8 | Track C over-alloc | 0DTE > 15% | Skip today's 0DTE | WARNING |
| 9 | Earnings proximity | ≤7 days to earnings | Skip Wheel + condor for symbol | INFO |
| 10 | Delta skew | Portfolio delta > | 0.50 | |
| 11 | Strangle stop | P&L ≤ -3x premium | Close immediately | CRITICAL |
| 12 | 0DTE noon | 12:00 PM ET | Force-close all 0DTE | INFO |
| 13 | TS API failure | 3 consecutive errors | Monitor-only mode | CRITICAL |
| 14 | OpenBao down | Can't read secrets | Alert + halt | CRITICAL |
| 15 | Fill deviation | Price > 20% from mid | Cancel, do not retry | WARNING |
portfolioRisk.check() called FIRST in every runMainCycle() before any routingas const — not in config, not from envthrottleKey to prevent email spam on repeated triggers/api/bot/stop and /api/bot/start write/delete kill switch file