mirror of
https://github.com/mortdeus/legacy-cc.git
synced 2026-09-28 14:55:15 +00:00
414 lines
12 KiB
JavaScript
414 lines
12 KiB
JavaScript
import { config } from '../config.js';
|
|
import logger from '../utils/logger.js';
|
|
import notificationService from './notifications.js';
|
|
|
|
class RiskManager {
|
|
constructor() {
|
|
this.dailyStats = {
|
|
totalTrades: 0,
|
|
profitableTrades: 0,
|
|
losingTrades: 0,
|
|
totalProfit: 0,
|
|
totalLoss: 0,
|
|
netPnL: 0,
|
|
startTime: new Date(),
|
|
};
|
|
|
|
this.activePositions = new Map();
|
|
this.tradeHistory = [];
|
|
this.lastTradeTime = 0;
|
|
|
|
// Reset daily stats at midnight
|
|
this.scheduleDailyReset();
|
|
}
|
|
|
|
// Check if a new trade is allowed
|
|
canExecuteTrade(amount, tokenMint) {
|
|
const now = Date.now();
|
|
const errors = [];
|
|
|
|
// Check daily loss limit
|
|
if (this.dailyStats.netPnL <= -config.risk.maxDailyLoss) {
|
|
errors.push(`Daily loss limit reached: ${this.dailyStats.netPnL.toFixed(4)} SOL`);
|
|
}
|
|
|
|
// Check single trade loss limit
|
|
if (amount > config.risk.maxSingleLoss) {
|
|
errors.push(`Trade amount ${amount} SOL exceeds single trade limit ${config.risk.maxSingleLoss} SOL`);
|
|
}
|
|
|
|
// Check position limit
|
|
if (this.activePositions.size >= config.trading.maxPositions) {
|
|
errors.push(`Maximum positions limit reached: ${this.activePositions.size}/${config.trading.maxPositions}`);
|
|
}
|
|
|
|
// Check cooldown period
|
|
if (now - this.lastTradeTime < config.risk.tradeCooldown) {
|
|
const remainingCooldown = config.risk.tradeCooldown - (now - this.lastTradeTime);
|
|
errors.push(`Trade cooldown active: ${Math.ceil(remainingCooldown / 1000)}s remaining`);
|
|
}
|
|
|
|
// Check if token is already in active positions
|
|
if (this.activePositions.has(tokenMint)) {
|
|
errors.push(`Token ${tokenMint} already has an active position`);
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
logger.warn('Trade blocked by risk manager', { errors, amount, tokenMint });
|
|
return { allowed: false, errors };
|
|
}
|
|
|
|
return { allowed: true, errors: [] };
|
|
}
|
|
|
|
// Record a new trade
|
|
recordTrade(tradeType, tokenMint, amount, price, txHash) {
|
|
const now = Date.now();
|
|
this.lastTradeTime = now;
|
|
|
|
const trade = {
|
|
id: `${tokenMint}-${now}`,
|
|
type: tradeType,
|
|
tokenMint,
|
|
amount,
|
|
price,
|
|
txHash,
|
|
timestamp: now,
|
|
status: 'pending',
|
|
};
|
|
|
|
if (tradeType === 'buy') {
|
|
this.activePositions.set(tokenMint, {
|
|
entryPrice: price,
|
|
entryAmount: amount,
|
|
entryTime: now,
|
|
entryValue: amount * price,
|
|
currentPrice: price,
|
|
tradeId: trade.id,
|
|
});
|
|
|
|
logger.info(`Position opened: ${tokenMint}`, {
|
|
entryPrice: price,
|
|
entryAmount: amount,
|
|
entryValue: amount * price,
|
|
});
|
|
|
|
notificationService.notifyPositionUpdate('opened', tokenMint, {
|
|
entryPrice: price,
|
|
entryAmount: amount,
|
|
entryValue: amount * price,
|
|
});
|
|
} else if (tradeType === 'sell') {
|
|
const position = this.activePositions.get(tokenMint);
|
|
if (position) {
|
|
const exitValue = amount * price;
|
|
const pnl = exitValue - position.entryValue;
|
|
const pnlRatio = exitValue / position.entryValue;
|
|
|
|
trade.pnl = pnl;
|
|
trade.pnlRatio = pnlRatio;
|
|
trade.entryPrice = position.entryPrice;
|
|
trade.entryValue = position.entryValue;
|
|
|
|
// Update daily stats
|
|
this.updateDailyStats(pnl, pnlRatio > 1);
|
|
|
|
// Remove from active positions
|
|
this.activePositions.delete(tokenMint);
|
|
|
|
logger.info(`Position closed: ${tokenMint}`, {
|
|
pnl,
|
|
pnlRatio: pnlRatio.toFixed(2),
|
|
entryPrice: position.entryPrice,
|
|
exitPrice: price,
|
|
});
|
|
|
|
// Send appropriate notification
|
|
if (pnlRatio >= config.trading.profitTarget) {
|
|
notificationService.notifyProfitTarget(tokenMint, pnlRatio, amount);
|
|
} else if (pnlRatio <= config.trading.stopLoss) {
|
|
notificationService.notifyStopLoss(tokenMint, pnlRatio, amount);
|
|
} else {
|
|
notificationService.notifyPositionUpdate('closed', tokenMint, {
|
|
pnl,
|
|
pnlRatio: pnlRatio.toFixed(2),
|
|
reason: 'manual',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
this.tradeHistory.push(trade);
|
|
logger.debug('Trade recorded', trade);
|
|
|
|
return trade;
|
|
}
|
|
|
|
// Update position price
|
|
updatePositionPrice(tokenMint, newPrice) {
|
|
const position = this.activePositions.get(tokenMint);
|
|
if (position) {
|
|
position.currentPrice = newPrice;
|
|
position.currentValue = position.entryAmount * newPrice;
|
|
position.pnl = position.currentValue - position.entryValue;
|
|
position.pnlRatio = position.currentValue / position.entryValue;
|
|
}
|
|
}
|
|
|
|
// Check if position should be closed based on risk parameters
|
|
shouldClosePosition(tokenMint) {
|
|
const position = this.activePositions.get(tokenMint);
|
|
if (!position) return { shouldClose: false, reason: null };
|
|
|
|
const now = Date.now();
|
|
const holdTime = now - position.entryTime;
|
|
|
|
// Check profit target
|
|
if (position.pnlRatio >= config.trading.profitTarget) {
|
|
return { shouldClose: true, reason: 'profit_target', pnlRatio: position.pnlRatio };
|
|
}
|
|
|
|
// Check stop loss
|
|
if (position.pnlRatio <= config.trading.stopLoss) {
|
|
return { shouldClose: true, reason: 'stop_loss', pnlRatio: position.pnlRatio };
|
|
}
|
|
|
|
// Check max hold time
|
|
if (holdTime >= config.trading.maxHoldTime) {
|
|
return { shouldClose: true, reason: 'max_hold_time', holdTime };
|
|
}
|
|
|
|
return { shouldClose: false, reason: null };
|
|
}
|
|
|
|
// Get position information
|
|
getPosition(tokenMint) {
|
|
return this.activePositions.get(tokenMint);
|
|
}
|
|
|
|
// Get all active positions
|
|
getActivePositions() {
|
|
return Array.from(this.activePositions.entries()).map(([mint, position]) => ({
|
|
mint,
|
|
...position,
|
|
holdTime: Date.now() - position.entryTime,
|
|
}));
|
|
}
|
|
|
|
// Get position summary
|
|
getPositionSummary() {
|
|
const positions = this.getActivePositions();
|
|
const totalValue = positions.reduce((sum, pos) => sum + pos.currentValue, 0);
|
|
const totalPnL = positions.reduce((sum, pos) => sum + pos.pnl, 0);
|
|
|
|
return {
|
|
activePositions: positions.length,
|
|
totalValue,
|
|
totalPnL,
|
|
averagePnL: positions.length > 0 ? totalPnL / positions.length : 0,
|
|
};
|
|
}
|
|
|
|
// Get daily statistics
|
|
getDailyStats() {
|
|
const now = new Date();
|
|
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
const uptime = now - this.dailyStats.startTime;
|
|
|
|
return {
|
|
...this.dailyStats,
|
|
uptime,
|
|
winRate: this.dailyStats.totalTrades > 0
|
|
? (this.dailyStats.profitableTrades / this.dailyStats.totalTrades * 100).toFixed(2)
|
|
: 0,
|
|
averageProfit: this.dailyStats.profitableTrades > 0
|
|
? this.dailyStats.totalProfit / this.dailyStats.profitableTrades
|
|
: 0,
|
|
averageLoss: this.dailyStats.losingTrades > 0
|
|
? this.dailyStats.totalLoss / this.dailyStats.losingTrades
|
|
: 0,
|
|
};
|
|
}
|
|
|
|
// Update daily statistics
|
|
updateDailyStats(pnl, isProfitable) {
|
|
this.dailyStats.totalTrades++;
|
|
|
|
if (isProfitable) {
|
|
this.dailyStats.profitableTrades++;
|
|
this.dailyStats.totalProfit += pnl;
|
|
} else {
|
|
this.dailyStats.losingTrades++;
|
|
this.dailyStats.totalLoss += Math.abs(pnl);
|
|
}
|
|
|
|
this.dailyStats.netPnL += pnl;
|
|
|
|
// Log daily stats update
|
|
logger.debug('Daily stats updated', {
|
|
totalTrades: this.dailyStats.totalTrades,
|
|
netPnL: this.dailyStats.netPnL.toFixed(4),
|
|
});
|
|
|
|
// Check if daily loss limit is approaching
|
|
if (this.dailyStats.netPnL <= -config.risk.maxDailyLoss * 0.8) {
|
|
notificationService.sendNotification(
|
|
`⚠️ Daily loss limit approaching: ${this.dailyStats.netPnL.toFixed(4)} SOL`,
|
|
'warning',
|
|
{ dailyStats: this.dailyStats }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Reset daily statistics
|
|
resetDailyStats() {
|
|
const previousStats = { ...this.dailyStats };
|
|
|
|
this.dailyStats = {
|
|
totalTrades: 0,
|
|
profitableTrades: 0,
|
|
losingTrades: 0,
|
|
totalProfit: 0,
|
|
totalLoss: 0,
|
|
netPnL: 0,
|
|
startTime: new Date(),
|
|
};
|
|
|
|
logger.info('Daily stats reset', { previousStats });
|
|
notificationService.sendNotification(
|
|
`📊 Daily trading session ended. Net PnL: ${previousStats.netPnL.toFixed(4)} SOL`,
|
|
'info',
|
|
{ previousStats }
|
|
);
|
|
}
|
|
|
|
// Schedule daily reset at midnight
|
|
scheduleDailyReset() {
|
|
const now = new Date();
|
|
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
const timeUntilMidnight = tomorrow - now;
|
|
|
|
setTimeout(() => {
|
|
this.resetDailyStats();
|
|
// Schedule next reset
|
|
this.scheduleDailyReset();
|
|
}, timeUntilMidnight);
|
|
}
|
|
|
|
// Emergency close all positions
|
|
async emergencyCloseAll(reason = 'emergency') {
|
|
logger.warn('Emergency closing all positions', { reason });
|
|
|
|
const positions = Array.from(this.activePositions.keys());
|
|
const results = [];
|
|
|
|
for (const mint of positions) {
|
|
try {
|
|
const position = this.activePositions.get(mint);
|
|
if (position) {
|
|
// Mark position for emergency closure
|
|
position.emergencyClose = true;
|
|
position.emergencyReason = reason;
|
|
|
|
results.push({
|
|
mint,
|
|
status: 'marked_for_closure',
|
|
reason,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error marking position for emergency closure', { mint, error });
|
|
results.push({
|
|
mint,
|
|
status: 'error',
|
|
error: error.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
notificationService.sendNotification(
|
|
`🚨 Emergency closure initiated for ${positions.length} positions`,
|
|
'warning',
|
|
{ reason, results }
|
|
);
|
|
|
|
return results;
|
|
}
|
|
|
|
// Get risk metrics
|
|
getRiskMetrics() {
|
|
const dailyStats = this.getDailyStats();
|
|
const positionSummary = this.getPositionSummary();
|
|
|
|
return {
|
|
dailyStats,
|
|
positionSummary,
|
|
riskLevel: this.calculateRiskLevel(),
|
|
recommendations: this.getRiskRecommendations(),
|
|
};
|
|
}
|
|
|
|
// Calculate overall risk level
|
|
calculateRiskLevel() {
|
|
const dailyStats = this.getDailyStats();
|
|
const positionSummary = this.getPositionSummary();
|
|
|
|
let riskScore = 0;
|
|
|
|
// Daily loss proximity
|
|
if (dailyStats.netPnL <= -config.risk.maxDailyLoss * 0.9) {
|
|
riskScore += 30;
|
|
} else if (dailyStats.netPnL <= -config.risk.maxDailyLoss * 0.7) {
|
|
riskScore += 20;
|
|
} else if (dailyStats.netPnL <= -config.risk.maxDailyLoss * 0.5) {
|
|
riskScore += 10;
|
|
}
|
|
|
|
// Position concentration
|
|
if (positionSummary.activePositions >= config.trading.maxPositions * 0.8) {
|
|
riskScore += 20;
|
|
}
|
|
|
|
// Win rate
|
|
if (dailyStats.winRate < 30) {
|
|
riskScore += 25;
|
|
} else if (dailyStats.winRate < 50) {
|
|
riskScore += 15;
|
|
}
|
|
|
|
if (riskScore >= 60) return 'HIGH';
|
|
if (riskScore >= 30) return 'MEDIUM';
|
|
return 'LOW';
|
|
}
|
|
|
|
// Get risk recommendations
|
|
getRiskRecommendations() {
|
|
const recommendations = [];
|
|
const dailyStats = this.getDailyStats();
|
|
const positionSummary = this.getPositionSummary();
|
|
|
|
if (dailyStats.netPnL <= -config.risk.maxDailyLoss * 0.8) {
|
|
recommendations.push('Consider reducing position sizes or stopping trading for the day');
|
|
}
|
|
|
|
if (positionSummary.activePositions >= config.trading.maxPositions * 0.8) {
|
|
recommendations.push('Approaching maximum position limit - consider closing some positions');
|
|
}
|
|
|
|
if (dailyStats.winRate < 40) {
|
|
recommendations.push('Low win rate - review trading strategy and risk parameters');
|
|
}
|
|
|
|
if (positionSummary.totalPnL < 0) {
|
|
recommendations.push('Overall portfolio in loss - consider implementing stricter stop losses');
|
|
}
|
|
|
|
return recommendations;
|
|
}
|
|
}
|
|
|
|
// Create singleton instance
|
|
const riskManager = new RiskManager();
|
|
|
|
export default riskManager;
|