Files
trading_bot_v4/app/api/trading/market-data/route.ts
mindesbunister d637aac2d7 feat: Deploy HA auto-failover with database promotion
- Enhanced DNS failover monitor on secondary (72.62.39.24)
- Auto-promotes database: pg_ctl promote on failover
- Creates DEMOTED flag on primary via SSH (split-brain protection)
- Telegram notifications with database promotion status
- Startup safety script ready (integration pending)
- 90-second automatic recovery vs 10-30 min manual
- Zero-cost 95% enterprise HA benefit

Status: DEPLOYED and MONITORING (14:52 CET)
Next: Controlled failover test during maintenance
2025-12-12 15:54:03 +01:00

195 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextRequest, NextResponse } from 'next/server'
import { getMarketDataCache } from '@/lib/trading/market-data-cache'
/**
* Market Data Webhook Endpoint
*
* Receives real-time metrics from TradingView alerts.
* Called every 1-5 minutes per symbol to keep cache fresh.
*
* TradingView Alert Message (JSON):
* {
* "action": "market_data",
* "symbol": "{{ticker}}",
* "timeframe": "{{interval}}",
* "atr": {{ta.atr(14)}},
* "adx": {{ta.dmi(14, 14)}},
* "rsi": {{ta.rsi(14)}},
* "volumeRatio": {{volume / ta.sma(volume, 20)}},
* "pricePosition": {{(close - ta.lowest(low, 100)) / (ta.highest(high, 100) - ta.lowest(low, 100)) * 100}},
* "currentPrice": {{close}},
* "timestamp": {{timenow}}
* }
*
* Webhook URL: https://your-domain.com/api/trading/market-data
*/
/**
* Normalize TradingView symbol format to Drift format
*/
function normalizeTradingViewSymbol(tvSymbol: string): string {
if (tvSymbol.includes('-PERP')) return tvSymbol
const symbolMap: Record<string, string> = {
'FARTCOINUSDT': 'FARTCOIN-PERP',
'FARTCOINUSD': 'FARTCOIN-PERP',
'FARTCOIN': 'FARTCOIN-PERP',
'FARTUSDT': 'FARTCOIN-PERP',
'FART': 'FARTCOIN-PERP',
'SOLUSDT': 'SOL-PERP',
'SOLUSDT.P': 'SOL-PERP',
'SOLUSD': 'SOL-PERP',
'SOL': 'SOL-PERP',
'ETHUSDT': 'ETH-PERP',
'ETHUSDT.P': 'ETH-PERP',
'ETHUSD': 'ETH-PERP',
'ETH': 'ETH-PERP',
'BTCUSDT': 'BTC-PERP',
'BTCUSDT.P': 'BTC-PERP',
'BTCUSD': 'BTC-PERP',
'BTC': 'BTC-PERP'
}
return symbolMap[tvSymbol.toUpperCase()] || `${tvSymbol.toUpperCase()}-PERP`
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
console.log('📡 Received market data webhook:', {
action: body.action,
symbol: body.symbol,
atr: body.atr,
adx: body.adx
})
// Validate it's a market data update (accept both variations)
const validActions = ['market_data', 'market_data_1min']
if (!validActions.includes(body.action)) {
console.log(`❌ Invalid action: ${body.action} (expected ${validActions.join(' or ')})`)
return NextResponse.json(
{ error: `Invalid action - expected ${validActions.join(' or ')}` },
{ status: 400 }
)
}
// Validate required fields
if (!body.symbol) {
return NextResponse.json(
{ error: 'Missing symbol' },
{ status: 400 }
)
}
const driftSymbol = normalizeTradingViewSymbol(body.symbol)
// Parse timestamp defensively fall back to now if malformed to avoid dropping data
const parsedTimestamp = body.timestamp ? new Date(body.timestamp) : new Date()
const timestamp = Number.isNaN(parsedTimestamp.getTime()) ? new Date() : parsedTimestamp
if (body.timestamp && Number.isNaN(parsedTimestamp.getTime())) {
console.warn('⚠️ Invalid timestamp in market data payload, falling back to now', {
symbol: driftSymbol,
provided: body.timestamp
})
}
// Store in cache for immediate use
const marketCache = getMarketDataCache()
marketCache.set(driftSymbol, {
symbol: driftSymbol,
atr: Number(body.atr) || 0,
adx: Number(body.adx) || 0,
rsi: Number(body.rsi) || 50,
volumeRatio: Number(body.volumeRatio) || 1.0,
pricePosition: Number(body.pricePosition) || 50,
maGap: Number(body.maGap) || undefined,
currentPrice: Number(body.currentPrice) || 0,
timestamp: Date.now(),
timeframe: body.timeframe || '5'
})
// CRITICAL (Dec 2, 2025): Store ALL 1-minute data in database for historical analysis
// User directive: "we want to store the data for 4 weeks"
// Purpose: Enable granular 8-hour analysis of blocked signals with full indicator data
try {
const { getPrismaClient } = await import('@/lib/database/trades')
const prisma = getPrismaClient()
await prisma.marketData.create({
data: {
symbol: driftSymbol,
timeframe: body.timeframe || '1',
price: Number(body.currentPrice) || 0,
atr: Number(body.atr) || 0,
adx: Number(body.adx) || 0,
rsi: Number(body.rsi) || 50,
volumeRatio: Number(body.volumeRatio) || 1.0,
pricePosition: Number(body.pricePosition) || 50,
maGap: Number(body.maGap) || undefined,
volume: Number(body.volume) || undefined,
timestamp
}
})
console.log(`💾 Stored 1-minute data in database for ${driftSymbol}`)
} catch (dbError) {
console.error('❌ Failed to store market data in database:', dbError)
// Don't fail the request if database save fails - cache still works
}
console.log(`✅ Market data cached for ${driftSymbol}`)
return NextResponse.json({
success: true,
symbol: driftSymbol,
message: 'Market data cached and stored successfully',
expiresInSeconds: 300
})
} catch (error) {
console.error('❌ Market data webhook error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
/**
* GET endpoint to view currently cached data (for debugging)
*/
export async function GET(request: NextRequest) {
try {
const marketCache = getMarketDataCache()
const availableSymbols = marketCache.getAvailableSymbols()
const cacheData: Record<string, any> = {}
for (const symbol of availableSymbols) {
const data = marketCache.get(symbol)
if (data) {
const ageSeconds = marketCache.getDataAge(symbol)
cacheData[symbol] = {
...data,
ageSeconds
}
}
}
return NextResponse.json({
success: true,
availableSymbols,
count: availableSymbols.length,
cache: cacheData
})
} catch (error) {
console.error('❌ Market data GET error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}