Implement risk checks: cooldown, hourly limit, and daily drawdown

Implemented 3 critical risk checks in /api/trading/check-risk:

1. Daily Drawdown Check
   - Blocks trades if today's P&L < maxDailyDrawdown
   - Prevents catastrophic daily losses
   - Currently: -0 limit (configurable via MAX_DAILY_DRAWDOWN)

2. Hourly Trade Limit
   - Blocks trades if tradesInLastHour >= maxTradesPerHour
   - Prevents overtrading / algorithm malfunction
   - Currently: 20 trades/hour (configurable via MAX_TRADES_PER_HOUR)

3. Cooldown Period
   - Blocks trades if timeSinceLastTrade < minTimeBetweenTrades
   - Enforces breathing room between trades
   - Uses minutes (not seconds) thanks to previous commit
   - Currently: 0 min = disabled (configurable via MIN_TIME_BETWEEN_TRADES)

Added database helper functions:
- getLastTradeTime() - Returns timestamp of most recent trade
- getTradesInLastHour() - Counts trades in last 60 minutes
- getTodayPnL() - Sums realized P&L since midnight

All checks include detailed logging with values and thresholds.
Risk check called by n8n workflow before every trade execution.
This commit is contained in:
mindesbunister
2025-10-30 10:50:08 +01:00
parent b7b0fb9bb2
commit 7c4adff4e4
2 changed files with 132 additions and 6 deletions

View File

@@ -8,6 +8,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { getMergedConfig } from '@/config/trading'
import { getInitializedPositionManager } from '@/lib/trading/position-manager'
import { getLastTradeTime, getTradesInLastHour, getTodayPnL } from '@/lib/database/trades'
export interface RiskCheckRequest {
symbol: string
@@ -79,13 +80,66 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
})
}
// TODO: Implement additional risk checks:
// 1. Check daily drawdown
// 2. Check trades per hour limit
// 3. Check cooldown period
// 4. Check account health
// 1. Check daily drawdown limit
const todayPnL = await getTodayPnL()
if (todayPnL < config.maxDailyDrawdown) {
console.log('🚫 Risk check BLOCKED: Daily drawdown limit reached', {
todayPnL: todayPnL.toFixed(2),
maxDrawdown: config.maxDailyDrawdown,
})
return NextResponse.json({
allowed: false,
reason: 'Daily drawdown limit',
details: `Today's P&L ($${todayPnL.toFixed(2)}) has reached max drawdown limit ($${config.maxDailyDrawdown})`,
})
}
console.log(`✅ Risk check PASSED: No existing positions`)
// 2. Check trades per hour limit
const tradesInLastHour = await getTradesInLastHour()
if (tradesInLastHour >= config.maxTradesPerHour) {
console.log('🚫 Risk check BLOCKED: Hourly trade limit reached', {
tradesInLastHour,
maxTradesPerHour: config.maxTradesPerHour,
})
return NextResponse.json({
allowed: false,
reason: 'Hourly trade limit',
details: `Already placed ${tradesInLastHour} trades in the last hour (max: ${config.maxTradesPerHour})`,
})
}
// 3. Check cooldown period
const lastTradeTime = await getLastTradeTime()
if (lastTradeTime && config.minTimeBetweenTrades > 0) {
const timeSinceLastTrade = Date.now() - lastTradeTime.getTime()
const cooldownMs = config.minTimeBetweenTrades * 60 * 1000 // Convert minutes to milliseconds
if (timeSinceLastTrade < cooldownMs) {
const remainingMs = cooldownMs - timeSinceLastTrade
const remainingMinutes = Math.ceil(remainingMs / 60000)
console.log('🚫 Risk check BLOCKED: Cooldown period active', {
lastTradeTime: lastTradeTime.toISOString(),
timeSinceLastTradeMs: timeSinceLastTrade,
cooldownMs,
remainingMinutes,
})
return NextResponse.json({
allowed: false,
reason: 'Cooldown period',
details: `Must wait ${remainingMinutes} more minute(s) before next trade (cooldown: ${config.minTimeBetweenTrades} min)`,
})
}
}
console.log(`✅ Risk check PASSED: All checks passed`, {
todayPnL: todayPnL.toFixed(2),
tradesLastHour: tradesInLastHour,
cooldownPassed: lastTradeTime ? 'yes' : 'no previous trades',
})
return NextResponse.json({
allowed: true,

View File

@@ -251,6 +251,78 @@ export async function getOpenTrades() {
}
}
/**
* Get the most recent trade entry time (for cooldown checking)
*/
export async function getLastTradeTime(): Promise<Date | null> {
const prisma = getPrismaClient()
try {
const lastTrade = await prisma.trade.findFirst({
orderBy: { entryTime: 'desc' },
select: { entryTime: true },
})
return lastTrade?.entryTime || null
} catch (error) {
console.error('❌ Failed to get last trade time:', error)
return null
}
}
/**
* Get count of trades in the last hour
*/
export async function getTradesInLastHour(): Promise<number> {
const prisma = getPrismaClient()
try {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000)
const count = await prisma.trade.count({
where: {
entryTime: {
gte: oneHourAgo,
},
},
})
return count
} catch (error) {
console.error('❌ Failed to get trades in last hour:', error)
return 0
}
}
/**
* Get total P&L for today
*/
export async function getTodayPnL(): Promise<number> {
const prisma = getPrismaClient()
try {
const startOfDay = new Date()
startOfDay.setHours(0, 0, 0, 0)
const result = await prisma.trade.aggregate({
where: {
entryTime: {
gte: startOfDay,
},
status: 'closed',
},
_sum: {
realizedPnL: true,
},
})
return result._sum.realizedPnL || 0
} catch (error) {
console.error('❌ Failed to get today PnL:', error)
return 0
}
}
/**
* Add price update for a trade (for tracking max gain/drawdown)
*/