mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-06 12:30:59 +08:00
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
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import type { DecisionRecord } from '../types'
|
||||
import type { DecisionRecord, DecisionAction } from '../types'
|
||||
import { t, type Language } from '../i18n/translations'
|
||||
|
||||
interface DecisionCardProps {
|
||||
@@ -7,154 +7,339 @@ interface DecisionCardProps {
|
||||
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 p-5 transition-all duration-300 hover:translate-y-[-2px]"
|
||||
className="rounded-xl p-5 transition-all duration-300 hover:translate-y-[-2px]"
|
||||
style={{
|
||||
border: '1px solid #2B3139',
|
||||
background: '#1E2329',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
|
||||
background: 'linear-gradient(180deg, #1E2329 0%, #181C21 100%)',
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{t('cycle', language)} #{decision.cycle_number}
|
||||
{/* 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 className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{new Date(decision.timestamp).toLocaleString()}
|
||||
<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-3 py-1 rounded text-xs font-bold"
|
||||
className="px-4 py-1.5 rounded-full text-xs font-bold tracking-wider"
|
||||
style={
|
||||
decision.success
|
||||
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
|
||||
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
|
||||
? { 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.input_prompt && (
|
||||
<div className="mb-3">
|
||||
<button
|
||||
onClick={() => setShowInputPrompt(!showInputPrompt)}
|
||||
className="flex items-center gap-2 text-sm transition-colors"
|
||||
style={{ color: '#60a5fa' }}
|
||||
>
|
||||
<span className="font-semibold">
|
||||
📥 {t('inputPrompt', language)}
|
||||
</span>
|
||||
<span className="text-xs">
|
||||
{showInputPrompt ? t('collapse', language) : t('expand', language)}
|
||||
</span>
|
||||
</button>
|
||||
{showInputPrompt && (
|
||||
<div
|
||||
className="mt-2 rounded 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>
|
||||
)}
|
||||
|
||||
{decision.cot_trace && (
|
||||
<div className="mb-3">
|
||||
<button
|
||||
onClick={() => setShowCoT(!showCoT)}
|
||||
className="flex items-center gap-2 text-sm transition-colors"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
<span className="font-semibold">
|
||||
📤 {t('aiThinking', language)}
|
||||
</span>
|
||||
<span className="text-xs">
|
||||
{showCoT ? t('collapse', language) : t('expand', language)}
|
||||
</span>
|
||||
</button>
|
||||
{showCoT && (
|
||||
<div
|
||||
className="mt-2 rounded 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>
|
||||
)}
|
||||
|
||||
{/* Decision Actions - Beautiful Grid */}
|
||||
{decision.decisions && decision.decisions.length > 0 && (
|
||||
<div className="space-y-2 mb-3">
|
||||
<div className="space-y-3 mb-4">
|
||||
{decision.decisions.map((action, index) => (
|
||||
<div
|
||||
key={`${action.symbol}-${index}`}
|
||||
className="flex items-center gap-2 text-sm rounded px-3 py-2"
|
||||
style={{ background: '#0B0E11' }}
|
||||
>
|
||||
<span
|
||||
className="font-mono font-bold"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{action.symbol}
|
||||
</span>
|
||||
<span
|
||||
className="px-2 py-0.5 rounded text-xs font-bold"
|
||||
style={
|
||||
action.action.includes('open')
|
||||
? {
|
||||
background: 'rgba(96, 165, 250, 0.1)',
|
||||
color: '#60a5fa',
|
||||
}
|
||||
: action.action.includes('close')
|
||||
? {
|
||||
background: 'rgba(14, 203, 129, 0.1)',
|
||||
color: '#0ECB81',
|
||||
}
|
||||
: action.action === 'wait' || action.action === 'hold'
|
||||
? {
|
||||
background: 'rgba(132, 142, 156, 0.1)',
|
||||
color: '#848E9C',
|
||||
}
|
||||
: {
|
||||
background: 'rgba(248, 113, 113, 0.1)',
|
||||
color: '#F87171',
|
||||
}
|
||||
}
|
||||
>
|
||||
{action.action}
|
||||
</span>
|
||||
{action.reasoning && (
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: '#848E9C', flex: 1 }}
|
||||
>
|
||||
{action.reasoning}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<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 p-3 text-xs font-mono space-y-1"
|
||||
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) => (
|
||||
@@ -165,9 +350,10 @@ export function DecisionCard({ decision, language }: DecisionCardProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{decision.error_message && (
|
||||
<div
|
||||
className="rounded p-3 mt-3 text-sm"
|
||||
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)',
|
||||
|
||||
Reference in New Issue
Block a user