feat(config): 增加新闻源配置与数据库迁移支持

- 在config.json.example中添加新闻源相关配置示例,支持telegram频道订阅
- 扩展数据库表结构,新增traders表多字段支持信号源和杠杆参数
- 新增NewsConfig结构体及相关telegram新闻配置数据模型
- 在交易决策上下文结构Context中添加新闻数据news字段支持传递新闻
- 在交易决策构建用户提示信息时加入相关新闻内容
- 优化数据库操作代码,支持交易所和交易员配置的完整字段读取与更新
- 添加数据库exchanges表迁移逻辑,重建表结构和触发器以支持新字段
- 引入第三方库github.com/samber/lo用于集合操作
- 在go.mod添加新的依赖模块,并更新相关依赖版本
This commit is contained in:
wwg
2025-11-01 23:10:02 +08:00
parent 17c927b04a
commit d2af549bac
12 changed files with 934 additions and 209 deletions

View File

@@ -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
}