feat: Use TRIGGER_MARKET for stop loss (guaranteed execution)

- Changed SL from TRIGGER_LIMIT to TRIGGER_MARKET
- Guarantees SL execution even during volatile moves/gaps
- Added optional TRIGGER_LIMIT mode for very liquid markets
- TP orders remain as LIMIT for price precision
- Follows best practice: stop-market for SL, limit for TP
This commit is contained in:
mindesbunister
2025-10-26 20:34:15 +01:00
parent f733c11711
commit 33821eae0c

View File

@@ -52,6 +52,19 @@ export interface PlaceExitOrdersResult {
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%)
}
/**
* Open a position with a market order
*/
@@ -171,21 +184,16 @@ 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.
* 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: {
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> {
export async function placeExitOrders(options: PlaceExitOrdersOptions): Promise<PlaceExitOrdersResult> {
try {
console.log('🛡️ Placing exit orders on-chain:', options.symbol)
@@ -265,29 +273,65 @@ export async function placeExitOrders(options: {
}
}
// Place Stop-Loss as TRIGGER_LIMIT order (proper stop loss that shows on Drift UI)
const slUSD = options.positionSizeUSD // place full-size SL
// Place Stop-Loss order
// Default: TRIGGER_MARKET (guaranteed execution, RECOMMENDED for most traders)
// Optional: TRIGGER_LIMIT with buffer (only for very liquid markets to avoid extreme wicks)
const slUSD = options.positionSizeUSD
const slBaseAmount = usdToBase(slUSD, options.stopLossPrice)
if (slBaseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) {
const orderParams: any = {
orderType: OrderType.TRIGGER_LIMIT, // Use TRIGGER_LIMIT (not LIMIT with trigger)
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.stopLossPrice * 1e6)), // trigger price
price: new BN(Math.floor(options.stopLossPrice * 0.995 * 1e6)), // limit price slightly lower (0.5% buffer)
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW // Long: trigger when price drops below
: OrderTriggerCondition.ABOVE, // Short: trigger when price rises above
reduceOnly: true,
const useStopLimit = options.useStopLimit ?? false
const stopLimitBuffer = options.stopLimitBuffer ?? 0.5 // default 0.5% buffer
if (useStopLimit) {
// TRIGGER_LIMIT: Protects against extreme wicks but may not fill during fast moves
const limitPriceMultiplier = options.direction === 'long'
? (1 - stopLimitBuffer / 100) // Long: limit below trigger
: (1 + stopLimitBuffer / 100) // Short: limit above trigger
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: Guaranteed execution (RECOMMENDED)
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)
}
console.log('🚧 Placing SL trigger-limit order (reduce-only)...')
console.log(` Trigger: ${options.direction === 'long' ? 'BELOW' : 'ABOVE'} $${options.stopLossPrice.toFixed(4)}`)
console.log(` Limit price: $${(options.stopLossPrice * 0.995).toFixed(4)}`)
const sig = await (driftClient as any).placePerpOrder(orderParams)
console.log('✅ SL trigger-limit order placed:', sig)
signatures.push(sig)
} else {
console.log('⚠️ SL size below market min, skipping on-chain SL')
}