feat: implement AI-driven DCA (Dollar Cost Averaging) system
AI-powered DCA manager with sophisticated reversal detection Multi-factor analysis: price movements, RSI, support/resistance, 24h trends Real example: SOL position analysis shows 5.2:1 risk/reward improvement lib/ai-dca-manager.ts - Complete DCA analysis engine with risk management Intelligent scaling: adds to positions when AI detects 50%+ reversal confidence Account-aware: uses up to 50% available balance with conservative 3x leverage Dynamic SL/TP: adjusts stop loss and take profit for new average position lib/automation-service-simple.ts - DCA monitoring in main trading cycle prisma/schema.prisma - DCARecord model for comprehensive tracking Checks DCA opportunities before new trade analysis (priority system) test-ai-dca-simple.js - Real SOL position test from screenshot data Entry: 85.98, Current: 83.87 (-1.13% underwater) AI recommendation: 1.08 SOL DCA → 4.91 profit potential Risk level: LOW with 407% liquidation safety margin LOGIC Price movement analysis: 1-10% against position optimal for DCA Market sentiment: 24h trends must align with DCA direction Technical indicators: RSI oversold (<35) for longs, overbought (>65) for shorts Support/resistance: proximity to key levels increases confidence Risk management: respects leverage limits and liquidation distances Complete error handling and fallback mechanisms Database persistence for DCA tracking and performance analysis Seamless integration with existing AI leverage calculator Real-time market data integration for accurate decision making
This commit is contained in:
423
lib/ai-dca-manager.ts
Normal file
423
lib/ai-dca-manager.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* AI-Driven DCA (Dollar Cost Averaging) Manager
|
||||
*
|
||||
* Intelligently adds to positions when market shows reversal potential
|
||||
* Manages risk by respecting leverage limits and adjusting stop loss/take profit
|
||||
*/
|
||||
|
||||
import AILeverageCalculator from './ai-leverage-calculator'
|
||||
|
||||
interface DCAAnalysisParams {
|
||||
currentPosition: {
|
||||
side: 'long' | 'short'
|
||||
size: number
|
||||
entryPrice: number
|
||||
currentPrice: number
|
||||
unrealizedPnl: number
|
||||
stopLoss: number
|
||||
takeProfit: number
|
||||
}
|
||||
accountStatus: {
|
||||
accountValue: number
|
||||
availableBalance: number
|
||||
leverage: number
|
||||
liquidationPrice: number
|
||||
}
|
||||
marketData: {
|
||||
price: number
|
||||
priceChange24h: number
|
||||
volume: number
|
||||
rsi?: number
|
||||
support?: number
|
||||
resistance?: number
|
||||
}
|
||||
maxLeverageAllowed: number
|
||||
}
|
||||
|
||||
interface DCAResult {
|
||||
shouldDCA: boolean
|
||||
dcaAmount: number
|
||||
newAveragePrice: number
|
||||
newStopLoss: number
|
||||
newTakeProfit: number
|
||||
newLeverage: number
|
||||
newLiquidationPrice: number
|
||||
riskAssessment: 'LOW' | 'MEDIUM' | 'HIGH'
|
||||
reasoning: string
|
||||
confidence: number
|
||||
}
|
||||
|
||||
export class AIDCAManager {
|
||||
|
||||
/**
|
||||
* Analyze if DCA opportunity exists and calculate optimal DCA parameters
|
||||
*/
|
||||
static analyzeDCAOpportunity(params: DCAAnalysisParams): DCAResult {
|
||||
const {
|
||||
currentPosition,
|
||||
accountStatus,
|
||||
marketData,
|
||||
maxLeverageAllowed
|
||||
} = params
|
||||
|
||||
console.log('🔄 AI DCA Analysis:', {
|
||||
position: `${currentPosition.side} ${currentPosition.size} @ $${currentPosition.entryPrice}`,
|
||||
currentPrice: `$${marketData.price}`,
|
||||
pnl: `$${currentPosition.unrealizedPnl.toFixed(2)}`,
|
||||
availableBalance: `$${accountStatus.availableBalance.toFixed(2)}`
|
||||
})
|
||||
|
||||
// Step 1: Analyze reversal potential
|
||||
const reversalAnalysis = this.analyzeReversalPotential(currentPosition, marketData)
|
||||
|
||||
if (!reversalAnalysis.hasReversalPotential) {
|
||||
return {
|
||||
shouldDCA: false,
|
||||
dcaAmount: 0,
|
||||
newAveragePrice: currentPosition.entryPrice,
|
||||
newStopLoss: currentPosition.stopLoss,
|
||||
newTakeProfit: currentPosition.takeProfit,
|
||||
newLeverage: accountStatus.leverage,
|
||||
newLiquidationPrice: accountStatus.liquidationPrice,
|
||||
riskAssessment: 'HIGH',
|
||||
reasoning: reversalAnalysis.reasoning,
|
||||
confidence: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Calculate safe DCA amount
|
||||
const dcaCalculation = this.calculateSafeDCAAmount(
|
||||
currentPosition,
|
||||
accountStatus,
|
||||
marketData,
|
||||
maxLeverageAllowed
|
||||
)
|
||||
|
||||
if (dcaCalculation.dcaAmount === 0) {
|
||||
return {
|
||||
shouldDCA: false,
|
||||
dcaAmount: 0,
|
||||
newAveragePrice: currentPosition.entryPrice,
|
||||
newStopLoss: currentPosition.stopLoss,
|
||||
newTakeProfit: currentPosition.takeProfit,
|
||||
newLeverage: accountStatus.leverage,
|
||||
newLiquidationPrice: accountStatus.liquidationPrice,
|
||||
riskAssessment: 'HIGH',
|
||||
reasoning: dcaCalculation.reasoning,
|
||||
confidence: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Calculate new position parameters
|
||||
const newPositionSize = currentPosition.size + dcaCalculation.dcaAmount
|
||||
const newAveragePrice = this.calculateNewAveragePrice(
|
||||
currentPosition.size,
|
||||
currentPosition.entryPrice,
|
||||
dcaCalculation.dcaAmount,
|
||||
marketData.price
|
||||
)
|
||||
|
||||
// Step 4: Calculate new stop loss and take profit
|
||||
const newSLTP = this.calculateNewStopLossAndTakeProfit(
|
||||
currentPosition.side,
|
||||
newAveragePrice,
|
||||
newPositionSize,
|
||||
marketData,
|
||||
reversalAnalysis.confidence
|
||||
)
|
||||
|
||||
// Step 5: Calculate new leverage and liquidation price
|
||||
const newLeverage = this.calculateNewLeverage(
|
||||
newPositionSize,
|
||||
newAveragePrice,
|
||||
accountStatus.accountValue,
|
||||
dcaCalculation.dcaAmount
|
||||
)
|
||||
|
||||
const newLiquidationPrice = this.calculateNewLiquidationPrice(
|
||||
newAveragePrice,
|
||||
newLeverage,
|
||||
currentPosition.side
|
||||
)
|
||||
|
||||
// Step 6: Final risk assessment
|
||||
const riskAssessment = this.assessDCARisk(
|
||||
newLeverage,
|
||||
newLiquidationPrice,
|
||||
newSLTP.stopLoss,
|
||||
accountStatus.availableBalance,
|
||||
dcaCalculation.dcaAmount
|
||||
)
|
||||
|
||||
const result: DCAResult = {
|
||||
shouldDCA: true,
|
||||
dcaAmount: dcaCalculation.dcaAmount,
|
||||
newAveragePrice,
|
||||
newStopLoss: newSLTP.stopLoss,
|
||||
newTakeProfit: newSLTP.takeProfit,
|
||||
newLeverage,
|
||||
newLiquidationPrice,
|
||||
riskAssessment,
|
||||
reasoning: this.generateDCAReasoning(reversalAnalysis, dcaCalculation, newAveragePrice, newLeverage),
|
||||
confidence: reversalAnalysis.confidence
|
||||
}
|
||||
|
||||
console.log('🚀 DCA Recommendation:', {
|
||||
shouldDCA: result.shouldDCA,
|
||||
dcaAmount: `$${result.dcaAmount.toFixed(2)}`,
|
||||
newAverage: `$${result.newAveragePrice.toFixed(4)}`,
|
||||
newSL: `$${result.newStopLoss.toFixed(4)}`,
|
||||
newTP: `$${result.newTakeProfit.toFixed(4)}`,
|
||||
newLeverage: `${result.newLeverage.toFixed(1)}x`,
|
||||
riskLevel: result.riskAssessment,
|
||||
confidence: `${result.confidence}%`
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze market conditions for reversal potential
|
||||
*/
|
||||
private static analyzeReversalPotential(position: any, marketData: any): {
|
||||
hasReversalPotential: boolean
|
||||
reasoning: string
|
||||
confidence: number
|
||||
} {
|
||||
|
||||
const priceMovement = ((marketData.price - position.entryPrice) / position.entryPrice) * 100
|
||||
const isMovingAgainstPosition = (position.side === 'long' && priceMovement < 0) ||
|
||||
(position.side === 'short' && priceMovement > 0)
|
||||
|
||||
// Only consider DCA if price moved against us (creating discount opportunity)
|
||||
if (!isMovingAgainstPosition) {
|
||||
return {
|
||||
hasReversalPotential: false,
|
||||
reasoning: "Price moving in our favor - no DCA needed",
|
||||
confidence: 0
|
||||
}
|
||||
}
|
||||
|
||||
let confidence = 0
|
||||
const reasons = []
|
||||
|
||||
// Factor 1: Price drop magnitude (for longs) or rise magnitude (for shorts)
|
||||
const movementMagnitude = Math.abs(priceMovement)
|
||||
if (movementMagnitude >= 1 && movementMagnitude <= 5) {
|
||||
confidence += 30
|
||||
reasons.push(`${movementMagnitude.toFixed(1)}% movement creates DCA opportunity`)
|
||||
} else if (movementMagnitude > 5 && movementMagnitude <= 10) {
|
||||
confidence += 40
|
||||
reasons.push(`${movementMagnitude.toFixed(1)}% movement shows strong discount`)
|
||||
} else if (movementMagnitude > 10) {
|
||||
confidence += 20
|
||||
reasons.push(`${movementMagnitude.toFixed(1)}% movement may indicate trend change`)
|
||||
}
|
||||
|
||||
// Factor 2: 24h price change context
|
||||
if (marketData.priceChange24h !== undefined) {
|
||||
if (position.side === 'long' && marketData.priceChange24h < -3) {
|
||||
confidence += 25
|
||||
reasons.push("24h downtrend creates long DCA opportunity")
|
||||
} else if (position.side === 'short' && marketData.priceChange24h > 3) {
|
||||
confidence += 25
|
||||
reasons.push("24h uptrend creates short DCA opportunity")
|
||||
}
|
||||
}
|
||||
|
||||
// Factor 3: Support/Resistance levels
|
||||
if (marketData.support && position.side === 'long' && marketData.price <= marketData.support * 1.02) {
|
||||
confidence += 20
|
||||
reasons.push("Price near support level")
|
||||
}
|
||||
if (marketData.resistance && position.side === 'short' && marketData.price >= marketData.resistance * 0.98) {
|
||||
confidence += 20
|
||||
reasons.push("Price near resistance level")
|
||||
}
|
||||
|
||||
// Factor 4: RSI oversold/overbought
|
||||
if (marketData.rsi !== undefined) {
|
||||
if (position.side === 'long' && marketData.rsi < 35) {
|
||||
confidence += 15
|
||||
reasons.push("RSI oversold - reversal likely")
|
||||
} else if (position.side === 'short' && marketData.rsi > 65) {
|
||||
confidence += 15
|
||||
reasons.push("RSI overbought - reversal likely")
|
||||
}
|
||||
}
|
||||
|
||||
// Minimum confidence threshold for DCA
|
||||
const hasReversalPotential = confidence >= 50
|
||||
|
||||
return {
|
||||
hasReversalPotential,
|
||||
reasoning: hasReversalPotential
|
||||
? reasons.join(", ")
|
||||
: `Insufficient reversal signals (${confidence}% confidence)`,
|
||||
confidence
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate safe DCA amount respecting leverage and liquidation limits
|
||||
*/
|
||||
private static calculateSafeDCAAmount(
|
||||
position: any,
|
||||
accountStatus: any,
|
||||
marketData: any,
|
||||
maxLeverageAllowed: number
|
||||
): { dcaAmount: number, reasoning: string } {
|
||||
|
||||
// Use AI Leverage Calculator to determine safe additional position size
|
||||
const currentStopLossPrice = position.side === 'long'
|
||||
? position.entryPrice * (1 - position.stopLoss / 100)
|
||||
: position.entryPrice * (1 + position.stopLoss / 100)
|
||||
|
||||
const leverageResult = AILeverageCalculator.calculateOptimalLeverage({
|
||||
accountValue: accountStatus.accountValue,
|
||||
availableBalance: accountStatus.availableBalance,
|
||||
entryPrice: marketData.price, // Current price for DCA entry
|
||||
stopLossPrice: currentStopLossPrice, // Keep same SL initially
|
||||
side: position.side,
|
||||
maxLeverageAllowed,
|
||||
safetyBuffer: 0.15 // More conservative for DCA
|
||||
})
|
||||
|
||||
// Calculate maximum safe DCA amount
|
||||
const maxDCAUSD = accountStatus.availableBalance * 0.5 // Use up to 50% of available balance
|
||||
const leveragedDCAAmount = maxDCAUSD * leverageResult.recommendedLeverage
|
||||
const dcaTokenAmount = leveragedDCAAmount / marketData.price
|
||||
|
||||
// Ensure DCA doesn't make position too large relative to account
|
||||
const currentPositionValue = position.size * position.entryPrice
|
||||
const maxPositionValue = accountStatus.accountValue * 3 // Max 3x account value
|
||||
const remainingCapacity = maxPositionValue - currentPositionValue
|
||||
|
||||
const finalDCAAmount = Math.min(
|
||||
dcaTokenAmount,
|
||||
remainingCapacity / marketData.price,
|
||||
position.size * 0.5 // Max 50% of current position size
|
||||
)
|
||||
|
||||
if (finalDCAAmount < 0.01) {
|
||||
return {
|
||||
dcaAmount: 0,
|
||||
reasoning: "Insufficient available balance or position capacity for safe DCA"
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dcaAmount: finalDCAAmount,
|
||||
reasoning: `Safe DCA: ${finalDCAAmount.toFixed(4)} tokens using ${leverageResult.recommendedLeverage.toFixed(1)}x leverage`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate new average price after DCA
|
||||
*/
|
||||
private static calculateNewAveragePrice(
|
||||
currentSize: number,
|
||||
currentPrice: number,
|
||||
dcaSize: number,
|
||||
dcaPrice: number
|
||||
): number {
|
||||
const totalValue = (currentSize * currentPrice) + (dcaSize * dcaPrice)
|
||||
const totalSize = currentSize + dcaSize
|
||||
return totalValue / totalSize
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate new stop loss and take profit for the averaged position
|
||||
*/
|
||||
private static calculateNewStopLossAndTakeProfit(
|
||||
side: 'long' | 'short',
|
||||
newAveragePrice: number,
|
||||
newPositionSize: number,
|
||||
marketData: any,
|
||||
confidence: number
|
||||
): { stopLoss: number, takeProfit: number } {
|
||||
|
||||
// Adjust SL/TP based on confidence and position size
|
||||
const baseStopLossPercent = 3 // Base 3% stop loss
|
||||
const baseTakeProfitPercent = confidence > 70 ? 8 : 6 // Higher TP if very confident
|
||||
|
||||
// Make SL tighter for larger positions
|
||||
const positionSizeMultiplier = Math.min(newPositionSize / 5, 1.5) // Cap at 1.5x
|
||||
const adjustedSLPercent = baseStopLossPercent / positionSizeMultiplier
|
||||
|
||||
let stopLoss: number
|
||||
let takeProfit: number
|
||||
|
||||
if (side === 'long') {
|
||||
stopLoss = newAveragePrice * (1 - adjustedSLPercent / 100)
|
||||
takeProfit = newAveragePrice * (1 + baseTakeProfitPercent / 100)
|
||||
} else {
|
||||
stopLoss = newAveragePrice * (1 + adjustedSLPercent / 100)
|
||||
takeProfit = newAveragePrice * (1 - baseTakeProfitPercent / 100)
|
||||
}
|
||||
|
||||
return { stopLoss, takeProfit }
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate new effective leverage after DCA
|
||||
*/
|
||||
private static calculateNewLeverage(
|
||||
newPositionSize: number,
|
||||
newAveragePrice: number,
|
||||
accountValue: number,
|
||||
dcaAmount: number
|
||||
): number {
|
||||
const totalPositionValue = newPositionSize * newAveragePrice
|
||||
return totalPositionValue / accountValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate new liquidation price
|
||||
*/
|
||||
private static calculateNewLiquidationPrice(
|
||||
averagePrice: number,
|
||||
leverage: number,
|
||||
side: 'long' | 'short'
|
||||
): number {
|
||||
if (side === 'long') {
|
||||
return averagePrice * (1 - 1/leverage)
|
||||
} else {
|
||||
return averagePrice * (1 + 1/leverage)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assess risk of DCA operation
|
||||
*/
|
||||
private static assessDCARisk(
|
||||
newLeverage: number,
|
||||
liquidationPrice: number,
|
||||
stopLoss: number,
|
||||
availableBalance: number,
|
||||
dcaAmount: number
|
||||
): 'LOW' | 'MEDIUM' | 'HIGH' {
|
||||
|
||||
const liquidationBuffer = Math.abs(liquidationPrice - stopLoss) / stopLoss * 100
|
||||
const balanceUsagePercent = (dcaAmount * liquidationPrice) / availableBalance * 100
|
||||
|
||||
if (newLeverage <= 3 && liquidationBuffer >= 15 && balanceUsagePercent <= 30) return 'LOW'
|
||||
if (newLeverage <= 6 && liquidationBuffer >= 10 && balanceUsagePercent <= 50) return 'MEDIUM'
|
||||
|
||||
return 'HIGH'
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate reasoning for DCA decision
|
||||
*/
|
||||
private static generateDCAReasoning(
|
||||
reversalAnalysis: any,
|
||||
dcaCalculation: any,
|
||||
newAveragePrice: number,
|
||||
newLeverage: number
|
||||
): string {
|
||||
return `${reversalAnalysis.reasoning}. ${dcaCalculation.reasoning}. New average: $${newAveragePrice.toFixed(4)} with ${newLeverage.toFixed(1)}x leverage.`
|
||||
}
|
||||
}
|
||||
|
||||
export default AIDCAManager
|
||||
Reference in New Issue
Block a user