Add automation timer and individual timeframe analysis display
Features Added: Analysis Timer: Shows countdown to next analysis with progress bar Individual Timeframe Results: Display analysis for each timeframe separately Real-time Countdown: Updates every second showing time until next analysis Enhanced Status API: Includes timing data and individual results Cycle Counter: Shows current automation cycle number UI Improvements: - Analysis Timer panel with countdown and progress bar - Individual Timeframe Analysis panel showing recommendation and confidence for each timeframe - Real-time updates of countdown timer - Visual indicators for BUY/SELL/HOLD recommendations - Analysis interval display (15m/1h/etc) Technical Changes: - Enhanced AutomationService with timing tracking - Added nextAnalysisIn, analysisInterval, currentCycle to status - Individual timeframe results stored and displayed - Real-time countdown effect in React - Progress bar visualization of analysis cycle - Enhanced status API endpoint with automation service integration Example Display: 15m analysis: SELL (80% confidence) 1h analysis: HOLD (65% confidence) Next Analysis In: 14m 32s [Progress Bar] Cycle #5 | Analysis Interval: 15m
This commit is contained in:
@@ -1,21 +1,12 @@
|
|||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { PrismaClient } from '@prisma/client'
|
import { automationService } from '../../../../lib/automation-service-simple'
|
||||||
|
|
||||||
const prisma = new PrismaClient()
|
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
// Get the latest automation session with correct data
|
// Get status from the automation service directly (includes timing and individual results)
|
||||||
const session = await prisma.automationSession.findFirst({
|
const status = await automationService.getStatus()
|
||||||
where: {
|
|
||||||
userId: 'default-user',
|
if (!status) {
|
||||||
symbol: 'SOLUSD',
|
|
||||||
timeframe: '1h'
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: 'desc' }
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
status: {
|
status: {
|
||||||
@@ -27,51 +18,18 @@ export async function GET() {
|
|||||||
successfulTrades: 0,
|
successfulTrades: 0,
|
||||||
winRate: 0,
|
winRate: 0,
|
||||||
totalPnL: 0,
|
totalPnL: 0,
|
||||||
errorCount: 0
|
errorCount: 0,
|
||||||
|
nextAnalysisIn: 0,
|
||||||
|
analysisInterval: 3600,
|
||||||
|
currentCycle: 0,
|
||||||
|
individualTimeframeResults: []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get actual trade data to calculate real statistics
|
|
||||||
const trades = await prisma.trade.findMany({
|
|
||||||
where: {
|
|
||||||
userId: session.userId,
|
|
||||||
symbol: session.symbol
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: 'desc' }
|
|
||||||
})
|
|
||||||
|
|
||||||
const completedTrades = trades.filter(t => t.status === 'COMPLETED')
|
|
||||||
const successfulTrades = completedTrades.filter(t => {
|
|
||||||
const profit = t.profit || 0
|
|
||||||
return profit > 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const totalPnL = completedTrades.reduce((sum, trade) => {
|
|
||||||
const profit = trade.profit || 0
|
|
||||||
return sum + profit
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
const winRate = completedTrades.length > 0 ?
|
|
||||||
(successfulTrades.length / completedTrades.length * 100) : 0
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
status: {
|
status: status
|
||||||
isActive: session.status === 'ACTIVE',
|
|
||||||
mode: session.mode,
|
|
||||||
symbol: session.symbol,
|
|
||||||
timeframe: session.timeframe,
|
|
||||||
totalTrades: completedTrades.length,
|
|
||||||
successfulTrades: successfulTrades.length,
|
|
||||||
winRate: Math.round(winRate * 10) / 10, // Round to 1 decimal
|
|
||||||
totalPnL: Math.round(totalPnL * 100) / 100, // Round to 2 decimals
|
|
||||||
lastAnalysis: session.lastAnalysis,
|
|
||||||
lastTrade: session.lastTrade,
|
|
||||||
nextScheduled: session.nextScheduled,
|
|
||||||
errorCount: session.errorCount,
|
|
||||||
lastError: session.lastError
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Automation status error:', error)
|
console.error('Automation status error:', error)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default function AutomationPageV2() {
|
|||||||
const [balance, setBalance] = useState(null)
|
const [balance, setBalance] = useState(null)
|
||||||
const [positions, setPositions] = useState([])
|
const [positions, setPositions] = useState([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [nextAnalysisCountdown, setNextAnalysisCountdown] = useState(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStatus()
|
fetchStatus()
|
||||||
@@ -44,6 +45,51 @@ export default function AutomationPageV2() {
|
|||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Timer effect for countdown
|
||||||
|
useEffect(() => {
|
||||||
|
let countdownInterval = null
|
||||||
|
|
||||||
|
if (status?.isActive && status?.nextAnalysisIn > 0) {
|
||||||
|
setNextAnalysisCountdown(status.nextAnalysisIn)
|
||||||
|
|
||||||
|
countdownInterval = setInterval(() => {
|
||||||
|
setNextAnalysisCountdown(prev => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
// Refresh status when timer reaches 0
|
||||||
|
fetchStatus()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return prev - 1
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
setNextAnalysisCountdown(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (countdownInterval) {
|
||||||
|
clearInterval(countdownInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [status?.nextAnalysisIn, status?.isActive])
|
||||||
|
|
||||||
|
// Helper function to format countdown time
|
||||||
|
const formatCountdown = (seconds) => {
|
||||||
|
if (seconds <= 0) return 'Analyzing now...'
|
||||||
|
|
||||||
|
const hours = Math.floor(seconds / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
const secs = seconds % 60
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}h ${minutes}m ${secs}s`
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}m ${secs}s`
|
||||||
|
} else {
|
||||||
|
return `${secs}s`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toggleTimeframe = (timeframe) => {
|
const toggleTimeframe = (timeframe) => {
|
||||||
setConfig(prev => ({
|
setConfig(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -498,6 +544,82 @@ export default function AutomationPageV2() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Analysis Timer */}
|
||||||
|
{status?.isActive && (
|
||||||
|
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-xl font-bold text-white">Analysis Timer</h3>
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
Cycle #{status.currentCycle || 0}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-3xl font-bold text-blue-400 mb-2">
|
||||||
|
{formatCountdown(nextAnalysisCountdown)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
{nextAnalysisCountdown > 0 ? 'Next Analysis In' : 'Analysis Running'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-700 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-blue-500 h-2 rounded-full transition-all duration-1000"
|
||||||
|
style={{
|
||||||
|
width: status.analysisInterval > 0 ?
|
||||||
|
`${Math.max(0, 100 - (nextAnalysisCountdown / status.analysisInterval) * 100)}%` :
|
||||||
|
'0%'
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 text-center">
|
||||||
|
Analysis Interval: {Math.floor((status.analysisInterval || 0) / 60)}m
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Individual Timeframe Results */}
|
||||||
|
{status?.individualTimeframeResults && status.individualTimeframeResults.length > 0 && (
|
||||||
|
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
|
||||||
|
<h3 className="text-xl font-bold text-white mb-4">Timeframe Analysis</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{status.individualTimeframeResults.map((result, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between p-3 bg-gray-700/50 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<span className="text-cyan-400 font-bold text-sm w-8">
|
||||||
|
{timeframes.find(tf => tf.value === result.timeframe)?.label || result.timeframe}
|
||||||
|
</span>
|
||||||
|
<span className={`font-semibold text-sm px-2 py-1 rounded ${
|
||||||
|
result.recommendation === 'BUY' ? 'bg-green-600/20 text-green-400' :
|
||||||
|
result.recommendation === 'SELL' ? 'bg-red-600/20 text-red-400' :
|
||||||
|
'bg-gray-600/20 text-gray-400'
|
||||||
|
}`}>
|
||||||
|
{result.recommendation}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-white font-semibold text-sm">
|
||||||
|
{result.confidence}%
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
confidence
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 p-3 bg-blue-600/10 border border-blue-600/30 rounded-lg">
|
||||||
|
<div className="text-xs text-blue-400">
|
||||||
|
✅ Last Updated: {status.individualTimeframeResults[0]?.timestamp ?
|
||||||
|
new Date(status.individualTimeframeResults[0].timestamp).toLocaleTimeString() :
|
||||||
|
'N/A'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Trading Metrics */}
|
{/* Trading Metrics */}
|
||||||
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
|
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
|
||||||
<h3 className="text-xl font-bold text-white mb-4">Trading Metrics</h3>
|
<h3 className="text-xl font-bold text-white mb-4">Trading Metrics</h3>
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user