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

@@ -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`)