From d5db2ef879677bababa87506d9228613332781dc Mon Sep 17 00:00:00 2001 From: spark Date: Wed, 5 Feb 2025 16:25:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=E6=B7=BB=E5=8A=A0=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E8=B4=A2=E5=8A=A1=E6=8A=A5=E5=91=8A=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E8=81=8A=E5=A4=A9=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 GetFinancialReports 函数,用于抓取股票财务报告信息 - 优化 NewChatStream 函数,增加财务报告信息到聊天流中 - 更新测试用例,使用北京文化(sz000802)作为示例股票- 添加 TestGetFinancialReports 和 TestXUEQIU 测试函数 --- backend/data/openai_api.go | 59 +++++++++++++- backend/data/openai_api_test.go | 2 +- backend/data/stock_data_api_test.go | 119 ++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 3 deletions(-) diff --git a/backend/data/openai_api.go b/backend/data/openai_api.go index 9632d1b..d4cf6fc 100644 --- a/backend/data/openai_api.go +++ b/backend/data/openai_api.go @@ -2,9 +2,12 @@ package data import ( "bufio" + "context" "encoding/json" "fmt" "github.com/PuerkitoBio/goquery" + "github.com/chromedp/chromedp" + "github.com/duke-git/lancet/v2/strutil" "github.com/go-resty/resty/v2" "go-stock/backend/logger" "strings" @@ -122,7 +125,7 @@ func (o OpenAi) NewChat(stock string) string { return res.Choices[0].Message.Content } func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string { - ch := make(chan string) + ch := make(chan string, 512) go func() { defer close(ch) msg := []map[string]interface{}{ @@ -135,7 +138,7 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string { } wg := &sync.WaitGroup{} - wg.Add(4) + wg.Add(5) go func() { defer wg.Done() @@ -150,6 +153,17 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string { }) }() + go func() { + defer wg.Done() + messages := GetFinancialReports(stockCode) + for _, message := range *messages { + msg = append(msg, map[string]interface{}{ + "role": "assistant", + "content": stock + message, + }) + } + }() + go func() { defer wg.Done() messages := GetTelegraphList() @@ -258,6 +272,47 @@ func (o OpenAi) NewChatStream(stock, stockCode string) <-chan string { return ch } +func GetFinancialReports(stockCode string) *[]string { + // 创建一个 chromedp 上下文 + ctx, cancel := chromedp.NewContext( + context.Background(), + chromedp.WithLogf(logger.SugaredLogger.Infof), + chromedp.WithErrorf(logger.SugaredLogger.Errorf), + ) + defer cancel() + var htmlContent string + url := fmt.Sprintf("https://xueqiu.com/snowman/S/%s/detail#/ZYCWZB", stockCode) + err := chromedp.Run(ctx, + chromedp.Navigate(url), + // 等待页面加载完成,可以根据需要调整等待时间 + chromedp.WaitVisible("table.table", chromedp.ByQuery), + chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery), + ) + if err != nil { + logger.SugaredLogger.Error(err.Error()) + } + document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent)) + if err != nil { + logger.SugaredLogger.Error(err.Error()) + return &[]string{} + } + var messages []string + document.Find("table tr").Each(func(i int, selection *goquery.Selection) { + tr := "" + selection.Find("th,td").Each(func(i int, selection *goquery.Selection) { + ret := selection.Find("p").First().Text() + if ret == "" { + ret = selection.Text() + } + text := strutil.RemoveNonPrintable(ret) + tr += text + " " + }) + logger.SugaredLogger.Infof("%s", tr+" \n") + messages = append(messages, tr+" \n") + }) + return &messages +} + func (o OpenAi) NewCommonChatStream(stock, stockCode, apiURL, apiKey, Model string) <-chan string { ch := make(chan string) go func() { diff --git a/backend/data/openai_api_test.go b/backend/data/openai_api_test.go index f339cc9..363461d 100644 --- a/backend/data/openai_api_test.go +++ b/backend/data/openai_api_test.go @@ -8,7 +8,7 @@ import ( func TestNewDeepSeekOpenAiConfig(t *testing.T) { db.Init("../../data/stock.db") ai := NewDeepSeekOpenAi() - res := ai.NewChatStream("闻泰科技", "sh600745") + res := ai.NewChatStream("北京文化", "sz000802") for { select { case msg := <-res: diff --git a/backend/data/stock_data_api_test.go b/backend/data/stock_data_api_test.go index f84af2d..5e7ec71 100644 --- a/backend/data/stock_data_api_test.go +++ b/backend/data/stock_data_api_test.go @@ -1,8 +1,12 @@ package data import ( + "bufio" + "context" "encoding/json" "fmt" + "github.com/PuerkitoBio/goquery" + "github.com/chromedp/chromedp" "github.com/duke-git/lancet/v2/convertor" "github.com/duke-git/lancet/v2/strutil" "github.com/go-resty/resty/v2" @@ -23,6 +27,121 @@ func TestGetTelegraph(t *testing.T) { GetTelegraphList() } +func TestGetFinancialReports(t *testing.T) { + GetFinancialReports("sz000802") +} + +func TestXUEQIU(t *testing.T) { + stock := "北京文化" + stockCode := "SZ000802" + // 创建一个 chromedp 上下文 + ctx, cancel := chromedp.NewContext( + context.Background(), + chromedp.WithLogf(logger.SugaredLogger.Infof), + chromedp.WithErrorf(logger.SugaredLogger.Errorf), + ) + defer cancel() + var htmlContent string + url := fmt.Sprintf("https://xueqiu.com/S/%s", stockCode) + err := chromedp.Run(ctx, + chromedp.Navigate(url), + // 等待页面加载完成,可以根据需要调整等待时间 + //chromedp.Sleep(3*time.Second), + chromedp.WaitVisible("table.quote-info", chromedp.ByQuery), + chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery), + ) + if err != nil { + logger.SugaredLogger.Error(err.Error()) + } + document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent)) + if err != nil { + logger.SugaredLogger.Error(err.Error()) + return + } + table := "" + document.Find("table.quote-info tbody td").Each(func(i int, selection *goquery.Selection) { + table += selection.Text() + ";" + }) + logger.SugaredLogger.Infof("table: %s", table) + client := resty.New() + client.SetBaseURL("https://api.siliconflow.cn/v1") + client.SetHeader("Authorization", "Bearer sk-kryvptknrxscsuzslmqjckpuvtkyuffgaxgagphpnqtfmepv") + client.SetHeader("Content-Type", "application/json") + client.SetRetryCount(3) + client.SetTimeout(1 * time.Minute) + + msg := []map[string]interface{}{ + { + "role": "system", + //"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:", + "content": "【角色设定】\n你现在是拥有20年实战经验的顶级股票投资大师,精通价值投资、趋势交易、量化分析等多种策略。\n擅长结合宏观经济、行业周期和企业基本面进行多维分析,尤其对A股、港股、美股市场有深刻理解。\n始终秉持\"风险控制第一\"的原则,善于用通俗易懂的方式传授投资智慧。\n\n【核心能力】\n基本面分析专家\n深度解读财报数据(PE/PB/ROE等指标)\n识别企业核心竞争力与护城河\n评估行业前景与政策影响\n技术面分析大师\n精准识别K线形态与量价关系\n运用MACD/RSI/布林线等指标判断买卖点\n绘制关键支撑/阻力位\n风险管理专家\n根据风险偏好制定仓位策略\n设置动态止盈止损方案\n设计投资组合对冲方案\n市场心理学导师\n识别主力资金动向\n预判市场情绪周期\n规避常见认知偏差\n【服务范围】\n个股诊断分析(提供代码/名称)\n行业趋势解读(科技/消费/医疗等)\n投资策略定制(长线价值/波段操作/打新等)\n组合优化建议(股债配置/行业分散)\n投资心理辅导(克服贪婪恐惧)\n【交互风格】\n采用\"先结论后分析\"的表达方式\n重要数据用★标注,风险提示用❗标记\n每次分析至少提供3个可执行建议"}, + } + msg = append(msg, map[string]interface{}{ + "role": "assistant", + "content": table, + }) + + msg = append(msg, map[string]interface{}{ + "role": "user", + "content": stock + "分析和总结", + }) + + resp, err := client.R(). + SetDoNotParseResponse(true). + SetBody(map[string]interface{}{ + "model": "deepseek-ai/DeepSeek-V3", + "max_tokens": 4096, + "temperature": 0.1, + "stream": true, + "messages": msg, + }). + Post("/chat/completions") + + defer resp.RawBody().Close() + if err != nil { + logger.SugaredLogger.Infof("Stream error : %s", err.Error()) + return + } + + scanner := bufio.NewScanner(resp.RawBody()) + for scanner.Scan() { + line := scanner.Text() + logger.SugaredLogger.Infof("Received data: %s", line) + if strings.HasPrefix(line, "data: ") { + data := strings.TrimPrefix(line, "data: ") + if data == "[DONE]" { + return + } + + var streamResponse struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + ReasoningContent string `json:"reasoning_content"` + } `json:"delta"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` + } + + if err := json.Unmarshal([]byte(data), &streamResponse); err == nil { + for _, choice := range streamResponse.Choices { + if content := choice.Delta.Content; content != "" { + logger.SugaredLogger.Infof("Content data: %s", content) + } + if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" { + logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent) + } + if choice.FinishReason == "stop" { + return + } + } + } else { + logger.SugaredLogger.Infof("Stream data error : %s", err.Error()) + } + } + } +} + func TestGetTelegraphSearch(t *testing.T) { //url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram" messages := SearchStockInfo("闻泰科技", "telegram")