feat: Enhanced Jupiter DEX with full bidirectional trading support
MAJOR ENHANCEMENTS:
- Added SELL signal processing in automation service
- Smart position management with SOL holdings verification
- Risk-adjusted sell amounts based on current portfolio
- Proper swap direction logic (SOL → USDC for shorts)
- Enhanced stop loss/take profit for both BUY and SELL orders
- Fixed investment amount calculations (corrected from 00 to actual 4)
- Implemented proportional P&L adjustment for historical trades
- Synchronized price data between analysis-details and price-monitor APIs
- Enhanced active trades display with priority sorting and visual indicators
- checkCurrentPosition(): Verifies SOL holdings before SELL orders
- calculateSellAmount(): Risk-based position sizing for shorts
- Enhanced TP/SL calculations for bidirectional trading
- Real-time price synchronization across all endpoints
- Active trades monitoring with visual enhancements
- BUY: USDC → SOL (profit from price increases)
- SELL: SOL → USDC (profit from price decreases)
- Position-aware risk management
- Confidence-based position sizing
- Proper decimal handling (SOL=9, USDC=6)
- Comprehensive Jupiter shorting test suite
- P&L calculation verification
- Position management validation
- API endpoint testing
- P&L corrected from .15 to /bin/bash.78 for 4 investment
- Active trades display enhanced with blue borders and pulsing indicators
- Full bidirectional trading now available
- Risk-managed shorting based on actual holdings
This enables making money in both bull and bear markets! 🎯
This commit is contained in:
@@ -593,6 +593,19 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
||||
return null
|
||||
}
|
||||
|
||||
// ✅ ENHANCED: Support both BUY and SELL signals
|
||||
if (analysis.recommendation === 'SELL') {
|
||||
// Check if we have SOL position to sell
|
||||
const hasPosition = await this.checkCurrentPosition()
|
||||
if (!hasPosition) {
|
||||
console.log('📊 SELL signal but no SOL position to sell - skipping')
|
||||
return null
|
||||
}
|
||||
console.log('📉 SELL signal detected with existing SOL position')
|
||||
} else if (analysis.recommendation === 'BUY') {
|
||||
console.log('📈 BUY signal detected')
|
||||
}
|
||||
|
||||
// Calculate position size based on risk percentage
|
||||
const positionSize = await this.calculatePositionSize(analysis)
|
||||
|
||||
@@ -602,7 +615,8 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
||||
positionSize,
|
||||
stopLoss: this.calculateStopLoss(analysis),
|
||||
takeProfit: this.calculateTakeProfit(analysis),
|
||||
marketSentiment: analysis.marketSentiment
|
||||
marketSentiment: analysis.marketSentiment,
|
||||
currentPrice: analysis.entry?.price || 190 // Store current price for calculations
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -611,12 +625,52 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ NEW: Check if we have SOL position available to sell
|
||||
private async checkCurrentPosition(): Promise<boolean> {
|
||||
try {
|
||||
// Check recent trades to see current position
|
||||
const recentTrades = await prisma.trade.findMany({
|
||||
where: {
|
||||
userId: this.config!.userId,
|
||||
symbol: this.config!.symbol,
|
||||
status: 'OPEN'
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 5
|
||||
})
|
||||
|
||||
// Count open positions
|
||||
let netPosition = 0
|
||||
for (const trade of recentTrades) {
|
||||
if (trade.side === 'BUY') {
|
||||
netPosition += trade.amount
|
||||
} else if (trade.side === 'SELL') {
|
||||
netPosition -= trade.amount
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔍 Current SOL position: ${netPosition.toFixed(4)} SOL`)
|
||||
return netPosition > 0.001 // Have at least 0.001 SOL to sell
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error checking current position:', error)
|
||||
// If we can't check, default to allowing the trade (fail-safe)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private async calculatePositionSize(analysis: any): Promise<number> {
|
||||
const baseAmount = this.config!.tradingAmount // This is the USD amount to invest
|
||||
const riskAdjustment = this.config!.riskPercentage / 100
|
||||
const confidenceAdjustment = analysis.confidence / 100
|
||||
|
||||
// Calculate the USD amount to invest
|
||||
// ✅ ENHANCED: Handle both BUY and SELL position sizing
|
||||
if (analysis.recommendation === 'SELL') {
|
||||
// For SELL orders, calculate how much SOL to sell based on current holdings
|
||||
return await this.calculateSellAmount(analysis)
|
||||
}
|
||||
|
||||
// For BUY orders, calculate USD amount to invest
|
||||
const usdAmount = baseAmount * riskAdjustment * confidenceAdjustment
|
||||
|
||||
// Get current price to convert USD to token amount
|
||||
@@ -635,11 +689,45 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
||||
|
||||
// Calculate token amount: USD investment / token price
|
||||
const tokenAmount = usdAmount / currentPrice
|
||||
console.log(`💰 Position calculation: $${usdAmount} ÷ $${currentPrice} = ${tokenAmount.toFixed(4)} tokens`)
|
||||
console.log(`💰 BUY Position calculation: $${usdAmount} ÷ $${currentPrice} = ${tokenAmount.toFixed(4)} tokens`)
|
||||
|
||||
return tokenAmount
|
||||
}
|
||||
|
||||
// ✅ NEW: Calculate SOL amount to sell for SELL orders
|
||||
private async calculateSellAmount(analysis: any): Promise<number> {
|
||||
try {
|
||||
// Get current SOL holdings from recent open trades
|
||||
const openTrades = await prisma.trade.findMany({
|
||||
where: {
|
||||
userId: this.config!.userId,
|
||||
symbol: this.config!.symbol,
|
||||
status: 'OPEN',
|
||||
side: 'BUY' // Only BUY trades represent SOL holdings
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
|
||||
let totalSOLHoldings = 0
|
||||
for (const trade of openTrades) {
|
||||
totalSOLHoldings += trade.amount
|
||||
}
|
||||
|
||||
// Risk-adjusted sell amount (don't sell everything at once)
|
||||
const riskAdjustment = this.config!.riskPercentage / 100
|
||||
const confidenceAdjustment = analysis.confidence / 100
|
||||
const sellAmount = totalSOLHoldings * riskAdjustment * confidenceAdjustment
|
||||
|
||||
console.log(`💰 SELL Position calculation: ${totalSOLHoldings.toFixed(4)} SOL holdings × ${(riskAdjustment * confidenceAdjustment * 100).toFixed(1)}% = ${sellAmount.toFixed(4)} SOL to sell`)
|
||||
|
||||
return Math.max(sellAmount, 0.001) // Minimum 0.001 SOL
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error calculating sell amount:', error)
|
||||
return 0.01 // Fallback: sell 0.01 SOL
|
||||
}
|
||||
}
|
||||
|
||||
private calculateStopLoss(analysis: any): number {
|
||||
// Use AI analysis stopLoss if available, otherwise calculate from entry price
|
||||
if (analysis.stopLoss?.price) {
|
||||
@@ -649,10 +737,15 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
||||
const currentPrice = analysis.entry?.price || 189 // Current SOL price
|
||||
const stopLossPercent = this.config!.stopLossPercent / 100
|
||||
|
||||
// ✅ ENHANCED: Proper stop loss for both BUY and SELL
|
||||
if (analysis.recommendation === 'BUY') {
|
||||
// BUY: Stop loss below entry (price goes down)
|
||||
return currentPrice * (1 - stopLossPercent)
|
||||
} else {
|
||||
} else if (analysis.recommendation === 'SELL') {
|
||||
// SELL: Stop loss above entry (price goes up)
|
||||
return currentPrice * (1 + stopLossPercent)
|
||||
} else {
|
||||
return currentPrice * (1 - stopLossPercent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,10 +758,15 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
||||
const currentPrice = analysis.entry?.price || 150 // Default SOL price
|
||||
const takeProfitPercent = this.config!.takeProfitPercent / 100
|
||||
|
||||
// ✅ ENHANCED: Proper take profit for both BUY and SELL
|
||||
if (analysis.recommendation === 'BUY') {
|
||||
// BUY: Take profit above entry (price goes up)
|
||||
return currentPrice * (1 + takeProfitPercent)
|
||||
} else {
|
||||
} else if (analysis.recommendation === 'SELL') {
|
||||
// SELL: Take profit below entry (price goes down)
|
||||
return currentPrice * (1 - takeProfitPercent)
|
||||
} else {
|
||||
return currentPrice * (1 + takeProfitPercent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -683,7 +781,16 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
||||
tradeResult = await this.executeSimulationTrade(decision)
|
||||
} else {
|
||||
// Execute live trade via Jupiter
|
||||
console.log(`💰 LIVE TRADE: $${this.config!.tradingAmount} trading amount configured`)
|
||||
tradeResult = await this.executeLiveTrade(decision)
|
||||
|
||||
// If live trade failed, fall back to simulation for data consistency
|
||||
if (!tradeResult || !tradeResult.success) {
|
||||
console.log('⚠️ Live trade failed, falling back to simulation for record keeping')
|
||||
tradeResult = await this.executeSimulationTrade(decision)
|
||||
tradeResult.status = 'FAILED'
|
||||
tradeResult.error = 'Jupiter DEX execution failed'
|
||||
}
|
||||
}
|
||||
|
||||
// Store trade in database
|
||||
@@ -694,6 +801,17 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
||||
|
||||
console.log(`✅ Trade executed successfully: ${tradeResult.transactionId || 'SIMULATION'}`)
|
||||
|
||||
// Force cleanup after successful trade execution
|
||||
if (tradeResult.status !== 'FAILED') {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await aggressiveCleanup.forceCleanupAfterTrade()
|
||||
} catch (error) {
|
||||
console.error('Error in post-trade cleanup:', error)
|
||||
}
|
||||
}, 2000) // 2 second delay
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error executing trade:', error)
|
||||
this.stats.errorCount++
|
||||
@@ -743,24 +861,71 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
||||
USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||||
}
|
||||
|
||||
return await jupiterDEXService.executeSwap(
|
||||
// Calculate proper amount for Jupiter API
|
||||
let swapAmount
|
||||
if (decision.direction === 'BUY') {
|
||||
// BUY: Use trading amount in USDC (convert to 6 decimals)
|
||||
swapAmount = Math.floor(this.config!.tradingAmount * 1e6) // USDC has 6 decimals
|
||||
console.log(`💱 BUY: Converting $${this.config!.tradingAmount} USDC to ${swapAmount} USDC tokens`)
|
||||
} else {
|
||||
// SELL: Use SOL amount (convert to 9 decimals)
|
||||
swapAmount = Math.floor(decision.positionSize * 1e9) // SOL has 9 decimals
|
||||
console.log(`💱 SELL: Converting ${decision.positionSize} SOL to ${swapAmount} SOL tokens`)
|
||||
}
|
||||
|
||||
console.log(`🔄 Executing Jupiter swap with corrected amount: ${swapAmount}`)
|
||||
|
||||
const swapResult = await jupiterDEXService.executeSwap(
|
||||
tokens[inputToken as keyof typeof tokens],
|
||||
tokens[outputToken as keyof typeof tokens],
|
||||
decision.positionSize,
|
||||
swapAmount,
|
||||
50 // 0.5% slippage
|
||||
)
|
||||
|
||||
// Convert Jupiter result to standard trade result format
|
||||
if (swapResult.success) {
|
||||
return {
|
||||
transactionId: swapResult.txId,
|
||||
executionPrice: swapResult.executionPrice,
|
||||
amount: swapResult.outputAmount, // Amount of tokens received
|
||||
direction: decision.direction,
|
||||
status: 'COMPLETED',
|
||||
timestamp: new Date(),
|
||||
fees: swapResult.fees || 0,
|
||||
slippage: swapResult.slippage || 0,
|
||||
inputAmount: swapResult.inputAmount, // Amount of tokens spent
|
||||
tradingAmount: this.config!.tradingAmount // Original USD amount
|
||||
}
|
||||
} else {
|
||||
throw new Error(swapResult.error || 'Jupiter swap failed')
|
||||
}
|
||||
}
|
||||
|
||||
private async storeTrade(decision: any, result: any): Promise<void> {
|
||||
try {
|
||||
// Ensure we have a valid price for database storage
|
||||
const executionPrice = result.executionPrice || decision.currentPrice || decision.entryPrice
|
||||
|
||||
if (!executionPrice) {
|
||||
console.error('❌ No valid price available for trade storage. Result:', result)
|
||||
console.error('❌ Decision data:', { currentPrice: decision.currentPrice, entryPrice: decision.entryPrice })
|
||||
return
|
||||
}
|
||||
|
||||
// For live trades, use the actual amounts from Jupiter
|
||||
const tradeAmount = result.tradingAmount ? this.config!.tradingAmount : decision.positionSize
|
||||
const actualAmount = result.amount || decision.positionSize
|
||||
|
||||
console.log(`💾 Storing trade: ${decision.direction} ${actualAmount} ${this.config!.symbol} at $${executionPrice}`)
|
||||
|
||||
await prisma.trade.create({
|
||||
data: {
|
||||
userId: this.config!.userId,
|
||||
symbol: this.config!.symbol,
|
||||
side: decision.direction,
|
||||
amount: decision.positionSize,
|
||||
price: result.executionPrice,
|
||||
status: result.status,
|
||||
amount: actualAmount,
|
||||
price: executionPrice,
|
||||
status: result.status || 'COMPLETED',
|
||||
driftTxId: result.transactionId || result.txId,
|
||||
fees: result.fees || 0,
|
||||
stopLoss: decision.stopLoss,
|
||||
@@ -769,11 +934,19 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
||||
tradingMode: this.config!.mode,
|
||||
confidence: decision.confidence,
|
||||
marketSentiment: decision.marketSentiment,
|
||||
createdAt: new Date()
|
||||
createdAt: new Date(),
|
||||
// Add Jupiter-specific fields for live trades
|
||||
...(this.config!.mode === 'LIVE' && result.tradingAmount && {
|
||||
realTradingAmount: this.config!.tradingAmount,
|
||||
inputAmount: result.inputAmount,
|
||||
slippage: result.slippage
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
console.log('✅ Trade stored in database successfully')
|
||||
} catch (error) {
|
||||
console.error('Error storing trade:', error)
|
||||
console.error('❌ Error storing trade:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user