feat: implement per-symbol cooldown period
CRITICAL: Cooldown was global across ALL symbols, causing missed opportunities Example: ETH trade at 10:00 blocked SOL trade at 10:04 (5min cooldown) Changes: - Added getLastTradeTimeForSymbol() function to query last trade per symbol - Updated check-risk endpoint to use symbol-specific cooldown - Each coin (SOL/ETH/BTC) now has independent cooldown timer - Cooldown message shows symbol: 'Must wait X min before next SOL-PERP trade' Result: Can trade ETH and SOL simultaneously without interference Example: ETH LONG at 10:00, SOL SHORT at 10:01 = both allowed
This commit is contained in:
@@ -8,7 +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'
|
import { getLastTradeTime, getLastTradeTimeForSymbol, getTradesInLastHour, getTodayPnL } from '@/lib/database/trades'
|
||||||
|
|
||||||
export interface RiskCheckRequest {
|
export interface RiskCheckRequest {
|
||||||
symbol: string
|
symbol: string
|
||||||
@@ -118,18 +118,18 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Check cooldown period
|
// 3. Check cooldown period PER SYMBOL (not global)
|
||||||
const lastTradeTime = await getLastTradeTime()
|
const lastTradeTimeForSymbol = await getLastTradeTimeForSymbol(body.symbol)
|
||||||
if (lastTradeTime && config.minTimeBetweenTrades > 0) {
|
if (lastTradeTimeForSymbol && config.minTimeBetweenTrades > 0) {
|
||||||
const timeSinceLastTrade = Date.now() - lastTradeTime.getTime()
|
const timeSinceLastTrade = Date.now() - lastTradeTimeForSymbol.getTime()
|
||||||
const cooldownMs = config.minTimeBetweenTrades * 60 * 1000 // Convert minutes to milliseconds
|
const cooldownMs = config.minTimeBetweenTrades * 60 * 1000 // Convert minutes to milliseconds
|
||||||
|
|
||||||
if (timeSinceLastTrade < cooldownMs) {
|
if (timeSinceLastTrade < cooldownMs) {
|
||||||
const remainingMs = cooldownMs - timeSinceLastTrade
|
const remainingMs = cooldownMs - timeSinceLastTrade
|
||||||
const remainingMinutes = Math.ceil(remainingMs / 60000)
|
const remainingMinutes = Math.ceil(remainingMs / 60000)
|
||||||
|
|
||||||
console.log('🚫 Risk check BLOCKED: Cooldown period active', {
|
console.log('🚫 Risk check BLOCKED: Cooldown period active for', body.symbol, {
|
||||||
lastTradeTime: lastTradeTime.toISOString(),
|
lastTradeTime: lastTradeTimeForSymbol.toISOString(),
|
||||||
timeSinceLastTradeMs: timeSinceLastTrade,
|
timeSinceLastTradeMs: timeSinceLastTrade,
|
||||||
cooldownMs,
|
cooldownMs,
|
||||||
remainingMinutes,
|
remainingMinutes,
|
||||||
@@ -138,7 +138,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
allowed: false,
|
allowed: false,
|
||||||
reason: 'Cooldown period',
|
reason: 'Cooldown period',
|
||||||
details: `Must wait ${remainingMinutes} more minute(s) before next trade (cooldown: ${config.minTimeBetweenTrades} min)`,
|
details: `Must wait ${remainingMinutes} more minute(s) before next ${body.symbol} trade (cooldown: ${config.minTimeBetweenTrades} min)`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
|||||||
console.log(`✅ Risk check PASSED: All checks passed`, {
|
console.log(`✅ Risk check PASSED: All checks passed`, {
|
||||||
todayPnL: todayPnL.toFixed(2),
|
todayPnL: todayPnL.toFixed(2),
|
||||||
tradesLastHour: tradesInLastHour,
|
tradesLastHour: tradesInLastHour,
|
||||||
cooldownPassed: lastTradeTime ? 'yes' : 'no previous trades',
|
cooldownPassed: lastTradeTimeForSymbol ? 'yes' : `no previous ${body.symbol} trades`,
|
||||||
qualityScore: qualityScore.score,
|
qualityScore: qualityScore.score,
|
||||||
qualityReasons: qualityScore.reasons
|
qualityReasons: qualityScore.reasons
|
||||||
})
|
})
|
||||||
@@ -191,7 +191,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
|||||||
console.log(`✅ Risk check PASSED: All checks passed`, {
|
console.log(`✅ Risk check PASSED: All checks passed`, {
|
||||||
todayPnL: todayPnL.toFixed(2),
|
todayPnL: todayPnL.toFixed(2),
|
||||||
tradesLastHour: tradesInLastHour,
|
tradesLastHour: tradesInLastHour,
|
||||||
cooldownPassed: lastTradeTime ? 'yes' : 'no previous trades',
|
cooldownPassed: lastTradeTimeForSymbol ? 'yes' : `no previous ${body.symbol} trades`,
|
||||||
})
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
|
|||||||
@@ -276,6 +276,26 @@ export async function getLastTradeTime(): Promise<Date | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the most recent trade time for a specific symbol
|
||||||
|
*/
|
||||||
|
export async function getLastTradeTimeForSymbol(symbol: string): Promise<Date | null> {
|
||||||
|
const prisma = getPrismaClient()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lastTrade = await prisma.trade.findFirst({
|
||||||
|
where: { symbol },
|
||||||
|
orderBy: { entryTime: 'desc' },
|
||||||
|
select: { entryTime: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
return lastTrade?.entryTime || null
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Failed to get last trade time for ${symbol}:`, error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the most recent trade with full details
|
* Get the most recent trade with full details
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user