mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-09 22:10:57 +08:00
feat(config): 增加新闻源配置与数据库迁移支持
- 在config.json.example中添加新闻源相关配置示例,支持telegram频道订阅 - 扩展数据库表结构,新增traders表多字段支持信号源和杠杆参数 - 新增NewsConfig结构体及相关telegram新闻配置数据模型 - 在交易决策上下文结构Context中添加新闻数据news字段支持传递新闻 - 在交易决策构建用户提示信息时加入相关新闻内容 - 优化数据库操作代码,支持交易所和交易员配置的完整字段读取与更新 - 添加数据库exchanges表迁移逻辑,重建表结构和触发器以支持新字段 - 引入第三方库github.com/samber/lo用于集合操作 - 在go.mod添加新的依赖模块,并更新相关依赖版本
This commit is contained in:
@@ -4,13 +4,18 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"nofx/config"
|
||||
"nofx/decision"
|
||||
"nofx/logger"
|
||||
"nofx/market"
|
||||
"nofx/mcp"
|
||||
"nofx/news"
|
||||
"nofx/news/provider/telegram"
|
||||
"nofx/pool"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// AutoTraderConfig 自动交易配置(简化版 - AI全权决策)
|
||||
@@ -68,11 +73,14 @@ type AutoTraderConfig struct {
|
||||
IsCrossMargin bool // true=全仓模式, false=逐仓模式
|
||||
|
||||
// 币种配置
|
||||
DefaultCoins []string // 默认币种列表(从数据库获取)
|
||||
TradingCoins []string // 实际交易币种列表
|
||||
DefaultCoins []string // 默认币种列表(从数据库获取)
|
||||
TradingCoins []string // 实际交易币种列表
|
||||
|
||||
// 系统提示词模板
|
||||
SystemPromptTemplate string // 系统提示词模板名称(如 "default", "aggressive")
|
||||
|
||||
// 新闻源配置
|
||||
NewsConfig []config.NewsConfig
|
||||
}
|
||||
|
||||
// AutoTrader 自动交易器
|
||||
@@ -87,9 +95,9 @@ type AutoTrader struct {
|
||||
decisionLogger *logger.DecisionLogger // 决策日志记录器
|
||||
initialBalance float64
|
||||
dailyPnL float64
|
||||
customPrompt string // 自定义交易策略prompt
|
||||
overrideBasePrompt bool // 是否覆盖基础prompt
|
||||
systemPromptTemplate string // 系统提示词模板名称
|
||||
customPrompt string // 自定义交易策略prompt
|
||||
overrideBasePrompt bool // 是否覆盖基础prompt
|
||||
systemPromptTemplate string // 系统提示词模板名称
|
||||
defaultCoins []string // 默认币种列表(从数据库获取)
|
||||
tradingCoins []string // 实际交易币种列表
|
||||
lastResetTime time.Time
|
||||
@@ -98,58 +106,81 @@ type AutoTrader struct {
|
||||
startTime time.Time // 系统启动时间
|
||||
callCount int // AI调用次数
|
||||
positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒)
|
||||
newsProcessor []news.Provider // 新闻
|
||||
}
|
||||
|
||||
// NewAutoTrader 创建自动交易器
|
||||
func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
|
||||
func NewAutoTrader(traderConfig AutoTraderConfig) (*AutoTrader, error) {
|
||||
// 设置默认值
|
||||
if config.ID == "" {
|
||||
config.ID = "default_trader"
|
||||
if traderConfig.ID == "" {
|
||||
traderConfig.ID = "default_trader"
|
||||
}
|
||||
if config.Name == "" {
|
||||
config.Name = "Default Trader"
|
||||
if traderConfig.Name == "" {
|
||||
traderConfig.Name = "Default Trader"
|
||||
}
|
||||
if config.AIModel == "" {
|
||||
if config.UseQwen {
|
||||
config.AIModel = "qwen"
|
||||
if traderConfig.AIModel == "" {
|
||||
if traderConfig.UseQwen {
|
||||
traderConfig.AIModel = "qwen"
|
||||
} else {
|
||||
config.AIModel = "deepseek"
|
||||
traderConfig.AIModel = "deepseek"
|
||||
}
|
||||
}
|
||||
|
||||
mcpClient := mcp.New()
|
||||
|
||||
// 初始化AI
|
||||
if config.AIModel == "custom" {
|
||||
if traderConfig.AIModel == "custom" {
|
||||
// 使用自定义API
|
||||
mcpClient.SetCustomAPI(config.CustomAPIURL, config.CustomAPIKey, config.CustomModelName)
|
||||
log.Printf("🤖 [%s] 使用自定义AI API: %s (模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName)
|
||||
} else if config.UseQwen || config.AIModel == "qwen" {
|
||||
mcpClient.SetCustomAPI(traderConfig.CustomAPIURL, traderConfig.CustomAPIKey, traderConfig.CustomModelName)
|
||||
log.Printf("🤖 [%s] 使用自定义AI API: %s (模型: %s)", traderConfig.Name, traderConfig.CustomAPIURL, traderConfig.CustomModelName)
|
||||
} else if traderConfig.UseQwen || traderConfig.AIModel == "qwen" {
|
||||
// 使用Qwen (支持自定义URL和Model)
|
||||
mcpClient.SetQwenAPIKey(config.QwenKey, config.CustomAPIURL, config.CustomModelName)
|
||||
if config.CustomAPIURL != "" || config.CustomModelName != "" {
|
||||
log.Printf("🤖 [%s] 使用阿里云Qwen AI (自定义URL: %s, 模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName)
|
||||
mcpClient.SetQwenAPIKey(traderConfig.QwenKey, traderConfig.CustomAPIURL, traderConfig.CustomModelName)
|
||||
if traderConfig.CustomAPIURL != "" || traderConfig.CustomModelName != "" {
|
||||
log.Printf("🤖 [%s] 使用阿里云Qwen AI (自定义URL: %s, 模型: %s)", traderConfig.Name, traderConfig.CustomAPIURL, traderConfig.CustomModelName)
|
||||
} else {
|
||||
log.Printf("🤖 [%s] 使用阿里云Qwen AI", config.Name)
|
||||
log.Printf("🤖 [%s] 使用阿里云Qwen AI", traderConfig.Name)
|
||||
}
|
||||
} else {
|
||||
// 默认使用DeepSeek (支持自定义URL和Model)
|
||||
mcpClient.SetDeepSeekAPIKey(config.DeepSeekKey, config.CustomAPIURL, config.CustomModelName)
|
||||
if config.CustomAPIURL != "" || config.CustomModelName != "" {
|
||||
log.Printf("🤖 [%s] 使用DeepSeek AI (自定义URL: %s, 模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName)
|
||||
mcpClient.SetDeepSeekAPIKey(traderConfig.DeepSeekKey, traderConfig.CustomAPIURL, traderConfig.CustomModelName)
|
||||
if traderConfig.CustomAPIURL != "" || traderConfig.CustomModelName != "" {
|
||||
log.Printf("🤖 [%s] 使用DeepSeek AI (自定义URL: %s, 模型: %s)", traderConfig.Name, traderConfig.CustomAPIURL, traderConfig.CustomModelName)
|
||||
} else {
|
||||
log.Printf("🤖 [%s] 使用DeepSeek AI", config.Name)
|
||||
log.Printf("🤖 [%s] 使用DeepSeek AI", traderConfig.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化新闻提取器
|
||||
var newsProcessor []news.Provider
|
||||
for _, newsCfg := range traderConfig.NewsConfig {
|
||||
switch newsCfg.Provider {
|
||||
case news.ProviderTelegram:
|
||||
newsProvider, err := telegram.NewSearcher(newsCfg.Telegram.BaseURL,
|
||||
newsCfg.Telegram.ProxyURL,
|
||||
lo.Map(newsCfg.TelegramChannel, func(item config.NewsConfigTelegramChannel, _ int) telegram.Channel {
|
||||
return telegram.Channel{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
}
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
newsProcessor = append(newsProcessor, newsProvider)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化币种池API
|
||||
if config.CoinPoolAPIURL != "" {
|
||||
pool.SetCoinPoolAPI(config.CoinPoolAPIURL)
|
||||
if traderConfig.CoinPoolAPIURL != "" {
|
||||
pool.SetCoinPoolAPI(traderConfig.CoinPoolAPIURL)
|
||||
}
|
||||
|
||||
// 设置默认交易平台
|
||||
if config.Exchange == "" {
|
||||
config.Exchange = "binance"
|
||||
if traderConfig.Exchange == "" {
|
||||
traderConfig.Exchange = "binance"
|
||||
}
|
||||
|
||||
// 根据配置创建对应的交易器
|
||||
@@ -158,64 +189,65 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
|
||||
|
||||
// 记录仓位模式(通用)
|
||||
marginModeStr := "全仓"
|
||||
if !config.IsCrossMargin {
|
||||
if !traderConfig.IsCrossMargin {
|
||||
marginModeStr = "逐仓"
|
||||
}
|
||||
log.Printf("📊 [%s] 仓位模式: %s", config.Name, marginModeStr)
|
||||
log.Printf("📊 [%s] 仓位模式: %s", traderConfig.Name, marginModeStr)
|
||||
|
||||
switch config.Exchange {
|
||||
switch traderConfig.Exchange {
|
||||
case "binance":
|
||||
log.Printf("🏦 [%s] 使用币安合约交易", config.Name)
|
||||
trader = NewFuturesTrader(config.BinanceAPIKey, config.BinanceSecretKey)
|
||||
log.Printf("🏦 [%s] 使用币安合约交易", traderConfig.Name)
|
||||
trader = NewFuturesTrader(traderConfig.BinanceAPIKey, traderConfig.BinanceSecretKey)
|
||||
case "hyperliquid":
|
||||
log.Printf("🏦 [%s] 使用Hyperliquid交易", config.Name)
|
||||
trader, err = NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidWalletAddr, config.HyperliquidTestnet)
|
||||
log.Printf("🏦 [%s] 使用Hyperliquid交易", traderConfig.Name)
|
||||
trader, err = NewHyperliquidTrader(traderConfig.HyperliquidPrivateKey, traderConfig.HyperliquidWalletAddr, traderConfig.HyperliquidTestnet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("初始化Hyperliquid交易器失败: %w", err)
|
||||
}
|
||||
case "aster":
|
||||
log.Printf("🏦 [%s] 使用Aster交易", config.Name)
|
||||
trader, err = NewAsterTrader(config.AsterUser, config.AsterSigner, config.AsterPrivateKey)
|
||||
log.Printf("🏦 [%s] 使用Aster交易", traderConfig.Name)
|
||||
trader, err = NewAsterTrader(traderConfig.AsterUser, traderConfig.AsterSigner, traderConfig.AsterPrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("初始化Aster交易器失败: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的交易平台: %s", config.Exchange)
|
||||
return nil, fmt.Errorf("不支持的交易平台: %s", traderConfig.Exchange)
|
||||
}
|
||||
|
||||
// 验证初始金额配置
|
||||
if config.InitialBalance <= 0 {
|
||||
if traderConfig.InitialBalance <= 0 {
|
||||
return nil, fmt.Errorf("初始金额必须大于0,请在配置中设置InitialBalance")
|
||||
}
|
||||
|
||||
// 初始化决策日志记录器(使用trader ID创建独立目录)
|
||||
logDir := fmt.Sprintf("decision_logs/%s", config.ID)
|
||||
logDir := fmt.Sprintf("decision_logs/%s", traderConfig.ID)
|
||||
decisionLogger := logger.NewDecisionLogger(logDir)
|
||||
|
||||
// 设置默认系统提示词模板
|
||||
systemPromptTemplate := config.SystemPromptTemplate
|
||||
systemPromptTemplate := traderConfig.SystemPromptTemplate
|
||||
if systemPromptTemplate == "" {
|
||||
systemPromptTemplate = "default" // 默认使用 default 模板
|
||||
}
|
||||
|
||||
return &AutoTrader{
|
||||
id: config.ID,
|
||||
name: config.Name,
|
||||
aiModel: config.AIModel,
|
||||
exchange: config.Exchange,
|
||||
config: config,
|
||||
id: traderConfig.ID,
|
||||
name: traderConfig.Name,
|
||||
aiModel: traderConfig.AIModel,
|
||||
exchange: traderConfig.Exchange,
|
||||
config: traderConfig,
|
||||
trader: trader,
|
||||
mcpClient: mcpClient,
|
||||
decisionLogger: decisionLogger,
|
||||
initialBalance: config.InitialBalance,
|
||||
initialBalance: traderConfig.InitialBalance,
|
||||
systemPromptTemplate: systemPromptTemplate,
|
||||
defaultCoins: config.DefaultCoins,
|
||||
tradingCoins: config.TradingCoins,
|
||||
defaultCoins: traderConfig.DefaultCoins,
|
||||
tradingCoins: traderConfig.TradingCoins,
|
||||
lastResetTime: time.Now(),
|
||||
startTime: time.Now(),
|
||||
callCount: 0,
|
||||
isRunning: false,
|
||||
positionFirstSeenTime: make(map[string]int64),
|
||||
newsProcessor: newsProcessor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -558,7 +590,23 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
||||
performance = nil
|
||||
}
|
||||
|
||||
// 6. 构建上下文
|
||||
// 6.提取新闻内容
|
||||
newsItem := make(map[string][]news.NewsItem)
|
||||
for _, newspro := range at.newsProcessor {
|
||||
// TODO: 此出是为后续扩展考虑,当前随意给了个值占位
|
||||
newsMap, err := newspro.FetchNews([]string{"btc"}, 100)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 获取新闻内容失败: %v", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for symbol, value := range newsMap {
|
||||
newsItem[symbol] = append(newsItem[symbol], value...)
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 构建上下文
|
||||
ctx := &decision.Context{
|
||||
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
RuntimeMinutes: int(time.Since(at.startTime).Minutes()),
|
||||
@@ -577,6 +625,7 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
||||
Positions: positionInfos,
|
||||
CandidateCoins: candidateCoins,
|
||||
Performance: performance, // 添加历史表现分析
|
||||
News: newsItem,
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
@@ -1016,7 +1065,7 @@ func (at *AutoTrader) getCandidateCoins() ([]decision.CandidateCoin, error) {
|
||||
if len(at.tradingCoins) == 0 {
|
||||
// 使用数据库配置的默认币种列表
|
||||
var candidateCoins []decision.CandidateCoin
|
||||
|
||||
|
||||
if len(at.defaultCoins) > 0 {
|
||||
// 使用数据库中配置的默认币种
|
||||
for _, coin := range at.defaultCoins {
|
||||
@@ -1032,7 +1081,7 @@ func (at *AutoTrader) getCandidateCoins() ([]decision.CandidateCoin, error) {
|
||||
} else {
|
||||
// 如果数据库中没有配置默认币种,则使用AI500+OI Top作为fallback
|
||||
const ai500Limit = 20 // AI500取前20个评分最高的币种
|
||||
|
||||
|
||||
mergedPool, err := pool.GetMergedCoinPool(ai500Limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取合并币种池失败: %w", err)
|
||||
@@ -1073,11 +1122,11 @@ func (at *AutoTrader) getCandidateCoins() ([]decision.CandidateCoin, error) {
|
||||
func normalizeSymbol(symbol string) string {
|
||||
// 转为大写
|
||||
symbol = strings.ToUpper(strings.TrimSpace(symbol))
|
||||
|
||||
|
||||
// 确保以USDT结尾
|
||||
if !strings.HasSuffix(symbol, "USDT") {
|
||||
symbol = symbol + "USDT"
|
||||
}
|
||||
|
||||
|
||||
return symbol
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user