feat: Implement re-entry analytics system with fresh TradingView data
- Add market data cache service (5min expiry) for storing TradingView metrics - Create /api/trading/market-data webhook endpoint for continuous data updates - Add /api/analytics/reentry-check endpoint for validating manual trades - Update execute endpoint to auto-cache metrics from incoming signals - Enhance Telegram bot with pre-execution analytics validation - Support --force flag to override analytics blocks - Use fresh ADX/ATR/RSI data when available, fallback to historical - Apply performance modifiers: -20 for losing streaks, +10 for winning - Minimum re-entry score 55 (vs 60 for new signals) - Fail-open design: proceeds if analytics unavailable - Show data freshness and source in Telegram responses - Add comprehensive setup guide in docs/guides/REENTRY_ANALYTICS_QUICKSTART.md Phase 1 implementation for smart manual trade validation.
This commit is contained in:
145
app/api/trading/market-data/route.ts
Normal file
145
app/api/trading/market-data/route.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
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> = {
|
||||
'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
|
||||
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,
|
||||
currentPrice: Number(body.currentPrice) || 0,
|
||||
timestamp: Date.now(),
|
||||
timeframe: body.timeframe || '5'
|
||||
})
|
||||
|
||||
console.log(`✅ Market data cached for ${driftSymbol}`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
symbol: driftSymbol,
|
||||
message: 'Market data cached 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user