diff --git a/api/server.go b/api/server.go index f5cfd4e1..ef652274 100644 --- a/api/server.go +++ b/api/server.go @@ -777,6 +777,9 @@ func (s *Server) handleDeleteTrader(c *gin.Context) { } } + // Remove trader from memory + s.traderManager.RemoveTrader(traderID) + logger.Infof("✓ Trader deleted: %s", traderID) c.JSON(http.StatusOK, gin.H{"message": "Trader deleted"}) } diff --git a/decision/strategy_engine.go b/decision/strategy_engine.go index 28649560..56f6c19d 100644 --- a/decision/strategy_engine.go +++ b/decision/strategy_engine.go @@ -179,11 +179,11 @@ func (e *StrategyEngine) FetchExternalData() (map[string]interface{}, error) { // QuantData quantitative data structure (fund flow, position changes, price changes) type QuantData struct { - Symbol string `json:"symbol"` - Price float64 `json:"price"` - Netflow *NetflowData `json:"netflow,omitempty"` - OI map[string]*OIData `json:"oi,omitempty"` - PriceChange map[string]float64 `json:"price_change,omitempty"` + Symbol string `json:"symbol"` + Price float64 `json:"price"` + Netflow *NetflowData `json:"netflow,omitempty"` + OI map[string]*OIData `json:"oi,omitempty"` + PriceChange map[string]float64 `json:"price_change,omitempty"` } type NetflowData struct { @@ -197,9 +197,9 @@ type FlowTypeData struct { } type OIData struct { - CurrentOI float64 `json:"current_oi"` - NetLong float64 `json:"net_long"` - NetShort float64 `json:"net_short"` + CurrentOI float64 `json:"current_oi"` + NetLong float64 `json:"net_long"` + NetShort float64 `json:"net_short"` Delta map[string]*OIDeltaData `json:"delta,omitempty"` } @@ -242,7 +242,7 @@ func (e *StrategyEngine) FetchQuantData(symbol string) (*QuantData, error) { // Parse response var apiResp struct { - Code int `json:"code"` + Code int `json:"code"` Data *QuantData `json:"data"` } @@ -285,84 +285,85 @@ func (e *StrategyEngine) formatQuantData(data *QuantData) string { return "" } + indicators := e.config.Indicators + // If both OI and Netflow are disabled, return empty + if !indicators.EnableQuantOI && !indicators.EnableQuantNetflow { + return "" + } + var sb strings.Builder sb.WriteString("📊 Quantitative Data:\n") - // Price changes + // Price changes (API returns decimals, multiply by 100 for percentage) if len(data.PriceChange) > 0 { sb.WriteString("Price Change: ") - timeframes := []string{"5m", "15m", "1h", "4h", "24h"} + timeframes := []string{"5m", "15m", "1h", "4h", "12h", "24h"} parts := []string{} for _, tf := range timeframes { if v, ok := data.PriceChange[tf]; ok { - parts = append(parts, fmt.Sprintf("%s: %+.2f%%", tf, v)) + parts = append(parts, fmt.Sprintf("%s: %+.4f%%", tf, v*100)) } } sb.WriteString(strings.Join(parts, " | ")) sb.WriteString("\n") } - // Fund flow - if data.Netflow != nil { - sb.WriteString("Fund Flow (USDT):\n") + // Fund flow (Netflow) - only show if enabled + if indicators.EnableQuantNetflow && data.Netflow != nil { + sb.WriteString("Fund Flow (Netflow):\n") + timeframes := []string{"5m", "15m", "1h", "4h", "12h", "24h"} // Institutional funds if data.Netflow.Institution != nil { - if data.Netflow.Institution.Future != nil { - sb.WriteString(" Institutional Futures: ") - parts := []string{} - for _, tf := range []string{"1h", "4h", "24h"} { + if data.Netflow.Institution.Future != nil && len(data.Netflow.Institution.Future) > 0 { + sb.WriteString(" Institutional Futures:\n") + for _, tf := range timeframes { if v, ok := data.Netflow.Institution.Future[tf]; ok { - parts = append(parts, fmt.Sprintf("%s: %+.0f", tf, v)) + sb.WriteString(fmt.Sprintf(" %s: %s\n", tf, formatFlowValue(v))) } } - sb.WriteString(strings.Join(parts, " | ")) - sb.WriteString("\n") } - if data.Netflow.Institution.Spot != nil { - sb.WriteString(" Institutional Spot: ") - parts := []string{} - for _, tf := range []string{"1h", "4h", "24h"} { + if data.Netflow.Institution.Spot != nil && len(data.Netflow.Institution.Spot) > 0 { + sb.WriteString(" Institutional Spot:\n") + for _, tf := range timeframes { if v, ok := data.Netflow.Institution.Spot[tf]; ok { - parts = append(parts, fmt.Sprintf("%s: %+.0f", tf, v)) + sb.WriteString(fmt.Sprintf(" %s: %s\n", tf, formatFlowValue(v))) } } - sb.WriteString(strings.Join(parts, " | ")) - sb.WriteString("\n") } } // Retail funds if data.Netflow.Personal != nil { - if data.Netflow.Personal.Future != nil { - sb.WriteString(" Retail Futures: ") - parts := []string{} - for _, tf := range []string{"1h", "4h", "24h"} { + if data.Netflow.Personal.Future != nil && len(data.Netflow.Personal.Future) > 0 { + sb.WriteString(" Retail Futures:\n") + for _, tf := range timeframes { if v, ok := data.Netflow.Personal.Future[tf]; ok { - parts = append(parts, fmt.Sprintf("%s: %+.0f", tf, v)) + sb.WriteString(fmt.Sprintf(" %s: %s\n", tf, formatFlowValue(v))) + } + } + } + if data.Netflow.Personal.Spot != nil && len(data.Netflow.Personal.Spot) > 0 { + sb.WriteString(" Retail Spot:\n") + for _, tf := range timeframes { + if v, ok := data.Netflow.Personal.Spot[tf]; ok { + sb.WriteString(fmt.Sprintf(" %s: %s\n", tf, formatFlowValue(v))) } } - sb.WriteString(strings.Join(parts, " | ")) - sb.WriteString("\n") } } } - // Position data - if len(data.OI) > 0 { + // Open Interest (OI) - only show if enabled + if indicators.EnableQuantOI && len(data.OI) > 0 { for exchange, oiData := range data.OI { - sb.WriteString(fmt.Sprintf("Open Interest (%s): Current %.2f | Long %.2f Short %.2f\n", - exchange, oiData.CurrentOI, oiData.NetLong, oiData.NetShort)) if len(oiData.Delta) > 0 { - sb.WriteString(" OI Change: ") - parts := []string{} - for _, tf := range []string{"1h", "4h", "24h"} { + sb.WriteString(fmt.Sprintf("Open Interest (%s):\n", exchange)) + for _, tf := range []string{"5m", "15m", "1h", "4h", "12h", "24h"} { if d, ok := oiData.Delta[tf]; ok { - parts = append(parts, fmt.Sprintf("%s: %+.2f%%", tf, d.OIDeltaPercent)) + sb.WriteString(fmt.Sprintf(" %s: %+.4f%% (%s)\n", tf, d.OIDeltaPercent, formatFlowValue(d.OIDeltaValue))) } } - sb.WriteString(strings.Join(parts, " | ")) - sb.WriteString("\n") } } } @@ -760,6 +761,26 @@ func (e *StrategyEngine) formatTimeframeSeriesData(sb *strings.Builder, data *ma sb.WriteString("\n") } +// formatFlowValue formats flow value with M/K units +func formatFlowValue(v float64) string { + sign := "" + if v >= 0 { + sign = "+" + } + absV := v + if absV < 0 { + absV = -absV + } + if absV >= 1e9 { + return fmt.Sprintf("%s%.2fB", sign, v/1e9) + } else if absV >= 1e6 { + return fmt.Sprintf("%s%.2fM", sign, v/1e6) + } else if absV >= 1e3 { + return fmt.Sprintf("%s%.2fK", sign, v/1e3) + } + return fmt.Sprintf("%s%.2f", sign, v) +} + // formatFloatSlice formats float slice func formatFloatSlice(values []float64) string { strValues := make([]string, len(values)) diff --git a/store/strategy.go b/store/strategy.go index 2e504519..f78fc320 100644 --- a/store/strategy.go +++ b/store/strategy.go @@ -94,8 +94,10 @@ type IndicatorConfig struct { // external data sources ExternalDataSources []ExternalDataSource `json:"external_data_sources,omitempty"` // quantitative data sources (capital flow, position changes, price changes) - EnableQuantData bool `json:"enable_quant_data"` // whether to enable quantitative data - QuantDataAPIURL string `json:"quant_data_api_url,omitempty"` // quantitative data API address + EnableQuantData bool `json:"enable_quant_data"` // whether to enable quantitative data + QuantDataAPIURL string `json:"quant_data_api_url,omitempty"` // quantitative data API address + EnableQuantOI bool `json:"enable_quant_oi"` // whether to show OI data + EnableQuantNetflow bool `json:"enable_quant_netflow"` // whether to show Netflow data } // KlineConfig K-line configuration @@ -216,8 +218,10 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig { EMAPeriods: []int{20, 50}, RSIPeriods: []int{7, 14}, ATRPeriods: []int{14}, - EnableQuantData: true, - QuantDataAPIURL: "http://nofxaios.com:30006/api/coin/{symbol}?include=netflow,oi,price&auth=cm_568c67eae410d912c54c", + EnableQuantData: true, + QuantDataAPIURL: "http://nofxaios.com:30006/api/coin/{symbol}?include=netflow,oi,price&auth=cm_568c67eae410d912c54c", + EnableQuantOI: true, + EnableQuantNetflow: true, }, RiskControl: RiskControlConfig{ MaxPositions: 3, diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index f07ee7dd..7c02f961 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -15,10 +15,6 @@ import { getExchangeIcon } from './ExchangeIcons' import { getModelIcon } from './ModelIcons' import { TraderConfigModal } from './TraderConfigModal' import { PunkAvatar, getTraderAvatar } from './PunkAvatar' -import { - TwoStageKeyModal, - type TwoStageKeyModalResult, -} from './TwoStageKeyModal' import { WebCryptoEnvironmentCheck, type WebCryptoCheckStatus, @@ -1603,11 +1599,6 @@ function ExchangeConfigModal({ const [lighterPrivateKey, setLighterPrivateKey] = useState('') const [lighterApiKeyPrivateKey, setLighterApiKeyPrivateKey] = useState('') - // 安全输入状态 - const [secureInputTarget, setSecureInputTarget] = useState< - null | 'hyperliquid' | 'aster' | 'lighter' - >(null) - // 获取当前编辑的交易所信息 const selectedExchange = allExchanges?.find( (e) => e.id === selectedExchangeId @@ -1705,44 +1696,6 @@ function ExchangeConfigModal({ } } - // 安全输入处理函数 - const secureInputContextLabel = - secureInputTarget === 'aster' - ? t('asterExchangeName', language) - : secureInputTarget === 'hyperliquid' - ? t('hyperliquidExchangeName', language) - : undefined - - const handleSecureInputCancel = () => { - setSecureInputTarget(null) - } - - const handleSecureInputComplete = ({ - value, - obfuscationLog, - }: TwoStageKeyModalResult) => { - const trimmed = value.trim() - if (secureInputTarget === 'hyperliquid') { - setApiKey(trimmed) - } - if (secureInputTarget === 'aster') { - setAsterPrivateKey(trimmed) - } - console.log('Secure input obfuscation log:', obfuscationLog) - setSecureInputTarget(null) - } - - // 掩盖敏感数据显示 - const maskSecret = (secret: string) => { - if (!secret || secret.length === 0) return '' - if (secret.length <= 8) return '*'.repeat(secret.length) - return ( - secret.slice(0, 4) + - '*'.repeat(Math.max(secret.length - 8, 4)) + - secret.slice(-4) - ) - } - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!selectedExchangeId) return @@ -2328,58 +2281,22 @@ function ExchangeConfigModal({ > {t('hyperliquidAgentPrivateKey', language)} -
-
- - - {apiKey && ( - - )} -
- {apiKey && ( -
- {t('secureInputHint', language)} -
+ setApiKey(e.target.value)} + placeholder={t( + 'enterHyperliquidAgentPrivateKey', + language )} -
+ className="w-full px-3 py-2 rounded" + style={{ + background: '#0B0E11', + border: '1px solid #2B3139', + color: '#EAECEF', + }} + required + />
)} - {/* Two Stage Key Modal */} -
) } diff --git a/web/src/components/CryptoFeatureCard.tsx b/web/src/components/CryptoFeatureCard.tsx deleted file mode 100644 index d206d8ae..00000000 --- a/web/src/components/CryptoFeatureCard.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import * as React from 'react' -import { motion } from 'framer-motion' -import { Check } from 'lucide-react' -import { cn } from '../lib/utils' - -interface CryptoFeatureCardProps { - icon: React.ReactNode - title: string - description: string - features: string[] - className?: string - delay?: number -} - -export const CryptoFeatureCard = React.forwardRef< - HTMLDivElement, - CryptoFeatureCardProps ->(({ icon, title, description, features, className, delay = 0 }, ref) => { - const [isHovered, setIsHovered] = React.useState(false) - - return ( - setIsHovered(true)} - onHoverEnd={() => setIsHovered(false)} - className="relative h-full" - > -
- {/* Animated glow border effect */} - -
- - - {/* Background pattern */} -
-
-
- -
- {/* Icon container */} - -
{icon}
-
- - {/* Title */} -

- {title} -

- - {/* Description */} -

- {description} -

- - {/* Features list */} -
- {features.map((feature, index) => ( - -
-
- -
-
- - {feature} - -
- ))} -
-
-
- - ) -}) - -CryptoFeatureCard.displayName = 'CryptoFeatureCard' diff --git a/web/src/components/DevToastController.tsx b/web/src/components/DevToastController.tsx deleted file mode 100644 index 912e3723..00000000 --- a/web/src/components/DevToastController.tsx +++ /dev/null @@ -1,116 +0,0 @@ -/// - -import { useState } from 'react' -import { confirmToast, notify } from '../lib/notify' - -const toastOptions = [ - 'message', - 'success', - 'info', - 'warning', - 'error', - 'custom', -] as const - -type ToastType = (typeof toastOptions)[number] - -const customRenderer = () => ( -
-

Sonner 自定义通知

-

- 这是一个通过 `notify.custom` 渲染的测试 Toast -

-
-) - -export function DevToastController() { - const [type, setType] = useState('success') - const [message, setMessage] = useState('来自 Dev 控制器的测试通知') - const [duration, setDuration] = useState(2200) - - if (!import.meta.env.DEV) { - return null - } - - const triggerToast = async () => { - switch (type) { - case 'message': - notify.message(message, { duration }) - break - case 'success': - notify.success(message, { duration }) - break - case 'info': - notify.info(message, { duration }) - break - case 'warning': - notify.warning(message, { duration }) - break - case 'error': - notify.error(message, { duration }) - break - case 'custom': - notify.custom(() => customRenderer(), { duration }) - break - } - } - - const triggerConfirm = async () => { - const confirmed = await confirmToast(message, { - okText: '继续', - cancelText: '取消', - }) - if (confirmed) { - notify.success('确认按钮已点击', { duration: 2000 }) - } else { - notify.message('已取消确认逻辑', { duration: 2000 }) - } - } - - return ( -
-
- Dev Sonner 控制器 - 仅在 dev 模式可见 -
-
- - - -
- - -
-
-
- ) -} - -export default DevToastController diff --git a/web/src/components/TraderConfigViewModal.tsx b/web/src/components/TraderConfigViewModal.tsx index 0dfcdc59..ade98677 100644 --- a/web/src/components/TraderConfigViewModal.tsx +++ b/web/src/components/TraderConfigViewModal.tsx @@ -1,5 +1,3 @@ -import { useState } from 'react' -import { toast } from 'sonner' import type { TraderConfigData } from '../types' import { PunkAvatar, getTraderAvatar } from './PunkAvatar' @@ -20,66 +18,20 @@ export function TraderConfigViewModal({ onClose, traderData, }: TraderConfigViewModalProps) { - const [copiedField, setCopiedField] = useState(null) - if (!isOpen || !traderData) return null - const copyToClipboard = async (text: string, fieldName: string) => { - try { - await navigator.clipboard.writeText(text) - setCopiedField(fieldName) - setTimeout(() => setCopiedField(null), 2000) - toast.success('已复制到剪贴板') - } catch (error) { - console.error('Failed to copy:', error) - toast.error('复制失败,请手动复制') - } - } - - const CopyButton = ({ - text, - fieldName, - }: { - text: string - fieldName: string - }) => ( - - ) - const InfoRow = ({ label, value, - copyable = false, - fieldName = '', }: { label: string value: string | number | boolean - copyable?: boolean - fieldName?: string }) => (
{label} -
- - {typeof value === 'boolean' ? (value ? '是' : '否') : value} - - {copyable && typeof value === 'string' && value && ( - - )} -
+ + {typeof value === 'boolean' ? (value ? '是' : '否') : value} +
) @@ -134,17 +86,9 @@ export function TraderConfigViewModal({ 🤖 基础信息
- -
-
- - {/* Trading Configuration */} -
-

- ⚖️ 交易配置 -

-
- -
- {/* Signal Sources */} -
-

- 📡 信号源配置 -

-
- - -
-
- - {/* Custom Prompt */} -
-
-

- 💬 交易策略提示词 + {/* Strategy Info - only show if strategy is bound */} + {traderData.strategy_id && ( +
+

+ 📋 使用策略

- {traderData.custom_prompt && ( - + - )} +

-
- - {traderData.custom_prompt ? ( -
-
- {traderData.override_base_prompt - ? '自定义提示词' - : '附加提示词'} - : -
-
- {traderData.custom_prompt} -
-
- ) : ( -
- 未设置自定义提示词,使用系统默认策略 -
- )} -
-
+ )}
{/* Footer */} -
+
-
diff --git a/web/src/components/Typewriter.tsx b/web/src/components/Typewriter.tsx deleted file mode 100644 index 5c3a29aa..00000000 --- a/web/src/components/Typewriter.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from 'react' - -interface TypewriterProps { - lines: string[] - typingSpeed?: number // 毫秒/字符 - lineDelay?: number // 每行结束的额外等待 - className?: string - style?: React.CSSProperties -} - -export default function Typewriter({ - lines, - typingSpeed = 50, - lineDelay = 600, - className, - style, -}: TypewriterProps) { - const [typedLines, setTypedLines] = useState(['']) - const [showCursor, setShowCursor] = useState(true) - const lineIndexRef = useRef(0) - const charIndexRef = useRef(0) - const timerRef = useRef(null) - const blinkRef = useRef(null) - const sanitizedLines = useMemo( - () => lines.map((l) => String(l ?? '')), - [lines] - ) - - useEffect(() => { - // 重置状态 - lineIndexRef.current = 0 - charIndexRef.current = 0 - setTypedLines(['']) - - function typeNext() { - const currentLine = sanitizedLines[lineIndexRef.current] ?? '' - if (charIndexRef.current < currentLine.length) { - const ch = currentLine.charAt(charIndexRef.current) - setTypedLines((prev) => { - const next = [...prev] - const lastIndex = next.length - 1 - next[lastIndex] = (next[lastIndex] ?? '') + ch - return next - }) - charIndexRef.current += 1 - timerRef.current = window.setTimeout(typeNext, typingSpeed) - } else { - // 行结束 - if (lineIndexRef.current < sanitizedLines.length - 1) { - lineIndexRef.current += 1 - charIndexRef.current = 0 - setTypedLines((prev) => [...prev, '']) - timerRef.current = window.setTimeout(typeNext, lineDelay) - } else { - // 最后一行输入完毕 - timerRef.current = null - } - } - } - - // 延迟一帧开始打字,确保状态已重置 - timerRef.current = window.setTimeout(typeNext, 0) - - // 光标闪烁 - blinkRef.current = window.setInterval(() => { - setShowCursor((v) => !v) - }, 500) - - return () => { - if (timerRef.current) window.clearTimeout(timerRef.current) - if (blinkRef.current) window.clearInterval(blinkRef.current) - } - }, [sanitizedLines, typingSpeed, lineDelay]) - - const displayText = useMemo( - () => typedLines.join('\n').replace(/undefined/g, ''), - [typedLines] - ) - - return ( -
-      {displayText}
-      
-    
- ) -} diff --git a/web/src/components/strategy/IndicatorEditor.tsx b/web/src/components/strategy/IndicatorEditor.tsx index 01353ce4..760ca47b 100644 --- a/web/src/components/strategy/IndicatorEditor.tsx +++ b/web/src/components/strategy/IndicatorEditor.tsx @@ -428,6 +428,30 @@ export function IndicatorEditor({ style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }} />

{t('symbolPlaceholder')}

+ + {/* OI and Netflow toggles */} +
+ + +
)} diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index 922872c9..c94de900 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -1339,8 +1339,7 @@ export const translations = { '使用代理钱包安全交易:代理钱包用于签名(餘額~0),主钱包持有资金(永不暴露私钥)', hyperliquidAgentPrivateKey: '代理私钥', enterHyperliquidAgentPrivateKey: '输入代理钱包私钥', - hyperliquidAgentPrivateKeyDesc: - '代理钱包私钥,用于签名交易(为了安全应保持余额接近0)', + hyperliquidAgentPrivateKeyDesc: '代理钱包仅有交易权限,无法提现', hyperliquidMainWalletAddress: '主钱包地址', enterHyperliquidMainWalletAddress: '输入主钱包地址', hyperliquidMainWalletAddressDesc: diff --git a/web/src/layouts/AuthLayout.tsx b/web/src/layouts/AuthLayout.tsx deleted file mode 100644 index b86bf270..00000000 --- a/web/src/layouts/AuthLayout.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { ReactNode } from 'react' -import { Outlet, Link } from 'react-router-dom' -import { Container } from '../components/Container' -import { useLanguage } from '../contexts/LanguageContext' - -interface AuthLayoutProps { - children?: ReactNode -} - -export default function AuthLayout({ children }: AuthLayoutProps) { - const { language, setLanguage } = useLanguage() - - return ( -
- {/* Simple Header with Logo and Language Selector */} - - - {/* Content with top padding to avoid overlap with fixed header */} -
{children || }
-
- ) -} diff --git a/web/src/layouts/MainLayout.tsx b/web/src/layouts/MainLayout.tsx deleted file mode 100644 index ad806a3b..00000000 --- a/web/src/layouts/MainLayout.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { ReactNode } from 'react' -import { Outlet, useLocation } from 'react-router-dom' -import HeaderBar from '../components/HeaderBar' -import { Container } from '../components/Container' -import { useLanguage } from '../contexts/LanguageContext' -import { useAuth } from '../contexts/AuthContext' -import { t } from '../i18n/translations' -import { OFFICIAL_LINKS } from '../constants/branding' - -interface MainLayoutProps { - children?: ReactNode -} - -export default function MainLayout({ children }: MainLayoutProps) { - const { language, setLanguage } = useLanguage() - const { user, logout } = useAuth() - const location = useLocation() - - // 根据路径自动判断当前页面 - const getCurrentPage = (): 'competition' | 'traders' | 'trader' | 'faq' => { - if (location.pathname === '/faq') return 'faq' - if (location.pathname === '/traders') return 'traders' - if (location.pathname === '/dashboard') return 'trader' - if (location.pathname === '/competition') return 'competition' - return 'competition' // 默认 - } - - return ( -
- { - // React Router handles navigation now - }} - /> - - {/* Main Content */} - - {children || } - - - {/* Footer */} - -
- ) -} diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts deleted file mode 100644 index fed2fe91..00000000 --- a/web/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx, type ClassValue } from 'clsx' -import { twMerge } from 'tailwind-merge' - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} diff --git a/web/src/pages/AITradersPage.tsx b/web/src/pages/AITradersPage.tsx deleted file mode 100644 index be7cc91f..00000000 --- a/web/src/pages/AITradersPage.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import { useEffect } from 'react' -import { useNavigate } from 'react-router-dom' -import useSWR from 'swr' -import { api } from '../lib/api' -import { useLanguage } from '../contexts/LanguageContext' -import { useAuth } from '../contexts/AuthContext' -import { useTradersConfigStore, useTradersModalStore } from '../stores' -import { useTraderActions } from '../hooks/useTraderActions' -import { TraderConfigModal } from '../components/TraderConfigModal' -import { - ModelConfigModal, - ExchangeConfigModal, -} from '../components/traders' -import { PageHeader } from '../components/traders/sections/PageHeader' -import { AIModelsSection } from '../components/traders/sections/AIModelsSection' -import { ExchangesSection } from '../components/traders/sections/ExchangesSection' -import { TradersGrid } from '../components/traders/sections/TradersGrid' - -interface AITradersPageProps { - onTraderSelect?: (traderId: string) => void -} - -export function AITradersPage({ onTraderSelect }: AITradersPageProps) { - const { language } = useLanguage() - const { user, token } = useAuth() - const navigate = useNavigate() - - // Zustand stores - const { - allModels, - allExchanges, - supportedModels, - supportedExchanges, - configuredModels, - configuredExchanges, - loadConfigs, - setAllModels, - setAllExchanges, - } = useTradersConfigStore() - - const { - showCreateModal, - showEditModal, - showModelModal, - showExchangeModal, - editingModel, - editingExchange, - editingTrader, - setShowCreateModal, - setShowEditModal, - setShowModelModal, - setShowExchangeModal, - setEditingModel, - setEditingExchange, - setEditingTrader, - } = useTradersModalStore() - - // SWR for traders data - const { data: traders, mutate: mutateTraders } = useSWR( - user && token ? 'traders' : null, - api.getTraders, - { refreshInterval: 5000 } - ) - - // Load configurations - useEffect(() => { - loadConfigs(user, token) - }, [user, token, loadConfigs]) - - // Business logic hook - const { - isModelInUse, - isExchangeInUse, - handleCreateTrader, - handleEditTrader, - handleSaveEditTrader, - handleDeleteTrader, - handleToggleTrader, - handleAddModel, - handleAddExchange, - handleModelClick, - handleExchangeClick, - handleSaveModel, - handleDeleteModel, - handleSaveExchange, - handleDeleteExchange, - } = useTraderActions({ - traders, - allModels, - allExchanges, - supportedModels, - supportedExchanges, - language, - mutateTraders, - setAllModels, - setAllExchanges, - setShowCreateModal, - setShowEditModal, - setShowModelModal, - setShowExchangeModal, - setEditingModel, - setEditingExchange, - editingTrader, - setEditingTrader, - }) - - // 计算派生状态 - const enabledModels = allModels?.filter((m) => m.enabled) || [] - const enabledExchanges = - allExchanges?.filter((e) => { - if (!e.enabled) return false - if (e.id === 'aster') { - return e.asterUser?.trim() && e.asterSigner?.trim() - } - if (e.id === 'hyperliquid') { - return e.hyperliquidWalletAddr?.trim() - } - return true - }) || [] - - // 处理交易员查看 - const handleTraderSelect = (traderId: string) => { - if (onTraderSelect) { - onTraderSelect(traderId) - } else { - navigate(`/dashboard?trader=${traderId}`) - } - } - - return ( -
- {/* Header */} - setShowCreateModal(true)} - /> - - {/* Configuration Status */} -
- - - -
- - {/* Traders Grid */} - - - {/* Modals */} - setShowCreateModal(false)} - isEditMode={false} - availableModels={enabledModels} - availableExchanges={enabledExchanges} - onSave={handleCreateTrader} - /> - - setShowEditModal(false)} - isEditMode={true} - traderData={editingTrader} - availableModels={enabledModels} - availableExchanges={enabledExchanges} - onSave={handleSaveEditTrader} - /> - - {showModelModal && ( - setShowModelModal(false)} - language={language} - /> - )} - - {showExchangeModal && ( - setShowExchangeModal(false)} - language={language} - /> - )} -
- ) -} diff --git a/web/src/pages/StrategyStudioPage.tsx b/web/src/pages/StrategyStudioPage.tsx index d83241d6..ec4ba30f 100644 --- a/web/src/pages/StrategyStudioPage.tsx +++ b/web/src/pages/StrategyStudioPage.tsx @@ -26,6 +26,8 @@ import { Terminal, Code, Send, + Download, + Upload, } from 'lucide-react' import type { Strategy, StrategyConfig, AIModel } from '../types' import { confirmToast, notify } from '../lib/notify' @@ -263,6 +265,67 @@ export function StrategyStudioPage() { } } + // Export strategy as JSON file + const handleExportStrategy = (strategy: Strategy) => { + const exportData = { + name: strategy.name, + description: strategy.description, + config: strategy.config, + exported_at: new Date().toISOString(), + version: '1.0', + } + const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `strategy_${strategy.name.replace(/\s+/g, '_')}_${new Date().toISOString().split('T')[0]}.json` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + notify.success(language === 'zh' ? '策略已导出' : 'Strategy exported') + } + + // Import strategy from JSON file + const handleImportStrategy = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (!file || !token) return + + try { + const text = await file.text() + const importData = JSON.parse(text) + + // Validate imported data + if (!importData.config || !importData.name) { + throw new Error(language === 'zh' ? '无效的策略文件' : 'Invalid strategy file') + } + + // Create new strategy with imported config + const response = await fetch(`${API_BASE}/api/strategies`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + name: `${importData.name} (${language === 'zh' ? '导入' : 'Imported'})`, + description: importData.description || '', + config: importData.config, + }), + }) + if (!response.ok) throw new Error('Failed to import strategy') + + notify.success(language === 'zh' ? '策略已导入' : 'Strategy imported') + await fetchStrategies() + } catch (err) { + const errorMsg = err instanceof Error ? err.message : 'Unknown error' + notify.error(errorMsg) + } finally { + // Reset file input + event.target.value = '' + } + } + // Save strategy const handleSaveStrategy = async () => { if (!token || !selectedStrategy || !editingConfig) return @@ -526,13 +589,26 @@ export function StrategyStudioPage() {
{t('strategies')} - +
+ {/* Import button with hidden file input */} + + +
{strategies.map((strategy) => ( @@ -554,22 +630,33 @@ export function StrategyStudioPage() { >
{strategy.name} - {!strategy.is_default && ( -
- - -
- )} +
+ + {!strategy.is_default && ( + <> + + + + )} +
{strategy.is_active && ( diff --git a/web/src/pages/TraderDashboard.tsx b/web/src/pages/TraderDashboard.tsx deleted file mode 100644 index 94f17921..00000000 --- a/web/src/pages/TraderDashboard.tsx +++ /dev/null @@ -1,1075 +0,0 @@ -import { useEffect, useState } from 'react' -import { useNavigate, useSearchParams } from 'react-router-dom' -import useSWR, { mutate } from 'swr' -import { api } from '../lib/api' -import { ChartTabs } from '../components/ChartTabs' -import { useLanguage } from '../contexts/LanguageContext' -import { useAuth } from '../contexts/AuthContext' -import { t, type Language } from '../i18n/translations' -import { - AlertTriangle, - Brain, - RefreshCw, - TrendingUp, - PieChart, - Inbox, - Send, - Check, - X, - XCircle, - LogOut, - Loader2, -} from 'lucide-react' -import { PunkAvatar, getTraderAvatar } from '../components/PunkAvatar' -import { stripLeadingIcons } from '../lib/text' -import { confirmToast, notify } from '../lib/notify' -import type { - SystemStatus, - AccountInfo, - Position, - DecisionRecord, - Statistics, - TraderInfo, -} from '../types' - -// 获取友好的AI模型名称 -function getModelDisplayName(modelId: string): string { - switch (modelId.toLowerCase()) { - case 'deepseek': - return 'DeepSeek' - case 'qwen': - return 'Qwen' - case 'claude': - return 'Claude' - default: - return modelId.toUpperCase() - } -} - -export default function TraderDashboard() { - const { language } = useLanguage() - const { user, token } = useAuth() - const navigate = useNavigate() - const [searchParams, setSearchParams] = useSearchParams() - const [selectedTraderId, setSelectedTraderId] = useState( - searchParams.get('trader') || undefined - ) - const [lastUpdate, setLastUpdate] = useState('--:--:--') - - // 决策记录数量选择(从 localStorage 读取,默认 5) - const [decisionLimit, setDecisionLimit] = useState(() => { - const saved = localStorage.getItem('decisionLimit') - return saved ? parseInt(saved, 10) : 5 - }) - - // 选中的币种(用于图表显示) - const [selectedChartSymbol, setSelectedChartSymbol] = useState() - const [chartUpdateKey, setChartUpdateKey] = useState(0) - - // 平仓操作状态 - const [closingPosition, setClosingPosition] = useState(null) // symbol being closed - - // 点击持仓币种时调用 - const handlePositionSymbolClick = (symbol: string) => { - setSelectedChartSymbol(symbol) - setChartUpdateKey(prev => prev + 1) // 强制触发更新 - } - - // 当 limit 变化时保存到 localStorage - const handleLimitChange = (newLimit: number) => { - setDecisionLimit(newLimit) - localStorage.setItem('decisionLimit', newLimit.toString()) - } - - // 平仓操作 - const handleClosePosition = async (symbol: string, side: string) => { - if (!selectedTraderId) return - - const confirmMsg = language === 'zh' - ? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?` - : `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?` - - const confirmed = await confirmToast(confirmMsg, { - title: language === 'zh' ? '确认平仓' : 'Confirm Close', - okText: language === 'zh' ? '确认' : 'Confirm', - cancelText: language === 'zh' ? '取消' : 'Cancel', - }) - - if (!confirmed) return - - setClosingPosition(symbol) - try { - await api.closePosition(selectedTraderId, symbol, side) - notify.success(language === 'zh' ? '平仓成功' : 'Position closed successfully') - // 使用 SWR mutate 刷新数据而非重新加载页面 - await Promise.all([ - mutate(`positions-${selectedTraderId}`), - mutate(`account-${selectedTraderId}`), - ]) - } catch (err: unknown) { - const errorMsg = err instanceof Error ? err.message : (language === 'zh' ? '平仓失败' : 'Failed to close position') - notify.error(errorMsg) - } finally { - setClosingPosition(null) - } - } - - // 获取trader列表(仅在用户登录时) - const { data: traders, error: tradersError } = useSWR( - user && token ? 'traders' : null, - api.getTraders, - { - refreshInterval: 10000, - shouldRetryOnError: false, - } - ) - - // 当获取到traders后,设置默认选中第一个 - useEffect(() => { - if (traders && traders.length > 0 && !selectedTraderId) { - const firstTraderId = traders[0].trader_id - setSelectedTraderId(firstTraderId) - setSearchParams({ trader: firstTraderId }) - } - }, [traders, selectedTraderId, setSearchParams]) - - // 更新URL参数 - const handleTraderSelect = (traderId: string) => { - setSelectedTraderId(traderId) - setSearchParams({ trader: traderId }) - } - - // 如果在trader页面,获取该trader的数据 - const { data: status } = useSWR( - user && token && selectedTraderId ? `status-${selectedTraderId}` : null, - () => api.getStatus(selectedTraderId), - { - refreshInterval: 15000, - revalidateOnFocus: false, - dedupingInterval: 10000, - } - ) - - const { data: account } = useSWR( - user && token && selectedTraderId ? `account-${selectedTraderId}` : null, - () => api.getAccount(selectedTraderId), - { - refreshInterval: 15000, - revalidateOnFocus: false, - dedupingInterval: 10000, - } - ) - - const { data: positions } = useSWR( - user && token && selectedTraderId ? `positions-${selectedTraderId}` : null, - () => api.getPositions(selectedTraderId), - { - refreshInterval: 15000, - revalidateOnFocus: false, - dedupingInterval: 10000, - } - ) - - const { data: decisions } = useSWR( - user && token && selectedTraderId - ? `decisions/latest-${selectedTraderId}-${decisionLimit}` - : null, - () => api.getLatestDecisions(selectedTraderId, decisionLimit), - { - refreshInterval: 30000, - revalidateOnFocus: false, - dedupingInterval: 20000, - } - ) - - const { data: stats } = useSWR( - user && token && selectedTraderId ? `statistics-${selectedTraderId}` : null, - () => api.getStatistics(selectedTraderId), - { - refreshInterval: 30000, - revalidateOnFocus: false, - dedupingInterval: 20000, - } - ) - - // Avoid unused variable warning - void stats - - useEffect(() => { - if (account) { - const now = new Date().toLocaleTimeString() - setLastUpdate(now) - } - }, [account]) - - const selectedTrader = traders?.find((t) => t.trader_id === selectedTraderId) - - // If API failed with error, show empty state - if (tradersError) { - return ( -
-
-
- - - -
-

- {t('dashboardEmptyTitle', language)} -

-

- {t('dashboardEmptyDescription', language)} -

- -
-
- ) - } - - // If traders is loaded and empty, show empty state - if (traders && traders.length === 0) { - return ( -
-
-
- - - -
-

- {t('dashboardEmptyTitle', language)} -

-

- {t('dashboardEmptyDescription', language)} -

- -
-
- ) - } - - // If traders is still loading or selectedTrader is not ready, show skeleton - if (!selectedTrader) { - return ( -
-
-
-
-
-
-
-
-
-
- {[1, 2, 3, 4].map((i) => ( -
-
-
-
- ))} -
-
-
-
-
-
- ) - } - - const highlightColor = '#60a5fa' - - return ( -
- {/* Trader Header */} -
-
-

- - {selectedTrader.trader_name} -

- - {/* Trader Selector */} - {traders && traders.length > 0 && ( -
- - {t('switchTrader', language)}: - - -
- )} -
-
- - AI Model:{' '} - - {getModelDisplayName( - selectedTrader.ai_model.split('_').pop() || - selectedTrader.ai_model - )} - - - - - Prompt: {selectedTrader.system_prompt_template || '-'} - - {status && ( - <> - - Cycles: {status.call_count} - - Runtime: {status.runtime_minutes} min - - )} -
-
- - {/* Debug Info */} - {account && ( -
-
- - Last Update: {lastUpdate} | Total Equity:{' '} - {account?.total_equity?.toFixed(2) || '0.00'} | Available:{' '} - {account?.available_balance?.toFixed(2) || '0.00'} | P&L:{' '} - {account?.total_pnl?.toFixed(2) || '0.00'} ( - {account?.total_pnl_pct?.toFixed(2) || '0.00'}%) -
-
- )} - - {/* Account Overview */} -
- 0} - /> - - = 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) || '0.00'} USDT`} - change={account?.total_pnl_pct || 0} - positive={(account?.total_pnl ?? 0) >= 0} - /> - -
- - {/* 主要内容区:左右分屏 */} -
- {/* 左侧:图表 + 持仓 */} -
- {/* Chart Tabs (Equity / K-line) */} -
- -
- - {/* Current Positions */} -
-
-

- - {t('currentPositions', language)} -

- {positions && positions.length > 0 && ( -
- {positions.length} {t('active', language)} -
- )} -
- {positions && positions.length > 0 ? ( -
- - - - - - - - - - - - - - - - - {positions.map((pos, i) => ( - - - - - - - - - - - - - ))} - -
- {t('symbol', language)} - - {t('side', language)} - - {language === 'zh' ? '操作' : 'Action'} - - {t('entryPrice', language)} - - {t('markPrice', language)} - - {t('quantity', language)} - - {t('positionValue', language)} - - {t('leverage', language)} - - {t('unrealizedPnL', language)} - - {t('liqPrice', language)} -
- - - - {t( - pos.side === 'long' ? 'long' : 'short', - language - )} - - - - - {pos.entry_price.toFixed(4)} - - {pos.mark_price.toFixed(4)} - - {pos.quantity.toFixed(4)} - - {(pos.quantity * pos.mark_price).toFixed(2)} USDT - - {pos.leverage}x - - = 0 ? '#0ECB81' : '#F6465D', - fontWeight: 'bold', - }} - > - {pos.unrealized_pnl >= 0 ? '+' : ''} - {pos.unrealized_pnl.toFixed(2)} ( - {pos.unrealized_pnl_pct.toFixed(2)}%) - - - {pos.liquidation_price.toFixed(4)} -
-
- ) : ( -
-
- -
-
- {t('noPositions', language)} -
-
- {t('noActivePositions', language)} -
-
- )} -
-
- - {/* 右侧:Recent Decisions */} -
-
-
-
- -
-
-

- {t('recentDecisions', language)} -

- {decisions && decisions.length > 0 && ( -
- {t('lastCycles', language, { count: decisions.length })} -
- )} -
-
- - {/* 显示数量选择器 */} -
- - {language === 'zh' ? '显示' : 'Show'}: - - - - {language === 'zh' ? '条' : ''} - -
-
- -
- {decisions && decisions.length > 0 ? ( - decisions.map((decision, i) => ( - - )) - ) : ( -
-
- -
-
- {t('noDecisionsYet', language)} -
-
- {t('aiDecisionsWillAppear', language)} -
-
- )} -
-
-
- -
- ) -} - -// Stat Card Component -function StatCard({ - title, - value, - change, - positive, - subtitle, -}: { - title: string - value: string - change?: number - positive?: boolean - subtitle?: string -}) { - return ( -
-
- {title} -
-
- {value} -
- {change !== undefined && ( -
-
- {positive ? '▲' : '▼'} {positive ? '+' : ''} - {change.toFixed(2)}% -
-
- )} - {subtitle && ( -
- {subtitle} -
- )} -
- ) -} - -// Decision Card Component -function DecisionCard({ - decision, - language, -}: { - decision: DecisionRecord - language: Language -}) { - const [showInputPrompt, setShowInputPrompt] = useState(false) - const [showCoT, setShowCoT] = useState(false) - - return ( -
- {/* Header */} -
-
-
- {t('cycle', language)} #{decision.cycle_number} -
-
- {new Date(decision.timestamp).toLocaleString()} -
-
-
- {t(decision.success ? 'success' : 'failed', language)} -
-
- - {/* Input Prompt - Collapsible */} - {decision.input_prompt && ( -
- - {showInputPrompt && ( -
- {decision.input_prompt} -
- )} -
- )} - - {/* AI Chain of Thought - Collapsible */} - {decision.cot_trace && ( -
- - {showCoT && ( -
- {decision.cot_trace} -
- )} -
- )} - - {/* Decisions Actions */} - {decision.decisions && decision.decisions.length > 0 && ( -
- {decision.decisions.map((action, j) => ( -
- - {action.symbol} - - - {action.action} - - {action.leverage > 0 && ( - {action.leverage}x - )} - {action.price > 0 && ( - - @{action.price.toFixed(4)} - - )} - - {action.success ? ( - - ) : ( - - )} - - {action.error && ( - - {action.error} - - )} -
- ))} -
- )} - - {/* Account State Summary */} - {decision.account_state && ( -
- - 净值: {decision.account_state.total_balance.toFixed(2)} USDT - - - 可用: {decision.account_state.available_balance.toFixed(2)} USDT - - - 保证金率: {decision.account_state.margin_used_pct.toFixed(1)}% - - 持仓: {decision.account_state.position_count} - - {t('candidateCoins', language)}:{' '} - {decision.candidate_coins?.length || 0} - -
- )} - - {/* Candidate Coins Warning */} - {decision.candidate_coins && decision.candidate_coins.length === 0 && ( -
- -
-
- {t('candidateCoinsZeroWarning', language)} -
-
-
{t('possibleReasons', language)}
-
    -
  • {t('coinPoolApiNotConfigured', language)}
  • -
  • {t('apiConnectionTimeout', language)}
  • -
  • {t('noCustomCoinsAndApiFailed', language)}
  • -
-
- {t('solutions', language)} -
-
    -
  • {t('setCustomCoinsInConfig', language)}
  • -
  • {t('orConfigureCorrectApiUrl', language)}
  • -
  • {t('orDisableCoinPoolOptions', language)}
  • -
-
-
-
- )} - - {/* Execution Logs */} - {decision.execution_log && decision.execution_log.length > 0 && ( -
- {decision.execution_log.map((log, k) => ( -
- {log} -
- ))} -
- )} - - {/* Error Message */} - {decision.error_message && ( -
- {decision.error_message} -
- )} -
- ) -} diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx deleted file mode 100644 index d0793e48..00000000 --- a/web/src/routes/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { createBrowserRouter, Navigate } from 'react-router-dom' -import MainLayout from '../layouts/MainLayout' -import AuthLayout from '../layouts/AuthLayout' -import { LandingPage } from '../pages/LandingPage' -import { FAQPage } from '../pages/FAQPage' -import { LoginPage } from '../components/LoginPage' -import { RegisterPage } from '../components/RegisterPage' -import { ResetPasswordPage } from '../components/ResetPasswordPage' -import { CompetitionPage } from '../components/CompetitionPage' -import { AITradersPage } from '../pages/AITradersPage' -import TraderDashboard from '../pages/TraderDashboard' - -export const router = createBrowserRouter([ - { - path: '/', - element: , - }, - // Auth routes - using AuthLayout - { - element: , - children: [ - { - path: '/login', - element: , - }, - { - path: '/register', - element: , - }, - { - path: '/reset-password', - element: , - }, - ], - }, - // Main app routes - using MainLayout with nested routes - { - element: , - children: [ - { - path: '/faq', - element: , - }, - { - path: '/competition', - element: , - }, - { - path: '/traders', - element: , - }, - { - path: '/dashboard', - element: , - }, - ], - }, - { - path: '*', - element: , - }, -]) diff --git a/web/src/types.ts b/web/src/types.ts index 102ac847..eac56805 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -205,20 +205,21 @@ export interface TraderConfigData { trader_name: string ai_model: string exchange_id: string - strategy_id?: string // 策略ID(新版) + strategy_id?: string // 策略ID + strategy_name?: string // 策略名称 is_cross_margin: boolean scan_interval_minutes: number initial_balance: number is_running: boolean // 以下为旧版字段(向后兼容) - btc_eth_leverage: number - altcoin_leverage: number - trading_symbols: string - custom_prompt: string - override_base_prompt: boolean - system_prompt_template: string - use_coin_pool: boolean - use_oi_top: boolean + btc_eth_leverage?: number + altcoin_leverage?: number + trading_symbols?: string + custom_prompt?: string + override_base_prompt?: boolean + system_prompt_template?: string + use_coin_pool?: boolean + use_oi_top?: boolean } // Backtest types @@ -411,6 +412,8 @@ export interface IndicatorConfig { // 量化数据源(资金流向、持仓变化、价格变化) enable_quant_data?: boolean; quant_data_api_url?: string; + enable_quant_oi?: boolean; + enable_quant_netflow?: boolean; } export interface KlineConfig { diff --git a/web/src/types/index.ts b/web/src/types/index.ts deleted file mode 100644 index 4f3ca3be..00000000 --- a/web/src/types/index.ts +++ /dev/null @@ -1,93 +0,0 @@ -// 系统状态 -export interface SystemStatus { - is_running: boolean - start_time: string - runtime_minutes: number - call_count: number - initial_balance: number - scan_interval: string - stop_until: string - last_reset_time: string - ai_provider: string -} - -// 账户信息 -export interface AccountInfo { - total_equity: number - available_balance: number - total_pnl: number - total_pnl_pct: number - total_unrealized_pnl: number - margin_used: number - margin_used_pct: number - position_count: number - initial_balance: number - daily_pnl: number -} - -// 持仓信息 -export interface Position { - symbol: string - side: string - entry_price: number - mark_price: number - quantity: number - leverage: number - unrealized_pnl: number - unrealized_pnl_pct: number - liquidation_price: number - margin_used: number -} - -// 决策动作 -export interface DecisionAction { - action: string - symbol: string - quantity: number - leverage: number - price: number - order_id: number - timestamp: string - success: boolean - error: string -} - -// 决策记录 -export interface DecisionRecord { - timestamp: string - cycle_number: number - input_prompt: string - cot_trace: string - decision_json: string - account_state: { - total_balance: number - available_balance: number - total_unrealized_profit: number - position_count: number - margin_used_pct: number - } - positions: Array<{ - symbol: string - side: string - position_amt: number - entry_price: number - mark_price: number - unrealized_profit: number - leverage: number - liquidation_price: number - }> - candidate_coins: string[] - decisions: DecisionAction[] - execution_log: string[] - success: boolean - error_message: string -} - -// 统计信息 -export interface Statistics { - total_cycles: number - successful_cycles: number - failed_cycles: number - total_open_positions: number - total_close_positions: number -} diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1 @@ +///