feat: phantom trade auto-closure system

- Auto-close phantom positions immediately via market order
- Return HTTP 200 (not 500) to allow n8n workflow continuation
- Save phantom trades to database with full P&L tracking
- Exit reason: 'manual' category for phantom auto-closes
- Protects user during unavailable hours (sleeping, no phone)
- Add Docker build best practices to instructions (background + tail)
- Document phantom system as Critical Component #1
- Add Common Pitfall #30: Phantom notification workflow

Why auto-close:
- User can't always respond to phantom alerts
- Unmonitored position = unlimited risk exposure
- Better to exit with small loss/gain than leave exposed
- Re-entry possible if setup actually good

Files changed:
- app/api/trading/execute/route.ts: Auto-close logic
- .github/copilot-instructions.md: Documentation + build pattern
This commit is contained in:
mindesbunister
2025-11-14 05:37:51 +01:00
parent 4ad509928f
commit 6590f4fb1e
7 changed files with 383 additions and 17 deletions

View File

@@ -7,7 +7,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { initializeDriftService } from '@/lib/drift/client'
import { openPosition, placeExitOrders } from '@/lib/drift/orders'
import { openPosition, placeExitOrders, closePosition } from '@/lib/drift/orders'
import { normalizeTradingViewSymbol } from '@/config/trading'
import { getMergedConfig } from '@/config/trading'
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
@@ -320,11 +320,42 @@ 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(`🚨 PHANTOM TRADE DETECTED - Auto-closing for safety`)
console.error(` Expected: $${positionSizeUSD.toFixed(2)}`)
console.error(` Actual: $${openResult.actualSizeUSD?.toFixed(2)}`)
// IMMEDIATELY close the phantom position (safety first)
let closeResult
let closedAtPrice = openResult.fillPrice!
let closePnL = 0
try {
console.log(`⚠️ Closing phantom position immediately for safety...`)
closeResult = await closePosition({
symbol: driftSymbol,
percentToClose: 100, // Close 100% of whatever size exists
slippageTolerance: config.slippageTolerance,
})
if (closeResult.success) {
closedAtPrice = closeResult.closePrice || openResult.fillPrice!
// Calculate P&L (usually small loss/gain)
const priceChange = body.direction === 'long'
? ((closedAtPrice - openResult.fillPrice!) / openResult.fillPrice!)
: ((openResult.fillPrice! - closedAtPrice) / openResult.fillPrice!)
closePnL = (openResult.actualSizeUSD || 0) * priceChange
console.log(`✅ Phantom position closed at $${closedAtPrice.toFixed(2)}`)
console.log(`💰 Phantom P&L: $${closePnL.toFixed(2)}`)
} else {
console.error(`❌ Failed to close phantom position: ${closeResult.error}`)
}
} catch (closeError) {
console.error(`❌ Error closing phantom position:`, closeError)
}
// Save phantom trade to database for analysis
let phantomTradeId: string | undefined
try {
const qualityResult = scoreSignalQuality({
atr: body.atr || 0,
@@ -336,14 +367,15 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
timeframe: body.timeframe,
})
await createTrade({
// Create trade record (without exit info initially)
const trade = await createTrade({
positionId: openResult.transactionSignature!,
symbol: driftSymbol,
direction: body.direction,
entryPrice: openResult.fillPrice!,
positionSizeUSD: positionSizeUSD,
leverage: leverage, // Use actual symbol-specific leverage
stopLossPrice: 0, // Not applicable for phantom
positionSizeUSD: openResult.actualSizeUSD || positionSizeUSD,
leverage: leverage,
stopLossPrice: 0,
takeProfit1Price: 0,
takeProfit2Price: 0,
tp1SizePercent: 0,
@@ -358,27 +390,72 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
volumeAtEntry: body.volumeRatio,
pricePositionAtEntry: body.pricePosition,
signalQualityScore: qualityResult.score,
indicatorVersion: body.indicatorVersion || 'v5', // Default to v5 for backward compatibility
// Phantom-specific fields
indicatorVersion: body.indicatorVersion || 'v5',
status: 'phantom',
isPhantom: true,
expectedSizeUSD: positionSizeUSD,
actualSizeUSD: openResult.actualSizeUSD,
phantomReason: 'ORACLE_PRICE_MISMATCH', // Likely cause based on logs
phantomReason: 'ORACLE_PRICE_MISMATCH',
})
phantomTradeId = trade.id
console.log(`💾 Phantom trade saved to database for analysis`)
// If close succeeded, update with exit info
if (closeResult?.success) {
await updateTradeExit({
positionId: openResult.transactionSignature!,
exitPrice: closedAtPrice,
exitReason: 'manual', // Phantom auto-close (manual category)
realizedPnL: closePnL,
exitOrderTx: closeResult.transactionSignature || 'PHANTOM_CLOSE',
holdTimeSeconds: 0, // Phantom trades close immediately
maxDrawdown: Math.abs(Math.min(0, closePnL)),
maxGain: Math.max(0, closePnL),
maxFavorableExcursion: Math.max(0, closePnL),
maxAdverseExcursion: Math.min(0, closePnL),
})
console.log(`💾 Phantom exit info updated in database`)
}
} catch (dbError) {
console.error('❌ Failed to save phantom trade:', dbError)
}
// Prepare notification message for n8n to send via Telegram
const phantomNotification =
`⚠️ PHANTOM TRADE AUTO-CLOSED\n\n` +
`Symbol: ${driftSymbol}\n` +
`Direction: ${body.direction.toUpperCase()}\n` +
`Expected Size: $${positionSizeUSD.toFixed(2)}\n` +
`Actual Size: $${(openResult.actualSizeUSD || 0).toFixed(2)} (${((openResult.actualSizeUSD || 0) / positionSizeUSD * 100).toFixed(1)}%)\n\n` +
`Entry: $${openResult.fillPrice!.toFixed(2)}\n` +
`Exit: $${closedAtPrice.toFixed(2)}\n` +
`P&L: $${closePnL.toFixed(2)}\n\n` +
`Reason: Size mismatch detected - likely oracle price issue or exchange rejection\n` +
`Action: Position auto-closed for safety (unmonitored positions = risk)\n\n` +
`TX: ${openResult.transactionSignature?.slice(0, 20)}...`
console.log(`📱 Phantom notification prepared:`, phantomNotification)
// Return HTTP 200 with warning (not 500) so n8n workflow continues to notification
return NextResponse.json(
{
success: false,
error: 'Phantom trade detected',
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.`,
success: true, // Changed from false - position was handled safely
warning: 'Phantom trade detected and auto-closed',
isPhantom: true,
message: phantomNotification, // Full notification message for n8n
phantomDetails: {
expectedSize: positionSizeUSD,
actualSize: openResult.actualSizeUSD,
sizeRatio: (openResult.actualSizeUSD || 0) / positionSizeUSD,
autoClosed: closeResult?.success || false,
pnl: closePnL,
entryTx: openResult.transactionSignature,
exitTx: closeResult?.transactionSignature,
}
},
{ status: 500 }
{ status: 200 } // Changed from 500 - allows n8n to continue
)
}