- Fixed ES modules error by converting automation-with-learning-v2.js to pure ES6 - Fixed singleton pattern in automation-singleton.js for proper async handling - Fixed EnhancedAILearningPanel to handle recommendation objects correctly - Updated API routes to use correct import paths (../../../../lib/) - Created proper db.js utility with ES6 exports - Fixed simplified-stop-loss-learner imports and exports Automation v2 page now loads without errors AI learning system fully integrated and operational Learning status API working with detailed reports Recommendation rendering fixed for object structure
531 lines
17 KiB
JavaScript
531 lines
17 KiB
JavaScript
/**
|
|
* Simplified Stop Loss Learning System
|
|
*
|
|
* Simplified approach focusing on essential learning patterns
|
|
* without complex statistical analysis.
|
|
*/
|
|
|
|
import { PrismaClient } from '@prisma/client';
|
|
import { getDB } from './db.js';
|
|
|
|
class SimplifiedStopLossLearner {
|
|
constructor() {
|
|
this.learningThresholds = {
|
|
emergency: 1.0, // Emergency exit at 1% from SL
|
|
risk: 2.0, // High risk at 2% from SL
|
|
mediumRisk: 5.0 // Medium risk at 5% from SL
|
|
};
|
|
}
|
|
|
|
async log(message) {
|
|
console.log(`[${new Date().toISOString()}] 🧠 SL Learner: ${message}`);
|
|
}
|
|
|
|
/**
|
|
* Record a stop loss related decision for learning
|
|
*/
|
|
async recordDecision(decisionData) {
|
|
try {
|
|
const learningRecord = {
|
|
type: 'STOP_LOSS_DECISION',
|
|
tradeId: decisionData.tradeId,
|
|
symbol: decisionData.symbol,
|
|
decision: decisionData.decision,
|
|
distanceFromSL: decisionData.distanceFromSL,
|
|
reasoning: decisionData.reasoning,
|
|
marketConditions: decisionData.marketConditions,
|
|
expectedOutcome: decisionData.expectedOutcome,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
const prisma = await getDB();
|
|
const record = await prisma.ai_learning_data.create({
|
|
data: {
|
|
id: `sl_decision_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
userId: 'default-user',
|
|
symbol: decisionData.symbol,
|
|
timeframe: 'DECISION',
|
|
analysisData: JSON.stringify(learningRecord),
|
|
marketConditions: JSON.stringify(decisionData.marketConditions || {}),
|
|
confidenceScore: 50 // Neutral starting confidence
|
|
}
|
|
});
|
|
|
|
await this.log(`📝 Decision recorded: ${decisionData.decision} for ${decisionData.symbol} at ${decisionData.distanceFromSL}%`);
|
|
|
|
return record.id;
|
|
} catch (error) {
|
|
await this.log(`❌ Error recording decision: ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the outcome of a previously recorded decision
|
|
*/
|
|
async assessDecisionOutcome(outcomeData) {
|
|
try {
|
|
const prisma = await getDB();
|
|
|
|
// Find the original decision record
|
|
const originalRecord = await prisma.ai_learning_data.findUnique({
|
|
where: { id: outcomeData.decisionId }
|
|
});
|
|
|
|
if (!originalRecord) {
|
|
await this.log(`⚠️ Original decision ${outcomeData.decisionId} not found`);
|
|
return false;
|
|
}
|
|
|
|
// Parse the original decision data
|
|
const originalDecision = JSON.parse(originalRecord.analysisData);
|
|
|
|
// Create outcome record with learning data
|
|
const outcomeRecord = {
|
|
type: 'STOP_LOSS_OUTCOME',
|
|
originalDecisionId: outcomeData.decisionId,
|
|
actualOutcome: outcomeData.actualOutcome,
|
|
timeToOutcome: outcomeData.timeToOutcome,
|
|
pnlImpact: outcomeData.pnlImpact,
|
|
wasCorrect: this.evaluateDecisionCorrectness(originalDecision, outcomeData),
|
|
learningData: {
|
|
originalDecision: originalDecision.decision,
|
|
distanceFromSL: originalDecision.distanceFromSL,
|
|
outcome: outcomeData.actualOutcome,
|
|
profitability: outcomeData.pnlImpact > 0 ? 'PROFITABLE' : 'LOSS'
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
await prisma.ai_learning_data.create({
|
|
data: {
|
|
id: `sl_outcome_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
userId: 'default-user',
|
|
symbol: originalDecision.symbol,
|
|
timeframe: 'OUTCOME',
|
|
analysisData: JSON.stringify(outcomeRecord),
|
|
marketConditions: originalRecord.marketConditions,
|
|
confidenceScore: outcomeRecord.wasCorrect ? 75 : 25
|
|
}
|
|
});
|
|
|
|
await this.log(`✅ Outcome assessed for ${outcomeData.decisionId}: ${outcomeData.actualOutcome} (${outcomeRecord.wasCorrect ? 'CORRECT' : 'INCORRECT'})`);
|
|
|
|
// Update learning thresholds based on outcomes
|
|
await this.updateThresholdsFromOutcome(originalDecision, outcomeRecord);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
await this.log(`❌ Error assessing outcome: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Evaluate if the original decision was correct based on outcome
|
|
*/
|
|
evaluateDecisionCorrectness(originalDecision, outcome) {
|
|
const decision = originalDecision.decision;
|
|
const actualOutcome = outcome.actualOutcome;
|
|
const pnlImpact = outcome.pnlImpact;
|
|
|
|
// Define what constitutes a "correct" decision
|
|
if (decision === 'EMERGENCY_EXIT' && (actualOutcome === 'STOPPED_OUT' || pnlImpact < -50)) {
|
|
return true; // Correctly identified emergency
|
|
}
|
|
|
|
if (decision === 'HOLD_POSITION' && pnlImpact > 0) {
|
|
return true; // Correctly held profitable position
|
|
}
|
|
|
|
if (decision === 'ADJUST_STOP_LOSS' && actualOutcome === 'TAKE_PROFIT') {
|
|
return true; // Adjustment led to profitable exit
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get smart recommendation based on learned patterns
|
|
*/
|
|
async getSmartRecommendation(requestData) {
|
|
try {
|
|
const { distanceFromSL, symbol, marketConditions } = requestData;
|
|
|
|
// Get historical data for similar situations
|
|
const prisma = await getDB();
|
|
const similarDecisions = await prisma.ai_learning_data.findMany({
|
|
where: {
|
|
symbol: symbol,
|
|
analysisData: {
|
|
string_contains: '"type":"STOP_LOSS_DECISION"'
|
|
}
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 20
|
|
});
|
|
|
|
// Analyze patterns from similar situations
|
|
let recommendation = this.getBaseRecommendation(distanceFromSL);
|
|
|
|
if (similarDecisions.length >= 3) {
|
|
const learnedRecommendation = await this.analyzePatterns(similarDecisions, distanceFromSL);
|
|
if (learnedRecommendation) {
|
|
recommendation = learnedRecommendation;
|
|
}
|
|
}
|
|
|
|
await this.log(`🎯 Smart recommendation for ${symbol} at ${distanceFromSL}%: ${recommendation.action}`);
|
|
|
|
return recommendation;
|
|
} catch (error) {
|
|
await this.log(`❌ Error getting smart recommendation: ${error.message}`);
|
|
return this.getBaseRecommendation(distanceFromSL);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get base recommendation using current thresholds
|
|
*/
|
|
getBaseRecommendation(distanceFromSL) {
|
|
if (distanceFromSL <= this.learningThresholds.emergency) {
|
|
return {
|
|
action: 'EMERGENCY_EXIT',
|
|
confidence: 0.8,
|
|
reasoning: `Very close to SL (${distanceFromSL}%), emergency exit recommended`
|
|
};
|
|
} else if (distanceFromSL <= this.learningThresholds.risk) {
|
|
return {
|
|
action: 'HIGH_ALERT',
|
|
confidence: 0.7,
|
|
reasoning: `Close to SL (${distanceFromSL}%), monitor closely`
|
|
};
|
|
} else if (distanceFromSL <= this.learningThresholds.mediumRisk) {
|
|
return {
|
|
action: 'MONITOR',
|
|
confidence: 0.6,
|
|
reasoning: `Moderate distance from SL (${distanceFromSL}%), continue monitoring`
|
|
};
|
|
} else {
|
|
return {
|
|
action: 'HOLD_POSITION',
|
|
confidence: 0.5,
|
|
reasoning: `Safe distance from SL (${distanceFromSL}%), maintain position`
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Analyze historical patterns to improve recommendations
|
|
*/
|
|
async analyzePatterns(decisions, currentDistance) {
|
|
const outcomes = await this.getOutcomesForDecisions(decisions);
|
|
|
|
// Find decisions made at similar distances
|
|
const similarDistanceDecisions = decisions.filter(d => {
|
|
const data = JSON.parse(d.analysisData);
|
|
const distance = data.distanceFromSL;
|
|
return Math.abs(distance - currentDistance) <= 1.0; // Within 1%
|
|
});
|
|
|
|
if (similarDistanceDecisions.length < 2) {
|
|
return null; // Not enough similar data
|
|
}
|
|
|
|
// Analyze success rate of different actions at this distance
|
|
const actionSuccess = {};
|
|
|
|
for (const decision of similarDistanceDecisions) {
|
|
const decisionData = JSON.parse(decision.analysisData);
|
|
const action = decisionData.decision;
|
|
const outcome = outcomes.find(o => o.originalDecisionId === decision.id);
|
|
|
|
if (outcome) {
|
|
if (!actionSuccess[action]) {
|
|
actionSuccess[action] = { total: 0, successful: 0 };
|
|
}
|
|
actionSuccess[action].total++;
|
|
if (outcome.wasCorrect) {
|
|
actionSuccess[action].successful++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find the action with highest success rate
|
|
let bestAction = null;
|
|
let bestSuccessRate = 0;
|
|
|
|
for (const [action, stats] of Object.entries(actionSuccess)) {
|
|
if (stats.total >= 2) { // Need at least 2 samples
|
|
const successRate = stats.successful / stats.total;
|
|
if (successRate > bestSuccessRate) {
|
|
bestSuccessRate = successRate;
|
|
bestAction = action;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestAction && bestSuccessRate > 0.6) {
|
|
return {
|
|
action: bestAction,
|
|
confidence: bestSuccessRate,
|
|
reasoning: `Learned pattern: ${bestAction} successful ${Math.round(bestSuccessRate * 100)}% of time at this distance`
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get outcomes for a set of decisions
|
|
*/
|
|
async getOutcomesForDecisions(decisions) {
|
|
const prisma = await getDB();
|
|
const decisionIds = decisions.map(d => d.id);
|
|
|
|
const outcomes = await prisma.ai_learning_data.findMany({
|
|
where: {
|
|
analysisData: {
|
|
string_contains: '"type":"STOP_LOSS_OUTCOME"'
|
|
}
|
|
}
|
|
});
|
|
|
|
return outcomes.map(o => JSON.parse(o.analysisData))
|
|
.filter(outcome => decisionIds.includes(outcome.originalDecisionId));
|
|
}
|
|
|
|
/**
|
|
* Update learning thresholds based on outcome data
|
|
*/
|
|
async updateThresholdsFromOutcome(originalDecision, outcome) {
|
|
// Simple threshold adjustment based on outcomes
|
|
const distance = originalDecision.distanceFromSL;
|
|
const wasCorrect = outcome.wasCorrect;
|
|
|
|
if (!wasCorrect) {
|
|
// If decision was wrong, adjust thresholds slightly
|
|
if (originalDecision.decision === 'HOLD_POSITION' && outcome.actualOutcome === 'STOPPED_OUT') {
|
|
// We should have exited earlier - make thresholds more conservative
|
|
this.learningThresholds.emergency = Math.min(2.0, this.learningThresholds.emergency + 0.1);
|
|
this.learningThresholds.risk = Math.min(3.0, this.learningThresholds.risk + 0.1);
|
|
}
|
|
}
|
|
|
|
await this.log(`🔧 Thresholds updated: emergency=${this.learningThresholds.emergency}, risk=${this.learningThresholds.risk}`);
|
|
}
|
|
|
|
/**
|
|
* Get current learning status and statistics
|
|
*/
|
|
async analyzeDecisionPatterns() {
|
|
try {
|
|
const prisma = await getDB();
|
|
|
|
// Get recent decisions and outcomes
|
|
const decisions = await prisma.ai_learning_data.findMany({
|
|
where: {
|
|
analysisData: {
|
|
string_contains: '"type":"STOP_LOSS_DECISION"'
|
|
},
|
|
createdAt: {
|
|
gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // Last 7 days
|
|
}
|
|
},
|
|
orderBy: { createdAt: 'desc' }
|
|
});
|
|
|
|
const outcomes = await prisma.ai_learning_data.findMany({
|
|
where: {
|
|
analysisData: {
|
|
string_contains: '"type":"STOP_LOSS_OUTCOME"'
|
|
},
|
|
createdAt: {
|
|
gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // Last 7 days
|
|
}
|
|
}
|
|
});
|
|
|
|
// Analyze patterns
|
|
const patterns = {
|
|
totalDecisions: decisions.length,
|
|
totalOutcomes: outcomes.length,
|
|
successfulDecisions: outcomes.filter(o => JSON.parse(o.analysisData).wasCorrect).length,
|
|
successRate: outcomes.length > 0 ? outcomes.filter(o => JSON.parse(o.analysisData).wasCorrect).length / outcomes.length : 0,
|
|
learnedThresholds: this.learningThresholds
|
|
};
|
|
|
|
return patterns;
|
|
} catch (error) {
|
|
await this.log(`❌ Error analyzing patterns: ${error.message}`);
|
|
return {
|
|
totalDecisions: 0,
|
|
totalOutcomes: 0,
|
|
successfulDecisions: 0,
|
|
successRate: 0,
|
|
learnedThresholds: this.learningThresholds
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get learning status
|
|
*/
|
|
async getLearningStatus() {
|
|
try {
|
|
const prisma = await getDB();
|
|
const totalDecisions = await prisma.ai_learning_data.count({
|
|
where: {
|
|
analysisData: {
|
|
string_contains: '"type":"STOP_LOSS_DECISION"'
|
|
}
|
|
}
|
|
});
|
|
|
|
const recentDecisions = await prisma.ai_learning_data.count({
|
|
where: {
|
|
analysisData: {
|
|
string_contains: '"type":"STOP_LOSS_DECISION"'
|
|
},
|
|
createdAt: {
|
|
gte: new Date(Date.now() - 24 * 60 * 60 * 1000) // Last 24 hours
|
|
}
|
|
}
|
|
});
|
|
|
|
return {
|
|
totalDecisions,
|
|
recentDecisions,
|
|
thresholds: this.learningThresholds,
|
|
isActive: totalDecisions > 0
|
|
};
|
|
|
|
} catch (error) {
|
|
await this.log(`❌ Error getting learning status: ${error.message}`);
|
|
return {
|
|
totalDecisions: 0,
|
|
recentDecisions: 0,
|
|
thresholds: this.learningThresholds,
|
|
isActive: false
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate comprehensive learning report
|
|
* Compatible implementation for enhanced-autonomous-risk-manager
|
|
*/
|
|
async generateLearningReport() {
|
|
try {
|
|
const status = await this.getLearningStatus();
|
|
const patterns = await this.analyzeDecisionPatterns();
|
|
|
|
// Calculate system confidence based on decisions made
|
|
const systemConfidence = this.calculateSystemConfidence(status.totalDecisions, status.recentDecisions, patterns.successRate);
|
|
|
|
const report = {
|
|
timestamp: new Date().toISOString(),
|
|
summary: {
|
|
totalDecisions: status.totalDecisions,
|
|
recentDecisions: status.recentDecisions,
|
|
successfulPatterns: patterns.successfulDecisions,
|
|
learningThresholds: this.learningThresholds,
|
|
systemConfidence: systemConfidence,
|
|
isActive: status.isActive,
|
|
successRate: patterns.successRate
|
|
},
|
|
insights: {
|
|
emergencyThreshold: this.learningThresholds.emergency,
|
|
riskThreshold: this.learningThresholds.risk,
|
|
mediumRiskThreshold: this.learningThresholds.mediumRisk,
|
|
confidenceLevel: systemConfidence > 0.7 ? 'HIGH' : systemConfidence > 0.4 ? 'MEDIUM' : 'LOW',
|
|
totalOutcomes: patterns.totalOutcomes,
|
|
decisionAccuracy: patterns.successRate
|
|
},
|
|
recommendations: this.generateSystemRecommendations(status, patterns)
|
|
};
|
|
|
|
await this.log(`📊 Learning report generated: ${report.summary.totalDecisions} decisions, ${(systemConfidence * 100).toFixed(1)}% confidence, ${(patterns.successRate * 100).toFixed(1)}% success rate`);
|
|
|
|
return report;
|
|
} catch (error) {
|
|
await this.log(`❌ Error generating learning report: ${error.message}`);
|
|
return {
|
|
timestamp: new Date().toISOString(),
|
|
summary: {
|
|
totalDecisions: 0,
|
|
recentDecisions: 0,
|
|
systemConfidence: 0.0,
|
|
isActive: false
|
|
},
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate system confidence based on learning data
|
|
*/
|
|
calculateSystemConfidence(totalDecisions, recentDecisions, successRate = 0) {
|
|
if (totalDecisions < 5) return 0.3; // Low confidence with insufficient data
|
|
if (totalDecisions < 20) return 0.4 + (successRate * 0.2); // Medium-low confidence boosted by success
|
|
if (totalDecisions < 50) return 0.6 + (successRate * 0.2); // Medium confidence boosted by success
|
|
|
|
// High confidence with lots of data, scaled by recent activity and success rate
|
|
const recentActivityFactor = Math.min(1.0, recentDecisions / 10);
|
|
const successFactor = successRate || 0.5; // Default to neutral if no success data
|
|
return Math.min(0.95, 0.7 + (recentActivityFactor * 0.1) + (successFactor * 0.15)); // Cap at 95%
|
|
}
|
|
|
|
/**
|
|
* Generate system recommendations based on learning status
|
|
*/
|
|
generateSystemRecommendations(status, patterns) {
|
|
const recommendations = [];
|
|
|
|
if (status.totalDecisions < 10) {
|
|
recommendations.push({
|
|
type: 'DATA_COLLECTION',
|
|
message: 'Need more decision data for reliable learning',
|
|
priority: 'HIGH'
|
|
});
|
|
}
|
|
|
|
if (status.recentDecisions < 3) {
|
|
recommendations.push({
|
|
type: 'ACTIVITY_LOW',
|
|
message: 'Recent trading activity is low - learning may be stale',
|
|
priority: 'MEDIUM'
|
|
});
|
|
}
|
|
|
|
if (patterns && patterns.successRate < 0.4 && patterns.totalOutcomes >= 5) {
|
|
recommendations.push({
|
|
type: 'THRESHOLD_ADJUSTMENT',
|
|
message: 'Low success rate detected - consider adjusting decision thresholds',
|
|
priority: 'HIGH'
|
|
});
|
|
}
|
|
|
|
if (status.totalDecisions >= 20 && patterns && patterns.successRate > 0.6) {
|
|
recommendations.push({
|
|
type: 'SYSTEM_PERFORMING',
|
|
message: 'System learning effectively with good success rate',
|
|
priority: 'LOW'
|
|
});
|
|
}
|
|
|
|
if (status.totalDecisions >= 50) {
|
|
recommendations.push({
|
|
type: 'OPTIMIZATION_READY',
|
|
message: 'Sufficient data available for advanced threshold optimization',
|
|
priority: 'LOW'
|
|
});
|
|
}
|
|
|
|
return recommendations;
|
|
}
|
|
}
|
|
|
|
export { SimplifiedStopLossLearner };
|