- Add PostgreSQL database with Prisma ORM - Trade model: tracks entry/exit, P&L, order signatures, config snapshots - PriceUpdate model: tracks price movements for drawdown analysis - SystemEvent model: logs errors and system events - DailyStats model: aggregated performance metrics - Implement dual stop loss system (enabled by default) - Soft stop (TRIGGER_LIMIT) at -1.5% to avoid wicks - Hard stop (TRIGGER_MARKET) at -2.5% to guarantee exit - Configurable via USE_DUAL_STOPS, SOFT_STOP_PERCENT, HARD_STOP_PERCENT - Backward compatible with single stop modes - Add database service layer (lib/database/trades.ts) - createTrade(): save new trades with all details - updateTradeExit(): close trades with P&L calculations - addPriceUpdate(): track price movements during trade - getTradeStats(): calculate win rate, profit factor, avg win/loss - logSystemEvent(): log errors and system events - Update execute endpoint to use dual stops and save to database - Calculate dual stop prices when enabled - Pass dual stop parameters to placeExitOrders - Save complete trade record to database after execution - Add test trade button to settings page - New /api/trading/test endpoint for executing test trades - Displays detailed results including dual stop prices - Confirmation dialog before execution - Shows entry price, position size, stops, and TX signature - Generate Prisma client in Docker build - Update DATABASE_URL for container networking
579 lines
20 KiB
TypeScript
579 lines
20 KiB
TypeScript
/**
|
||
* Drift Order Execution
|
||
*
|
||
* Handles opening and closing positions with market orders
|
||
*/
|
||
|
||
import { getDriftService } 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
|
||
slippage?: number
|
||
error?: string
|
||
}
|
||
|
||
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
|
||
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,
|
||
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(`✅ Order placed! Transaction: ${txSig}`)
|
||
|
||
// 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 (optional - may not be immediate in DRY_RUN)
|
||
const position = await driftService.getPosition(marketConfig.driftMarketIndex)
|
||
|
||
if (position && position.side !== 'none') {
|
||
const fillPrice = position.entryPrice
|
||
const slippage = Math.abs((fillPrice - oraclePrice) / oraclePrice) * 100
|
||
|
||
console.log(`💰 Fill details:`)
|
||
console.log(` Fill price: $${fillPrice.toFixed(4)}`)
|
||
console.log(` Slippage: ${slippage.toFixed(3)}%`)
|
||
|
||
return {
|
||
success: true,
|
||
transactionSignature: txSig,
|
||
fillPrice,
|
||
fillSize: baseAssetSize,
|
||
slippage,
|
||
}
|
||
} 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,
|
||
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
|
||
const usdToBase = (usd: number, price: number) => {
|
||
const base = usd / price
|
||
return Math.floor(base * 1e9) // 9 decimals expected by SDK
|
||
}
|
||
|
||
// Calculate sizes in USD for each TP
|
||
const tp1USD = (options.positionSizeUSD * options.tp1SizePercent) / 100
|
||
const tp2USD = (options.positionSizeUSD * options.tp2SizePercent) / 100
|
||
|
||
// 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, options.tp1Price)
|
||
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, options.tp2Price)
|
||
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, options.stopLossPrice)
|
||
|
||
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
|
||
const sizeToClose = position.size * (params.percentToClose / 100)
|
||
|
||
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
|
||
const pnlPerUnit = oraclePrice - position.entryPrice
|
||
const realizedPnL = pnlPerUnit * sizeToClose * (position.side === 'long' ? 1 : -1)
|
||
|
||
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(` 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}`)
|
||
|
||
// Wait for confirmation (transaction is likely already confirmed by placeAndTakePerpOrder)
|
||
console.log('⏳ Waiting for transaction confirmation...')
|
||
console.log('✅ Transaction confirmed')
|
||
|
||
// Calculate realized P&L
|
||
const pnlPerUnit = oraclePrice - position.entryPrice
|
||
const realizedPnL = pnlPerUnit * sizeToClose * (position.side === 'long' ? 1 : -1)
|
||
|
||
console.log(`💰 Close details:`)
|
||
console.log(` Close price: $${oraclePrice.toFixed(4)}`)
|
||
console.log(` Realized P&L: $${realizedPnL.toFixed(2)}`)
|
||
|
||
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',
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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: [],
|
||
}
|
||
}
|
||
}
|