Files
nofx/web/src/components/DecisionCard.tsx
tinkle-community 3f084005e4 feat: upgrade Binance to Algo Order API and improve trading flow
- Upgrade go-binance to v2.8.9 with new Algo Order API
- Migrate SetStopLoss/SetTakeProfit to use AlgoOrderTypeStopMarket/TakeProfitMarket
- Update cancel functions to handle both legacy and Algo orders
- Fix Lighter stop orders using correct order types (type=2/4) with TriggerPrice
- Add CancelAllOrders before opening positions for Bybit and Lighter
- Fix decision limit selector in API handler
- Add stop_loss/take_profit/confidence fields to DecisionAction
- Store decisions array in database with proper serialization
- Redesign DecisionCard with beautiful entry/SL/TP display
2025-12-15 21:22:22 +08:00

369 lines
14 KiB
TypeScript

import { useState } from 'react'
import type { DecisionRecord, DecisionAction } from '../types'
import { t, type Language } from '../i18n/translations'
interface DecisionCardProps {
decision: DecisionRecord
language: Language
}
// Action type configuration
const ACTION_CONFIG: Record<string, { color: string; bg: string; icon: string; label: string }> = {
open_long: { color: '#0ECB81', bg: 'rgba(14, 203, 129, 0.15)', icon: '📈', label: 'LONG' },
open_short: { color: '#F6465D', bg: 'rgba(246, 70, 93, 0.15)', icon: '📉', label: 'SHORT' },
close_long: { color: '#F0B90B', bg: 'rgba(240, 185, 11, 0.15)', icon: '💰', label: 'CLOSE' },
close_short: { color: '#F0B90B', bg: 'rgba(240, 185, 11, 0.15)', icon: '💰', label: 'CLOSE' },
hold: { color: '#848E9C', bg: 'rgba(132, 142, 156, 0.15)', icon: '⏸️', label: 'HOLD' },
wait: { color: '#848E9C', bg: 'rgba(132, 142, 156, 0.15)', icon: '⏳', label: 'WAIT' },
}
// Format price with proper decimals
function formatPrice(price: number | undefined): string {
if (!price || price === 0) return '-'
if (price >= 1000) return price.toFixed(2)
if (price >= 1) return price.toFixed(4)
return price.toFixed(6)
}
// Calculate percentage change
function calcPctChange(entry: number | undefined, target: number | undefined, isLong: boolean): string {
if (!entry || !target || entry === 0) return '-'
const pct = ((target - entry) / entry) * 100
const adjustedPct = isLong ? pct : -pct
return `${adjustedPct >= 0 ? '+' : ''}${adjustedPct.toFixed(2)}%`
}
// Get confidence color
function getConfidenceColor(confidence: number | undefined): string {
if (!confidence) return '#848E9C'
if (confidence >= 80) return '#0ECB81'
if (confidence >= 60) return '#F0B90B'
return '#F6465D'
}
// Single Action Card Component
function ActionCard({ action, language }: { action: DecisionAction; language: Language }) {
const config = ACTION_CONFIG[action.action] || ACTION_CONFIG.wait
const isLong = action.action.includes('long')
const isOpen = action.action.includes('open')
return (
<div
className="rounded-lg p-4 transition-all duration-200 hover:scale-[1.01]"
style={{
background: 'linear-gradient(135deg, #1E2329 0%, #181C21 100%)',
border: `1px solid ${config.color}33`,
boxShadow: `0 4px 12px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.03)`,
}}
>
{/* Header Row */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<span className="text-xl">{config.icon}</span>
<span className="font-mono font-bold text-lg" style={{ color: '#EAECEF' }}>
{action.symbol.replace('USDT', '')}
</span>
<span
className="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider"
style={{ background: config.bg, color: config.color, border: `1px solid ${config.color}55` }}
>
{config.label}
</span>
</div>
{/* Status Badge */}
<div className="flex items-center gap-2">
{action.confidence !== undefined && action.confidence > 0 && (
<div
className="px-2 py-1 rounded text-xs font-semibold"
style={{
background: `${getConfidenceColor(action.confidence)}22`,
color: getConfidenceColor(action.confidence)
}}
>
{action.confidence.toFixed(0)}%
</div>
)}
<div
className="w-2 h-2 rounded-full"
style={{ background: action.success ? '#0ECB81' : '#F6465D' }}
/>
</div>
</div>
{/* Trading Details Grid */}
{isOpen && (
<div className="grid grid-cols-4 gap-3 mt-3 pt-3" style={{ borderTop: '1px solid #2B3139' }}>
{/* Entry Price */}
<div className="text-center">
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
{t('entryPrice', language)}
</div>
<div className="font-mono font-semibold" style={{ color: '#EAECEF' }}>
{formatPrice(action.price)}
</div>
</div>
{/* Stop Loss */}
<div className="text-center">
<div className="text-xs mb-1" style={{ color: '#F6465D' }}>
{t('stopLoss', language)}
</div>
<div className="font-mono font-semibold" style={{ color: '#F6465D' }}>
{formatPrice(action.stop_loss)}
</div>
{action.stop_loss && action.price && (
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
{calcPctChange(action.price, action.stop_loss, isLong)}
</div>
)}
</div>
{/* Take Profit */}
<div className="text-center">
<div className="text-xs mb-1" style={{ color: '#0ECB81' }}>
{t('takeProfit', language)}
</div>
<div className="font-mono font-semibold" style={{ color: '#0ECB81' }}>
{formatPrice(action.take_profit)}
</div>
{action.take_profit && action.price && (
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
{calcPctChange(action.price, action.take_profit, isLong)}
</div>
)}
</div>
{/* Leverage */}
<div className="text-center">
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
{t('leverage', language)}
</div>
<div className="font-mono font-semibold" style={{ color: '#F0B90B' }}>
{action.leverage}x
</div>
</div>
</div>
)}
{/* Risk/Reward Ratio for open positions */}
{isOpen && action.stop_loss && action.take_profit && action.price && (
<div className="mt-3 pt-3 flex items-center justify-between" style={{ borderTop: '1px solid #2B3139' }}>
<span className="text-xs" style={{ color: '#848E9C' }}>{t('riskReward', language)}</span>
<div className="flex items-center gap-2">
{(() => {
const slDist = Math.abs(action.price - action.stop_loss)
const tpDist = Math.abs(action.take_profit - action.price)
const ratio = slDist > 0 ? (tpDist / slDist) : 0
const ratioColor = ratio >= 3 ? '#0ECB81' : ratio >= 2 ? '#F0B90B' : '#F6465D'
return (
<>
<div className="flex gap-1">
<span style={{ color: '#F6465D' }}>1</span>
<span style={{ color: '#848E9C' }}>:</span>
<span style={{ color: '#0ECB81' }}>{ratio.toFixed(1)}</span>
</div>
<div
className="h-1.5 rounded-full"
style={{
width: '60px',
background: '#2B3139',
}}
>
<div
className="h-full rounded-full transition-all duration-300"
style={{
width: `${Math.min(ratio / 5 * 100, 100)}%`,
background: ratioColor
}}
/>
</div>
</>
)
})()}
</div>
</div>
)}
{/* Reasoning */}
{action.reasoning && (
<div className="mt-3 pt-3" style={{ borderTop: '1px solid #2B3139' }}>
<div className="text-xs line-clamp-2" style={{ color: '#848E9C' }}>
💡 {action.reasoning}
</div>
</div>
)}
{/* Error Message */}
{action.error && (
<div
className="mt-3 rounded p-2 text-xs"
style={{
background: 'rgba(246, 70, 93, 0.1)',
border: '1px solid rgba(246, 70, 93, 0.3)',
color: '#F6465D',
}}
>
{action.error}
</div>
)}
</div>
)
}
export function DecisionCard({ decision, language }: DecisionCardProps) {
const [showInputPrompt, setShowInputPrompt] = useState(false)
const [showCoT, setShowCoT] = useState(false)
return (
<div
className="rounded-xl p-5 transition-all duration-300 hover:translate-y-[-2px]"
style={{
border: '1px solid #2B3139',
background: 'linear-gradient(180deg, #1E2329 0%, #181C21 100%)',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.3)',
}}
>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center"
style={{ background: 'rgba(240, 185, 11, 0.15)' }}
>
<span className="text-xl">🤖</span>
</div>
<div>
<div className="font-bold" style={{ color: '#EAECEF' }}>
{t('cycle', language)} #{decision.cycle_number}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
{new Date(decision.timestamp).toLocaleString()}
</div>
</div>
</div>
<div
className="px-4 py-1.5 rounded-full text-xs font-bold tracking-wider"
style={
decision.success
? { background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81', border: '1px solid rgba(14, 203, 129, 0.3)' }
: { background: 'rgba(246, 70, 93, 0.15)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.3)' }
}
>
{t(decision.success ? 'success' : 'failed', language)}
</div>
</div>
{/* Decision Actions - Beautiful Grid */}
{decision.decisions && decision.decisions.length > 0 && (
<div className="space-y-3 mb-4">
{decision.decisions.map((action, index) => (
<ActionCard key={`${action.symbol}-${index}`} action={action} language={language} />
))}
</div>
)}
{/* Collapsible Sections */}
<div className="space-y-2">
{/* Input Prompt */}
{decision.input_prompt && (
<div>
<button
onClick={() => setShowInputPrompt(!showInputPrompt)}
className="flex items-center gap-2 text-sm transition-colors w-full justify-between p-2 rounded hover:bg-white/5"
>
<div className="flex items-center gap-2">
<span className="text-base">📥</span>
<span className="font-semibold" style={{ color: '#60a5fa' }}>
{t('inputPrompt', language)}
</span>
</div>
<span
className="text-xs px-2 py-0.5 rounded"
style={{ background: 'rgba(96, 165, 250, 0.15)', color: '#60a5fa' }}
>
{showInputPrompt ? t('collapse', language) : t('expand', language)}
</span>
</button>
{showInputPrompt && (
<div
className="mt-2 rounded-lg p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
>
{decision.input_prompt}
</div>
)}
</div>
)}
{/* AI Thinking */}
{decision.cot_trace && (
<div>
<button
onClick={() => setShowCoT(!showCoT)}
className="flex items-center gap-2 text-sm transition-colors w-full justify-between p-2 rounded hover:bg-white/5"
>
<div className="flex items-center gap-2">
<span className="text-base">🧠</span>
<span className="font-semibold" style={{ color: '#F0B90B' }}>
{t('aiThinking', language)}
</span>
</div>
<span
className="text-xs px-2 py-0.5 rounded"
style={{ background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }}
>
{showCoT ? t('collapse', language) : t('expand', language)}
</span>
</button>
{showCoT && (
<div
className="mt-2 rounded-lg p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
>
{decision.cot_trace}
</div>
)}
</div>
)}
</div>
{/* Execution Log */}
{decision.execution_log && decision.execution_log.length > 0 && (
<div
className="rounded-lg p-3 mt-4 text-xs font-mono space-y-1"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
{decision.execution_log.map((log, index) => (
<div key={`${log}-${index}`} style={{ color: '#EAECEF' }}>
{log}
</div>
))}
</div>
)}
{/* Error Message */}
{decision.error_message && (
<div
className="rounded-lg p-3 mt-4 text-sm"
style={{
background: 'rgba(246, 70, 93, 0.1)',
border: '1px solid rgba(246, 70, 93, 0.4)',
color: '#F6465D',
}}
>
{decision.error_message}
</div>
)}
</div>
)
}