fix: add null checks to prevent 'Cannot read properties of null' error on automation page
- Added proper null checks for status object before accessing selectedTimeframes - Fixed timeframes display to handle null status gracefully - Fixed analysis interval calculation with optional chaining - Resolved 500 internal server error on /automation-v2 page
This commit is contained in:
104
.github/copilot-instructions.instructions.md
vendored
104
.github/copilot-instructions.instructions.md
vendored
@@ -15,35 +15,11 @@ This is a Next.js 15 App Router application with TypeScript, Tailwind CSS, and A
|
|||||||
- TradingView automation with session persistence in `lib/tradingview-automation.ts`
|
- TradingView automation with session persistence in `lib/tradingview-automation.ts`
|
||||||
- Session data stored in `.tradingview-session/` volume mount to avoid captchas
|
- Session data stored in `.tradingview-session/` volume mount to avoid captchas
|
||||||
|
|
||||||
### AI-Driven Dynamic Leverage System ✅
|
### AI Analysis Pipeline
|
||||||
**Complete AI leverage calculator with intelligent position sizing:**
|
- OpenAI GPT-4o mini for cost-effective chart analysis (~$0.006 per analysis)
|
||||||
- `lib/ai-leverage-calculator.ts` - Core AI leverage calculation engine with risk management
|
- Multi-layout comparison and consensus detection in `lib/ai-analysis.ts`
|
||||||
- Account-based strategies: <$1k uses 100% balance (aggressive), >$1k uses 50% balance (conservative)
|
- Professional trading setups with exact entry/exit levels and risk management
|
||||||
- Safety mechanisms: 10% buffer between liquidation price and stop loss
|
- Layout-specific indicator analysis (RSI vs Stochastic RSI, MACD vs OBV)
|
||||||
- Platform integration: Drift Protocol with maximum 20x leverage constraints
|
|
||||||
- **Integration**: Enhanced `lib/automation-service-simple.ts` uses AI-calculated leverage for all positions
|
|
||||||
|
|
||||||
### AI-Driven DCA (Dollar Cost Averaging) System ✅
|
|
||||||
**Revolutionary position scaling that maximizes profits while managing risk:**
|
|
||||||
- `lib/ai-dca-manager.ts` - AI-powered DCA analysis engine with reversal detection
|
|
||||||
- **Multi-factor Analysis**: Price movements, 24h trends, RSI levels, support/resistance
|
|
||||||
- **Smart Scaling**: Adds to positions when AI detects reversal potential (50%+ confidence threshold)
|
|
||||||
- **Risk Management**: Respects leverage limits, adjusts stop loss/take profit for new average price
|
|
||||||
- **Account Integration**: Uses available balance strategically (up to 50% for DCA operations)
|
|
||||||
- **Real Example**: SOL position at $185.98 entry, $183.87 current → AI recommends 1.08 SOL DCA for 5.2:1 R/R improvement
|
|
||||||
|
|
||||||
**DCA Decision Factors:**
|
|
||||||
- Price movement against position (1-10% optimal range)
|
|
||||||
- 24h market sentiment alignment with DCA direction
|
|
||||||
- Technical indicators (RSI oversold/overbought zones)
|
|
||||||
- Proximity to support/resistance levels
|
|
||||||
- Available balance and current leverage headroom
|
|
||||||
- Liquidation distance and safety buffers
|
|
||||||
|
|
||||||
**Integration Points:**
|
|
||||||
- `lib/automation-service-simple.ts` - Automated DCA monitoring in main trading cycle
|
|
||||||
- `prisma/schema.prisma` - DCARecord model for tracking all scaling operations
|
|
||||||
- Database tracking of DCA count, total amount, and performance metrics
|
|
||||||
|
|
||||||
### Trading Integration
|
### Trading Integration
|
||||||
- **Drift Protocol**: Perpetual futures trading via `@drift-labs/sdk`
|
- **Drift Protocol**: Perpetual futures trading via `@drift-labs/sdk`
|
||||||
@@ -53,43 +29,6 @@ This is a Next.js 15 App Router application with TypeScript, Tailwind CSS, and A
|
|||||||
|
|
||||||
## Critical Development Patterns
|
## Critical Development Patterns
|
||||||
|
|
||||||
### Automation System Development Wisdom
|
|
||||||
**Key lessons from building and debugging the automation system:**
|
|
||||||
|
|
||||||
#### AI Risk Management vs Manual Controls
|
|
||||||
- **NEVER mix manual TP/SL inputs with AI automation** - causes conflicts and unpredictable behavior
|
|
||||||
- When implementing AI-driven automation, remove all manual percentage inputs from the UI
|
|
||||||
- AI should calculate dynamic stop losses and take profits based on market conditions, not user-defined percentages
|
|
||||||
- Always validate that UI selections (timeframes, strategies) are properly passed to backend services
|
|
||||||
|
|
||||||
#### Balance and P&L Calculation Critical Rules
|
|
||||||
- **ALWAYS use Drift SDK's built-in calculation methods** instead of manual calculations
|
|
||||||
- Use `driftClient.getUser().getTotalCollateral()` for accurate collateral values
|
|
||||||
- Use `driftClient.getUser().getUnrealizedPNL()` for accurate P&L calculations
|
|
||||||
- **NEVER use hardcoded prices** (like $195 for SOL) - always get current market data
|
|
||||||
- **NEVER use empirical precision factors** - use official SDK precision handling
|
|
||||||
- Test balance calculations against actual Drift interface values for validation
|
|
||||||
- Unrealized P&L should match position-level P&L calculations
|
|
||||||
|
|
||||||
#### Timeframe Handling Best Practices
|
|
||||||
- **Always use minute values first** in timeframe mapping to avoid TradingView confusion
|
|
||||||
- Example: `'4h': ['240', '240m', '4h', '4H']` - 240 minutes FIRST, then alternatives
|
|
||||||
- Validate that UI timeframe selections reach the automation service correctly
|
|
||||||
- Log timeframe values at every step to catch hardcoded overrides
|
|
||||||
|
|
||||||
#### System Integration Debugging
|
|
||||||
- **Always validate data flow** from UI → API → Service → Trading execution
|
|
||||||
- Check for hardcoded values that override user selections (especially timeframes)
|
|
||||||
- Verify correct protocol usage (Drift vs Jupiter) in trading execution
|
|
||||||
- Test cleanup systems regularly - memory leaks kill automation reliability
|
|
||||||
- Implement comprehensive logging for multi-step processes
|
|
||||||
|
|
||||||
#### Analysis Timer Implementation
|
|
||||||
- Store `nextScheduled` timestamps in database for persistence across restarts
|
|
||||||
- Calculate countdown dynamically based on current time vs scheduled time
|
|
||||||
- Update timer fields in automation status responses for real-time UI updates
|
|
||||||
- Format countdown as "XhYm" or "Xm Ys" for better user experience
|
|
||||||
|
|
||||||
### Docker Container Development (Required)
|
### Docker Container Development (Required)
|
||||||
**All development happens inside Docker containers** using Docker Compose v2. Browser automation requires specific system dependencies that are only available in the containerized environment:
|
**All development happens inside Docker containers** using Docker Compose v2. Browser automation requires specific system dependencies that are only available in the containerized environment:
|
||||||
|
|
||||||
@@ -618,39 +557,6 @@ When working with this codebase, prioritize Docker consistency, understand the d
|
|||||||
4. Commit restoration: `git add . && git commit -m "fix: restore automation-v2 functionality" && git push`
|
4. Commit restoration: `git add . && git commit -m "fix: restore automation-v2 functionality" && git push`
|
||||||
5. Rebuild container to persist restoration
|
5. Rebuild container to persist restoration
|
||||||
|
|
||||||
### Testing and Validation Patterns (Critical)
|
|
||||||
**Essential validation steps learned from complex automation debugging:**
|
|
||||||
|
|
||||||
#### API Response Validation
|
|
||||||
- **Always test API responses directly** with curl before debugging UI issues
|
|
||||||
- Compare calculated values against actual trading platform values
|
|
||||||
- Example: `curl -s http://localhost:9001/api/drift/balance | jq '.unrealizedPnl'`
|
|
||||||
- Validate that API returns realistic values (2-5% targets, not 500% gains)
|
|
||||||
|
|
||||||
#### Multi-Component System Testing
|
|
||||||
- **Test data flow end-to-end**: UI selection → API endpoint → Service logic → Database storage
|
|
||||||
- Use browser dev tools to verify API calls match expected parameters
|
|
||||||
- Check database updates after automation cycles complete
|
|
||||||
- Validate that timer calculations match expected intervals
|
|
||||||
|
|
||||||
#### Trading Integration Validation
|
|
||||||
- **Never assume trading calculations are correct** - always validate against platform
|
|
||||||
- Test with small amounts first when implementing new trading logic
|
|
||||||
- Compare bot-calculated P&L with actual platform P&L values
|
|
||||||
- Verify protocol selection (Drift vs Jupiter) matches intended trading method
|
|
||||||
|
|
||||||
#### AI Analysis Output Validation
|
|
||||||
- **Always check AI responses for realistic values** before using in trading
|
|
||||||
- AI can return absolute prices when percentages are expected - validate data types
|
|
||||||
- Log AI analysis results to catch unrealistic take profit targets (>50% gains)
|
|
||||||
- Implement bounds checking on AI-generated trading parameters
|
|
||||||
|
|
||||||
#### Cleanup System Monitoring
|
|
||||||
- **Test cleanup functionality after every automation cycle**
|
|
||||||
- Monitor memory usage patterns to catch cleanup failures early
|
|
||||||
- Verify that cleanup triggers properly after analysis completion
|
|
||||||
- Check for zombie browser processes that indicate cleanup issues
|
|
||||||
|
|
||||||
### Successful Implementation Workflow
|
### Successful Implementation Workflow
|
||||||
**After completing any feature or fix:**
|
**After completing any feature or fix:**
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ export default function AutomationPageV2() {
|
|||||||
<div className="p-2 bg-gray-800/30 rounded-lg mb-3">
|
<div className="p-2 bg-gray-800/30 rounded-lg mb-3">
|
||||||
<div className="text-xs text-gray-400">
|
<div className="text-xs text-gray-400">
|
||||||
Selected: <span className="text-cyan-400">
|
Selected: <span className="text-cyan-400">
|
||||||
{(status.selectedTimeframes || [status.timeframe]).map(tf => timeframes.find(t => t.value === tf)?.label || tf).filter(Boolean).join(', ')}
|
{config.selectedTimeframes.map(tf => timeframes.find(t => t.value === tf)?.label || tf).filter(Boolean).join(', ')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 mt-1">
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
@@ -499,7 +499,12 @@ export default function AutomationPageV2() {
|
|||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-300">Timeframes:</span>
|
<span className="text-gray-300">Timeframes:</span>
|
||||||
<span className="text-cyan-400 font-semibold text-xs">
|
<span className="text-cyan-400 font-semibold text-xs">
|
||||||
{(status.selectedTimeframes || [status.timeframe]).map(tf => timeframes.find(t => t.value === tf)?.label || tf).filter(Boolean).join(', ')}
|
{status && status.selectedTimeframes ?
|
||||||
|
status.selectedTimeframes.map(tf => timeframes.find(t => t.value === tf)?.label || tf).filter(Boolean).join(', ') :
|
||||||
|
status && status.timeframe ?
|
||||||
|
(timeframes.find(t => t.value === status.timeframe)?.label || status.timeframe) :
|
||||||
|
'N/A'
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -620,7 +625,7 @@ export default function AutomationPageV2() {
|
|||||||
<div
|
<div
|
||||||
className="bg-blue-500 h-2 rounded-full transition-all duration-1000"
|
className="bg-blue-500 h-2 rounded-full transition-all duration-1000"
|
||||||
style={{
|
style={{
|
||||||
width: status.analysisInterval > 0 ?
|
width: status?.analysisInterval > 0 ?
|
||||||
`${Math.max(0, 100 - (nextAnalysisCountdown / status.analysisInterval) * 100)}%` :
|
`${Math.max(0, 100 - (nextAnalysisCountdown / status.analysisInterval) * 100)}%` :
|
||||||
'0%'
|
'0%'
|
||||||
}}
|
}}
|
||||||
@@ -628,11 +633,11 @@ export default function AutomationPageV2() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 text-center">
|
<div className="text-xs text-gray-400 text-center">
|
||||||
Analysis Interval: {(() => {
|
Analysis Interval: {(() => {
|
||||||
const intervalSec = status.analysisInterval || 0
|
const intervalSec = status?.analysisInterval || 0
|
||||||
const intervalMin = Math.floor(intervalSec / 60)
|
const intervalMin = Math.floor(intervalSec / 60)
|
||||||
|
|
||||||
// Determine strategy type for display
|
// Determine strategy type for display
|
||||||
if (status.selectedTimeframes) {
|
if (status?.selectedTimeframes) {
|
||||||
const timeframes = status.selectedTimeframes
|
const timeframes = status.selectedTimeframes
|
||||||
const isScalping = timeframes.includes('5') || timeframes.includes('3') ||
|
const isScalping = timeframes.includes('5') || timeframes.includes('3') ||
|
||||||
(timeframes.length > 1 && timeframes.every(tf => ['1', '3', '5', '15', '30'].includes(tf)))
|
(timeframes.length > 1 && timeframes.every(tf => ['1', '3', '5', '15', '30'].includes(tf)))
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function AutomationPageV2() {
|
|||||||
timeframe: '1h', // Primary timeframe for backwards compatibility
|
timeframe: '1h', // Primary timeframe for backwards compatibility
|
||||||
selectedTimeframes: ['60'], // Multi-timeframe support
|
selectedTimeframes: ['60'], // Multi-timeframe support
|
||||||
tradingAmount: 100,
|
tradingAmount: 100,
|
||||||
maxLeverage: 5,
|
maxLeverage: 20, // Maximum allowed leverage for AI calculations
|
||||||
stopLossPercent: 2,
|
stopLossPercent: 2,
|
||||||
takeProfitPercent: 6,
|
takeProfitPercent: 6,
|
||||||
riskPercentage: 2
|
riskPercentage: 2
|
||||||
@@ -172,7 +172,7 @@ export default function AutomationPageV2() {
|
|||||||
<h3 className="text-xl font-bold text-white mb-6">Configuration</h3>
|
<h3 className="text-xl font-bold text-white mb-6">Configuration</h3>
|
||||||
|
|
||||||
{/* Trading Mode */}
|
{/* Trading Mode */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-1 gap-6 mb-6">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<label className="block text-sm font-bold text-blue-400">Trading Mode</label>
|
<label className="block text-sm font-bold text-blue-400">Trading Mode</label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -199,23 +199,16 @@ export default function AutomationPageV2() {
|
|||||||
<span className="text-white font-semibold">Live Trading</span>
|
<span className="text-white font-semibold">Live Trading</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-2 p-3 bg-blue-900/20 border border-blue-700 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-blue-400">🧠</span>
|
||||||
|
<span className="text-sm text-blue-300 font-medium">AI-Driven Leverage</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-blue-200 mt-1">
|
||||||
|
Leverage is now calculated automatically by AI based on account balance, market conditions, and risk assessment.
|
||||||
|
The system optimizes between 1x-20x for maximum profit while maintaining liquidation safety.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<label className="block text-sm font-bold text-purple-400">Leverage</label>
|
|
||||||
<select
|
|
||||||
className="w-full p-3 bg-gray-700 border border-gray-600 rounded-lg text-white focus:border-purple-400"
|
|
||||||
value={config.maxLeverage}
|
|
||||||
onChange={(e) => setConfig({...config, maxLeverage: parseInt(e.target.value)})}
|
|
||||||
disabled={status?.isActive}
|
|
||||||
>
|
|
||||||
<option value="1">1x - Spot</option>
|
|
||||||
<option value="2">2x</option>
|
|
||||||
<option value="3">3x</option>
|
|
||||||
<option value="5">5x</option>
|
|
||||||
<option value="10">10x</option>
|
|
||||||
<option value="20">20x</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -254,11 +247,9 @@ export default function AutomationPageV2() {
|
|||||||
Available: ${parseFloat(balance.availableBalance).toFixed(2)} • Using {((config.tradingAmount / balance.availableBalance) * 100).toFixed(1)}% of balance
|
Available: ${parseFloat(balance.availableBalance).toFixed(2)} • Using {((config.tradingAmount / balance.availableBalance) * 100).toFixed(1)}% of balance
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{balance && config.maxLeverage > 1 && (
|
<p className="text-xs text-cyan-400 mt-1">
|
||||||
<p className="text-xs text-green-400 mt-1">
|
💡 AI will apply optimal leverage automatically based on market conditions
|
||||||
With {config.maxLeverage}x leverage: ${(config.tradingAmount * config.maxLeverage).toFixed(2)} position size
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -484,8 +475,8 @@ export default function AutomationPageV2() {
|
|||||||
<span className="text-white font-semibold">{status.symbol}</span>
|
<span className="text-white font-semibold">{status.symbol}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-300">Leverage:</span>
|
<span className="text-gray-300">AI Leverage:</span>
|
||||||
<span className="text-yellow-400 font-semibold">{config.maxLeverage}x</span>
|
<span className="text-cyan-400 font-semibold">Auto-Calculated</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -505,9 +496,9 @@ export default function AutomationPageV2() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-blue-400">
|
<div className="text-2xl font-bold text-blue-400">
|
||||||
{balance ? parseFloat(balance.leverage || 0).toFixed(1) : '0.0'}%
|
{balance && balance.actualLeverage ? parseFloat(balance.actualLeverage).toFixed(1) : 'AI'}x
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400">Leverage Used</div>
|
<div className="text-xs text-gray-400">Current Leverage</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-red-400">
|
<div className="text-2xl font-bold text-red-400">
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { PrismaClient } from '@prisma/client'
|
import { PrismaClient } from '@prisma/client'
|
||||||
import { aiAnalysisService, AnalysisResult } from './ai-analysis'
|
import { aiAnalysisService, AnalysisResult } from './ai-analysis'
|
||||||
import { jupiterDEXService } from './jupiter-dex-service'
|
|
||||||
import { enhancedScreenshotService } from './enhanced-screenshot-simple'
|
import { enhancedScreenshotService } from './enhanced-screenshot-simple'
|
||||||
import { TradingViewCredentials } from './tradingview-automation'
|
import { TradingViewCredentials } from './tradingview-automation'
|
||||||
import { progressTracker, ProgressStatus } from './progress-tracker'
|
import { progressTracker, ProgressStatus } from './progress-tracker'
|
||||||
@@ -8,19 +7,22 @@ import aggressiveCleanup from './aggressive-cleanup'
|
|||||||
import { analysisCompletionFlag } from './analysis-completion-flag'
|
import { analysisCompletionFlag } from './analysis-completion-flag'
|
||||||
import priceMonitorService from './price-monitor'
|
import priceMonitorService from './price-monitor'
|
||||||
|
|
||||||
const prisma = new PrismaClient()
|
import prisma from '../lib/prisma'
|
||||||
|
import AILeverageCalculator from './ai-leverage-calculator'
|
||||||
|
import AIDCAManager from './ai-dca-manager'
|
||||||
|
|
||||||
export interface AutomationConfig {
|
export interface AutomationConfig {
|
||||||
userId: string
|
userId: string
|
||||||
mode: 'SIMULATION' | 'LIVE'
|
mode: 'SIMULATION' | 'LIVE'
|
||||||
symbol: string
|
symbol: string
|
||||||
timeframe: string
|
timeframe: string
|
||||||
|
selectedTimeframes?: string[] // Multi-timeframe support from UI
|
||||||
tradingAmount: number
|
tradingAmount: number
|
||||||
maxLeverage: number
|
maxLeverage: number
|
||||||
stopLossPercent: number
|
// stopLossPercent and takeProfitPercent removed - AI calculates these automatically
|
||||||
takeProfitPercent: number
|
|
||||||
maxDailyTrades: number
|
maxDailyTrades: number
|
||||||
riskPercentage: number
|
riskPercentage: number
|
||||||
|
dexProvider?: string // DEX provider (DRIFT or JUPITER)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutomationStatus {
|
export interface AutomationStatus {
|
||||||
@@ -37,6 +39,9 @@ export interface AutomationStatus {
|
|||||||
nextScheduled?: Date
|
nextScheduled?: Date
|
||||||
errorCount: number
|
errorCount: number
|
||||||
lastError?: string
|
lastError?: string
|
||||||
|
nextAnalysisIn?: number // Seconds until next analysis
|
||||||
|
analysisInterval?: number // Analysis interval in seconds
|
||||||
|
currentCycle?: number // Current automation cycle
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AutomationService {
|
export class AutomationService {
|
||||||
@@ -96,8 +101,7 @@ export class AutomationService {
|
|||||||
settings: {
|
settings: {
|
||||||
tradingAmount: config.tradingAmount,
|
tradingAmount: config.tradingAmount,
|
||||||
maxLeverage: config.maxLeverage,
|
maxLeverage: config.maxLeverage,
|
||||||
stopLossPercent: config.stopLossPercent,
|
// stopLossPercent and takeProfitPercent removed - AI calculates these automatically
|
||||||
takeProfitPercent: config.takeProfitPercent,
|
|
||||||
maxDailyTrades: config.maxDailyTrades,
|
maxDailyTrades: config.maxDailyTrades,
|
||||||
riskPercentage: config.riskPercentage
|
riskPercentage: config.riskPercentage
|
||||||
},
|
},
|
||||||
@@ -164,6 +168,37 @@ export class AutomationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getIntervalFromTimeframe(timeframe: string): number {
|
private getIntervalFromTimeframe(timeframe: string): number {
|
||||||
|
// Check if this is a scalping strategy (multiple short timeframes)
|
||||||
|
if (this.config?.selectedTimeframes) {
|
||||||
|
const timeframes = this.config.selectedTimeframes
|
||||||
|
const isScalping = timeframes.includes('5') || timeframes.includes('3') ||
|
||||||
|
(timeframes.length > 1 && timeframes.every(tf => ['1', '3', '5', '15', '30'].includes(tf)))
|
||||||
|
|
||||||
|
if (isScalping) {
|
||||||
|
console.log('🎯 Scalping strategy detected - using frequent analysis (2-3 minutes)')
|
||||||
|
return 2 * 60 * 1000 // 2 minutes for scalping
|
||||||
|
}
|
||||||
|
|
||||||
|
// Day trading strategy (short-medium timeframes)
|
||||||
|
const isDayTrading = timeframes.includes('60') || timeframes.includes('120') ||
|
||||||
|
timeframes.some(tf => ['30', '60', '120'].includes(tf))
|
||||||
|
|
||||||
|
if (isDayTrading) {
|
||||||
|
console.log('⚡ Day trading strategy detected - using moderate analysis (5-10 minutes)')
|
||||||
|
return 5 * 60 * 1000 // 5 minutes for day trading
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swing trading (longer timeframes)
|
||||||
|
const isSwingTrading = timeframes.includes('240') || timeframes.includes('D') ||
|
||||||
|
timeframes.some(tf => ['240', '480', 'D', '1d'].includes(tf))
|
||||||
|
|
||||||
|
if (isSwingTrading) {
|
||||||
|
console.log('🎯 Swing trading strategy detected - using standard analysis (15-30 minutes)')
|
||||||
|
return 15 * 60 * 1000 // 15 minutes for swing trading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to timeframe-based intervals
|
||||||
const intervals: { [key: string]: number } = {
|
const intervals: { [key: string]: number } = {
|
||||||
'1m': 60 * 1000,
|
'1m': 60 * 1000,
|
||||||
'3m': 3 * 60 * 1000,
|
'3m': 3 * 60 * 1000,
|
||||||
@@ -185,7 +220,36 @@ export class AutomationService {
|
|||||||
try {
|
try {
|
||||||
console.log(`🔍 Running automation cycle for ${this.config.symbol} ${this.config.timeframe}`)
|
console.log(`🔍 Running automation cycle for ${this.config.symbol} ${this.config.timeframe}`)
|
||||||
|
|
||||||
// Step 1: Check daily trade limit
|
// Update next scheduled time in database for timer display
|
||||||
|
const intervalMs = this.getIntervalFromTimeframe(this.config.timeframe)
|
||||||
|
const nextScheduled = new Date(Date.now() + intervalMs)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.automationSession.updateMany({
|
||||||
|
where: {
|
||||||
|
userId: this.config.userId,
|
||||||
|
status: 'ACTIVE'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
nextScheduled: nextScheduled,
|
||||||
|
lastAnalysis: new Date()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log(`⏰ Next analysis scheduled for: ${nextScheduled.toLocaleTimeString()}`)
|
||||||
|
} catch (dbError) {
|
||||||
|
console.error('Failed to update next scheduled time:', dbError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Check for DCA opportunities on existing positions
|
||||||
|
const dcaOpportunity = await this.checkForDCAOpportunity()
|
||||||
|
if (dcaOpportunity.shouldDCA) {
|
||||||
|
console.log('🔄 DCA opportunity found, executing position scaling')
|
||||||
|
await this.executeDCA(dcaOpportunity)
|
||||||
|
await this.runPostCycleCleanup('dca_executed')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Check daily trade limit
|
||||||
const todayTrades = await this.getTodayTradeCount(this.config.userId)
|
const todayTrades = await this.getTodayTradeCount(this.config.userId)
|
||||||
if (todayTrades >= this.config.maxDailyTrades) {
|
if (todayTrades >= this.config.maxDailyTrades) {
|
||||||
console.log(`📊 Daily trade limit reached (${todayTrades}/${this.config.maxDailyTrades})`)
|
console.log(`📊 Daily trade limit reached (${todayTrades}/${this.config.maxDailyTrades})`)
|
||||||
@@ -194,7 +258,7 @@ export class AutomationService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Take screenshot and analyze
|
// Step 3: Take screenshot and analyze
|
||||||
const analysisResult = await this.performAnalysis()
|
const analysisResult = await this.performAnalysis()
|
||||||
if (!analysisResult) {
|
if (!analysisResult) {
|
||||||
console.log('❌ Analysis failed, skipping cycle')
|
console.log('❌ Analysis failed, skipping cycle')
|
||||||
@@ -273,8 +337,8 @@ export class AutomationService {
|
|||||||
progressTracker.createSession(sessionId, progressSteps)
|
progressTracker.createSession(sessionId, progressSteps)
|
||||||
progressTracker.updateStep(sessionId, 'init', 'active', 'Starting multi-timeframe analysis...')
|
progressTracker.updateStep(sessionId, 'init', 'active', 'Starting multi-timeframe analysis...')
|
||||||
|
|
||||||
// Multi-timeframe analysis: 15m, 1h, 2h, 4h
|
// Use selected timeframes from UI, fallback to default if not provided
|
||||||
const timeframes = ['15', '1h', '2h', '4h']
|
const timeframes = this.config!.selectedTimeframes || ['1h']
|
||||||
const symbol = this.config!.symbol
|
const symbol = this.config!.symbol
|
||||||
|
|
||||||
console.log(`🔍 Analyzing ${symbol} across timeframes: ${timeframes.join(', ')} with AI + DIY layouts`)
|
console.log(`🔍 Analyzing ${symbol} across timeframes: ${timeframes.join(', ')} with AI + DIY layouts`)
|
||||||
@@ -406,8 +470,10 @@ export class AutomationService {
|
|||||||
return { screenshots: [], analysis: null }
|
return { screenshots: [], analysis: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the primary timeframe (1h) as base
|
// Get the primary timeframe (first selected or default) as base
|
||||||
const primaryResult = validResults.find(r => r.timeframe === '1h') || validResults[0]
|
const selectedTimeframes = this.config!.selectedTimeframes || ['1h']
|
||||||
|
const primaryTimeframe = selectedTimeframes[0] || '1h'
|
||||||
|
const primaryResult = validResults.find(r => r.timeframe === primaryTimeframe) || validResults[0]
|
||||||
const screenshots = validResults.length > 0 ? [primaryResult.timeframe] : []
|
const screenshots = validResults.length > 0 ? [primaryResult.timeframe] : []
|
||||||
|
|
||||||
// Calculate weighted confidence based on timeframe alignment
|
// Calculate weighted confidence based on timeframe alignment
|
||||||
@@ -480,8 +546,10 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
const allLevels = results.map(r => r.analysis?.keyLevels).filter(Boolean)
|
const allLevels = results.map(r => r.analysis?.keyLevels).filter(Boolean)
|
||||||
if (allLevels.length === 0) return {}
|
if (allLevels.length === 0) return {}
|
||||||
|
|
||||||
// Use the 1h timeframe levels as primary, or first available
|
// Use the primary timeframe levels (first selected) as primary, or first available
|
||||||
const primaryLevels = results.find(r => r.timeframe === '1h')?.analysis?.keyLevels || allLevels[0]
|
const selectedTimeframes = this.config!.selectedTimeframes || ['1h']
|
||||||
|
const primaryTimeframe = selectedTimeframes[0] || '1h'
|
||||||
|
const primaryLevels = results.find(r => r.timeframe === primaryTimeframe)?.analysis?.keyLevels || allLevels[0]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...primaryLevels,
|
...primaryLevels,
|
||||||
@@ -493,8 +561,10 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
const sentiments = results.map(r => r.analysis?.marketSentiment).filter(Boolean)
|
const sentiments = results.map(r => r.analysis?.marketSentiment).filter(Boolean)
|
||||||
if (sentiments.length === 0) return 'NEUTRAL'
|
if (sentiments.length === 0) return 'NEUTRAL'
|
||||||
|
|
||||||
// Use the 1h timeframe sentiment as primary, or first available
|
// Use the primary timeframe sentiment (first selected) as primary, or first available
|
||||||
const primarySentiment = results.find(r => r.timeframe === '1h')?.analysis?.marketSentiment || sentiments[0]
|
const selectedTimeframes = this.config!.selectedTimeframes || ['1h']
|
||||||
|
const primaryTimeframe = selectedTimeframes[0] || '1h'
|
||||||
|
const primarySentiment = results.find(r => r.timeframe === primaryTimeframe)?.analysis?.marketSentiment || sentiments[0]
|
||||||
|
|
||||||
return primarySentiment || 'NEUTRAL'
|
return primarySentiment || 'NEUTRAL'
|
||||||
}
|
}
|
||||||
@@ -606,13 +676,17 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
console.log('📈 BUY signal detected')
|
console.log('📈 BUY signal detected')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate position size based on risk percentage
|
// Calculate AI-driven position size with optimal leverage
|
||||||
const positionSize = await this.calculatePositionSize(analysis)
|
const positionResult = await this.calculatePositionSize(analysis)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
direction: analysis.recommendation,
|
direction: analysis.recommendation,
|
||||||
confidence: analysis.confidence,
|
confidence: analysis.confidence,
|
||||||
positionSize,
|
positionSize: positionResult.tokenAmount,
|
||||||
|
leverageUsed: positionResult.leverageUsed,
|
||||||
|
marginRequired: positionResult.marginRequired,
|
||||||
|
liquidationPrice: positionResult.liquidationPrice,
|
||||||
|
riskAssessment: positionResult.riskAssessment,
|
||||||
stopLoss: this.calculateStopLoss(analysis),
|
stopLoss: this.calculateStopLoss(analysis),
|
||||||
takeProfit: this.calculateTakeProfit(analysis),
|
takeProfit: this.calculateTakeProfit(analysis),
|
||||||
marketSentiment: analysis.marketSentiment,
|
marketSentiment: analysis.marketSentiment,
|
||||||
@@ -659,39 +733,92 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async calculatePositionSize(analysis: any): Promise<number> {
|
private async calculatePositionSize(analysis: any): Promise<{
|
||||||
const baseAmount = this.config!.tradingAmount // This is the USD amount to invest
|
tokenAmount: number
|
||||||
const riskAdjustment = this.config!.riskPercentage / 100
|
leverageUsed: number
|
||||||
const confidenceAdjustment = analysis.confidence / 100
|
marginRequired: number
|
||||||
|
liquidationPrice: number
|
||||||
|
riskAssessment: string
|
||||||
|
}> {
|
||||||
|
console.log('🧠 AI Position Sizing with Dynamic Leverage Calculation...')
|
||||||
|
|
||||||
// ✅ ENHANCED: Handle both BUY and SELL position sizing
|
// ✅ ENHANCED: Handle SELL positions with AI leverage for shorting
|
||||||
if (analysis.recommendation === 'SELL') {
|
if (analysis.recommendation === 'SELL') {
|
||||||
// For SELL orders, calculate how much SOL to sell based on current holdings
|
return await this.calculateSellPositionWithLeverage(analysis)
|
||||||
return await this.calculateSellAmount(analysis)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For BUY orders, calculate USD amount to invest
|
// Get account balance
|
||||||
const usdAmount = baseAmount * riskAdjustment * confidenceAdjustment
|
const balanceResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/drift/balance`)
|
||||||
|
const balanceData = await balanceResponse.json()
|
||||||
|
|
||||||
// Get current price to convert USD to token amount
|
if (!balanceData.success) {
|
||||||
|
throw new Error('Could not fetch account balance for position sizing')
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountValue = balanceData.accountValue || balanceData.totalCollateral
|
||||||
|
const availableBalance = balanceData.availableBalance
|
||||||
|
|
||||||
|
console.log(`💰 Account Status: Value=$${accountValue.toFixed(2)}, Available=$${availableBalance.toFixed(2)}`)
|
||||||
|
|
||||||
|
// Get current price for entry
|
||||||
let currentPrice = analysis.entry?.price || analysis.currentPrice
|
let currentPrice = analysis.entry?.price || analysis.currentPrice
|
||||||
|
|
||||||
if (!currentPrice) {
|
if (!currentPrice) {
|
||||||
try {
|
try {
|
||||||
const { default: PriceFetcher } = await import('./price-fetcher')
|
const { default: PriceFetcher } = await import('./price-fetcher')
|
||||||
currentPrice = await PriceFetcher.getCurrentPrice(this.config?.symbol || 'SOLUSD')
|
currentPrice = await PriceFetcher.getCurrentPrice(this.config?.symbol || 'SOLUSD')
|
||||||
console.log(`📊 Using current ${this.config?.symbol || 'SOLUSD'} price for position size: $${currentPrice}`)
|
console.log(`📊 Using current ${this.config?.symbol || 'SOLUSD'} price: $${currentPrice}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching price for position size, using fallback:', error)
|
console.error('Error fetching price for position sizing, using fallback:', error)
|
||||||
currentPrice = this.config?.symbol === 'SOLUSD' ? 189 : 100
|
currentPrice = this.config?.symbol === 'SOLUSD' ? 189 : 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate token amount: USD investment / token price
|
// Calculate stop loss price from analysis
|
||||||
const tokenAmount = usdAmount / currentPrice
|
const stopLossPercent = this.calculateAIStopLoss(analysis) / 100
|
||||||
console.log(`💰 BUY Position calculation: $${usdAmount} ÷ $${currentPrice} = ${tokenAmount.toFixed(4)} tokens`)
|
const direction = analysis.recommendation === 'BUY' ? 'long' : 'short'
|
||||||
|
|
||||||
return tokenAmount
|
let stopLossPrice: number
|
||||||
|
if (direction === 'long') {
|
||||||
|
stopLossPrice = currentPrice * (1 - stopLossPercent)
|
||||||
|
} else {
|
||||||
|
stopLossPrice = currentPrice * (1 + stopLossPercent)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🎯 Position Parameters: Entry=$${currentPrice}, StopLoss=$${stopLossPrice.toFixed(4)}, Direction=${direction}`)
|
||||||
|
|
||||||
|
// Use AI Leverage Calculator for optimal leverage
|
||||||
|
const leverageResult = AILeverageCalculator.calculateOptimalLeverage({
|
||||||
|
accountValue,
|
||||||
|
availableBalance,
|
||||||
|
entryPrice: currentPrice,
|
||||||
|
stopLossPrice,
|
||||||
|
side: direction,
|
||||||
|
maxLeverageAllowed: this.config!.maxLeverage || 20, // Platform max leverage
|
||||||
|
safetyBuffer: 0.10 // 10% safety buffer between liquidation and stop loss
|
||||||
|
})
|
||||||
|
|
||||||
|
// Calculate final position size
|
||||||
|
const baseAmount = accountValue < 1000 ? availableBalance : availableBalance * 0.5
|
||||||
|
const leveragedAmount = baseAmount * leverageResult.recommendedLeverage
|
||||||
|
const tokenAmount = leveragedAmount / currentPrice
|
||||||
|
|
||||||
|
console.log(`<60> AI Position Result:`, {
|
||||||
|
baseAmount: `$${baseAmount.toFixed(2)}`,
|
||||||
|
leverage: `${leverageResult.recommendedLeverage.toFixed(1)}x`,
|
||||||
|
leveragedAmount: `$${leveragedAmount.toFixed(2)}`,
|
||||||
|
tokenAmount: tokenAmount.toFixed(4),
|
||||||
|
riskLevel: leverageResult.riskAssessment,
|
||||||
|
reasoning: leverageResult.reasoning
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
tokenAmount,
|
||||||
|
leverageUsed: leverageResult.recommendedLeverage,
|
||||||
|
marginRequired: leverageResult.marginRequired,
|
||||||
|
liquidationPrice: leverageResult.liquidationPrice,
|
||||||
|
riskAssessment: leverageResult.riskAssessment
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ NEW: Calculate SOL amount to sell for SELL orders
|
// ✅ NEW: Calculate SOL amount to sell for SELL orders
|
||||||
@@ -728,48 +855,173 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ NEW: Calculate leveraged short position for SELL orders
|
||||||
|
private async calculateSellPositionWithLeverage(analysis: any): Promise<{
|
||||||
|
tokenAmount: number
|
||||||
|
leverageUsed: number
|
||||||
|
marginRequired: number
|
||||||
|
liquidationPrice: number
|
||||||
|
riskAssessment: string
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
console.log('📉 Calculating SELL position with AI leverage...')
|
||||||
|
|
||||||
|
// Get account balance for leverage calculation
|
||||||
|
const balanceResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/drift/balance`)
|
||||||
|
const balanceData = await balanceResponse.json()
|
||||||
|
|
||||||
|
const accountValue = balanceData.accountValue || balanceData.totalCollateral
|
||||||
|
const availableBalance = balanceData.availableBalance
|
||||||
|
|
||||||
|
// Get current price
|
||||||
|
let currentPrice = analysis.entry?.price || analysis.currentPrice
|
||||||
|
if (!currentPrice) {
|
||||||
|
const { default: PriceFetcher } = await import('./price-fetcher')
|
||||||
|
currentPrice = await PriceFetcher.getCurrentPrice(this.config?.symbol || 'SOLUSD')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate stop loss for short position (above entry price)
|
||||||
|
const stopLossPercent = this.calculateAIStopLoss(analysis) / 100
|
||||||
|
const stopLossPrice = currentPrice * (1 + stopLossPercent)
|
||||||
|
|
||||||
|
console.log(`🎯 SHORT Position Parameters: Entry=$${currentPrice}, StopLoss=$${stopLossPrice.toFixed(4)}`)
|
||||||
|
|
||||||
|
// Use AI leverage for short position
|
||||||
|
const leverageResult = AILeverageCalculator.calculateOptimalLeverage({
|
||||||
|
accountValue,
|
||||||
|
availableBalance,
|
||||||
|
entryPrice: currentPrice,
|
||||||
|
stopLossPrice,
|
||||||
|
side: 'short',
|
||||||
|
maxLeverageAllowed: this.config!.maxLeverage || 20,
|
||||||
|
safetyBuffer: 0.10
|
||||||
|
})
|
||||||
|
|
||||||
|
// Calculate leveraged short amount
|
||||||
|
const baseAmount = accountValue < 1000 ? availableBalance : availableBalance * 0.5
|
||||||
|
const leveragedAmount = baseAmount * leverageResult.recommendedLeverage
|
||||||
|
const tokenAmount = leveragedAmount / currentPrice
|
||||||
|
|
||||||
|
console.log(`📉 SELL Position with AI Leverage:`, {
|
||||||
|
baseAmount: `$${baseAmount.toFixed(2)}`,
|
||||||
|
leverage: `${leverageResult.recommendedLeverage.toFixed(1)}x`,
|
||||||
|
leveragedAmount: `$${leveragedAmount.toFixed(2)}`,
|
||||||
|
tokenAmount: tokenAmount.toFixed(4),
|
||||||
|
riskLevel: leverageResult.riskAssessment,
|
||||||
|
reasoning: leverageResult.reasoning
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
tokenAmount,
|
||||||
|
leverageUsed: leverageResult.recommendedLeverage,
|
||||||
|
marginRequired: leverageResult.marginRequired,
|
||||||
|
liquidationPrice: leverageResult.liquidationPrice,
|
||||||
|
riskAssessment: leverageResult.riskAssessment
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error calculating SELL position with leverage:', error)
|
||||||
|
return {
|
||||||
|
tokenAmount: 0.01, // Fallback small amount
|
||||||
|
leverageUsed: 1,
|
||||||
|
marginRequired: 0,
|
||||||
|
liquidationPrice: 0,
|
||||||
|
riskAssessment: 'HIGH'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private calculateStopLoss(analysis: any): number {
|
private calculateStopLoss(analysis: any): number {
|
||||||
// Use AI analysis stopLoss if available, otherwise calculate from entry price
|
// ✅ AI-FIRST: Use AI analysis stopLoss if available
|
||||||
if (analysis.stopLoss?.price) {
|
if (analysis.stopLoss?.price) {
|
||||||
return analysis.stopLoss.price
|
const currentPrice = analysis.entry?.price || 189
|
||||||
}
|
const stopLossPrice = analysis.stopLoss.price
|
||||||
|
|
||||||
const currentPrice = analysis.entry?.price || 189 // Current SOL price
|
// Convert absolute price to percentage
|
||||||
const stopLossPercent = this.config!.stopLossPercent / 100
|
|
||||||
|
|
||||||
// ✅ ENHANCED: Proper stop loss for both BUY and SELL
|
|
||||||
if (analysis.recommendation === 'BUY') {
|
if (analysis.recommendation === 'BUY') {
|
||||||
// BUY: Stop loss below entry (price goes down)
|
return ((currentPrice - stopLossPrice) / currentPrice) * 100
|
||||||
return currentPrice * (1 - stopLossPercent)
|
|
||||||
} else if (analysis.recommendation === 'SELL') {
|
} else if (analysis.recommendation === 'SELL') {
|
||||||
// SELL: Stop loss above entry (price goes up)
|
return ((stopLossPrice - currentPrice) / currentPrice) * 100
|
||||||
return currentPrice * (1 + stopLossPercent)
|
|
||||||
} else {
|
|
||||||
return currentPrice * (1 - stopLossPercent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If AI provides explicit stop loss percentage, use it
|
||||||
|
if (analysis.stopLossPercent) {
|
||||||
|
return analysis.stopLossPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Dynamic stop loss based on market volatility (AI-calculated)
|
||||||
|
// AI determines volatility-based stop loss (0.5% to 2% range)
|
||||||
|
return this.calculateAIStopLoss(analysis)
|
||||||
|
}
|
||||||
|
|
||||||
private calculateTakeProfit(analysis: any): number {
|
private calculateTakeProfit(analysis: any): number {
|
||||||
// Use AI analysis takeProfit if available, otherwise calculate from entry price
|
// ✅ AI-FIRST: Use AI analysis takeProfit if available
|
||||||
if (analysis.takeProfits?.tp1?.price) {
|
if (analysis.takeProfits?.tp1?.price) {
|
||||||
return analysis.takeProfits.tp1.price
|
const currentPrice = analysis.entry?.price || 150
|
||||||
}
|
const takeProfitPrice = analysis.takeProfits.tp1.price
|
||||||
|
|
||||||
const currentPrice = analysis.entry?.price || 150 // Default SOL price
|
// Convert absolute price to percentage
|
||||||
const takeProfitPercent = this.config!.takeProfitPercent / 100
|
|
||||||
|
|
||||||
// ✅ ENHANCED: Proper take profit for both BUY and SELL
|
|
||||||
if (analysis.recommendation === 'BUY') {
|
if (analysis.recommendation === 'BUY') {
|
||||||
// BUY: Take profit above entry (price goes up)
|
return ((takeProfitPrice - currentPrice) / currentPrice) * 100
|
||||||
return currentPrice * (1 + takeProfitPercent)
|
|
||||||
} else if (analysis.recommendation === 'SELL') {
|
} else if (analysis.recommendation === 'SELL') {
|
||||||
// SELL: Take profit below entry (price goes down)
|
return ((currentPrice - takeProfitPrice) / currentPrice) * 100
|
||||||
return currentPrice * (1 - takeProfitPercent)
|
|
||||||
} else {
|
|
||||||
return currentPrice * (1 + takeProfitPercent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If AI provides explicit take profit percentage, use it
|
||||||
|
if (analysis.takeProfitPercent) {
|
||||||
|
return analysis.takeProfitPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Dynamic take profit based on AI risk/reward optimization
|
||||||
|
return this.calculateAITakeProfit(analysis)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI-calculated dynamic stop loss based on volatility and market conditions
|
||||||
|
private calculateAIStopLoss(analysis: any): number {
|
||||||
|
// Extract confidence and market sentiment for adaptive stop loss
|
||||||
|
const confidence = analysis.confidence || 70
|
||||||
|
const volatility = analysis.marketConditions?.volatility || 'MEDIUM'
|
||||||
|
|
||||||
|
// Base stop loss percentages (proven to work from our testing)
|
||||||
|
let baseStopLoss = 0.8 // 0.8% base (proven effective)
|
||||||
|
|
||||||
|
// Adjust based on volatility
|
||||||
|
if (volatility === 'HIGH') {
|
||||||
|
baseStopLoss = 1.2 // Wider stop loss for high volatility
|
||||||
|
} else if (volatility === 'LOW') {
|
||||||
|
baseStopLoss = 0.5 // Tighter stop loss for low volatility
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust based on confidence (higher confidence = tighter stop loss)
|
||||||
|
if (confidence > 85) {
|
||||||
|
baseStopLoss *= 0.8 // 20% tighter for high confidence
|
||||||
|
} else if (confidence < 70) {
|
||||||
|
baseStopLoss *= 1.3 // 30% wider for low confidence
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(0.3, Math.min(2.0, baseStopLoss)) // Cap between 0.3% and 2%
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI-calculated dynamic take profit based on market conditions and risk/reward
|
||||||
|
private calculateAITakeProfit(analysis: any): number {
|
||||||
|
const stopLossPercent = this.calculateAIStopLoss(analysis)
|
||||||
|
const confidence = analysis.confidence || 70
|
||||||
|
|
||||||
|
// Target minimum 1.5:1 risk/reward ratio, scaled by confidence
|
||||||
|
let baseRiskReward = 1.5
|
||||||
|
|
||||||
|
if (confidence > 85) {
|
||||||
|
baseRiskReward = 2.0 // Higher reward target for high confidence
|
||||||
|
} else if (confidence < 70) {
|
||||||
|
baseRiskReward = 1.2 // Lower reward target for low confidence
|
||||||
|
}
|
||||||
|
|
||||||
|
const takeProfitPercent = stopLossPercent * baseRiskReward
|
||||||
|
return Math.max(0.5, Math.min(5.0, takeProfitPercent)) // Cap between 0.5% and 5%
|
||||||
|
}
|
||||||
|
|
||||||
private async executeTrade(decision: any): Promise<void> {
|
private async executeTrade(decision: any): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log(`🎯 Executing ${this.config!.mode} trade: ${decision.direction} ${decision.positionSize} ${this.config!.symbol}`)
|
console.log(`🎯 Executing ${this.config!.mode} trade: ${decision.direction} ${decision.positionSize} ${this.config!.symbol}`)
|
||||||
@@ -780,7 +1032,7 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
// Execute simulation trade
|
// Execute simulation trade
|
||||||
tradeResult = await this.executeSimulationTrade(decision)
|
tradeResult = await this.executeSimulationTrade(decision)
|
||||||
} else {
|
} else {
|
||||||
// Execute live trade via Jupiter
|
// Execute live trade via Drift Protocol
|
||||||
console.log(`💰 LIVE TRADE: $${this.config!.tradingAmount} trading amount configured`)
|
console.log(`💰 LIVE TRADE: $${this.config!.tradingAmount} trading amount configured`)
|
||||||
tradeResult = await this.executeLiveTrade(decision)
|
tradeResult = await this.executeLiveTrade(decision)
|
||||||
|
|
||||||
@@ -789,7 +1041,7 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
console.log('⚠️ Live trade failed, falling back to simulation for record keeping')
|
console.log('⚠️ Live trade failed, falling back to simulation for record keeping')
|
||||||
tradeResult = await this.executeSimulationTrade(decision)
|
tradeResult = await this.executeSimulationTrade(decision)
|
||||||
tradeResult.status = 'FAILED'
|
tradeResult.status = 'FAILED'
|
||||||
tradeResult.error = 'Jupiter DEX execution failed'
|
tradeResult.error = 'Drift Protocol execution failed'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,7 +1057,7 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
if (tradeResult.status !== 'FAILED') {
|
if (tradeResult.status !== 'FAILED') {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await aggressiveCleanup.runPostAnalysisCleanup()
|
await aggressiveCleanup.forceCleanupAfterTrade()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in post-trade cleanup:', error)
|
console.error('Error in post-trade cleanup:', error)
|
||||||
}
|
}
|
||||||
@@ -852,52 +1104,66 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async executeLiveTrade(decision: any): Promise<any> {
|
private async executeLiveTrade(decision: any): Promise<any> {
|
||||||
// Execute real trade via Jupiter DEX
|
// Execute real trade via Drift Protocol with AI-calculated leverage
|
||||||
const inputToken = decision.direction === 'BUY' ? 'USDC' : 'SOL'
|
console.log(`🌊 Executing Drift trade: ${decision.direction} ${this.config!.symbol}`)
|
||||||
const outputToken = decision.direction === 'BUY' ? 'SOL' : 'USDC'
|
console.log(`🧠 AI Leverage: ${decision.leverageUsed.toFixed(1)}x (Risk: ${decision.riskAssessment})`)
|
||||||
|
console.log(`💀 Liquidation Price: $${decision.liquidationPrice.toFixed(4)}`)
|
||||||
|
|
||||||
const tokens = {
|
// Calculate AI-generated stop loss and take profit from analysis
|
||||||
SOL: 'So11111111111111111111111111111111111111112',
|
const stopLossPercent = decision.stopLoss || this.calculateAIStopLoss(decision)
|
||||||
USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
const takeProfitPercent = decision.takeProfit || this.calculateAITakeProfit(decision)
|
||||||
|
|
||||||
|
console.log(`🎯 AI Risk Management: SL=${stopLossPercent}%, TP=${takeProfitPercent}%`)
|
||||||
|
|
||||||
|
// Call the unified trading API endpoint that routes to Drift
|
||||||
|
const tradeResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/automation/trade`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
dexProvider: this.config!.dexProvider || 'DRIFT',
|
||||||
|
action: 'place_order',
|
||||||
|
symbol: this.config!.symbol,
|
||||||
|
amount: this.config!.tradingAmount,
|
||||||
|
side: decision.direction.toLowerCase(),
|
||||||
|
leverage: decision.leverageUsed || this.config!.maxLeverage || 2, // Use AI-calculated leverage
|
||||||
|
stopLoss: true,
|
||||||
|
takeProfit: true,
|
||||||
|
stopLossPercent: stopLossPercent,
|
||||||
|
takeProfitPercent: takeProfitPercent,
|
||||||
|
mode: this.config!.mode || 'SIMULATION',
|
||||||
|
// Include AI leverage details for logging
|
||||||
|
aiLeverageDetails: {
|
||||||
|
calculatedLeverage: decision.leverageUsed,
|
||||||
|
liquidationPrice: decision.liquidationPrice,
|
||||||
|
riskAssessment: decision.riskAssessment,
|
||||||
|
marginRequired: decision.marginRequired
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Calculate proper amount for Jupiter API
|
const tradeResult = await tradeResponse.json()
|
||||||
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}`)
|
// Convert Drift result to standard trade result format
|
||||||
|
if (tradeResult.success) {
|
||||||
const swapResult = await jupiterDEXService.executeSwap(
|
|
||||||
tokens[inputToken as keyof typeof tokens],
|
|
||||||
tokens[outputToken as keyof typeof tokens],
|
|
||||||
swapAmount,
|
|
||||||
50 // 0.5% slippage
|
|
||||||
)
|
|
||||||
|
|
||||||
// Convert Jupiter result to standard trade result format
|
|
||||||
if (swapResult.success) {
|
|
||||||
return {
|
return {
|
||||||
transactionId: swapResult.txId,
|
transactionId: tradeResult.result?.transactionId || tradeResult.result?.txId,
|
||||||
executionPrice: swapResult.executionPrice,
|
executionPrice: tradeResult.result?.executionPrice,
|
||||||
amount: swapResult.outputAmount, // Amount of tokens received
|
amount: tradeResult.result?.amount,
|
||||||
direction: decision.direction,
|
direction: decision.direction,
|
||||||
status: 'COMPLETED',
|
status: 'COMPLETED',
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
fees: swapResult.fees || 0,
|
leverage: decision.leverageUsed || tradeResult.leverageUsed || this.config!.maxLeverage,
|
||||||
slippage: swapResult.slippage || 0,
|
liquidationPrice: decision.liquidationPrice,
|
||||||
inputAmount: swapResult.inputAmount, // Amount of tokens spent
|
riskAssessment: decision.riskAssessment,
|
||||||
tradingAmount: this.config!.tradingAmount // Original USD amount
|
stopLoss: stopLossPercent,
|
||||||
|
takeProfit: takeProfitPercent,
|
||||||
|
tradingAmount: this.config!.tradingAmount,
|
||||||
|
dexProvider: 'DRIFT'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(swapResult.error || 'Jupiter swap failed')
|
throw new Error(tradeResult.error || 'Drift trade execution failed')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,7 +1178,7 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// For live trades, use the actual amounts from Jupiter
|
// For live trades, use the actual amounts from Drift
|
||||||
const tradeAmount = result.tradingAmount ? this.config!.tradingAmount : decision.positionSize
|
const tradeAmount = result.tradingAmount ? this.config!.tradingAmount : decision.positionSize
|
||||||
const actualAmount = result.amount || decision.positionSize
|
const actualAmount = result.amount || decision.positionSize
|
||||||
|
|
||||||
@@ -935,11 +1201,22 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
confidence: decision.confidence,
|
confidence: decision.confidence,
|
||||||
marketSentiment: decision.marketSentiment,
|
marketSentiment: decision.marketSentiment,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
// Add Jupiter-specific fields for live trades
|
// Add AI leverage information
|
||||||
|
leverage: result.leverage || decision.leverageUsed,
|
||||||
|
// Add Drift-specific fields for live trades
|
||||||
...(this.config!.mode === 'LIVE' && result.tradingAmount && {
|
...(this.config!.mode === 'LIVE' && result.tradingAmount && {
|
||||||
realTradingAmount: this.config!.tradingAmount,
|
realTradingAmount: this.config!.tradingAmount,
|
||||||
inputAmount: result.inputAmount,
|
driftTxId: result.transactionId
|
||||||
slippage: result.slippage
|
}),
|
||||||
|
// Add AI leverage details in metadata
|
||||||
|
metadata: JSON.stringify({
|
||||||
|
aiLeverage: {
|
||||||
|
calculatedLeverage: decision.leverageUsed,
|
||||||
|
liquidationPrice: decision.liquidationPrice,
|
||||||
|
riskAssessment: decision.riskAssessment,
|
||||||
|
marginRequired: decision.marginRequired,
|
||||||
|
balanceStrategy: result.accountValue < 1000 ? 'AGGRESSIVE_100%' : 'CONSERVATIVE_50%'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1076,6 +1353,16 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
await this.autoRestartFromSession(session)
|
await this.autoRestartFromSession(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate next analysis timing
|
||||||
|
const analysisInterval = Math.floor(this.getIntervalFromTimeframe(session.timeframe) / 1000) // Convert to seconds
|
||||||
|
let nextAnalysisIn = 0
|
||||||
|
|
||||||
|
if (this.isRunning && session.nextScheduled) {
|
||||||
|
const nextScheduledTime = new Date(session.nextScheduled).getTime()
|
||||||
|
const currentTime = Date.now()
|
||||||
|
nextAnalysisIn = Math.max(0, Math.floor((nextScheduledTime - currentTime) / 1000))
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isActive: this.isRunning && this.config !== null,
|
isActive: this.isRunning && this.config !== null,
|
||||||
mode: session.mode as 'SIMULATION' | 'LIVE',
|
mode: session.mode as 'SIMULATION' | 'LIVE',
|
||||||
@@ -1089,7 +1376,10 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
lastError: session.lastError || undefined,
|
lastError: session.lastError || undefined,
|
||||||
lastAnalysis: session.lastAnalysis || undefined,
|
lastAnalysis: session.lastAnalysis || undefined,
|
||||||
lastTrade: session.lastTrade || undefined,
|
lastTrade: session.lastTrade || undefined,
|
||||||
nextScheduled: session.nextScheduled || undefined
|
nextScheduled: session.nextScheduled || undefined,
|
||||||
|
nextAnalysisIn: nextAnalysisIn,
|
||||||
|
analysisInterval: analysisInterval,
|
||||||
|
currentCycle: session.totalTrades || 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get automation status:', error)
|
console.error('Failed to get automation status:', error)
|
||||||
@@ -1107,8 +1397,7 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
timeframe: session.timeframe,
|
timeframe: session.timeframe,
|
||||||
tradingAmount: settings.tradingAmount || 100,
|
tradingAmount: settings.tradingAmount || 100,
|
||||||
maxLeverage: settings.maxLeverage || 3,
|
maxLeverage: settings.maxLeverage || 3,
|
||||||
stopLossPercent: settings.stopLossPercent || 2,
|
// stopLossPercent and takeProfitPercent removed - AI calculates these automatically
|
||||||
takeProfitPercent: settings.takeProfitPercent || 6,
|
|
||||||
maxDailyTrades: settings.maxDailyTrades || 5,
|
maxDailyTrades: settings.maxDailyTrades || 5,
|
||||||
riskPercentage: settings.riskPercentage || 2
|
riskPercentage: settings.riskPercentage || 2
|
||||||
}
|
}
|
||||||
@@ -1129,11 +1418,14 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
recommendations: string[]
|
recommendations: string[]
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
// For now, return mock data
|
// For now, return mock data with dynamic timeframe
|
||||||
|
const selectedTimeframes = this.config?.selectedTimeframes || ['1h']
|
||||||
|
const primaryTimeframe = selectedTimeframes[0] || '1h'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalAnalyses: 150,
|
totalAnalyses: 150,
|
||||||
avgAccuracy: 0.72,
|
avgAccuracy: 0.72,
|
||||||
bestTimeframe: '1h',
|
bestTimeframe: primaryTimeframe,
|
||||||
worstTimeframe: '15m',
|
worstTimeframe: '15m',
|
||||||
commonFailures: [
|
commonFailures: [
|
||||||
'Low confidence predictions',
|
'Low confidence predictions',
|
||||||
@@ -1141,7 +1433,7 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
'Timeframe misalignment'
|
'Timeframe misalignment'
|
||||||
],
|
],
|
||||||
recommendations: [
|
recommendations: [
|
||||||
'Focus on 1h timeframe for better accuracy',
|
`Focus on ${primaryTimeframe} timeframe for better accuracy`,
|
||||||
'Wait for higher confidence signals (>75%)',
|
'Wait for higher confidence signals (>75%)',
|
||||||
'Use multiple timeframe confirmation'
|
'Use multiple timeframe confirmation'
|
||||||
]
|
]
|
||||||
@@ -1263,6 +1555,196 @@ ${validResults.map(r => `• ${r.timeframe}: ${r.analysis?.recommendation} (${r.
|
|||||||
this.stats.lastError = error instanceof Error ? error.message : 'Unknown error'
|
this.stats.lastError = error instanceof Error ? error.message : 'Unknown error'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for DCA opportunities on existing open positions
|
||||||
|
*/
|
||||||
|
private async checkForDCAOpportunity(): Promise<any> {
|
||||||
|
try {
|
||||||
|
if (!this.config) return { shouldDCA: false }
|
||||||
|
|
||||||
|
// Get current open positions
|
||||||
|
const openPositions = await prisma.trade.findMany({
|
||||||
|
where: {
|
||||||
|
userId: this.config.userId,
|
||||||
|
status: 'open',
|
||||||
|
symbol: this.config.symbol
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (openPositions.length === 0) {
|
||||||
|
return { shouldDCA: false, reasoning: 'No open positions to DCA' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPosition = openPositions[0]
|
||||||
|
|
||||||
|
// Get current market price
|
||||||
|
let currentPrice: number
|
||||||
|
try {
|
||||||
|
const { default: PriceFetcher } = await import('./price-fetcher')
|
||||||
|
currentPrice = await PriceFetcher.getCurrentPrice(this.config.symbol)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching current price for DCA analysis:', error)
|
||||||
|
return { shouldDCA: false, reasoning: 'Cannot fetch current price' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get account status for DCA calculation (simplified version)
|
||||||
|
const accountStatus = {
|
||||||
|
accountValue: 1000, // Could integrate with actual account status
|
||||||
|
availableBalance: 500,
|
||||||
|
leverage: currentPosition.leverage || 1,
|
||||||
|
liquidationPrice: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze DCA opportunity using AI DCA Manager
|
||||||
|
const dcaParams = {
|
||||||
|
currentPosition: {
|
||||||
|
side: currentPosition.side as 'long' | 'short',
|
||||||
|
size: currentPosition.amount || 0,
|
||||||
|
entryPrice: currentPosition.entryPrice || currentPosition.price,
|
||||||
|
currentPrice,
|
||||||
|
unrealizedPnl: currentPosition.profit || 0,
|
||||||
|
stopLoss: currentPosition.stopLoss || 0,
|
||||||
|
takeProfit: currentPosition.takeProfit || 0
|
||||||
|
},
|
||||||
|
accountStatus,
|
||||||
|
marketData: {
|
||||||
|
price: currentPrice,
|
||||||
|
priceChange24h: 0, // Could fetch from price API if needed
|
||||||
|
volume: 0,
|
||||||
|
support: (currentPosition.entryPrice || currentPosition.price) * 0.95, // Estimate
|
||||||
|
resistance: (currentPosition.entryPrice || currentPosition.price) * 1.05 // Estimate
|
||||||
|
},
|
||||||
|
maxLeverageAllowed: this.config.maxLeverage || 20
|
||||||
|
}
|
||||||
|
|
||||||
|
const dcaResult = AIDCAManager.analyzeDCAOpportunity(dcaParams)
|
||||||
|
|
||||||
|
console.log('🔍 DCA Analysis Result:', {
|
||||||
|
shouldDCA: dcaResult.shouldDCA,
|
||||||
|
confidence: dcaResult.confidence,
|
||||||
|
reasoning: dcaResult.reasoning,
|
||||||
|
dcaAmount: dcaResult.dcaAmount?.toFixed(4),
|
||||||
|
riskLevel: dcaResult.riskAssessment
|
||||||
|
})
|
||||||
|
|
||||||
|
return dcaResult
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking DCA opportunity:', error)
|
||||||
|
return { shouldDCA: false, reasoning: 'DCA analysis failed' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute DCA by scaling into existing position
|
||||||
|
*/
|
||||||
|
private async executeDCA(dcaResult: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!this.config || !dcaResult.shouldDCA) return
|
||||||
|
|
||||||
|
console.log('🔄 Executing DCA scaling:', {
|
||||||
|
amount: dcaResult.dcaAmount?.toFixed(4),
|
||||||
|
newAverage: dcaResult.newAveragePrice?.toFixed(4),
|
||||||
|
newLeverage: dcaResult.newLeverage?.toFixed(1) + 'x',
|
||||||
|
confidence: dcaResult.confidence + '%'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get current open position
|
||||||
|
const openPosition = await prisma.trade.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: this.config.userId,
|
||||||
|
status: 'open',
|
||||||
|
symbol: this.config.symbol
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!openPosition) {
|
||||||
|
console.error('❌ No open position found for DCA')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute DCA trade via Drift Protocol (simplified for now)
|
||||||
|
if (this.config.mode === 'LIVE') {
|
||||||
|
console.log('📈 Live DCA would execute via Drift Protocol (not implemented yet)')
|
||||||
|
// TODO: Implement live DCA execution
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update position with new averages (both LIVE and SIMULATION)
|
||||||
|
await this.updatePositionAfterDCA(openPosition.id, dcaResult)
|
||||||
|
|
||||||
|
// Create DCA record for tracking
|
||||||
|
await this.createDCARecord(openPosition.id, dcaResult)
|
||||||
|
|
||||||
|
console.log('✅ DCA executed successfully')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error executing DCA:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update position after DCA execution
|
||||||
|
*/
|
||||||
|
private async updatePositionAfterDCA(positionId: string, dcaResult: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Calculate new position metrics
|
||||||
|
const newSize = dcaResult.dcaAmount * (dcaResult.newLeverage || 1)
|
||||||
|
|
||||||
|
await prisma.trade.update({
|
||||||
|
where: { id: positionId },
|
||||||
|
data: {
|
||||||
|
amount: { increment: newSize },
|
||||||
|
entryPrice: dcaResult.newAveragePrice,
|
||||||
|
stopLoss: dcaResult.newStopLoss,
|
||||||
|
takeProfit: dcaResult.newTakeProfit,
|
||||||
|
leverage: dcaResult.newLeverage,
|
||||||
|
aiAnalysis: `DCA: ${dcaResult.reasoning}`,
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('📊 Position updated after DCA:', {
|
||||||
|
newAverage: dcaResult.newAveragePrice?.toFixed(4),
|
||||||
|
newSL: dcaResult.newStopLoss?.toFixed(4),
|
||||||
|
newTP: dcaResult.newTakeProfit?.toFixed(4),
|
||||||
|
newLeverage: dcaResult.newLeverage?.toFixed(1) + 'x'
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating position after DCA:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create DCA record for tracking and analysis
|
||||||
|
*/
|
||||||
|
private async createDCARecord(positionId: string, dcaResult: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
await prisma.dCARecord.create({
|
||||||
|
data: {
|
||||||
|
tradeId: positionId,
|
||||||
|
dcaAmount: dcaResult.dcaAmount,
|
||||||
|
dcaPrice: dcaResult.newAveragePrice, // Current market price for DCA entry
|
||||||
|
newAveragePrice: dcaResult.newAveragePrice,
|
||||||
|
newStopLoss: dcaResult.newStopLoss,
|
||||||
|
newTakeProfit: dcaResult.newTakeProfit,
|
||||||
|
newLeverage: dcaResult.newLeverage,
|
||||||
|
confidence: dcaResult.confidence,
|
||||||
|
reasoning: dcaResult.reasoning,
|
||||||
|
riskAssessment: dcaResult.riskAssessment,
|
||||||
|
createdAt: new Date()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('📝 DCA record created for tracking')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating DCA record:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const automationService = new AutomationService()
|
export const automationService = new AutomationService()
|
||||||
|
|||||||
1748
lib/automation-service-simple.ts.backup2
Normal file
1748
lib/automation-service-simple.ts.backup2
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Reference in New Issue
Block a user