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:
mindesbunister
2025-10-26 13:30:07 +01:00
parent 993ae64c64
commit 4cc294baef
6 changed files with 365 additions and 17 deletions

View File

@@ -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
*/