- 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
195 lines
5.9 KiB
TypeScript
195 lines
5.9 KiB
TypeScript
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 }
|
||
)
|
||
}
|
||
}
|