mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
feat: improve strategy studio and fix trader deletion bug
- Add strategy export/import functionality to Strategy Studio - Fix trader deletion not removing from memory (competition page ghost data) - Simplify TraderConfigViewModal: remove unused fields, show strategy name - Improve quant data formatting: OI/Netflow multi-timeframe display - Add configurable OI/Netflow toggles in indicator settings - Clean up unused frontend components and dead code
This commit is contained in:
@@ -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"})
|
||||
}
|
||||
|
||||
+67
-46
@@ -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))
|
||||
|
||||
+8
-4
@@ -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,
|
||||
|
||||
@@ -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)}
|
||||
</label>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={maskSecret(apiKey)}
|
||||
readOnly
|
||||
placeholder={t(
|
||||
'enterHyperliquidAgentPrivateKey',
|
||||
language
|
||||
)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSecureInputTarget('hyperliquid')}
|
||||
className="px-3 py-2 rounded text-xs font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: '#F0B90B',
|
||||
color: '#000',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{apiKey
|
||||
? t('secureInputReenter', language)
|
||||
: t('secureInputButton', language)}
|
||||
</button>
|
||||
{apiKey && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setApiKey('')}
|
||||
className="px-3 py-2 rounded text-xs font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: '#1B1F2B',
|
||||
color: '#848E9C',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{t('secureInputClear', language)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{apiKey && (
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('secureInputHint', language)}
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={t(
|
||||
'enterHyperliquidAgentPrivateKey',
|
||||
language
|
||||
)}
|
||||
</div>
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
required
|
||||
/>
|
||||
<div
|
||||
className="text-xs mt-1"
|
||||
style={{ color: '#848E9C' }}
|
||||
@@ -2605,15 +2522,6 @@ function ExchangeConfigModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Two Stage Key Modal */}
|
||||
<TwoStageKeyModal
|
||||
isOpen={secureInputTarget !== null}
|
||||
language={language}
|
||||
contextLabel={secureInputContextLabel}
|
||||
expectedLength={64}
|
||||
onCancel={handleSecureInputCancel}
|
||||
onComplete={handleSecureInputComplete}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay }}
|
||||
onHoverStart={() => setIsHovered(true)}
|
||||
onHoverEnd={() => setIsHovered(false)}
|
||||
className="relative h-full"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative h-full overflow-hidden border-2 transition-all duration-300 rounded-xl',
|
||||
'bg-gradient-to-br from-[#000000] to-[#0A0A0A]',
|
||||
'border-[#1A1A1A] hover:border-[#F0B90B]/50',
|
||||
isHovered && 'shadow-[0_0_20px_rgba(240,185,11,0.2)]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Animated glow border effect */}
|
||||
<motion.div
|
||||
className="absolute inset-0 opacity-0 pointer-events-none"
|
||||
animate={{
|
||||
opacity: isHovered ? 1 : 0,
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-[#F0B90B]/20 to-transparent animate-[shimmer_2s_infinite]" />
|
||||
</motion.div>
|
||||
|
||||
{/* Background pattern */}
|
||||
<div className="absolute inset-0 opacity-5">
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(circle at 2px 2px, #F0B90B 1px, transparent 0)`,
|
||||
backgroundSize: '32px 32px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 p-8 flex flex-col h-full">
|
||||
{/* Icon container */}
|
||||
<motion.div
|
||||
className="mb-6 inline-flex items-center justify-center w-16 h-16 rounded-xl"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(240, 185, 11, 0.2) 0%, rgba(240, 185, 11, 0.05) 100%)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.3)',
|
||||
}}
|
||||
animate={{
|
||||
scale: isHovered ? 1.1 : 1,
|
||||
boxShadow: isHovered
|
||||
? '0 0 20px rgba(240, 185, 11, 0.4)'
|
||||
: '0 0 0px rgba(240, 185, 11, 0)',
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div style={{ color: 'var(--brand-yellow)' }}>{icon}</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Title */}
|
||||
<h3
|
||||
className="text-2xl font-bold mb-3"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p
|
||||
className="mb-6 flex-grow leading-relaxed"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{/* Features list */}
|
||||
<div className="space-y-3 mb-6">
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: delay + index * 0.1 }}
|
||||
className="flex items-start gap-3"
|
||||
>
|
||||
<div className="mt-0.5 flex-shrink-0">
|
||||
<div
|
||||
className="w-5 h-5 rounded-full flex items-center justify-center"
|
||||
style={{ background: 'rgba(240, 185, 11, 0.2)' }}
|
||||
>
|
||||
<Check
|
||||
className="w-3 h-3"
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className="text-sm"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{feature}
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})
|
||||
|
||||
CryptoFeatureCard.displayName = 'CryptoFeatureCard'
|
||||
@@ -1,116 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
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 = () => (
|
||||
<div className="dev-custom-toast">
|
||||
<p className="dev-custom-title">Sonner 自定义通知</p>
|
||||
<p className="dev-custom-body">
|
||||
这是一个通过 `notify.custom` 渲染的测试 Toast
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
export function DevToastController() {
|
||||
const [type, setType] = useState<ToastType>('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 (
|
||||
<div className="dev-toast-controller">
|
||||
<div className="dev-toast-controller__header">
|
||||
<span>Dev Sonner 控制器</span>
|
||||
<small>仅在 dev 模式可见</small>
|
||||
</div>
|
||||
<div className="dev-toast-controller__content">
|
||||
<label className="dev-toast-controller__label">
|
||||
类型
|
||||
<select
|
||||
value={type}
|
||||
onChange={(event) => setType(event.target.value as ToastType)}
|
||||
>
|
||||
{toastOptions.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="dev-toast-controller__label">
|
||||
文案
|
||||
<input
|
||||
value={message}
|
||||
onChange={(event) => setMessage(event.target.value)}
|
||||
placeholder="输入通知/确认文案"
|
||||
/>
|
||||
</label>
|
||||
<label className="dev-toast-controller__label">
|
||||
持续(ms)
|
||||
<input
|
||||
type="number"
|
||||
min={600}
|
||||
value={duration}
|
||||
onChange={(event) => setDuration(Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<div className="dev-toast-controller__actions">
|
||||
<button onClick={triggerToast}>触发通知</button>
|
||||
<button onClick={triggerConfirm}>触发确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DevToastController
|
||||
@@ -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<string | null>(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
|
||||
}) => (
|
||||
<button
|
||||
onClick={() => copyToClipboard(text, fieldName)}
|
||||
className="ml-2 px-2 py-1 text-xs rounded transition-all duration-200 hover:scale-105"
|
||||
style={{
|
||||
background:
|
||||
copiedField === fieldName
|
||||
? 'rgba(14, 203, 129, 0.1)'
|
||||
: 'rgba(240, 185, 11, 0.1)',
|
||||
color: copiedField === fieldName ? '#0ECB81' : '#F0B90B',
|
||||
border: `1px solid ${copiedField === fieldName ? 'rgba(14, 203, 129, 0.3)' : 'rgba(240, 185, 11, 0.3)'}`,
|
||||
}}
|
||||
>
|
||||
{copiedField === fieldName ? '✓ 已复制' : '📋 复制'}
|
||||
</button>
|
||||
)
|
||||
|
||||
const InfoRow = ({
|
||||
label,
|
||||
value,
|
||||
copyable = false,
|
||||
fieldName = '',
|
||||
}: {
|
||||
label: string
|
||||
value: string | number | boolean
|
||||
copyable?: boolean
|
||||
fieldName?: string
|
||||
}) => (
|
||||
<div className="flex justify-between items-start py-2 border-b border-[#2B3139] last:border-b-0">
|
||||
<span className="text-sm text-[#848E9C] font-medium">{label}</span>
|
||||
<div className="flex items-center text-right">
|
||||
<span className="text-sm text-[#EAECEF] font-mono">
|
||||
{typeof value === 'boolean' ? (value ? '是' : '否') : value}
|
||||
</span>
|
||||
{copyable && typeof value === 'string' && value && (
|
||||
<CopyButton text={value} fieldName={fieldName} />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm text-[#EAECEF] font-mono text-right">
|
||||
{typeof value === 'boolean' ? (value ? '是' : '否') : value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -134,17 +86,9 @@ export function TraderConfigViewModal({
|
||||
🤖 基础信息
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<InfoRow
|
||||
label="交易员ID"
|
||||
value={traderData.trader_id || ''}
|
||||
copyable
|
||||
fieldName="trader_id"
|
||||
/>
|
||||
<InfoRow
|
||||
label="交易员名称"
|
||||
value={traderData.trader_name}
|
||||
copyable
|
||||
fieldName="trader_name"
|
||||
/>
|
||||
<InfoRow
|
||||
label="AI模型"
|
||||
@@ -158,118 +102,41 @@ export function TraderConfigViewModal({
|
||||
label="初始余额"
|
||||
value={`$${traderData.initial_balance.toLocaleString()}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trading Configuration */}
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
|
||||
⚖️ 交易配置
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<InfoRow
|
||||
label="保证金模式"
|
||||
value={traderData.is_cross_margin ? '全仓' : '逐仓'}
|
||||
/>
|
||||
<InfoRow
|
||||
label="BTC/ETH 杠杆"
|
||||
value={`${traderData.btc_eth_leverage}x`}
|
||||
/>
|
||||
<InfoRow
|
||||
label="山寨币杠杆"
|
||||
value={`${traderData.altcoin_leverage}x`}
|
||||
/>
|
||||
<InfoRow
|
||||
label="交易币种"
|
||||
value={traderData.trading_symbols || '使用默认币种'}
|
||||
copyable
|
||||
fieldName="trading_symbols"
|
||||
label="扫描间隔"
|
||||
value={`${traderData.scan_interval_minutes || 3} 分钟`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Signal Sources */}
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
|
||||
📡 信号源配置
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<InfoRow
|
||||
label="Coin Pool 信号"
|
||||
value={traderData.use_coin_pool}
|
||||
/>
|
||||
<InfoRow label="OI Top 信号" value={traderData.use_oi_top} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Prompt */}
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] flex items-center gap-2">
|
||||
💬 交易策略提示词
|
||||
{/* Strategy Info - only show if strategy is bound */}
|
||||
{traderData.strategy_id && (
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
|
||||
📋 使用策略
|
||||
</h3>
|
||||
{traderData.custom_prompt && (
|
||||
<CopyButton
|
||||
text={traderData.custom_prompt}
|
||||
fieldName="custom_prompt"
|
||||
<div className="space-y-3">
|
||||
<InfoRow
|
||||
label="策略名称"
|
||||
value={traderData.strategy_name || traderData.strategy_id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<InfoRow
|
||||
label="覆盖默认提示词"
|
||||
value={traderData.override_base_prompt}
|
||||
/>
|
||||
{traderData.custom_prompt ? (
|
||||
<div>
|
||||
<div className="text-sm text-[#848E9C] mb-2">
|
||||
{traderData.override_base_prompt
|
||||
? '自定义提示词'
|
||||
: '附加提示词'}
|
||||
:
|
||||
</div>
|
||||
<div
|
||||
className="p-3 rounded border text-sm text-[#EAECEF] font-mono leading-relaxed max-h-48 overflow-y-auto"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{traderData.custom_prompt}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="text-sm text-[#848E9C] italic p-3 rounded border"
|
||||
style={{ border: '1px solid #2B3139' }}
|
||||
>
|
||||
未设置自定义提示词,使用系统默认策略
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end gap-3 p-6 border-t border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35]">
|
||||
<div className="flex justify-end p-6 border-t border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35]">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
JSON.stringify(traderData, null, 2),
|
||||
'full_config'
|
||||
)
|
||||
}
|
||||
className="px-6 py-3 bg-gradient-to-r from-[#F0B90B] to-[#E1A706] text-black rounded-lg hover:from-[#E1A706] hover:to-[#D4951E] transition-all duration-200 font-medium shadow-lg"
|
||||
>
|
||||
{copiedField === 'full_config' ? '✓ 已复制配置' : '📋 复制完整配置'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<string[]>([''])
|
||||
const [showCursor, setShowCursor] = useState(true)
|
||||
const lineIndexRef = useRef(0)
|
||||
const charIndexRef = useRef(0)
|
||||
const timerRef = useRef<number | null>(null)
|
||||
const blinkRef = useRef<number | null>(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 (
|
||||
<pre className={className} style={{ whiteSpace: 'pre-wrap', ...style }}>
|
||||
{displayText}
|
||||
<span style={{ opacity: showCursor ? 1 : 0 }}> ▍</span>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
@@ -428,6 +428,30 @@ export function IndicatorEditor({
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
/>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('symbolPlaceholder')}</p>
|
||||
|
||||
{/* OI and Netflow toggles */}
|
||||
<div className="flex gap-4 mt-3">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_quant_oi !== false}
|
||||
onChange={(e) => !disabled && onChange({ ...config, enable_quant_oi: e.target.checked })}
|
||||
disabled={disabled}
|
||||
className="w-3.5 h-3.5 rounded accent-blue-500"
|
||||
/>
|
||||
<span className="text-xs" style={{ color: '#EAECEF' }}>OI</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_quant_netflow !== false}
|
||||
onChange={(e) => !disabled && onChange({ ...config, enable_quant_netflow: e.target.checked })}
|
||||
disabled={disabled}
|
||||
className="w-3.5 h-3.5 rounded accent-blue-500"
|
||||
/>
|
||||
<span className="text-xs" style={{ color: '#EAECEF' }}>Netflow</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1339,8 +1339,7 @@ export const translations = {
|
||||
'使用代理钱包安全交易:代理钱包用于签名(餘額~0),主钱包持有资金(永不暴露私钥)',
|
||||
hyperliquidAgentPrivateKey: '代理私钥',
|
||||
enterHyperliquidAgentPrivateKey: '输入代理钱包私钥',
|
||||
hyperliquidAgentPrivateKeyDesc:
|
||||
'代理钱包私钥,用于签名交易(为了安全应保持余额接近0)',
|
||||
hyperliquidAgentPrivateKeyDesc: '代理钱包仅有交易权限,无法提现',
|
||||
hyperliquidMainWalletAddress: '主钱包地址',
|
||||
enterHyperliquidMainWalletAddress: '输入主钱包地址',
|
||||
hyperliquidMainWalletAddressDesc:
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen" style={{ background: '#0B0E11' }}>
|
||||
{/* Simple Header with Logo and Language Selector */}
|
||||
<nav
|
||||
className="fixed top-0 w-full z-50"
|
||||
style={{
|
||||
background: 'rgba(11, 14, 17, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
}}
|
||||
>
|
||||
<Container className="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-3 hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<img src="/icons/nofx.svg" alt="NOFX Logo" className="w-8 h-8" />
|
||||
<span className="text-xl font-bold" style={{ color: '#F0B90B' }}>
|
||||
NOFX
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Language Selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setLanguage(language === 'zh' ? 'en' : 'zh')}
|
||||
className="px-3 py-1.5 rounded text-sm font-medium transition-colors"
|
||||
style={{
|
||||
background: '#1E2329',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
>
|
||||
{language === 'zh' ? 'English' : '中文'}
|
||||
</button>
|
||||
</div>
|
||||
</Container>
|
||||
</nav>
|
||||
|
||||
{/* Content with top padding to avoid overlap with fixed header */}
|
||||
<div className="pt-16">{children || <Outlet />}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
className="min-h-screen"
|
||||
style={{ background: '#0B0E11', color: '#EAECEF' }}
|
||||
>
|
||||
<HeaderBar
|
||||
isLoggedIn={!!user}
|
||||
currentPage={getCurrentPage()}
|
||||
language={language}
|
||||
onLanguageChange={setLanguage}
|
||||
user={user}
|
||||
onLogout={logout}
|
||||
onPageChange={() => {
|
||||
// React Router handles navigation now
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Main Content */}
|
||||
<Container as="main" className="py-6 pt-24">
|
||||
{children || <Outlet />}
|
||||
</Container>
|
||||
|
||||
{/* Footer */}
|
||||
<footer
|
||||
className="mt-16"
|
||||
style={{ borderTop: '1px solid #2B3139', background: '#181A20' }}
|
||||
>
|
||||
<Container
|
||||
className="py-6 text-center text-sm"
|
||||
style={{ color: '#5E6673' }}
|
||||
>
|
||||
<p>{t('footerTitle', language)}</p>
|
||||
<p className="mt-1">{t('footerWarning', language)}</p>
|
||||
<div className="mt-4 flex items-center justify-center gap-3 flex-wrap">
|
||||
{/* GitHub */}
|
||||
<a
|
||||
href={OFFICIAL_LINKS.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: '#1E2329',
|
||||
color: '#848E9C',
|
||||
border: '1px solid #2B3139',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#2B3139'
|
||||
e.currentTarget.style.color = '#EAECEF'
|
||||
e.currentTarget.style.borderColor = '#F0B90B'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = '#1E2329'
|
||||
e.currentTarget.style.color = '#848E9C'
|
||||
e.currentTarget.style.borderColor = '#2B3139'
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
|
||||
</svg>
|
||||
GitHub
|
||||
</a>
|
||||
{/* Twitter/X */}
|
||||
<a
|
||||
href={OFFICIAL_LINKS.twitter}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: '#1E2329',
|
||||
color: '#848E9C',
|
||||
border: '1px solid #2B3139',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#2B3139'
|
||||
e.currentTarget.style.color = '#EAECEF'
|
||||
e.currentTarget.style.borderColor = '#1DA1F2'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = '#1E2329'
|
||||
e.currentTarget.style.color = '#848E9C'
|
||||
e.currentTarget.style.borderColor = '#2B3139'
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
Twitter
|
||||
</a>
|
||||
{/* Telegram */}
|
||||
<a
|
||||
href={OFFICIAL_LINKS.telegram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: '#1E2329',
|
||||
color: '#848E9C',
|
||||
border: '1px solid #2B3139',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#2B3139'
|
||||
e.currentTarget.style.color = '#EAECEF'
|
||||
e.currentTarget.style.borderColor = '#0088cc'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = '#1E2329'
|
||||
e.currentTarget.style.color = '#848E9C'
|
||||
e.currentTarget.style.borderColor = '#2B3139'
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||
</svg>
|
||||
Telegram
|
||||
</a>
|
||||
</div>
|
||||
</Container>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="space-y-4 md:space-y-6 animate-fade-in">
|
||||
{/* Header */}
|
||||
<PageHeader
|
||||
language={language}
|
||||
tradersCount={traders?.length || 0}
|
||||
configuredModelsCount={configuredModels.length}
|
||||
configuredExchangesCount={configuredExchanges.length}
|
||||
onAddModel={handleAddModel}
|
||||
onAddExchange={handleAddExchange}
|
||||
onCreateTrader={() => setShowCreateModal(true)}
|
||||
/>
|
||||
|
||||
{/* Configuration Status */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
||||
<AIModelsSection
|
||||
language={language}
|
||||
configuredModels={configuredModels}
|
||||
isModelInUse={isModelInUse}
|
||||
onModelClick={handleModelClick}
|
||||
/>
|
||||
|
||||
<ExchangesSection
|
||||
language={language}
|
||||
configuredExchanges={configuredExchanges}
|
||||
isExchangeInUse={isExchangeInUse}
|
||||
onExchangeClick={handleExchangeClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Traders Grid */}
|
||||
<TradersGrid
|
||||
language={language}
|
||||
traders={traders}
|
||||
onTraderSelect={handleTraderSelect}
|
||||
onEditTrader={handleEditTrader}
|
||||
onDeleteTrader={handleDeleteTrader}
|
||||
onToggleTrader={handleToggleTrader}
|
||||
/>
|
||||
|
||||
{/* Modals */}
|
||||
<TraderConfigModal
|
||||
isOpen={showCreateModal}
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
isEditMode={false}
|
||||
availableModels={enabledModels}
|
||||
availableExchanges={enabledExchanges}
|
||||
onSave={handleCreateTrader}
|
||||
/>
|
||||
|
||||
<TraderConfigModal
|
||||
isOpen={showEditModal}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
isEditMode={true}
|
||||
traderData={editingTrader}
|
||||
availableModels={enabledModels}
|
||||
availableExchanges={enabledExchanges}
|
||||
onSave={handleSaveEditTrader}
|
||||
/>
|
||||
|
||||
{showModelModal && (
|
||||
<ModelConfigModal
|
||||
allModels={supportedModels}
|
||||
configuredModels={allModels}
|
||||
editingModelId={editingModel}
|
||||
onSave={handleSaveModel}
|
||||
onDelete={handleDeleteModel}
|
||||
onClose={() => setShowModelModal(false)}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showExchangeModal && (
|
||||
<ExchangeConfigModal
|
||||
allExchanges={supportedExchanges}
|
||||
editingExchangeId={editingExchange}
|
||||
onSave={handleSaveExchange}
|
||||
onDelete={handleDeleteExchange}
|
||||
onClose={() => setShowExchangeModal(false)}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
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() {
|
||||
<div className="p-2">
|
||||
<div className="flex items-center justify-between mb-2 px-2">
|
||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('strategies')}</span>
|
||||
<button
|
||||
onClick={handleCreateStrategy}
|
||||
className="p-1 rounded hover:bg-white/10 transition-colors"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Import button with hidden file input */}
|
||||
<label className="p-1 rounded hover:bg-white/10 transition-colors cursor-pointer" style={{ color: '#848E9C' }} title={language === 'zh' ? '导入策略' : 'Import Strategy'}>
|
||||
<Upload className="w-4 h-4" />
|
||||
<input
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleImportStrategy}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
onClick={handleCreateStrategy}
|
||||
className="p-1 rounded hover:bg-white/10 transition-colors"
|
||||
style={{ color: '#F0B90B' }}
|
||||
title={language === 'zh' ? '新建策略' : 'New Strategy'}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{strategies.map((strategy) => (
|
||||
@@ -554,22 +630,33 @@ export function StrategyStudioPage() {
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm truncate" style={{ color: '#EAECEF' }}>{strategy.name}</span>
|
||||
{!strategy.is_default && (
|
||||
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDuplicateStrategy(strategy.id) }}
|
||||
className="p-1 rounded hover:bg-white/10"
|
||||
>
|
||||
<Copy className="w-3 h-3" style={{ color: '#848E9C' }} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
|
||||
className="p-1 rounded hover:bg-red-500/20"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" style={{ color: '#F6465D' }} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleExportStrategy(strategy) }}
|
||||
className="p-1 rounded hover:bg-white/10"
|
||||
title={language === 'zh' ? '导出' : 'Export'}
|
||||
>
|
||||
<Download className="w-3 h-3" style={{ color: '#848E9C' }} />
|
||||
</button>
|
||||
{!strategy.is_default && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDuplicateStrategy(strategy.id) }}
|
||||
className="p-1 rounded hover:bg-white/10"
|
||||
title={language === 'zh' ? '复制' : 'Duplicate'}
|
||||
>
|
||||
<Copy className="w-3 h-3" style={{ color: '#848E9C' }} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
|
||||
className="p-1 rounded hover:bg-red-500/20"
|
||||
title={language === 'zh' ? '删除' : 'Delete'}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" style={{ color: '#F6465D' }} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
{strategy.is_active && (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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: <LandingPage />,
|
||||
},
|
||||
// Auth routes - using AuthLayout
|
||||
{
|
||||
element: <AuthLayout />,
|
||||
children: [
|
||||
{
|
||||
path: '/login',
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
element: <RegisterPage />,
|
||||
},
|
||||
{
|
||||
path: '/reset-password',
|
||||
element: <ResetPasswordPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
// Main app routes - using MainLayout with nested routes
|
||||
{
|
||||
element: <MainLayout />,
|
||||
children: [
|
||||
{
|
||||
path: '/faq',
|
||||
element: <FAQPage />,
|
||||
},
|
||||
{
|
||||
path: '/competition',
|
||||
element: <CompetitionPage />,
|
||||
},
|
||||
{
|
||||
path: '/traders',
|
||||
element: <AITradersPage />,
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
element: <TraderDashboard />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to="/" replace />,
|
||||
},
|
||||
])
|
||||
+12
-9
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user