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:
@@ -8,6 +8,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { getMergedConfig } from '@/config/trading'
|
import { getMergedConfig } from '@/config/trading'
|
||||||
import { getInitializedPositionManager } from '@/lib/trading/position-manager'
|
import { getInitializedPositionManager } from '@/lib/trading/position-manager'
|
||||||
|
import { getLastTradeTime, getTradesInLastHour, getTodayPnL } from '@/lib/database/trades'
|
||||||
|
|
||||||
export interface RiskCheckRequest {
|
export interface RiskCheckRequest {
|
||||||
symbol: string
|
symbol: string
|
||||||
@@ -79,13 +80,66 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement additional risk checks:
|
// 1. Check daily drawdown limit
|
||||||
// 1. Check daily drawdown
|
const todayPnL = await getTodayPnL()
|
||||||
// 2. Check trades per hour limit
|
if (todayPnL < config.maxDailyDrawdown) {
|
||||||
// 3. Check cooldown period
|
console.log('🚫 Risk check BLOCKED: Daily drawdown limit reached', {
|
||||||
// 4. Check account health
|
todayPnL: todayPnL.toFixed(2),
|
||||||
|
maxDrawdown: config.maxDailyDrawdown,
|
||||||
|
})
|
||||||
|
|
||||||
console.log(`✅ Risk check PASSED: No existing positions`)
|
return NextResponse.json({
|
||||||
|
allowed: false,
|
||||||
|
reason: 'Daily drawdown limit',
|
||||||
|
details: `Today's P&L ($${todayPnL.toFixed(2)}) has reached max drawdown limit ($${config.maxDailyDrawdown})`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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({
|
return NextResponse.json({
|
||||||
allowed: true,
|
allowed: true,
|
||||||
|
|||||||
@@ -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)
|
* Add price update for a trade (for tracking max gain/drawdown)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user