feat: Add on-chain TP/SL order placement
- Add placeExitOrders() to create reduce-only LIMIT orders for TP1, TP2, and SL - Orders now visible in Drift UI - Tested with real tiny position (0 base x 5x = 0) - All 3 exit orders placed successfully on-chain - Position manager continues monitoring as backup - Added test script and results documentation
This commit is contained in:
@@ -46,6 +46,12 @@ export interface ClosePositionResult {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface PlaceExitOrdersResult {
|
||||
success: boolean
|
||||
signatures?: string[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a position with a market order
|
||||
*/
|
||||
@@ -163,6 +169,130 @@ export async function openPosition(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Place on-chain exit orders (reduce-only LIMIT orders) so TP/SL show up in Drift UI.
|
||||
* This places reduce-only LIMIT orders for TP1, TP2 and a stop-loss LIMIT order.
|
||||
* NOTE: For a safer, more aggressive stop you'd want a trigger-market order; here
|
||||
* we use reduce-only LIMIT orders to ensure they are visible in the UI and low-risk.
|
||||
*/
|
||||
export async function placeExitOrders(options: {
|
||||
symbol: string
|
||||
positionSizeUSD: number
|
||||
tp1Price: number
|
||||
tp2Price: number
|
||||
stopLossPrice: number
|
||||
tp1SizePercent: number // percent of position to close at TP1 (0-100)
|
||||
tp2SizePercent: number // percent of position to close at TP2 (0-100)
|
||||
direction: 'long' | 'short'
|
||||
}): 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 LIMIT reduce-only (note: trigger-market would be preferable)
|
||||
const slUSD = options.positionSizeUSD // place full-size SL
|
||||
const slBaseAmount = usdToBase(slUSD, options.stopLossPrice)
|
||||
if (slBaseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) {
|
||||
const orderParams: any = {
|
||||
orderType: OrderType.LIMIT,
|
||||
marketIndex: marketConfig.driftMarketIndex,
|
||||
direction: orderDirection,
|
||||
baseAssetAmount: new BN(slBaseAmount),
|
||||
price: new BN(Math.floor(options.stopLossPrice * 1e6)),
|
||||
reduceOnly: true,
|
||||
}
|
||||
|
||||
console.log('🚧 Placing SL limit order (reduce-only)...')
|
||||
const sig = await (driftClient as any).placePerpOrder(orderParams)
|
||||
console.log('✅ SL 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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user