From 7c4adff4e4a757337bb8f7c113c9bc6dd33a3b76 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Thu, 30 Oct 2025 10:50:08 +0100 Subject: [PATCH] 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. --- app/api/trading/check-risk/route.ts | 66 +++++++++++++++++++++++--- lib/database/trades.ts | 72 +++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 6 deletions(-) diff --git a/app/api/trading/check-risk/route.ts b/app/api/trading/check-risk/route.ts index 0c3d85d..aaadaab 100644 --- a/app/api/trading/check-risk/route.ts +++ b/app/api/trading/check-risk/route.ts @@ -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= 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, diff --git a/lib/database/trades.ts b/lib/database/trades.ts index 2ef5416..e06184f 100644 --- a/lib/database/trades.ts +++ b/lib/database/trades.ts @@ -251,6 +251,78 @@ export async function getOpenTrades() { } } +/** + * Get the most recent trade entry time (for cooldown checking) + */ +export async function getLastTradeTime(): Promise { + 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 { + 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 { + 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) */