diff --git a/app/api/trading/execute/route.ts b/app/api/trading/execute/route.ts index 4dee290..79c8786 100644 --- a/app/api/trading/execute/route.ts +++ b/app/api/trading/execute/route.ts @@ -372,6 +372,68 @@ export async function POST(request: NextRequest): Promise threshold) +- Implement pre-entry oracle validation (add 2% discrepancy check before placing order) + +## Files Modified + +- `lib/drift/orders.ts` - Added phantom detection in `openPosition()` +- `app/api/trading/execute/route.ts` - Added phantom handling after opening +- `lib/database/trades.ts` - Added phantom fields to CreateTradeParams +- `prisma/schema.prisma` - Added phantom trade fields to Trade model +- `prisma/migrations/20251104091741_add_phantom_trade_fields/` - Database migration + +## Testing + +To test phantom detection: +1. Modify `openPosition()` to simulate phantom (set actualSizeUSD = 10) +2. Send test trade signal +3. Verify: + - Error returned from execute endpoint + - Phantom trade saved to database with `isPhantom: true` + - NO position added to Position Manager + - Logs show "🚨 PHANTOM TRADE DETECTED" + +## Future Improvements + +If phantom trades remain an issue: +1. **Auto-retry with delay:** Wait 5s for oracle to catch up, retry once +2. **Oracle price validation:** Check Pyth price vs Drift oracle before placing order +3. **Volatility-based cooldown:** Longer cooldown during high ATR periods +4. **Symbol-specific thresholds:** SOL might need different validation than ETH diff --git a/lib/database/trades.ts b/lib/database/trades.ts index b82f8c6..d139c5b 100644 --- a/lib/database/trades.ts +++ b/lib/database/trades.ts @@ -52,6 +52,12 @@ export interface CreateTradeParams { volumeAtEntry?: number pricePositionAtEntry?: number signalQualityScore?: number + // Phantom trade fields + status?: string + isPhantom?: boolean + expectedSizeUSD?: number + actualSizeUSD?: number + phantomReason?: string } export interface UpdateTradeStateParams { @@ -124,7 +130,7 @@ export async function createTrade(params: CreateTradeParams) { signalSource: params.signalSource, signalStrength: params.signalStrength, timeframe: params.timeframe, - status: 'open', + status: params.status || 'open', isTestTrade: params.isTestTrade || false, // Market context expectedEntryPrice: params.expectedEntryPrice, @@ -136,6 +142,11 @@ export async function createTrade(params: CreateTradeParams) { volumeAtEntry: params.volumeAtEntry, pricePositionAtEntry: params.pricePositionAtEntry, signalQualityScore: params.signalQualityScore, + // Phantom trade fields + isPhantom: params.isPhantom || false, + expectedSizeUSD: params.expectedSizeUSD, + actualSizeUSD: params.actualSizeUSD, + phantomReason: params.phantomReason, }, }) diff --git a/lib/drift/orders.ts b/lib/drift/orders.ts index 702f25a..782636c 100644 --- a/lib/drift/orders.ts +++ b/lib/drift/orders.ts @@ -29,6 +29,8 @@ export interface OpenPositionResult { fillSize?: number slippage?: number error?: string + isPhantom?: boolean // Position opened but size mismatch detected + actualSizeUSD?: number // Actual position size if different from requested } export interface ClosePositionParams { @@ -179,16 +181,37 @@ export async function openPosition( const fillPrice = position.entryPrice 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 = actualSizeUSD / expectedSizeUSD + console.log(`💰 Fill details:`) console.log(` Fill price: $${fillPrice.toFixed(4)}`) console.log(` Slippage: ${slippage.toFixed(3)}%`) + console.log(` Expected size: $${expectedSizeUSD.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 + const isPhantom = sizeRatio < 0.5 + + if (isPhantom) { + console.error(`🚨 PHANTOM POSITION DETECTED!`) + console.error(` Expected: $${expectedSizeUSD.toFixed(2)}`) + console.error(` Actual: $${actualSizeUSD.toFixed(2)}`) + console.error(` This indicates the order was rejected or partially filled by Drift`) + } return { success: true, transactionSignature: txSig, fillPrice, - fillSize: baseAssetSize, + fillSize: position.size, // Use actual size from Drift, not calculated slippage, + isPhantom, + actualSizeUSD, } } else { // Position not found yet (may be DRY_RUN mode) diff --git a/prisma/migrations/20251104091741_add_phantom_trade_fields/migration.sql b/prisma/migrations/20251104091741_add_phantom_trade_fields/migration.sql new file mode 100644 index 0000000..24a4bc7 --- /dev/null +++ b/prisma/migrations/20251104091741_add_phantom_trade_fields/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Trade" ADD COLUMN "actualSizeUSD" DOUBLE PRECISION, +ADD COLUMN "expectedSizeUSD" DOUBLE PRECISION, +ADD COLUMN "isPhantom" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "phantomReason" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0bc6655..4802665 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -103,9 +103,15 @@ model Trade { timeframe String? // "5", "15", "60" // Status - status String @default("open") // "open", "closed", "failed" + status String @default("open") // "open", "closed", "failed", "phantom" isTestTrade Boolean @default(false) // Flag test trades for exclusion from analytics + // Phantom trade detection + isPhantom Boolean @default(false) // Position opened but size mismatch >50% + expectedSizeUSD Float? // Expected position size (when phantom) + actualSizeUSD Float? // Actual position size from Drift (when phantom) + phantomReason String? // "ORACLE_PRICE_MISMATCH", "PARTIAL_FILL", "ORDER_REJECTED" + // Relations priceUpdates PriceUpdate[]