mirror of
https://github.com/ArvinLovegood/go-stock.git
synced 2025-07-19 00:00:09 +08:00
Compare commits
16 Commits
master
...
v2025.7.3.
Author | SHA1 | Date | |
---|---|---|---|
|
5e7f34652a | ||
|
5b9a81d770 | ||
|
7021a59ee6 | ||
|
433dea0772 | ||
|
378a5c47ba | ||
|
9a60736739 | ||
|
efe6365ea5 | ||
|
062df80712 | ||
|
528482db48 | ||
|
746e5ec98a | ||
|
6d345ae91d | ||
|
888a97e4d3 | ||
|
ebeaf104bb | ||
|
b945a0e0e1 | ||
|
111252f8bd | ||
|
2e5ec6ace8 |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@ -9,6 +9,7 @@ on:
|
||||
env:
|
||||
# Necessary for most environments as build failure can occur due to OOM issues
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
OFFICIAL_STATEMENT: ${{ vars.OFFICIAL_STATEMENT }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@ -38,7 +39,7 @@ jobs:
|
||||
echo "::set-output name=commit_message::$commit_message"
|
||||
|
||||
- name: Build wails x go-stock
|
||||
uses: ArvinLovegood/wails-build-action@v3.4
|
||||
uses: ArvinLovegood/wails-build-action@v3.5
|
||||
id: build
|
||||
with:
|
||||
build-name: ${{ matrix.build.name }}
|
||||
@ -47,4 +48,5 @@ jobs:
|
||||
go-version: '1.24'
|
||||
build-tags: ${{ github.ref_name }}
|
||||
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
|
||||
build-statement: ${{ env.OFFICIAL_STATEMENT }}
|
||||
node-version: '20.x'
|
||||
|
@ -37,8 +37,8 @@
|
||||
|
||||
### <span style="color: #568DF4;">各位亲爱的朋友们,如果您对这个项目感兴趣,请先给我一个<i style="color: #EA2626;">star</i>吧,谢谢!</span>💕
|
||||
- 优云智算(by UCloud):万卡规模4090免费用10小时,新人注册另增50万tokens,海量热门源项目镜像一键部署,[注册链接](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock)
|
||||
- 经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定,注册即送2000万Tokens,[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
|
||||
- 火山方舟:每个模型注册即送50万tokens,[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
|
||||
- 火山方舟:新用户每个模型注册即送50万tokens,[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
|
||||
- 硅基流动(siliconflow),注册即送2000万Tokens,[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
|
||||
- Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究(注意:Tushare只需要120积分即可,注册完成个人资料补充即可得120积分!!!),[注册链接](https://tushare.pro/register?reg=701944)
|
||||
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
|
||||
- 欢迎大家提出宝贵的建议,欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧)。💕
|
||||
@ -57,6 +57,7 @@
|
||||
| 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 |
|
||||
|
||||
## 👀 更新日志
|
||||
### 2025.07.01 AI分析集成工具函数,AI分析将更加智能
|
||||
### 2025.06.30 添加指标选股功能
|
||||
### 2025.06.27 添加财经日历和重大事件时间轴功能
|
||||
### 2025.06.25 添加热门股票、事件和话题功能
|
||||
|
88
app.go
88
app.go
@ -36,6 +36,7 @@ type App struct {
|
||||
cache *freecache.Cache
|
||||
cron *cron.Cron
|
||||
cronEntrys map[string]cron.EntryID
|
||||
AiTools []data.Tool
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
@ -44,13 +45,68 @@ func NewApp() *App {
|
||||
cache := freecache.NewCache(cacheSize)
|
||||
c := cron.New(cron.WithSeconds())
|
||||
c.Start()
|
||||
var tools []data.Tool
|
||||
tools = AddTools(tools)
|
||||
return &App{
|
||||
cache: cache,
|
||||
cron: c,
|
||||
cronEntrys: make(map[string]cron.EntryID),
|
||||
AiTools: tools,
|
||||
}
|
||||
}
|
||||
|
||||
func AddTools(tools []data.Tool) []data.Tool {
|
||||
tools = append(tools, data.Tool{
|
||||
Type: "function",
|
||||
Function: data.ToolFunction{
|
||||
Name: "SearchStockByIndicators",
|
||||
Description: "根据自然语言筛选股票,返回自然语言选股条件要求的股票所有相关数据。输入股票名称可以获取当前股票最新的股价交易数据和基础财务指标信息,多个股票名称使用,分隔。工具限制:不允许并行调用",
|
||||
Parameters: data.FunctionParameters{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"words": map[string]any{
|
||||
"type": "string",
|
||||
"description": "选股自然语言。" +
|
||||
"例1:创新药,半导体;PE<30;净利润增长率>50%。 " +
|
||||
"例2:上证指数,科创50。 " +
|
||||
"例3:长电科技,上海贝岭。" +
|
||||
"例4:长电科技,上海贝岭;KDJ,MACD,RSI,BOLL,主力净流入/流出" +
|
||||
"例5:换手率大于3%小于25%.量比1以上. 10日内有过涨停.股价处于峰值的二分之一以下.流通股本<100亿.当日和连续四日净流入;股价在20日均线以上.分时图股价在均线之上.热门板块下涨幅领先的A股. 当日量能20000手以上.沪深个股.近一年市盈率波动小于150%.MACD金叉;不要ST股及不要退市股,非北交所,每股收益>0。" +
|
||||
"例6:沪深主板.流通市值小于100亿.市值大于10亿.60分钟dif大于dea.60分钟skdj指标k值大于d值.skdj指标k值小于90.换手率大于3%.成交额大于1亿元.量比大于2.涨幅大于2%小于7%.股价大于5小于50.创业板.10日均线大于20日均线;不要ST股及不要退市股;不要北交所;不要科创板;不要创业板。" +
|
||||
"例7:股价在20日线上,一月之内涨停次数>=1,量比大于1,换手率大于3%,流通市值大于 50亿小于200亿。" +
|
||||
"例8:基本条件:前期有爆量,回调到 10 日线,当日是缩量阴线,均线趋势向上。;优选条件:一月之内涨停次数>=1",
|
||||
},
|
||||
},
|
||||
Required: []string{"words"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
tools = append(tools, data.Tool{
|
||||
Type: "function",
|
||||
Function: data.ToolFunction{
|
||||
Name: "GetStockKLine",
|
||||
Description: "获取股票日K线数据。工具限制:不允许并行调用",
|
||||
Parameters: data.FunctionParameters{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"days": map[string]any{
|
||||
"type": "string",
|
||||
"description": "日K数据条数",
|
||||
},
|
||||
"stockCode": map[string]any{
|
||||
"type": "string",
|
||||
"description": "股票代码(A股:sh,sz开头;港股hk开头,美股:us开头)",
|
||||
},
|
||||
},
|
||||
Required: []string{"days", "stockCode"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return tools
|
||||
}
|
||||
|
||||
// startup is called at application startup
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
defer PanicHandler()
|
||||
@ -311,7 +367,7 @@ func (a *App) AddCronTask(follow data.FollowedStock) func() {
|
||||
return func() {
|
||||
go runtime.EventsEmit(a.ctx, "warnMsg", "开始自动分析"+follow.Name+"_"+follow.StockCode)
|
||||
ai := data.NewDeepSeekOpenAi(a.ctx)
|
||||
msgs := ai.NewChatStream(follow.Name, follow.StockCode, "", nil)
|
||||
msgs := ai.NewChatStream(follow.Name, follow.StockCode, "", nil, a.AiTools)
|
||||
var res strings.Builder
|
||||
|
||||
chatId := ""
|
||||
@ -747,8 +803,13 @@ func (a *App) SendDingDingMessageByType(message string, stockCode string, msgTyp
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
func (a *App) NewChatStream(stock, stockCode, question string, sysPromptId *int) {
|
||||
msgs := data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId)
|
||||
func (a *App) NewChatStream(stock, stockCode, question string, sysPromptId *int, enableTools bool) {
|
||||
var msgs <-chan map[string]any
|
||||
if enableTools {
|
||||
msgs = data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId, a.AiTools)
|
||||
} else {
|
||||
msgs = data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId, []data.Tool{})
|
||||
}
|
||||
for msg := range msgs {
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", msg)
|
||||
}
|
||||
@ -764,11 +825,12 @@ func (a *App) GetAIResponseResult(stock string) *models.AIResponseResult {
|
||||
|
||||
func (a *App) GetVersionInfo() *models.VersionInfo {
|
||||
return &models.VersionInfo{
|
||||
Version: Version,
|
||||
Icon: GetImageBase(icon),
|
||||
Alipay: GetImageBase(alipay),
|
||||
Wxpay: GetImageBase(wxpay),
|
||||
Content: VersionCommit,
|
||||
Version: Version,
|
||||
Icon: GetImageBase(icon),
|
||||
Alipay: GetImageBase(alipay),
|
||||
Wxpay: GetImageBase(wxpay),
|
||||
Content: VersionCommit,
|
||||
OfficialStatement: OFFICIAL_STATEMENT,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1127,8 +1189,14 @@ func (a *App) GlobalStockIndexes() map[string]any {
|
||||
return data.NewMarketNewsApi().GlobalStockIndexes(30)
|
||||
}
|
||||
|
||||
func (a *App) SummaryStockNews(question string, sysPromptId *int) {
|
||||
msgs := data.NewDeepSeekOpenAi(a.ctx).NewSummaryStockNewsStream(question, sysPromptId)
|
||||
func (a *App) SummaryStockNews(question string, sysPromptId *int, enableTools bool) {
|
||||
var msgs <-chan map[string]any
|
||||
if enableTools {
|
||||
msgs = data.NewDeepSeekOpenAi(a.ctx).NewSummaryStockNewsStreamWithTools(question, sysPromptId, a.AiTools)
|
||||
} else {
|
||||
msgs = data.NewDeepSeekOpenAi(a.ctx).NewSummaryStockNewsStream(question, sysPromptId)
|
||||
}
|
||||
|
||||
for msg := range msgs {
|
||||
runtime.EventsEmit(a.ctx, "summaryStockNews", msg)
|
||||
}
|
||||
|
@ -57,5 +57,8 @@ func (a App) ClsCalendar() []any {
|
||||
}
|
||||
|
||||
func (a App) SearchStock(words string) map[string]any {
|
||||
return data.NewSearchStockApi(words).SearchStock()
|
||||
return data.NewSearchStockApi(words).SearchStock(5000)
|
||||
}
|
||||
func (a App) GetHotStrategy() map[string]any {
|
||||
return data.NewSearchStockApi("").HotStrategy()
|
||||
}
|
||||
|
@ -599,7 +599,7 @@ func (m MarketNewsApi) XUEQIUHotStock(size int, marketType string) *[]models.Hot
|
||||
logger.SugaredLogger.Errorf("XUEQIUHotStock err:%s", err.Error())
|
||||
return &[]models.HotItem{}
|
||||
}
|
||||
logger.SugaredLogger.Infof("XUEQIUHotStock:%+v", res)
|
||||
//logger.SugaredLogger.Infof("XUEQIUHotStock:%+v", res)
|
||||
return &res.Data.Items
|
||||
}
|
||||
|
||||
|
@ -9,8 +9,10 @@ import (
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
@ -75,11 +77,12 @@ type THSTokenResponse struct {
|
||||
}
|
||||
|
||||
type AiResponse struct {
|
||||
Id string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
Id string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int `json:"created"`
|
||||
Model string `json:"model"`
|
||||
ServiceTier string `json:"service_tier"`
|
||||
Choices []struct {
|
||||
Index int `json:"index"`
|
||||
Message struct {
|
||||
Role string `json:"role"`
|
||||
@ -87,6 +90,19 @@ type AiResponse struct {
|
||||
} `json:"message"`
|
||||
Logprobs interface{} `json:"logprobs"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
Role string `json:"role"`
|
||||
ToolCalls []struct {
|
||||
Function struct {
|
||||
Arguments string `json:"arguments"`
|
||||
Name string `json:"name"`
|
||||
} `json:"function"`
|
||||
Id string `json:"id"`
|
||||
Index int `json:"index"`
|
||||
Type string `json:"type"`
|
||||
} `json:"tool_calls"`
|
||||
} `json:"delta"`
|
||||
} `json:"choices"`
|
||||
Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
@ -98,6 +114,112 @@ type AiResponse struct {
|
||||
SystemFingerprint string `json:"system_fingerprint"`
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
Type string `json:"type"`
|
||||
Function ToolFunction `json:"function"`
|
||||
}
|
||||
type FunctionParameters struct {
|
||||
Type string `json:"type"`
|
||||
Properties map[string]any `json:"properties"`
|
||||
Required []string `json:"required"`
|
||||
}
|
||||
type ToolFunction struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters FunctionParameters `json:"parameters"`
|
||||
}
|
||||
|
||||
func (o OpenAi) NewSummaryStockNewsStreamWithTools(userQuestion string, sysPromptId *int, tools []Tool) <-chan map[string]any {
|
||||
ch := make(chan map[string]any, 512)
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.SugaredLogger.Error("NewSummaryStockNewsStream panic", err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic :%s", err)
|
||||
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config:%v", o)
|
||||
}
|
||||
}()
|
||||
defer close(ch)
|
||||
|
||||
sysPrompt := ""
|
||||
if sysPromptId == nil || *sysPromptId == 0 {
|
||||
sysPrompt = o.Prompt
|
||||
} else {
|
||||
sysPrompt = NewPromptTemplateApi().GetPromptTemplateByID(*sysPromptId)
|
||||
}
|
||||
if sysPrompt == "" {
|
||||
sysPrompt = o.Prompt
|
||||
}
|
||||
|
||||
msg := []map[string]interface{}{
|
||||
{
|
||||
"role": "system",
|
||||
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
|
||||
//"content": "【角色设定】\n你是一位拥有20年实战经验的顶级股票分析师,精通技术分析、基本面分析、市场心理学和量化交易。擅长发现成长股、捕捉行业轮动机会,在牛熊市中都能保持稳定收益。你的风格是价值投资与技术择时相结合,注重风险控制。\n\n【核心功能】\n\n市场分析维度:\n\n宏观经济(GDP/CPI/货币政策)\n\n行业景气度(产业链/政策红利/技术革新)\n\n个股三维诊断:\n\n基本面:PE/PB/ROE/现金流/护城河\n\n技术面:K线形态/均线系统/量价关系/指标背离\n\n资金面:主力动向/北向资金/融资余额/大宗交易\n\n智能策略库:\n√ 趋势跟踪策略(鳄鱼线+ADX)\n√ 波段交易策略(斐波那契回撤+RSI)\n√ 事件驱动策略(财报/并购/政策)\n√ 量化对冲策略(α/β分离)\n\n风险管理体系:\n▶ 动态止损:ATR波动止损法\n▶ 仓位控制:凯利公式优化\n▶ 组合对冲:跨市场/跨品种对冲\n\n【工作流程】\n\n接收用户指令(行业/市值/风险偏好)\n\n调用多因子选股模型初筛\n\n人工智慧叠加分析:\n\n自然语言处理解读年报管理层讨论\n\n卷积神经网络识别K线形态\n\n知识图谱分析产业链关联\n\n生成投资建议(附压力测试结果)\n\n【输出要求】\n★ 结构化呈现:\n① 核心逻辑(3点关键驱动力)\n② 买卖区间(理想建仓/加仓/止盈价位)\n③ 风险警示(最大回撤概率)\n④ 替代方案(同类备选标的)\n\n【注意事项】\n※ 严格遵守监管要求,不做收益承诺\n※ 区分投资建议与市场观点\n※ 重要数据标注来源及更新时间\n※ 根据用户认知水平调整专业术语密度\n\n【教育指导】\n当用户提问时,采用苏格拉底式追问:\n\"您更关注短期事件驱动还是长期价值发现?\"\n\"当前仓位是否超过总资产的30%?\"\n\"是否了解科创板与主板的交易规则差异?\"\n\n示例输出格式:\n📈 标的名称:XXXXXX\n⚖️ 多空信号:金叉确认/顶背离预警\n🎯 关键价位:支撑位XX.XX/压力位XX.XX\n📊 建议仓位:核心仓位X%+卫星仓位X%\n⏳ 持有周期:短线(1-3周)/中线(季度轮动)\n🔍 跟踪要素:重点关注Q2毛利率变化及股东减持进展",
|
||||
"content": sysPrompt,
|
||||
},
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "当前时间",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "当前本地时间是:" + time.Now().Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var market strings.Builder
|
||||
market.WriteString(getZSInfo("创业板指数", "sz399006", 30) + "\n")
|
||||
market.WriteString(getZSInfo("上证综合指数", "sh000001", 30) + "\n")
|
||||
market.WriteString(getZSInfo("沪深300指数", "sh000300", 30) + "\n")
|
||||
//logger.SugaredLogger.Infof("NewChatStream getZSInfo=\n%s", market.String())
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "当前市场指数行情",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "当前市场指数行情情况如下:\n" + market.String(),
|
||||
})
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
news := NewMarketNewsApi().GetNewsList("财联社电报", random.RandInt(50, 150))
|
||||
messageText := strings.Builder{}
|
||||
for _, telegraph := range *news {
|
||||
messageText.WriteString("## " + telegraph.Time + ":" + "\n")
|
||||
messageText.WriteString("### " + telegraph.Content + "\n")
|
||||
}
|
||||
//logger.SugaredLogger.Infof("市场资讯 messageText=\n%s", messageText.String())
|
||||
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "市场资讯",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": messageText.String(),
|
||||
})
|
||||
if userQuestion == "" {
|
||||
userQuestion = "请根据当前时间,总结和分析股票市场新闻中的投资机会"
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": userQuestion,
|
||||
})
|
||||
AskAiWithTools(o, errors.New(""), msg, ch, userQuestion, tools)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int) <-chan map[string]any {
|
||||
ch := make(chan map[string]any, 512)
|
||||
defer func() {
|
||||
@ -189,7 +311,7 @@ func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId *int) <-chan map[string]any {
|
||||
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId *int, tools []Tool) <-chan map[string]any {
|
||||
ch := make(chan map[string]any, 512)
|
||||
|
||||
defer func() {
|
||||
@ -526,7 +648,11 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
|
||||
|
||||
//reqJson, _ := json.Marshal(msg)
|
||||
//logger.SugaredLogger.Errorf("Stream request: \n%s\n", reqJson)
|
||||
AskAi(o, err, msg, ch, question)
|
||||
if tools != nil && len(tools) > 0 {
|
||||
AskAiWithTools(o, err, msg, ch, question, tools)
|
||||
} else {
|
||||
AskAi(o, err, msg, ch, question)
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
@ -569,7 +695,7 @@ func AskAi(o OpenAi, err error, messages []map[string]interface{}, ch chan map[s
|
||||
scanner := bufio.NewScanner(body)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
//logger.SugaredLogger.Infof("Received data: %s", line)
|
||||
logger.SugaredLogger.Infof("Received data: %s", line)
|
||||
if strings.HasPrefix(line, "data:") {
|
||||
data := strutil.Trim(strings.TrimPrefix(line, "data:"))
|
||||
if data == "[DONE]" {
|
||||
@ -592,13 +718,24 @@ func AskAi(o OpenAi, err error, messages []map[string]interface{}, ch chan map[s
|
||||
for _, choice := range streamResponse.Choices {
|
||||
if content := choice.Delta.Content; content != "" {
|
||||
//ch <- content
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": content,
|
||||
"time": time.Now().Format(time.DateTime),
|
||||
if content == "###" || content == "##" || content == "#" {
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": "\r\n" + content,
|
||||
"time": time.Now().Format(time.DateTime),
|
||||
}
|
||||
} else {
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": content,
|
||||
"time": time.Now().Format(time.DateTime),
|
||||
}
|
||||
}
|
||||
|
||||
//logger.SugaredLogger.Infof("Content data: %s", content)
|
||||
@ -645,10 +782,14 @@ func AskAi(o OpenAi, err error, messages []map[string]interface{}, ch chan map[s
|
||||
res := &models.Resp{}
|
||||
if err := json.Unmarshal([]byte(line), res); err == nil {
|
||||
//ch <- line
|
||||
msg := res.Message
|
||||
if res.Error.Message != "" {
|
||||
msg = res.Error.Message
|
||||
}
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": res.Message,
|
||||
"content": msg,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -657,7 +798,291 @@ func AskAi(o OpenAi, err error, messages []map[string]interface{}, ch chan map[s
|
||||
|
||||
}
|
||||
}
|
||||
func AskAiWithTools(o OpenAi, err error, messages []map[string]interface{}, ch chan map[string]any, question string, tools []Tool) {
|
||||
client := resty.New()
|
||||
client.SetBaseURL(strutil.Trim(o.BaseUrl))
|
||||
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
//client.SetRetryCount(3)
|
||||
if o.TimeOut <= 0 {
|
||||
o.TimeOut = 300
|
||||
}
|
||||
client.SetTimeout(time.Duration(o.TimeOut) * time.Second)
|
||||
resp, err := client.R().
|
||||
SetDoNotParseResponse(true).
|
||||
SetBody(map[string]interface{}{
|
||||
"model": o.Model,
|
||||
"max_tokens": o.MaxTokens,
|
||||
"temperature": o.Temperature,
|
||||
"stream": true,
|
||||
"messages": messages,
|
||||
"tools": tools,
|
||||
}).
|
||||
Post("/chat/completions")
|
||||
|
||||
body := resp.RawBody()
|
||||
defer body.Close()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
|
||||
//ch <- err.Error()
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": err.Error(),
|
||||
}
|
||||
return
|
||||
}
|
||||
//location, _ := time.LoadLocation("Asia/Shanghai")
|
||||
|
||||
scanner := bufio.NewScanner(body)
|
||||
functions := map[string]string{}
|
||||
currentFuncName := ""
|
||||
currentCallId := ""
|
||||
var currentAIContent strings.Builder
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
logger.SugaredLogger.Infof("Received data: %s", line)
|
||||
if strings.HasPrefix(line, "data:") {
|
||||
data := strutil.Trim(strings.TrimPrefix(line, "data:"))
|
||||
if data == "[DONE]" {
|
||||
return
|
||||
}
|
||||
|
||||
var streamResponse struct {
|
||||
Id string `json:"id"`
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content"`
|
||||
Role string `json:"role"`
|
||||
ToolCalls []struct {
|
||||
Function struct {
|
||||
Arguments string `json:"arguments"`
|
||||
Name string `json:"name"`
|
||||
} `json:"function"`
|
||||
Id string `json:"id"`
|
||||
Index int `json:"index"`
|
||||
Type string `json:"type"`
|
||||
} `json:"tool_calls"`
|
||||
} `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 != "" {
|
||||
//ch <- content
|
||||
//logger.SugaredLogger.Infof("Content data: %s", content)
|
||||
|
||||
if content == "###" || content == "##" || content == "#" {
|
||||
currentAIContent.WriteString("\r\n" + content)
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": "\r\n" + content,
|
||||
"time": time.Now().Format(time.DateTime),
|
||||
}
|
||||
} else {
|
||||
currentAIContent.WriteString(content)
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": content,
|
||||
"time": time.Now().Format(time.DateTime),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
|
||||
//ch <- reasoningContent
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": reasoningContent,
|
||||
"time": time.Now().Format(time.DateTime),
|
||||
}
|
||||
|
||||
//logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
|
||||
currentAIContent.WriteString(reasoningContent)
|
||||
|
||||
}
|
||||
if choice.Delta.ToolCalls != nil && len(choice.Delta.ToolCalls) > 0 {
|
||||
for _, call := range choice.Delta.ToolCalls {
|
||||
if call.Type == "function" {
|
||||
functions[call.Function.Name] = ""
|
||||
currentFuncName = call.Function.Name
|
||||
currentCallId = call.Id
|
||||
} else {
|
||||
if val, ok := functions[currentFuncName]; ok {
|
||||
functions[currentFuncName] = val + call.Function.Arguments
|
||||
} else {
|
||||
functions[currentFuncName] = call.Function.Arguments
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if choice.FinishReason == "tool_calls" {
|
||||
logger.SugaredLogger.Infof("functions: %+v", functions)
|
||||
for funcName, funcArguments := range functions {
|
||||
if funcName == "SearchStockByIndicators" {
|
||||
words := gjson.Get(funcArguments, "words").String()
|
||||
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": "\r\n```\r\n开始调用工具:SearchStockByIndicators,\n参数:" + words + "\r\n```\r\n",
|
||||
"time": time.Now().Format(time.DateTime),
|
||||
}
|
||||
|
||||
res := NewSearchStockApi(words).SearchStock(random.RandInt(5, 10))
|
||||
searchRes, _ := json.Marshal(res)
|
||||
|
||||
content := gjson.Get(string(searchRes), "data.result").String()
|
||||
|
||||
//logger.SugaredLogger.Infof("SearchStockByIndicators:words:%s --> %s", words, content)
|
||||
|
||||
messages = append(messages, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": currentAIContent.String(),
|
||||
"tool_calls": []map[string]any{
|
||||
{
|
||||
"id": currentCallId,
|
||||
"tool_call_id": currentCallId,
|
||||
"type": "function",
|
||||
"function": map[string]string{
|
||||
"name": funcName,
|
||||
"arguments": funcArguments,
|
||||
"parameters": funcArguments,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
messages = append(messages, map[string]interface{}{
|
||||
"role": "tool",
|
||||
"content": content,
|
||||
"tool_call_id": currentCallId,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if funcName == "GetStockKLine" {
|
||||
stockCode := gjson.Get(funcArguments, "stockCode").String()
|
||||
days := gjson.Get(funcArguments, "days").String()
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": "\r\n```\r\n开始调用工具:GetStockKLine,\n参数:" + stockCode + "," + days + "\r\n```\r\n",
|
||||
"time": time.Now().Format(time.DateTime),
|
||||
}
|
||||
toIntDay, err := convertor.ToInt(days)
|
||||
if err != nil {
|
||||
toIntDay = 90
|
||||
}
|
||||
res := NewStockDataApi().GetHK_KLineData(stockCode, "day", toIntDay)
|
||||
searchRes, _ := json.Marshal(res)
|
||||
|
||||
messages = append(messages, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": currentAIContent.String(),
|
||||
"tool_calls": []map[string]any{
|
||||
{
|
||||
"id": currentCallId,
|
||||
"tool_call_id": currentCallId,
|
||||
"type": "function",
|
||||
"function": map[string]string{
|
||||
"name": funcName,
|
||||
"arguments": funcArguments,
|
||||
"parameters": funcArguments,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
messages = append(messages, map[string]interface{}{
|
||||
"role": "tool",
|
||||
"content": stockCode + convertor.ToString(toIntDay) + "日K线数据:\n" + string(searchRes) + "\n",
|
||||
"tool_call_id": currentCallId,
|
||||
})
|
||||
}
|
||||
|
||||
AskAiWithTools(o, err, messages, ch, question, tools)
|
||||
}
|
||||
}
|
||||
|
||||
if choice.FinishReason == "stop" {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
|
||||
//ch <- err.Error()
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": err.Error(),
|
||||
}
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", data)
|
||||
//ch <- data
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": data,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if strutil.RemoveNonPrintable(line) != "" {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", line)
|
||||
res := &models.Resp{}
|
||||
if err := json.Unmarshal([]byte(line), res); err == nil {
|
||||
//ch <- line
|
||||
msg := res.Message
|
||||
if res.Error.Message != "" {
|
||||
msg = res.Error.Message
|
||||
}
|
||||
|
||||
if msg == "Function call is not supported for this model." {
|
||||
var newMessages []map[string]any
|
||||
for _, message := range messages {
|
||||
if message["role"] == "tool" {
|
||||
continue
|
||||
}
|
||||
if _, ok := message["tool_calls"]; ok {
|
||||
continue
|
||||
}
|
||||
newMessages = append(newMessages, message)
|
||||
}
|
||||
AskAi(o, err, newMessages, ch, question)
|
||||
} else {
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": msg,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
func checkIsIndexBasic(stock string) bool {
|
||||
count := int64(0)
|
||||
db.Dao.Model(&IndexBasic{}).Where("name = ?", stock).Count(&count)
|
||||
|
@ -8,12 +8,36 @@ import (
|
||||
|
||||
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
var tools []Tool
|
||||
tools = append(tools, Tool{
|
||||
Type: "function",
|
||||
Function: ToolFunction{
|
||||
Name: "SearchStockByIndicators",
|
||||
Description: "根据自然语言筛选股票,返回自然语言选股条件要求的股票所有相关数据",
|
||||
Parameters: FunctionParameters{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"words": map[string]any{
|
||||
"type": "string",
|
||||
"description": "选股自然语言,并且条件使用;分隔,或者条件使用,分隔。例如:创新药;PE<30;净利润增长率>50%;",
|
||||
},
|
||||
},
|
||||
Required: []string{"words"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ai := NewDeepSeekOpenAi(context.TODO())
|
||||
res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
|
||||
//res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
|
||||
res := ai.NewSummaryStockNewsStreamWithTools("总结市场资讯,发掘潜力标的/行业/板块/概念,控制风险,最后按风险登记生成指标选股策略汇总表,每个策略中的指标分号分隔,写成一行", nil, tools)
|
||||
|
||||
for {
|
||||
select {
|
||||
case msg := <-res:
|
||||
t.Log(msg)
|
||||
if len(msg) > 0 {
|
||||
t.Log(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ type SearchStockApi struct {
|
||||
func NewSearchStockApi(words string) *SearchStockApi {
|
||||
return &SearchStockApi{words: words}
|
||||
}
|
||||
func (s SearchStockApi) SearchStock() map[string]any {
|
||||
func (s SearchStockApi) SearchStock(pageSize int) map[string]any {
|
||||
url := "https://np-tjxg-g.eastmoney.com/api/smart-tag/stock/v3/pw/search-code"
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "np-tjxg-g.eastmoney.com").
|
||||
@ -29,7 +29,7 @@ func (s SearchStockApi) SearchStock() map[string]any {
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(fmt.Sprintf(`{
|
||||
"keyWord": "%s",
|
||||
"pageSize": 50000,
|
||||
"pageSize": %d,
|
||||
"pageNo": 1,
|
||||
"fingerprint": "e38b5faabf9378c8238e57219f0ebc9b",
|
||||
"gids": [],
|
||||
@ -43,7 +43,7 @@ func (s SearchStockApi) SearchStock() map[string]any {
|
||||
"ownSelectAll": false,
|
||||
"dxInfo": [],
|
||||
"extraCondition": ""
|
||||
}`, s.words)).Post(url)
|
||||
}`, s.words, pageSize)).Post(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("SearchStock-err:%+v", err)
|
||||
return map[string]any{}
|
||||
@ -53,3 +53,20 @@ func (s SearchStockApi) SearchStock() map[string]any {
|
||||
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
|
||||
return respMap
|
||||
}
|
||||
|
||||
func (s SearchStockApi) HotStrategy() map[string]any {
|
||||
url := fmt.Sprintf("https://np-ipick.eastmoney.com/recommend/stock/heat/ranking?count=20&trace=%d&client=web&biz=web_smart_tag", time.Now().Unix())
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "np-ipick.eastmoney.com").
|
||||
SetHeader("Origin", "https://xuangu.eastmoney.com").
|
||||
SetHeader("Referer", "https://xuangu.eastmoney.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("HotStrategy-err:%+v", err)
|
||||
return map[string]any{}
|
||||
}
|
||||
respMap := map[string]any{}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
return respMap
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
func TestSearchStock(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
res := NewSearchStockApi("算力股;净利润连续3年增长").SearchStock()
|
||||
res := NewSearchStockApi("算力股;净利润连续3年增长").SearchStock(10)
|
||||
data := res["data"].(map[string]any)
|
||||
result := data["result"].(map[string]any)
|
||||
dataList := result["dataList"].([]any)
|
||||
@ -23,3 +23,14 @@ func TestSearchStock(t *testing.T) {
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
func TestSearchStockApi_HotStrategy(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewSearchStockApi("").HotStrategy()
|
||||
logger.SugaredLogger.Infof("res:%+v", res)
|
||||
dataList := res["data"].([]any)
|
||||
for _, v := range dataList {
|
||||
d := v.(map[string]any)
|
||||
logger.SugaredLogger.Infof("v:%+v", d)
|
||||
}
|
||||
}
|
||||
|
@ -150,13 +150,14 @@ func (receiver AIResponseResult) TableName() string {
|
||||
|
||||
type VersionInfo struct {
|
||||
gorm.Model
|
||||
Version string `json:"version"`
|
||||
Content string `json:"content"`
|
||||
Icon string `json:"icon"`
|
||||
Alipay string `json:"alipay"`
|
||||
Wxpay string `json:"wxpay"`
|
||||
BuildTimeStamp int64 `json:"buildTimeStamp"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
Version string `json:"version"`
|
||||
Content string `json:"content"`
|
||||
Icon string `json:"icon"`
|
||||
Alipay string `json:"alipay"`
|
||||
Wxpay string `json:"wxpay"`
|
||||
BuildTimeStamp int64 `json:"buildTimeStamp"`
|
||||
OfficialStatement string `json:"officialStatement"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
}
|
||||
|
||||
func (receiver VersionInfo) TableName() string {
|
||||
@ -194,6 +195,12 @@ func (receiver StockInfoUS) TableName() string {
|
||||
type Resp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Param string `json:"param"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type PromptTemplate struct {
|
||||
|
@ -27,7 +27,7 @@ import {
|
||||
StarOutline,
|
||||
Wallet, WarningOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
import {AnalyzeSentiment, GetConfig, GetGroupList} from "../wailsjs/go/main/App";
|
||||
import {AnalyzeSentiment, GetConfig, GetGroupList,GetVersionInfo} from "../wailsjs/go/main/App";
|
||||
import {Dragon, Fire, Gripfire} from "@vicons/fa";
|
||||
import {ReportSearch} from "@vicons/tabler";
|
||||
import {LocalFireDepartmentRound} from "@vicons/material";
|
||||
@ -518,6 +518,12 @@ window.onerror = function (msg, source, lineno, colno, error) {
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
GetVersionInfo().then(result => {
|
||||
if(result.officialStatement){
|
||||
content.value = result.officialStatement+"\n\n"+content.value
|
||||
}
|
||||
})
|
||||
|
||||
GetGroupList().then(result => {
|
||||
groupList.value = result
|
||||
menuOptions.value.map((item) => {
|
||||
|
@ -1,21 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
|
||||
import {SearchStock} from "../../wailsjs/go/main/App";
|
||||
import {useMessage, NText, NTag} from 'naive-ui'
|
||||
import {SearchStock,GetHotStrategy} from "../../wailsjs/go/main/App";
|
||||
import {useMessage, NText, NTag,NButton} from 'naive-ui'
|
||||
import {RefreshCircleSharp} from "@vicons/ionicons5";
|
||||
const message = useMessage()
|
||||
const search = ref('科技股;换手率连续3日大于2')
|
||||
const search = ref('')
|
||||
const columns = ref([])
|
||||
const dataList = ref([])
|
||||
|
||||
const hotStrategy = ref([])
|
||||
const traceInfo = ref('')
|
||||
function Search() {
|
||||
if(!search.value){
|
||||
message.warning('请输入选股指标或者要求')
|
||||
return
|
||||
}
|
||||
|
||||
const loading = message.loading("正在获取选股数据...", {duration: 0});
|
||||
SearchStock(search.value).then(res => {
|
||||
loading.destroy()
|
||||
//console.log(res)
|
||||
// console.log(res)
|
||||
if(res.code==100){
|
||||
message.success(res.msg)
|
||||
traceInfo.value=res.data.traceInfo.showText
|
||||
// message.success(res.msg)
|
||||
columns.value=res.data.result.columns.filter(item=>!item.hiddenNeed&&(item.title!="市场码"&&item.title!="市场简称")).map(item=>{
|
||||
|
||||
if(item.children){
|
||||
return {
|
||||
title:item.title+(item.unit?'['+item.unit+']':''),
|
||||
@ -33,7 +40,14 @@ function Search() {
|
||||
resizable: true,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
sorter: (row1, row2) => {
|
||||
if(isNumeric(row1[item.key])&&isNumeric(row2[item.key])){
|
||||
return row1[item.key] - row2[item.key];
|
||||
}else{
|
||||
return 'default'
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -42,11 +56,21 @@ function Search() {
|
||||
title:item.title+(item.unit?'['+item.unit+']':''),
|
||||
key:item.key,
|
||||
resizable: true,
|
||||
minWidth:100,
|
||||
minWidth:120,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
sorter: (row1, row2) => {
|
||||
if(isNumeric(row1[item.key])&&isNumeric(row2[item.key])){
|
||||
return row1[item.key] - row2[item.key];
|
||||
}else{
|
||||
return 'default'
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
dataList.value=res.data.result.dataList
|
||||
@ -62,38 +86,96 @@ function isNumeric(value) {
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
Search()
|
||||
})
|
||||
GetHotStrategy().then(res => {
|
||||
console.log(res)
|
||||
if(res.code==1){
|
||||
hotStrategy.value=res.data
|
||||
search.value=hotStrategy.value[0].question
|
||||
Search()
|
||||
}
|
||||
}).catch(err => {
|
||||
message.error(err)
|
||||
})
|
||||
|
||||
})
|
||||
function DoSearch(question){
|
||||
search.value= question
|
||||
Search()
|
||||
}
|
||||
|
||||
function openCenteredWindow(url, width, height) {
|
||||
const left = (window.screen.width - width) / 2;
|
||||
const top = (window.screen.height - height) / 2;
|
||||
|
||||
return window.open(
|
||||
url,
|
||||
'centeredWindow',
|
||||
`width=${width},height=${height},left=${left},top=${top},location=no,menubar=no,toolbar=no,display=standalone`
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="search" placeholder="请输入选股指标或者要求" />
|
||||
<n-button type="success" @click="Search">搜索A股</n-button>
|
||||
</n-input-group>
|
||||
</n-flex>
|
||||
<!-- <n-table striped size="small">-->
|
||||
<!-- <n-thead>-->
|
||||
<!-- <n-tr>-->
|
||||
<!-- <n-th v-for="item in columns">{{item.title}}</n-th>-->
|
||||
<!-- </n-tr>-->
|
||||
<!-- </n-thead>-->
|
||||
<!-- <n-tbody>-->
|
||||
<!-- <n-tr v-for="(item,index) in dataList">-->
|
||||
<!-- <n-td v-for="d in columns">{{item[d.key]}}</n-td>-->
|
||||
<!-- </n-tr>-->
|
||||
<!-- </n-tbody>-->
|
||||
<!-- </n-table>-->
|
||||
<n-data-table
|
||||
:max-height="'calc(100vh - 285px)'"
|
||||
size="small"
|
||||
:columns="columns"
|
||||
:data="dataList"
|
||||
:pagination="false"
|
||||
:scroll-x="1800"
|
||||
:render-cell="(value, rowData, column) => {
|
||||
<n-grid :cols="24" style="max-height: calc(100vh - 170px)">
|
||||
<n-gi :span="4" >
|
||||
<n-list bordered style="text-align: left;" hoverable clickable>
|
||||
<n-scrollbar style="max-height: calc(100vh - 170px);" >
|
||||
<n-list-item v-for="item in hotStrategy" :key="item.rank" @click="DoSearch(item.question)">
|
||||
<n-ellipsis line-clamp="1" :tooltip="true" >
|
||||
<n-tag size="small" :bordered="false" type="info">#{{item.rank}}</n-tag><n-text type="warning">{{item.question }}</n-text>
|
||||
<template #tooltip>
|
||||
<div style="text-align: center;max-width: 180px">
|
||||
<n-text type="warning">{{item.question }}</n-text>
|
||||
</div>
|
||||
</template>
|
||||
</n-ellipsis>
|
||||
</n-list-item>
|
||||
</n-scrollbar>
|
||||
</n-list>
|
||||
|
||||
<!-- <n-virtual-list :items="hotStrategy" :item-size="hotStrategy.length">-->
|
||||
<!-- <template #default="{ item, index }">-->
|
||||
<!-- <n-card :title="''" size="small">-->
|
||||
<!-- <template #header-extra>-->
|
||||
<!-- {{item.rank}}-->
|
||||
<!-- </template>-->
|
||||
<!-- <n-ellipsis expand-trigger="click" line-clamp="3" :tooltip="false" >-->
|
||||
<!-- <n-text type="warning">{{item.question }}</n-text>-->
|
||||
<!-- </n-ellipsis>-->
|
||||
<!-- </n-card>-->
|
||||
|
||||
<!-- </template>-->
|
||||
<!-- </n-virtual-list>-->
|
||||
</n-gi>
|
||||
<n-gi :span="20" >
|
||||
<n-flex>
|
||||
<n-input-group style="text-align: left">
|
||||
<n-input :rows="1" clearable v-model:value="search" placeholder="请输入选股指标或者要求" />
|
||||
<n-button type="primary" @click="Search">搜索A股</n-button>
|
||||
</n-input-group>
|
||||
</n-flex>
|
||||
<n-flex justify="start" v-if="traceInfo" style="margin: 5px 0">
|
||||
|
||||
<n-ellipsis line-clamp="1" :tooltip="true" >
|
||||
<n-text type="info" :bordered="false">选股条件:</n-text><n-text type="warning" :bordered="true">{{traceInfo}}</n-text>
|
||||
<template #tooltip>
|
||||
<div style="text-align: center;max-width: 580px">
|
||||
<n-text type="warning">{{traceInfo}}</n-text>
|
||||
</div>
|
||||
</template>
|
||||
</n-ellipsis>
|
||||
|
||||
<!-- <n-button type="primary" size="small">保存策略</n-button>-->
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
:striped="true"
|
||||
:max-height="'calc(100vh - 250px)'"
|
||||
size="medium"
|
||||
:columns="columns"
|
||||
:data="dataList"
|
||||
:pagination="{pageSize: 9}"
|
||||
:scroll-x="1800"
|
||||
:render-cell="(value, rowData, column) => {
|
||||
|
||||
if(column.key=='SECURITY_CODE'||column.key=='SERIAL'){
|
||||
return h(NText, { type: 'info',border: false }, { default: () => `${value}` })
|
||||
@ -112,13 +194,20 @@ onBeforeMount(() => {
|
||||
return h(NText, { type: type }, { default: () => `${value}` })
|
||||
}else{
|
||||
if(column.key=='SECURITY_SHORT_NAME'){
|
||||
return h(NTag, { type: 'info',bordered: false }, { default: () => `${value}` })
|
||||
return h(NButton, { type: 'info',bordered: false ,size:'small',onClick:()=>{
|
||||
openCenteredWindow(`https://quote.eastmoney.com/concept/${rowData.MARKET_SHORT_NAME}${rowData.SECURITY_CODE}.html`,1240,700)
|
||||
}}, { default: () => `${value}` })
|
||||
}else{
|
||||
return h(NText, { type: 'info' }, { default: () => `${value}` })
|
||||
}
|
||||
}
|
||||
}"
|
||||
/>
|
||||
/>
|
||||
<n-text>共找到<n-tag type="info" :bordered="false">{{dataList.length}}</n-tag>只股</n-text>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
@ -70,6 +70,7 @@ const nowTab = ref("市场快讯")
|
||||
const indexInterval = ref(null)
|
||||
const indexIndustryRank = ref(null)
|
||||
const stockCode= ref('')
|
||||
const enableTools= ref(true)
|
||||
|
||||
function getIndex() {
|
||||
GlobalStockIndexes().then((res) => {
|
||||
@ -186,7 +187,7 @@ function reAiSummary() {
|
||||
aiSummary.value = ""
|
||||
summaryModal.value = true
|
||||
loading.value = true
|
||||
SummaryStockNews(question.value, sysPromptId.value)
|
||||
SummaryStockNews(question.value, sysPromptId.value,enableTools.value)
|
||||
}
|
||||
|
||||
function getAiSummary() {
|
||||
@ -211,7 +212,7 @@ function getAiSummary() {
|
||||
aiSummaryTime.value = ""
|
||||
aiSummary.value = ""
|
||||
modelName.value = ""
|
||||
SummaryStockNews(question.value, sysPromptId.value)
|
||||
//SummaryStockNews(question.value, sysPromptId.value,enableTools.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -615,6 +616,17 @@ function ReFlesh(source) {
|
||||
</n-flex>
|
||||
</template>
|
||||
<template #action>
|
||||
<n-flex justify="left" style="margin-bottom: 10px">
|
||||
<n-switch v-model:value="enableTools" :round="false">
|
||||
<template #checked>
|
||||
启用AI函数工具调用
|
||||
</template>
|
||||
<template #unchecked>
|
||||
不启用AI函数工具调用
|
||||
</template>
|
||||
</n-switch>
|
||||
<n-gradient-text type="error" style="margin-left: 10px">*AI函数工具调用可以增强AI获取数据的能力,但会消耗更多tokens。</n-gradient-text>
|
||||
</n-flex>
|
||||
<n-flex justify="space-between" style="margin-bottom: 10px">
|
||||
<n-select style="width: 49%" v-model:value="sysPromptId" label-field="name" value-field="ID"
|
||||
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
|
||||
|
@ -63,6 +63,7 @@ import vueDanmaku from 'vue3-danmaku'
|
||||
import {keys, padStart} from "lodash";
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import MoneyTrend from "./moneyTrend.vue";
|
||||
import {TaskTools} from "@vicons/carbon";
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@ -101,6 +102,7 @@ const modalShow3 = ref(false)
|
||||
const modalShow4 = ref(false)
|
||||
const modalShow5 = ref(false)
|
||||
const addBTN = ref(true)
|
||||
const enableTools= ref(false)
|
||||
const formModel = ref({
|
||||
name: "",
|
||||
code: "",
|
||||
@ -223,8 +225,11 @@ onMounted(() => {
|
||||
if (!stocks.value.includes(followedStock.StockCode)) {
|
||||
stocks.value.push(followedStock.StockCode)
|
||||
}
|
||||
Greet(followedStock.StockCode).then(result => {
|
||||
updateData(result)
|
||||
})
|
||||
}
|
||||
monitor()
|
||||
//monitor()
|
||||
message.destroyAll()
|
||||
})
|
||||
|
||||
@ -464,6 +469,7 @@ function removeMonitor(code, name, key) {
|
||||
|
||||
UnFollow(code).then(result => {
|
||||
message.success(result)
|
||||
monitor()
|
||||
})
|
||||
}
|
||||
|
||||
@ -581,7 +587,6 @@ async function monitor() {
|
||||
showPopover.value = true
|
||||
}
|
||||
for (let code of stocks.value) {
|
||||
|
||||
Greet(code).then(result => {
|
||||
updateData(result)
|
||||
})
|
||||
@ -590,8 +595,7 @@ async function monitor() {
|
||||
|
||||
|
||||
function GetSortKey(sort, code) {
|
||||
let sortKey = padStart(sort, 8, '0') + "_" + code
|
||||
return sortKey
|
||||
return padStart(sort, 8, '0') + "_" + code
|
||||
}
|
||||
|
||||
function onSelect(item) {
|
||||
@ -1354,7 +1358,7 @@ function aiReCheckStock(stock, stockCode) {
|
||||
//
|
||||
|
||||
//message.info("sysPromptId:"+data.sysPromptId)
|
||||
NewChatStream(stock, stockCode, data.question, data.sysPromptId)
|
||||
NewChatStream(stock, stockCode, data.question, data.sysPromptId,enableTools.value)
|
||||
}
|
||||
|
||||
function aiCheckStock(stock, stockCode) {
|
||||
@ -1383,12 +1387,12 @@ function aiCheckStock(stock, stockCode) {
|
||||
data.time = ""
|
||||
data.name = stock
|
||||
data.code = stockCode
|
||||
data.loading = true
|
||||
data.loading = false
|
||||
modalShow4.value = true
|
||||
message.loading("ai检测中...", {
|
||||
duration: 0,
|
||||
})
|
||||
NewChatStream(stock, stockCode, "", data.sysPromptId)
|
||||
// message.loading("ai检测中...", {
|
||||
// duration: 0,
|
||||
// })
|
||||
// NewChatStream(stock, stockCode, "", data.sysPromptId)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1573,18 +1577,20 @@ function AddStockGroupInfo(groupId, code, name) {
|
||||
}
|
||||
|
||||
function updateTab(name) {
|
||||
stocks.value = []
|
||||
currentGroupId.value = Number(name)
|
||||
GetFollowList(currentGroupId.value).then(result => {
|
||||
stocks.value = []
|
||||
followList.value = result
|
||||
for (const followedStock of result) {
|
||||
if (followedStock.StockCode.startsWith("us")) {
|
||||
followedStock.StockCode = "gb_" + followedStock.StockCode.replace("us", "").toLowerCase()
|
||||
}
|
||||
////console.log("followList",followedStock.StockCode)
|
||||
stocks.value.push(followedStock.StockCode)
|
||||
Greet(followedStock.StockCode).then(result => {
|
||||
updateData(result)
|
||||
})
|
||||
}
|
||||
monitor()
|
||||
//monitor()
|
||||
message.destroyAll()
|
||||
})
|
||||
}
|
||||
@ -1739,11 +1745,10 @@ function searchStockReport(stockCode) {
|
||||
@click="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
|
||||
取消关注
|
||||
</n-button>
|
||||
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning"
|
||||
@click="aiCheckStock(result['股票名称'],result['股票代码'])">
|
||||
|
||||
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])">
|
||||
AI分析
|
||||
</n-button>
|
||||
|
||||
</template>
|
||||
<template #footer>
|
||||
<n-flex justify="center">
|
||||
@ -1876,10 +1881,10 @@ function searchStockReport(stockCode) {
|
||||
@click="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
|
||||
取消关注
|
||||
</n-button>
|
||||
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning"
|
||||
@click="aiCheckStock(result['股票名称'],result['股票代码'])">
|
||||
AI分析
|
||||
</n-button>
|
||||
|
||||
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])">
|
||||
AI分析
|
||||
</n-button>
|
||||
<n-button secondary type="error" size="tiny"
|
||||
@click="delStockGroup(result['股票代码'],result['股票名称'],group.ID)">移出分组
|
||||
</n-button>
|
||||
@ -2045,7 +2050,7 @@ function searchStockReport(stockCode) {
|
||||
</n-modal>
|
||||
|
||||
<n-modal transform-origin="center" v-model:show="modalShow4" preset="card" style="width: 800px;"
|
||||
:title="'['+data.name+']AI分析结果'">
|
||||
:title="'['+data.name+']AI分析'">
|
||||
<n-spin size="small" :show="data.loading">
|
||||
<MdEditor v-if="enableEditor" :toolbars="toolbars" ref="mdEditorRef" style="height: 440px;text-align: left"
|
||||
:modelValue="data.airesult" :theme="theme">
|
||||
@ -2069,7 +2074,17 @@ function searchStockReport(stockCode) {
|
||||
</n-flex>
|
||||
</template>
|
||||
<template #action>
|
||||
|
||||
<n-flex justify="left" style="margin-bottom: 10px">
|
||||
<n-switch v-model:value="enableTools" :round="false">
|
||||
<template #checked>
|
||||
启用AI函数工具调用
|
||||
</template>
|
||||
<template #unchecked>
|
||||
不启用AI函数工具调用
|
||||
</template>
|
||||
</n-switch>
|
||||
<n-gradient-text type="error" style="margin-left: 10px">*AI函数工具调用可以增强AI获取数据的能力,但会消耗更多tokens。</n-gradient-text>
|
||||
</n-flex>
|
||||
<n-flex justify="space-between" style="margin-bottom: 10px">
|
||||
<n-select style="width: 49%" v-model:value="data.sysPromptId" label-field="name" value-field="ID"
|
||||
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
|
||||
@ -2087,7 +2102,7 @@ function searchStockReport(stockCode) {
|
||||
}"
|
||||
/>
|
||||
<!-- <n-button size="tiny" type="error" @click="enableEditor=!enableEditor">编辑/预览</n-button>-->
|
||||
<n-button size="tiny" type="warning" @click="aiReCheckStock(data.name,data.code)">再次分析</n-button>
|
||||
<n-button size="tiny" type="warning" @click="aiReCheckStock(data.name,data.code)">开始AI分析</n-button>
|
||||
<n-button size="tiny" type="info" @click="saveAsImage(data.name,data.code)">保存为图片</n-button>
|
||||
<n-button size="tiny" type="success" @click="copyToClipboard">复制到剪切板</n-button>
|
||||
<n-button size="tiny" type="primary" @click="saveAsMarkdown">保存为Markdown文件</n-button>
|
||||
|
6
frontend/wailsjs/go/main/App.d.ts
vendored
6
frontend/wailsjs/go/main/App.d.ts
vendored
@ -39,6 +39,8 @@ export function GetGroupList():Promise<Array<data.Group>>;
|
||||
|
||||
export function GetGroupStockList(arg1:number):Promise<Array<data.GroupStock>>;
|
||||
|
||||
export function GetHotStrategy():Promise<Record<string, any>>;
|
||||
|
||||
export function GetIndustryMoneyRankSina(arg1:string,arg2:string):Promise<Array<Record<string, any>>>;
|
||||
|
||||
export function GetIndustryRank(arg1:string,arg2:number):Promise<Array<any>>;
|
||||
@ -79,7 +81,7 @@ export function InvestCalendarTimeLine(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function LongTigerRank(arg1:string):Promise<any>;
|
||||
|
||||
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any):Promise<void>;
|
||||
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any,arg5:boolean):Promise<void>;
|
||||
|
||||
export function NewsPush(arg1:any):Promise<void>;
|
||||
|
||||
@ -113,7 +115,7 @@ export function StockNotice(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function StockResearchReport(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function SummaryStockNews(arg1:string,arg2:any):Promise<void>;
|
||||
export function SummaryStockNews(arg1:string,arg2:any,arg3:boolean):Promise<void>;
|
||||
|
||||
export function UnFollow(arg1:string):Promise<string>;
|
||||
|
||||
|
@ -74,6 +74,10 @@ export function GetGroupStockList(arg1) {
|
||||
return window['go']['main']['App']['GetGroupStockList'](arg1);
|
||||
}
|
||||
|
||||
export function GetHotStrategy() {
|
||||
return window['go']['main']['App']['GetHotStrategy']();
|
||||
}
|
||||
|
||||
export function GetIndustryMoneyRankSina(arg1, arg2) {
|
||||
return window['go']['main']['App']['GetIndustryMoneyRankSina'](arg1, arg2);
|
||||
}
|
||||
@ -154,8 +158,8 @@ export function LongTigerRank(arg1) {
|
||||
return window['go']['main']['App']['LongTigerRank'](arg1);
|
||||
}
|
||||
|
||||
export function NewChatStream(arg1, arg2, arg3, arg4) {
|
||||
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4);
|
||||
export function NewChatStream(arg1, arg2, arg3, arg4, arg5) {
|
||||
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4, arg5);
|
||||
}
|
||||
|
||||
export function NewsPush(arg1) {
|
||||
@ -222,8 +226,8 @@ export function StockResearchReport(arg1) {
|
||||
return window['go']['main']['App']['StockResearchReport'](arg1);
|
||||
}
|
||||
|
||||
export function SummaryStockNews(arg1, arg2) {
|
||||
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2);
|
||||
export function SummaryStockNews(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function UnFollow(arg1) {
|
||||
|
@ -701,6 +701,7 @@ export namespace models {
|
||||
alipay: string;
|
||||
wxpay: string;
|
||||
buildTimeStamp: number;
|
||||
officialStatement: string;
|
||||
IsDel: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
@ -719,6 +720,7 @@ export namespace models {
|
||||
this.alipay = source["alipay"];
|
||||
this.wxpay = source["wxpay"];
|
||||
this.buildTimeStamp = source["buildTimeStamp"];
|
||||
this.officialStatement = source["officialStatement"];
|
||||
this.IsDel = source["IsDel"];
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user