feat(backend): 添加获取财务报告功能并优化聊天流

- 新增 GetFinancialReports 函数,用于抓取股票财务报告信息
- 优化 NewChatStream 函数,增加财务报告信息到聊天流中
- 更新测试用例,使用北京文化(sz000802)作为示例股票- 添加 TestGetFinancialReports 和 TestXUEQIU 测试函数
This commit is contained in:
spark 2025-02-05 16:25:24 +08:00
parent 509cd2dbca
commit d5db2ef879
3 changed files with 177 additions and 3 deletions

View File

@ -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() {

View File

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

View File

@ -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")