#!/usr/bin/env npx tsx // Place a trigger order (stop loss or take profit) import { getClient } from '../core/client.js'; import { formatUsd, parseArgs } from '../core/utils.js'; function printUsage() { console.log(` Open Broker - Trigger Order =========================== Place a trigger order that activates when price reaches a target level. Used for stop losses, take profits, and conditional entries. Usage: npx tsx scripts/operations/trigger-order.ts --coin --side --size --trigger --type Options: --coin Asset to trade (e.g., ETH, BTC, HYPE) --side Order side when triggered: buy or sell --size Order size in base asset --trigger Trigger price (order activates when price reaches this) --type Order type: tp (take profit) or sl (stop loss) --limit Limit price when triggered (default: trigger price for TP, with slippage for SL) --slippage Slippage for SL in bps (default: 100 = 1%) --exec Execution when triggered: market or limit (default: market for SL, limit for TP) --leverage Set leverage (e.g., 10 for 10x). Cross for main perps, isolated for HIP-3 --reduce Reduce-only order (default: true for TP/SL) --dry Dry run - show order without placing Trigger Order Behavior: - Order is dormant until price reaches trigger level - TP: rests as a limit order at the limit price (favorable) - SL: fires as a market order capped by the limit price (trigger ± slippage); pass --exec limit for a stop-limit, but note a gap move past the band can skip the stop entirely and leave the position unprotected Examples: # Take profit: sell 0.5 HYPE when price rises to $40 npx tsx scripts/operations/trigger-order.ts --coin HYPE --side sell --size 0.5 --trigger 40 --type tp # Stop loss: sell 0.5 HYPE when price drops to $30 npx tsx scripts/operations/trigger-order.ts --coin HYPE --side sell --size 0.5 --trigger 30 --type sl # Buy stop: buy BTC when it breaks above $75000 npx tsx scripts/operations/trigger-order.ts --coin BTC --side buy --size 0.01 --trigger 75000 --type sl --reduce false # Preview order npx tsx scripts/operations/trigger-order.ts --coin ETH --side sell --size 0.1 --trigger 4000 --type tp --dry `); } async function main() { const args = parseArgs(process.argv.slice(2)); if (args.help) { printUsage(); process.exit(0); } const coin = args.coin as string; const side = args.side as string; const size = parseFloat(args.size as string); const triggerPrice = parseFloat(args.trigger as string); const orderType = args.type as string; const limitPriceOverride = args.limit ? parseFloat(args.limit as string) : undefined; const slippageBps = args.slippage ? parseInt(args.slippage as string) : 100; const execOverride = args.exec as string | undefined; if (execOverride && execOverride !== 'market' && execOverride !== 'limit') { console.error('Error: --exec must be "market" or "limit"'); process.exit(1); } const leverage = args.leverage ? parseInt(args.leverage as string) : undefined; const reduceOnly = args.reduce !== 'false'; // Default true const dryRun = args.dry as boolean; if (!coin || !side || isNaN(size) || isNaN(triggerPrice) || !orderType) { printUsage(); process.exit(1); } if (side !== 'buy' && side !== 'sell') { console.error('Error: --side must be "buy" or "sell"'); process.exit(1); } if (orderType !== 'tp' && orderType !== 'sl') { console.error('Error: --type must be "tp" or "sl"'); process.exit(1); } if (size <= 0 || triggerPrice <= 0) { console.error('Error: --size and --trigger must be positive'); process.exit(1); } const isBuy = side === 'buy'; const tpsl = orderType as 'tp' | 'sl'; const client = getClient(); if (args.verbose) { client.verbose = true; } console.log('Open Broker - Trigger Order'); console.log('===========================\n'); try { // Get current price const mids = await client.getAllMids(); const currentPrice = parseFloat(mids[coin]); if (!currentPrice) { console.error(`Error: No market data for ${coin}`); process.exit(1); } // Calculate limit price let limitPrice: number; if (limitPriceOverride) { limitPrice = limitPriceOverride; } else if (tpsl === 'tp') { // TP: use trigger price as limit (favorable) limitPrice = triggerPrice; } else { // SL: slippage band past the trigger — caps the market fill, or is the // resting price for a stop-limit const slippageMult = slippageBps / 10000; limitPrice = isBuy ? triggerPrice * (1 + slippageMult) : triggerPrice * (1 - slippageMult); } // SL fires as a market trigger by default (a stop-limit can be gapped // over and never fill); TP rests as a limit at the target. const isMarket = execOverride ? execOverride === 'market' : tpsl === 'sl'; const distanceFromCurrent = ((triggerPrice - currentPrice) / currentPrice) * 100; const notional = triggerPrice * size; console.log('Trigger Order Details'); console.log('---------------------'); console.log(`Coin: ${coin}`); console.log(`Type: ${tpsl === 'tp' ? 'Take Profit' : 'Stop Loss'}`); console.log(`Side: ${isBuy ? 'BUY' : 'SELL'} (when triggered)`); console.log(`Size: ${size}`); console.log(`Current Price: ${formatUsd(currentPrice)}`); console.log(`Trigger Price: ${formatUsd(triggerPrice)} (${distanceFromCurrent >= 0 ? '+' : ''}${distanceFromCurrent.toFixed(2)}%)`); console.log(`Execution: ${isMarket ? `market (fill capped at ${formatUsd(limitPrice)})` : `limit @ ${formatUsd(limitPrice)}`}`); console.log(`Reduce Only: ${reduceOnly ? 'Yes' : 'No'}`); console.log(`Est. Notional: ${formatUsd(notional)}`); // Sanity checks if (tpsl === 'tp') { if (isBuy && triggerPrice > currentPrice) { console.log(`\n⚠️ Warning: Buy TP above current price is unusual`); } if (!isBuy && triggerPrice < currentPrice) { console.log(`\n⚠️ Warning: Sell TP below current price is unusual`); } } else { if (isBuy && triggerPrice < currentPrice) { console.log(`\n⚠️ Warning: Buy SL below current price - are you shorting?`); } if (!isBuy && triggerPrice > currentPrice) { console.log(`\n⚠️ Warning: Sell SL above current price - are you longing?`); } } if (dryRun) { console.log('\n🔍 Dry run - order not placed'); return; } console.log('\nPlacing trigger order...'); const response = await client.triggerOrder( coin, isBuy, size, triggerPrice, limitPrice, tpsl, reduceOnly, leverage, isMarket ); console.log('\nResult'); console.log('------'); if (response.status === 'ok' && response.response && typeof response.response === 'object') { const statuses = response.response.data.statuses; for (const status of statuses) { if (status.resting) { console.log(`✅ Trigger order placed`); console.log(` Order ID: ${status.resting.oid}`); console.log(` Trigger: ${formatUsd(triggerPrice)}`); console.log(` Limit: ${formatUsd(limitPrice)}`); console.log(`\n Order will ${isBuy ? 'BUY' : 'SELL'} ${size} ${coin} when price reaches ${formatUsd(triggerPrice)}`); } else if (status.error) { console.log(`❌ Error: ${status.error}`); } else { console.log(`⚠️ Status:`, JSON.stringify(status, null, 2)); } } } else { console.log(`❌ Failed: ${typeof response.response === 'string' ? response.response : JSON.stringify(response)}`); } } catch (error) { console.error('Error:', error); process.exit(1); } } main();