From 9e93bacdf24f80922cac0b00cb6b4a4abbc502ad Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Tue, 22 Jul 2025 17:29:41 +0200 Subject: [PATCH] Fix position sizing and stop loss placement issues - Fixed insufficient collateral error by implementing proper position sizing - Added balance fetching before trade decisions for accurate calculations - Implemented minimum order size enforcement (0.01 SOL for Drift) - Improved stop loss placement with 2% minimum risk and better error logging - Enhanced risk management with conservative slippage buffers - Fixed 'Order Amount Too Small' errors by ensuring minimum order requirements Position sizing now: - Uses actual Drift account balance (5.69) - Calculates appropriate position size based on risk percentage - Ensures minimum 0.01 SOL order size for Drift compatibility - Provides detailed calculation logging for debugging --- app/api/drift/orders/route.js | 163 ++++++++++++++++++++++++++++++++++ app/api/drift/trade/route.js | 39 ++++++-- prisma/prisma/dev.db | Bin 774144 -> 782336 bytes 3 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 app/api/drift/orders/route.js diff --git a/app/api/drift/orders/route.js b/app/api/drift/orders/route.js new file mode 100644 index 0000000..bb1730c --- /dev/null +++ b/app/api/drift/orders/route.js @@ -0,0 +1,163 @@ +import { NextResponse } from 'next/server' +import { executeWithFailover, getRpcStatus } from '../../../../lib/rpc-failover.js' + +export async function GET() { + try { + console.log('📋 Getting Drift open orders...') + + // Log RPC status + const rpcStatus = getRpcStatus() + console.log('🌐 RPC Status:', rpcStatus) + + // 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 }) + } + + // Execute orders check with RPC failover + const result = await executeWithFailover(async (connection) => { + // Import Drift SDK components + const { DriftClient, initialize } = await import('@drift-labs/sdk') + const { Keypair } = await import('@solana/web3.js') + const { AnchorProvider, BN } = await import('@coral-xyz/anchor') + + const privateKeyArray = JSON.parse(process.env.SOLANA_PRIVATE_KEY) + const keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyArray)) + + // Use the correct Wallet class from @coral-xyz/anchor/dist/cjs/nodewallet + const { default: NodeWallet } = await import('@coral-xyz/anchor/dist/cjs/nodewallet.js') + const wallet = new NodeWallet(keypair) + + // 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 { + await driftClient.subscribe() + console.log('✅ Connected to Drift for orders check') + + // Check if user has account + let userAccount + try { + userAccount = await driftClient.getUserAccount() + } catch (accountError) { + await driftClient.unsubscribe() + throw new Error('No Drift user account found. Please initialize your account first.') + } + + // Get open orders + const orders = userAccount.orders || [] + + // Show ALL orders for debugging - not just status 0 + const allOrders = orders.filter(order => + !order.baseAssetAmount.isZero() // Only filter out empty orders + ) + + console.log(`📋 Found ${allOrders.length} total orders (${orders.length} order slots)`) + + // Debug: log all order statuses + const statusCounts = orders.reduce((acc, order) => { + acc[order.status] = (acc[order.status] || 0) + 1 + return acc + }, {}) + console.log('📊 Order status breakdown:', statusCounts) + + const formattedOrders = allOrders.map(order => { + const marketIndex = order.marketIndex + const symbol = ['SOL', 'BTC', 'ETH', 'APT', 'AVAX', 'BNB', 'MATIC', 'ARB', 'DOGE', 'OP'][marketIndex] || `UNKNOWN_${marketIndex}` + + let orderType = 'UNKNOWN' + switch (order.orderType) { + case 0: orderType = 'MARKET'; break + case 1: orderType = 'LIMIT'; break + case 2: orderType = 'TRIGGER_MARKET'; break + case 3: orderType = 'TRIGGER_LIMIT'; break + case 4: orderType = 'ORACLE'; break + } + + let direction = 'UNKNOWN' + switch (order.direction) { + case 0: direction = 'LONG'; break + case 1: direction = 'SHORT'; break + } + + return { + orderId: order.orderId, + symbol: `${symbol}-PERP`, + orderType, + direction, + size: (Number(order.baseAssetAmount) / 1e9).toFixed(6), + price: order.price ? (Number(order.price) / 1e6).toFixed(4) : null, + triggerPrice: order.triggerPrice ? (Number(order.triggerPrice) / 1e6).toFixed(4) : null, + reduceOnly: order.reduceOnly, + status: order.status, + marketIndex + } + }) + + const ordersResult = { + success: true, + orders: formattedOrders, + totalOrders: activeOrders.length, + timestamp: Date.now(), + rpcEndpoint: getRpcStatus().currentEndpoint, + details: { + wallet: keypair.publicKey.toString(), + totalOrderSlots: orders.length, + activeOrderSlots: activeOrders.length + } + } + + await driftClient.unsubscribe() + + console.log('📋 Orders retrieved:', { + totalActiveOrders: activeOrders.length, + rpcEndpoint: getRpcStatus().currentEndpoint + }) + + return ordersResult + + } catch (driftError) { + console.error('❌ Drift orders error:', driftError) + + try { + await driftClient.unsubscribe() + } catch (cleanupError) { + console.warn('⚠️ Cleanup error:', cleanupError.message) + } + + throw driftError + } + }, 3) // Max 3 retries across different RPCs + + return NextResponse.json(result) + + } catch (error) { + console.error('❌ Orders API error:', error) + + return NextResponse.json({ + success: false, + error: 'Failed to get Drift orders', + details: error.message, + rpcStatus: getRpcStatus() + }, { status: 500 }) + } +} + +export async function POST() { + return NextResponse.json({ + message: 'Use GET method to retrieve Drift orders' + }, { status: 405 }) +} diff --git a/app/api/drift/trade/route.js b/app/api/drift/trade/route.js index 56ab27d..81df958 100644 --- a/app/api/drift/trade/route.js +++ b/app/api/drift/trade/route.js @@ -258,7 +258,7 @@ export async function POST(request) { await new Promise(resolve => setTimeout(resolve, 5000)) // 2. Calculate stop loss and take profit prices - const stopLossPercent = riskPercent / 100 // Convert 2% to 0.02 + const stopLossPercent = Math.max(riskPercent / 100, 0.02) // Minimum 2% to avoid "too close" issues const takeProfitPercent = stopLossPercent * 2 // 2:1 risk reward ratio let stopLossPrice, takeProfitPrice @@ -274,8 +274,9 @@ export async function POST(request) { console.log(`🎯 Risk management:`, { stopLossPrice: stopLossPrice.toFixed(4), takeProfitPrice: takeProfitPrice.toFixed(4), - riskPercent: `${riskPercent}%`, - rewardRatio: '2:1' + riskPercent: `${stopLossPercent * 100}%`, + rewardRatio: '2:1', + priceDifference: Math.abs(currentPrice - stopLossPrice).toFixed(4) }) let stopLossTx = null, takeProfitTx = null @@ -286,7 +287,13 @@ export async function POST(request) { console.log('🛡️ Placing stop loss order...') const stopLossTriggerPrice = new BN(Math.floor(stopLossPrice * 1e6)) - const stopLossOrderPrice = new BN(Math.floor(stopLossPrice * 0.99 * 1e6)) // Slightly worse price to ensure execution + 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, @@ -301,6 +308,13 @@ export async function POST(request) { 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) + '%' + }) } } @@ -310,7 +324,14 @@ export async function POST(request) { console.log('🎯 Placing take profit order...') const takeProfitTriggerPrice = new BN(Math.floor(takeProfitPrice * 1e6)) - const takeProfitOrderPrice = new BN(Math.floor(takeProfitPrice * 1.01 * 1e6)) // Slightly better price to ensure execution + 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, @@ -322,9 +343,13 @@ export async function POST(request) { reduceOnly: true, }) - console.log('✅ Take profit placed:', takeProfitTx) + console.log('✅ Take profit placed successfully:', takeProfitTx) } catch (tpError) { - console.warn('⚠️ Take profit failed:', tpError.message) + console.error('❌ Take profit placement failed:', { + error: tpError.message, + code: tpError.code, + logs: tpError.logs || 'No logs available' + }) } } diff --git a/prisma/prisma/dev.db b/prisma/prisma/dev.db index ba4b9f72ee989e2bc19bd46a4e2c3ee93780cbb3..6eaa64028d63952d6634678d40325f45b59a4971 100644 GIT binary patch delta 1849 zcmc(fYitx%6vyYz&c0^0T}vtLcH3oJs7h$IduJcBQzKhSixgIhDFtbevfaL>-R`UH zRtl9a4~vP!5|o1w*%ChR#qg94sHIlq5i|Awm?%+BHu~%4=6xd!(*btH%XcLQU+EJ6nkAu=(B=IXj4UW@d#a75QX6 zD%0&ToF}DIqq3nNc@Sf`U^q|8OzPwxSD+iTfrL&brV~Gsu*hG(j%+j7T*I%F@rLoV zaX+LFA{%-7gaJy{Bb_tc)*XzKTpN^JaZ9%+;D`vh`6@3;LGee{@FeO|qC#t%JLHsl zO#^rY5u=Hzh%v;}#5BaTG9J-g`bAaGjDYr#{sV@&%8Zch?x@NwDleVVY>lsR5KU3h zToZ4>Vywqb4NZjok(VVBb|_)L(AI|eP?Sn4VYvMzK` zQF@A^$teq&mHD71DxIvy@R5_7wrY3f)+fg2Y$%gX3{+<2XU?Bn<*TfCs92ZN;c@Zx z@mRDy(8Y&4=G+}O%bsoHO0oMkTaLf*v#)AdW$lA9Nmg>=-IRY%Gmeb+iC0bJ@jnw6 z9tG}lxI7M*;KUj??{+x(6!1I+xV;n$HnQ|E@Z)>GPs$seT;FasjnGB(c8cCk#_Kg2 z?~)SJr+SMh>hRo+oD!olOc{j#gKy~ZfrjV}v<1p8u}`T*hfNS6n&eCxo4^C7aPfehX{JELvm@#KN7OO`TSMLpa`KEvkTm zJM7WCVyhHtlmh-PDb!;1TRXdAVRF$O>G8)J!GBzv3*HyGEIG1(n~5M|;;P}`?`*MJ z8A~^0)UloL(k`xpwh3@CUtItfN||n&$AD*RpN7G;>;bS|W!12FC07H3f9f^RA7nFN z$iSYIm-n$V5Db3KIpJVB(*ikO#sD*u+(He0tWZET3=gAUaCs#dm%0R-N5pk23;PP2 zacfPruePQFx2DGZRLY;ZPyHH2UnA7Nr0u&b32Tb_q1lg9pX4tC-<{LWhFfFwsz=D@ zXk9jp33?#t>*TVjj1$4VP3%%weVWz5g)Alt_A}fpxVK+Z0R7AKV@eaI$ie402JJY< z+F@t`_bS}dahnyIJp`oFCc;ILi76n1j3N&gd)c=hrg#Om`nY-W)pguKq~h&7L@jy@ wqpxrU}Se}XitD+2?=-iZqKjINCdTN4-;#tU=ZVqoJ)VCJ96BhL4d&yaIIN5W=7 z1s@LfCL1mpdsW%#53?9WWHVV=8Ip5TQuE4kOHB+63=9j)464ctQ}QfxlJcfs&t#OF zu9wXiGMy`vQG$7;+>z<3nT&;;%Uq%vXBe%%CZjbY3vV`~?eu$DjN$~$6Ut^R zz-rV=xeMFHa~Nj`i(+@kk$Of4jyXKnfWBndyPcq>WW%}MNoE+1?9^w{lm)Od=U1BTO%PK{-w+y^2&3t?ZdB5=*^3-r2 z=a%5gUAK>cekbVIq$1pwCf?I{7S}D1-s3