- Add Pyth Network price monitoring (WebSocket + polling fallback) - Add Position Manager with automatic exit logic (TP1/TP2/SL) - Implement dynamic stop-loss adjustment (breakeven + profit lock) - Add real-time P&L tracking and multi-position support - Create comprehensive test suite (3 test scripts) - Add 5 detailed documentation files (2500+ lines) - Update configuration to $50 position size for safe testing - All Phase 2 features complete and tested Core Components: - v4/lib/pyth/price-monitor.ts - Real-time price monitoring - v4/lib/trading/position-manager.ts - Autonomous position management - v4/app/api/trading/positions/route.ts - Query positions endpoint - v4/test-*.ts - Comprehensive testing suite Documentation: - PHASE_2_COMPLETE_REPORT.md - Implementation summary - v4/PHASE_2_SUMMARY.md - Detailed feature overview - v4/TESTING.md - Testing guide - v4/QUICKREF_PHASE2.md - Quick reference - install-phase2.sh - Automated installation script
281 lines
8.2 KiB
TypeScript
281 lines
8.2 KiB
TypeScript
/**
|
|
* Drift Order Execution
|
|
*
|
|
* Handles opening and closing positions with market orders
|
|
*/
|
|
|
|
import { getDriftService } from './client'
|
|
import { getMarketConfig } from '../../config/trading'
|
|
import {
|
|
MarketType,
|
|
PositionDirection,
|
|
OrderType,
|
|
OrderParams,
|
|
} 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
|
|
}
|
|
|
|
/**
|
|
* 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)}`)
|
|
|
|
// Prepare order parameters
|
|
const orderParams: OrderParams = {
|
|
orderType: OrderType.MARKET,
|
|
marketIndex: marketConfig.driftMarketIndex,
|
|
marketType: MarketType.PERP,
|
|
direction: params.direction === 'long'
|
|
? PositionDirection.LONG
|
|
: PositionDirection.SHORT,
|
|
baseAssetAmount: BigInt(Math.floor(baseAssetSize * 1e9)), // 9 decimals
|
|
// Optional: add price limit for protection
|
|
// price: BigInt(Math.floor(worstPrice * 1e6)), // 6 decimals
|
|
}
|
|
|
|
// Place market order
|
|
console.log('🚀 Placing market order...')
|
|
const txSig = await driftClient.placeAndTakePerpOrder(orderParams)
|
|
|
|
console.log(`✅ Order placed! Transaction: ${txSig}`)
|
|
|
|
// Wait for confirmation
|
|
await driftClient.txSender.confirmTransaction(txSig)
|
|
console.log('✅ Transaction confirmed')
|
|
|
|
// Get actual fill price from position
|
|
const position = await driftService.getPosition(marketConfig.driftMarketIndex)
|
|
|
|
if (!position) {
|
|
throw new Error('Position not found after order execution')
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ Failed to open position:', 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)}`)
|
|
|
|
// Prepare close order (opposite direction)
|
|
const orderParams: OrderParams = {
|
|
orderType: OrderType.MARKET,
|
|
marketIndex: marketConfig.driftMarketIndex,
|
|
marketType: MarketType.PERP,
|
|
direction: position.side === 'long'
|
|
? PositionDirection.SHORT
|
|
: PositionDirection.LONG,
|
|
baseAssetAmount: BigInt(Math.floor(sizeToClose * 1e9)), // 9 decimals
|
|
reduceOnly: true, // Important: only close existing position
|
|
}
|
|
|
|
// Place market close order
|
|
console.log('🚀 Placing market close order...')
|
|
const txSig = await driftClient.placeAndTakePerpOrder(orderParams)
|
|
|
|
console.log(`✅ Close order placed! Transaction: ${txSig}`)
|
|
|
|
// Wait for confirmation
|
|
await driftClient.txSender.confirmTransaction(txSig)
|
|
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: [],
|
|
}
|
|
}
|
|
}
|