diff --git a/.env b/.env index ca44597..7018327 100644 --- a/.env +++ b/.env @@ -61,7 +61,7 @@ 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=80 +MAX_POSITION_SIZE_USD=78 # Leverage multiplier (1-20, default: 10) # Higher leverage = bigger gains AND bigger losses @@ -70,7 +70,7 @@ LEVERAGE=10 # 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=-1.5 +STOP_LOSS_PERCENT=-1.1 # ================================ # DUAL STOP SYSTEM (Advanced) @@ -93,7 +93,7 @@ HARD_STOP_PERCENT=-2.5 # Take Profit 1: Close 50% of position at this profit level # Example: +0.7% on 10x = +7% account gain -TAKE_PROFIT_1_PERCENT=0.7 +TAKE_PROFIT_1_PERCENT=0.4 # Take Profit 1 Size: What % of position to close at TP1 # Example: 50 = close 50% of position @@ -101,7 +101,7 @@ 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=1.1 +TAKE_PROFIT_2_PERCENT=0.7 # Take Profit 2 Size: What % of remaining position to close at TP2 # Example: 100 = close all remaining position diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index 7e8441a..3403087 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -7,7 +7,7 @@ import { getDriftService } from '../drift/client' import { closePosition } from '../drift/orders' import { getPythPriceMonitor, PriceUpdate } from '../pyth/price-monitor' -import { getMergedConfig, TradingConfig } from '../../config/trading' +import { getMergedConfig, TradingConfig, getMarketConfig } from '../../config/trading' import { updateTradeExit, updateTradeState, getOpenTrades } from '../database/trades' export interface ActiveTrade { @@ -271,6 +271,85 @@ export class PositionManager { trade: ActiveTrade, currentPrice: number ): Promise { + // CRITICAL: First check if on-chain position still exists + // (may have been closed by TP/SL orders without us knowing) + try { + const driftService = await getDriftService() + const marketConfig = getMarketConfig(trade.symbol) + const position = await driftService.getPosition(marketConfig.driftMarketIndex) + + if (position === null || position.size === 0) { + // Position closed externally (by on-chain TP/SL order) + console.log(`⚠️ Position ${trade.symbol} was closed externally (by on-chain order)`) + + // Determine exit reason based on price + let exitReason: 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' = 'SL' + + if (trade.direction === 'long') { + if (currentPrice >= trade.tp2Price) { + exitReason = 'TP2' + } else if (currentPrice >= trade.tp1Price) { + exitReason = 'TP1' + } else if (currentPrice <= trade.stopLossPrice) { + exitReason = 'HARD_SL' // Assume hard stop if below SL + } + } else { + // Short + if (currentPrice <= trade.tp2Price) { + exitReason = 'TP2' + } else if (currentPrice <= trade.tp1Price) { + exitReason = 'TP1' + } else if (currentPrice >= trade.stopLossPrice) { + exitReason = 'HARD_SL' // Assume hard stop if above SL + } + } + + // Calculate final P&L + const profitPercent = this.calculateProfitPercent( + trade.entryPrice, + currentPrice, + trade.direction + ) + const accountPnL = profitPercent * trade.leverage + const realizedPnL = (trade.currentSize * accountPnL) / 100 + + // Update database + const holdTimeSeconds = Math.floor((Date.now() - trade.entryTime) / 1000) + try { + await updateTradeExit({ + positionId: trade.positionId, + exitPrice: currentPrice, + exitReason, + realizedPnL, + exitOrderTx: 'ON_CHAIN_ORDER', + holdTimeSeconds, + maxDrawdown: 0, + maxGain: trade.peakPnL, + }) + console.log(`💾 External closure recorded: ${exitReason} at $${currentPrice} | P&L: $${realizedPnL.toFixed(2)}`) + } catch (dbError) { + console.error('❌ Failed to save external closure:', dbError) + } + + // Remove from monitoring + await this.removeTrade(trade.id) + return + } + + // Position exists but size mismatch (partial close by TP1?) + if (position.size < trade.currentSize * 0.95) { // 5% tolerance + console.log(`⚠️ Position size mismatch: expected ${trade.currentSize}, got ${position.size}`) + // Update current size to match reality + trade.currentSize = position.size * (trade.positionSize / trade.currentSize) // Convert to USD + trade.tp1Hit = true + await this.saveTradeState(trade) + } + + } catch (error) { + // If we can't check position, continue with monitoring (don't want to false-positive) + console.error(`⚠️ Could not verify on-chain position for ${trade.symbol}:`, error) + } + // Update trade data trade.lastPrice = currentPrice trade.lastUpdateTime = Date.now()