Major fixes: - Fixed position size calculation: converts USD amount to SOL tokens properly - Fixed insufficient collateral error by using correct position sizing - Added proper TP/SL parameter passing through automation chain - Enhanced position sizing UI with balance percentage slider Position Sizing Fixes: - Convert 2 USD to SOL tokens using current price (2 ÷ 97.87 = ~0.162 SOL) - Remove incorrect 32 SOL token calculation (was 32,000,000,000 base units) - Use USD position value for perpetual futures trading correctly Take Profit & Stop Loss Improvements: - Pass TP/SL percentages from config through automation → trade → drift chain - Use actual config percentages instead of hardcoded 2:1 ratio - Enable proper risk management with user-defined TP/SL levels UI/UX Enhancements: - Remove redundant 'Risk Per Trade (%)' field that caused confusion - Remove conflicting 'Auto-Size (%)' dropdown - Keep clean balance percentage slider (10% - 100% of available balance) - Simplify position sizing to: Balance % → Position Size → Leverage → TP/SL Technical Changes: - Update Drift API position calculation from SOL tokens to USD conversion - Fix automation trade route parameter passing - Clean up AutomationConfig interface - Improve position size validation and safety margins These changes enable proper leveraged perpetual futures trading with correct position sizing, collateral usage, and automated TP/SL order placement.
439 lines
16 KiB
JavaScript
439 lines
16 KiB
JavaScript
import { NextResponse } from 'next/server'
|
|
|
|
// Helper function to get market index from symbol
|
|
function getMarketIndex(symbol) {
|
|
const marketMap = {
|
|
'SOL': 0,
|
|
'BTC': 1,
|
|
'ETH': 2,
|
|
'APT': 3,
|
|
'AVAX': 4,
|
|
'BNB': 5,
|
|
'MATIC': 6,
|
|
'ARB': 7,
|
|
'DOGE': 8,
|
|
'OP': 9
|
|
}
|
|
|
|
const index = marketMap[symbol.toUpperCase()]
|
|
if (index === undefined) {
|
|
throw new Error(`Unsupported symbol: ${symbol}`)
|
|
}
|
|
|
|
return index
|
|
}
|
|
|
|
// Helper function to get symbol from market index
|
|
function getSymbolFromMarketIndex(marketIndex) {
|
|
const symbols = ['SOL', 'BTC', 'ETH', 'APT', 'AVAX', 'BNB', 'MATIC', 'ARB', 'DOGE', 'OP']
|
|
return symbols[marketIndex] || `UNKNOWN_${marketIndex}`
|
|
}
|
|
|
|
// Helper function to get trading balance with better error handling
|
|
async function getTradingBalance(driftClient) {
|
|
try {
|
|
const userAccount = await driftClient.getUserAccount()
|
|
|
|
if (!userAccount) {
|
|
throw new Error('User account is null')
|
|
}
|
|
|
|
console.log('📊 Raw user account data keys:', Object.keys(userAccount))
|
|
|
|
// Get all spot positions
|
|
const spotPositions = userAccount.spotPositions || []
|
|
const usdcPosition = spotPositions.find(pos => pos.marketIndex === 0) // USDC is usually index 0
|
|
|
|
// Convert BigNumber values to regular numbers
|
|
const BN = (await import('bn.js')).default
|
|
|
|
// Get collateral info - convert from BN to number
|
|
const totalCollateral = userAccount.totalCollateral ?
|
|
(userAccount.totalCollateral instanceof BN ? userAccount.totalCollateral.toNumber() / 1e6 :
|
|
parseFloat(userAccount.totalCollateral.toString()) / 1e6) : 0
|
|
|
|
const freeCollateral = userAccount.freeCollateral ?
|
|
(userAccount.freeCollateral instanceof BN ? userAccount.freeCollateral.toNumber() / 1e6 :
|
|
parseFloat(userAccount.freeCollateral.toString()) / 1e6) : 0
|
|
|
|
// Get USDC balance
|
|
const usdcBalance = usdcPosition && usdcPosition.scaledBalance ?
|
|
(usdcPosition.scaledBalance instanceof BN ? usdcPosition.scaledBalance.toNumber() / 1e6 :
|
|
parseFloat(usdcPosition.scaledBalance.toString()) / 1e6) : 0
|
|
|
|
console.log('💰 Parsed balances:', {
|
|
totalCollateral,
|
|
freeCollateral,
|
|
usdcBalance,
|
|
spotPositionsCount: spotPositions.length
|
|
})
|
|
|
|
return {
|
|
totalCollateral: totalCollateral.toString(),
|
|
freeCollateral: freeCollateral.toString(),
|
|
usdcBalance: usdcBalance.toString(),
|
|
marginRatio: userAccount.marginRatio ? userAccount.marginRatio.toString() : '0',
|
|
accountExists: true,
|
|
spotPositions: spotPositions.map(pos => ({
|
|
marketIndex: pos.marketIndex,
|
|
balance: pos.scaledBalance ?
|
|
(pos.scaledBalance instanceof BN ? pos.scaledBalance.toNumber() / 1e6 :
|
|
parseFloat(pos.scaledBalance.toString()) / 1e6) : 0
|
|
}))
|
|
}
|
|
} catch (error) {
|
|
throw new Error(`Balance retrieval failed: ${error.message}`)
|
|
}
|
|
}
|
|
|
|
export async function POST(request) {
|
|
try {
|
|
console.log('🌊 Drift leverage trading endpoint...')
|
|
|
|
// Check if environment is configured
|
|
if (!process.env.SOLANA_PRIVATE_KEY) {
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: 'Drift not configured - missing SOLANA_PRIVATE_KEY'
|
|
}, { status: 400 })
|
|
}
|
|
|
|
const {
|
|
action = 'get_balance',
|
|
symbol = 'SOL',
|
|
amount,
|
|
side,
|
|
leverage = 1,
|
|
stopLoss = true,
|
|
takeProfit = true,
|
|
riskPercent = 2,
|
|
takeProfitPercent = 4
|
|
} = await request.json()
|
|
|
|
// Import Drift SDK components
|
|
const { DriftClient, initialize } = await import('@drift-labs/sdk')
|
|
const { Connection, Keypair } = await import('@solana/web3.js')
|
|
|
|
// Initialize connection with Helius
|
|
const heliusApiKey = '5e236449-f936-4af7-ae38-f15e2f1a3757'
|
|
const rpcUrl = `https://mainnet.helius-rpc.com/?api-key=${heliusApiKey}`
|
|
const connection = new Connection(rpcUrl, 'confirmed')
|
|
|
|
console.log('🌐 Using mainnet with Helius RPC')
|
|
|
|
const privateKeyArray = JSON.parse(process.env.SOLANA_PRIVATE_KEY)
|
|
const keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyArray))
|
|
|
|
// Create wallet using manual interface (most reliable)
|
|
const wallet = {
|
|
publicKey: keypair.publicKey,
|
|
signTransaction: async (tx) => {
|
|
if (typeof tx.partialSign === 'function') {
|
|
tx.partialSign(keypair)
|
|
} else if (typeof tx.sign === 'function') {
|
|
tx.sign([keypair])
|
|
}
|
|
return tx
|
|
},
|
|
signAllTransactions: async (txs) => {
|
|
return txs.map(tx => {
|
|
if (typeof tx.partialSign === 'function') {
|
|
tx.partialSign(keypair)
|
|
} else if (typeof tx.sign === 'function') {
|
|
tx.sign([keypair])
|
|
}
|
|
return tx
|
|
})
|
|
}
|
|
}
|
|
|
|
console.log('🔐 Connecting to Drift with wallet:', keypair.publicKey.toString())
|
|
|
|
// Initialize Drift SDK
|
|
const env = 'mainnet-beta'
|
|
const sdkConfig = initialize({ env })
|
|
|
|
const driftClient = new DriftClient({
|
|
connection,
|
|
wallet,
|
|
programID: sdkConfig.DRIFT_PROGRAM_ID,
|
|
opts: {
|
|
commitment: 'confirmed',
|
|
},
|
|
})
|
|
|
|
try {
|
|
// Subscribe to drift client
|
|
await driftClient.subscribe()
|
|
console.log('✅ Connected to Drift successfully')
|
|
|
|
// Handle action
|
|
let result = {}
|
|
|
|
if (action === 'get_balance') {
|
|
try {
|
|
// Simple and direct approach
|
|
console.log('🔍 Getting user account...')
|
|
const userAccount = await driftClient.getUserAccount()
|
|
|
|
if (userAccount) {
|
|
console.log('✅ User account found, getting balance...')
|
|
result = await getTradingBalance(driftClient)
|
|
console.log('✅ Balance retrieved successfully')
|
|
} else {
|
|
console.log('❌ User account is null')
|
|
result = {
|
|
message: 'User account exists but returns null',
|
|
accountExists: false
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log('❌ Error getting user account:', error.message)
|
|
|
|
// Check wallet SOL balance as fallback
|
|
const walletBalance = await connection.getBalance(keypair.publicKey)
|
|
const solBalance = walletBalance / 1e9
|
|
|
|
result = {
|
|
message: 'Cannot access user account data',
|
|
error: error.message,
|
|
solBalance: solBalance,
|
|
walletAddress: keypair.publicKey.toString(),
|
|
suggestion: 'Account may need to be accessed through Drift UI first or deposit USDC directly'
|
|
}
|
|
}
|
|
} else if (action === 'place_order') {
|
|
// Place a leverage order with stop loss and take profit
|
|
if (!amount || !side) {
|
|
result = {
|
|
error: 'Missing required parameters: amount and side'
|
|
}
|
|
} else {
|
|
try {
|
|
const { OrderType, PositionDirection } = await import('@drift-labs/sdk')
|
|
const BN = (await import('bn.js')).default
|
|
|
|
const marketIndex = getMarketIndex(symbol)
|
|
|
|
// Get current market price for stop loss/take profit calculations
|
|
const perpMarketAccount = driftClient.getPerpMarketAccount(marketIndex)
|
|
const currentPrice = Number(perpMarketAccount.amm.lastMarkPriceTwap) / 1e6
|
|
|
|
console.log(`📊 Current ${symbol} price: $${currentPrice}`)
|
|
|
|
// For perpetual futures: amount is USD position size, convert to base asset amount
|
|
// Example: $32 position at $197.87/SOL = 0.162 SOL base asset amount
|
|
const solTokenAmount = amount / currentPrice
|
|
const baseAssetAmount = new BN(Math.floor(solTokenAmount * 1e9))
|
|
|
|
console.log(`💰 Position size conversion:`, {
|
|
usdPositionSize: amount,
|
|
solPrice: currentPrice,
|
|
solTokenAmount: solTokenAmount,
|
|
calculatedBaseAsset: solTokenAmount * 1e9,
|
|
flooredBaseAsset: Math.floor(solTokenAmount * 1e9),
|
|
baseAssetAmount: baseAssetAmount.toString()
|
|
})
|
|
|
|
// Determine direction
|
|
const direction = side.toLowerCase() === 'buy' ? PositionDirection.LONG : PositionDirection.SHORT
|
|
|
|
console.log(`📊 Placing ${side} order:`, {
|
|
symbol,
|
|
marketIndex,
|
|
usdAmount: amount,
|
|
solAmount: solTokenAmount,
|
|
leverage,
|
|
currentPrice,
|
|
baseAssetAmount: baseAssetAmount.toString()
|
|
})
|
|
|
|
// 1. Place main perpetual market order
|
|
console.log('🚀 Placing main market order...')
|
|
const mainOrderTx = await driftClient.placePerpOrder({
|
|
orderType: OrderType.MARKET,
|
|
marketIndex,
|
|
direction,
|
|
baseAssetAmount,
|
|
reduceOnly: false,
|
|
})
|
|
|
|
console.log('✅ Main order placed:', mainOrderTx)
|
|
|
|
// Wait for main order to fill
|
|
await new Promise(resolve => setTimeout(resolve, 5000))
|
|
|
|
// 2. Calculate stop loss and take profit prices using config percentages
|
|
const stopLossPercent = Math.max(riskPercent / 100, 0.02) // Use riskPercent from config, minimum 2%
|
|
const takeProfitPercentCalc = Math.max(takeProfitPercent / 100, 0.04) // Use takeProfitPercent from config, minimum 4%
|
|
|
|
let stopLossPrice, takeProfitPrice
|
|
|
|
if (direction === PositionDirection.LONG) {
|
|
stopLossPrice = currentPrice * (1 - stopLossPercent)
|
|
takeProfitPrice = currentPrice * (1 + takeProfitPercentCalc)
|
|
} else {
|
|
stopLossPrice = currentPrice * (1 + stopLossPercent)
|
|
takeProfitPrice = currentPrice * (1 - takeProfitPercentCalc)
|
|
}
|
|
|
|
console.log(`🎯 Risk management:`, {
|
|
stopLossPrice: stopLossPrice.toFixed(4),
|
|
takeProfitPrice: takeProfitPrice.toFixed(4),
|
|
stopLossPercent: `${stopLossPercent * 100}%`,
|
|
takeProfitPercent: `${takeProfitPercentCalc * 100}%`,
|
|
priceDifference: Math.abs(currentPrice - stopLossPrice).toFixed(4)
|
|
})
|
|
|
|
let stopLossTx = null, takeProfitTx = null
|
|
|
|
// 3. Place stop loss order
|
|
if (stopLoss) {
|
|
try {
|
|
console.log('🛡️ Placing stop loss order...')
|
|
|
|
const stopLossTriggerPrice = new BN(Math.floor(stopLossPrice * 1e6))
|
|
const stopLossOrderPrice = new BN(Math.floor(stopLossPrice * 0.995 * 1e6)) // 0.5% slippage buffer
|
|
|
|
console.log(`🛡️ Stop Loss Details:`, {
|
|
triggerPrice: (stopLossTriggerPrice.toNumber() / 1e6).toFixed(4),
|
|
orderPrice: (stopLossOrderPrice.toNumber() / 1e6).toFixed(4),
|
|
baseAssetAmount: baseAssetAmount.toString()
|
|
})
|
|
|
|
stopLossTx = await driftClient.placePerpOrder({
|
|
orderType: OrderType.TRIGGER_LIMIT,
|
|
marketIndex,
|
|
direction: direction === PositionDirection.LONG ? PositionDirection.SHORT : PositionDirection.LONG,
|
|
baseAssetAmount,
|
|
price: stopLossOrderPrice,
|
|
triggerPrice: stopLossTriggerPrice,
|
|
reduceOnly: true,
|
|
})
|
|
|
|
console.log('✅ Stop loss placed:', stopLossTx)
|
|
} catch (slError) {
|
|
console.warn('⚠️ Stop loss failed:', slError.message)
|
|
// Log more details about the stop loss failure
|
|
console.warn('🛡️ Stop loss failure details:', {
|
|
stopLossPrice,
|
|
currentPrice,
|
|
priceDiff: Math.abs(currentPrice - stopLossPrice),
|
|
percentDiff: ((Math.abs(currentPrice - stopLossPrice) / currentPrice) * 100).toFixed(2) + '%'
|
|
})
|
|
}
|
|
}
|
|
|
|
// 4. Place take profit order
|
|
if (takeProfit) {
|
|
try {
|
|
console.log('🎯 Placing take profit order...')
|
|
|
|
const takeProfitTriggerPrice = new BN(Math.floor(takeProfitPrice * 1e6))
|
|
const takeProfitOrderPrice = new BN(Math.floor(takeProfitPrice * 1.005 * 1e6)) // 0.5% slippage for execution
|
|
|
|
console.log('🎯 Take Profit Details:', {
|
|
takeProfitPrice: takeProfitPrice.toFixed(4),
|
|
triggerPrice: (Number(takeProfitTriggerPrice) / 1e6).toFixed(4),
|
|
orderPrice: (Number(takeProfitOrderPrice) / 1e6).toFixed(4),
|
|
baseAssetAmount: baseAssetAmount.toString()
|
|
})
|
|
|
|
takeProfitTx = await driftClient.placePerpOrder({
|
|
orderType: OrderType.TRIGGER_LIMIT,
|
|
marketIndex,
|
|
direction: direction === PositionDirection.LONG ? PositionDirection.SHORT : PositionDirection.LONG,
|
|
baseAssetAmount,
|
|
price: takeProfitOrderPrice,
|
|
triggerPrice: takeProfitTriggerPrice,
|
|
reduceOnly: true,
|
|
})
|
|
|
|
console.log('✅ Take profit placed successfully:', takeProfitTx)
|
|
} catch (tpError) {
|
|
console.error('❌ Take profit placement failed:', {
|
|
error: tpError.message,
|
|
code: tpError.code,
|
|
logs: tpError.logs || 'No logs available'
|
|
})
|
|
}
|
|
}
|
|
|
|
// 5. Get final position after all orders
|
|
const userAccount = await driftClient.getUserAccount()
|
|
const position = userAccount.perpPositions.find(pos => pos.marketIndex === marketIndex && !pos.baseAssetAmount.isZero())
|
|
|
|
result = {
|
|
success: true,
|
|
transactionId: mainOrderTx,
|
|
stopLossTransactionId: stopLossTx,
|
|
takeProfitTransactionId: takeProfitTx,
|
|
symbol,
|
|
side,
|
|
amount,
|
|
leverage,
|
|
currentPrice,
|
|
stopLossPrice: stopLoss ? stopLossPrice : null,
|
|
takeProfitPrice: takeProfit ? takeProfitPrice : null,
|
|
riskManagement: {
|
|
stopLoss: !!stopLossTx,
|
|
takeProfit: !!takeProfitTx,
|
|
riskPercent
|
|
},
|
|
position: position ? {
|
|
marketIndex: position.marketIndex,
|
|
baseAssetAmount: position.baseAssetAmount.toString(),
|
|
quoteAssetAmount: position.quoteAssetAmount.toString(),
|
|
avgEntryPrice: (Number(position.quoteAssetAmount) / Number(position.baseAssetAmount) * 1e9).toFixed(4)
|
|
} : null
|
|
}
|
|
} catch (orderError) {
|
|
console.log('❌ Failed to place order:', orderError.message)
|
|
result = {
|
|
success: false,
|
|
error: 'Failed to place order',
|
|
details: orderError.message
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
result = { message: `Action ${action} not yet implemented` }
|
|
}
|
|
|
|
// Clean up connection
|
|
await driftClient.unsubscribe()
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
action,
|
|
result,
|
|
timestamp: Date.now()
|
|
})
|
|
|
|
} catch (driftError) {
|
|
console.error('❌ Drift trading error:', driftError)
|
|
|
|
try {
|
|
await driftClient.unsubscribe()
|
|
} catch (cleanupError) {
|
|
console.warn('⚠️ Cleanup error:', cleanupError.message)
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: 'Drift trading failed',
|
|
details: driftError.message
|
|
}, { status: 500 })
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ Trading API error:', error)
|
|
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: 'Internal server error',
|
|
details: error.message
|
|
}, { status: 500 })
|
|
}
|
|
}
|