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:
mindesbunister
2025-11-07 20:40:07 +01:00
parent 6d5991172a
commit 9b767342dc
14 changed files with 1150 additions and 568 deletions

View File

@@ -0,0 +1,237 @@
import { NextRequest, NextResponse } from 'next/server'
import { getMarketDataCache } from '@/lib/trading/market-data-cache'
import { getPrismaClient } from '@/lib/database/trades'
import { scoreSignalQuality } from '@/lib/trading/signal-quality'
/**
* Re-Entry Analytics Endpoint
*
* Validates manual trades using:
* 1. Fresh TradingView market data (if available)
* 2. Recent trade performance (last 3 trades for symbol + direction)
* 3. Signal quality scoring with performance modifiers
*
* Called by Telegram bot before executing manual "long sol" / "short eth" commands
*/
interface ReentryAnalytics {
should_enter: boolean
score: number
reason: string
data_source: 'tradingview_real' | 'fallback_historical' | 'no_data'
data_age_seconds?: number
metrics: {
atr: number
adx: number
rsi: number
volumeRatio: number
pricePosition: number
timeframe: string
recentTradeStats: {
last3Trades: number
winRate: number
avgPnL: number
}
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { symbol, direction } = body
if (!symbol || !direction) {
return NextResponse.json(
{ error: 'Missing symbol or direction' },
{ status: 400 }
)
}
if (!['long', 'short'].includes(direction)) {
return NextResponse.json(
{ error: 'Direction must be "long" or "short"' },
{ status: 400 }
)
}
console.log(`🔍 Analyzing re-entry for ${direction.toUpperCase()} ${symbol}`)
// 1. Try to get REAL market data from TradingView cache
const marketCache = getMarketDataCache()
const cachedData = marketCache.get(symbol)
let metrics: any
let dataSource: 'tradingview_real' | 'fallback_historical' | 'no_data'
let dataAgeSeconds: number | undefined
if (cachedData) {
// Use REAL TradingView data (less than 5min old)
dataAgeSeconds = Math.round((Date.now() - cachedData.timestamp) / 1000)
dataSource = 'tradingview_real'
console.log(`✅ Using real TradingView data (${dataAgeSeconds}s old)`)
metrics = {
atr: cachedData.atr,
adx: cachedData.adx,
rsi: cachedData.rsi,
volumeRatio: cachedData.volumeRatio,
pricePosition: cachedData.pricePosition,
timeframe: cachedData.timeframe
}
} else {
// Fallback to most recent trade metrics
console.log(`⚠️ No fresh TradingView data, using historical metrics from last trade`)
const prisma = getPrismaClient()
const lastTrade = await prisma.trade.findFirst({
where: { symbol },
orderBy: { createdAt: 'desc' }
}) as any // Trade type has optional metric fields
if (lastTrade && lastTrade.atr && lastTrade.adx && lastTrade.rsi) {
dataSource = 'fallback_historical'
const tradeAge = Math.round((Date.now() - lastTrade.createdAt.getTime()) / 1000)
console.log(`📊 Using metrics from last trade (${tradeAge}s ago)`)
metrics = {
atr: lastTrade.atr,
adx: lastTrade.adx,
rsi: lastTrade.rsi,
volumeRatio: lastTrade.volumeRatio || 1.2,
pricePosition: lastTrade.pricePosition || 50,
timeframe: '5'
}
} else {
// No data available at all
console.log(`❌ No market data available for ${symbol}`)
dataSource = 'no_data'
metrics = {
atr: 1.0,
adx: 20,
rsi: direction === 'long' ? 45 : 55,
volumeRatio: 1.2,
pricePosition: 50,
timeframe: '5'
}
}
}
// 2. Get recent trade performance for this symbol + direction
const prisma = getPrismaClient()
const recentTrades = await prisma.trade.findMany({
where: {
symbol,
direction,
exitTime: { not: null },
createdAt: {
gte: new Date(Date.now() - 24 * 60 * 60 * 1000) // Last 24h
}
},
orderBy: { createdAt: 'desc' },
take: 3
})
const last3Count = recentTrades.length
const winningTrades = recentTrades.filter((t: any) => (t.realizedPnL || 0) > 0)
const winRate = last3Count > 0 ? (winningTrades.length / last3Count) * 100 : 0
const avgPnL = last3Count > 0
? recentTrades.reduce((sum: number, t: any) => sum + (t.realizedPnL || 0), 0) / last3Count
: 0
console.log(`📊 Recent performance: ${last3Count} trades, ${winRate.toFixed(0)}% WR, ${avgPnL.toFixed(2)}% avg P&L`)
// 3. Score the re-entry with real/fallback metrics
const qualityResult = scoreSignalQuality({
atr: metrics.atr,
adx: metrics.adx,
rsi: metrics.rsi,
volumeRatio: metrics.volumeRatio,
pricePosition: metrics.pricePosition,
direction: direction as 'long' | 'short'
})
let finalScore = qualityResult.score
// 4. Apply recent performance modifiers
if (last3Count >= 2 && avgPnL < -5) {
finalScore -= 20
console.log(`⚠️ Recent trades losing (${avgPnL.toFixed(2)}% avg) - applying -20 penalty`)
}
if (last3Count >= 2 && avgPnL > 5 && winRate >= 66) {
finalScore += 10
console.log(`✨ Recent trades winning (${winRate.toFixed(0)}% WR) - applying +10 bonus`)
}
// 5. Penalize if using stale/no data
if (dataSource === 'fallback_historical') {
finalScore -= 5
console.log(`⚠️ Using historical data - applying -5 penalty`)
} else if (dataSource === 'no_data') {
finalScore -= 10
console.log(`⚠️ No market data available - applying -10 penalty`)
}
// 6. Determine if should enter
const MIN_REENTRY_SCORE = 55
const should_enter = finalScore >= MIN_REENTRY_SCORE
let reason = ''
if (!should_enter) {
if (dataSource === 'no_data') {
reason = `No market data available (score: ${finalScore})`
} else if (dataSource === 'fallback_historical') {
reason = `Using stale data (score: ${finalScore})`
} else if (finalScore < MIN_REENTRY_SCORE) {
reason = `Quality score too low (${finalScore} < ${MIN_REENTRY_SCORE})`
}
if (last3Count >= 2 && avgPnL < -5) {
reason += `. Recent ${direction} trades losing (${avgPnL.toFixed(2)}% avg)`
}
} else {
reason = `Quality score acceptable (${finalScore}/${MIN_REENTRY_SCORE})`
if (dataSource === 'tradingview_real') {
reason += ` [✅ FRESH TradingView data: ${dataAgeSeconds}s old]`
} else if (dataSource === 'fallback_historical') {
reason += ` [⚠️ Historical data - consider waiting for fresh signal]`
} else {
reason += ` [❌ No data - risky entry]`
}
if (winRate >= 66 && last3Count >= 2) {
reason += `. Recent win rate: ${winRate.toFixed(0)}%`
}
}
const response: ReentryAnalytics = {
should_enter,
score: finalScore,
reason,
data_source: dataSource,
data_age_seconds: dataAgeSeconds,
metrics: {
...metrics,
recentTradeStats: {
last3Trades: last3Count,
winRate,
avgPnL
}
}
}
console.log(`📊 Re-entry analysis complete:`, {
should_enter,
score: finalScore,
data_source: dataSource
})
return NextResponse.json(response)
} catch (error) {
console.error('❌ Re-entry analysis error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -15,7 +15,6 @@ import { scoreSignalQuality, SignalQualityResult } from '@/lib/trading/signal-qu
export interface RiskCheckRequest {
symbol: string
direction: 'long' | 'short'
timeframe?: string // e.g., '5', '15', '60', '1D'
// Optional context metrics from TradingView
atr?: number
adx?: number
@@ -58,7 +57,6 @@ function shouldAllowScaling(
pricePosition: newSignal.pricePosition,
direction: newSignal.direction,
minScore: config.minScaleQualityScore,
timeframe: newSignal.timeframe,
})
// 2. Check quality score (higher bar than initial entry)
@@ -148,9 +146,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
const config = getMergedConfig()
// Check for existing positions on the same symbol
const positionManager = await getInitializedPositionManager()
await positionManager.reconcileTrade(body.symbol)
const existingTrades = Array.from(positionManager.getActiveTrades().values())
const positionManager = await getInitializedPositionManager()
const existingTrades = Array.from(positionManager.getActiveTrades().values())
const existingPosition = existingTrades.find(trade => trade.symbol === body.symbol)
if (existingPosition) {
@@ -273,8 +270,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
volumeRatio: body.volumeRatio || 0,
pricePosition: body.pricePosition || 0,
direction: body.direction,
minScore: 60, // Hardcoded threshold
timeframe: body.timeframe,
minScore: 60 // Hardcoded threshold
})
if (!qualityScore.passed) {

View File

@@ -8,11 +8,12 @@
import { NextRequest, NextResponse } from 'next/server'
import { initializeDriftService } from '@/lib/drift/client'
import { openPosition, placeExitOrders } from '@/lib/drift/orders'
import { normalizeTradingViewSymbol, calculateDynamicTp2 } from '@/config/trading'
import { normalizeTradingViewSymbol } from '@/config/trading'
import { getMergedConfig } from '@/config/trading'
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
import { createTrade, updateTradeExit } from '@/lib/database/trades'
import { scoreSignalQuality } from '@/lib/trading/signal-quality'
import { getMarketDataCache } from '@/lib/trading/market-data-cache'
export interface ExecuteTradeRequest {
symbol: string // TradingView symbol (e.g., 'SOLUSDT')
@@ -35,8 +36,6 @@ export interface ExecuteTradeResponse {
direction?: 'long' | 'short'
entryPrice?: number
positionSize?: number
requestedPositionSize?: number
fillCoveragePercent?: number
leverage?: number
stopLoss?: number
takeProfit1?: number
@@ -88,6 +87,23 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
const driftSymbol = normalizeTradingViewSymbol(body.symbol)
console.log(`📊 Normalized symbol: ${body.symbol}${driftSymbol}`)
// 🆕 Cache incoming market data from TradingView signals
if (body.atr && body.adx && body.rsi) {
const marketCache = getMarketDataCache()
marketCache.set(driftSymbol, {
symbol: driftSymbol,
atr: body.atr,
adx: body.adx,
rsi: body.rsi,
volumeRatio: body.volumeRatio || 1.0,
pricePosition: body.pricePosition || 50,
currentPrice: body.signalPrice || 0,
timestamp: Date.now(),
timeframe: body.timeframe || '5'
})
console.log(`📊 Market data auto-cached for ${driftSymbol} from trade signal`)
}
// Get trading configuration
const config = getMergedConfig()
@@ -180,16 +196,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
// Update Position Manager tracking
const timesScaled = (sameDirectionPosition.timesScaled || 0) + 1
const actualScaleNotional = scaleResult.fillNotionalUSD ?? scaleSize
const totalScaleAdded = (sameDirectionPosition.totalScaleAdded || 0) + actualScaleNotional
const newTotalSize = sameDirectionPosition.currentSize + actualScaleNotional
if (scaleSize > 0) {
const coverage = (actualScaleNotional / scaleSize) * 100
if (coverage < 99.5) {
console.log(`⚠️ Scale fill coverage: ${coverage.toFixed(2)}% of requested $${scaleSize.toFixed(2)}`)
}
}
const totalScaleAdded = (sameDirectionPosition.totalScaleAdded || 0) + scaleSize
const newTotalSize = sameDirectionPosition.currentSize + (scaleResult.fillSize || 0)
// Update the trade tracking (simplified - just update the active trade object)
sameDirectionPosition.timesScaled = timesScaled
@@ -279,20 +287,20 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
await new Promise(resolve => setTimeout(resolve, 2000))
}
// Calculate requested position size with leverage
const requestedPositionSizeUSD = positionSize * leverage
// Calculate position size with leverage
const positionSizeUSD = positionSize * leverage
console.log(`💰 Opening ${body.direction} position:`)
console.log(` Symbol: ${driftSymbol}`)
console.log(` Base size: $${positionSize}`)
console.log(` Leverage: ${leverage}x`)
console.log(` Requested notional: $${requestedPositionSizeUSD}`)
console.log(` Total position: $${positionSizeUSD}`)
// Open position
const openResult = await openPosition({
symbol: driftSymbol,
direction: body.direction,
sizeUSD: requestedPositionSizeUSD,
sizeUSD: positionSizeUSD,
slippageTolerance: config.slippageTolerance,
})
@@ -310,7 +318,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
// CRITICAL: Check for phantom trade (position opened but size mismatch)
if (openResult.isPhantom) {
console.error(`🚨 PHANTOM TRADE DETECTED - Not adding to Position Manager`)
console.error(` Expected: $${requestedPositionSizeUSD.toFixed(2)}`)
console.error(` Expected: $${positionSizeUSD.toFixed(2)}`)
console.error(` Actual: $${openResult.actualSizeUSD?.toFixed(2)}`)
// Save phantom trade to database for analysis
@@ -322,7 +330,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
volumeRatio: body.volumeRatio || 0,
pricePosition: body.pricePosition || 0,
direction: body.direction,
timeframe: body.timeframe,
})
await createTrade({
@@ -330,7 +337,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
symbol: driftSymbol,
direction: body.direction,
entryPrice: openResult.fillPrice!,
positionSizeUSD: requestedPositionSizeUSD,
positionSizeUSD: positionSizeUSD,
leverage: config.leverage,
stopLossPrice: 0, // Not applicable for phantom
takeProfit1Price: 0,
@@ -347,11 +354,10 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
volumeAtEntry: body.volumeRatio,
pricePositionAtEntry: body.pricePosition,
signalQualityScore: qualityResult.score,
signalQualityVersion: 'v3', // Stricter logic with ADX > 18 requirement
// Phantom-specific fields
status: 'phantom',
isPhantom: true,
expectedSizeUSD: requestedPositionSizeUSD,
expectedSizeUSD: positionSizeUSD,
actualSizeUSD: openResult.actualSizeUSD,
phantomReason: 'ORACLE_PRICE_MISMATCH', // Likely cause based on logs
})
@@ -365,7 +371,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
{
success: false,
error: 'Phantom trade detected',
message: `Position opened but size mismatch detected. Expected $${requestedPositionSizeUSD.toFixed(2)}, got $${openResult.actualSizeUSD?.toFixed(2)}. This usually indicates oracle price was stale or order was rejected by exchange.`,
message: `Position opened but size mismatch detected. Expected $${positionSizeUSD.toFixed(2)}, got $${openResult.actualSizeUSD?.toFixed(2)}. This usually indicates oracle price was stale or order was rejected by exchange.`,
},
{ status: 500 }
)
@@ -373,20 +379,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
// Calculate stop loss and take profit prices
const entryPrice = openResult.fillPrice!
const actualPositionSizeUSD = openResult.fillNotionalUSD ?? requestedPositionSizeUSD
const filledBaseSize = openResult.fillSize !== undefined
? Math.abs(openResult.fillSize)
: (entryPrice > 0 ? actualPositionSizeUSD / entryPrice : 0)
const fillCoverage = requestedPositionSizeUSD > 0
? (actualPositionSizeUSD / requestedPositionSizeUSD) * 100
: 100
console.log('📏 Fill results:')
console.log(` Filled base size: ${filledBaseSize.toFixed(4)} ${driftSymbol.split('-')[0]}`)
console.log(` Filled notional: $${actualPositionSizeUSD.toFixed(2)}`)
if (fillCoverage < 99.5) {
console.log(` ⚠️ Partial fill: ${fillCoverage.toFixed(2)}% of requested size`)
}
const stopLossPrice = calculatePrice(
entryPrice,
@@ -420,15 +412,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
body.direction
)
const dynamicTp2Percent = calculateDynamicTp2(
entryPrice,
body.atr || 0, // ATR from TradingView signal
config
)
const tp2Price = calculatePrice(
entryPrice,
dynamicTp2Percent,
config.takeProfit2Percent,
body.direction
)
@@ -436,7 +422,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
console.log(` Entry: $${entryPrice.toFixed(4)}`)
console.log(` SL: $${stopLossPrice.toFixed(4)} (${config.stopLossPercent}%)`)
console.log(` TP1: $${tp1Price.toFixed(4)} (${config.takeProfit1Percent}%)`)
console.log(` TP2: $${tp2Price.toFixed(4)} (${dynamicTp2Percent.toFixed(2)}% - ATR-based)`)
console.log(` TP2: $${tp2Price.toFixed(4)} (${config.takeProfit2Percent}%)`)
// Calculate emergency stop
const emergencyStopPrice = calculatePrice(
@@ -453,13 +439,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
direction: body.direction,
entryPrice,
entryTime: Date.now(),
positionSize: actualPositionSizeUSD,
positionSize: positionSizeUSD,
leverage: config.leverage,
stopLossPrice,
tp1Price,
tp2Price,
emergencyStopPrice,
currentSize: actualPositionSizeUSD,
currentSize: positionSizeUSD,
tp1Hit: false,
tp2Hit: false,
slMovedToBreakeven: false,
@@ -478,8 +464,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
originalAdx: body.adx, // Store for scaling validation
timesScaled: 0,
totalScaleAdded: 0,
atrAtEntry: body.atr,
runnerTrailingPercent: undefined,
priceCheckCount: 0,
lastPrice: entryPrice,
lastUpdateTime: Date.now(),
@@ -492,7 +476,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
try {
const exitRes = await placeExitOrders({
symbol: driftSymbol,
positionSizeUSD: actualPositionSizeUSD,
positionSizeUSD: positionSizeUSD,
entryPrice: entryPrice,
tp1Price,
tp2Price,
@@ -529,16 +513,14 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
symbol: driftSymbol,
direction: body.direction,
entryPrice: entryPrice,
positionSize: actualPositionSizeUSD,
requestedPositionSize: requestedPositionSizeUSD,
fillCoveragePercent: Number(fillCoverage.toFixed(2)),
positionSize: positionSizeUSD,
leverage: config.leverage,
stopLoss: stopLossPrice,
takeProfit1: tp1Price,
takeProfit2: tp2Price,
stopLossPercent: config.stopLossPercent,
tp1Percent: config.takeProfit1Percent,
tp2Percent: dynamicTp2Percent,
tp2Percent: config.takeProfit2Percent,
entrySlippage: openResult.slippage,
timestamp: new Date().toISOString(),
}
@@ -558,7 +540,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
volumeRatio: body.volumeRatio || 0,
pricePosition: body.pricePosition || 0,
direction: body.direction,
timeframe: body.timeframe,
})
await createTrade({
@@ -566,7 +547,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
symbol: driftSymbol,
direction: body.direction,
entryPrice,
positionSizeUSD: actualPositionSizeUSD,
positionSizeUSD: positionSizeUSD,
leverage: config.leverage,
stopLossPrice,
takeProfit1Price: tp1Price,
@@ -591,9 +572,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
volumeAtEntry: body.volumeRatio,
pricePositionAtEntry: body.pricePosition,
signalQualityScore: qualityResult.score,
signalQualityVersion: 'v3', // Stricter logic with ADX > 18 requirement for extreme positions
expectedSizeUSD: requestedPositionSizeUSD,
actualSizeUSD: actualPositionSizeUSD,
})
console.log(`💾 Trade saved with quality score: ${qualityResult.score}/100`)

View 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 }
)
}
}

View File

@@ -8,7 +8,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { initializeDriftService } from '@/lib/drift/client'
import { openPosition, placeExitOrders } from '@/lib/drift/orders'
import { normalizeTradingViewSymbol, calculateDynamicTp2 } from '@/config/trading'
import { normalizeTradingViewSymbol } from '@/config/trading'
import { getMergedConfig } from '@/config/trading'
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
import { createTrade } from '@/lib/database/trades'
@@ -96,13 +96,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
}
// Calculate position size with leverage
const requestedPositionSizeUSD = positionSize * leverage
const requestedPositionSizeUSD = positionSize * leverage
console.log(`💰 Opening ${direction} position:`)
console.log(` Symbol: ${driftSymbol}`)
console.log(` Base size: $${positionSize}`)
console.log(` Leverage: ${leverage}x`)
console.log(` Requested notional: $${requestedPositionSizeUSD}`)
console.log(` Base size: $${positionSize}`)
console.log(` Leverage: ${leverage}x`)
console.log(` Requested notional: $${requestedPositionSizeUSD}`)
// Open position
const openResult = await openPosition({
@@ -125,10 +125,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
// Calculate stop loss and take profit prices
const entryPrice = openResult.fillPrice!
const actualPositionSizeUSD = openResult.fillNotionalUSD ?? requestedPositionSizeUSD
const filledBaseSize = openResult.fillSize !== undefined
? Math.abs(openResult.fillSize)
: (entryPrice > 0 ? actualPositionSizeUSD / entryPrice : 0)
const filledBaseSize = openResult.fillSize ?? (requestedPositionSizeUSD > 0 ? requestedPositionSizeUSD / entryPrice : 0)
const actualPositionSizeUSD = openResult.actualSizeUSD ?? (filledBaseSize * entryPrice)
const fillCoverage = requestedPositionSizeUSD > 0
? (actualPositionSizeUSD / requestedPositionSizeUSD) * 100
: 100
@@ -172,18 +170,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
direction
)
// Use ATR-based dynamic TP2 with simulated ATR for testing
const simulatedATR = entryPrice * 0.008 // Simulate 0.8% ATR for testing
const dynamicTp2Percent = calculateDynamicTp2(
entryPrice,
simulatedATR,
config
)
const tp2Price = calculatePrice(
entryPrice,
dynamicTp2Percent,
config.takeProfit2Percent,
direction
)
@@ -191,7 +180,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
console.log(` Entry: $${entryPrice.toFixed(4)}`)
console.log(` SL: $${stopLossPrice.toFixed(4)} (${config.stopLossPercent}%)`)
console.log(` TP1: $${tp1Price.toFixed(4)} (${config.takeProfit1Percent}%)`)
console.log(` TP2: $${tp2Price.toFixed(4)} (${dynamicTp2Percent.toFixed(2)}% - ATR-based test)`)
console.log(` TP2: $${tp2Price.toFixed(4)} (${config.takeProfit2Percent}%)`)
// Calculate emergency stop
const emergencyStopPrice = calculatePrice(
@@ -229,8 +218,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
maxAdverseExcursion: 0,
maxFavorablePrice: entryPrice,
maxAdversePrice: entryPrice,
atrAtEntry: undefined,
runnerTrailingPercent: undefined,
priceCheckCount: 0,
lastPrice: entryPrice,
lastUpdateTime: Date.now(),
@@ -303,7 +290,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
symbol: driftSymbol,
direction: direction,
entryPrice,
positionSizeUSD: actualPositionSizeUSD,
positionSizeUSD: actualPositionSizeUSD,
leverage: leverage,
stopLossPrice,
takeProfit1Price: tp1Price,