Files
trading_bot_v4/lib/drift/orders.ts
2025-11-05 15:28:12 +01:00

761 lines
28 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Drift Order Execution
*
* Handles opening and closing positions with market orders
*/
import { getDriftService, initializeDriftService } from './client'
import { getMarketConfig } from '../../config/trading'
import BN from 'bn.js'
import {
MarketType,
PositionDirection,
OrderType,
OrderParams,
OrderTriggerCondition,
} from '@drift-labs/sdk'
export interface OpenPositionParams {
symbol: string // e.g., 'SOL-PERP'
direction: 'long' | 'short'
sizeUSD: number // USD notional size
slippageTolerance: number // Percentage (e.g., 1.0 for 1%)
}
export interface OpenPositionResult {
success: boolean
transactionSignature?: string
fillPrice?: number
fillSize?: number
fillNotionalUSD?: 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 {
symbol: string
percentToClose: number // 0-100
slippageTolerance: number
}
export interface ClosePositionResult {
success: boolean
transactionSignature?: string
closePrice?: number
closedSize?: number
realizedPnL?: number
error?: string
}
export interface PlaceExitOrdersResult {
success: boolean
signatures?: string[]
error?: string
}
export interface PlaceExitOrdersOptions {
symbol: string
positionSizeUSD: number
entryPrice: number // CRITICAL: Entry price for calculating position size in base assets
tp1Price: number
tp2Price: number
stopLossPrice: number
tp1SizePercent: number
tp2SizePercent: number
direction: 'long' | 'short'
useStopLimit?: boolean // Optional: use TRIGGER_LIMIT instead of TRIGGER_MARKET for SL
stopLimitBuffer?: number // Optional: buffer percentage for stop-limit (default 0.5%)
// Dual Stop System
useDualStops?: boolean // Enable dual stop system
softStopPrice?: number // Soft stop trigger price (TRIGGER_LIMIT)
softStopBuffer?: number // Buffer for soft stop limit price
hardStopPrice?: number // Hard stop trigger price (TRIGGER_MARKET)
}
/**
* Open a position with a market order
*/
export async function openPosition(
params: OpenPositionParams
): Promise<OpenPositionResult> {
try {
console.log('📊 Opening position:', params)
const driftService = getDriftService()
const marketConfig = getMarketConfig(params.symbol)
const driftClient = driftService.getClient()
// Get current oracle price
const oraclePrice = await driftService.getOraclePrice(marketConfig.driftMarketIndex)
console.log(`💰 Current ${params.symbol} price: $${oraclePrice.toFixed(4)}`)
// Calculate position size in base asset
const baseAssetSize = params.sizeUSD / oraclePrice
// Validate minimum order size
if (baseAssetSize < marketConfig.minOrderSize) {
throw new Error(
`Order size ${baseAssetSize.toFixed(4)} is below minimum ${marketConfig.minOrderSize}`
)
}
// Calculate worst acceptable price (with slippage)
const slippageMultiplier = params.direction === 'long'
? 1 + (params.slippageTolerance / 100)
: 1 - (params.slippageTolerance / 100)
const worstPrice = oraclePrice * slippageMultiplier
console.log(`📝 Order details:`)
console.log(` Size: ${baseAssetSize.toFixed(4)} ${params.symbol.split('-')[0]}`)
console.log(` Notional: $${params.sizeUSD.toFixed(2)}`)
console.log(` Oracle price: $${oraclePrice.toFixed(4)}`)
console.log(` Worst price (${params.slippageTolerance}% slippage): $${worstPrice.toFixed(4)}`)
// Check DRY_RUN mode
const isDryRun = process.env.DRY_RUN === 'true'
if (isDryRun) {
console.log('🧪 DRY RUN MODE: Simulating order (not executing on blockchain)')
const mockTxSig = `DRY_RUN_${Date.now()}_${Math.random().toString(36).substring(7)}`
return {
success: true,
transactionSignature: mockTxSig,
fillPrice: oraclePrice,
fillSize: baseAssetSize,
fillNotionalUSD: baseAssetSize * oraclePrice,
slippage: 0,
}
}
// Prepare order parameters - use simple structure like v3
const orderParams = {
orderType: OrderType.MARKET,
marketIndex: marketConfig.driftMarketIndex,
direction: params.direction === 'long'
? PositionDirection.LONG
: PositionDirection.SHORT,
baseAssetAmount: new BN(Math.floor(baseAssetSize * 1e9)), // 9 decimals
reduceOnly: false,
}
// Place market order using simple placePerpOrder (like v3)
console.log('🚀 Placing REAL market order...')
const txSig = await driftClient.placePerpOrder(orderParams)
console.log(`📝 Transaction submitted: ${txSig}`)
// CRITICAL: Confirm transaction actually executed on-chain
console.log('⏳ Confirming transaction on-chain...')
const connection = driftService.getConnection()
try {
const confirmation = await connection.confirmTransaction(txSig, 'confirmed')
if (confirmation.value.err) {
console.error(`❌ Transaction failed on-chain:`, confirmation.value.err)
return {
success: false,
error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
}
}
console.log(`✅ Transaction confirmed on-chain: ${txSig}`)
} catch (confirmError) {
console.error(`❌ Failed to confirm transaction:`, confirmError)
return {
success: false,
error: `Transaction confirmation failed: ${confirmError instanceof Error ? confirmError.message : 'Unknown error'}`,
}
}
// Wait a moment for position to update
console.log('⏳ Waiting for position to update...')
await new Promise(resolve => setTimeout(resolve, 2000))
// Get actual fill price from position
const position = await driftService.getPosition(marketConfig.driftMarketIndex)
if (position && position.side !== 'none') {
const fillPrice = position.entryPrice
const filledBaseSize = Math.abs(position.size)
const fillNotionalUSD = filledBaseSize * fillPrice
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 expectedSizeUSD = params.sizeUSD
const sizeRatio = expectedSizeUSD > 0 ? fillNotionalUSD / expectedSizeUSD : 1
console.log(`💰 Fill details:`)
console.log(` Fill price: $${fillPrice.toFixed(4)}`)
console.log(` Filled base size: ${filledBaseSize.toFixed(4)} ${params.symbol.split('-')[0]}`)
console.log(` Filled notional: $${fillNotionalUSD.toFixed(2)}`)
console.log(` Slippage: ${slippage.toFixed(3)}%`)
console.log(` Expected size: $${expectedSizeUSD.toFixed(2)}`)
console.log(` Actual size: $${fillNotionalUSD.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: $${fillNotionalUSD.toFixed(2)}`)
console.error(` This indicates the order was rejected or partially filled by Drift`)
}
return {
success: true,
transactionSignature: txSig,
fillPrice,
fillSize: filledBaseSize,
fillNotionalUSD,
slippage,
isPhantom,
actualSizeUSD: fillNotionalUSD,
}
} else {
// Position not found yet (may be DRY_RUN mode)
console.log(`⚠️ Position not immediately visible (may be DRY_RUN mode)`)
console.log(` Using oracle price as estimate: $${oraclePrice.toFixed(4)}`)
return {
success: true,
transactionSignature: txSig,
fillPrice: oraclePrice,
fillSize: baseAssetSize,
fillNotionalUSD: baseAssetSize * oraclePrice,
slippage: 0,
}
}
} catch (error) {
console.error('❌ Failed to open position:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}
/**
* Place on-chain exit orders (reduce-only orders) so TP/SL show up in Drift UI.
*
* Stop Loss Strategy:
* - Default: TRIGGER_MARKET (guaranteed execution, recommended for most traders)
* - Optional: TRIGGER_LIMIT with buffer (protects against extreme wicks in liquid markets)
*
* Take Profit Strategy:
* - Always uses LIMIT orders to lock in desired prices
*/
export async function placeExitOrders(options: PlaceExitOrdersOptions): Promise<PlaceExitOrdersResult> {
try {
console.log('🛡️ Placing exit orders on-chain:', options.symbol)
const driftService = getDriftService()
const driftClient = driftService.getClient()
const marketConfig = getMarketConfig(options.symbol)
const isDryRun = process.env.DRY_RUN === 'true'
if (isDryRun) {
console.log('🧪 DRY RUN: Simulating placement of exit orders')
return {
success: true,
signatures: [
`DRY_TP1_${Date.now()}`,
`DRY_TP2_${Date.now()}`,
`DRY_SL_${Date.now()}`,
],
}
}
const signatures: string[] = []
// Helper to compute base asset amount from USD notional and price
// CRITICAL: Use ENTRY price to calculate position size, not TP price!
// This ensures we close the correct percentage of the actual position
const usdToBase = (usd: number) => {
const base = usd / options.entryPrice // Use entry price for size calculation
return Math.floor(base * 1e9) // 9 decimals expected by SDK
}
// Calculate sizes in USD for each TP
// CRITICAL FIX: TP2 must be percentage of REMAINING size after TP1, not original size
const tp1USD = (options.positionSizeUSD * options.tp1SizePercent) / 100
const remainingAfterTP1 = options.positionSizeUSD - tp1USD
const tp2USD = (remainingAfterTP1 * options.tp2SizePercent) / 100
console.log(`📊 Exit order sizes:`)
console.log(` TP1: ${options.tp1SizePercent}% of $${options.positionSizeUSD.toFixed(2)} = $${tp1USD.toFixed(2)}`)
console.log(` Remaining after TP1: $${remainingAfterTP1.toFixed(2)}`)
console.log(` TP2: ${options.tp2SizePercent}% of remaining = $${tp2USD.toFixed(2)}`)
console.log(` Runner (if any): $${(remainingAfterTP1 - tp2USD).toFixed(2)}`)
// For orders that close a long, the order direction should be SHORT (sell)
const orderDirection = options.direction === 'long' ? PositionDirection.SHORT : PositionDirection.LONG
// Place TP1 LIMIT reduce-only
if (tp1USD > 0) {
const baseAmount = usdToBase(tp1USD)
if (baseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) {
const orderParams: any = {
orderType: OrderType.LIMIT,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(baseAmount),
price: new BN(Math.floor(options.tp1Price * 1e6)), // price in 1e6
reduceOnly: true,
}
console.log('🚧 Placing TP1 limit order (reduce-only)...')
const sig = await (driftClient as any).placePerpOrder(orderParams)
console.log('✅ TP1 order placed:', sig)
signatures.push(sig)
} else {
console.log('⚠️ TP1 size below market min, skipping on-chain TP1')
}
}
// Place TP2 LIMIT reduce-only
if (tp2USD > 0) {
const baseAmount = usdToBase(tp2USD)
if (baseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) {
const orderParams: any = {
orderType: OrderType.LIMIT,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(baseAmount),
price: new BN(Math.floor(options.tp2Price * 1e6)),
reduceOnly: true,
}
console.log('🚧 Placing TP2 limit order (reduce-only)...')
const sig = await (driftClient as any).placePerpOrder(orderParams)
console.log('✅ TP2 order placed:', sig)
signatures.push(sig)
} else {
console.log('⚠️ TP2 size below market min, skipping on-chain TP2')
}
}
// Place Stop-Loss order(s)
// Supports three modes:
// 1. Dual Stop System (soft stop-limit + hard stop-market)
// 2. Single TRIGGER_LIMIT (for liquid markets)
// 3. Single TRIGGER_MARKET (default, guaranteed execution)
const slUSD = options.positionSizeUSD
const slBaseAmount = usdToBase(slUSD)
if (slBaseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) {
const useDualStops = options.useDualStops ?? false
if (useDualStops && options.softStopPrice && options.hardStopPrice) {
// ============== DUAL STOP SYSTEM ==============
console.log('🛡️🛡️ Placing DUAL STOP SYSTEM...')
// 1. Soft Stop (TRIGGER_LIMIT) - Avoids wicks
const softStopBuffer = options.softStopBuffer ?? 0.4
const softStopMultiplier = options.direction === 'long'
? (1 - softStopBuffer / 100)
: (1 + softStopBuffer / 100)
const softStopParams: any = {
orderType: OrderType.TRIGGER_LIMIT,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.softStopPrice * 1e6)),
price: new BN(Math.floor(options.softStopPrice * softStopMultiplier * 1e6)),
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW
: OrderTriggerCondition.ABOVE,
reduceOnly: true,
}
console.log(` 1⃣ Soft Stop (TRIGGER_LIMIT):`)
console.log(` Trigger: $${options.softStopPrice.toFixed(4)}`)
console.log(` Limit: $${(options.softStopPrice * softStopMultiplier).toFixed(4)}`)
console.log(` Purpose: Avoid false breakouts/wicks`)
const softStopSig = await (driftClient as any).placePerpOrder(softStopParams)
console.log(` ✅ Soft stop placed: ${softStopSig}`)
signatures.push(softStopSig)
// 2. Hard Stop (TRIGGER_MARKET) - Guarantees exit
const hardStopParams: any = {
orderType: OrderType.TRIGGER_MARKET,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.hardStopPrice * 1e6)),
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW
: OrderTriggerCondition.ABOVE,
reduceOnly: true,
}
console.log(` 2⃣ Hard Stop (TRIGGER_MARKET):`)
console.log(` Trigger: $${options.hardStopPrice.toFixed(4)}`)
console.log(` Purpose: Guaranteed exit if soft stop doesn't fill`)
const hardStopSig = await (driftClient as any).placePerpOrder(hardStopParams)
console.log(` ✅ Hard stop placed: ${hardStopSig}`)
signatures.push(hardStopSig)
console.log(`🎯 Dual stop system active: Soft @ $${options.softStopPrice.toFixed(2)} | Hard @ $${options.hardStopPrice.toFixed(2)}`)
} else {
// ============== SINGLE STOP SYSTEM ==============
const useStopLimit = options.useStopLimit ?? false
const stopLimitBuffer = options.stopLimitBuffer ?? 0.5
if (useStopLimit) {
// TRIGGER_LIMIT: For liquid markets
const limitPriceMultiplier = options.direction === 'long'
? (1 - stopLimitBuffer / 100)
: (1 + stopLimitBuffer / 100)
const orderParams: any = {
orderType: OrderType.TRIGGER_LIMIT,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.stopLossPrice * 1e6)),
price: new BN(Math.floor(options.stopLossPrice * limitPriceMultiplier * 1e6)),
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW
: OrderTriggerCondition.ABOVE,
reduceOnly: true,
}
console.log(`🛡️ Placing SL as TRIGGER_LIMIT (${stopLimitBuffer}% buffer)...`)
console.log(` Trigger: ${options.direction === 'long' ? 'BELOW' : 'ABOVE'} $${options.stopLossPrice.toFixed(4)}`)
console.log(` Limit: $${(options.stopLossPrice * limitPriceMultiplier).toFixed(4)}`)
console.log(` ⚠️ May not fill during fast moves - use for liquid markets only!`)
const sig = await (driftClient as any).placePerpOrder(orderParams)
console.log('✅ SL trigger-limit order placed:', sig)
signatures.push(sig)
} else {
// TRIGGER_MARKET: Default, guaranteed execution
const orderParams: any = {
orderType: OrderType.TRIGGER_MARKET,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.stopLossPrice * 1e6)),
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW
: OrderTriggerCondition.ABOVE,
reduceOnly: true,
}
console.log(`🛡️ Placing SL as TRIGGER_MARKET (guaranteed execution - RECOMMENDED)...`)
console.log(` Trigger: ${options.direction === 'long' ? 'BELOW' : 'ABOVE'} $${options.stopLossPrice.toFixed(4)}`)
console.log(` ✅ Will execute at market price when triggered (may slip but WILL fill)`)
const sig = await (driftClient as any).placePerpOrder(orderParams)
console.log('✅ SL trigger-market order placed:', sig)
signatures.push(sig)
}
}
} else {
console.log('⚠️ SL size below market min, skipping on-chain SL')
}
return { success: true, signatures }
} catch (error) {
console.error('❌ Failed to place exit orders:', error)
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }
}
}
/**
* Close a position (partially or fully) with a market order
*/
export async function closePosition(
params: ClosePositionParams
): Promise<ClosePositionResult> {
try {
console.log('📊 Closing position:', params)
const driftService = getDriftService()
const marketConfig = getMarketConfig(params.symbol)
const driftClient = driftService.getClient()
// Get current position
const position = await driftService.getPosition(marketConfig.driftMarketIndex)
if (!position || position.side === 'none') {
throw new Error(`No active position for ${params.symbol}`)
}
// Calculate size to close
let sizeToClose = position.size * (params.percentToClose / 100)
// CRITICAL FIX: If calculated size is below minimum, close 100% instead
// This prevents "runner" positions from being too small to close
if (sizeToClose < marketConfig.minOrderSize) {
console.log(`⚠️ Calculated close size ${sizeToClose.toFixed(4)} is below minimum ${marketConfig.minOrderSize}`)
console.log(` Forcing 100% close to avoid Drift rejection`)
sizeToClose = position.size // Close entire position
}
console.log(`📝 Close order details:`)
console.log(` Current position: ${position.size.toFixed(4)} ${position.side}`)
console.log(` Closing: ${params.percentToClose}% (${sizeToClose.toFixed(4)})`)
console.log(` Entry price: $${position.entryPrice.toFixed(4)}`)
console.log(` Unrealized P&L: $${position.unrealizedPnL.toFixed(2)}`)
// Get current oracle price
const oraclePrice = await driftService.getOraclePrice(marketConfig.driftMarketIndex)
console.log(` Current price: $${oraclePrice.toFixed(4)}`)
// Check DRY_RUN mode
const isDryRun = process.env.DRY_RUN === 'true'
if (isDryRun) {
console.log('🧪 DRY RUN MODE: Simulating close order (not executing on blockchain)')
// Calculate realized P&L with leverage (default 10x in dry run)
const profitPercent = ((oraclePrice - position.entryPrice) / position.entryPrice) * 100 * (position.side === 'long' ? 1 : -1)
const closedNotional = sizeToClose * oraclePrice
const realizedPnL = (closedNotional * profitPercent) / 100
const accountPnLPercent = profitPercent * 10 // display using default leverage
const mockTxSig = `DRY_RUN_CLOSE_${Date.now()}_${Math.random().toString(36).substring(7)}`
console.log(`💰 Simulated close:`)
console.log(` Close price: $${oraclePrice.toFixed(4)}`)
console.log(` Profit %: ${profitPercent.toFixed(3)}% → Account P&L (10x): ${accountPnLPercent.toFixed(2)}%`)
console.log(` Realized P&L: $${realizedPnL.toFixed(2)}`)
return {
success: true,
transactionSignature: mockTxSig,
closePrice: oraclePrice,
closedSize: sizeToClose,
realizedPnL,
}
}
// Prepare close order (opposite direction) - use simple structure like v3
const orderParams = {
orderType: OrderType.MARKET,
marketIndex: marketConfig.driftMarketIndex,
direction: position.side === 'long'
? PositionDirection.SHORT
: PositionDirection.LONG,
baseAssetAmount: new BN(Math.floor(sizeToClose * 1e9)), // 9 decimals
reduceOnly: true, // Important: only close existing position
}
// Place market close order using simple placePerpOrder (like v3)
console.log('🚀 Placing REAL market close order...')
const txSig = await driftClient.placePerpOrder(orderParams)
console.log(`✅ Close order placed! Transaction: ${txSig}`)
// CRITICAL: Confirm transaction on-chain to prevent phantom closes
console.log('⏳ Confirming transaction on-chain...')
const connection = driftService.getConnection()
const confirmation = await connection.confirmTransaction(txSig, 'confirmed')
if (confirmation.value.err) {
console.error('❌ Transaction failed on-chain:', confirmation.value.err)
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`)
}
console.log('✅ Transaction confirmed on-chain')
// Calculate realized P&L with leverage
// CRITICAL: P&L must account for leverage and be calculated on USD notional, not base asset size
const profitPercent = ((oraclePrice - position.entryPrice) / position.entryPrice) * 100 * (position.side === 'long' ? 1 : -1)
// Get leverage from user account (defaults to 10x if not found)
let leverage = 10
try {
const userAccount = driftClient.getUserAccount()
if (userAccount && userAccount.maxMarginRatio) {
// maxMarginRatio is in 1e4 scale, leverage = 1 / (margin / 10000)
leverage = 10000 / Number(userAccount.maxMarginRatio)
}
} catch (err) {
console.log('⚠️ Could not determine leverage from account, using 10x default')
}
// Calculate closed notional value (USD)
const closedNotional = sizeToClose * oraclePrice
const realizedPnL = (closedNotional * profitPercent) / 100
const accountPnLPercent = profitPercent * leverage
console.log(`💰 Close details:`)
console.log(` Close price: $${oraclePrice.toFixed(4)}`)
console.log(` Profit %: ${profitPercent.toFixed(3)}% | Account P&L (${leverage}x): ${accountPnLPercent.toFixed(2)}%`)
console.log(` Closed notional: $${closedNotional.toFixed(2)}`)
console.log(` Realized P&L: $${realizedPnL.toFixed(2)}`)
// If closing 100%, cancel all remaining orders for this market
if (params.percentToClose === 100) {
console.log('🗑️ Position fully closed, cancelling remaining orders...')
const cancelResult = await cancelAllOrders(params.symbol)
if (cancelResult.success && cancelResult.cancelledCount! > 0) {
console.log(`✅ Cancelled ${cancelResult.cancelledCount} orders`)
}
}
return {
success: true,
transactionSignature: txSig,
closePrice: oraclePrice,
closedSize: sizeToClose,
realizedPnL,
}
} catch (error) {
console.error('❌ Failed to close position:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}
/**
* Cancel all open orders for a specific market
*/
export async function cancelAllOrders(
symbol: string
): Promise<{ success: boolean; cancelledCount?: number; error?: string }> {
try {
console.log(`🗑️ Cancelling all orders for ${symbol}...`)
// Ensure Drift service is initialized
let driftService = getDriftService()
if (!driftService) {
console.log('⚠️ Drift service not initialized, initializing now...')
driftService = await initializeDriftService()
}
const driftClient = driftService.getClient()
const marketConfig = getMarketConfig(symbol)
const isDryRun = process.env.DRY_RUN === 'true'
if (isDryRun) {
console.log('🧪 DRY RUN: Simulating order cancellation')
return { success: true, cancelledCount: 0 }
}
// Get user account to check for orders
const userAccount = driftClient.getUserAccount()
if (!userAccount) {
throw new Error('User account not found')
}
// Filter orders for this market (check for active orders, not just status)
// Note: Trigger orders may have different status values, so we check for non-zero orderId
const ordersToCancel = userAccount.orders.filter(
(order: any) =>
order.marketIndex === marketConfig.driftMarketIndex &&
order.orderId > 0 // Active orders have orderId > 0
)
if (ordersToCancel.length === 0) {
console.log('✅ No open orders to cancel')
return { success: true, cancelledCount: 0 }
}
console.log(`📋 Found ${ordersToCancel.length} open orders to cancel (including trigger orders)`)
// Cancel all orders for this market (cancels all types: LIMIT, TRIGGER_MARKET, TRIGGER_LIMIT)
const txSig = await driftClient.cancelOrders(
undefined, // Cancel by market type
marketConfig.driftMarketIndex,
undefined // No specific direction filter
)
console.log(`✅ Orders cancelled! Transaction: ${txSig}`)
return {
success: true,
cancelledCount: ordersToCancel.length,
}
} catch (error) {
console.error('❌ Failed to cancel orders:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}
/**
* Close entire position for a market
*/
export async function closeEntirePosition(
symbol: string,
slippageTolerance: number = 1.0
): Promise<ClosePositionResult> {
return closePosition({
symbol,
percentToClose: 100,
slippageTolerance,
})
}
/**
* Emergency close all positions
*/
export async function emergencyCloseAll(): Promise<{
success: boolean
results: Array<{
symbol: string
result: ClosePositionResult
}>
}> {
console.log('🚨 EMERGENCY: Closing all positions')
try {
const driftService = getDriftService()
const positions = await driftService.getAllPositions()
if (positions.length === 0) {
console.log('✅ No positions to close')
return { success: true, results: [] }
}
const results = []
for (const position of positions) {
console.log(`🔴 Emergency closing ${position.symbol}...`)
const result = await closeEntirePosition(position.symbol, 2.0) // Allow 2% slippage
results.push({
symbol: position.symbol,
result,
})
}
console.log('✅ Emergency close complete')
return {
success: true,
results,
}
} catch (error) {
console.error('❌ Emergency close failed:', error)
return {
success: false,
results: [],
}
}
}