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 ? (
-
-
-
-
- |
- {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)}
- |
-
-
-
- {positions.map((pos, i) => (
-
- |
-
- |
-
-
- {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 @@
+///