mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-04 03:21:04 +08:00
Merge branch 'dev' of https://github.com/NoFxAiOS/nofx into fix/stop-loss-take-profit-method-calls
This commit is contained in:
@@ -762,7 +762,21 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个交易员获取AI模型和交易所配置
|
||||
// 🔧 性能优化:在循环外只查询一次AI模型和交易所配置
|
||||
// 避免在循环中重复查询相同的数据,减少数据库压力和锁持有时间
|
||||
aiModels, err := database.GetAIModels(userID)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 获取用户 %s 的AI模型配置失败: %v", userID, err)
|
||||
return fmt.Errorf("获取AI模型配置失败: %w", err)
|
||||
}
|
||||
|
||||
exchanges, err := database.GetExchanges(userID)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 获取用户 %s 的交易所配置失败: %v", userID, err)
|
||||
return fmt.Errorf("获取交易所配置失败: %w", err)
|
||||
}
|
||||
|
||||
// 为每个交易员加载配置
|
||||
for _, traderCfg := range traders {
|
||||
// 检查是否已经加载过这个交易员
|
||||
if _, exists := tm.traders[traderCfg.ID]; exists {
|
||||
@@ -770,12 +784,7 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取AI模型配置(使用该用户的配置)
|
||||
aiModels, err := database.GetAIModels(userID)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 获取用户 %s 的AI模型配置失败: %v", userID, err)
|
||||
continue
|
||||
}
|
||||
// 从已查询的列表中查找AI模型配置
|
||||
|
||||
var aiModelCfg *config.AIModelConfig
|
||||
// 优先精确匹配 model.ID(新版逻辑)
|
||||
@@ -806,13 +815,7 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取交易所配置(使用该用户的配置)
|
||||
exchanges, err := database.GetExchanges(userID)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 获取用户 %s 的交易所配置失败: %v", userID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 从已查询的列表中查找交易所配置
|
||||
var exchangeCfg *config.ExchangeConfig
|
||||
for _, exchange := range exchanges {
|
||||
if exchange.ID == traderCfg.ExchangeID {
|
||||
|
||||
11
start.sh
11
start.sh
@@ -117,12 +117,21 @@ read_env_vars() {
|
||||
# Validation: Database File (config.db)
|
||||
# ------------------------------------------------------------------------
|
||||
check_database() {
|
||||
if [ ! -f "config.db" ]; then
|
||||
if [ -d "config.db" ]; then
|
||||
# 如果存在的是目录,删除它
|
||||
print_warning "config.db 是目录而非文件,正在删除目录..."
|
||||
rm -rf config.db
|
||||
print_info "✓ 已删除目录,现在创建文件..."
|
||||
touch config.db
|
||||
print_success "✓ 已创建空数据库文件,系统将在启动时初始化"
|
||||
elif [ ! -f "config.db" ]; then
|
||||
# 如果不存在文件,创建它
|
||||
print_warning "数据库文件不存在,创建空数据库文件..."
|
||||
# 创建空文件以避免Docker创建目录
|
||||
touch config.db
|
||||
print_info "✓ 已创建空数据库文件,系统将在启动时初始化"
|
||||
else
|
||||
# 文件存在
|
||||
print_success "数据库文件存在"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -131,9 +131,22 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
loadConfigs()
|
||||
}, [user, token])
|
||||
|
||||
// 显示所有用户的模型和交易所配置(用于调试)
|
||||
const configuredModels = allModels || []
|
||||
const configuredExchanges = allExchanges || []
|
||||
// 只显示已配置的模型和交易所(有API Key的才算配置过)
|
||||
const configuredModels =
|
||||
allModels?.filter((m) => m.apiKey && m.apiKey.trim() !== '') || []
|
||||
const configuredExchanges =
|
||||
allExchanges?.filter((e) => {
|
||||
// Aster 交易所检查特殊字段
|
||||
if (e.id === 'aster') {
|
||||
return e.asterUser && e.asterUser.trim() !== ''
|
||||
}
|
||||
// Hyperliquid 只检查私钥
|
||||
if (e.id === 'hyperliquid') {
|
||||
return e.apiKey && e.apiKey.trim() !== ''
|
||||
}
|
||||
// 其他交易所检查 apiKey
|
||||
return e.apiKey && e.apiKey.trim() !== ''
|
||||
}) || []
|
||||
|
||||
// 只在创建交易员时使用已启用且配置完整的
|
||||
const enabledModels = allModels?.filter((m) => m.enabled && m.apiKey) || []
|
||||
@@ -167,17 +180,34 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
)
|
||||
}) || []
|
||||
|
||||
// 检查模型是否正在被运行中的交易员使用
|
||||
// 检查模型是否正在被运行中的交易员使用(用于UI禁用)
|
||||
const isModelInUse = (modelId: string) => {
|
||||
return traders?.some((t) => t.ai_model === modelId && t.is_running) || false
|
||||
return traders?.some((t) => t.ai_model === modelId && t.is_running)
|
||||
}
|
||||
|
||||
// 检查交易所是否正在被运行中的交易员使用
|
||||
// 检查交易所是否正在被运行中的交易员使用(用于UI禁用)
|
||||
const isExchangeInUse = (exchangeId: string) => {
|
||||
return (
|
||||
traders?.some((t) => t.exchange_id === exchangeId && t.is_running) ||
|
||||
false
|
||||
)
|
||||
return traders?.some((t) => t.exchange_id === exchangeId && t.is_running)
|
||||
}
|
||||
|
||||
// 检查模型是否被任何交易员使用(包括停止状态的)
|
||||
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) => {
|
||||
@@ -298,27 +328,81 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteModelConfig = async (modelId: string) => {
|
||||
if (!confirm(t('confirmDeleteModel', language))) return
|
||||
// 通用删除配置处理函数
|
||||
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(', ')
|
||||
alert(
|
||||
t(config.cannotDeleteKey, language) +
|
||||
'\n\n' +
|
||||
t('tradersUsing', language) +
|
||||
': ' +
|
||||
traderNames +
|
||||
'\n\n' +
|
||||
t('pleaseDeleteTradersFirst', language)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!confirm(t(config.confirmDeleteKey, language))) return
|
||||
|
||||
try {
|
||||
const updatedModels =
|
||||
allModels?.map((m) =>
|
||||
m.id === modelId
|
||||
? {
|
||||
...m,
|
||||
apiKey: '',
|
||||
customApiUrl: '',
|
||||
customModelName: '',
|
||||
enabled: false,
|
||||
}
|
||||
: m
|
||||
const updatedItems =
|
||||
config.allItems?.map((item) =>
|
||||
item.id === config.id ? config.clearFields(item) : item
|
||||
) || []
|
||||
|
||||
const request = {
|
||||
const request = config.buildRequest(updatedItems)
|
||||
await config.updateApi(request)
|
||||
|
||||
// 重新获取用户配置以确保数据同步
|
||||
const refreshedItems = await config.refreshApi()
|
||||
config.setItems(refreshedItems)
|
||||
|
||||
config.closeModal()
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete ${config.type} config:`, error)
|
||||
alert(t(config.errorKey, language))
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteModelConfig = 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(
|
||||
updatedModels.map((model) => [
|
||||
model.provider, // 使用 provider 而不是 id
|
||||
models.map((model) => [
|
||||
model.provider,
|
||||
{
|
||||
enabled: model.enabled,
|
||||
api_key: model.apiKey || '',
|
||||
@@ -327,16 +411,19 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
},
|
||||
])
|
||||
),
|
||||
}
|
||||
|
||||
await api.updateModelConfigs(request)
|
||||
setAllModels(updatedModels)
|
||||
setShowModelModal(false)
|
||||
setEditingModel(null)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete model config:', error)
|
||||
alert(t('deleteConfigFailed', language))
|
||||
}
|
||||
}),
|
||||
updateApi: api.updateModelConfigs,
|
||||
refreshApi: api.getModelConfigs,
|
||||
setItems: (items) => {
|
||||
// 使用函数式更新确保状态正确更新
|
||||
setAllModels([...items])
|
||||
},
|
||||
closeModal: () => {
|
||||
setShowModelModal(false)
|
||||
setEditingModel(null)
|
||||
},
|
||||
errorKey: 'deleteConfigFailed',
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveModelConfig = async (
|
||||
@@ -413,19 +500,23 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
const handleDeleteExchangeConfig = async (exchangeId: string) => {
|
||||
if (!confirm(t('confirmDeleteExchange', language))) return
|
||||
|
||||
try {
|
||||
const updatedExchanges =
|
||||
allExchanges?.map((e) =>
|
||||
e.id === exchangeId
|
||||
? { ...e, apiKey: '', secretKey: '', enabled: false }
|
||||
: e
|
||||
) || []
|
||||
|
||||
const request = {
|
||||
await handleDeleteConfig({
|
||||
id: exchangeId,
|
||||
type: 'exchange',
|
||||
checkInUse: isExchangeUsedByAnyTrader,
|
||||
getUsingTraders: getTradersUsingExchange,
|
||||
cannotDeleteKey: 'cannotDeleteExchangeInUse',
|
||||
confirmDeleteKey: 'confirmDeleteExchange',
|
||||
allItems: allExchanges,
|
||||
clearFields: (e) => ({
|
||||
...e,
|
||||
apiKey: '',
|
||||
secretKey: '',
|
||||
enabled: false,
|
||||
}),
|
||||
buildRequest: (exchanges) => ({
|
||||
exchanges: Object.fromEntries(
|
||||
updatedExchanges.map((exchange) => [
|
||||
exchanges.map((exchange) => [
|
||||
exchange.id,
|
||||
{
|
||||
enabled: exchange.enabled,
|
||||
@@ -435,16 +526,19 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
},
|
||||
])
|
||||
),
|
||||
}
|
||||
|
||||
await api.updateExchangeConfigs(request)
|
||||
setAllExchanges(updatedExchanges)
|
||||
setShowExchangeModal(false)
|
||||
setEditingExchange(null)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete exchange config:', error)
|
||||
alert(t('deleteExchangeConfigFailed', language))
|
||||
}
|
||||
}),
|
||||
updateApi: api.updateExchangeConfigs,
|
||||
refreshApi: api.getExchangeConfigs,
|
||||
setItems: (items) => {
|
||||
// 使用函数式更新确保状态正确更新
|
||||
setAllExchanges([...items])
|
||||
},
|
||||
closeModal: () => {
|
||||
setShowExchangeModal(false)
|
||||
setEditingExchange(null)
|
||||
},
|
||||
errorKey: 'deleteExchangeConfigFailed',
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveExchangeConfig = async (
|
||||
@@ -1354,14 +1448,10 @@ function ModelConfigModal({
|
||||
{editingModelId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (confirm(t('confirmDeleteModel', language))) {
|
||||
onDelete(editingModelId)
|
||||
}
|
||||
}}
|
||||
onClick={() => onDelete(editingModelId)}
|
||||
className="p-2 rounded hover:bg-red-100 transition-colors"
|
||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
|
||||
title={t('deleteConfigFailed', language)}
|
||||
title={t('delete', language)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -1713,17 +1803,13 @@ function ExchangeConfigModal({
|
||||
{editingExchangeId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (confirm(t('confirmDeleteExchange', language))) {
|
||||
onDelete(editingExchangeId)
|
||||
}
|
||||
}}
|
||||
onClick={() => onDelete(editingExchangeId)}
|
||||
className="p-2 rounded hover:bg-red-100 transition-colors"
|
||||
style={{
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
}}
|
||||
title={t('deleteConfigFailed', language)}
|
||||
title={t('delete', language)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
@@ -265,6 +265,11 @@ export const translations = {
|
||||
addAIModel: 'Add AI Model',
|
||||
confirmDeleteModel:
|
||||
'Are you sure you want to delete this AI model configuration?',
|
||||
cannotDeleteModelInUse:
|
||||
'Cannot delete this AI model because it is being used by traders',
|
||||
tradersUsing: 'Traders using this configuration',
|
||||
pleaseDeleteTradersFirst:
|
||||
'Please delete or reconfigure these traders first',
|
||||
selectModel: 'Select AI Model',
|
||||
pleaseSelectModel: 'Please select a model',
|
||||
customBaseURL: 'Base URL (Optional)',
|
||||
@@ -281,6 +286,8 @@ export const translations = {
|
||||
addExchange: 'Add Exchange',
|
||||
confirmDeleteExchange:
|
||||
'Are you sure you want to delete this exchange configuration?',
|
||||
cannotDeleteExchangeInUse:
|
||||
'Cannot delete this exchange because it is being used by traders',
|
||||
pleaseSelectExchange: 'Please select an exchange',
|
||||
exchangeConfigWarning1:
|
||||
'• API keys will be encrypted, recommend using read-only or futures trading permissions',
|
||||
@@ -929,6 +936,9 @@ export const translations = {
|
||||
editAIModel: '编辑AI模型',
|
||||
addAIModel: '添加AI模型',
|
||||
confirmDeleteModel: '确定要删除此AI模型配置吗?',
|
||||
cannotDeleteModelInUse: '无法删除此AI模型,因为有交易员正在使用',
|
||||
tradersUsing: '正在使用此配置的交易员',
|
||||
pleaseDeleteTradersFirst: '请先删除或重新配置这些交易员',
|
||||
selectModel: '选择AI模型',
|
||||
pleaseSelectModel: '请选择模型',
|
||||
customBaseURL: 'Base URL (可选)',
|
||||
@@ -941,6 +951,7 @@ export const translations = {
|
||||
editExchange: '编辑交易所',
|
||||
addExchange: '添加交易所',
|
||||
confirmDeleteExchange: '确定要删除此交易所配置吗?',
|
||||
cannotDeleteExchangeInUse: '无法删除此交易所,因为有交易员正在使用',
|
||||
pleaseSelectExchange: '请选择交易所',
|
||||
exchangeConfigWarning1: '• API密钥将被加密存储,建议使用只读或期货交易权限',
|
||||
exchangeConfigWarning2: '• 不要授予提现权限,确保资金安全',
|
||||
|
||||
Reference in New Issue
Block a user