feat: update timeframe presets and improve sorting
- Add 30m and 2h timeframes to available options - Update Scalping preset: 5m, 15m, 30m (was 5m, 15m, 1h) - Update Day Trading preset: 1h, 2h, 4h (was 1h, 4h, 1d) - Enhance sorting logic for screenshots and analysis results - Ensure consistent timeframe order: 5m → 15m → 30m → 1h → 2h → 4h → 1d - Improve multi-timeframe analysis display with proper sorting - Update filename parsing to handle new timeframes Changes improve trading workflow with more logical timeframe progressions for scalping and day trading strategies.
This commit is contained in:
@@ -8,7 +8,9 @@ const timeframes = [
|
|||||||
{ label: '1m', value: '1' },
|
{ label: '1m', value: '1' },
|
||||||
{ label: '5m', value: '5' },
|
{ label: '5m', value: '5' },
|
||||||
{ label: '15m', value: '15' },
|
{ label: '15m', value: '15' },
|
||||||
|
{ label: '30m', value: '30' },
|
||||||
{ label: '1h', value: '60' },
|
{ label: '1h', value: '60' },
|
||||||
|
{ label: '2h', value: '120' },
|
||||||
{ label: '4h', value: '240' },
|
{ label: '4h', value: '240' },
|
||||||
{ label: '1d', value: 'D' },
|
{ label: '1d', value: 'D' },
|
||||||
{ label: '1w', value: 'W' },
|
{ label: '1w', value: 'W' },
|
||||||
@@ -38,7 +40,6 @@ interface ProgressStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AnalysisProgress {
|
interface AnalysisProgress {
|
||||||
sessionId: string
|
|
||||||
currentStep: number
|
currentStep: number
|
||||||
totalSteps: number
|
totalSteps: number
|
||||||
steps: ProgressStep[]
|
steps: ProgressStep[]
|
||||||
@@ -545,16 +546,16 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
|||||||
<label className="block text-xs font-medium text-gray-400 mb-2">Quick Timeframe Presets</label>
|
<label className="block text-xs font-medium text-gray-400 mb-2">Quick Timeframe Presets</label>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedTimeframes(['5', '15', '60'])}
|
onClick={() => setSelectedTimeframes(['5', '15', '30'])}
|
||||||
className="py-2 px-3 rounded-lg text-xs font-medium bg-purple-600/20 text-purple-300 hover:bg-purple-600/30 transition-all"
|
className="py-2 px-3 rounded-lg text-xs font-medium bg-purple-600/20 text-purple-300 hover:bg-purple-600/30 transition-all"
|
||||||
>
|
>
|
||||||
🕒 Scalping (5m, 15m, 1h)
|
🕒 Scalping (5m, 15m, 30m)
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedTimeframes(['60', '240', 'D'])}
|
onClick={() => setSelectedTimeframes(['60', '120', '240'])}
|
||||||
className="py-2 px-3 rounded-lg text-xs font-medium bg-blue-600/20 text-blue-300 hover:bg-blue-600/30 transition-all"
|
className="py-2 px-3 rounded-lg text-xs font-medium bg-blue-600/20 text-blue-300 hover:bg-blue-600/30 transition-all"
|
||||||
>
|
>
|
||||||
📊 Day Trading (1h, 4h, 1d)
|
📊 Day Trading (1h, 2h, 4h)
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedTimeframes(['240', 'D', 'W'])}
|
onClick={() => setSelectedTimeframes(['240', 'D', 'W'])}
|
||||||
@@ -944,7 +945,23 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{result.results.map((timeframeResult: any, index: number) => (
|
{result.results
|
||||||
|
.sort((a: any, b: any) => {
|
||||||
|
// Sort by timeframe order: 5m, 15m, 30m, 1h, 2h, 4h, 1D
|
||||||
|
const timeframeOrder: {[key: string]: number} = {
|
||||||
|
'5': 1, '5m': 1,
|
||||||
|
'15': 2, '15m': 2,
|
||||||
|
'30': 3, '30m': 3,
|
||||||
|
'60': 4, '1h': 4,
|
||||||
|
'120': 5, '2h': 5,
|
||||||
|
'240': 6, '4h': 6,
|
||||||
|
'D': 7, '1D': 7
|
||||||
|
}
|
||||||
|
const orderA = timeframeOrder[a.timeframe] || timeframeOrder[a.timeframeLabel] || 999
|
||||||
|
const orderB = timeframeOrder[b.timeframe] || timeframeOrder[b.timeframeLabel] || 999
|
||||||
|
return orderA - orderB
|
||||||
|
})
|
||||||
|
.map((timeframeResult: any, index: number) => (
|
||||||
<div key={index} className={`p-4 rounded-lg border ${
|
<div key={index} className={`p-4 rounded-lg border ${
|
||||||
timeframeResult.success
|
timeframeResult.success
|
||||||
? 'bg-green-500/5 border-green-500/30'
|
? 'bg-green-500/5 border-green-500/30'
|
||||||
@@ -1425,9 +1442,43 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
|||||||
{/* Multi-timeframe Screenshot Gallery */}
|
{/* Multi-timeframe Screenshot Gallery */}
|
||||||
{result && result.type === 'multi_timeframe' && result.results && (
|
{result && result.type === 'multi_timeframe' && result.results && (
|
||||||
<ScreenshotGallery
|
<ScreenshotGallery
|
||||||
screenshots={result.results.filter((r: any) => r.success && r.result.screenshots).flatMap((r: any) => r.result.screenshots)}
|
screenshots={result.results
|
||||||
|
.filter((r: any) => r.success && r.result.screenshots)
|
||||||
|
.sort((a: any, b: any) => {
|
||||||
|
// Sort by timeframe order: 5m, 15m, 30m, 1h, 2h, 4h, 1D
|
||||||
|
const timeframeOrder: {[key: string]: number} = {
|
||||||
|
'5': 1, '5m': 1,
|
||||||
|
'15': 2, '15m': 2,
|
||||||
|
'30': 3, '30m': 3,
|
||||||
|
'60': 4, '1h': 4,
|
||||||
|
'120': 5, '2h': 5,
|
||||||
|
'240': 6, '4h': 6,
|
||||||
|
'D': 7, '1D': 7
|
||||||
|
}
|
||||||
|
const orderA = timeframeOrder[a.timeframe] || timeframeOrder[a.timeframeLabel] || 999
|
||||||
|
const orderB = timeframeOrder[b.timeframe] || timeframeOrder[b.timeframeLabel] || 999
|
||||||
|
return orderA - orderB
|
||||||
|
})
|
||||||
|
.flatMap((r: any) => r.result.screenshots)}
|
||||||
symbol={symbol}
|
symbol={symbol}
|
||||||
timeframes={result.results.filter((r: any) => r.success).map((r: any) => r.timeframeLabel)}
|
timeframes={result.results
|
||||||
|
.filter((r: any) => r.success)
|
||||||
|
.sort((a: any, b: any) => {
|
||||||
|
// Sort by timeframe order: 5m, 15m, 30m, 1h, 2h, 4h, 1D
|
||||||
|
const timeframeOrder: {[key: string]: number} = {
|
||||||
|
'5': 1, '5m': 1,
|
||||||
|
'15': 2, '15m': 2,
|
||||||
|
'30': 3, '30m': 3,
|
||||||
|
'60': 4, '1h': 4,
|
||||||
|
'120': 5, '2h': 5,
|
||||||
|
'240': 6, '4h': 6,
|
||||||
|
'D': 7, '1D': 7
|
||||||
|
}
|
||||||
|
const orderA = timeframeOrder[a.timeframe] || timeframeOrder[a.timeframeLabel] || 999
|
||||||
|
const orderB = timeframeOrder[b.timeframe] || timeframeOrder[b.timeframeLabel] || 999
|
||||||
|
return orderA - orderB
|
||||||
|
})
|
||||||
|
.map((r: any) => r.timeframeLabel)}
|
||||||
enlargedImage={enlargedScreenshot}
|
enlargedImage={enlargedScreenshot}
|
||||||
onImageClick={handleScreenshotClick}
|
onImageClick={handleScreenshotClick}
|
||||||
onClose={() => setEnlargedScreenshot(null)}
|
onClose={() => setEnlargedScreenshot(null)}
|
||||||
|
|||||||
@@ -34,6 +34,56 @@ export default function ScreenshotGallery({
|
|||||||
|
|
||||||
if (screenshots.length === 0) return null
|
if (screenshots.length === 0) return null
|
||||||
|
|
||||||
|
// Utility function to convert timeframe to sortable number
|
||||||
|
const timeframeToMinutes = (timeframe: string): number => {
|
||||||
|
const tf = timeframe.toLowerCase()
|
||||||
|
if (tf.includes('5m') || tf === '5') return 5
|
||||||
|
if (tf.includes('15m') || tf === '15') return 15
|
||||||
|
if (tf.includes('30m') || tf === '30') return 30
|
||||||
|
if (tf.includes('1h') || tf === '60') return 60
|
||||||
|
if (tf.includes('2h') || tf === '120') return 120
|
||||||
|
if (tf.includes('4h') || tf === '240') return 240
|
||||||
|
if (tf.includes('1d') || tf === 'D') return 1440
|
||||||
|
// Default fallback
|
||||||
|
return parseInt(tf) || 999
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract timeframe from filename
|
||||||
|
const extractTimeframeFromFilename = (filename: string) => {
|
||||||
|
const match = filename.match(/_(\d+|D)_/)
|
||||||
|
if (!match) return 'Unknown'
|
||||||
|
const tf = match[1]
|
||||||
|
if (tf === 'D') return '1D'
|
||||||
|
if (tf === '5') return '5m'
|
||||||
|
if (tf === '15') return '15m'
|
||||||
|
if (tf === '30') return '30m'
|
||||||
|
if (tf === '60') return '1h'
|
||||||
|
if (tf === '120') return '2h'
|
||||||
|
if (tf === '240') return '4h'
|
||||||
|
return `${tf}m`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create sorted screenshot data with timeframes
|
||||||
|
const screenshotData = screenshots.map((screenshot, index) => {
|
||||||
|
const screenshotUrl = typeof screenshot === 'string'
|
||||||
|
? screenshot
|
||||||
|
: (screenshot as any)?.url || String(screenshot)
|
||||||
|
const filename = screenshotUrl.split('/').pop() || ''
|
||||||
|
const timeframe = timeframes[index] || extractTimeframeFromFilename(filename)
|
||||||
|
|
||||||
|
return {
|
||||||
|
screenshot,
|
||||||
|
screenshotUrl,
|
||||||
|
filename,
|
||||||
|
timeframe,
|
||||||
|
index,
|
||||||
|
sortOrder: timeframeToMinutes(timeframe)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sort by timeframe (smallest to largest)
|
||||||
|
const sortedData = screenshotData.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
|
|
||||||
// Helper function to format screenshot URL
|
// Helper function to format screenshot URL
|
||||||
const formatScreenshotUrl = (screenshot: string | any) => {
|
const formatScreenshotUrl = (screenshot: string | any) => {
|
||||||
// Handle both string URLs and screenshot objects
|
// Handle both string URLs and screenshot objects
|
||||||
@@ -56,36 +106,17 @@ export default function ScreenshotGallery({
|
|||||||
Chart Screenshots
|
Chart Screenshots
|
||||||
</h4>
|
</h4>
|
||||||
<div className="text-xs text-gray-400">
|
<div className="text-xs text-gray-400">
|
||||||
{screenshots.length} captured • Click to enlarge
|
{sortedData.length} captured • Click to enlarge
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{screenshots.map((screenshot, index) => {
|
{sortedData.map((item, displayIndex) => {
|
||||||
// Handle both string URLs and screenshot objects
|
const imageUrl = formatScreenshotUrl(item.screenshot)
|
||||||
const screenshotUrl = typeof screenshot === 'string'
|
|
||||||
? screenshot
|
|
||||||
: (screenshot as any)?.url || String(screenshot)
|
|
||||||
const filename = screenshotUrl.split('/').pop() || ''
|
|
||||||
// Extract timeframe from filename (e.g., SOLUSD_5_ai_timestamp.png -> "5m")
|
|
||||||
const extractTimeframeFromFilename = (filename: string) => {
|
|
||||||
const match = filename.match(/_(\d+|D)_/)
|
|
||||||
if (!match) return 'Unknown'
|
|
||||||
const tf = match[1]
|
|
||||||
if (tf === 'D') return '1D'
|
|
||||||
if (tf === '5') return '5m'
|
|
||||||
if (tf === '15') return '15m'
|
|
||||||
if (tf === '60') return '1h'
|
|
||||||
if (tf === '240') return '4h'
|
|
||||||
return `${tf}m`
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeframe = timeframes[index] || extractTimeframeFromFilename(filename)
|
|
||||||
const imageUrl = formatScreenshotUrl(screenshot)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={displayIndex}
|
||||||
className="group relative bg-gray-800/30 rounded-lg overflow-hidden border border-gray-700 hover:border-purple-500/50 transition-all cursor-pointer transform hover:scale-[1.02]"
|
className="group relative bg-gray-800/30 rounded-lg overflow-hidden border border-gray-700 hover:border-purple-500/50 transition-all cursor-pointer transform hover:scale-[1.02]"
|
||||||
onClick={() => onImageClick(imageUrl)}
|
onClick={() => onImageClick(imageUrl)}
|
||||||
>
|
>
|
||||||
@@ -93,7 +124,7 @@ export default function ScreenshotGallery({
|
|||||||
<div className="aspect-video bg-gray-800 flex items-center justify-center relative">
|
<div className="aspect-video bg-gray-800 flex items-center justify-center relative">
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={`${symbol} - ${timeframe} chart`}
|
alt={`${symbol} - ${item.timeframe} chart`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
onError={(e: any) => {
|
onError={(e: any) => {
|
||||||
const target = e.target as HTMLImageElement
|
const target = e.target as HTMLImageElement
|
||||||
@@ -106,7 +137,7 @@ export default function ScreenshotGallery({
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-3xl mb-2">📊</div>
|
<div className="text-3xl mb-2">📊</div>
|
||||||
<div className="text-sm">Chart Preview</div>
|
<div className="text-sm">Chart Preview</div>
|
||||||
<div className="text-xs text-gray-500">{filename}</div>
|
<div className="text-xs text-gray-500">{item.filename}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -125,7 +156,7 @@ export default function ScreenshotGallery({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-white">{symbol}</div>
|
<div className="text-sm font-medium text-white">{symbol}</div>
|
||||||
<div className="text-xs text-purple-300">{timeframe} Timeframe</div>
|
<div className="text-xs text-purple-300">{item.timeframe} Timeframe</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400">
|
<div className="text-xs text-gray-400">
|
||||||
Click to view
|
Click to view
|
||||||
|
|||||||
Reference in New Issue
Block a user