feat: add Qwen LLM integration for AI-powered indicator calculation

- Add QwenAgent client for Alibaba Cloud Bailian platform
- Support both sync and streaming chat modes
- Add multi-turn conversation with session management
- Add OpenAI-compatible API support
- Add indicator calculation tests comparing local vs AI results
- Use environment variables for API credentials (QWEN_APP_ID, QWEN_API_KEY)
This commit is contained in:
tinkle-community
2025-12-30 21:26:07 +08:00
parent ad04994d75
commit 70eb35d6de
3 changed files with 1513 additions and 0 deletions

737
llm/qwen_indicator_test.go Normal file
View File

@@ -0,0 +1,737 @@
package llm
import (
"context"
"encoding/json"
"fmt"
"math"
"nofx/market"
"nofx/provider/coinank"
"nofx/provider/coinank/coinank_api"
"nofx/provider/coinank/coinank_enum"
"regexp"
"strconv"
"strings"
"testing"
"time"
)
// IndicatorResult AI 计算的指标结果
type IndicatorResult struct {
EMA12 float64 `json:"ema12"`
EMA26 float64 `json:"ema26"`
MACD float64 `json:"macd"`
RSI14 float64 `json:"rsi14"`
BOLLUp float64 `json:"boll_upper"`
BOLLMid float64 `json:"boll_middle"`
BOLLLow float64 `json:"boll_lower"`
ATR14 float64 `json:"atr14"`
SMA20 float64 `json:"sma20"`
}
// 本地计算指标(使用 market 包的函数)
func calculateLocalIndicators(klines []market.Kline) IndicatorResult {
result := IndicatorResult{}
if len(klines) >= 12 {
result.EMA12 = market.ExportCalculateEMA(klines, 12)
}
if len(klines) >= 26 {
result.EMA26 = market.ExportCalculateEMA(klines, 26)
result.MACD = market.ExportCalculateMACD(klines)
}
if len(klines) > 14 {
result.RSI14 = market.ExportCalculateRSI(klines, 14)
}
if len(klines) >= 20 {
result.BOLLUp, result.BOLLMid, result.BOLLLow = market.ExportCalculateBOLL(klines, 20, 2.0)
// SMA20 就是 BOLL 中轨
result.SMA20 = result.BOLLMid
}
if len(klines) > 14 {
result.ATR14 = market.ExportCalculateATR(klines, 14)
}
return result
}
// 格式化 K 线数据为文本,发给 AI
func formatKlinesForAI(klines []market.Kline) string {
var sb strings.Builder
sb.WriteString("以下是K线数据从旧到新排列\n")
sb.WriteString("序号 | 时间 | 开盘价 | 最高价 | 最低价 | 收盘价 | 成交量\n")
sb.WriteString("-----|------|--------|--------|--------|--------|--------\n")
for i, k := range klines {
t := time.UnixMilli(k.OpenTime)
sb.WriteString(fmt.Sprintf("%d | %s | %.2f | %.2f | %.2f | %.2f | %.2f\n",
i+1, t.Format("01-02 15:04"), k.Open, k.High, k.Low, k.Close, k.Volume))
}
return sb.String()
}
// 构建 AI 计算指标的 prompt
func buildIndicatorPrompt(klines []market.Kline) string {
klinesText := formatKlinesForAI(klines)
prompt := fmt.Sprintf(`%s
请根据以上 %d 根K线数据计算以下技术指标使用标准算法
1. EMA1212周期指数移动平均线
2. EMA2626周期指数移动平均线
3. MACDEMA12 - EMA26
4. RSI1414周期相对强弱指标使用Wilder平滑法
5. BOLL布林带20周期2倍标准差上轨、中轨、下轨
6. ATR1414周期平均真实波幅使用Wilder平滑法
7. SMA2020周期简单移动平均线
请严格按照以下 JSON 格式返回结果,不要添加任何其他文字:
{
"ema12": 数值,
"ema26": 数值,
"macd": 数值,
"rsi14": 数值,
"boll_upper": 数值,
"boll_middle": 数值,
"boll_lower": 数值,
"atr14": 数值,
"sma20": 数值
}
注意:
- 所有数值保留2位小数
- EMA计算使用SMA作为初始值乘数为 2/(period+1)
- RSI使用Wilder平滑法
- 只返回JSON不要解释过程`, klinesText, len(klines))
return prompt
}
// 从 AI 响应中提取 JSON
func extractJSONFromResponse(text string) (IndicatorResult, error) {
var result IndicatorResult
// 尝试直接解析
if err := json.Unmarshal([]byte(text), &result); err == nil {
return result, nil
}
// 提取 JSON 部分
re := regexp.MustCompile(`\{[^{}]*"ema12"[^{}]*\}`)
match := re.FindString(text)
if match == "" {
// 尝试更宽松的匹配
start := strings.Index(text, "{")
end := strings.LastIndex(text, "}")
if start != -1 && end != -1 && end > start {
match = text[start : end+1]
}
}
if match == "" {
return result, fmt.Errorf("no JSON found in response: %s", text[:min(200, len(text))])
}
if err := json.Unmarshal([]byte(match), &result); err != nil {
return result, fmt.Errorf("parse JSON failed: %w, json: %s", err, match)
}
return result, nil
}
// 比较两个指标结果,返回误差百分比
func compareIndicators(local, ai IndicatorResult) map[string]float64 {
errors := make(map[string]float64)
calcError := func(name string, localVal, aiVal float64) {
if localVal == 0 {
if aiVal == 0 {
errors[name] = 0
} else {
errors[name] = 100 // 本地为0但AI不为0
}
return
}
errors[name] = math.Abs(localVal-aiVal) / math.Abs(localVal) * 100
}
calcError("EMA12", local.EMA12, ai.EMA12)
calcError("EMA26", local.EMA26, ai.EMA26)
calcError("MACD", local.MACD, ai.MACD)
calcError("RSI14", local.RSI14, ai.RSI14)
calcError("BOLL_UP", local.BOLLUp, ai.BOLLUp)
calcError("BOLL_MID", local.BOLLMid, ai.BOLLMid)
calcError("BOLL_LOW", local.BOLLLow, ai.BOLLLow)
calcError("ATR14", local.ATR14, ai.ATR14)
calcError("SMA20", local.SMA20, ai.SMA20)
return errors
}
// 生成测试用 K 线数据
func generateTestKlines(count int, basePrice float64) []market.Kline {
klines := make([]market.Kline, count)
price := basePrice
now := time.Now()
for i := 0; i < count; i++ {
// 模拟价格波动
change := (float64(i%7) - 3) * 0.5 // -1.5 到 +1.5 的波动
price = price + change
open := price
high := price + math.Abs(change)*0.5 + 0.5
low := price - math.Abs(change)*0.5 - 0.3
close := price + (change * 0.3)
klines[i] = market.Kline{
OpenTime: now.Add(time.Duration(-count+i) * time.Hour).UnixMilli(),
Open: open,
High: high,
Low: low,
Close: close,
Volume: 1000 + float64(i*100),
CloseTime: now.Add(time.Duration(-count+i+1) * time.Hour).UnixMilli(),
}
}
return klines
}
// TestQwenIndicatorCalculation 测试 AI 计算技术指标
func TestQwenIndicatorCalculation(t *testing.T) {
agent := NewQwenAgent(QwenAppID, QwenAPIKey)
ctx := context.Background()
// 生成 30 根测试 K 线
klines := generateTestKlines(30, 95000)
t.Log("===== K线数据 (最后5根) =====")
for i := len(klines) - 5; i < len(klines); i++ {
k := klines[i]
t.Logf(" [%d] O:%.2f H:%.2f L:%.2f C:%.2f", i+1, k.Open, k.High, k.Low, k.Close)
}
// 本地计算
t.Log("\n===== 本地计算结果 =====")
localResult := calculateLocalIndicators(klines)
t.Logf(" EMA12: %.2f", localResult.EMA12)
t.Logf(" EMA26: %.2f", localResult.EMA26)
t.Logf(" MACD: %.2f", localResult.MACD)
t.Logf(" RSI14: %.2f", localResult.RSI14)
t.Logf(" BOLL上轨: %.2f", localResult.BOLLUp)
t.Logf(" BOLL中轨: %.2f", localResult.BOLLMid)
t.Logf(" BOLL下轨: %.2f", localResult.BOLLLow)
t.Logf(" ATR14: %.2f", localResult.ATR14)
t.Logf(" SMA20: %.2f", localResult.SMA20)
// AI 计算
t.Log("\n===== 调用 AI 计算 =====")
prompt := buildIndicatorPrompt(klines)
t.Logf("Prompt 长度: %d 字符", len(prompt))
start := time.Now()
resp, err := agent.Chat(ctx, prompt)
elapsed := time.Since(start)
if err != nil {
t.Fatalf("AI 调用失败: %v", err)
}
t.Logf("AI 响应耗时: %v", elapsed)
t.Logf("AI 原始响应:\n%s", resp.Output.Text)
// 解析 AI 结果
aiResult, err := extractJSONFromResponse(resp.Output.Text)
if err != nil {
t.Fatalf("解析 AI 结果失败: %v", err)
}
t.Log("\n===== AI 计算结果 =====")
t.Logf(" EMA12: %.2f", aiResult.EMA12)
t.Logf(" EMA26: %.2f", aiResult.EMA26)
t.Logf(" MACD: %.2f", aiResult.MACD)
t.Logf(" RSI14: %.2f", aiResult.RSI14)
t.Logf(" BOLL上轨: %.2f", aiResult.BOLLUp)
t.Logf(" BOLL中轨: %.2f", aiResult.BOLLMid)
t.Logf(" BOLL下轨: %.2f", aiResult.BOLLLow)
t.Logf(" ATR14: %.2f", aiResult.ATR14)
t.Logf(" SMA20: %.2f", aiResult.SMA20)
// 对比结果
t.Log("\n===== 误差对比 (%) =====")
errors := compareIndicators(localResult, aiResult)
totalError := 0.0
for name, errPct := range errors {
status := "✓"
if errPct > 5 {
status = "⚠"
}
if errPct > 10 {
status = "✗"
}
t.Logf(" %s %s: %.2f%%", status, name, errPct)
totalError += errPct
}
avgError := totalError / float64(len(errors))
t.Logf("\n 平均误差: %.2f%%", avgError)
if avgError > 10 {
t.Logf("警告: AI 计算误差较大,可能算法理解有差异")
} else if avgError < 5 {
t.Log("AI 计算精度良好!")
}
}
// TestQwenIndicatorWithRealKlines 使用真实 K 线测试
func TestQwenIndicatorWithRealKlines(t *testing.T) {
// 尝试获取真实 K 线数据
client := market.NewAPIClient()
klines, err := client.GetKlines("BTC", "1h", 30)
if err != nil {
t.Skipf("获取真实 K 线失败,跳过测试: %v", err)
return
}
if len(klines) < 26 {
t.Skipf("K 线数量不足: %d", len(klines))
return
}
agent := NewQwenAgent(QwenAppID, QwenAPIKey)
ctx := context.Background()
t.Logf("获取到 %d 根 BTC 1h K线", len(klines))
t.Log("最新价格:", klines[len(klines)-1].Close)
// 本地计算
localResult := calculateLocalIndicators(klines)
t.Log("\n===== 本地计算 =====")
t.Logf(" EMA12: %.2f, EMA26: %.2f, MACD: %.2f", localResult.EMA12, localResult.EMA26, localResult.MACD)
t.Logf(" RSI14: %.2f", localResult.RSI14)
t.Logf(" BOLL: %.2f / %.2f / %.2f", localResult.BOLLUp, localResult.BOLLMid, localResult.BOLLLow)
// AI 计算
prompt := buildIndicatorPrompt(klines)
resp, err := agent.Chat(ctx, prompt)
if err != nil {
t.Fatalf("AI 调用失败: %v", err)
}
t.Log("\n===== AI 响应 =====")
t.Log(resp.Output.Text)
aiResult, err := extractJSONFromResponse(resp.Output.Text)
if err != nil {
t.Logf("解析失败: %v", err)
return
}
// 对比
errors := compareIndicators(localResult, aiResult)
t.Log("\n===== 误差 =====")
for name, errPct := range errors {
t.Logf(" %s: %.2f%%", name, errPct)
}
}
// TestQwenIndicatorMultiTimeframe 测试多个时间周期
func TestQwenIndicatorMultiTimeframe(t *testing.T) {
agent := NewQwenAgent(QwenAppID, QwenAPIKey)
ctx := context.Background()
timeframes := []struct {
name string
count int
price float64
}{
{"5m周期", 30, 95000},
{"1h周期", 50, 95000},
{"4h周期", 40, 95000},
}
for _, tf := range timeframes {
t.Run(tf.name, func(t *testing.T) {
klines := generateTestKlines(tf.count, tf.price)
localResult := calculateLocalIndicators(klines)
// 简化的 prompt
prompt := buildSimpleIndicatorPrompt(klines)
resp, err := agent.Chat(ctx, prompt)
if err != nil {
t.Fatalf("AI 调用失败: %v", err)
}
aiResult, err := extractJSONFromResponse(resp.Output.Text)
if err != nil {
t.Logf("解析失败: %v", err)
t.Logf("AI 响应: %s", resp.Output.Text[:min(500, len(resp.Output.Text))])
return
}
errors := compareIndicators(localResult, aiResult)
// 计算平均误差
total := 0.0
for _, e := range errors {
total += e
}
avgErr := total / float64(len(errors))
t.Logf("本地 MACD: %.2f, AI MACD: %.2f, 误差: %.2f%%", localResult.MACD, aiResult.MACD, errors["MACD"])
t.Logf("本地 RSI: %.2f, AI RSI: %.2f, 误差: %.2f%%", localResult.RSI14, aiResult.RSI14, errors["RSI14"])
t.Logf("平均误差: %.2f%%", avgErr)
})
time.Sleep(2 * time.Second) // 避免请求过快
}
}
// 简化的 prompt
func buildSimpleIndicatorPrompt(klines []market.Kline) string {
// 只提供收盘价序列,减少 token
var prices []string
for _, k := range klines {
prices = append(prices, fmt.Sprintf("%.2f", k.Close))
}
return fmt.Sprintf(`收盘价序列(从旧到新): [%s]
请计算技术指标并返回 JSON
- ema12: 12周期EMA
- ema26: 26周期EMA
- macd: EMA12-EMA26
- rsi14: 14周期RSI(Wilder平滑)
- boll_upper, boll_middle, boll_lower: 20周期BOLL(2倍标准差)
- atr14: 0 (无高低价数据)
- sma20: 20周期SMA
只返回JSON格式{"ema12":数值,"ema26":数值,...}`, strings.Join(prices, ","))
}
// TestQwenIndicatorAccuracy 精度测试:使用简单数据验证算法
func TestQwenIndicatorAccuracy(t *testing.T) {
agent := NewQwenAgent(QwenAppID, QwenAPIKey)
ctx := context.Background()
// 使用简单递增数据,便于验证
prices := []float64{
100, 101, 102, 103, 104, 105, 106, 107, 108, 109, // 1-10
110, 111, 112, 113, 114, 115, 116, 117, 118, 119, // 11-20
120, 121, 122, 123, 124, 125, 126, 127, 128, 129, // 21-30
}
// 构建 K 线
klines := make([]market.Kline, len(prices))
for i, p := range prices {
klines[i] = market.Kline{
Open: p - 0.5,
High: p + 1,
Low: p - 1,
Close: p,
}
}
// 本地计算
localResult := calculateLocalIndicators(klines)
t.Log("===== 简单递增数据测试 =====")
t.Logf("价格序列: %v", prices)
t.Logf("本地计算:")
t.Logf(" SMA20 = %.4f (理论值: 119.5)", localResult.SMA20)
t.Logf(" EMA12 = %.4f", localResult.EMA12)
t.Logf(" RSI14 = %.4f (持续上涨应接近100)", localResult.RSI14)
// AI 计算
var priceStrs []string
for _, p := range prices {
priceStrs = append(priceStrs, strconv.FormatFloat(p, 'f', 0, 64))
}
prompt := fmt.Sprintf(`收盘价序列: [%s]
请计算:
1. SMA20 (20周期简单移动平均)
2. EMA12 (12周期指数移动平均初始值用SMA乘数=2/13)
3. RSI14 (14周期RSIWilder平滑法)
返回JSON: {"sma20":数值,"ema12":数值,"rsi14":数值}
只返回JSON`, strings.Join(priceStrs, ","))
resp, err := agent.Chat(ctx, prompt)
if err != nil {
t.Fatalf("AI 调用失败: %v", err)
}
t.Logf("\nAI 响应: %s", resp.Output.Text)
// 简单解析
var aiSimple struct {
SMA20 float64 `json:"sma20"`
EMA12 float64 `json:"ema12"`
RSI14 float64 `json:"rsi14"`
}
text := resp.Output.Text
start := strings.Index(text, "{")
end := strings.LastIndex(text, "}")
if start != -1 && end > start {
json.Unmarshal([]byte(text[start:end+1]), &aiSimple)
}
t.Logf("\nAI 计算:")
t.Logf(" SMA20 = %.4f", aiSimple.SMA20)
t.Logf(" EMA12 = %.4f", aiSimple.EMA12)
t.Logf(" RSI14 = %.4f", aiSimple.RSI14)
// 验证 SMA20 (理论值应该是 110+...+129 的平均 = 119.5)
expectedSMA := 119.5
if math.Abs(aiSimple.SMA20-expectedSMA) < 0.1 {
t.Log("\n✓ AI 的 SMA20 计算正确!")
} else {
t.Logf("\n✗ AI 的 SMA20 有误差,期望 %.2f", expectedSMA)
}
}
// coinankKlinesToMarket 将 coinank K线转换为 market.Kline
func coinankKlinesToMarket(klines []coinank.KlineResult) []market.Kline {
result := make([]market.Kline, len(klines))
for i, k := range klines {
result[i] = market.Kline{
OpenTime: k.StartTime,
Open: k.Open,
High: k.High,
Low: k.Low,
Close: k.Close,
Volume: k.Volume,
CloseTime: k.EndTime,
}
}
return result
}
// TestQwenETHMultiTimeframe 使用 Coinank 免费 API 获取真实 ETH 数据测试多周期指标
func TestQwenETHMultiTimeframe(t *testing.T) {
ctx := context.Background()
agent := NewQwenAgent(QwenAppID, QwenAPIKey)
// 测试多个时间周期
timeframes := []struct {
name string
interval coinank_enum.Interval
size int
}{
{"5分钟", coinank_enum.Minute5, 50},
{"1小时", coinank_enum.Hour1, 50},
{"4小时", coinank_enum.Hour4, 50},
{"日线", coinank_enum.Day1, 30},
}
now := time.Now()
for _, tf := range timeframes {
t.Run(tf.name, func(t *testing.T) {
// 使用 coinank 免费 API 获取 ETH K线数据
coinankKlines, err := coinank_api.Kline(ctx, "ETHUSDT", coinank_enum.Binance,
now.UnixMilli(), coinank_enum.To, tf.size, tf.interval)
if err != nil {
t.Fatalf("获取 %s K线失败: %v", tf.name, err)
}
if len(coinankKlines) < 26 {
t.Skipf("K线数量不足: %d", len(coinankKlines))
return
}
// 转换为 market.Kline
klines := coinankKlinesToMarket(coinankKlines)
t.Logf("获取到 %d 根 ETH %s K线", len(klines), tf.name)
t.Logf("最新收盘价: %.2f, 时间: %s",
klines[len(klines)-1].Close,
time.UnixMilli(klines[len(klines)-1].CloseTime).Format("2006-01-02 15:04"))
// 本地计算
localResult := calculateLocalIndicators(klines)
t.Log("\n===== 本地计算 =====")
t.Logf(" EMA12: %.2f, EMA26: %.2f, MACD: %.4f",
localResult.EMA12, localResult.EMA26, localResult.MACD)
t.Logf(" RSI14: %.2f", localResult.RSI14)
t.Logf(" BOLL: %.2f / %.2f / %.2f",
localResult.BOLLUp, localResult.BOLLMid, localResult.BOLLLow)
t.Logf(" ATR14: %.4f", localResult.ATR14)
// AI 计算 - 使用简化 prompt只发收盘价
prompt := buildSimpleIndicatorPrompt(klines)
t.Logf("\nPrompt 长度: %d 字符", len(prompt))
start := time.Now()
resp, err := agent.Chat(ctx, prompt)
elapsed := time.Since(start)
if err != nil {
t.Fatalf("AI 调用失败: %v", err)
}
t.Logf("AI 响应耗时: %v", elapsed)
// 解析 AI 结果
aiResult, err := extractJSONFromResponse(resp.Output.Text)
if err != nil {
t.Logf("AI 原始响应:\n%s", resp.Output.Text[:min(500, len(resp.Output.Text))])
t.Fatalf("解析失败: %v", err)
}
t.Log("\n===== AI 计算 =====")
t.Logf(" EMA12: %.2f, EMA26: %.2f, MACD: %.4f",
aiResult.EMA12, aiResult.EMA26, aiResult.MACD)
t.Logf(" RSI14: %.2f", aiResult.RSI14)
t.Logf(" BOLL: %.2f / %.2f / %.2f",
aiResult.BOLLUp, aiResult.BOLLMid, aiResult.BOLLLow)
// 对比误差
t.Log("\n===== 误差对比 =====")
errors := compareIndicators(localResult, aiResult)
totalErr := 0.0
for name, errPct := range errors {
status := "✓"
if errPct > 1 {
status = "⚠"
}
if errPct > 5 {
status = "✗"
}
t.Logf(" %s %-10s: %.2f%%", status, name, errPct)
totalErr += errPct
}
avgErr := totalErr / float64(len(errors))
t.Logf("\n 平均误差: %.2f%%", avgErr)
if avgErr < 1 {
t.Log(" ✓ AI 计算精度优秀!")
} else if avgErr < 5 {
t.Log(" ⚠ AI 计算精度良好")
} else {
t.Log(" ✗ AI 计算误差较大")
}
// 等待避免请求过快
time.Sleep(2 * time.Second)
})
}
}
// TestQwenETHIndicatorComparison ETH 指标对比:使用 Coinank 免费 API + Qwen 标准 API
func TestQwenETHIndicatorComparison(t *testing.T) {
ctx := context.Background()
agent := NewQwenAgent(QwenAppID, QwenAPIKey)
// 使用 coinank 免费 API 获取 ETH 1小时 K线
now := time.Now()
coinankKlines, err := coinank_api.Kline(ctx, "ETHUSDT", coinank_enum.Binance,
now.UnixMilli(), coinank_enum.To, 30, coinank_enum.Hour1)
if err != nil {
t.Fatalf("获取 K线失败: %v", err)
}
// 转换为 market.Kline
klines := coinankKlinesToMarket(coinankKlines)
t.Logf("获取到 %d 根 ETH 1h K线", len(klines))
// 只用收盘价,简化 prompt
var prices []string
for _, k := range klines {
prices = append(prices, fmt.Sprintf("%.2f", k.Close))
}
// 本地计算
localResult := calculateLocalIndicators(klines)
t.Log("\n===== 本地计算结果 =====")
t.Logf("SMA20: %.2f", localResult.SMA20)
t.Logf("EMA12: %.2f", localResult.EMA12)
t.Logf("EMA26: %.2f", localResult.EMA26)
t.Logf("MACD: %.4f", localResult.MACD)
t.Logf("RSI14: %.2f", localResult.RSI14)
// 简化的 AI prompt
prompt := fmt.Sprintf(`ETH 最近30根1小时K线收盘价从旧到新:
[%s]
请计算以下指标并返回纯 JSON:
1. sma20: 最后20个价格的简单移动平均
2. ema12: 12周期EMA初始值用前12个价格的SMA乘数=2/13
3. ema26: 26周期EMA初始值用前26个价格的SMA乘数=2/27
4. macd: EMA12 - EMA26
5. rsi14: 14周期RSIWilder平滑法
只返回JSON格式: {"sma20":数值,"ema12":数值,"ema26":数值,"macd":数值,"rsi14":数值}
不要任何解释文字`, strings.Join(prices, ", "))
t.Logf("\n发送 Prompt (%d 字符)", len(prompt))
// 使用标准 API
resp, err := agent.ChatWithModel(ctx, "qwen-max", prompt)
if err != nil {
t.Fatalf("AI 调用失败: %v", err)
}
aiText := resp.GetContent()
t.Logf("\nAI 响应:\n%s", aiText)
// 解析
var aiResult struct {
SMA20 float64 `json:"sma20"`
EMA12 float64 `json:"ema12"`
EMA26 float64 `json:"ema26"`
MACD float64 `json:"macd"`
RSI14 float64 `json:"rsi14"`
}
start := strings.Index(aiText, "{")
end := strings.LastIndex(aiText, "}")
if start != -1 && end > start {
if err := json.Unmarshal([]byte(aiText[start:end+1]), &aiResult); err != nil {
t.Logf("JSON 解析失败: %v", err)
}
}
t.Log("\n===== AI 计算结果 =====")
t.Logf("SMA20: %.2f", aiResult.SMA20)
t.Logf("EMA12: %.2f", aiResult.EMA12)
t.Logf("EMA26: %.2f", aiResult.EMA26)
t.Logf("MACD: %.4f", aiResult.MACD)
t.Logf("RSI14: %.2f", aiResult.RSI14)
// 计算误差
t.Log("\n===== 误差 =====")
calcErr := func(name string, local, ai float64) {
if local == 0 {
t.Logf(" %s: 本地=0, AI=%.2f", name, ai)
return
}
errPct := math.Abs(local-ai) / math.Abs(local) * 100
status := "✓"
if errPct > 1 {
status = "⚠"
}
if errPct > 5 {
status = "✗"
}
t.Logf(" %s %s: 本地=%.2f, AI=%.2f, 误差=%.2f%%", status, name, local, ai, errPct)
}
calcErr("SMA20", localResult.SMA20, aiResult.SMA20)
calcErr("EMA12", localResult.EMA12, aiResult.EMA12)
calcErr("EMA26", localResult.EMA26, aiResult.EMA26)
calcErr("MACD", localResult.MACD, aiResult.MACD)
calcErr("RSI14", localResult.RSI14, aiResult.RSI14)
}