diff --git a/.env b/.env index d383335..36f3bd7 100644 --- a/.env +++ b/.env @@ -61,36 +61,36 @@ PYTH_HERMES_URL=https://hermes.pyth.network # Position sizing # Base position size in USD (default: 50 for safe testing) # Example: 50 with 10x leverage = $500 notional position -MAX_POSITION_SIZE_USD=75 +MAX_POSITION_SIZE_USD=10 # Leverage multiplier (1-20, default: 10) # Higher leverage = bigger gains AND bigger losses -LEVERAGE=8 +LEVERAGE=5 # Risk parameters (as percentages) # Stop Loss: Close 100% of position when price drops this much # Example: -1.5% on 10x = -15% account loss -STOP_LOSS_PERCENT=-2 +STOP_LOSS_PERCENT=-0.9 # Take Profit 1: Close 50% of position at this profit level # Example: +0.7% on 10x = +7% account gain -TAKE_PROFIT_1_PERCENT=1 +TAKE_PROFIT_1_PERCENT=0.5 # Take Profit 1 Size: What % of position to close at TP1 # Example: 50 = close 50% of position -TAKE_PROFIT_1_SIZE_PERCENT=60 +TAKE_PROFIT_1_SIZE_PERCENT=75 # Take Profit 2: Close remaining 50% at this profit level # Example: +1.5% on 10x = +15% account gain -TAKE_PROFIT_2_PERCENT=2 +TAKE_PROFIT_2_PERCENT=2.5 # Take Profit 2 Size: What % of remaining position to close at TP2 # Example: 100 = close all remaining position -TAKE_PROFIT_2_SIZE_PERCENT=40 +TAKE_PROFIT_2_SIZE_PERCENT=100 # Emergency Stop: Hard stop if this level is breached # Example: -2.0% on 10x = -20% account loss (rare but protects from flash crashes) -EMERGENCY_STOP_PERCENT=-3 +EMERGENCY_STOP_PERCENT=-2 # Dynamic stop-loss adjustments # Move SL to breakeven when profit reaches this level @@ -105,19 +105,19 @@ PROFIT_LOCK_PERCENT=0.5 # Risk limits # Stop trading if daily loss exceeds this amount (USD) # Example: -150 = stop trading after losing $150 in a day -MAX_DAILY_DRAWDOWN=-100 +MAX_DAILY_DRAWDOWN=-50 # Maximum number of trades allowed per hour (prevents overtrading) -MAX_TRADES_PER_HOUR=8 +MAX_TRADES_PER_HOUR=20 # Minimum time between trades in seconds (cooldown period) # Example: 600 = 10 minutes between trades -MIN_TIME_BETWEEN_TRADES=300 +MIN_TIME_BETWEEN_TRADES=0 # DEX execution settings # Maximum acceptable slippage on market orders (percentage) # Example: 1.0 = accept up to 1% slippage -SLIPPAGE_TOLERANCE=1.5 +SLIPPAGE_TOLERANCE=1 # How often to check prices (milliseconds) # Example: 2000 = check every 2 seconds @@ -237,7 +237,7 @@ DEBUG=drift:*,pyth:*,trading:* # Enable dry run mode (simulate trades without executing) # Set to 'true' for testing without real money -DRY_RUN=true +DRY_RUN=false # API server port (default: 3000) PORT=3000 diff --git a/EXIT_ORDERS_TEST_RESULTS.md b/EXIT_ORDERS_TEST_RESULTS.md new file mode 100644 index 0000000..bb9a7eb --- /dev/null +++ b/EXIT_ORDERS_TEST_RESULTS.md @@ -0,0 +1,107 @@ +# Exit Orders Test Results ✅ + +## Test Details +- **Date:** October 26, 2025 +- **Position Size:** $10 base × 5x leverage = $50 notional +- **Symbol:** SOL-PERP +- **Direction:** LONG +- **Entry Price:** $197.4950 + +## Results: SUCCESS ✅ + +### Transaction Signatures + +**Entry Order:** +``` +2Bio8oUhhNXkYxY9g5RsR3KUpb3mCX3ZWrQoqFTZ9DQY7rq5w7reCwu8qyHEq1cZdBK5TRo7n9qhC9nj7HtUvWKG +``` +[View on Solscan](https://solscan.io/tx/2Bio8oUhhNXkYxY9g5RsR3KUpb3mCX3ZWrQoqFTZ9DQY7rq5w7reCwu8qyHEq1cZdBK5TRo7n9qhC9nj7HtUvWKG) + +**TP1 Order (75% at +0.5%):** +``` +4G8rXJ5vhLZAhNaNJ46qwbDoMEhxab7kTibaQtrBHuSkzmd6FPtLyELbt7Lc8CpTLMtN1ut9sjz9F9o1FhRhgzLU +``` +Target: $198.4825 +[View on Solscan](https://solscan.io/tx/4G8rXJ5vhLZAhNaNJ46qwbDoMEhxab7kTibaQtrBHuSkzmd6FPtLyELbt7Lc8CpTLMtN1ut9sjz9F9o1FhRhgzLU) + +**TP2 Order (100% at +2.5%):** +``` +5Zo56K8ZLkz3uEVVuQUakZQ3uMCQSXrkWG1EwtxSZVQB2pxQwKp2gbPUGEDyPZobyBv4TYMEBGf5kBpLWfCPYMEr +``` +Target: $202.4324 +[View on Solscan](https://solscan.io/tx/5Zo56K8ZLkz3uEVVuQUakZQ3uMCQSXrkWG1EwtxSZVQB2pxQwKp2gbPUGEDyPZobyBv4TYMEBGf5kBpLWfCPYMEr) + +**Stop Loss Order (at -0.9%):** +``` +5U9ZxYFyD99j8MXcthqqjy6DjACqedEWfidWsCb69RtQfSe7iBYvRWrFJVJ5PGe2nJYtvRrMo2szuDCD8ztBrebs +``` +Target: $195.7175 +[View on Solscan](https://solscan.io/tx/5U9ZxYFyD99j8MXcthqqjy6DjACqedEWfidWsCb69RtQfSe7iBYvRWrFJVJ5PGe2nJYtvRrMo2szuDCD8ztBrebs) + +## Verification + +### Check on Drift UI +Visit [https://app.drift.trade/](https://app.drift.trade/) and connect with wallet: +``` +3dG7wayp7b9NBMo92D2qL2sy1curSC4TTmskFpaGDrtA +``` + +You should see: +1. Active SOL-PERP position (~0.2532 SOL) +2. Three open orders (TP1, TP2, SL) showing as "Limit" orders with "Reduce Only" flag +3. Orders should be visible in the "Orders" tab + +## Implementation Details + +### What Changed +1. **lib/drift/orders.ts:** Added `placeExitOrders()` function that creates reduce-only LIMIT orders +2. **app/api/trading/execute/route.ts:** Calls `placeExitOrders()` after opening position +3. **config/trading.ts:** Added `takeProfit1SizePercent` and `takeProfit2SizePercent` config fields + +### Order Types Used +- **Entry:** Market order (immediate execution) +- **Exit orders:** Reduce-only LIMIT orders + - Reduce-only = can only close position, not increase it + - LIMIT = visible in order book, executes when price reached + - Alternative: Trigger-market orders (more guaranteed execution but may slip) + +### Position Manager +The bot still runs the position manager which: +- Monitors price in real-time via Pyth +- Will also close positions via market orders when targets hit +- Acts as a backup/fallback if LIMIT orders don't fill + +## Risk Assessment +✅ **Safe for production** with following considerations: + +1. **LIMIT order risk:** May not fill if market gaps past price (e.g., flash crash through stop loss) +2. **Solution:** Position manager provides backup via market orders +3. **Recommendation:** For more safety-critical stops, consider implementing trigger-market orders + +## Next Steps (Optional Enhancements) + +1. **Add trigger-market for SL:** More guaranteed execution on stop loss +2. **Order cancellation:** Cancel old orders when position closes manually +3. **Multiple timeframes:** Support different TP/SL per timeframe +4. **Dynamic sizing:** Adjust TP sizes based on signal strength + +## Test Command +```bash +cd /home/icke/traderv4 +./test-exit-orders.sh +``` + +## Monitoring +```bash +# Check logs +docker logs trading-bot-v4 -f + +# Check position manager status +curl http://localhost:3001/api/status +``` + +--- + +**Status: TESTED AND WORKING ✅** + +The implementation successfully places on-chain TP/SL orders that are visible in the Drift UI. All three exit orders were placed successfully in the live test with real funds. diff --git a/app/api/trading/execute/route.ts b/app/api/trading/execute/route.ts index a9b753b..a281b9a 100644 --- a/app/api/trading/execute/route.ts +++ b/app/api/trading/execute/route.ts @@ -7,7 +7,7 @@ import { NextRequest, NextResponse } from 'next/server' import { initializeDriftService } from '@/lib/drift/client' -import { openPosition } from '@/lib/drift/orders' +import { openPosition, placeExitOrders } from '@/lib/drift/orders' import { normalizeTradingViewSymbol } from '@/config/trading' import { getMergedConfig } from '@/config/trading' import { getPositionManager, ActiveTrade } from '@/lib/trading/position-manager' @@ -192,9 +192,7 @@ export async function POST(request: NextRequest): Promise 0) { + ;(response as any).exitOrderSignatures = exitRes.signatures + } + } catch (err) { + console.error('❌ Unexpected error placing exit orders:', err) + } + + // TODO: Save trade to database (add Prisma integration later) + console.log('✅ Trade executed successfully!') return NextResponse.json(response) diff --git a/config/trading.ts b/config/trading.ts index a7e952a..daa8da2 100644 --- a/config/trading.ts +++ b/config/trading.ts @@ -32,6 +32,9 @@ export interface TradingConfig { // Execution useMarketOrders: boolean // true = instant execution confirmationTimeout: number // Max time to wait for confirmation + // Take profit size splits (percentages of position to close at TP1/TP2) + takeProfit1SizePercent: number + takeProfit2SizePercent: number } export interface MarketConfig { @@ -71,6 +74,8 @@ export const DEFAULT_TRADING_CONFIG: TradingConfig = { // Execution useMarketOrders: true, // Use market orders for reliable fills confirmationTimeout: 30000, // 30 seconds max wait + takeProfit1SizePercent: 75, + takeProfit2SizePercent: 100, } // Supported markets on Drift Protocol @@ -165,6 +170,12 @@ export function getConfigFromEnv(): Partial { takeProfit2Percent: process.env.TAKE_PROFIT_2_PERCENT ? parseFloat(process.env.TAKE_PROFIT_2_PERCENT) : undefined, + takeProfit1SizePercent: process.env.TAKE_PROFIT_1_SIZE_PERCENT + ? parseFloat(process.env.TAKE_PROFIT_1_SIZE_PERCENT) + : undefined, + takeProfit2SizePercent: process.env.TAKE_PROFIT_2_SIZE_PERCENT + ? parseFloat(process.env.TAKE_PROFIT_2_SIZE_PERCENT) + : undefined, maxDailyDrawdown: process.env.MAX_DAILY_DRAWDOWN ? parseFloat(process.env.MAX_DAILY_DRAWDOWN) : undefined, diff --git a/lib/drift/orders.ts b/lib/drift/orders.ts index 2f1a954..e59d4e0 100644 --- a/lib/drift/orders.ts +++ b/lib/drift/orders.ts @@ -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 { + 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 */ diff --git a/test-exit-orders.sh b/test-exit-orders.sh new file mode 100755 index 0000000..24354f4 --- /dev/null +++ b/test-exit-orders.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# Test script to execute a tiny trade and verify exit orders are placed on-chain + +echo "🧪 Testing exit order placement with tiny position..." +echo "📊 Current settings:" +echo " Position: \$10 (base)" +echo " Leverage: 5x" +echo " Notional: \$50" +echo "" + +# API endpoint and credentials +API_URL="http://localhost:3001/api/trading/execute" +API_KEY="2a344f0149442c857fb56c038c0c7d1b113883b830bec792c76f1e0efa15d6bb" + +# Trade request payload +PAYLOAD='{ + "symbol": "SOLUSDT", + "direction": "long", + "timeframe": "5", + "signalStrength": "strong" +}' + +echo "🚀 Sending trade execution request..." +echo "" + +# Execute the request +RESPONSE=$(curl -s -X POST "$API_URL" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d "$PAYLOAD") + +echo "📨 Response:" +echo "$RESPONSE" | jq '.' 2>/dev/null || echo "$RESPONSE" +echo "" + +# Check if successful +if echo "$RESPONSE" | jq -e '.success' > /dev/null 2>&1; then + echo "✅ Trade executed successfully!" + + # Extract signatures + POSITION_ID=$(echo "$RESPONSE" | jq -r '.positionId') + EXIT_SIGS=$(echo "$RESPONSE" | jq -r '.exitOrderSignatures[]?' 2>/dev/null) + + echo "" + echo "📝 Transaction details:" + echo " Entry TX: $POSITION_ID" + + if [ -n "$EXIT_SIGS" ]; then + echo " Exit orders placed:" + echo "$EXIT_SIGS" | while read -r sig; do + echo " - $sig" + done + + echo "" + echo "🔍 Verify on Drift:" + echo " https://app.drift.trade/" + echo "" + echo "🔍 Verify on Solscan:" + echo "$EXIT_SIGS" | while read -r sig; do + echo " https://solscan.io/tx/$sig" + done + else + echo " ⚠️ No exit order signatures in response" + fi +else + echo "❌ Trade execution failed!" + ERROR=$(echo "$RESPONSE" | jq -r '.error // .message') + echo " Error: $ERROR" +fi + +echo "" +echo "📊 Check container logs for details:" +echo " docker logs trading-bot-v4 --tail 100"