mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-03 11:00:58 +08:00
Feature/custom strategy (#1172)
* feat: add Strategy Studio with multi-timeframe support - Add Strategy Studio page with three-column layout for strategy management - Support multi-timeframe K-line data selection (5m, 15m, 1h, 4h, etc.) - Add GetWithTimeframes() function in market package for fetching multiple timeframes - Add TimeframeSeriesData struct for storing per-timeframe technical indicators - Update formatMarketData() to display all selected timeframes in AI prompt - Add strategy API endpoints for CRUD operations and test run - Integrate real AI test runs with configured AI models - Support custom AI500 and OI Top API URLs from strategy config * docs: add Strategy Studio screenshot to README files * fix: correct strategy-studio.png filename case in README * refactor: remove legacy signal source config and simplify trader creation - Remove signal source configuration from traders page (now handled by strategy) - Remove advanced options (legacy config) from TraderConfigModal - Rename default strategy to "默认山寨策略" with AI500 coin pool URL - Delete SignalSourceModal and SignalSourceWarning components - Clean up related stores, hooks, and page components
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import type { AIModel, Exchange, CreateTraderRequest } from '../types'
|
||||
import type { AIModel, Exchange, CreateTraderRequest, Strategy } from '../types'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { t } from '../i18n/translations'
|
||||
import { toast } from 'sonner'
|
||||
import { Pencil, Plus, X as IconX } from 'lucide-react'
|
||||
import { Pencil, Plus, X as IconX, Sparkles } from 'lucide-react'
|
||||
import { httpClient } from '../lib/httpClient'
|
||||
|
||||
// 提取下划线后面的名称部分
|
||||
@@ -12,22 +12,18 @@ function getShortName(fullName: string): string {
|
||||
return parts.length > 1 ? parts[parts.length - 1] : fullName
|
||||
}
|
||||
|
||||
interface TraderConfigData {
|
||||
import type { TraderConfigData } from '../types'
|
||||
|
||||
// 表单内部状态类型
|
||||
interface FormState {
|
||||
trader_id?: string
|
||||
trader_name: string
|
||||
ai_model: string
|
||||
exchange_id: string
|
||||
btc_eth_leverage: number
|
||||
altcoin_leverage: number
|
||||
trading_symbols: string
|
||||
custom_prompt: string
|
||||
override_base_prompt: boolean
|
||||
system_prompt_template: string
|
||||
strategy_id: string
|
||||
is_cross_margin: boolean
|
||||
use_coin_pool: boolean
|
||||
use_oi_top: boolean
|
||||
initial_balance?: number // 可选:创建时不需要,编辑时使用
|
||||
scan_interval_minutes: number
|
||||
initial_balance?: number
|
||||
}
|
||||
|
||||
interface TraderConfigModalProps {
|
||||
@@ -50,154 +46,68 @@ export function TraderConfigModal({
|
||||
onSave,
|
||||
}: TraderConfigModalProps) {
|
||||
const { language } = useLanguage()
|
||||
const [formData, setFormData] = useState<TraderConfigData>({
|
||||
const [formData, setFormData] = useState<FormState>({
|
||||
trader_name: '',
|
||||
ai_model: '',
|
||||
exchange_id: '',
|
||||
btc_eth_leverage: 5,
|
||||
altcoin_leverage: 3,
|
||||
trading_symbols: '',
|
||||
custom_prompt: '',
|
||||
override_base_prompt: false,
|
||||
system_prompt_template: 'default',
|
||||
strategy_id: '',
|
||||
is_cross_margin: true,
|
||||
use_coin_pool: false,
|
||||
use_oi_top: false,
|
||||
scan_interval_minutes: 3,
|
||||
})
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [availableCoins, setAvailableCoins] = useState<string[]>([])
|
||||
const [selectedCoins, setSelectedCoins] = useState<string[]>([])
|
||||
const [showCoinSelector, setShowCoinSelector] = useState(false)
|
||||
const [promptTemplates, setPromptTemplates] = useState<{ name: string }[]>([])
|
||||
const [strategies, setStrategies] = useState<Strategy[]>([])
|
||||
const [isFetchingBalance, setIsFetchingBalance] = useState(false)
|
||||
const [balanceFetchError, setBalanceFetchError] = useState<string>('')
|
||||
|
||||
// 获取用户的策略列表
|
||||
useEffect(() => {
|
||||
const fetchStrategies = async () => {
|
||||
try {
|
||||
const result = await httpClient.get<{ strategies: Strategy[] }>('/api/strategies')
|
||||
if (result.success && result.data?.strategies) {
|
||||
const strategyList = result.data.strategies
|
||||
setStrategies(strategyList)
|
||||
// 如果没有选择策略,默认选中激活的策略
|
||||
if (!formData.strategy_id && !isEditMode) {
|
||||
const activeStrategy = strategyList.find(s => s.is_active)
|
||||
if (activeStrategy) {
|
||||
setFormData(prev => ({ ...prev, strategy_id: activeStrategy.id }))
|
||||
} else if (strategyList.length > 0) {
|
||||
setFormData(prev => ({ ...prev, strategy_id: strategyList[0].id }))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch strategies:', error)
|
||||
}
|
||||
}
|
||||
if (isOpen) {
|
||||
fetchStrategies()
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (traderData) {
|
||||
setFormData(traderData)
|
||||
// 设置已选择的币种
|
||||
if (traderData.trading_symbols) {
|
||||
const coins = traderData.trading_symbols
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s)
|
||||
setSelectedCoins(coins)
|
||||
}
|
||||
setFormData({
|
||||
...traderData,
|
||||
strategy_id: traderData.strategy_id || '',
|
||||
})
|
||||
} else if (!isEditMode) {
|
||||
setFormData({
|
||||
trader_name: '',
|
||||
ai_model: availableModels[0]?.id || '',
|
||||
exchange_id: availableExchanges[0]?.id || '',
|
||||
btc_eth_leverage: 5,
|
||||
altcoin_leverage: 3,
|
||||
trading_symbols: '',
|
||||
custom_prompt: '',
|
||||
override_base_prompt: false,
|
||||
system_prompt_template: 'default',
|
||||
strategy_id: '',
|
||||
is_cross_margin: true,
|
||||
use_coin_pool: false,
|
||||
use_oi_top: false,
|
||||
initial_balance: 1000,
|
||||
scan_interval_minutes: 3,
|
||||
})
|
||||
}
|
||||
// 确保旧数据也有默认的 system_prompt_template
|
||||
if (traderData && traderData.system_prompt_template === undefined) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
system_prompt_template: 'default',
|
||||
}))
|
||||
}
|
||||
}, [traderData, isEditMode, availableModels, availableExchanges])
|
||||
|
||||
// 获取系统配置中的币种列表
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const result = await httpClient.get<{ default_coins?: string[] }>(
|
||||
'/api/config'
|
||||
)
|
||||
if (result.success && result.data?.default_coins) {
|
||||
setAvailableCoins(result.data.default_coins)
|
||||
} else {
|
||||
// 使用默认币种列表
|
||||
setAvailableCoins([
|
||||
'BTCUSDT',
|
||||
'ETHUSDT',
|
||||
'SOLUSDT',
|
||||
'BNBUSDT',
|
||||
'XRPUSDT',
|
||||
'DOGEUSDT',
|
||||
'ADAUSDT',
|
||||
])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch config:', error)
|
||||
// 使用默认币种列表
|
||||
setAvailableCoins([
|
||||
'BTCUSDT',
|
||||
'ETHUSDT',
|
||||
'SOLUSDT',
|
||||
'BNBUSDT',
|
||||
'XRPUSDT',
|
||||
'DOGEUSDT',
|
||||
'ADAUSDT',
|
||||
])
|
||||
}
|
||||
}
|
||||
fetchConfig()
|
||||
}, [])
|
||||
|
||||
// 获取系统提示词模板列表
|
||||
useEffect(() => {
|
||||
const fetchPromptTemplates = async () => {
|
||||
try {
|
||||
const result = await httpClient.get<{ templates?: { name: string }[] }>(
|
||||
'/api/prompt-templates'
|
||||
)
|
||||
if (result.success && result.data?.templates) {
|
||||
setPromptTemplates(result.data.templates)
|
||||
} else {
|
||||
// 使用默认模板列表
|
||||
setPromptTemplates([{ name: 'default' }, { name: 'aggressive' }])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch prompt templates:', error)
|
||||
// 使用默认模板列表
|
||||
setPromptTemplates([{ name: 'default' }, { name: 'aggressive' }])
|
||||
}
|
||||
}
|
||||
fetchPromptTemplates()
|
||||
}, [])
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const handleInputChange = (field: keyof TraderConfigData, value: any) => {
|
||||
const handleInputChange = (field: keyof FormState, value: any) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||
|
||||
// 如果是直接编辑trading_symbols,同步更新selectedCoins
|
||||
if (field === 'trading_symbols') {
|
||||
const coins = value
|
||||
.split(',')
|
||||
.map((s: string) => s.trim())
|
||||
.filter((s: string) => s)
|
||||
setSelectedCoins(coins)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCoinToggle = (coin: string) => {
|
||||
setSelectedCoins((prev) => {
|
||||
const newCoins = prev.includes(coin)
|
||||
? prev.filter((c) => c !== coin)
|
||||
: [...prev, coin]
|
||||
|
||||
// 同时更新 formData.trading_symbols
|
||||
const symbolsString = newCoins.join(',')
|
||||
setFormData((current) => ({ ...current, trading_symbols: symbolsString }))
|
||||
|
||||
return newCoins
|
||||
})
|
||||
}
|
||||
|
||||
const handleFetchCurrentBalance = async () => {
|
||||
@@ -216,11 +126,8 @@ export function TraderConfigModal({
|
||||
}>(`/api/account?trader_id=${traderData.trader_id}`)
|
||||
|
||||
if (result.success && result.data) {
|
||||
// total_equity = 当前账户净值(包含未实现盈亏)
|
||||
// 这应该作为新的初始余额
|
||||
const currentBalance =
|
||||
result.data.total_equity || result.data.balance || 0
|
||||
|
||||
setFormData((prev) => ({ ...prev, initial_balance: currentBalance }))
|
||||
toast.success('已获取当前余额')
|
||||
} else {
|
||||
@@ -229,7 +136,6 @@ export function TraderConfigModal({
|
||||
} catch (error) {
|
||||
console.error('获取余额失败:', error)
|
||||
setBalanceFetchError('获取余额失败,请检查网络连接')
|
||||
// Note: Network/system errors already shown via toast by httpClient
|
||||
} finally {
|
||||
setIsFetchingBalance(false)
|
||||
}
|
||||
@@ -244,19 +150,12 @@ export function TraderConfigModal({
|
||||
name: formData.trader_name,
|
||||
ai_model_id: formData.ai_model,
|
||||
exchange_id: formData.exchange_id,
|
||||
btc_eth_leverage: formData.btc_eth_leverage,
|
||||
altcoin_leverage: formData.altcoin_leverage,
|
||||
trading_symbols: formData.trading_symbols,
|
||||
custom_prompt: formData.custom_prompt,
|
||||
override_base_prompt: formData.override_base_prompt,
|
||||
system_prompt_template: formData.system_prompt_template,
|
||||
strategy_id: formData.strategy_id || undefined,
|
||||
is_cross_margin: formData.is_cross_margin,
|
||||
use_coin_pool: formData.use_coin_pool,
|
||||
use_oi_top: formData.use_oi_top,
|
||||
scan_interval_minutes: formData.scan_interval_minutes,
|
||||
}
|
||||
|
||||
// 只在编辑模式时包含initial_balance(用于手动更新)
|
||||
// 只在编辑模式时包含initial_balance
|
||||
if (isEditMode && formData.initial_balance !== undefined) {
|
||||
saveData.initial_balance = formData.initial_balance
|
||||
}
|
||||
@@ -274,10 +173,12 @@ export function TraderConfigModal({
|
||||
}
|
||||
}
|
||||
|
||||
const selectedStrategy = strategies.find(s => s.id === formData.strategy_id)
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm p-4 overflow-y-auto">
|
||||
<div
|
||||
className="bg-[#1E2329] border border-[#2B3139] rounded-xl shadow-2xl max-w-3xl w-full my-8"
|
||||
className="bg-[#1E2329] border border-[#2B3139] rounded-xl shadow-2xl max-w-2xl w-full my-8"
|
||||
style={{ maxHeight: 'calc(100vh - 4rem)' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@@ -296,7 +197,7 @@ export function TraderConfigModal({
|
||||
{isEditMode ? '修改交易员' : '创建交易员'}
|
||||
</h2>
|
||||
<p className="text-sm text-[#848E9C] mt-1">
|
||||
{isEditMode ? '修改交易员配置参数' : '配置新的AI交易员'}
|
||||
{isEditMode ? '修改交易员配置' : '选择策略并配置基础参数'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -310,18 +211,18 @@ export function TraderConfigModal({
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
className="p-6 space-y-8 overflow-y-auto"
|
||||
className="p-6 space-y-6 overflow-y-auto"
|
||||
style={{ maxHeight: 'calc(100vh - 16rem)' }}
|
||||
>
|
||||
{/* Basic Info */}
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
|
||||
🤖 基础配置
|
||||
<span className="text-[#F0B90B]">1</span> 基础配置
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
交易员名称
|
||||
交易员名称 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -336,7 +237,7 @@ export function TraderConfigModal({
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
AI模型
|
||||
AI模型 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={formData.ai_model}
|
||||
@@ -354,7 +255,7 @@ export function TraderConfigModal({
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
交易所
|
||||
交易所 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={formData.exchange_id}
|
||||
@@ -376,13 +277,77 @@ export function TraderConfigModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trading Configuration */}
|
||||
{/* Strategy Selection */}
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
|
||||
⚖️ 交易配置
|
||||
<span className="text-[#F0B90B]">2</span> 选择交易策略
|
||||
<Sparkles className="w-4 h-4 text-[#F0B90B]" />
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
使用策略
|
||||
</label>
|
||||
<select
|
||||
value={formData.strategy_id}
|
||||
onChange={(e) =>
|
||||
handleInputChange('strategy_id', e.target.value)
|
||||
}
|
||||
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||
>
|
||||
<option value="">-- 不使用策略(手动配置)--</option>
|
||||
{strategies.map((strategy) => (
|
||||
<option key={strategy.id} value={strategy.id}>
|
||||
{strategy.name}
|
||||
{strategy.is_active ? ' (当前激活)' : ''}
|
||||
{strategy.is_default ? ' [默认]' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{strategies.length === 0 && (
|
||||
<p className="text-xs text-[#848E9C] mt-2">
|
||||
暂无策略,请先在策略工作室创建策略
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Strategy Preview */}
|
||||
{selectedStrategy && (
|
||||
<div className="mt-3 p-4 bg-[#1E2329] border border-[#2B3139] rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-[#F0B90B] text-sm font-medium">
|
||||
策略详情
|
||||
</span>
|
||||
{selectedStrategy.is_active && (
|
||||
<span className="px-2 py-0.5 bg-green-500/20 text-green-400 text-xs rounded">
|
||||
激活中
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-[#848E9C] mb-2">
|
||||
{selectedStrategy.description || '无描述'}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs text-[#848E9C]">
|
||||
<div>
|
||||
币种来源: {selectedStrategy.config.coin_source.source_type === 'static' ? '固定币种' :
|
||||
selectedStrategy.config.coin_source.source_type === 'coinpool' ? 'Coin Pool' :
|
||||
selectedStrategy.config.coin_source.source_type === 'oi_top' ? 'OI Top' : '混合'}
|
||||
</div>
|
||||
<div>
|
||||
风控等级: {((selectedStrategy.config.risk_control?.max_position_ratio || 0.3) * 100).toFixed(0)}% 仓位
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trading Parameters */}
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
|
||||
<span className="text-[#F0B90B]">3</span> 交易参数
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{/* 第一行:保证金模式和初始余额 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
@@ -415,81 +380,6 @@ export function TraderConfigModal({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isEditMode && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-sm text-[#EAECEF]">
|
||||
初始余额 ($)
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFetchCurrentBalance}
|
||||
disabled={isFetchingBalance}
|
||||
className="px-3 py-1 text-xs bg-[#F0B90B] text-black rounded hover:bg-[#E1A706] transition-colors disabled:bg-[#848E9C] disabled:cursor-not-allowed"
|
||||
>
|
||||
{isFetchingBalance ? '获取中...' : '获取当前余额'}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.initial_balance || 0}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
'initial_balance',
|
||||
Number(e.target.value)
|
||||
)
|
||||
}
|
||||
onBlur={(e) => {
|
||||
// Force minimum value on blur
|
||||
const value = Number(e.target.value)
|
||||
if (value < 100) {
|
||||
handleInputChange('initial_balance', 100)
|
||||
}
|
||||
}}
|
||||
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||
min="100"
|
||||
step="0.01"
|
||||
/>
|
||||
<p className="text-xs text-[#848E9C] mt-1">
|
||||
用于手动更新初始余额基准(例如充值/提现后)
|
||||
</p>
|
||||
{balanceFetchError && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{balanceFetchError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isEditMode && (
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] mb-2 block">
|
||||
初始余额
|
||||
</label>
|
||||
<div className="w-full px-3 py-2 bg-[#1E2329] border border-[#2B3139] rounded text-[#848E9C] flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-4 h-4 text-[#F0B90B]"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" x2="12" y1="8" y2="12" />
|
||||
<line x1="12" x2="12.01" y1="16" y2="16" />
|
||||
</svg>
|
||||
<span className="text-sm">
|
||||
系统将自动获取您的账户净值作为初始余额
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 第二行:AI 扫描决策间隔 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
{t('aiScanInterval', language)}
|
||||
@@ -513,242 +403,54 @@ export function TraderConfigModal({
|
||||
{t('scanIntervalRecommend', language)}
|
||||
</p>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
{/* 第三行:杠杆设置 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Initial Balance (Edit mode only) */}
|
||||
{isEditMode && (
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
BTC/ETH 杠杆
|
||||
</label>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-sm text-[#EAECEF]">
|
||||
初始余额 ($)
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFetchCurrentBalance}
|
||||
disabled={isFetchingBalance}
|
||||
className="px-3 py-1 text-xs bg-[#F0B90B] text-black rounded hover:bg-[#E1A706] transition-colors disabled:bg-[#848E9C] disabled:cursor-not-allowed"
|
||||
>
|
||||
{isFetchingBalance ? '获取中...' : '获取当前余额'}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.btc_eth_leverage}
|
||||
value={formData.initial_balance || 0}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
'btc_eth_leverage',
|
||||
'initial_balance',
|
||||
Number(e.target.value)
|
||||
)
|
||||
}
|
||||
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||
min="1"
|
||||
max="125"
|
||||
min="100"
|
||||
step="0.01"
|
||||
/>
|
||||
<p className="text-xs text-[#848E9C] mt-1">
|
||||
用于手动更新初始余额基准(例如充值/提现后)
|
||||
</p>
|
||||
{balanceFetchError && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{balanceFetchError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
山寨币杠杆
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.altcoin_leverage}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
'altcoin_leverage',
|
||||
Number(e.target.value)
|
||||
)
|
||||
}
|
||||
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||
min="1"
|
||||
max="75"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 第三行:交易币种 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-sm text-[#EAECEF]">
|
||||
交易币种 (用逗号分隔,留空使用默认)
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCoinSelector(!showCoinSelector)}
|
||||
className="px-3 py-1 text-xs bg-[#F0B90B] text-black rounded hover:bg-[#E1A706] transition-colors"
|
||||
>
|
||||
{showCoinSelector ? '收起选择' : '快速选择'}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.trading_symbols}
|
||||
onChange={(e) =>
|
||||
handleInputChange('trading_symbols', e.target.value)
|
||||
}
|
||||
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||
placeholder="例如: BTCUSDT,ETHUSDT,ADAUSDT"
|
||||
/>
|
||||
|
||||
{/* 币种选择器 */}
|
||||
{showCoinSelector && (
|
||||
<div className="mt-3 p-3 bg-[#0B0E11] border border-[#2B3139] rounded">
|
||||
<div className="text-xs text-[#848E9C] mb-2">
|
||||
点击选择币种:
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableCoins.map((coin) => (
|
||||
<button
|
||||
key={coin}
|
||||
type="button"
|
||||
onClick={() => handleCoinToggle(coin)}
|
||||
className={`px-2 py-1 text-xs rounded transition-colors ${
|
||||
selectedCoins.includes(coin)
|
||||
? 'bg-[#F0B90B] text-black'
|
||||
: 'bg-[#1E2329] text-[#848E9C] border border-[#2B3139] hover:border-[#F0B90B]'
|
||||
}`}
|
||||
>
|
||||
{coin.replace('USDT', '')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Signal Sources */}
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
|
||||
📡 信号源配置
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.use_coin_pool}
|
||||
onChange={(e) =>
|
||||
handleInputChange('use_coin_pool', e.target.checked)
|
||||
}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<label className="text-sm text-[#EAECEF]">
|
||||
使用 Coin Pool 信号
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.use_oi_top}
|
||||
onChange={(e) =>
|
||||
handleInputChange('use_oi_top', e.target.checked)
|
||||
}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<label className="text-sm text-[#EAECEF]">
|
||||
使用 OI Top 信号
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trading Prompt */}
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
|
||||
💬 交易策略提示词
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{/* 系统提示词模板选择 */}
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
{t('systemPromptTemplate', language)}
|
||||
</label>
|
||||
<select
|
||||
value={formData.system_prompt_template}
|
||||
onChange={(e) =>
|
||||
handleInputChange('system_prompt_template', e.target.value)
|
||||
}
|
||||
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||
>
|
||||
{promptTemplates.map((template) => {
|
||||
// Template name mapping with i18n
|
||||
const getTemplateName = (name: string) => {
|
||||
const keyMap: Record<string, string> = {
|
||||
default: 'promptTemplateDefault',
|
||||
adaptive: 'promptTemplateAdaptive',
|
||||
adaptive_relaxed: 'promptTemplateAdaptiveRelaxed',
|
||||
Hansen: 'promptTemplateHansen',
|
||||
nof1: 'promptTemplateNof1',
|
||||
taro_long_prompts: 'promptTemplateTaroLong',
|
||||
}
|
||||
const key = keyMap[name]
|
||||
return key
|
||||
? t(key, language)
|
||||
: name.charAt(0).toUpperCase() + name.slice(1)
|
||||
}
|
||||
|
||||
return (
|
||||
<option key={template.name} value={template.name}>
|
||||
{getTemplateName(template.name)}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
|
||||
{/* 動態描述區域 */}
|
||||
<div
|
||||
className="mt-2 p-3 rounded"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.05)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.15)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="text-xs font-semibold mb-1"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
{(() => {
|
||||
const titleKeyMap: Record<string, string> = {
|
||||
default: 'promptDescDefault',
|
||||
adaptive: 'promptDescAdaptive',
|
||||
adaptive_relaxed: 'promptDescAdaptiveRelaxed',
|
||||
Hansen: 'promptDescHansen',
|
||||
nof1: 'promptDescNof1',
|
||||
taro_long_prompts: 'promptDescTaroLong',
|
||||
}
|
||||
const key = titleKeyMap[formData.system_prompt_template]
|
||||
return key
|
||||
? t(key, language)
|
||||
: t('promptDescDefault', language)
|
||||
})()}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{(() => {
|
||||
const contentKeyMap: Record<string, string> = {
|
||||
default: 'promptDescDefaultContent',
|
||||
adaptive: 'promptDescAdaptiveContent',
|
||||
adaptive_relaxed: 'promptDescAdaptiveRelaxedContent',
|
||||
Hansen: 'promptDescHansenContent',
|
||||
nof1: 'promptDescNof1Content',
|
||||
taro_long_prompts: 'promptDescTaroLongContent',
|
||||
}
|
||||
const key = contentKeyMap[formData.system_prompt_template]
|
||||
return key
|
||||
? t(key, language)
|
||||
: t('promptDescDefaultContent', language)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-[#848E9C] mt-1">
|
||||
选择预设的交易策略模板(包含交易哲学、风控原则等)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.override_base_prompt}
|
||||
onChange={(e) =>
|
||||
handleInputChange('override_base_prompt', e.target.checked)
|
||||
}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<label className="text-sm text-[#EAECEF]">覆盖默认提示词</label>
|
||||
<span className="text-xs text-[#F0B90B] inline-flex items-center gap-1">
|
||||
{/* Create mode info */}
|
||||
{!isEditMode && (
|
||||
<div className="p-3 bg-[#1E2329] border border-[#2B3139] rounded flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-3.5 h-3.5"
|
||||
className="w-4 h-4 text-[#F0B90B]"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -756,34 +458,18 @@ export function TraderConfigModal({
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" />
|
||||
<line x1="12" x2="12" y1="9" y2="13" />
|
||||
<line x1="12" x2="12.01" y1="17" y2="17" />
|
||||
</svg>{' '}
|
||||
启用后将完全替换默认策略
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
{formData.override_base_prompt
|
||||
? '自定义提示词'
|
||||
: '附加提示词'}
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.custom_prompt}
|
||||
onChange={(e) =>
|
||||
handleInputChange('custom_prompt', e.target.value)
|
||||
}
|
||||
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none h-24 resize-none"
|
||||
placeholder={
|
||||
formData.override_base_prompt
|
||||
? '输入完整的交易策略提示词...'
|
||||
: '输入额外的交易策略提示...'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" x2="12" y1="8" y2="12" />
|
||||
<line x1="12" x2="12.01" y1="16" y2="16" />
|
||||
</svg>
|
||||
<span className="text-sm text-[#848E9C]">
|
||||
系统将自动获取您的账户净值作为初始余额
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
|
||||
Reference in New Issue
Block a user