Features: - Autonomous trading system with Drift Protocol on Solana - Real-time position monitoring with Pyth price feeds - Dynamic stop-loss and take-profit management - n8n workflow integration for TradingView signals - Beautiful web UI for settings management - REST API for trade execution and monitoring - Next.js 15 with standalone output mode - TypeScript with strict typing - Docker containerization with multi-stage builds - PostgreSQL database for trade history - Singleton pattern for Drift client connection pooling - BN.js for BigNumber handling (Drift SDK requirement) - Configurable stop-loss and take-profit levels - Breakeven trigger and profit locking - Daily loss limits and trade cooldowns - Slippage tolerance controls - DRY_RUN mode for safe testing - Real-time risk calculator - Interactive sliders for all parameters - Live preview of trade outcomes - Position sizing and leverage controls - Beautiful gradient design with Tailwind CSS - POST /api/trading/execute - Execute trades - POST /api/trading/close - Close positions - GET /api/trading/positions - Monitor active trades - GET /api/trading/check-risk - Validate trade signals - GET /api/settings - View configuration - POST /api/settings - Update configuration - Fixed Borsh serialization errors (simplified order params) - Resolved RPC rate limiting with singleton pattern - Fixed BigInt vs BN type mismatches - Corrected order execution flow - Improved position state management - Complete setup guides - Docker deployment instructions - n8n workflow configuration - API reference documentation - Risk management guidelines - Runs on port 3001 (external), 3000 (internal) - Uses Helius RPC for optimal performance - Production-ready with error handling - Health monitoring and logging
247 lines
6.8 KiB
TypeScript
247 lines
6.8 KiB
TypeScript
/**
|
|
* Execute Trade API Endpoint
|
|
*
|
|
* Called by n8n workflow when TradingView signal is received
|
|
* POST /api/trading/execute
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
import { initializeDriftService } from '@/lib/drift/client'
|
|
import { openPosition } from '@/lib/drift/orders'
|
|
import { normalizeTradingViewSymbol } from '@/config/trading'
|
|
import { getMergedConfig } from '@/config/trading'
|
|
import { getPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
|
|
|
export interface ExecuteTradeRequest {
|
|
symbol: string // TradingView symbol (e.g., 'SOLUSDT')
|
|
direction: 'long' | 'short'
|
|
timeframe: string // e.g., '5'
|
|
signalStrength?: 'strong' | 'moderate' | 'weak'
|
|
signalPrice?: number
|
|
}
|
|
|
|
export interface ExecuteTradeResponse {
|
|
success: boolean
|
|
positionId?: string
|
|
symbol?: string
|
|
direction?: 'long' | 'short'
|
|
entryPrice?: number
|
|
positionSize?: number
|
|
stopLoss?: number
|
|
takeProfit1?: number
|
|
takeProfit2?: number
|
|
stopLossPercent?: number
|
|
tp1Percent?: number
|
|
tp2Percent?: number
|
|
entrySlippage?: number
|
|
timestamp?: string
|
|
error?: string
|
|
message?: string
|
|
}
|
|
|
|
export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTradeResponse>> {
|
|
try {
|
|
// Verify authorization
|
|
const authHeader = request.headers.get('authorization')
|
|
const expectedAuth = `Bearer ${process.env.API_SECRET_KEY}`
|
|
|
|
if (!authHeader || authHeader !== expectedAuth) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'Unauthorized',
|
|
message: 'Invalid API key',
|
|
},
|
|
{ status: 401 }
|
|
)
|
|
}
|
|
|
|
// Parse request body
|
|
const body: ExecuteTradeRequest = await request.json()
|
|
|
|
console.log('🎯 Trade execution request received:', body)
|
|
|
|
// Validate required fields
|
|
if (!body.symbol || !body.direction) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'Missing required fields',
|
|
message: 'symbol and direction are required',
|
|
},
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
// Normalize symbol
|
|
const driftSymbol = normalizeTradingViewSymbol(body.symbol)
|
|
console.log(`📊 Normalized symbol: ${body.symbol} → ${driftSymbol}`)
|
|
|
|
// Get trading configuration
|
|
const config = getMergedConfig()
|
|
|
|
// Initialize Drift service if not already initialized
|
|
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 }
|
|
)
|
|
}
|
|
|
|
// Calculate position size with leverage
|
|
const positionSizeUSD = config.positionSize * config.leverage
|
|
|
|
console.log(`💰 Opening ${body.direction} position:`)
|
|
console.log(` Symbol: ${driftSymbol}`)
|
|
console.log(` Base size: $${config.positionSize}`)
|
|
console.log(` Leverage: ${config.leverage}x`)
|
|
console.log(` Total position: $${positionSizeUSD}`)
|
|
|
|
// Open position
|
|
const openResult = await openPosition({
|
|
symbol: driftSymbol,
|
|
direction: body.direction,
|
|
sizeUSD: positionSizeUSD,
|
|
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 stopLossPrice = calculatePrice(
|
|
entryPrice,
|
|
config.stopLossPercent,
|
|
body.direction
|
|
)
|
|
|
|
const tp1Price = calculatePrice(
|
|
entryPrice,
|
|
config.takeProfit1Percent,
|
|
body.direction
|
|
)
|
|
|
|
const tp2Price = calculatePrice(
|
|
entryPrice,
|
|
config.takeProfit2Percent,
|
|
body.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)} (${config.takeProfit2Percent}%)`)
|
|
|
|
// Calculate emergency stop
|
|
const emergencyStopPrice = calculatePrice(
|
|
entryPrice,
|
|
config.emergencyStopPercent,
|
|
body.direction
|
|
)
|
|
|
|
// Create active trade object
|
|
const activeTrade: ActiveTrade = {
|
|
id: `trade-${Date.now()}`,
|
|
positionId: openResult.transactionSignature!,
|
|
symbol: driftSymbol,
|
|
direction: body.direction,
|
|
entryPrice,
|
|
entryTime: Date.now(),
|
|
positionSize: positionSizeUSD,
|
|
leverage: config.leverage,
|
|
stopLossPrice,
|
|
tp1Price,
|
|
tp2Price,
|
|
emergencyStopPrice,
|
|
currentSize: positionSizeUSD,
|
|
tp1Hit: false,
|
|
slMovedToBreakeven: false,
|
|
slMovedToProfit: false,
|
|
realizedPnL: 0,
|
|
unrealizedPnL: 0,
|
|
peakPnL: 0,
|
|
priceCheckCount: 0,
|
|
lastPrice: entryPrice,
|
|
lastUpdateTime: Date.now(),
|
|
}
|
|
|
|
// Add to position manager for monitoring
|
|
const positionManager = getPositionManager()
|
|
await positionManager.addTrade(activeTrade)
|
|
|
|
console.log('✅ Trade added to position manager for monitoring')
|
|
|
|
// TODO: Save trade to database (add Prisma integration later)
|
|
|
|
|
|
const response: ExecuteTradeResponse = {
|
|
success: true,
|
|
positionId: openResult.transactionSignature,
|
|
symbol: driftSymbol,
|
|
direction: body.direction,
|
|
entryPrice: entryPrice,
|
|
positionSize: positionSizeUSD,
|
|
stopLoss: stopLossPrice,
|
|
takeProfit1: tp1Price,
|
|
takeProfit2: tp2Price,
|
|
stopLossPercent: config.stopLossPercent,
|
|
tp1Percent: config.takeProfit1Percent,
|
|
tp2Percent: config.takeProfit2Percent,
|
|
entrySlippage: openResult.slippage,
|
|
timestamp: new Date().toISOString(),
|
|
}
|
|
|
|
console.log('✅ Trade executed successfully!')
|
|
|
|
return NextResponse.json(response)
|
|
|
|
} catch (error) {
|
|
console.error('❌ 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)
|
|
}
|
|
}
|