mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-13 07:46:54 +08:00
- Split AI prompts into system (cacheable) and user (dynamic) messages - Add confidence and risk_usd fields to trading decisions - Reduce temperature to 0.5 for more stable JSON output - Add retry mechanism for AI API calls - Simplify candidate display (show top 10 only) - Improve prompt structure for better AI decision quality
606 lines
18 KiB
Go
606 lines
18 KiB
Go
package market
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// SignalType 交易信号类型
|
||
type SignalType string
|
||
|
||
const (
|
||
SignalOpenLong SignalType = "OPEN_LONG" // 开多仓
|
||
SignalOpenShort SignalType = "OPEN_SHORT" // 开空仓
|
||
SignalCloseLong SignalType = "CLOSE_LONG" // 平多仓
|
||
SignalCloseShort SignalType = "CLOSE_SHORT" // 平空仓
|
||
SignalHold SignalType = "HOLD" // 持仓不动
|
||
SignalWait SignalType = "WAIT" // 观望
|
||
)
|
||
|
||
// TradingSignal AI返回的交易信号
|
||
type TradingSignal struct {
|
||
Symbol string `json:"symbol"` // 币种符号
|
||
Signal SignalType `json:"signal"` // 信号类型
|
||
Confidence float64 `json:"confidence"` // 信心度 (0-100)
|
||
Reasoning string `json:"reasoning"` // 分析理由
|
||
EntryPrice float64 `json:"entry_price"` // 建议入场价格
|
||
StopLoss float64 `json:"stop_loss"` // 建议止损价格
|
||
TakeProfit float64 `json:"take_profit"` // 建议止盈价格
|
||
Timestamp time.Time `json:"timestamp"` // 信号生成时间
|
||
}
|
||
|
||
// AIProvider AI提供商类型
|
||
type AIProvider string
|
||
|
||
const (
|
||
ProviderDeepSeek AIProvider = "deepseek"
|
||
ProviderQwen AIProvider = "qwen"
|
||
)
|
||
|
||
// AIConfig AI API配置
|
||
type AIConfig struct {
|
||
Provider AIProvider
|
||
APIKey string
|
||
SecretKey string // 阿里云需要
|
||
BaseURL string
|
||
Model string
|
||
Timeout time.Duration
|
||
}
|
||
|
||
// 默认配置
|
||
var defaultConfig = AIConfig{
|
||
Provider: ProviderDeepSeek,
|
||
BaseURL: "https://api.deepseek.com/v1",
|
||
Model: "deepseek-chat",
|
||
Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据
|
||
}
|
||
|
||
// SetDeepSeekAPIKey 设置DeepSeek API密钥
|
||
func SetDeepSeekAPIKey(apiKey string) {
|
||
defaultConfig.Provider = ProviderDeepSeek
|
||
defaultConfig.APIKey = apiKey
|
||
defaultConfig.BaseURL = "https://api.deepseek.com/v1"
|
||
defaultConfig.Model = "deepseek-chat"
|
||
}
|
||
|
||
// SetQwenAPIKey 设置阿里云Qwen API密钥
|
||
func SetQwenAPIKey(apiKey, secretKey string) {
|
||
defaultConfig.Provider = ProviderQwen
|
||
defaultConfig.APIKey = apiKey
|
||
defaultConfig.SecretKey = secretKey
|
||
defaultConfig.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||
defaultConfig.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max
|
||
}
|
||
|
||
// SetAIConfig 设置完整的AI配置(高级用户)
|
||
func SetAIConfig(config AIConfig) {
|
||
if config.Timeout == 0 {
|
||
config.Timeout = 30 * time.Second
|
||
}
|
||
defaultConfig = config
|
||
}
|
||
|
||
// DeepSeekConfig 兼容旧代码
|
||
type DeepSeekConfig = AIConfig
|
||
|
||
// SetDeepSeekConfig 兼容旧代码
|
||
func SetDeepSeekConfig(config DeepSeekConfig) {
|
||
SetAIConfig(config)
|
||
}
|
||
|
||
// GetAITradingSignal 获取AI交易信号
|
||
func GetAITradingSignal(symbol string) (*TradingSignal, error) {
|
||
// 1. 获取市场数据
|
||
marketData, err := GetMarketData(symbol)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取市场数据失败: %w", err)
|
||
}
|
||
|
||
// 2. 格式化为AI提示
|
||
prompt := formatMarketDataForAI(marketData)
|
||
|
||
// 3. 调用DeepSeek API
|
||
aiResponse, err := callDeepSeekAPI(prompt)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("调用DeepSeek API失败: %w", err)
|
||
}
|
||
|
||
// 4. 解析AI响应
|
||
signal, err := parseAIResponse(aiResponse, marketData)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("解析AI响应失败: %w", err)
|
||
}
|
||
|
||
signal.Symbol = marketData.Symbol
|
||
signal.Timestamp = time.Now()
|
||
|
||
return signal, nil
|
||
}
|
||
|
||
// formatMarketDataForAI 将市场数据格式化为AI提示
|
||
func formatMarketDataForAI(data *MarketData) string {
|
||
var sb strings.Builder
|
||
|
||
sb.WriteString("你是一位专业的加密货币交易员,请根据以下市场数据分析并给出交易建议。\n\n")
|
||
sb.WriteString(fmt.Sprintf("【币种】%s\n\n", data.Symbol))
|
||
|
||
// 当前指标
|
||
sb.WriteString("【当前实时指标】(基于3分钟K线)\n")
|
||
sb.WriteString(fmt.Sprintf("• 当前价格: %.4f USDT\n", data.CurrentPrice))
|
||
sb.WriteString(fmt.Sprintf("• EMA20: %.4f (价格%s均线)\n", data.CurrentEMA20,
|
||
pricePosition(data.CurrentPrice, data.CurrentEMA20)))
|
||
sb.WriteString(fmt.Sprintf("• MACD: %.4f (%s)\n", data.CurrentMACD, macdTrend(data.CurrentMACD)))
|
||
sb.WriteString(fmt.Sprintf("• RSI(7期): %.2f (%s)\n\n", data.CurrentRSI7, rsiStatus(data.CurrentRSI7)))
|
||
|
||
// 持仓量和资金费率
|
||
if data.OpenInterest != nil {
|
||
oiChange := ((data.OpenInterest.Latest - data.OpenInterest.Average) / data.OpenInterest.Average) * 100
|
||
sb.WriteString("【持仓量与资金费率】\n")
|
||
sb.WriteString(fmt.Sprintf("• 当前持仓量: %.2f (较平均%+.2f%%)\n",
|
||
data.OpenInterest.Latest, oiChange))
|
||
sb.WriteString(fmt.Sprintf("• 资金费率: %.6f (%s)\n\n",
|
||
data.FundingRate, fundingRateStatus(data.FundingRate)))
|
||
}
|
||
|
||
// 日内趋势
|
||
if data.IntradaySeries != nil && len(data.IntradaySeries.MACDValues) > 0 {
|
||
sb.WriteString("【日内趋势】(3分钟K线最近10个点)\n")
|
||
sb.WriteString(fmt.Sprintf("• 价格序列: %s\n", formatFloatArray(data.IntradaySeries.MidPrices)))
|
||
sb.WriteString(fmt.Sprintf("• MACD序列: %s (%s)\n",
|
||
formatFloatArray(data.IntradaySeries.MACDValues),
|
||
seriesTrend(data.IntradaySeries.MACDValues)))
|
||
sb.WriteString(fmt.Sprintf("• RSI(7期)序列: %s (%s)\n\n",
|
||
formatFloatArray(data.IntradaySeries.RSI7Values),
|
||
rsiSeriesTrend(data.IntradaySeries.RSI7Values)))
|
||
}
|
||
|
||
// 长期背景
|
||
if data.LongerTermContext != nil {
|
||
sb.WriteString("【长期背景】(4小时K线)\n")
|
||
sb.WriteString(fmt.Sprintf("• EMA20: %.2f vs EMA50: %.2f (%s)\n",
|
||
data.LongerTermContext.EMA20, data.LongerTermContext.EMA50,
|
||
emaCross(data.LongerTermContext.EMA20, data.LongerTermContext.EMA50)))
|
||
sb.WriteString(fmt.Sprintf("• ATR(3期): %.2f vs ATR(14期): %.2f (波动率%s)\n",
|
||
data.LongerTermContext.ATR3, data.LongerTermContext.ATR14,
|
||
atrStatus(data.LongerTermContext.ATR3, data.LongerTermContext.ATR14)))
|
||
sb.WriteString(fmt.Sprintf("• 当前成交量: %.2f vs 平均成交量: %.2f (%s)\n",
|
||
data.LongerTermContext.CurrentVolume, data.LongerTermContext.AverageVolume,
|
||
volumeStatus(data.LongerTermContext.CurrentVolume, data.LongerTermContext.AverageVolume)))
|
||
|
||
if len(data.LongerTermContext.RSI14Values) > 0 {
|
||
sb.WriteString(fmt.Sprintf("• 4小时RSI(14期): %.2f (%s)\n\n",
|
||
data.LongerTermContext.RSI14Values[len(data.LongerTermContext.RSI14Values)-1],
|
||
rsiStatus(data.LongerTermContext.RSI14Values[len(data.LongerTermContext.RSI14Values)-1])))
|
||
}
|
||
}
|
||
|
||
// AI指令
|
||
sb.WriteString("【交易建议要求】\n")
|
||
sb.WriteString("你是一位**激进型交易员**,善于捕捉市场机会。请基于以上数据,给出一个**明确的交易信号**。\n\n")
|
||
sb.WriteString("**重要原则:**\n")
|
||
sb.WriteString("1. 优先给出 OPEN_LONG 或 OPEN_SHORT 信号,而不是观望\n")
|
||
sb.WriteString("2. 即使信号不完美,也要找出最可能的方向\n")
|
||
sb.WriteString("3. RSI超买可能是强势延续,RSI超卖可能是抄底机会\n")
|
||
sb.WriteString("4. MACD负值转正 = 买入信号,正值转负 = 卖出信号\n")
|
||
sb.WriteString("5. 价格突破EMA20 = 趋势确认\n")
|
||
sb.WriteString("6. 持仓量增加 + 价格上涨 = 多头强势\n")
|
||
sb.WriteString("7. 只有在多空完全平衡、无法判断时才给 WAIT\n\n")
|
||
sb.WriteString("请严格按照以下JSON格式返回:\n\n")
|
||
sb.WriteString("```json\n")
|
||
sb.WriteString("{\n")
|
||
sb.WriteString(" \"signal\": \"OPEN_LONG | OPEN_SHORT | CLOSE_LONG | CLOSE_SHORT | HOLD | WAIT\",\n")
|
||
sb.WriteString(" \"confidence\": 85.5,\n")
|
||
sb.WriteString(" \"reasoning\": \"详细分析理由(200字以内)\",\n")
|
||
sb.WriteString(" \"entry_price\": 1.234,\n")
|
||
sb.WriteString(" \"stop_loss\": 1.100,\n")
|
||
sb.WriteString(" \"take_profit\": 1.450\n")
|
||
sb.WriteString("}\n")
|
||
sb.WriteString("```\n\n")
|
||
sb.WriteString("注意:\n")
|
||
sb.WriteString("1. signal必须是以下之一: OPEN_LONG(开多), OPEN_SHORT(开空), CLOSE_LONG(平多), CLOSE_SHORT(平空), HOLD(持有), WAIT(观望)\n")
|
||
sb.WriteString("2. confidence是信心度(0-100),即使是中等信号也应该给出\n")
|
||
sb.WriteString("3. reasoning要简洁有力,说明最关键的交易依据\n")
|
||
sb.WriteString("4. entry_price是建议入场价格(可以略高于或低于当前价)\n")
|
||
sb.WriteString("5. stop_loss和take_profit要合理,建议风险回报比至少1:2\n")
|
||
|
||
return sb.String()
|
||
}
|
||
|
||
// callDeepSeekAPI 调用AI API(支持DeepSeek和Qwen),带重试机制
|
||
// 兼容旧代码:只传user prompt
|
||
func callDeepSeekAPI(prompt string) (string, error) {
|
||
return callAIWithMessages("", prompt)
|
||
}
|
||
|
||
// callAIWithMessages 使用 system + user prompt 调用AI API(推荐)
|
||
func callAIWithMessages(systemPrompt, userPrompt string) (string, error) {
|
||
if defaultConfig.APIKey == "" {
|
||
return "", fmt.Errorf("AI API密钥未设置,请先调用 SetDeepSeekAPIKey() 或 SetQwenAPIKey()")
|
||
}
|
||
|
||
// 重试配置
|
||
maxRetries := 3
|
||
var lastErr error
|
||
|
||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||
if attempt > 1 {
|
||
fmt.Printf("⚠️ AI API调用失败,正在重试 (%d/%d)...\n", attempt, maxRetries)
|
||
}
|
||
|
||
result, err := callAIWithMessagesOnce(systemPrompt, userPrompt)
|
||
if err == nil {
|
||
if attempt > 1 {
|
||
fmt.Printf("✓ AI API重试成功\n")
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
lastErr = err
|
||
// 如果不是网络错误,不重试
|
||
if !isRetryableError(err) {
|
||
return "", err
|
||
}
|
||
|
||
// 重试前等待
|
||
if attempt < maxRetries {
|
||
waitTime := time.Duration(attempt) * 2 * time.Second
|
||
fmt.Printf("⏳ 等待%v后重试...\n", waitTime)
|
||
time.Sleep(waitTime)
|
||
}
|
||
}
|
||
|
||
return "", fmt.Errorf("重试%d次后仍然失败: %w", maxRetries, lastErr)
|
||
}
|
||
|
||
// callDeepSeekAPIOnce 单次调用AI API(兼容旧代码)
|
||
func callDeepSeekAPIOnce(prompt string) (string, error) {
|
||
return callAIWithMessagesOnce("", prompt)
|
||
}
|
||
|
||
// callAIWithMessagesOnce 单次调用AI API(支持 system + user prompt)
|
||
func callAIWithMessagesOnce(systemPrompt, userPrompt string) (string, error) {
|
||
// 构建 messages 数组
|
||
messages := []map[string]string{}
|
||
|
||
// 如果有 system prompt,添加 system message
|
||
if systemPrompt != "" {
|
||
messages = append(messages, map[string]string{
|
||
"role": "system",
|
||
"content": systemPrompt,
|
||
})
|
||
}
|
||
|
||
// 添加 user message
|
||
messages = append(messages, map[string]string{
|
||
"role": "user",
|
||
"content": userPrompt,
|
||
})
|
||
|
||
// 构建请求体
|
||
requestBody := map[string]interface{}{
|
||
"model": defaultConfig.Model,
|
||
"messages": messages,
|
||
"temperature": 0.5, // 降低temperature以提高JSON格式稳定性
|
||
"max_tokens": 2000,
|
||
}
|
||
|
||
// 注意:response_format 参数仅 OpenAI 支持,DeepSeek/Qwen 不支持
|
||
// 我们通过强化 prompt 和后处理来确保 JSON 格式正确
|
||
|
||
jsonData, err := json.Marshal(requestBody)
|
||
if err != nil {
|
||
return "", fmt.Errorf("序列化请求失败: %w", err)
|
||
}
|
||
|
||
// 创建HTTP请求
|
||
url := fmt.Sprintf("%s/chat/completions", defaultConfig.BaseURL)
|
||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||
if err != nil {
|
||
return "", fmt.Errorf("创建请求失败: %w", err)
|
||
}
|
||
|
||
req.Header.Set("Content-Type", "application/json")
|
||
|
||
// 根据不同的Provider设置认证方式
|
||
switch defaultConfig.Provider {
|
||
case ProviderDeepSeek:
|
||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey))
|
||
case ProviderQwen:
|
||
// 阿里云Qwen使用API-Key认证
|
||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey))
|
||
// 注意:如果使用的不是兼容模式,可能需要不同的认证方式
|
||
default:
|
||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey))
|
||
}
|
||
|
||
// 发送请求
|
||
client := &http.Client{Timeout: defaultConfig.Timeout}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return "", fmt.Errorf("发送请求失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 读取响应
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return "", fmt.Errorf("读取响应失败: %w", err)
|
||
}
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
return "", fmt.Errorf("API返回错误 (status %d): %s", resp.StatusCode, string(body))
|
||
}
|
||
|
||
// 解析响应
|
||
var result struct {
|
||
Choices []struct {
|
||
Message struct {
|
||
Content string `json:"content"`
|
||
} `json:"message"`
|
||
} `json:"choices"`
|
||
}
|
||
|
||
if err := json.Unmarshal(body, &result); err != nil {
|
||
return "", fmt.Errorf("解析响应失败: %w", err)
|
||
}
|
||
|
||
if len(result.Choices) == 0 {
|
||
return "", fmt.Errorf("API返回空响应")
|
||
}
|
||
|
||
return result.Choices[0].Message.Content, nil
|
||
}
|
||
|
||
// isRetryableError 判断错误是否可重试
|
||
func isRetryableError(err error) bool {
|
||
errStr := err.Error()
|
||
// 网络错误、超时、EOF等可以重试
|
||
retryableErrors := []string{
|
||
"EOF",
|
||
"timeout",
|
||
"connection reset",
|
||
"connection refused",
|
||
"temporary failure",
|
||
"no such host",
|
||
}
|
||
for _, retryable := range retryableErrors {
|
||
if strings.Contains(errStr, retryable) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// parseAIResponse 解析AI响应
|
||
func parseAIResponse(aiResponse string, marketData *MarketData) (*TradingSignal, error) {
|
||
// 尝试从响应中提取JSON
|
||
jsonStart := strings.Index(aiResponse, "```json")
|
||
jsonEnd := strings.Index(aiResponse, "```\n")
|
||
|
||
// 如果没找到结束标记,尝试找第二个```
|
||
if jsonEnd == -1 || jsonEnd <= jsonStart {
|
||
// 从jsonStart之后找第一个```
|
||
jsonEnd = strings.Index(aiResponse[jsonStart+7:], "```")
|
||
if jsonEnd != -1 {
|
||
jsonEnd += jsonStart + 7
|
||
}
|
||
}
|
||
|
||
var jsonContent string
|
||
if jsonStart != -1 && jsonEnd != -1 && jsonEnd > jsonStart {
|
||
jsonContent = aiResponse[jsonStart+7 : jsonEnd]
|
||
} else {
|
||
// 如果没有markdown代码块,尝试查找第一个完整的JSON对象
|
||
jsonStart = strings.Index(aiResponse, "{")
|
||
if jsonStart == -1 {
|
||
return nil, fmt.Errorf("无法从AI响应中提取JSON: %s", aiResponse)
|
||
}
|
||
|
||
// 找到匹配的右括号
|
||
braceCount := 0
|
||
jsonEnd = -1
|
||
for i := jsonStart; i < len(aiResponse); i++ {
|
||
if aiResponse[i] == '{' {
|
||
braceCount++
|
||
} else if aiResponse[i] == '}' {
|
||
braceCount--
|
||
if braceCount == 0 {
|
||
jsonEnd = i + 1
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if jsonEnd == -1 {
|
||
return nil, fmt.Errorf("无法找到完整的JSON对象")
|
||
}
|
||
jsonContent = aiResponse[jsonStart:jsonEnd]
|
||
}
|
||
|
||
// 解析JSON
|
||
var signal TradingSignal
|
||
if err := json.Unmarshal([]byte(jsonContent), &signal); err != nil {
|
||
return nil, fmt.Errorf("解析JSON失败: %w, JSON内容: %s", err, jsonContent)
|
||
}
|
||
|
||
// 验证信号类型
|
||
validSignals := map[SignalType]bool{
|
||
SignalOpenLong: true,
|
||
SignalOpenShort: true,
|
||
SignalCloseLong: true,
|
||
SignalCloseShort: true,
|
||
SignalHold: true,
|
||
SignalWait: true,
|
||
}
|
||
|
||
if !validSignals[signal.Signal] {
|
||
return nil, fmt.Errorf("无效的信号类型: %s", signal.Signal)
|
||
}
|
||
|
||
// 验证信心度范围
|
||
if signal.Confidence < 0 || signal.Confidence > 100 {
|
||
signal.Confidence = 50 // 默认值
|
||
}
|
||
|
||
return &signal, nil
|
||
}
|
||
|
||
// 辅助函数:价格与均线位置
|
||
func pricePosition(price, ema float64) string {
|
||
if price > ema {
|
||
return "位于上方"
|
||
}
|
||
return "位于下方"
|
||
}
|
||
|
||
// 辅助函数:MACD趋势
|
||
func macdTrend(macd float64) string {
|
||
if macd > 0 {
|
||
return "多头"
|
||
}
|
||
return "空头"
|
||
}
|
||
|
||
// 辅助函数:RSI状态
|
||
func rsiStatus(rsi float64) string {
|
||
if rsi >= 70 {
|
||
return "超买"
|
||
} else if rsi <= 30 {
|
||
return "超卖"
|
||
}
|
||
return "中性"
|
||
}
|
||
|
||
// 辅助函数:价格趋势(基于1h和4h变化)
|
||
func priceTrend(change1h, change4h float64) string {
|
||
if change1h > 2 && change4h > 5 {
|
||
return "强势上涨"
|
||
} else if change1h > 0 && change4h > 0 {
|
||
return "温和上涨"
|
||
} else if change1h < -2 && change4h < -5 {
|
||
return "强势下跌"
|
||
} else if change1h < 0 && change4h < 0 {
|
||
return "温和下跌"
|
||
} else {
|
||
return "震荡"
|
||
}
|
||
}
|
||
|
||
// 辅助函数:资金费率信号(交易机会解读)
|
||
func fundingRateSignal(rate float64) string {
|
||
if rate > 0.001 {
|
||
return "多头拥挤,考虑做空"
|
||
} else if rate > 0.0005 {
|
||
return "多头占优"
|
||
} else if rate < -0.001 {
|
||
return "空头拥挤,考虑做多"
|
||
} else if rate < -0.0005 {
|
||
return "空头占优"
|
||
}
|
||
return "中性"
|
||
}
|
||
|
||
// 辅助函数:资金费率状态
|
||
func fundingRateStatus(rate float64) string {
|
||
if rate > 0.0005 {
|
||
return "多头占优,费率偏高"
|
||
} else if rate < -0.0005 {
|
||
return "空头占优,费率为负"
|
||
}
|
||
return "费率中性"
|
||
}
|
||
|
||
// 辅助函数:EMA交叉状态
|
||
func emaCross(ema20, ema50 float64) string {
|
||
if ema20 > ema50 {
|
||
return "金叉,多头趋势"
|
||
}
|
||
return "死叉,空头趋势"
|
||
}
|
||
|
||
// 辅助函数:ATR状态
|
||
func atrStatus(atr3, atr14 float64) string {
|
||
if atr3 > atr14*1.2 {
|
||
return "急剧上升"
|
||
} else if atr3 < atr14*0.8 {
|
||
return "逐渐下降"
|
||
}
|
||
return "稳定"
|
||
}
|
||
|
||
// 辅助函数:成交量状态
|
||
func volumeStatus(current, average float64) string {
|
||
ratio := current / average
|
||
if ratio > 1.5 {
|
||
return "放量明显"
|
||
} else if ratio < 0.5 {
|
||
return "缩量明显"
|
||
}
|
||
return "正常水平"
|
||
}
|
||
|
||
// 辅助函数:序列趋势
|
||
func seriesTrend(values []float64) string {
|
||
if len(values) < 2 {
|
||
return "数据不足"
|
||
}
|
||
|
||
recent := values[len(values)-1]
|
||
prev := values[len(values)-2]
|
||
|
||
if recent > prev*1.1 {
|
||
return "强势上升"
|
||
} else if recent > prev {
|
||
return "小幅上升"
|
||
} else if recent < prev*0.9 {
|
||
return "快速下降"
|
||
} else if recent < prev {
|
||
return "小幅下降"
|
||
}
|
||
return "横盘整理"
|
||
}
|
||
|
||
// 辅助函数:RSI序列趋势
|
||
func rsiSeriesTrend(values []float64) string {
|
||
if len(values) < 2 {
|
||
return "数据不足"
|
||
}
|
||
|
||
recent := values[len(values)-1]
|
||
prev := values[len(values)-2]
|
||
|
||
if recent > 70 && prev > 70 {
|
||
return "持续超买"
|
||
} else if recent < 30 && prev < 30 {
|
||
return "持续超卖"
|
||
} else if recent > prev {
|
||
return "强度上升"
|
||
} else if recent < prev {
|
||
return "强度下降"
|
||
}
|
||
return "稳定"
|
||
}
|
||
|
||
// 辅助函数:格式化浮点数组
|
||
func formatFloatArray(values []float64) string {
|
||
if len(values) == 0 {
|
||
return "[]"
|
||
}
|
||
|
||
var sb strings.Builder
|
||
sb.WriteString("[")
|
||
for i, v := range values {
|
||
if i > 0 {
|
||
sb.WriteString(", ")
|
||
}
|
||
sb.WriteString(fmt.Sprintf("%.3f", v))
|
||
}
|
||
sb.WriteString("]")
|
||
return sb.String()
|
||
}
|