- Added fallback SL/TP calculation when AI values missing (rate limits) - Stop loss: 1.5% from entry (scalping-optimized) - Take profit: 3% from entry (2:1 risk/reward) - Relaxed API validation to require only stop loss (most critical) - Disabled problematic import in position-history route - System now guarantees risk management on every trade No more unprotected positions - works with or without AI analysis
395 lines
15 KiB
JavaScript
395 lines
15 KiB
JavaScript
import { NextResponse } from 'next/server'
|
|
import { Connection, Keypair, PublicKey } from '@solana/web3.js'
|
|
import { DriftClient, getUserAccountPublicKey, initialize } from '@drift-labs/sdk'
|
|
|
|
const getRpcStatus = () => {
|
|
const rpcEndpoints = [
|
|
process.env.SOLANA_RPC_URL,
|
|
process.env.HELIUS_RPC_URL,
|
|
'https://api.mainnet-beta.solana.com'
|
|
].filter(Boolean)
|
|
|
|
return {
|
|
primary: rpcEndpoints[0] || 'Not configured',
|
|
fallbacks: rpcEndpoints.slice(1),
|
|
total: rpcEndpoints.length
|
|
}
|
|
}
|
|
|
|
// Function to record recently closed positions for learning
|
|
async function recordRecentlyClosedPosition() {
|
|
try {
|
|
// Check if there's a recent automation decision that should be closed
|
|
// Note: simple-automation import disabled to prevent API issues
|
|
// const { simpleAutomation } = await import('../../../lib/simple-automation.js');
|
|
|
|
// Temporarily disabled automation integration
|
|
if (false) { // simpleAutomation.lastDecision && simpleAutomation.lastDecision.executed) {
|
|
const decision = simpleAutomation.lastDecision;
|
|
const timeSinceDecision = Date.now() - new Date(decision.timestamp).getTime();
|
|
|
|
// If decision was executed recently (within 1 hour) and no position exists, record as closed
|
|
if (timeSinceDecision < 3600000) { // 1 hour
|
|
console.log('🔍 Found recent executed decision - checking if position was closed');
|
|
|
|
// Estimate profit based on current price vs entry
|
|
const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd');
|
|
const priceData = await response.json();
|
|
const currentPrice = priceData.solana.usd;
|
|
|
|
const entryPrice = decision.executionDetails.currentPrice;
|
|
const side = decision.executionDetails.side.toLowerCase();
|
|
const amount = decision.executionDetails.amount;
|
|
|
|
// Calculate P&L based on side and price movement
|
|
let pnl = 0;
|
|
let outcome = 'UNKNOWN';
|
|
|
|
if (side === 'long') {
|
|
pnl = (currentPrice - entryPrice) * (amount / entryPrice);
|
|
outcome = currentPrice > entryPrice ? 'WIN' : 'LOSS';
|
|
} else if (side === 'short') {
|
|
pnl = (entryPrice - currentPrice) * (amount / entryPrice);
|
|
outcome = currentPrice < entryPrice ? 'WIN' : 'LOSS';
|
|
}
|
|
|
|
const pnlPercent = (pnl / amount) * 100;
|
|
|
|
// Record the trade in database
|
|
const { PrismaClient } = await import('@prisma/client');
|
|
const prisma = new PrismaClient();
|
|
|
|
try {
|
|
const tradeRecord = await prisma.trades.create({
|
|
data: {
|
|
id: `trade_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
userId: 'automation_user', // Default automation user
|
|
symbol: decision.executionDetails.symbol || 'SOL-PERP',
|
|
side: side.toUpperCase(),
|
|
amount: amount,
|
|
price: entryPrice,
|
|
entryPrice: entryPrice,
|
|
exitPrice: currentPrice,
|
|
stopLoss: decision.executionDetails.stopLoss,
|
|
takeProfit: decision.executionDetails.takeProfit,
|
|
leverage: decision.executionDetails.leverage || 1,
|
|
profit: pnl,
|
|
pnlPercent: pnlPercent,
|
|
outcome: outcome,
|
|
status: 'COMPLETED',
|
|
confidence: decision.confidence,
|
|
aiAnalysis: decision.reasoning,
|
|
isAutomated: true,
|
|
tradingMode: 'PERP',
|
|
driftTxId: decision.executionDetails.txId,
|
|
executedAt: new Date(decision.timestamp),
|
|
closedAt: new Date(),
|
|
createdAt: new Date(decision.timestamp),
|
|
updatedAt: new Date()
|
|
}
|
|
});
|
|
|
|
console.log('✅ Recorded completed trade:', tradeRecord.id);
|
|
|
|
// Clear the decision to avoid re-recording
|
|
simpleAutomation.lastDecision = null;
|
|
|
|
return tradeRecord;
|
|
|
|
} finally {
|
|
await prisma.$disconnect();
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.error('❌ Error recording closed position:', error.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function GET() {
|
|
try {
|
|
console.log('📊 Position History API called')
|
|
|
|
// Get keypair from private key
|
|
if (!process.env.SOLANA_PRIVATE_KEY) {
|
|
throw new Error('SOLANA_PRIVATE_KEY environment variable not set')
|
|
}
|
|
|
|
const privateKeyArray = JSON.parse(process.env.SOLANA_PRIVATE_KEY)
|
|
const keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyArray))
|
|
|
|
// Setup connection with failover
|
|
const rpcEndpoints = [
|
|
process.env.SOLANA_RPC_URL,
|
|
process.env.HELIUS_RPC_URL,
|
|
'https://api.mainnet-beta.solana.com'
|
|
].filter(Boolean)
|
|
|
|
let connection
|
|
let connectedEndpoint = null
|
|
|
|
for (const endpoint of rpcEndpoints) {
|
|
try {
|
|
console.log(`🔗 Attempting connection to: ${endpoint.substring(0, 50)}...`)
|
|
connection = new Connection(endpoint, 'confirmed')
|
|
|
|
// Test the connection
|
|
const balance = await connection.getBalance(keypair.publicKey)
|
|
console.log(`✅ Connected successfully. Balance: ${(balance / 1e9).toFixed(6)} SOL`)
|
|
connectedEndpoint = endpoint
|
|
break
|
|
} catch (connError) {
|
|
console.log(`❌ Connection failed: ${connError.message}`)
|
|
continue
|
|
}
|
|
}
|
|
|
|
if (!connection || !connectedEndpoint) {
|
|
throw new Error('All RPC endpoints failed')
|
|
}
|
|
|
|
// Initialize Drift SDK
|
|
await initialize({ env: 'mainnet-beta' })
|
|
|
|
const userAccountPDA = getUserAccountPublicKey(
|
|
new PublicKey('dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH'),
|
|
keypair.publicKey,
|
|
0
|
|
)
|
|
|
|
console.log('🏦 User PDA:', userAccountPDA.toString())
|
|
|
|
// Create Drift client
|
|
const driftClient = new DriftClient({
|
|
connection,
|
|
wallet: {
|
|
publicKey: keypair.publicKey,
|
|
signTransaction: () => Promise.reject(new Error('Read-only')),
|
|
signAllTransactions: () => Promise.reject(new Error('Read-only'))
|
|
},
|
|
programID: new PublicKey('dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH'),
|
|
opts: { commitment: 'confirmed' }
|
|
})
|
|
|
|
// Try to get real trading history
|
|
let realTradeHistory = []
|
|
|
|
try {
|
|
console.log('🔍 Attempting to fetch trading history from database...')
|
|
|
|
// Import Prisma client
|
|
const { PrismaClient } = await import('@prisma/client');
|
|
const prisma = new PrismaClient();
|
|
|
|
try {
|
|
// Get all relevant trades (both completed and executed)
|
|
const allTrades = await prisma.trades.findMany({
|
|
where: {
|
|
status: { in: ['COMPLETED', 'EXECUTED'] } // Include both completed and executed trades
|
|
},
|
|
orderBy: {
|
|
updatedAt: 'desc' // Order by updatedAt to get most recently modified trades
|
|
},
|
|
take: 200 // Increased to get more trades
|
|
});
|
|
|
|
console.log(`📊 Found ${allTrades.length} trades with relevant data`);
|
|
|
|
// Filter out simulation trades after fetching
|
|
const realTrades = allTrades.filter(trade => {
|
|
// Exclude if driftTxId starts with SIM_
|
|
if (trade.driftTxId && trade.driftTxId.startsWith('SIM_')) {
|
|
console.log(`🚫 Excluding simulation trade: ${trade.driftTxId}`);
|
|
return false;
|
|
}
|
|
// Exclude if tradingMode is explicitly SIMULATION
|
|
if (trade.tradingMode === 'SIMULATION') {
|
|
console.log(`🚫 Excluding simulation mode trade: ${trade.id}`);
|
|
return false;
|
|
}
|
|
console.log(`✅ Including real trade: ${trade.id} (${trade.status}) - ${trade.tradingMode || 'REAL'}`);
|
|
return true;
|
|
});
|
|
|
|
console.log(`📊 After filtering simulations: ${realTrades.length} real trades`);
|
|
|
|
// Convert to standardized format
|
|
realTradeHistory = realTrades.map(trade => {
|
|
// Calculate outcome if missing
|
|
let outcome = trade.outcome;
|
|
let pnl = trade.profit;
|
|
|
|
// For EXECUTED trades without profit, estimate current P&L if possible
|
|
if (trade.status === 'EXECUTED' && !pnl && trade.entryPrice) {
|
|
// These are open positions, we'll show them as "OPEN"
|
|
outcome = 'OPEN';
|
|
pnl = 0; // Will be calculated when position closes
|
|
} else if (!outcome && pnl !== null) {
|
|
outcome = pnl > 0 ? 'WIN' : 'LOSS';
|
|
}
|
|
|
|
return {
|
|
id: trade.id,
|
|
symbol: trade.symbol,
|
|
side: trade.side,
|
|
amount: trade.amount,
|
|
entryPrice: trade.entryPrice,
|
|
exitPrice: trade.exitPrice,
|
|
pnl: pnl,
|
|
pnlPercent: trade.pnlPercent,
|
|
outcome: outcome,
|
|
leverage: trade.leverage || 1,
|
|
stopLoss: trade.stopLoss,
|
|
takeProfit: trade.takeProfit,
|
|
entryTime: trade.executedAt || trade.createdAt,
|
|
exitTime: trade.closedAt,
|
|
txId: trade.driftTxId,
|
|
confidence: trade.confidence,
|
|
aiAnalysis: trade.aiAnalysis,
|
|
status: trade.status // Add status to distinguish COMPLETED vs EXECUTED
|
|
};
|
|
});
|
|
|
|
console.log(`✅ Successfully processed ${realTradeHistory.length} real trades from database`);
|
|
|
|
// Try to enhance trades with recent AI analysis data
|
|
try {
|
|
const recentAnalyses = await prisma.ai_learning_data.findMany({
|
|
where: {
|
|
timeframe: { not: 'DECISION' },
|
|
timeframe: { not: 'OUTCOME' },
|
|
analysisData: { not: null }
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 20 // Get recent analysis records
|
|
});
|
|
|
|
console.log(`Found ${recentAnalyses.length} recent AI analysis records`);
|
|
|
|
// Link analysis to trades based on timing and symbol
|
|
realTradeHistory.forEach(trade => {
|
|
if (!trade.aiAnalysis) {
|
|
const tradeTime = new Date(trade.entryTime);
|
|
|
|
// Find analysis within 1 hour of trade time and same symbol
|
|
const matchingAnalysis = recentAnalyses.find(analysis => {
|
|
const analysisTime = new Date(analysis.createdAt);
|
|
const timeDiff = Math.abs(tradeTime.getTime() - analysisTime.getTime());
|
|
const isWithinTimeWindow = timeDiff <= 3600000; // 1 hour
|
|
const symbolMatch = analysis.symbol === trade.symbol ||
|
|
analysis.symbol === trade.symbol.replace('USD', '') ||
|
|
analysis.symbol === trade.symbol.replace('USDT', '');
|
|
|
|
return isWithinTimeWindow && symbolMatch;
|
|
});
|
|
|
|
if (matchingAnalysis) {
|
|
try {
|
|
const analysisData = JSON.parse(matchingAnalysis.analysisData);
|
|
trade.aiAnalysis = analysisData.reasoning || analysisData.summary || `AI Confidence: ${matchingAnalysis.confidenceScore}%`;
|
|
} catch (e) {
|
|
trade.aiAnalysis = `AI Analysis (Confidence: ${matchingAnalysis.confidenceScore}%)`;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
} catch (analysisError) {
|
|
console.log('⚠️ Could not enhance trades with AI analysis:', analysisError.message);
|
|
}
|
|
|
|
} finally {
|
|
await prisma.$disconnect();
|
|
}
|
|
|
|
} catch (error) {
|
|
console.log('❌ Could not fetch trading history from database:', error.message)
|
|
|
|
// Fallback: Try to detect recently closed position and record it
|
|
try {
|
|
console.log('🔍 Checking for recently closed positions to record...');
|
|
await recordRecentlyClosedPosition();
|
|
} catch (recordError) {
|
|
console.log('⚠️ Could not record recent position:', recordError.message);
|
|
}
|
|
}
|
|
|
|
// Only use real data - no demo/mock data
|
|
const historicalTrades = realTradeHistory
|
|
|
|
// Calculate statistics (case-insensitive matching, exclude OPEN positions)
|
|
const completedTrades = historicalTrades.filter(trade =>
|
|
trade.outcome && trade.outcome.toUpperCase() !== 'OPEN'
|
|
)
|
|
const wins = completedTrades.filter(trade =>
|
|
trade.outcome && trade.outcome.toUpperCase() === 'WIN'
|
|
)
|
|
const losses = completedTrades.filter(trade =>
|
|
trade.outcome && trade.outcome.toUpperCase() === 'LOSS'
|
|
)
|
|
|
|
const totalPnl = completedTrades.reduce((sum, trade) => sum + (trade.pnl || 0), 0)
|
|
const winsPnl = wins.reduce((sum, trade) => sum + (trade.pnl || 0), 0)
|
|
const lossesPnl = losses.reduce((sum, trade) => sum + (trade.pnl || 0), 0)
|
|
|
|
const winRate = completedTrades.length > 0 ? (wins.length / completedTrades.length) * 100 : 0
|
|
const avgWin = wins.length > 0 ? winsPnl / wins.length : 0
|
|
const avgLoss = losses.length > 0 ? lossesPnl / losses.length : 0
|
|
const profitFactor = Math.abs(lossesPnl) > 0 ? Math.abs(winsPnl / lossesPnl) : 0
|
|
|
|
const statistics = {
|
|
totalTrades: historicalTrades.length, // Include all trades (OPEN + COMPLETED)
|
|
completedTrades: completedTrades.length, // Only completed trades
|
|
openTrades: historicalTrades.filter(t => t.outcome === 'OPEN').length,
|
|
wins: wins.length,
|
|
losses: losses.length,
|
|
winRate: Math.round(winRate),
|
|
totalPnl: Math.round(totalPnl * 100) / 100,
|
|
winsPnl: Math.round(winsPnl * 100) / 100,
|
|
lossesPnl: Math.round(lossesPnl * 100) / 100,
|
|
avgWin: Math.round(avgWin * 100) / 100,
|
|
avgLoss: Math.round(avgLoss * 100) / 100,
|
|
profitFactor: Math.round(profitFactor * 100) / 100
|
|
}
|
|
|
|
console.log('📈 Trading Statistics:', statistics)
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
trades: historicalTrades,
|
|
statistics,
|
|
rpcStatus: {
|
|
connected: connectedEndpoint,
|
|
status: getRpcStatus()
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
note: "Real trading history API - showing only actual trades when available"
|
|
}, {
|
|
headers: {
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
'Pragma': 'no-cache',
|
|
'Expires': '0'
|
|
}
|
|
})
|
|
|
|
} catch (error) {
|
|
console.error('❌ Position history API error:', error)
|
|
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: 'Failed to get position history',
|
|
details: error.message,
|
|
rpcStatus: getRpcStatus()
|
|
}, { status: 500 })
|
|
}
|
|
}
|
|
|
|
export async function POST() {
|
|
return NextResponse.json({
|
|
message: 'Use GET method to retrieve position history'
|
|
}, { status: 405 })
|
|
}
|