Files
trading_bot_v4/app/api/trading/test/route.ts
mindesbunister 988fdb9ea4 Fix runner system + strengthen anti-chop filter
Three critical bugs fixed:
1. P&L calculation (65x inflation) - now uses collateralUSD not notional
2. handlePostTp1Adjustments() - checks tp2SizePercent===0 for runner mode
3. JavaScript || operator bug - changed to ?? for proper 0 handling

Signal quality improvements:
- Added anti-chop filter: price position <40% + ADX <25 = -25 points
- Prevents range-bound flip-flops (caught all 3 today)
- Backtest: 43.8% → 55.6% win rate, +86% profit per trade

Changes:
- lib/trading/signal-quality.ts: RANGE-BOUND CHOP penalty
- lib/drift/orders.ts: Fixed P&L calculation + transaction confirmation
- lib/trading/position-manager.ts: Runner system logic
- app/api/trading/execute/route.ts: || to ?? for tp2SizePercent
- app/api/trading/test/route.ts: || to ?? for tp1/tp2SizePercent
- prisma/schema.prisma: Added collateralUSD field
- scripts/fix_pnl_calculations.sql: Historical P&L correction
2025-11-10 15:36:51 +01:00

372 lines
12 KiB
TypeScript

/**
* Test Trade API Endpoint
*
* Executes a test trade with current settings (no authentication required from settings page)
* POST /api/trading/test
*/
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 { getMergedConfig } from '@/config/trading'
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
import { createTrade } from '@/lib/database/trades'
export interface TestTradeRequest {
symbol?: string // Default: SOLUSDT
direction?: 'long' | 'short' // Default: long
}
export interface TestTradeResponse {
success: boolean
positionId?: string
symbol?: string
direction?: 'long' | 'short'
entryPrice?: number
positionSize?: number
requestedPositionSize?: number
fillCoveragePercent?: number
stopLoss?: number
takeProfit1?: number
takeProfit2?: number
softStopPrice?: number
hardStopPrice?: number
useDualStops?: boolean
timestamp?: string
error?: string
message?: string
}
export async function POST(request: NextRequest): Promise<NextResponse<TestTradeResponse>> {
try {
// Parse request body
const body: TestTradeRequest = await request.json().catch(() => ({}))
const symbol = body.symbol || 'SOLUSDT'
const direction = body.direction || 'long'
console.log('🧪 Test trade request:', { symbol, direction })
// Normalize symbol
const driftSymbol = normalizeTradingViewSymbol(symbol)
console.log(`📊 Normalized symbol: ${symbol}${driftSymbol}`)
// Get trading configuration
const config = getMergedConfig()
// Initialize Drift service to get account balance
const driftService = await initializeDriftService()
// Check account health before trading
const health = await driftService.getAccountHealth()
console.log('💊 Account health:', health)
if (health.freeCollateral <= 0) {
return NextResponse.json(
{
success: false,
error: 'Insufficient collateral',
message: `Free collateral: $${health.freeCollateral.toFixed(2)}`,
},
{ status: 400 }
)
}
// Get symbol-specific position sizing (with percentage support)
const { getActualPositionSizeForSymbol } = await import('@/config/trading')
const { size: positionSize, leverage, enabled, usePercentage } = await getActualPositionSizeForSymbol(
driftSymbol,
config,
health.freeCollateral
)
// Check if trading is enabled for this symbol
if (!enabled) {
console.log(`⛔ Trading disabled for ${driftSymbol}`)
return NextResponse.json(
{
success: false,
error: 'Symbol trading disabled',
message: `Trading is currently disabled for ${driftSymbol}. Enable it in settings.`,
},
{ status: 400 }
)
}
console.log(`📐 Symbol-specific sizing for ${driftSymbol}:`)
console.log(` Enabled: ${enabled}`)
console.log(` Position size: $${positionSize}`)
console.log(` Leverage: ${leverage}x`)
console.log(` Using percentage: ${usePercentage}`)
console.log(` Free collateral: $${health.freeCollateral.toFixed(2)}`)
// Calculate position size with 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}`)
// Open position
const openResult = await openPosition({
symbol: driftSymbol,
direction: direction,
sizeUSD: requestedPositionSizeUSD,
slippageTolerance: config.slippageTolerance,
})
if (!openResult.success) {
return NextResponse.json(
{
success: false,
error: 'Position open failed',
message: openResult.error,
},
{ status: 500 }
)
}
// Calculate stop loss and take profit prices
const entryPrice = openResult.fillPrice!
const actualPositionSizeUSD = openResult.actualSizeUSD ?? 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,
config.stopLossPercent,
direction
)
// Calculate dual stop prices if enabled
let softStopPrice: number | undefined
let hardStopPrice: number | undefined
if (config.useDualStops) {
softStopPrice = calculatePrice(
entryPrice,
config.softStopPercent,
direction
)
hardStopPrice = calculatePrice(
entryPrice,
config.hardStopPercent,
direction
)
console.log('🛡️🛡️ Dual stop system enabled:')
console.log(` Soft stop: $${softStopPrice.toFixed(4)} (${config.softStopPercent}%)`)
console.log(` Hard stop: $${hardStopPrice.toFixed(4)} (${config.hardStopPercent}%)`)
}
const tp1Price = calculatePrice(
entryPrice,
config.takeProfit1Percent,
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,
direction
)
console.log('📊 Trade targets:')
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)`)
// Calculate emergency stop
const emergencyStopPrice = calculatePrice(
entryPrice,
config.emergencyStopPercent,
direction
)
// Create active trade object
const activeTrade: ActiveTrade = {
id: `test-trade-${Date.now()}`,
positionId: openResult.transactionSignature!,
symbol: driftSymbol,
direction: direction,
entryPrice,
entryTime: Date.now(),
positionSize: actualPositionSizeUSD,
leverage: leverage,
stopLossPrice,
tp1Price,
tp2Price,
emergencyStopPrice,
currentSize: actualPositionSizeUSD,
tp1Hit: false,
tp2Hit: false,
slMovedToBreakeven: false,
slMovedToProfit: false,
trailingStopActive: false,
realizedPnL: 0,
unrealizedPnL: 0,
peakPnL: 0,
peakPrice: entryPrice,
// MAE/MFE tracking
maxFavorableExcursion: 0,
maxAdverseExcursion: 0,
maxFavorablePrice: entryPrice,
maxAdversePrice: entryPrice,
atrAtEntry: undefined,
runnerTrailingPercent: undefined,
priceCheckCount: 0,
lastPrice: entryPrice,
lastUpdateTime: Date.now(),
}
// Add to position manager for monitoring
const positionManager = await getInitializedPositionManager()
await positionManager.addTrade(activeTrade)
console.log('✅ Trade added to position manager for monitoring')
// Create response object
const response: TestTradeResponse = {
success: true,
positionId: openResult.transactionSignature,
symbol: driftSymbol,
direction: direction,
entryPrice: entryPrice,
positionSize: actualPositionSizeUSD,
requestedPositionSize: requestedPositionSizeUSD,
fillCoveragePercent: Number(fillCoverage.toFixed(2)),
stopLoss: stopLossPrice,
takeProfit1: tp1Price,
takeProfit2: tp2Price,
softStopPrice: softStopPrice,
hardStopPrice: hardStopPrice,
useDualStops: config.useDualStops,
timestamp: new Date().toISOString(),
}
// Place on-chain TP/SL orders so they appear in Drift UI
let exitOrderSignatures: string[] = []
try {
const exitRes = await placeExitOrders({
symbol: driftSymbol,
positionSizeUSD: actualPositionSizeUSD,
entryPrice: entryPrice,
tp1Price,
tp2Price,
stopLossPrice,
tp1SizePercent: config.takeProfit1SizePercent ?? 50,
tp2SizePercent: config.takeProfit2SizePercent ?? 100, // Use ?? instead of || to allow 0
direction: direction,
// Dual stop parameters
useDualStops: config.useDualStops,
softStopPrice: softStopPrice,
softStopBuffer: config.softStopBuffer,
hardStopPrice: hardStopPrice,
})
if (!exitRes.success) {
console.error('❌ Failed to place on-chain exit orders:', exitRes.error)
} else {
console.log('📨 Exit orders placed on-chain:', exitRes.signatures)
exitOrderSignatures = exitRes.signatures || []
}
// Attach signatures to response when available
if (exitRes.signatures && exitRes.signatures.length > 0) {
;(response as any).exitOrderSignatures = exitRes.signatures
}
} catch (err) {
console.error('❌ Unexpected error placing exit orders:', err)
}
// Save trade to database
try {
await createTrade({
positionId: openResult.transactionSignature!,
symbol: driftSymbol,
direction: direction,
entryPrice,
positionSizeUSD: actualPositionSizeUSD,
leverage: leverage,
stopLossPrice,
takeProfit1Price: tp1Price,
takeProfit2Price: tp2Price,
tp1SizePercent: config.takeProfit1SizePercent ?? 50,
tp2SizePercent: config.takeProfit2SizePercent ?? 100, // Use ?? instead of || to allow 0
configSnapshot: config,
entryOrderTx: openResult.transactionSignature!,
tp1OrderTx: exitOrderSignatures[0],
tp2OrderTx: exitOrderSignatures[1],
slOrderTx: config.useDualStops ? undefined : exitOrderSignatures[2],
softStopOrderTx: config.useDualStops ? exitOrderSignatures[2] : undefined,
hardStopOrderTx: config.useDualStops ? exitOrderSignatures[3] : undefined,
softStopPrice,
hardStopPrice,
signalStrength: 'test',
timeframe: 'manual',
expectedSizeUSD: requestedPositionSizeUSD,
actualSizeUSD: actualPositionSizeUSD,
})
console.log('💾 Trade saved to database')
} catch (dbError) {
console.error('❌ Failed to save trade to database:', dbError)
// Don't fail the trade if database save fails
}
console.log('✅ Test trade executed successfully!')
return NextResponse.json(response)
} catch (error) {
console.error('❌ Test trade execution error:', error)
return NextResponse.json(
{
success: false,
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
/**
* Helper function to calculate price based on percentage
*/
function calculatePrice(
entryPrice: number,
percent: number,
direction: 'long' | 'short'
): number {
if (direction === 'long') {
return entryPrice * (1 + percent / 100)
} else {
return entryPrice * (1 - percent / 100)
}
}