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:
@@ -27,7 +27,6 @@ export interface OpenPositionResult {
|
||||
transactionSignature?: string
|
||||
fillPrice?: number
|
||||
fillSize?: number
|
||||
fillNotionalUSD?: number
|
||||
slippage?: number
|
||||
error?: string
|
||||
isPhantom?: boolean // Position opened but size mismatch detected
|
||||
@@ -46,8 +45,6 @@ export interface ClosePositionResult {
|
||||
closePrice?: number
|
||||
closedSize?: number
|
||||
realizedPnL?: number
|
||||
fullyClosed?: boolean
|
||||
remainingSize?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
@@ -127,7 +124,6 @@ export async function openPosition(
|
||||
transactionSignature: mockTxSig,
|
||||
fillPrice: oraclePrice,
|
||||
fillSize: baseAssetSize,
|
||||
fillNotionalUSD: baseAssetSize * oraclePrice,
|
||||
slippage: 0,
|
||||
}
|
||||
}
|
||||
@@ -183,22 +179,19 @@ export async function openPosition(
|
||||
|
||||
if (position && position.side !== 'none') {
|
||||
const fillPrice = position.entryPrice
|
||||
const filledBaseSize = Math.abs(position.size)
|
||||
const fillNotionalUSD = filledBaseSize * fillPrice
|
||||
const slippage = Math.abs((fillPrice - oraclePrice) / oraclePrice) * 100
|
||||
|
||||
// CRITICAL: Validate actual position size vs expected
|
||||
// Phantom trade detection: Check if position is significantly smaller than expected
|
||||
const actualSizeUSD = position.size * fillPrice
|
||||
const expectedSizeUSD = params.sizeUSD
|
||||
const sizeRatio = expectedSizeUSD > 0 ? fillNotionalUSD / expectedSizeUSD : 1
|
||||
const sizeRatio = actualSizeUSD / expectedSizeUSD
|
||||
|
||||
console.log(`💰 Fill details:`)
|
||||
console.log(` Fill price: $${fillPrice.toFixed(4)}`)
|
||||
console.log(` Filled base size: ${filledBaseSize.toFixed(4)} ${params.symbol.split('-')[0]}`)
|
||||
console.log(` Filled notional: $${fillNotionalUSD.toFixed(2)}`)
|
||||
console.log(` Slippage: ${slippage.toFixed(3)}%`)
|
||||
console.log(` Expected size: $${expectedSizeUSD.toFixed(2)}`)
|
||||
console.log(` Actual size: $${fillNotionalUSD.toFixed(2)}`)
|
||||
console.log(` Actual size: $${actualSizeUSD.toFixed(2)}`)
|
||||
console.log(` Size ratio: ${(sizeRatio * 100).toFixed(1)}%`)
|
||||
|
||||
// Flag as phantom if actual size is less than 50% of expected
|
||||
@@ -207,7 +200,7 @@ export async function openPosition(
|
||||
if (isPhantom) {
|
||||
console.error(`🚨 PHANTOM POSITION DETECTED!`)
|
||||
console.error(` Expected: $${expectedSizeUSD.toFixed(2)}`)
|
||||
console.error(` Actual: $${fillNotionalUSD.toFixed(2)}`)
|
||||
console.error(` Actual: $${actualSizeUSD.toFixed(2)}`)
|
||||
console.error(` This indicates the order was rejected or partially filled by Drift`)
|
||||
}
|
||||
|
||||
@@ -215,11 +208,10 @@ export async function openPosition(
|
||||
success: true,
|
||||
transactionSignature: txSig,
|
||||
fillPrice,
|
||||
fillSize: filledBaseSize,
|
||||
fillNotionalUSD,
|
||||
fillSize: position.size, // Use actual size from Drift, not calculated
|
||||
slippage,
|
||||
isPhantom,
|
||||
actualSizeUSD: fillNotionalUSD,
|
||||
actualSizeUSD,
|
||||
}
|
||||
} else {
|
||||
// Position not found yet (may be DRY_RUN mode)
|
||||
@@ -231,7 +223,6 @@ export async function openPosition(
|
||||
transactionSignature: txSig,
|
||||
fillPrice: oraclePrice,
|
||||
fillSize: baseAssetSize,
|
||||
fillNotionalUSD: baseAssetSize * oraclePrice,
|
||||
slippage: 0,
|
||||
}
|
||||
}
|
||||
@@ -500,24 +491,19 @@ export async function closePosition(
|
||||
}
|
||||
|
||||
// Calculate size to close
|
||||
const sizeToClose = position.size * (params.percentToClose / 100)
|
||||
const remainingSize = position.size - sizeToClose
|
||||
let sizeToClose = position.size * (params.percentToClose / 100)
|
||||
|
||||
// CRITICAL: Check if remaining position would be below Drift minimum
|
||||
// If so, Drift will force-close the entire position anyway
|
||||
// Better to detect this upfront and return fullyClosed=true
|
||||
const willForceFullClose = remainingSize > 0 && remainingSize < marketConfig.minOrderSize
|
||||
|
||||
if (willForceFullClose && params.percentToClose < 100) {
|
||||
console.log(`⚠️ WARNING: Remaining size ${remainingSize.toFixed(4)} would be below Drift minimum ${marketConfig.minOrderSize}`)
|
||||
console.log(` Drift will force-close entire position. Proceeding with 100% close.`)
|
||||
console.log(` 💡 TIP: Increase position size or decrease TP2 close % to enable runner`)
|
||||
// CRITICAL FIX: If calculated size is below minimum, close 100% instead
|
||||
// This prevents "runner" positions from being too small to close
|
||||
if (sizeToClose < marketConfig.minOrderSize) {
|
||||
console.log(`⚠️ Calculated close size ${sizeToClose.toFixed(4)} is below minimum ${marketConfig.minOrderSize}`)
|
||||
console.log(` Forcing 100% close to avoid Drift rejection`)
|
||||
sizeToClose = position.size // Close entire position
|
||||
}
|
||||
|
||||
console.log(`📝 Close order details:`)
|
||||
console.log(` Current position: ${position.size.toFixed(4)} ${position.side}`)
|
||||
console.log(` Closing: ${params.percentToClose}% (${sizeToClose.toFixed(4)})`)
|
||||
console.log(` Remaining after close: ${remainingSize.toFixed(4)}`)
|
||||
console.log(` Entry price: $${position.entryPrice.toFixed(4)}`)
|
||||
console.log(` Unrealized P&L: $${position.unrealizedPnL.toFixed(2)}`)
|
||||
|
||||
@@ -532,18 +518,10 @@ export async function closePosition(
|
||||
console.log('🧪 DRY RUN MODE: Simulating close order (not executing on blockchain)')
|
||||
|
||||
// Calculate realized P&L with leverage (default 10x in dry run)
|
||||
// For LONG: profit when exit > entry → (exit - entry) / entry
|
||||
// For SHORT: profit when exit < entry → (entry - exit) / entry
|
||||
const priceDiff = position.side === 'long'
|
||||
? (oraclePrice - position.entryPrice) // Long: profit when price rises
|
||||
: (position.entryPrice - oraclePrice) // Short: profit when price falls
|
||||
|
||||
const profitPercent = (priceDiff / position.entryPrice) * 100
|
||||
const profitPercent = ((oraclePrice - position.entryPrice) / position.entryPrice) * 100 * (position.side === 'long' ? 1 : -1)
|
||||
const closedNotional = sizeToClose * oraclePrice
|
||||
const leverage = 10
|
||||
const collateral = closedNotional / leverage
|
||||
const realizedPnL = collateral * (profitPercent / 100) * leverage
|
||||
const accountPnLPercent = profitPercent * leverage
|
||||
const realizedPnL = (closedNotional * profitPercent) / 100
|
||||
const accountPnLPercent = profitPercent * 10 // display using default leverage
|
||||
|
||||
const mockTxSig = `DRY_RUN_CLOSE_${Date.now()}_${Math.random().toString(36).substring(7)}`
|
||||
|
||||
@@ -591,13 +569,8 @@ export async function closePosition(
|
||||
console.log('✅ Transaction confirmed on-chain')
|
||||
|
||||
// Calculate realized P&L with leverage
|
||||
// For LONG: profit when exit > entry → (exit - entry) / entry
|
||||
// For SHORT: profit when exit < entry → (entry - exit) / entry
|
||||
const priceDiff = position.side === 'long'
|
||||
? (oraclePrice - position.entryPrice) // Long: profit when price rises
|
||||
: (position.entryPrice - oraclePrice) // Short: profit when price falls
|
||||
|
||||
const profitPercent = (priceDiff / position.entryPrice) * 100
|
||||
// CRITICAL: P&L must account for leverage and be calculated on USD notional, not base asset size
|
||||
const profitPercent = ((oraclePrice - position.entryPrice) / position.entryPrice) * 100 * (position.side === 'long' ? 1 : -1)
|
||||
|
||||
// Get leverage from user account (defaults to 10x if not found)
|
||||
let leverage = 10
|
||||
@@ -611,10 +584,9 @@ export async function closePosition(
|
||||
console.log('⚠️ Could not determine leverage from account, using 10x default')
|
||||
}
|
||||
|
||||
// Calculate closed notional value (USD) and actual P&L with leverage
|
||||
// Calculate closed notional value (USD)
|
||||
const closedNotional = sizeToClose * oraclePrice
|
||||
const collateral = closedNotional / leverage
|
||||
const realizedPnL = collateral * (profitPercent / 100) * leverage // Leveraged P&L
|
||||
const realizedPnL = (closedNotional * profitPercent) / 100
|
||||
const accountPnLPercent = profitPercent * leverage
|
||||
|
||||
console.log(`💰 Close details:`)
|
||||
@@ -623,21 +595,13 @@ export async function closePosition(
|
||||
console.log(` Closed notional: $${closedNotional.toFixed(2)}`)
|
||||
console.log(` Realized P&L: $${realizedPnL.toFixed(2)}`)
|
||||
|
||||
// Check remaining position size after close
|
||||
const updatedPosition = await driftService.getPosition(marketConfig.driftMarketIndex)
|
||||
const actualRemainingSize = updatedPosition ? Math.abs(updatedPosition.size) : 0
|
||||
const fullyClosed = !updatedPosition || actualRemainingSize === 0 || willForceFullClose
|
||||
|
||||
if (fullyClosed) {
|
||||
// If closing 100%, cancel all remaining orders for this market
|
||||
if (params.percentToClose === 100) {
|
||||
console.log('🗑️ Position fully closed, cancelling remaining orders...')
|
||||
const cancelResult = await cancelAllOrders(params.symbol)
|
||||
if (cancelResult.success && (cancelResult.cancelledCount || 0) > 0) {
|
||||
if (cancelResult.success && cancelResult.cancelledCount! > 0) {
|
||||
console.log(`✅ Cancelled ${cancelResult.cancelledCount} orders`)
|
||||
}
|
||||
} else if (params.percentToClose === 100) {
|
||||
console.log(
|
||||
`⚠️ Requested 100% close but ${actualRemainingSize.toFixed(4)} base remains on-chain`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -646,8 +610,6 @@ export async function closePosition(
|
||||
closePrice: oraclePrice,
|
||||
closedSize: sizeToClose,
|
||||
realizedPnL,
|
||||
fullyClosed,
|
||||
remainingSize: actualRemainingSize,
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user