Files
trading_bot_v4/app/api/trading/market-data/route.ts
mindesbunister 267f7943df fix: FARTCOIN symbol normalization priority
- Problem: FARTCOIN signals being treated as SOL-PERP
- Root cause: Symbol normalization checked includes('SOL') before FARTCOIN
- Since TradingView may send symbols with 'SOL' in name, order matters

Files changed:
- config/trading.ts: Reordered checks (FARTCOIN before SOL)
- app/api/trading/market-data/route.ts: Added FARTCOIN mappings

Symbol matching now checks:
1. FARTCOIN/FART (most specific)
2. SOL (catch-all for Solana)
3. BTC, ETH (other majors)
4. Default fallback

This fixes TradingView alerts for FARTCOIN 5-min and 1-min data
collection being incorrectly stored as SOL-PERP in BlockedSignal table.

Status:  DEPLOYED Dec 7, 2025 19:30 CET
Next FARTCOIN signal will correctly save as FARTCOIN-PERP
2025-12-07 19:45:24 +01:00

181 lines
5.3 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',
'SOLUSD': 'SOL-PERP',
'SOL': 'SOL-PERP',
'ETHUSDT': 'ETH-PERP',
'ETHUSD': 'ETH-PERP',
'ETH': 'ETH-PERP',
'BTCUSDT': '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
if (body.action !== 'market_data') {
console.log(`❌ Invalid action: ${body.action} (expected "market_data")`)
return NextResponse.json(
{ error: 'Invalid action - expected "market_data"' },
{ status: 400 }
)
}
// Validate required fields
if (!body.symbol) {
return NextResponse.json(
{ error: 'Missing symbol' },
{ status: 400 }
)
}
const driftSymbol = normalizeTradingViewSymbol(body.symbol)
// 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: new Date(body.timestamp || Date.now())
}
})
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 }
)
}
}