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! 🎯
293 lines
8.8 KiB
TypeScript
293 lines
8.8 KiB
TypeScript
import { Connection, Keypair, VersionedTransaction, PublicKey } from '@solana/web3.js'
|
|
import fetch from 'cross-fetch'
|
|
|
|
export interface JupiterQuote {
|
|
inputMint: string
|
|
inAmount: string
|
|
outputMint: string
|
|
outAmount: string
|
|
otherAmountThreshold: string
|
|
swapMode: string
|
|
slippageBps: number
|
|
priceImpactPct: string
|
|
routePlan: any[]
|
|
}
|
|
|
|
export interface TradeOrder {
|
|
id: string
|
|
symbol: string
|
|
side: 'BUY' | 'SELL'
|
|
amount: number
|
|
entryPrice?: number
|
|
stopLoss?: number
|
|
takeProfit?: number
|
|
status: 'PENDING' | 'FILLED' | 'CANCELLED' | 'MONITORING'
|
|
txId?: string
|
|
timestamp: number
|
|
}
|
|
|
|
class JupiterDEXService {
|
|
private connection: Connection
|
|
private keypair: Keypair | null = null
|
|
private activeOrders: TradeOrder[] = []
|
|
|
|
// Token mint addresses
|
|
private tokens = {
|
|
SOL: 'So11111111111111111111111111111111111111112', // Wrapped SOL
|
|
USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
|
USDT: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
|
|
}
|
|
|
|
constructor() {
|
|
const rpcUrl = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com'
|
|
this.connection = new Connection(rpcUrl, 'confirmed')
|
|
this.initializeWallet()
|
|
}
|
|
|
|
private initializeWallet() {
|
|
try {
|
|
if (process.env.SOLANA_PRIVATE_KEY) {
|
|
const privateKeyArray = JSON.parse(process.env.SOLANA_PRIVATE_KEY)
|
|
this.keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyArray))
|
|
console.log('✅ Jupiter DEX wallet initialized:', this.keypair.publicKey.toString())
|
|
} else {
|
|
console.warn('⚠️ No SOLANA_PRIVATE_KEY found for Jupiter DEX')
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Failed to initialize Jupiter DEX wallet:', error)
|
|
}
|
|
}
|
|
|
|
async getQuote(
|
|
inputMint: string,
|
|
outputMint: string,
|
|
amount: number,
|
|
slippageBps: number = 50 // 0.5% slippage
|
|
): Promise<JupiterQuote | null> {
|
|
try {
|
|
const url = `https://quote-api.jup.ag/v6/quote?inputMint=${inputMint}&outputMint=${outputMint}&amount=${amount}&slippageBps=${slippageBps}`
|
|
|
|
console.log('🔍 Getting Jupiter quote:', { inputMint, outputMint, amount })
|
|
|
|
const response = await fetch(url)
|
|
if (!response.ok) {
|
|
throw new Error(`Jupiter API error: ${response.status}`)
|
|
}
|
|
|
|
const quote = await response.json()
|
|
console.log('📊 Jupiter quote received:', quote)
|
|
|
|
return quote
|
|
} catch (error) {
|
|
console.error('❌ Failed to get Jupiter quote:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
async executeSwap(
|
|
inputMint: string,
|
|
outputMint: string,
|
|
amount: number,
|
|
slippageBps: number = 50
|
|
): Promise<{
|
|
success: boolean
|
|
txId?: string
|
|
error?: string
|
|
executionPrice?: number
|
|
inputAmount?: number
|
|
outputAmount?: number
|
|
fees?: number
|
|
slippage?: number
|
|
}> {
|
|
if (!this.keypair) {
|
|
return { success: false, error: 'Wallet not initialized' }
|
|
}
|
|
|
|
try {
|
|
console.log('🔄 Executing Jupiter swap:', { inputMint, outputMint, amount })
|
|
|
|
// 1. Get quote
|
|
const quote = await this.getQuote(inputMint, outputMint, amount, slippageBps)
|
|
if (!quote) {
|
|
return { success: false, error: 'Failed to get quote' }
|
|
}
|
|
|
|
// 2. Get swap transaction
|
|
const swapResponse = await fetch('https://quote-api.jup.ag/v6/swap', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
quoteResponse: quote,
|
|
userPublicKey: this.keypair.publicKey.toString(),
|
|
wrapAndUnwrapSol: true,
|
|
})
|
|
})
|
|
|
|
if (!swapResponse.ok) {
|
|
throw new Error(`Swap API error: ${swapResponse.status}`)
|
|
}
|
|
|
|
const { swapTransaction } = await swapResponse.json()
|
|
|
|
// 3. Deserialize and sign transaction
|
|
const swapTransactionBuf = Buffer.from(swapTransaction, 'base64')
|
|
const transaction = VersionedTransaction.deserialize(swapTransactionBuf)
|
|
transaction.sign([this.keypair])
|
|
|
|
// 4. Submit transaction
|
|
const txId = await this.connection.sendTransaction(transaction)
|
|
|
|
// 5. Confirm transaction
|
|
const confirmation = await this.connection.confirmTransaction(txId, 'confirmed')
|
|
|
|
if (confirmation.value.err) {
|
|
return { success: false, error: `Transaction failed: ${confirmation.value.err}` }
|
|
}
|
|
|
|
// 6. Calculate execution details from quote
|
|
const inputAmount = parseFloat(quote.inAmount)
|
|
const outputAmount = parseFloat(quote.outAmount)
|
|
|
|
// Calculate execution price based on swap direction
|
|
let executionPrice: number
|
|
if (inputMint === this.tokens.USDC) {
|
|
// Buying SOL with USDC: price = USDC spent / SOL received
|
|
executionPrice = (inputAmount / 1e6) / (outputAmount / 1e9) // Convert from token units
|
|
} else {
|
|
// Selling SOL for USDC: price = USDC received / SOL spent
|
|
executionPrice = (outputAmount / 1e6) / (inputAmount / 1e9) // Convert from token units
|
|
}
|
|
|
|
console.log('✅ Jupiter swap successful:', txId)
|
|
return {
|
|
success: true,
|
|
txId,
|
|
executionPrice,
|
|
inputAmount: inputAmount / (inputMint === this.tokens.USDC ? 1e6 : 1e9),
|
|
outputAmount: outputAmount / (outputMint === this.tokens.USDC ? 1e6 : 1e9),
|
|
fees: (inputAmount / (inputMint === this.tokens.USDC ? 1e6 : 1e9)) * 0.001, // Estimate 0.1% fees
|
|
slippage: parseFloat(quote.priceImpactPct || '0')
|
|
}
|
|
|
|
} catch (error: any) {
|
|
console.error('❌ Jupiter swap failed:', error)
|
|
return { success: false, error: error.message }
|
|
}
|
|
}
|
|
|
|
async executeTrade(params: {
|
|
symbol: string
|
|
side: 'BUY' | 'SELL'
|
|
amount: number
|
|
stopLoss?: number
|
|
takeProfit?: number
|
|
tradingPair?: string
|
|
quickSwap?: boolean
|
|
}): Promise<{
|
|
success: boolean
|
|
orderId?: string
|
|
txId?: string
|
|
error?: string
|
|
}> {
|
|
try {
|
|
const { symbol, side, amount, stopLoss, takeProfit, tradingPair, quickSwap } = params
|
|
|
|
console.log('🎯 Executing real DEX trade:', params)
|
|
|
|
// Handle different trading pairs
|
|
let inputMint: string
|
|
let outputMint: string
|
|
let amountLamports: number
|
|
|
|
if (quickSwap || tradingPair === 'SOL/USDC') {
|
|
// SOL to USDC swap
|
|
inputMint = this.tokens.SOL
|
|
outputMint = this.tokens.USDC
|
|
amountLamports = Math.floor(amount * 1000000000) // SOL has 9 decimals
|
|
} else if (tradingPair === 'USDC/SOL') {
|
|
// USDC to SOL swap
|
|
inputMint = this.tokens.USDC
|
|
outputMint = this.tokens.SOL
|
|
amountLamports = Math.floor(amount * 1000000) // USDC has 6 decimals
|
|
} else {
|
|
// Default behavior based on side
|
|
inputMint = side === 'BUY' ? this.tokens.USDC : this.tokens.SOL
|
|
outputMint = side === 'BUY' ? this.tokens.SOL : this.tokens.USDC
|
|
amountLamports = side === 'BUY'
|
|
? Math.floor(amount * 1000000) // USDC has 6 decimals
|
|
: Math.floor(amount * 1000000000) // SOL has 9 decimals
|
|
}
|
|
|
|
// Execute the swap
|
|
const swapResult = await this.executeSwap(inputMint, outputMint, amountLamports)
|
|
|
|
if (!swapResult.success) {
|
|
return { success: false, error: swapResult.error }
|
|
}
|
|
|
|
// Create order tracking
|
|
const orderId = `jupiter_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`
|
|
const order: TradeOrder = {
|
|
id: orderId,
|
|
symbol,
|
|
side,
|
|
amount,
|
|
stopLoss,
|
|
takeProfit,
|
|
status: stopLoss || takeProfit ? 'MONITORING' : 'FILLED',
|
|
txId: swapResult.txId,
|
|
timestamp: Date.now()
|
|
}
|
|
|
|
this.activeOrders.push(order)
|
|
|
|
// Start monitoring for TP/SL if needed
|
|
if (stopLoss || takeProfit) {
|
|
this.startOrderMonitoring(order)
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
orderId,
|
|
txId: swapResult.txId
|
|
}
|
|
|
|
} catch (error: any) {
|
|
console.error('❌ DEX trade execution failed:', error)
|
|
return { success: false, error: error.message }
|
|
}
|
|
}
|
|
|
|
private async startOrderMonitoring(order: TradeOrder) {
|
|
console.log('👁️ Starting TP/SL monitoring for order:', order.id)
|
|
|
|
// This would run in a background process
|
|
// For now, we'll log that monitoring started
|
|
|
|
// TODO: Implement continuous price monitoring
|
|
// - Check current price every few seconds
|
|
// - Execute reverse trade when TP/SL is hit
|
|
// - Update order status
|
|
}
|
|
|
|
getActiveOrders(): TradeOrder[] {
|
|
return this.activeOrders
|
|
}
|
|
|
|
async cancelOrder(orderId: string): Promise<boolean> {
|
|
const orderIndex = this.activeOrders.findIndex(o => o.id === orderId)
|
|
if (orderIndex >= 0) {
|
|
this.activeOrders[orderIndex].status = 'CANCELLED'
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
isConfigured(): boolean {
|
|
return this.keypair !== null
|
|
}
|
|
}
|
|
|
|
export const jupiterDEXService = new JupiterDEXService()
|
|
export default JupiterDEXService
|