mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-09 14:00:57 +08:00
feat: add OI ranking data support and fix trader config update issues
- Add OI ranking data fetching and formatting for AI prompts - Fix trader update not saving strategy_id, ai_model_id, initial_balance - Fix AI API key not set for non-qwen/deepseek providers - Add strategy_id to trader config API response - Remove old trader from memory before reloading on update - Clean up unused useTraderActions.ts
This commit is contained in:
@@ -290,6 +290,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
const handleSaveEditTrader = async (data: CreateTraderRequest) => {
|
||||
console.log('🔥🔥🔥 handleSaveEditTrader CALLED with data:', data)
|
||||
if (!editingTrader) return
|
||||
|
||||
try {
|
||||
@@ -310,19 +311,17 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
name: data.name,
|
||||
ai_model_id: data.ai_model_id,
|
||||
exchange_id: data.exchange_id,
|
||||
strategy_id: data.strategy_id,
|
||||
initial_balance: data.initial_balance,
|
||||
scan_interval_minutes: data.scan_interval_minutes,
|
||||
btc_eth_leverage: data.btc_eth_leverage,
|
||||
altcoin_leverage: data.altcoin_leverage,
|
||||
trading_symbols: data.trading_symbols,
|
||||
custom_prompt: data.custom_prompt,
|
||||
override_base_prompt: data.override_base_prompt,
|
||||
system_prompt_template: data.system_prompt_template,
|
||||
is_cross_margin: data.is_cross_margin,
|
||||
use_coin_pool: data.use_coin_pool,
|
||||
use_oi_top: data.use_oi_top,
|
||||
show_in_competition: data.show_in_competition,
|
||||
}
|
||||
|
||||
console.log('🔥 handleSaveEditTrader - data:', data)
|
||||
console.log('🔥 handleSaveEditTrader - data.strategy_id:', data.strategy_id)
|
||||
console.log('🔥 handleSaveEditTrader - request:', request)
|
||||
|
||||
await toast.promise(api.updateTrader(editingTrader.trader_id, request), {
|
||||
loading: '正在保存…',
|
||||
success: '保存成功',
|
||||
|
||||
@@ -163,7 +163,7 @@ export function TraderConfigModal({
|
||||
name: formData.trader_name,
|
||||
ai_model_id: formData.ai_model,
|
||||
exchange_id: formData.exchange_id,
|
||||
strategy_id: formData.strategy_id || undefined,
|
||||
strategy_id: formData.strategy_id,
|
||||
is_cross_margin: formData.is_cross_margin,
|
||||
show_in_competition: formData.show_in_competition,
|
||||
scan_interval_minutes: formData.scan_interval_minutes,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Clock, Activity, Database, TrendingUp, BarChart2, Info, Lock } from 'lucide-react'
|
||||
import { Clock, Activity, Database, TrendingUp, BarChart2, Info, Lock, LineChart } from 'lucide-react'
|
||||
import type { IndicatorConfig } from '../../types'
|
||||
|
||||
// Default API URL for quant data (must contain {symbol} placeholder)
|
||||
const DEFAULT_QUANT_DATA_API_URL = 'http://nofxaios.com:30006/api/coin/{symbol}?include=netflow,oi,price&auth=cm_568c67eae410d912c54c'
|
||||
// Default API base URL for OI ranking data
|
||||
const DEFAULT_OI_RANKING_API_URL = 'http://nofxaios.com:30006'
|
||||
|
||||
interface IndicatorEditorProps {
|
||||
config: IndicatorConfig
|
||||
@@ -82,6 +84,13 @@ export function IndicatorEditor({
|
||||
fillDefault: { zh: '填入默认', en: 'Fill Default' },
|
||||
symbolPlaceholder: { zh: '{symbol} 会被替换为币种', en: '{symbol} will be replaced with coin' },
|
||||
|
||||
// OI Ranking
|
||||
oiRanking: { zh: 'OI 排行数据', en: 'OI Ranking Data' },
|
||||
oiRankingDesc: { zh: '市场持仓量增减排行,反映资金流向', en: 'Market-wide OI changes, reflects capital flow' },
|
||||
oiRankingDuration: { zh: '时间周期', en: 'Duration' },
|
||||
oiRankingLimit: { zh: '排行数量', en: 'Top N' },
|
||||
oiRankingNote: { zh: '显示持仓量增加/减少的币种排行,帮助发现资金流向', en: 'Shows coins with OI increase/decrease, helps identify capital flow' },
|
||||
|
||||
// Tips
|
||||
aiCanCalculate: { zh: '💡 提示:AI 可自行计算这些指标,开启可减少 AI 计算量', en: '💡 Tip: AI can calculate these, enabling reduces AI workload' },
|
||||
}
|
||||
@@ -456,6 +465,82 @@ export function IndicatorEditor({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 5: OI Ranking Data (Market-wide) */}
|
||||
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
||||
<LineChart className="w-4 h-4" style={{ color: '#22c55e' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('oiRanking')}</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('oiRankingDesc')}</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3 space-y-3">
|
||||
{/* Enable Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#22c55e' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('oiRanking')}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_oi_ranking || false}
|
||||
onChange={(e) => !disabled && onChange({
|
||||
...config,
|
||||
enable_oi_ranking: e.target.checked,
|
||||
// Set defaults when enabling
|
||||
...(e.target.checked && !config.oi_ranking_api_url ? { oi_ranking_api_url: DEFAULT_OI_RANKING_API_URL } : {}),
|
||||
...(e.target.checked && !config.oi_ranking_duration ? { oi_ranking_duration: '1h' } : {}),
|
||||
...(e.target.checked && !config.oi_ranking_limit ? { oi_ranking_limit: 10 } : {}),
|
||||
})}
|
||||
disabled={disabled}
|
||||
className="w-4 h-4 rounded accent-green-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Settings */}
|
||||
{config.enable_oi_ranking && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-3">
|
||||
{/* Duration */}
|
||||
<div className="flex-1">
|
||||
<label className="text-[10px] mb-1 block" style={{ color: '#848E9C' }}>
|
||||
{t('oiRankingDuration')}
|
||||
</label>
|
||||
<select
|
||||
value={config.oi_ranking_duration || '1h'}
|
||||
onChange={(e) => !disabled && onChange({ ...config, oi_ranking_duration: e.target.value })}
|
||||
disabled={disabled}
|
||||
className="w-full px-2 py-1.5 rounded text-xs"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
<option value="1h">{language === 'zh' ? '1小时' : '1 Hour'}</option>
|
||||
<option value="4h">{language === 'zh' ? '4小时' : '4 Hours'}</option>
|
||||
<option value="24h">{language === 'zh' ? '24小时' : '24 Hours'}</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* Limit */}
|
||||
<div className="flex-1">
|
||||
<label className="text-[10px] mb-1 block" style={{ color: '#848E9C' }}>
|
||||
{t('oiRankingLimit')}
|
||||
</label>
|
||||
<select
|
||||
value={config.oi_ranking_limit || 10}
|
||||
onChange={(e) => !disabled && onChange({ ...config, oi_ranking_limit: parseInt(e.target.value) })}
|
||||
disabled={disabled}
|
||||
className="w-full px-2 py-1.5 rounded text-xs"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
{[5, 10, 15, 20].map(n => (
|
||||
<option key={n} value={n}>{n}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px]" style={{ color: '#5E6673' }}>{t('oiRankingNote')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,649 +0,0 @@
|
||||
import { api } from '../lib/api'
|
||||
import type {
|
||||
TraderInfo,
|
||||
CreateTraderRequest,
|
||||
TraderConfigData,
|
||||
AIModel,
|
||||
Exchange,
|
||||
} from '../types'
|
||||
import { t } from '../i18n/translations'
|
||||
import { confirmToast } from '../lib/notify'
|
||||
import { toast } from 'sonner'
|
||||
import type { Language } from '../i18n/translations'
|
||||
|
||||
interface UseTraderActionsParams {
|
||||
traders: TraderInfo[] | undefined
|
||||
allModels: AIModel[]
|
||||
allExchanges: Exchange[]
|
||||
supportedModels: AIModel[]
|
||||
supportedExchanges: Exchange[]
|
||||
language: Language
|
||||
mutateTraders: () => Promise<any>
|
||||
setAllModels: (models: AIModel[]) => void
|
||||
setAllExchanges: (exchanges: Exchange[]) => void
|
||||
setShowCreateModal: (show: boolean) => void
|
||||
setShowEditModal: (show: boolean) => void
|
||||
setShowModelModal: (show: boolean) => void
|
||||
setShowExchangeModal: (show: boolean) => void
|
||||
setEditingModel: (modelId: string | null) => void
|
||||
setEditingExchange: (exchangeId: string | null) => void
|
||||
editingTrader: TraderConfigData | null
|
||||
setEditingTrader: (trader: TraderConfigData | null) => void
|
||||
}
|
||||
|
||||
export function useTraderActions({
|
||||
traders,
|
||||
allModels,
|
||||
allExchanges,
|
||||
supportedModels,
|
||||
supportedExchanges,
|
||||
language,
|
||||
mutateTraders,
|
||||
setAllModels,
|
||||
setAllExchanges,
|
||||
setShowCreateModal,
|
||||
setShowEditModal,
|
||||
setShowModelModal,
|
||||
setShowExchangeModal,
|
||||
setEditingModel,
|
||||
setEditingExchange,
|
||||
editingTrader,
|
||||
setEditingTrader,
|
||||
}: UseTraderActionsParams) {
|
||||
// 检查模型是否正在被运行中的交易员使用(用于UI禁用)
|
||||
const isModelInUse = (modelId: string) => {
|
||||
return traders?.some((t) => t.ai_model === modelId && t.is_running) || false
|
||||
}
|
||||
|
||||
// 检查交易所是否正在被运行中的交易员使用(用于UI禁用)
|
||||
const isExchangeInUse = (exchangeId: string) => {
|
||||
return (
|
||||
traders?.some((t) => t.exchange_id === exchangeId && t.is_running) ||
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
// 检查模型是否被任何交易员使用(包括停止状态的)
|
||||
const isModelUsedByAnyTrader = (modelId: string) => {
|
||||
return traders?.some((t) => t.ai_model === modelId) || false
|
||||
}
|
||||
|
||||
// 检查交易所是否被任何交易员使用(包括停止状态的)
|
||||
const isExchangeUsedByAnyTrader = (exchangeId: string) => {
|
||||
return traders?.some((t) => t.exchange_id === exchangeId) || false
|
||||
}
|
||||
|
||||
// 获取使用特定模型的交易员列表
|
||||
const getTradersUsingModel = (modelId: string) => {
|
||||
return traders?.filter((t) => t.ai_model === modelId) || []
|
||||
}
|
||||
|
||||
// 获取使用特定交易所的交易员列表
|
||||
const getTradersUsingExchange = (exchangeId: string) => {
|
||||
return traders?.filter((t) => t.exchange_id === exchangeId) || []
|
||||
}
|
||||
|
||||
const handleCreateTrader = async (data: CreateTraderRequest) => {
|
||||
try {
|
||||
const model = allModels?.find((m) => m.id === data.ai_model_id)
|
||||
const exchange = allExchanges?.find((e) => e.id === data.exchange_id)
|
||||
|
||||
if (!model?.enabled) {
|
||||
toast.error(t('modelNotConfigured', language))
|
||||
return
|
||||
}
|
||||
|
||||
if (!exchange?.enabled) {
|
||||
toast.error(t('exchangeNotConfigured', language))
|
||||
return
|
||||
}
|
||||
|
||||
await toast.promise(api.createTrader(data), {
|
||||
loading: '正在创建…',
|
||||
success: '创建成功',
|
||||
error: '创建失败',
|
||||
})
|
||||
setShowCreateModal(false)
|
||||
// Immediately refresh traders list for better UX
|
||||
await mutateTraders()
|
||||
} catch (error) {
|
||||
console.error('Failed to create trader:', error)
|
||||
toast.error(t('createTraderFailed', language))
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditTrader = async (traderId: string) => {
|
||||
try {
|
||||
const traderConfig = await api.getTraderConfig(traderId)
|
||||
setEditingTrader(traderConfig)
|
||||
setShowEditModal(true)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch trader config:', error)
|
||||
toast.error(t('getTraderConfigFailed', language))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveEditTrader = async (data: CreateTraderRequest) => {
|
||||
if (!editingTrader || !editingTrader.trader_id) return
|
||||
|
||||
try {
|
||||
const enabledModels = allModels?.filter((m) => m.enabled) || []
|
||||
const enabledExchanges =
|
||||
allExchanges?.filter((e) => {
|
||||
if (!e.enabled) return false
|
||||
|
||||
// Aster 交易所需要特殊字段
|
||||
if (e.id === 'aster') {
|
||||
return (
|
||||
e.asterUser &&
|
||||
e.asterUser.trim() !== '' &&
|
||||
e.asterSigner &&
|
||||
e.asterSigner.trim() !== ''
|
||||
)
|
||||
}
|
||||
|
||||
// Hyperliquid 需要钱包地址
|
||||
if (e.id === 'hyperliquid') {
|
||||
return (
|
||||
e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== ''
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}) || []
|
||||
|
||||
const model = enabledModels?.find((m) => m.id === data.ai_model_id)
|
||||
const exchange = enabledExchanges?.find((e) => e.id === data.exchange_id)
|
||||
|
||||
if (!model) {
|
||||
toast.error(t('modelConfigNotExist', language))
|
||||
return
|
||||
}
|
||||
|
||||
if (!exchange) {
|
||||
toast.error(t('exchangeConfigNotExist', language))
|
||||
return
|
||||
}
|
||||
|
||||
const request = {
|
||||
name: data.name,
|
||||
ai_model_id: data.ai_model_id,
|
||||
exchange_id: data.exchange_id,
|
||||
initial_balance: data.initial_balance,
|
||||
scan_interval_minutes: data.scan_interval_minutes,
|
||||
btc_eth_leverage: data.btc_eth_leverage,
|
||||
altcoin_leverage: data.altcoin_leverage,
|
||||
trading_symbols: data.trading_symbols,
|
||||
custom_prompt: data.custom_prompt,
|
||||
override_base_prompt: data.override_base_prompt,
|
||||
system_prompt_template: data.system_prompt_template,
|
||||
is_cross_margin: data.is_cross_margin,
|
||||
use_coin_pool: data.use_coin_pool,
|
||||
use_oi_top: data.use_oi_top,
|
||||
}
|
||||
|
||||
await toast.promise(api.updateTrader(editingTrader.trader_id, request), {
|
||||
loading: '正在保存…',
|
||||
success: '保存成功',
|
||||
error: '保存失败',
|
||||
})
|
||||
setShowEditModal(false)
|
||||
setEditingTrader(null)
|
||||
// Immediately refresh traders list for better UX
|
||||
await mutateTraders()
|
||||
} catch (error) {
|
||||
console.error('Failed to update trader:', error)
|
||||
toast.error(t('updateTraderFailed', language))
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteTrader = async (traderId: string) => {
|
||||
{
|
||||
const ok = await confirmToast(t('confirmDeleteTrader', language))
|
||||
if (!ok) return
|
||||
}
|
||||
|
||||
try {
|
||||
await toast.promise(api.deleteTrader(traderId), {
|
||||
loading: '正在删除…',
|
||||
success: '删除成功',
|
||||
error: '删除失败',
|
||||
})
|
||||
|
||||
// Immediately refresh traders list for better UX
|
||||
await mutateTraders()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete trader:', error)
|
||||
toast.error(t('deleteTraderFailed', language))
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleTrader = async (traderId: string, running: boolean) => {
|
||||
try {
|
||||
if (running) {
|
||||
await toast.promise(api.stopTrader(traderId), {
|
||||
loading: '正在停止…',
|
||||
success: '已停止',
|
||||
error: '停止失败',
|
||||
})
|
||||
} else {
|
||||
await toast.promise(api.startTrader(traderId), {
|
||||
loading: '正在启动…',
|
||||
success: '已启动',
|
||||
error: '启动失败',
|
||||
})
|
||||
}
|
||||
|
||||
// Immediately refresh traders list to update running status
|
||||
await mutateTraders()
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle trader:', error)
|
||||
toast.error(t('operationFailed', language))
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleCompetition = async (traderId: string, currentShowInCompetition: boolean) => {
|
||||
try {
|
||||
const newValue = !currentShowInCompetition
|
||||
await toast.promise(api.toggleCompetition(traderId, newValue), {
|
||||
loading: '正在更新…',
|
||||
success: newValue ? '已在竞技场显示' : '已在竞技场隐藏',
|
||||
error: '更新失败',
|
||||
})
|
||||
|
||||
// Immediately refresh traders list to update status
|
||||
await mutateTraders()
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle competition visibility:', error)
|
||||
toast.error(t('operationFailed', language))
|
||||
}
|
||||
}
|
||||
|
||||
const handleModelClick = (modelId: string) => {
|
||||
if (!isModelInUse(modelId)) {
|
||||
setEditingModel(modelId)
|
||||
setShowModelModal(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleExchangeClick = (exchangeId: string) => {
|
||||
if (!isExchangeInUse(exchangeId)) {
|
||||
setEditingExchange(exchangeId)
|
||||
setShowExchangeModal(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 通用删除配置处理函数
|
||||
const handleDeleteConfig = async <T extends { id: string }>(config: {
|
||||
id: string
|
||||
type: 'model' | 'exchange'
|
||||
checkInUse: (id: string) => boolean
|
||||
getUsingTraders: (id: string) => any[]
|
||||
cannotDeleteKey: string
|
||||
confirmDeleteKey: string
|
||||
allItems: T[] | undefined
|
||||
clearFields: (item: T) => T
|
||||
buildRequest: (items: T[]) => any
|
||||
updateApi: (request: any) => Promise<void>
|
||||
refreshApi: () => Promise<T[]>
|
||||
setItems: (items: T[]) => void
|
||||
closeModal: () => void
|
||||
errorKey: string
|
||||
}) => {
|
||||
// 检查是否有交易员正在使用
|
||||
if (config.checkInUse(config.id)) {
|
||||
const usingTraders = config.getUsingTraders(config.id)
|
||||
const traderNames = usingTraders.map((t) => t.trader_name).join(', ')
|
||||
toast.error(
|
||||
`${t(config.cannotDeleteKey, language)} · ${t('tradersUsing', language)}: ${traderNames} · ${t('pleaseDeleteTradersFirst', language)}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
{
|
||||
const ok = await confirmToast(t(config.confirmDeleteKey, language))
|
||||
if (!ok) return
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedItems =
|
||||
config.allItems?.map((item) =>
|
||||
item.id === config.id ? config.clearFields(item) : item
|
||||
) || []
|
||||
|
||||
const request = config.buildRequest(updatedItems)
|
||||
await toast.promise(config.updateApi(request), {
|
||||
loading: '正在更新配置…',
|
||||
success: '配置已更新',
|
||||
error: '更新配置失败',
|
||||
})
|
||||
|
||||
// 重新获取用户配置以确保数据同步
|
||||
const refreshedItems = await config.refreshApi()
|
||||
config.setItems(refreshedItems)
|
||||
|
||||
config.closeModal()
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete ${config.type} config:`, error)
|
||||
toast.error(t(config.errorKey, language))
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteModel = async (modelId: string) => {
|
||||
await handleDeleteConfig({
|
||||
id: modelId,
|
||||
type: 'model',
|
||||
checkInUse: isModelUsedByAnyTrader,
|
||||
getUsingTraders: getTradersUsingModel,
|
||||
cannotDeleteKey: 'cannotDeleteModelInUse',
|
||||
confirmDeleteKey: 'confirmDeleteModel',
|
||||
allItems: allModels,
|
||||
clearFields: (m) => ({
|
||||
...m,
|
||||
apiKey: '',
|
||||
customApiUrl: '',
|
||||
customModelName: '',
|
||||
enabled: false,
|
||||
}),
|
||||
buildRequest: (models) => ({
|
||||
models: Object.fromEntries(
|
||||
models.map((model) => [
|
||||
model.provider,
|
||||
{
|
||||
enabled: model.enabled,
|
||||
api_key: model.apiKey || '',
|
||||
custom_api_url: model.customApiUrl || '',
|
||||
custom_model_name: model.customModelName || '',
|
||||
},
|
||||
])
|
||||
),
|
||||
}),
|
||||
updateApi: api.updateModelConfigs,
|
||||
refreshApi: api.getModelConfigs,
|
||||
setItems: (items) => {
|
||||
// 使用函数式更新确保状态正确更新
|
||||
setAllModels([...items])
|
||||
},
|
||||
closeModal: () => {
|
||||
setShowModelModal(false)
|
||||
setEditingModel(null)
|
||||
},
|
||||
errorKey: 'deleteConfigFailed',
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveModel = async (
|
||||
modelId: string,
|
||||
apiKey: string,
|
||||
customApiUrl?: string,
|
||||
customModelName?: string
|
||||
) => {
|
||||
try {
|
||||
// 创建或更新用户的模型配置
|
||||
const existingModel = allModels?.find((m) => m.id === modelId)
|
||||
let updatedModels
|
||||
|
||||
// 找到要配置的模型(优先从已配置列表,其次从支持列表)
|
||||
const modelToUpdate =
|
||||
existingModel || supportedModels?.find((m) => m.id === modelId)
|
||||
if (!modelToUpdate) {
|
||||
toast.error(t('modelNotExist', language))
|
||||
return
|
||||
}
|
||||
|
||||
if (existingModel) {
|
||||
// 更新现有配置
|
||||
updatedModels =
|
||||
allModels?.map((m) =>
|
||||
m.id === modelId
|
||||
? {
|
||||
...m,
|
||||
apiKey,
|
||||
customApiUrl: customApiUrl || '',
|
||||
customModelName: customModelName || '',
|
||||
enabled: true,
|
||||
}
|
||||
: m
|
||||
) || []
|
||||
} else {
|
||||
// 添加新配置
|
||||
const newModel = {
|
||||
...modelToUpdate,
|
||||
apiKey,
|
||||
customApiUrl: customApiUrl || '',
|
||||
customModelName: customModelName || '',
|
||||
enabled: true,
|
||||
}
|
||||
updatedModels = [...(allModels || []), newModel]
|
||||
}
|
||||
|
||||
const request = {
|
||||
models: Object.fromEntries(
|
||||
updatedModels.map((model) => [
|
||||
model.provider, // 使用 provider 而不是 id
|
||||
{
|
||||
enabled: model.enabled,
|
||||
api_key: model.apiKey || '',
|
||||
custom_api_url: model.customApiUrl || '',
|
||||
custom_model_name: model.customModelName || '',
|
||||
},
|
||||
])
|
||||
),
|
||||
}
|
||||
|
||||
await toast.promise(api.updateModelConfigs(request), {
|
||||
loading: '正在更新模型配置…',
|
||||
success: '模型配置已更新',
|
||||
error: '更新模型配置失败',
|
||||
})
|
||||
|
||||
// 重新获取用户配置以确保数据同步
|
||||
const refreshedModels = await api.getModelConfigs()
|
||||
setAllModels(refreshedModels)
|
||||
|
||||
setShowModelModal(false)
|
||||
setEditingModel(null)
|
||||
} catch (error) {
|
||||
console.error('Failed to save model config:', error)
|
||||
toast.error(t('saveConfigFailed', language))
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteExchange = async (exchangeId: string) => {
|
||||
await handleDeleteConfig({
|
||||
id: exchangeId,
|
||||
type: 'exchange',
|
||||
checkInUse: isExchangeUsedByAnyTrader,
|
||||
getUsingTraders: getTradersUsingExchange,
|
||||
cannotDeleteKey: 'cannotDeleteExchangeInUse',
|
||||
confirmDeleteKey: 'confirmDeleteExchange',
|
||||
allItems: allExchanges,
|
||||
clearFields: (e) => ({
|
||||
...e,
|
||||
apiKey: '',
|
||||
secretKey: '',
|
||||
passphrase: '', // OKX专用
|
||||
hyperliquidWalletAddr: '',
|
||||
asterUser: '',
|
||||
asterSigner: '',
|
||||
asterPrivateKey: '',
|
||||
enabled: false,
|
||||
}),
|
||||
buildRequest: (exchanges) => ({
|
||||
exchanges: Object.fromEntries(
|
||||
exchanges.map((exchange) => [
|
||||
exchange.id,
|
||||
{
|
||||
enabled: exchange.enabled,
|
||||
api_key: exchange.apiKey || '',
|
||||
secret_key: exchange.secretKey || '',
|
||||
passphrase: exchange.passphrase || '', // OKX专用
|
||||
testnet: exchange.testnet || false,
|
||||
hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '',
|
||||
aster_user: exchange.asterUser || '',
|
||||
aster_signer: exchange.asterSigner || '',
|
||||
aster_private_key: exchange.asterPrivateKey || '',
|
||||
},
|
||||
])
|
||||
),
|
||||
}),
|
||||
updateApi: api.updateExchangeConfigsEncrypted,
|
||||
refreshApi: api.getExchangeConfigs,
|
||||
setItems: (items) => {
|
||||
// 使用函数式更新确保状态正确更新
|
||||
setAllExchanges([...items])
|
||||
},
|
||||
closeModal: () => {
|
||||
setShowExchangeModal(false)
|
||||
setEditingExchange(null)
|
||||
},
|
||||
errorKey: 'deleteExchangeConfigFailed',
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveExchange = async (
|
||||
exchangeId: string,
|
||||
apiKey: string,
|
||||
secretKey?: string,
|
||||
passphrase?: string, // OKX专用
|
||||
testnet?: boolean,
|
||||
hyperliquidWalletAddr?: string,
|
||||
asterUser?: string,
|
||||
asterSigner?: string,
|
||||
asterPrivateKey?: string,
|
||||
lighterWalletAddr?: string,
|
||||
lighterPrivateKey?: string,
|
||||
lighterApiKeyPrivateKey?: string
|
||||
) => {
|
||||
try {
|
||||
// 找到要配置的交易所(从supportedExchanges中)
|
||||
const exchangeToUpdate = supportedExchanges?.find(
|
||||
(e) => e.id === exchangeId
|
||||
)
|
||||
if (!exchangeToUpdate) {
|
||||
toast.error(t('exchangeNotExist', language))
|
||||
return
|
||||
}
|
||||
|
||||
// 创建或更新用户的交易所配置
|
||||
const existingExchange = allExchanges?.find((e) => e.id === exchangeId)
|
||||
let updatedExchanges
|
||||
|
||||
if (existingExchange) {
|
||||
// 更新现有配置
|
||||
updatedExchanges =
|
||||
allExchanges?.map((e) =>
|
||||
e.id === exchangeId
|
||||
? {
|
||||
...e,
|
||||
apiKey,
|
||||
secretKey,
|
||||
passphrase, // OKX专用
|
||||
testnet,
|
||||
hyperliquidWalletAddr,
|
||||
asterUser,
|
||||
asterSigner,
|
||||
asterPrivateKey,
|
||||
lighterWalletAddr,
|
||||
lighterPrivateKey,
|
||||
lighterApiKeyPrivateKey,
|
||||
enabled: true,
|
||||
}
|
||||
: e
|
||||
) || []
|
||||
} else {
|
||||
// 添加新配置
|
||||
const newExchange = {
|
||||
...exchangeToUpdate,
|
||||
apiKey,
|
||||
secretKey,
|
||||
passphrase, // OKX专用
|
||||
testnet,
|
||||
hyperliquidWalletAddr,
|
||||
asterUser,
|
||||
asterSigner,
|
||||
asterPrivateKey,
|
||||
lighterWalletAddr,
|
||||
lighterPrivateKey,
|
||||
lighterApiKeyPrivateKey,
|
||||
enabled: true,
|
||||
}
|
||||
updatedExchanges = [...(allExchanges || []), newExchange]
|
||||
}
|
||||
|
||||
const request = {
|
||||
exchanges: Object.fromEntries(
|
||||
updatedExchanges.map((exchange) => [
|
||||
exchange.id,
|
||||
{
|
||||
enabled: exchange.enabled,
|
||||
api_key: exchange.apiKey || '',
|
||||
secret_key: exchange.secretKey || '',
|
||||
passphrase: exchange.passphrase || '', // OKX专用
|
||||
testnet: exchange.testnet || false,
|
||||
hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '',
|
||||
aster_user: exchange.asterUser || '',
|
||||
aster_signer: exchange.asterSigner || '',
|
||||
aster_private_key: exchange.asterPrivateKey || '',
|
||||
lighter_wallet_addr: exchange.lighterWalletAddr || '',
|
||||
lighter_private_key: exchange.lighterPrivateKey || '',
|
||||
lighter_api_key_private_key: exchange.lighterApiKeyPrivateKey || '',
|
||||
},
|
||||
])
|
||||
),
|
||||
}
|
||||
|
||||
await toast.promise(api.updateExchangeConfigsEncrypted(request), {
|
||||
loading: '正在更新交易所配置…',
|
||||
success: '交易所配置已更新',
|
||||
error: '更新交易所配置失败',
|
||||
})
|
||||
|
||||
// 重新获取用户配置以确保数据同步
|
||||
const refreshedExchanges = await api.getExchangeConfigs()
|
||||
setAllExchanges(refreshedExchanges)
|
||||
|
||||
setShowExchangeModal(false)
|
||||
setEditingExchange(null)
|
||||
} catch (error) {
|
||||
console.error('Failed to save exchange config:', error)
|
||||
toast.error(t('saveConfigFailed', language))
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddModel = () => {
|
||||
setEditingModel(null)
|
||||
setShowModelModal(true)
|
||||
}
|
||||
|
||||
const handleAddExchange = () => {
|
||||
setEditingExchange(null)
|
||||
setShowExchangeModal(true)
|
||||
}
|
||||
|
||||
return {
|
||||
// 辅助函数
|
||||
isModelInUse,
|
||||
isExchangeInUse,
|
||||
isModelUsedByAnyTrader,
|
||||
isExchangeUsedByAnyTrader,
|
||||
getTradersUsingModel,
|
||||
getTradersUsingExchange,
|
||||
|
||||
// 事件处理函数
|
||||
handleCreateTrader,
|
||||
handleEditTrader,
|
||||
handleSaveEditTrader,
|
||||
handleDeleteTrader,
|
||||
handleToggleTrader,
|
||||
handleToggleCompetition,
|
||||
handleAddModel,
|
||||
handleAddExchange,
|
||||
handleModelClick,
|
||||
handleExchangeClick,
|
||||
handleSaveModel,
|
||||
handleDeleteModel,
|
||||
handleSaveExchange,
|
||||
handleDeleteExchange,
|
||||
}
|
||||
}
|
||||
@@ -438,6 +438,11 @@ export interface IndicatorConfig {
|
||||
quant_data_api_url?: string;
|
||||
enable_quant_oi?: boolean;
|
||||
enable_quant_netflow?: boolean;
|
||||
// OI 排行数据(市场持仓量增减排行)
|
||||
enable_oi_ranking?: boolean;
|
||||
oi_ranking_api_url?: string;
|
||||
oi_ranking_duration?: string; // "1h", "4h", "24h"
|
||||
oi_ranking_limit?: number;
|
||||
}
|
||||
|
||||
export interface KlineConfig {
|
||||
@@ -579,6 +584,10 @@ export interface CreateDebateRequest {
|
||||
prompt_variant?: string; // balanced, aggressive, conservative, scalping
|
||||
auto_execute?: boolean;
|
||||
trader_id?: string; // Trader to use for auto-execute
|
||||
// OI Ranking data options
|
||||
enable_oi_ranking?: boolean; // Whether to include OI ranking data
|
||||
oi_ranking_limit?: number; // Number of OI ranking entries (default 10)
|
||||
oi_duration?: string; // Duration for OI data (1h, 4h, 24h, etc.)
|
||||
participants: {
|
||||
ai_model_id: string;
|
||||
personality: DebatePersonality;
|
||||
|
||||
Reference in New Issue
Block a user