Compare commits

...

25 Commits

Author SHA1 Message Date
ArvinLovegood
7412d56409 feat(data):添加AI分析报告和ai推荐股票的历史数据查看功能
- 创建aiRecommendStocksList组件展示AI股票推荐列表
- 创建researchIndex组件作为研究分析主界面包含分析报告和股票推荐
- 创建researchReport组件展示AI分析报告列表和详情预览
2026-01-25 14:17:57 +08:00
ArvinLovegood
f75b457082 feat(stock):添加AI股票推荐功能和分析报告管理
- 新增AI股票推荐相关数据模型和数据库迁移
- 实现AI分析报告列表查询和删除功能
- 集成股票推荐记录的增删查接口
- 添加研究中心界面和股票推荐记录展示页面
- 更新K线图组件支持股票名称显示和数值计算
- 优化后端日志配置和测试代码调试功能
- 添加VIP等级管理和批量删除响应结果功能
2026-01-25 14:13:53 +08:00
ArvinLovegood
a43095cdd4 fix(pricing): 移除VIP2套餐中的硅基流动AI分析服务描述
- 从前端组件about.vue中移除VIP2套餐的硅基流动AI分析服务描述
- 从README.md中移除VIP2套餐的硅基流动AI分析服务描述
- 统一VIP2套餐功能描述为仅包含市场资讯同步功能
2026-01-23 16:01:31 +08:00
ArvinLovegood
6ca0d0df32 feat(settings):添加模型api服务专属HTTP代理配置功能
- 在AIConfig模型中新增httpProxy和httpProxyEnabled字段
- 更新OpenAi结构体以支持代理配置
- 修改OpenAI API调用逻辑使用实例级别的代理设置
- 在前端设置页面添加代理开关和地址输入框
- 扩展数据库AIConfig表结构以存储代理配置
- 增加代理配置的增删改查功能支持
- 调整默认maxTokens值从1024到4096
2026-01-19 09:55:33 +08:00
ArvinLovegood
fea9b06a27 feat(settings):添加模型api服务专属HTTP代理配置功能
- 在AIConfig模型中新增httpProxy和httpProxyEnabled字段
- 更新OpenAi结构体以支持代理配置
- 修改OpenAI API调用逻辑使用实例级别的代理设置
- 在前端设置页面添加代理开关和地址输入框
- 扩展数据库AIConfig表结构以存储代理配置
- 增加代理配置的增删改查功能支持
- 调整默认maxTokens值从1024到4096
2026-01-19 09:47:42 +08:00
ArvinLovegood
1d1e437b47 feat(stock):添加股票资金流向和概念信息查询功能
- 新增 GetStockMoneyData 工具用于查询今日股票资金流入排名
- 新增 GetStockConceptInfo 工具用于获取股票所属概念详细信息
- 添加 StockMoneyDataResp、StockMoneyData、StockMoneyDataDiff 数据模型
- 添加 StockConceptInfoResp、StockConceptInfoResult、StockConceptInfo 数据模型
- 实现 GetStockMoneyData 方法从东方财富接口获取资金流向数据
- 实现 GetStockConceptInfo 方法通过股票代码查询概念题材信息
- 在 OpenAI API 中集成两个新工具的调用逻辑
- 添加相关单元测试验证功能正常工作
2026-01-16 19:32:04 +08:00
ArvinLovegood
eca2f8adee feat(app):添加新闻推送时间限制和代理配置支持
- 在app.go中添加时间差检查,仅当数据时间与当前时间差小于5分钟时才推送新闻
- 在openai_api.go中添加HTTP代理配置支持,根据设置启用代理连接
- 重构openai_api.go中的请求体构建逻辑,支持动态配置thinking参数
- 更新openai_api_test.go中的测试参数以匹配新功能
- 移除settings_api.go中的默认设置初始化逻辑以优化启动流程
2026-01-15 16:42:12 +08:00
ArvinLovegood
49d2109d60 fix(config):修正K线数据天数配置限制
- 将默认K线天数从120调整为60
- 更新前端表单验证最大值为60
- 修复后端配置默认值为60天
- 添加更详细的选股语言示例说明
2026-01-07 17:51:12 +08:00
ArvinLovegood
f9294fbffd feat(stock):添加热门股票排名功能
- 在选股自然语言描述中增加了近期趋势示例说明
- 新增HotStockTable工具函数用于获取热门股票数据
- 实现HotStockTable工具的调用逻辑和参数处理
- 集成雪球热门股票API接口
- 添加分页大小参数支持,默认为50条记录
- 实现Markdown表格格式的热门股票排名展示
2026-01-06 15:33:35 +08:00
ArvinLovegood
cc12a886c1 feat(news):扩展新闻标签匹配和添加热门股票数据功能
- 在新闻标签匹配中增加"外媒简讯"和"外媒"标签支持
- 过滤掉"rotating_light"和"loudspeaker"标签避免重复处理
- 优化GetSource函数支持多标签匹配判断新闻来源
- 添加THSHotStrategy数据模型定义热门策略数据结构
- 实现StrategySquare接口获取热门选股策略数据
- 在OpenAI接口中增加热门股票排名数据查询功能
- 为HotItem模型添加markdown标签支持表格显示
- 增加测试函数验证热门策略表格功能
2026-01-04 13:31:09 +08:00
ArvinLovegood
34ea989d47 refactor(data):优化数据API和测试用例
- 调整并发请求数量,移除市场指数行情的获取逻辑
- 注释掉多个市场指数行情查询的协程,减少不必要的数据请求
- 添加投资者互动数据和宏观经济数据的获取功能
- 更新24小时新闻列表的格式化方式,包含时间和内容标题
- 移除顶部新闻列表和外媒全球新闻资讯的获取逻辑
- 调整测试用例中的参数值,将历史数据天数从30改为5
- 修复股价数据检查逻辑,移除对空字符串的特殊处理
2026-01-01 22:34:19 +08:00
ArvinLovegood
aadff1c5eb ci(workflow):添加macOS平台的Intel和ARM64架构构建支持
- 添加 go-stock-darwin-intel 构建配置
- 添加 go-stock-darwin-arm64 构建配置
- 扩展 macOS 平台的架构覆盖范围
2026-01-01 16:18:29 +08:00
ArvinLovegood
6b7fed00a3 refactor(openai_api):优化新闻资讯获取逻辑
- 移除 TradingViewNews 和 ReutersNew 相关的 goroutine 调用
- 将 GetNewsList2 替换为 GetNews24HoursList 获取24小时市场资讯
- 添加 ClsCalendar 功能获取近期重大事件/会议信息
- 调整 WaitGroup 的计数以匹配新的并发任务数量
- 修改时间格式化显示为标准格式 "2006-01-02 15:04:05"
2026-01-01 13:58:07 +08:00
ArvinLovegood
5af0e28601 feat(telegraph):电报标签功能和前端显示优化
- 实现电报数据的标签关联功能,支持主题标签存储
- 添加电报列表分页查询接口,支持标签预加载
- 在前端新闻列表组件中添加时间显示功能
- 优化新闻列表按日期分组显示的布局结构
- 添加定时刷新财联社电报、新浪财经和外媒数据功能
- 根据新闻列表长度动态调整网格列数显示
2026-01-01 12:00:32 +08:00
ArvinLovegood
c4af77954e refactor(app):将syncNews函数改为方法并集成情感分析
- 将syncNews函数转换为App结构体的方法
- 在新闻同步逻辑中集成情感分析功能
- 添加新闻推送功能到同步流程中
- 修复VIP用户新闻同步的调用方式
2026-01-01 09:07:25 +08:00
ArvinLovegood
8236adb680 feat(news):添加新闻标签旋转灯标识处理
- 新增 rotating_light 标签检测逻辑
- 根据标签内容动态设置 isRed 属性
- 优化新闻电报数据结构的红色标识字段处理
2026-01-01 08:48:01 +08:00
ArvinLovegood
0adfdab441 feat(app):添加VIP2用户市场资讯自动同步功能
- 在VIP2级别中添加自动同步最近24小时市场资讯功能
- 实现自动同步外媒简讯、财联社电报、新浪财经资讯
- 重构VIP验证逻辑到独立的isVip方法中
- 添加syncNews方法实现资讯获取和存储功能
- 更新前端about组件中VIP2功能描述
- 更新README中VIP2功能说明
- 添加NtfyNews数据模型定义
- 添加golang.org/x/exp依赖包
2025-12-31 18:27:26 +08:00
ArvinLovegood
b1c618a9de feat(app):添加VIP用户市场资讯自动同步功能
- 在VIP2级别中添加自动同步最近24小时市场资讯功能
- 实现自动同步外媒简讯、财联社电报、新浪财经资讯
- 重构VIP验证逻辑到独立的isVip方法中
- 添加syncNews方法实现资讯获取和存储功能
- 更新前端about组件中VIP2功能描述
- 更新README中VIP2功能说明
- 添加NtfyNews数据模型定义
- 添加golang.org/x/exp依赖包
2025-12-31 18:27:08 +08:00
ArvinLovegood
709c372bf3 feat(app):添加VIP用户市场资讯自动同步功能
- 在VIP2级别中添加自动同步最近24小时市场资讯功能
- 实现自动同步外媒简讯、财联社电报、新浪财经资讯
- 重构VIP验证逻辑到独立的isVip方法中
- 添加syncNews方法实现资讯获取和存储功能
- 更新前端about组件中VIP2功能描述
- 更新README中VIP2功能说明
- 添加NtfyNews数据模型定义
- 添加golang.org/x/exp依赖包
2025-12-31 18:06:51 +08:00
ArvinLovegood
beb022c448 docs(readme): 更新重大更新部分
- 添加2025.11.21新增带频率权重的情感分析功能
- 插入新功能截图img_1.png
- 保留原有市场资讯AI分析和总结功能说明
- 维持市场行情模块介绍内容
2025-12-19 17:21:37 +08:00
ArvinLovegood
d1aed3419b docs(readme): 更新日志新增AI思考模式与热门选股策略功能
- 在更新日志中添加2025.12.16新增AI思考模式与热门选股策略功能
- 补充相关功能描述与日期信息
- 维护文档结构与格式一致性
2025-12-19 17:16:30 +08:00
ArvinLovegood
48511c61df docs(readme):更新README文档中的更新日志和功能描述
- 添加2025.11.21新增带频率权重的情感分析功能说明
- 添加2025.10.30 AI智能体功能开关及页面水印移除说明
- 添加2025.09.27机构研究报告AI工具函数说明
- 添加2025.08.09 AI智能体聊天功能说明
- 更新选股自然语言描述,增加技术指标和成交量筛选示例
2025-12-19 17:15:05 +08:00
ArvinLovegood
163e8800f9 docs(readme):更新技术支持联系方式
- 将技术支持微信联系方式改为QQ
- 移除微信二维码图片显示
- 更新表格中的联系方式描述
2025-12-17 22:59:20 +08:00
ArvinLovegood
70e0d19c58 feat(ai):新增AI思考模式与热门选股策略功能
- 在NewChatStream和SummaryStockNews接口中增加think参数以支持AI思考模式
- 更新前端组件market.vue和stock.vue,添加思考模式开关控件
- 升级github.com/cloudwego/eino依赖至v0.7.9版本
- 修改ToolFunction结构体,新增Strict字段并调整Parameters为指针类型
- 实现HotStrategyTable工具函数,用于获取并展示热门选股策略表格
- 优化市场新闻API中的去重逻辑,当标题为空时使用内容进行判重
- 在AI对话流中增加对reasoning_content的支持,提升推理过程可见性
- 完善AskAi和AskAiWithTools方法,支持传递thinkingMode参数控制模型推理行为
- 调整FunctionParameters结构体,新增AdditionalProperties字段以增强灵活性
- 修复测试文件中IndustryResearchReport调用参数及注释问题
2025-12-16 15:49:59 +08:00
ArvinLovegood
4e04bacf22 refactor(data):重构情感分析与词频统计模块
- 将 SentimentResult 和 WordFreqWithWeight 结构体移至 models 包统一管理
- 更新 AnalyzeSentiment 和 AnalyzeSentimentWithFreqWeight 函数返回值类型
- 优化 NewsAnalyze 函数实现新闻内容自动获取与分析
- 新增定时任务定期执行新闻情感分析
- 完善数据库模型迁移,支持词频与情感分析结果存储
- 调整前端接口声明文件,确保类型一致性
- 移除冗余代码注释,提升代码可读性
2025-12-12 15:35:50 +08:00
47 changed files with 3110 additions and 549 deletions

View File

@@ -29,6 +29,12 @@ jobs:
- name: 'go-stock-darwin-universal'
platform: 'darwin/universal'
os: 'macos-latest'
- name: 'go-stock-darwin-intel'
platform: 'darwin'
os: 'macos-latest'
- name: 'go-stock-darwin-arm64'
platform: 'darwin/arm64'
os: 'macos-latest'
runs-on: ${{ matrix.build.os }}
steps:

View File

@@ -53,7 +53,7 @@
|:--------------------------------|----------------|:-------------------------------------------------------|
| 每月 0 RMB | vip0 | 🌟 全部功能,软件自动更新(从GitHub下载),自行解决github平台网络问题。 |
| 每月赞助 18.8 RMB<br>每年赞助 120 RMB | vip1 | 💕 全部功能,软件自动更新(从CDN下载),更新快速便捷。AI配置指导提示词参考等 |
| 每月赞助 28.8 RMB<br>每年赞助 240 RMB | vip2 | 💕 💕 vip1全部功能,赠送硅基流动AI分析服务 |
| 每月赞助 28.8 RMB<br>每年赞助 240 RMB | vip2 | 💕 💕 vip1全部功能,启动时自动同步最近24小时市场资讯(包括外媒简讯) |
| 每月赞助 X RMB | vipX | 🧩 更多计划视go-stock开源项目发展情况而定...(承接GitHub项目README广告推广💖) |
## 🧩 重大功能开发计划
@@ -69,6 +69,11 @@
| 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 |
## 👀 更新日志
### 2025.12.16 新增AI思考模式与热门选股策略功能
### 2025.11.21 新增带频率权重的情感分析功能
### 2025.10.30 添加AI智能体功能开关(默认关闭,因为使用体验不理想),移除页面水印
### 2025.09.27 添加机构/券商的研究报告AI工具函数
### 2025.08.09 添加AI智能体聊天功能
### 2025.07.08 实现软件自动更新功能
### 2025.07.07 卡片添加迷你分时图
### 2025.07.05 MacOs支持
@@ -116,6 +121,8 @@
## 🦄 重大更新
### BIG NEWS !!! 重大更新!!!
- 2025.11.21 新增带频率权重的情感分析功能
![img_1.png](build/screenshot/img15.png)
- 2025.04.25 市场资讯支持AI分析和总结让AI帮你读市场
![img.png](img.png)
- 2025.04.24 新增市场行情模块:即时掌握全球市场行情资讯/动态从此再也不用偷摸去各大财经网站啦。go-stock一键帮你搞定
@@ -162,15 +169,15 @@
## 🐳 关于技术支持申明
- 本软件基于开源技术构建使用Wails、NaiveUI、Vue、AI大模型等开源项目。 技术上如有问题,可以先向对应的开源社区请求帮助。
- 开源不易,本人精力和时间有限,如需一对一技术支持,请先赞助。联系微信(备注 技术支持)ArvinLovegood
- 开源不易,本人精力和时间有限,如需一对一技术支持,请先赞助。联系QQ(备注 技术支持)506808970
<img src="./build/wx.jpg" width="301px" height="402px" alt="ArvinLovegood">
[//]: # (<img src="./build/wx.jpg" width="301px" height="402px" alt="ArvinLovegood">)
| 技术支持方式 | 赞助(元) |
|:--------------------------------|:-----:|
| 加 QQ506808970微信ArvinLovegood | 100/次 |
| 长期技术支持(不限次数,新功能优先体验等) | 5000 |
| 加 QQ506808970 | 100/次 |
| 长期技术支持(不限次数,新功能优先体验等) | 5000 |

376
app.go
View File

@@ -1,6 +1,7 @@
package main
import (
"bufio"
"bytes"
"context"
"encoding/base64"
@@ -18,6 +19,7 @@ import (
"github.com/duke-git/lancet/v2/cryptor"
"github.com/inconshreveable/go-update"
"golang.org/x/exp/slices"
"github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache"
@@ -38,6 +40,7 @@ type App struct {
cronEntrys map[string]cron.EntryID
AiTools []data.Tool
SponsorInfo map[string]any
VipLevel int64
}
// NewApp creates a new App application struct
@@ -62,19 +65,23 @@ func AddTools(tools []data.Tool) []data.Tool {
Function: data.ToolFunction{
Name: "SearchStockByIndicators",
Description: "根据自然语言筛选股票,返回自然语言选股条件要求的股票所有相关数据。输入股票名称可以获取当前股票最新的股价交易数据和基础财务指标信息,多个股票名称使用,分隔。",
Parameters: data.FunctionParameters{
Parameters: &data.FunctionParameters{
Type: "object",
Properties: map[string]any{
"words": map[string]any{
"type": "string",
"description": "选股自然语言。" +
"例如:查看技术指标:上海贝岭,macd,rsi,kdj,boll,5日均线,14日均线,30日均线,60日均线,成交量,OBV,EMA" +
"例如:查看近期趋势量比连续2天>1主力连续2日净流入且递增主力净额>3000万元行业股价在20日线上" +
"例如:当日成交量 ≥ 近 5 日平均成交量 ×1.5,收盘价 ≥ 20 日均线20 日均线 ≥ 60 日均线,当日涨幅 3%-7% 3日主力资金净流入累计≥5000 万元,当日换手率 5%-15%筹码集中度90% 筹码峰≤15%非创业板非科创板非ST非北交所行业" +
"例如:查看有潜力的成交量爆发股最近7日成交量量比大于3出现过一次非ST" +
"例1创新药,半导体;PE<30;净利润增长率>50%。 " +
"例2上证指数,科创50。 " +
"例3长电科技,上海贝岭。" +
"例4长电科技,上海贝岭;KDJ,MACD,RSI,BOLL,主力净流入/流出" +
"例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亿。" +
"例7股价在20日线上一月之内涨停次数>=1量比大于1换手率大于3%" +
"例8基本条件前期有爆量回调到 10 日线,当日是缩量阴线,均线趋势向上。;优选条件:一月之内涨停次数>=1",
},
},
@@ -88,7 +95,7 @@ func AddTools(tools []data.Tool) []data.Tool {
Function: data.ToolFunction{
Name: "GetStockKLine",
Description: "获取股票日K线数据。",
Parameters: data.FunctionParameters{
Parameters: &data.FunctionParameters{
Type: "object",
Properties: map[string]any{
"days": map[string]any{
@@ -110,7 +117,7 @@ func AddTools(tools []data.Tool) []data.Tool {
Function: data.ToolFunction{
Name: "InteractiveAnswer",
Description: "获取投资者与上市公司互动问答的数据,反映当前投资者关注的热点问题",
Parameters: data.FunctionParameters{
Parameters: &data.FunctionParameters{
Type: "object",
Properties: map[string]any{
"page": map[string]any{
@@ -162,7 +169,7 @@ func AddTools(tools []data.Tool) []data.Tool {
Function: data.ToolFunction{
Name: "GetStockResearchReport",
Description: "获取市场分析师的股票研究报告",
Parameters: data.FunctionParameters{
Parameters: &data.FunctionParameters{
Type: "object",
Properties: map[string]any{
"stockCode": map[string]any{
@@ -175,6 +182,138 @@ func AddTools(tools []data.Tool) []data.Tool {
},
})
tools = append(tools, data.Tool{
Type: "function",
Function: data.ToolFunction{
Name: "HotStrategyTable",
Description: "获取当前热门选股策略",
},
})
tools = append(tools, data.Tool{
Type: "function",
Function: data.ToolFunction{
Name: "HotStockTable",
Description: "当前热门股票排名",
Parameters: &data.FunctionParameters{
Type: "object",
Properties: map[string]any{
"pageSize": map[string]any{
"type": "string",
"description": "分页大小",
},
},
Required: []string{"pageSize"},
},
},
})
tools = append(tools, data.Tool{
Type: "function",
Function: data.ToolFunction{
Name: "GetStockMoneyData",
Description: "今日股票资金流入排名",
Parameters: &data.FunctionParameters{
Type: "object",
Properties: map[string]any{
"pageSize": map[string]any{
"type": "string",
"description": "分页大小",
},
},
Required: []string{"pageSize"},
},
},
})
tools = append(tools, data.Tool{
Type: "function",
Function: data.ToolFunction{
Name: "GetStockConceptInfo",
Description: "获取股票所属概念详细信息",
Parameters: &data.FunctionParameters{
Type: "object",
Properties: map[string]any{
"code": map[string]any{
"type": "string",
"description": "股票代码,如601138.SH。注意 上海证券交易所股票以.SH结尾深圳证券交易所股票以.SZ结尾港股股票以.HK结尾北交所股票以.BJ结尾",
},
},
Required: []string{"code"},
},
},
})
//CreateAiRecommendStocks
tools = append(tools, data.Tool{
Type: "function",
Function: data.ToolFunction{
Name: "CreateAiRecommendStocks",
Description: "创建/保存AI推荐股票记录",
Parameters: &data.FunctionParameters{
Type: "object",
Properties: map[string]any{
"modelName": map[string]any{
"type": "string",
"description": "模型名称",
},
"stockCode": map[string]any{
"type": "string",
"description": "股票代码,如601138.SH。注意 上海证券交易所股票以.SH结尾深圳证券交易所股票以.SZ结尾港股股票以.HK结尾北交所股票以.BJ结尾",
},
"stockName": map[string]any{
"type": "string",
"description": "股票名称",
},
"bkCode": map[string]any{
"type": "string",
"description": "板块/行业代码",
},
"bkName": map[string]any{
"type": "string",
"description": "板块/行业名称",
},
"stockPrice": map[string]any{
"type": "string",
"description": "推荐时股票价格",
},
"stockPrePrice": map[string]any{
"type": "string",
"description": "前一交易日股票价格",
},
"stockClosePrice": map[string]any{
"type": "string",
"description": "推荐时股票收盘价格",
},
"recommendReason": map[string]any{
"type": "string",
"description": "推荐理由/驱动因素/逻辑",
},
"recommendBuyPrice": map[string]any{
"type": "string",
"description": "ai建议买入价",
},
"recommendStopProfitPrice": map[string]any{
"type": "string",
"description": "ai建议止盈价",
},
"recommendStopLossPrice": map[string]any{
"type": "string",
"description": "ai建议止损价",
},
"riskRemarks": map[string]any{
"type": "string",
"description": "风险提示",
},
"remarks": map[string]any{
"type": "string",
"description": "备注",
},
},
Required: []string{"stockCode", "stockName"},
},
},
})
return tools
}
@@ -245,8 +384,16 @@ func (a *App) CheckUpdate(flag int) {
return
}
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
if releaseVersion.TagName != Version {
if _, vipLevel, ok := a.isVip(sponsorCode, "", releaseVersion); ok {
level, _ := convertor.ToInt(vipLevel)
a.VipLevel = level
if level >= 2 {
go a.syncNews()
}
}
if releaseVersion.TagName != Version {
tag := &models.Tag{}
_, err = resty.New().R().
SetResult(tag).
@@ -271,59 +418,9 @@ func (a *App) CheckUpdate(flag int) {
if IsMacOS() {
downloadUrl = fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-darwin-universal", releaseVersion.TagName)
}
sponsorCode = strutil.Trim(a.GetConfig().SponsorCode)
if sponsorCode != "" {
encrypted, err := hex.DecodeString(sponsorCode)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
key, err := hex.DecodeString(BuildKey)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
decrypt := string(cryptor.AesEcbDecrypt(encrypted, key))
err = json.Unmarshal([]byte(decrypt), &a.SponsorInfo)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
vipStartTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipStartTime"].(string), time.Local)
vipEndTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipEndTime"].(string), time.Local)
vipAuthTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipAuthTime"].(string), time.Local)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return
}
isVip := false
if time.Now().After(vipAuthTime) && time.Now().After(vipStartTime) && time.Now().Before(vipEndTime) {
isVip = true
}
if IsWindows() {
if isVip {
if a.SponsorInfo["winDownUrl"] == nil {
downloadUrl = fmt.Sprintf("https://gitproxy.click/https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-windows-amd64.exe", releaseVersion.TagName)
} else {
downloadUrl = a.SponsorInfo["winDownUrl"].(string)
}
} else {
downloadUrl = fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-windows-amd64.exe", releaseVersion.TagName)
}
}
if IsMacOS() {
if isVip {
if a.SponsorInfo["macDownUrl"] == nil {
downloadUrl = fmt.Sprintf("https://gitproxy.click/https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-darwin-universal", releaseVersion.TagName)
} else {
downloadUrl = a.SponsorInfo["macDownUrl"].(string)
}
} else {
downloadUrl = fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-darwin-universal", releaseVersion.TagName)
}
}
downloadUrl, _, done := a.isVip(sponsorCode, downloadUrl, releaseVersion)
if !done {
return
}
go runtime.EventsEmit(a.ctx, "newsPush", map[string]any{
"time": "发现新版本:" + releaseVersion.TagName,
@@ -379,6 +476,147 @@ func (a *App) CheckUpdate(flag int) {
}
}
func (a *App) isVip(sponsorCode string, downloadUrl string, releaseVersion *models.GitHubReleaseVersion) (string, string, bool) {
isVip := false
vipLevel := "0"
sponsorCode = strutil.Trim(a.GetConfig().SponsorCode)
if sponsorCode != "" {
encrypted, err := hex.DecodeString(sponsorCode)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "", "0", false
}
key, err := hex.DecodeString(BuildKey)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "", "0", false
}
decrypt := string(cryptor.AesEcbDecrypt(encrypted, key))
err = json.Unmarshal([]byte(decrypt), &a.SponsorInfo)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "", "0", false
}
vipLevel = a.SponsorInfo["vipLevel"].(string)
vipStartTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipStartTime"].(string), time.Local)
vipEndTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipEndTime"].(string), time.Local)
vipAuthTime, err := time.ParseInLocation("2006-01-02 15:04:05", a.SponsorInfo["vipAuthTime"].(string), time.Local)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "", vipLevel, false
}
if time.Now().After(vipAuthTime) && time.Now().After(vipStartTime) && time.Now().Before(vipEndTime) {
isVip = true
}
if IsWindows() {
if isVip {
if a.SponsorInfo["winDownUrl"] == nil {
downloadUrl = fmt.Sprintf("https://gitproxy.click/https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-windows-amd64.exe", releaseVersion.TagName)
} else {
downloadUrl = a.SponsorInfo["winDownUrl"].(string)
}
} else {
downloadUrl = fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-windows-amd64.exe", releaseVersion.TagName)
}
}
if IsMacOS() {
if isVip {
if a.SponsorInfo["macDownUrl"] == nil {
downloadUrl = fmt.Sprintf("https://gitproxy.click/https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-darwin-universal", releaseVersion.TagName)
} else {
downloadUrl = a.SponsorInfo["macDownUrl"].(string)
}
} else {
downloadUrl = fmt.Sprintf("https://github.com/ArvinLovegood/go-stock/releases/download/%s/go-stock-darwin-universal", releaseVersion.TagName)
}
}
}
return downloadUrl, vipLevel, isVip
}
func (a *App) syncNews() {
defer PanicHandler()
client := resty.New()
url := fmt.Sprintf("http://go-stock.sparkmemory.top:16666/FinancialNews/json?since=%d", time.Now().Add(-24*time.Hour).Unix())
logger.SugaredLogger.Infof("syncNews:%s", url)
resp, err := client.R().SetDoNotParseResponse(true).Get(url)
body := resp.RawBody()
defer body.Close()
if err != nil {
logger.SugaredLogger.Errorf("syncNews error:%s", err.Error())
}
scanner := bufio.NewScanner(body)
for scanner.Scan() {
line := scanner.Text()
logger.SugaredLogger.Infof("Received data: %s", line)
news := &models.NtfyNews{}
err := json.Unmarshal(scanner.Bytes(), news)
if err != nil {
return
}
dataTime := time.UnixMilli(int64(news.Time * 1000))
if slice.ContainAny(news.Tags, []string{"外媒资讯", "财联社电报", "新浪财经", "外媒简讯", "外媒"}) {
isRed := false
if slice.Contain(news.Tags, "rotating_light") {
isRed = true
}
telegraph := &models.Telegraph{
Title: news.Title,
Content: news.Message,
DataTime: &dataTime,
IsRed: isRed,
Time: dataTime.Format("15:04:05"),
Source: GetSource(news.Tags),
SentimentResult: data.AnalyzeSentiment(news.Message).Description,
}
cnt := int64(0)
if telegraph.Title == "" {
db.Dao.Model(telegraph).Where("content=?", telegraph.Content).Count(&cnt)
} else {
db.Dao.Model(telegraph).Where("title=?", telegraph.Title).Count(&cnt)
}
if cnt == 0 {
db.Dao.Model(telegraph).Create(&telegraph)
//计算时间差如果<5分钟则推送
if time.Now().Sub(dataTime) < 5*time.Minute {
a.NewsPush(&[]models.Telegraph{*telegraph})
}
tags := slice.Filter(news.Tags, func(index int, item string) bool {
return !(item == "rotating_light" || item == "loudspeaker")
})
for _, subject := range tags {
tag := &models.Tags{
Name: subject,
Type: "subject",
}
db.Dao.Model(tag).Where("name=? and type=?", subject, "subject").FirstOrCreate(&tag)
db.Dao.Model(models.TelegraphTags{}).Where("telegraph_id=? and tag_id=?", telegraph.ID, tag.ID).FirstOrCreate(&models.TelegraphTags{
TelegraphId: telegraph.ID,
TagId: tag.ID,
})
}
}
}
}
}
func GetSource(tags []string) string {
if slice.ContainAny(tags, []string{"外媒简讯", "外媒资讯", "外媒"}) {
return "外媒"
}
if slices.Contains(tags, "财联社电报") {
return "财联社电报"
}
if slices.Contains(tags, "新浪财经") {
return "新浪财经"
}
return ""
}
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
defer PanicHandler()
@@ -418,6 +656,10 @@ func (a *App) domReady(ctx context.Context) {
if interval <= 0 {
interval = 1
}
a.cron.AddFunc(fmt.Sprintf("@every %ds", interval+60), func() {
data.NewsAnalyze("", true)
})
//ticker := time.NewTicker(time.Second * time.Duration(interval))
//defer ticker.Stop()
//for range ticker.C {
@@ -686,7 +928,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, follow.AiConfigId)
msgs := ai.NewChatStream(follow.Name, follow.StockCode, "", nil, a.AiTools)
msgs := ai.NewChatStream(follow.Name, follow.StockCode, "", nil, a.AiTools, true)
var res strings.Builder
chatId := ""
@@ -1051,12 +1293,12 @@ func (a *App) SendDingDingMessageByType(message string, stockCode string, msgTyp
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) NewChatStream(stock, stockCode, question string, aiConfigId int, sysPromptId *int, enableTools bool) {
func (a *App) NewChatStream(stock, stockCode, question string, aiConfigId int, sysPromptId *int, enableTools bool, think bool) {
var msgs <-chan map[string]any
if enableTools {
msgs = data.NewDeepSeekOpenAi(a.ctx, aiConfigId).NewChatStream(stock, stockCode, question, sysPromptId, a.AiTools)
msgs = data.NewDeepSeekOpenAi(a.ctx, aiConfigId).NewChatStream(stock, stockCode, question, sysPromptId, a.AiTools, think)
} else {
msgs = data.NewDeepSeekOpenAi(a.ctx, aiConfigId).NewChatStream(stock, stockCode, question, sysPromptId, []data.Tool{})
msgs = data.NewDeepSeekOpenAi(a.ctx, aiConfigId).NewChatStream(stock, stockCode, question, sysPromptId, []data.Tool{}, think)
}
for msg := range msgs {
runtime.EventsEmit(a.ctx, "newChatStream", msg)
@@ -1386,12 +1628,12 @@ func (a *App) GlobalStockIndexes() map[string]any {
return data.NewMarketNewsApi().GlobalStockIndexes(30)
}
func (a *App) SummaryStockNews(question string, aiConfigId int, sysPromptId *int, enableTools bool) {
func (a *App) SummaryStockNews(question string, aiConfigId int, sysPromptId *int, enableTools bool, think bool) {
var msgs <-chan map[string]any
if enableTools {
msgs = data.NewDeepSeekOpenAi(a.ctx, aiConfigId).NewSummaryStockNewsStreamWithTools(question, sysPromptId, a.AiTools)
msgs = data.NewDeepSeekOpenAi(a.ctx, aiConfigId).NewSummaryStockNewsStreamWithTools(question, sysPromptId, a.AiTools, think)
} else {
msgs = data.NewDeepSeekOpenAi(a.ctx, aiConfigId).NewSummaryStockNewsStream(question, sysPromptId)
msgs = data.NewDeepSeekOpenAi(a.ctx, aiConfigId).NewSummaryStockNewsStream(question, sysPromptId, think)
}
for msg := range msgs {

View File

@@ -4,7 +4,6 @@ import (
"go-stock/backend/agent"
"go-stock/backend/data"
"go-stock/backend/models"
"strings"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
@@ -32,7 +31,7 @@ func (a *App) EMDictCode(code string) []any {
return data.NewMarketNewsApi().EMDictCode(code, a.cache)
}
func (a *App) AnalyzeSentiment(text string) data.SentimentResult {
func (a *App) AnalyzeSentiment(text string) models.SentimentResult {
return data.AnalyzeSentiment(text)
}
@@ -75,19 +74,39 @@ func (a *App) ChatWithAgent(question string, aiConfigId int, sysPromptId *int) {
}
func (a *App) AnalyzeSentimentWithFreqWeight(text string) map[string]any {
if text == "" {
telegraphs := data.NewMarketNewsApi().GetNews24HoursList("", 1000*10)
messageText := strings.Builder{}
for _, telegraph := range *telegraphs {
messageText.WriteString(telegraph.Content + "\n")
}
text = messageText.String()
}
result, frequencies := data.AnalyzeSentimentWithFreqWeight(text)
// 过滤标点符号和分隔符
cleanFrequencies := data.FilterAndSortWords(frequencies)
result, cleanFrequencies := data.NewsAnalyze(text, false)
return map[string]any{
"result": result,
"frequencies": cleanFrequencies,
}
}
func (a *App) GetAIResponseResultList(query models.AIResponseResultQuery) *models.AIResponseResultPageData {
page, err := data.NewAIResponseResultService().GetAIResponseResultList(query)
if err != nil {
return &models.AIResponseResultPageData{}
}
return page
}
func (a *App) DeleteAIResponseResult(id string) string {
err := data.NewAIResponseResultService().DeleteAIResponseResult(id)
if err != nil {
return "删除失败"
}
return "删除成功"
}
func (a *App) BatchDeleteAIResponseResult(ids []uint) string {
err := data.NewAIResponseResultService().BatchDeleteAIResponseResult(ids)
if err != nil {
return "删除失败"
}
return "删除成功"
}
func (a *App) GetAiRecommendStocksList(query models.AiRecommendStocksQuery) *models.AiRecommendStocksPageData {
page, err := data.NewAiRecommendStocksService().GetAiRecommendStocksList(&query)
if err != nil {
return &models.AiRecommendStocksPageData{}
}
return page
}

View File

@@ -62,3 +62,13 @@ func TestUpdateCheck(t *testing.T) {
}
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion)
}
func TestGetScreenResolution(t *testing.T) {
x, y, w, h, err := getScreenResolution()
if err != nil {
logger.SugaredLogger.Errorf("get screen resolution error:%s", err.Error())
return
}
logger.SugaredLogger.Infof("x:%d,y:%d,w:%d,h:%d", x, y, w, h)
}

View File

@@ -6,16 +6,17 @@ package main
import (
"context"
"fmt"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"time"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/strutil"
"github.com/energye/systray"
"github.com/go-toast/toast"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"time"
)
// startup is called at application startup
@@ -209,6 +210,7 @@ func getScreenResolution() (int, int, int, int, error) {
//
//width, _, _ := getSystemMetrics.Call(0)
//height, _, _ := getSystemMetrics.Call(1)
//return int(width), int(height), 1456, 768, nil
return int(1366), int(768), 1456, 768, nil
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/flow/agent"
"github.com/cloudwego/eino/schema"
"github.com/duke-git/lancet/v2/fileutil"
)
// @Author spark
@@ -61,7 +62,7 @@ func TestGetStockAiAgent(t *testing.T) {
logger.SugaredLogger.Errorf("failed to recv: %v", err)
return
}
//logger.SugaredLogger.Infof("stream recv: %v", msg)
logger.SugaredLogger.Infof("stream recv: %v", msg)
if msg.ReasoningContent != "" {
md.WriteString(msg.ReasoningContent)
}
@@ -76,9 +77,12 @@ func TestGetStockAiAgent(t *testing.T) {
func TestAgent(t *testing.T) {
db.Init("../../data/stock.db")
ch := NewStockAiAgentApi().Chat("分析一下海立股份,使用工具", 1, nil)
md := strings.Builder{}
ch := NewStockAiAgentApi().Chat("分析一下立讯精密", 0, nil)
for message := range ch {
logger.SugaredLogger.Infof("res:%s", message.String())
md.WriteString(message.String())
}
logger.SugaredLogger.Info(md.String())
fileutil.WriteStringToFile("../../data/result.md", md.String(), false)
}

View File

@@ -0,0 +1,117 @@
// Package data ai_recommend_stocks_api.go
package data
import (
"go-stock/backend/db"
"go-stock/backend/models"
"github.com/duke-git/lancet/v2/strutil"
)
type AiRecommendStocksService struct{}
func NewAiRecommendStocksService() *AiRecommendStocksService {
return &AiRecommendStocksService{}
}
// CreateAiRecommendStocks 创建AI推荐股票记录
func (s *AiRecommendStocksService) CreateAiRecommendStocks(recommend *models.AiRecommendStocks) error {
result := db.Dao.Create(recommend)
return result.Error
}
// GetAiRecommendStocksList 分页查询AI推荐股票记录
func (s *AiRecommendStocksService) GetAiRecommendStocksList(query *models.AiRecommendStocksQuery) (*models.AiRecommendStocksPageData, error) {
var list []models.AiRecommendStocks
var total int64
q := db.Dao.Model(&models.AiRecommendStocks{})
// 构建查询条件
if query.StockCode != "" {
q.Or("stock_code LIKE ?", "%"+query.StockCode+"%")
}
if query.StockName != "" {
q.Or("stock_name LIKE ?", "%"+query.StockName+"%")
}
if query.BkCode != "" {
q.Or("bk_code LIKE ?", "%"+query.BkCode+"%")
}
if query.BkName != "" {
q.Or("bk_name LIKE ?", "%"+query.BkName+"%")
}
if query.StartDate != "" && query.EndDate != "" {
query.StartDate = strutil.ReplaceWithMap(query.StartDate, map[string]string{
"T": " ",
"Z": "",
})
query.StartDate = strutil.ReplaceWithMap(query.StartDate, map[string]string{
"T": " ",
"Z": "",
})
q = q.Where("data_time BETWEEN ? AND ?", query.StartDate, query.EndDate)
}
// 计算总数
err := q.Count(&total).Error
if err != nil {
return nil, err
}
// 设置默认分页参数
page := query.Page
pageSize := query.PageSize
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
// 执行分页查询
offset := (page - 1) * pageSize
err = q.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&list).Error
if err != nil {
return nil, err
}
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
return &models.AiRecommendStocksPageData{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}, nil
}
// GetAiRecommendStocksByID 根据ID获取AI推荐股票记录
func (s *AiRecommendStocksService) GetAiRecommendStocksByID(id uint) (*models.AiRecommendStocks, error) {
var recommend models.AiRecommendStocks
err := db.Dao.First(&recommend, id).Error
if err != nil {
return nil, err
}
return &recommend, nil
}
// UpdateAiRecommendStocks 更新AI推荐股票记录
func (s *AiRecommendStocksService) UpdateAiRecommendStocks(id uint, recommend *models.AiRecommendStocks) error {
result := db.Dao.Model(&models.AiRecommendStocks{}).Where("id = ?", id).Updates(recommend)
return result.Error
}
// DeleteAiRecommendStocks 根据ID删除AI推荐股票记录
func (s *AiRecommendStocksService) DeleteAiRecommendStocks(id uint) error {
// 使用软删除
result := db.Dao.Where("id = ?", id).Delete(&models.AiRecommendStocks{})
return result.Error
}
// BatchDeleteAiRecommendStocks 批量删除AI推荐股票记录
func (s *AiRecommendStocksService) BatchDeleteAiRecommendStocks(ids []uint) error {
// 使用软删除
result := db.Dao.Where("id IN ?", ids).Delete(&models.AiRecommendStocks{})
return result.Error
}

View File

@@ -0,0 +1,97 @@
package data
import (
"go-stock/backend/db"
"go-stock/backend/models"
"github.com/duke-git/lancet/v2/strutil"
)
type AIResponseResultService struct{}
func NewAIResponseResultService() *AIResponseResultService {
return &AIResponseResultService{}
}
// GetAIResponseResultList 分页查询AI响应结果
func (s *AIResponseResultService) GetAIResponseResultList(query models.AIResponseResultQuery) (*models.AIResponseResultPageData, error) {
var list []models.AIResponseResult
var total int64
q := db.Dao.Model(&models.AIResponseResult{})
// 构建查询条件
if query.ChatId != "" {
q.Where("chat_id LIKE ?", "%"+query.ChatId+"%")
}
if query.ModelName != "" {
q.Or("model_name LIKE ?", "%"+query.ModelName+"%")
}
if query.StockCode != "" {
q.Or("stock_code LIKE ?", "%"+query.StockCode+"%")
}
if query.Question != "" {
q.Or("question LIKE ?", "%"+query.Question+"%")
}
if query.StartDate != "" && query.EndDate != "" {
query.StartDate = strutil.ReplaceWithMap(query.StartDate, map[string]string{
"T": " ",
"Z": "",
})
query.StartDate = strutil.ReplaceWithMap(query.StartDate, map[string]string{
"T": " ",
"Z": "",
})
q = q.Where("created_at BETWEEN ? AND ?", query.StartDate, query.EndDate)
}
// 计算总数
err := q.Count(&total).Error
if err != nil {
return nil, err
}
// 设置默认分页参数
page := query.Page
pageSize := query.PageSize
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
// 执行分页查询
offset := (page - 1) * pageSize
err = q.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&list).Error
if err != nil {
return nil, err
}
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
return &models.AIResponseResultPageData{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}, nil
}
// DeleteAIResponseResult 根据ID删除AI响应结果
func (s *AIResponseResultService) DeleteAIResponseResult(id string) error {
// 使用软删除
result := db.Dao.Where("id = ?", id).Delete(&models.AIResponseResult{})
return result.Error
}
// BatchDeleteAIResponseResult 批量删除AI响应结果
func (s *AIResponseResultService) BatchDeleteAIResponseResult(ids []uint) error {
// 使用软删除
result := db.Dao.Where("id IN ?", ids).Delete(&models.AIResponseResult{})
return result.Error
}

View File

@@ -0,0 +1,26 @@
package data
import (
"go-stock/backend/db"
"go-stock/backend/models"
"testing"
)
// @Author spark
// @Date 2026/1/23 17:39
// @Desc
//-----------------------------------------------------------------------------------
func TestAIResponseResultService_GetAIResponseResultList(t *testing.T) {
db.Init("../../data/stock.db")
service := NewAIResponseResultService()
list, err := service.GetAIResponseResultList(models.AIResponseResultQuery{
Page: 1,
PageSize: 10,
})
if err != nil {
return
}
t.Log(list)
}

View File

@@ -55,7 +55,6 @@ func (m MarketNewsApi) TelegraphList(crawlTimeOut int64) *[]models.Telegraph {
news := v.(map[string]any)
ctime, _ := convertor.ToInt(news["ctime"])
dataTime := time.Unix(ctime, 0).Local()
logger.SugaredLogger.Debugf("dataTime: %s", dataTime)
telegraph := models.Telegraph{
Title: news["title"].(string),
Content: news["content"].(string),
@@ -67,7 +66,11 @@ func (m MarketNewsApi) TelegraphList(crawlTimeOut int64) *[]models.Telegraph {
SentimentResult: AnalyzeSentiment(news["content"].(string)).Description,
}
cnt := int64(0)
db.Dao.Model(telegraph).Where("time=? and content=?", telegraph.Time, telegraph.Content).Count(&cnt)
if telegraph.Title == "" {
db.Dao.Model(telegraph).Where("content=?", telegraph.Content).Count(&cnt)
} else {
db.Dao.Model(telegraph).Where("title=?", telegraph.Title).Count(&cnt)
}
if cnt > 0 {
continue
}
@@ -225,6 +228,29 @@ func (m MarketNewsApi) GetTelegraphList(source string) *[]*models.Telegraph {
}
return news
}
func (m MarketNewsApi) GetTelegraphListWithPaging(source string, page, pageSize int) *[]*models.Telegraph {
// 计算偏移量
offset := (page - 1) * pageSize
news := &[]*models.Telegraph{}
if source != "" {
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("data_time desc,time desc").Limit(pageSize).Offset(offset).Find(news)
} else {
db.Dao.Model(news).Preload("TelegraphTags").Order("data_time desc,time desc").Limit(pageSize).Offset(offset).Find(news)
}
for _, item := range *news {
tags := &[]models.Tags{}
db.Dao.Model(&models.Tags{}).Where("id in ?", lo.Map(item.TelegraphTags, func(item models.TelegraphTags, index int) uint {
return item.TagId
})).Find(&tags)
tagNames := lo.Map(*tags, func(item models.Tags, index int) string {
return item.Name
})
item.SubjectTags = tagNames
logger.SugaredLogger.Infof("tagNames %v SubjectTags%s", tagNames, item.SubjectTags)
}
return news
}
func (m MarketNewsApi) GetSinaNews(crawlTimeOut uint) *[]models.Telegraph {
news := &[]models.Telegraph{}
@@ -290,7 +316,11 @@ func (m MarketNewsApi) GetSinaNews(crawlTimeOut uint) *[]models.Telegraph {
if telegraph.Content != "" {
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
cnt := int64(0)
db.Dao.Model(telegraph).Where("content=? and source=?", telegraph.Content, telegraph.Source).Count(&cnt)
if telegraph.Title == "" {
db.Dao.Model(telegraph).Where("content=?", telegraph.Content).Count(&cnt)
} else {
db.Dao.Model(telegraph).Where("title=?", telegraph.Title).Count(&cnt)
}
if cnt == 0 {
db.Dao.Create(&telegraph)
telegraphs = append(telegraphs, telegraph)
@@ -703,7 +733,11 @@ func (m MarketNewsApi) TradingViewNews() *[]models.Telegraph {
SentimentResult: sentimentResult,
}
cnt := int64(0)
db.Dao.Model(telegraph).Where("time=? and title=? and source=?", telegraph.Time, telegraph.Title, "外媒").Count(&cnt)
if telegraph.Title == "" {
db.Dao.Model(telegraph).Where("content=?", telegraph.Content).Count(&cnt)
} else {
db.Dao.Model(telegraph).Where("title=?", telegraph.Title).Count(&cnt)
}
if cnt > 0 {
continue
}

View File

@@ -92,12 +92,13 @@ func TestStockResearchReport(t *testing.T) {
func TestIndustryResearchReport(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().IndustryResearchReport("456", 7)
resp := NewMarketNewsApi().IndustryResearchReport("", 7)
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
data := a.(map[string]any)
logger.SugaredLogger.Debugf("value: %s infoCode:%s", data["title"], data["infoCode"])
NewMarketNewsApi().GetIndustryReportInfo(data["infoCode"].(string))
logger.SugaredLogger.Debugf("url: https://pdf.dfcfw.com/pdf/H3_%s_1.pdf", data["infoCode"])
//NewMarketNewsApi().GetIndustryReportInfo(data["infoCode"].(string))
}
}
@@ -141,6 +142,8 @@ func TestXUEQIUHotStock(t *testing.T) {
logger.SugaredLogger.Debugf("value: %+v", a)
}
md := util.MarkdownTableWithTitle("当前热门股票排名", res)
logger.SugaredLogger.Debugf(md)
}
func TestHotEvent(t *testing.T) {
@@ -234,7 +237,7 @@ func TestReutersNew(t *testing.T) {
func TestInteractiveAnswer(t *testing.T) {
db.Init("../../data/stock.db")
datas := NewMarketNewsApi().InteractiveAnswer(1, 100, "")
datas := NewMarketNewsApi().InteractiveAnswer(1, 100, "立讯精密")
logger.SugaredLogger.Debugf("PageSize:%d", datas.PageSize)
md := util.MarkdownTableWithTitle("投资互动", datas.Results)
logger.SugaredLogger.Debugf(md)

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@ func TestNewDeepSeekOpenAiConfig(t *testing.T) {
Function: ToolFunction{
Name: "SearchStockByIndicators",
Description: "根据自然语言筛选股票,返回自然语言选股条件要求的股票所有相关数据",
Parameters: FunctionParameters{
Parameters: &FunctionParameters{
Type: "object",
Properties: map[string]any{
"words": map[string]any{
@@ -30,9 +30,9 @@ func TestNewDeepSeekOpenAiConfig(t *testing.T) {
},
})
ai := NewDeepSeekOpenAi(context.TODO(), 0)
ai := NewDeepSeekOpenAi(context.TODO(), 11)
//res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
res := ai.NewSummaryStockNewsStreamWithTools("总结市场资讯,发掘潜力标的/行业/板块/概念,控制风险。调用工具函数验证", nil, tools)
res := ai.NewSummaryStockNewsStreamWithTools("总结市场资讯,发掘潜力标的/行业/板块/概念,控制风险。调用工具函数验证", nil, tools, false)
for {
select {
@@ -65,6 +65,6 @@ func TestSearchGuShiTongStockInfo(t *testing.T) {
func TestGetZSInfo(t *testing.T) {
db.Init("../../data/stock.db")
GetZSInfo("中证银行", "sz399986", 30)
GetZSInfo("上海贝岭", "sh600171", 30)
GetZSInfo("中证银行", "sz399986", 5)
GetZSInfo("上海贝岭", "sh600171", 5)
}

View File

@@ -4,8 +4,11 @@ import (
"encoding/json"
"fmt"
"go-stock/backend/logger"
"go-stock/backend/models"
"go-stock/backend/util"
"time"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/go-resty/resty/v2"
)
@@ -81,3 +84,35 @@ func (s SearchStockApi) HotStrategy() map[string]any {
json.Unmarshal(resp.Body(), &respMap)
return respMap
}
func (s SearchStockApi) HotStrategyTable() string {
markdownTable := ""
res := s.HotStrategy()
bytes, _ := json.Marshal(res)
strategy := &models.HotStrategy{}
json.Unmarshal(bytes, strategy)
for _, data := range strategy.Data {
data.Chg = mathutil.RoundToFloat(100*data.Chg, 2)
}
markdownTable = util.MarkdownTableWithTitle("当前热门选股策略", strategy.Data)
return markdownTable
}
func (s SearchStockApi) StrategySquare() map[string]any {
//https://backtest.10jqka.com.cn/strategysquare/list?order=desc&page=1&pageNum=10&sortType=hot&keyword=
url := "https://backtest.10jqka.com.cn/strategysquare/list?order=desc&page=1&pageNum=10&sortType=hot&keyword="
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "backtest.10jqka.com.cn").
SetHeader("Origin", "https://backtest.10jqka.com.cn").
SetHeader("Referer", "https://backtest.10jqka.com.cn/strategysquare/list").
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("StrategySquare-err:%+v", err)
return map[string]any{}
}
respMap := map[string]any{}
json.Unmarshal(resp.Body(), &respMap)
logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return respMap
}

View File

@@ -4,10 +4,13 @@ import (
"encoding/json"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"go-stock/backend/util"
"math"
"testing"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/duke-git/lancet/v2/random"
)
@@ -60,10 +63,25 @@ 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)
bytes, err := json.Marshal(res)
if err != nil {
return
}
strategy := &models.HotStrategy{}
json.Unmarshal(bytes, strategy)
for _, data := range strategy.Data {
data.Chg = mathutil.RoundToFloat(100*data.Chg, 2)
}
markdownTable := util.MarkdownTable(strategy.Data)
logger.SugaredLogger.Infof("res:%s", markdownTable)
//dataList := res["data"].([]any)
//for _, v := range dataList {
// d := v.(map[string]any)
// logger.SugaredLogger.Infof("v:%+v", d)
//}
}
func TestSearchStockApi_HotStrategyTable(t *testing.T) {
db.Init("../../data/stock.db")
res := NewSearchStockApi("").StrategySquare()
logger.SugaredLogger.Infof("res:%+v", res)
}

View File

@@ -45,16 +45,18 @@ func (receiver Settings) TableName() string {
}
type AIConfig struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `json:"name"`
BaseUrl string `json:"baseUrl"`
ApiKey string `json:"apiKey" `
ModelName string `json:"modelName"`
MaxTokens int `json:"maxTokens"`
Temperature float64 `json:"temperature"`
TimeOut int `json:"timeOut"`
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `json:"name"`
BaseUrl string `json:"baseUrl"`
ApiKey string `json:"apiKey" `
ModelName string `json:"modelName"`
MaxTokens int `json:"maxTokens"`
Temperature float64 `json:"temperature"`
TimeOut int `json:"timeOut"`
HttpProxy string `json:"httpProxy"`
HttpProxyEnabled bool `json:"httpProxyEnabled"`
}
func (AIConfig) TableName() string {
@@ -163,13 +165,15 @@ func updateAiConfigs(aiConfigs []*AIConfig) error {
} else {
notDeleteIds = append(notDeleteIds, item.ID)
e = db.Dao.Model(&AIConfig{}).Where("id=?", item.ID).Updates(map[string]interface{}{
"name": item.Name,
"base_url": item.BaseUrl,
"api_key": item.ApiKey,
"model_name": item.ModelName,
"max_tokens": item.MaxTokens,
"temperature": item.Temperature,
"time_out": item.TimeOut,
"name": item.Name,
"base_url": item.BaseUrl,
"api_key": item.ApiKey,
"model_name": item.ModelName,
"max_tokens": item.MaxTokens,
"temperature": item.Temperature,
"time_out": item.TimeOut,
"http_proxy": item.HttpProxy,
"http_proxy_enabled": item.HttpProxyEnabled,
}).Error
if e != nil {
return
@@ -198,12 +202,6 @@ func GetSettingConfig() *SettingConfig {
aiConfigs := make([]*AIConfig, 0)
// 处理数据库查询可能返回的空结果
result := db.Dao.Model(&Settings{}).First(settings)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// 初始化默认设置并保存到数据库
settings = &Settings{OpenAiEnable: false, CrawlTimeOut: 60}
db.Dao.Create(settings)
}
if settings.OpenAiEnable {
// 处理AI配置查询可能出现的错误
result = db.Dao.Model(&AIConfig{}).Find(&aiConfigs)
@@ -220,7 +218,7 @@ func GetSettingConfig() *SettingConfig {
settings.CrawlTimeOut = 60
}
if settings.KDays < 30 {
settings.KDays = 120
settings.KDays = 60
}
}
if settings.BrowserPath == "" {

View File

@@ -14,6 +14,7 @@ import (
"go-stock/backend/models"
"io"
"io/ioutil"
url2 "net/url"
"strings"
"time"
@@ -1203,7 +1204,7 @@ func GetZSInfo(name, stockCode string, crawlTimeOut int64) string {
price := strutil.RemoveWhiteSpace(document.Find("div#price").First().Text(), false)
hqTime := strutil.RemoveWhiteSpace(document.Find("div#hqTime").First().Text(), false)
if strutil.ContainsAny(price, []string{"-", "--", ""}) {
if strutil.ContainsAny(price, []string{"-", "--"}) {
return "暂无数据"
}
@@ -1750,6 +1751,65 @@ func (receiver StockDataApi) GetStockHistoryMoneyData() {
}
// GetStockMoneyData 获取个股资金流数据
func (receiver StockDataApi) GetStockMoneyData() models.StockMoneyDataResp {
var resData models.StockMoneyDataResp
url := "https://push2.eastmoney.com/api/qt/clist/get?cb=data&fid=f62&po=1&pz=50&pn=1&np=1&fltt=2&invt=2&ut=8dec03ba335b81bf4ebdf7b29ec27d15&fs=m:0+t:6+f:!2,m:0+t:13+f:!2,m:0+t:80+f:!2,m:1+t:2+f:!2,m:1+t:23+f:!2,m:0+t:7+f:!2,m:1+t:3+f:!2&fields=f12,f14,f2,f3,f62,f184,f66,f69,f72,f75,f78,f81,f84,f87,f204,f205,f124,f1,f13,f100,f265"
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "push2.eastmoney.com").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
}
body := string(resp.Body())
logger.SugaredLogger.Infof("resp:%s", body)
vm := otto.New()
vm.Run("function data(res){return res};")
val, err := vm.Run(body)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
}
value, err := val.Export()
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
}
marshal, err := json.Marshal(value)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return models.StockMoneyDataResp{}
}
err = json.Unmarshal(marshal, &resData)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return models.StockMoneyDataResp{}
}
return resData
}
// 获取股票概念题材信息
func (receiver StockDataApi) GetStockConceptInfo(stockCode string) models.StockConceptInfoResp {
//601138.SH
url := "https://datacenter.eastmoney.com/securities/api/data/v1/get?reportName=RPT_F10_CORETHEME_BOARDTYPE&columns=SECUCODE%2CSECURITY_CODE%2CSECURITY_NAME_ABBR%2CNEW_BOARD_CODE%2CBOARD_NAME%2CSELECTED_BOARD_REASON%2CIS_PRECISE%2CBOARD_RANK%2CBOARD_YIELD%2CDERIVE_BOARD_CODE&quoteColumns=f3~05~NEW_BOARD_CODE~BOARD_YIELD&filter=(SECUCODE%3D%22" + stockCode + "%22)(IS_PRECISE%3D%221%22)&pageNumber=1&pageSize=&sortTypes=1&sortColumns=BOARD_RANK&source=HSF10&client=PC&v=005634233622011753"
logger.SugaredLogger.Infof("url:%s", url2.QueryEscape(url))
var data models.StockConceptInfoResp
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "datacenter.eastmoney.com").
SetHeader("Referer", "https://emweb.securities.eastmoney.com/").
SetHeader("Origin", "https://emweb.securities.eastmoney.com").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
}
err = json.Unmarshal(resp.Body(), &data)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return models.StockConceptInfoResp{}
}
return data
}
// JSONToMarkdownTable 将JSON数据转换为Markdown表格
func JSONToMarkdownTable(jsonData []byte) (string, error) {
var data []map[string]interface{}

View File

@@ -283,3 +283,17 @@ func TestName(t *testing.T) {
//}
}
func TestGetStockMoneyData(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
res := stockDataApi.GetStockMoneyData()
logger.SugaredLogger.Infof("%s", util.MarkdownTableWithTitle("今日个股资金流向Top50", res.Data.Diff))
}
func TestGetStockConceptInfo(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
res := stockDataApi.GetStockConceptInfo("601138.SH")
logger.SugaredLogger.Infof("%s", util.MarkdownTableWithTitle("601138.SH所属概念/板块信息", res.Result.Data))
}

View File

@@ -199,14 +199,6 @@ func InitAnalyzeSentiment() {
seg.CalcToken()
}
// WordFreqWithWeight 词频统计结果,包含权重信息
type WordFreqWithWeight struct {
Word string
Frequency int
Weight float64
Score float64
}
// getWordWeight 获取词汇权重
func getWordWeight(word string) float64 {
// 从分词器获取词汇权重
@@ -220,9 +212,9 @@ func getWordWeight(word string) float64 {
}
// SortByWeightAndFrequency 按权重和频次排序词频结果
func SortByWeightAndFrequency(frequencies map[string]WordFreqWithWeight) []WordFreqWithWeight {
func SortByWeightAndFrequency(frequencies map[string]models.WordFreqWithWeight) []models.WordFreqWithWeight {
// 将map转换为slice以便排序
freqSlice := make([]WordFreqWithWeight, 0, len(frequencies))
freqSlice := make([]models.WordFreqWithWeight, 0, len(frequencies))
for _, freq := range frequencies {
freqSlice = append(freqSlice, freq)
}
@@ -237,7 +229,7 @@ func SortByWeightAndFrequency(frequencies map[string]WordFreqWithWeight) []WordF
}
// FilterAndSortWords 过滤标点符号并按权重频次排序
func FilterAndSortWords(frequencies map[string]WordFreqWithWeight) []WordFreqWithWeight {
func FilterAndSortWords(frequencies map[string]models.WordFreqWithWeight) []models.WordFreqWithWeight {
// 先过滤标点符号和分隔符
cleanFrequencies := FilterPunctuationAndSeparators(frequencies)
@@ -246,8 +238,8 @@ func FilterAndSortWords(frequencies map[string]WordFreqWithWeight) []WordFreqWit
return sortedFrequencies
}
func FilterPunctuationAndSeparators(frequencies map[string]WordFreqWithWeight) map[string]WordFreqWithWeight {
filteredWords := make(map[string]WordFreqWithWeight)
func FilterPunctuationAndSeparators(frequencies map[string]models.WordFreqWithWeight) map[string]models.WordFreqWithWeight {
filteredWords := make(map[string]models.WordFreqWithWeight)
for word, freqInfo := range frequencies {
// 过滤纯标点符号和分隔符
@@ -275,8 +267,8 @@ func isPunctuationOrSeparator(word string) bool {
}
// FilterWithRegex 使用正则表达式过滤标点和特殊字符
func FilterWithRegex(frequencies map[string]WordFreqWithWeight) map[string]WordFreqWithWeight {
filteredWords := make(map[string]WordFreqWithWeight)
func FilterWithRegex(frequencies map[string]models.WordFreqWithWeight) map[string]models.WordFreqWithWeight {
filteredWords := make(map[string]models.WordFreqWithWeight)
// 匹配标点符号、特殊字符的正则表达式
punctuationRegex := regexp.MustCompile(`^[[:punct:][:space:]]+$`)
@@ -291,9 +283,9 @@ func FilterWithRegex(frequencies map[string]WordFreqWithWeight) map[string]WordF
}
// countWordFrequencyWithWeight 统计词频并包含权重信息
func countWordFrequencyWithWeight(text string) map[string]WordFreqWithWeight {
func countWordFrequencyWithWeight(text string) map[string]models.WordFreqWithWeight {
words := splitWords(text)
freqMap := make(map[string]WordFreqWithWeight)
freqMap := make(map[string]models.WordFreqWithWeight)
// 统计词频
wordCount := make(map[string]int)
@@ -305,7 +297,7 @@ func countWordFrequencyWithWeight(text string) map[string]WordFreqWithWeight {
for word, frequency := range wordCount {
weight := getWordWeight(word)
if weight >= basefreq {
freqMap[word] = WordFreqWithWeight{
freqMap[word] = models.WordFreqWithWeight{
Word: word,
Frequency: frequency,
Weight: weight,
@@ -319,7 +311,7 @@ func countWordFrequencyWithWeight(text string) map[string]WordFreqWithWeight {
}
// AnalyzeSentimentWithFreqWeight 带权重词频统计的情感分析
func AnalyzeSentimentWithFreqWeight(text string) (SentimentResult, map[string]WordFreqWithWeight) {
func AnalyzeSentimentWithFreqWeight(text string) (models.SentimentResult, map[string]models.WordFreqWithWeight) {
// 原有情感分析逻辑
result := AnalyzeSentiment(text)
@@ -329,26 +321,14 @@ func AnalyzeSentimentWithFreqWeight(text string) (SentimentResult, map[string]Wo
return result, frequencies
}
// SentimentResult 情感分析结果类型
type SentimentResult struct {
Score float64 // 情感得分
Category SentimentType // 情感类别
PositiveCount int // 正面词数量
NegativeCount int // 负面词数量
Description string // 情感描述
}
// SentimentType 情感类型枚举
type SentimentType int
const (
Positive SentimentType = iota
Positive models.SentimentType = iota
Negative
Neutral
)
// AnalyzeSentiment 判断文本的情感
func AnalyzeSentiment(text string) SentimentResult {
func AnalyzeSentiment(text string) models.SentimentResult {
// 初始化得分
score := 0.0
positiveCount := 0
@@ -388,7 +368,7 @@ func AnalyzeSentiment(text string) SentimentResult {
}
// 确定情感类别
var category SentimentType
var category models.SentimentType
switch {
case score > 1.0:
category = Positive
@@ -398,7 +378,7 @@ func AnalyzeSentiment(text string) SentimentResult {
category = Neutral
}
return SentimentResult{
return models.SentimentResult{
Score: score,
Category: category,
PositiveCount: positiveCount,
@@ -511,7 +491,7 @@ func splitWords(text string) []string {
}
// GetSentimentDescription 获取情感类别的文本描述
func GetSentimentDescription(category SentimentType) string {
func GetSentimentDescription(category models.SentimentType) string {
switch category {
case Positive:
return "看涨"
@@ -556,3 +536,43 @@ func main() {
result.NegativeCount)
}
}
func SaveAnalyzeSentimentWithFreqWeight(frequencies []models.WordFreqWithWeight) {
sort.Slice(frequencies, func(i, j int) bool {
return frequencies[i].Frequency > frequencies[j].Frequency
})
wordAnalyzes := make([]models.WordAnalyze, 0)
for _, freq := range frequencies[:10] {
wordAnalyze := models.WordAnalyze{
WordFreqWithWeight: freq,
}
wordAnalyzes = append(wordAnalyzes, wordAnalyze)
}
db.Dao.CreateInBatches(wordAnalyzes, 1000)
}
func SaveStockSentimentAnalysis(result models.SentimentResult) {
db.Dao.Create(&models.SentimentResultAnalyze{
SentimentResult: result,
})
}
func NewsAnalyze(text string, save bool) (models.SentimentResult, []models.WordFreqWithWeight) {
if text == "" {
telegraphs := NewMarketNewsApi().GetNews24HoursList("", 1000*10)
messageText := strings.Builder{}
for _, telegraph := range *telegraphs {
messageText.WriteString(telegraph.Content + "\n")
}
text = messageText.String()
}
result, frequencies := AnalyzeSentimentWithFreqWeight(text)
// 过滤标点符号和分隔符
cleanFrequencies := FilterAndSortWords(frequencies)
if save {
go SaveAnalyzeSentimentWithFreqWeight(cleanFrequencies)
go SaveStockSentimentAnalysis(result)
}
return result, cleanFrequencies
}

View File

@@ -149,6 +149,34 @@ func (receiver AIResponseResult) TableName() string {
return "ai_response_result"
}
// AIResponseResultQuery 分页查询参数
type AIResponseResultQuery struct {
Page int `form:"page" json:"page"` // 页码
PageSize int `form:"pageSize" json:"pageSize"` // 每页大小
ChatId string `form:"chatId" json:"chatId"` // 聊天ID筛选
ModelName string `form:"modelName" json:"modelName"` // 模型名称筛选
StockCode string `form:"stockCode" json:"stockCode"` // 股票代码筛选
StockName string `form:"stockName" json:"stockName"` // 股票名称筛选
Question string `form:"question" json:"question"` // 问题内容模糊搜索
StartDate string `form:"startDate" json:"startDate"` // 开始日期
EndDate string `form:"endDate" json:"endDate"` // 结束日期
}
// AIResponseResultPageResp 分页查询响应
type AIResponseResultPageResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data AIResponseResultPageData `json:"data"`
}
type AIResponseResultPageData struct {
List []AIResponseResult `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
TotalPages int `json:"totalPages"`
}
type VersionInfo struct {
gorm.Model
Version string `json:"version"`
@@ -359,29 +387,29 @@ type XUEQIUHot struct {
}
type HotItem struct {
Type int `json:"type"`
Code string `json:"code"`
Name string `json:"name"`
Value float64 `json:"value"`
Increment int `json:"increment"`
RankChange int `json:"rank_change"`
HasExist interface{} `json:"has_exist"`
Symbol string `json:"symbol"`
Percent float64 `json:"percent"`
Current float64 `json:"current"`
Chg float64 `json:"chg"`
Exchange string `json:"exchange"`
StockType int `json:"stock_type"`
SubType string `json:"sub_type"`
Ad int `json:"ad"`
AdId interface{} `json:"ad_id"`
ContentId interface{} `json:"content_id"`
Page interface{} `json:"page"`
Model interface{} `json:"model"`
Location interface{} `json:"location"`
TradeSession interface{} `json:"trade_session"`
CurrentExt interface{} `json:"current_ext"`
PercentExt interface{} `json:"percent_ext"`
Type int `json:"type" md:"-"`
Code string `json:"code" md:"股票代码"`
Name string `json:"name" md:"股票名称"`
Value float64 `json:"value" md:"热度"`
Increment int `json:"increment" md:"热度变化"`
RankChange int `json:"rank_change" md:"排名变化"`
HasExist interface{} `json:"has_exist" md:"-"`
Symbol string `json:"symbol" md:"-"`
Percent float64 `json:"percent" md:"涨跌幅(%)"`
Current float64 `json:"current" md:"股价"`
Chg float64 `json:"chg" md:"股价变化"`
Exchange string `json:"exchange" md:"交易所代码"`
StockType int `json:"stock_type" md:"-"`
SubType string `json:"sub_type" md:"-"`
Ad int `json:"ad" md:"-"`
AdId interface{} `json:"ad_id" md:"-"`
ContentId interface{} `json:"content_id" md:"-"`
Page interface{} `json:"page" md:"-"`
Model interface{} `json:"model" md:"-"`
Location interface{} `json:"location" md:"-"`
TradeSession interface{} `json:"trade_session" md:"-"`
CurrentExt interface{} `json:"current_ext" md:"-"`
PercentExt interface{} `json:"percent_ext" md:"-"`
}
type HotEvent struct {
@@ -719,3 +747,206 @@ type BKDict struct {
func (b BKDict) TableName() string {
return "bk_dict"
}
type WordAnalyze struct {
gorm.Model
DataTime *time.Time `json:"dataTime" gorm:"index;autoCreateTime"`
WordFreqWithWeight
}
// WordFreqWithWeight 词频统计结果,包含权重信息
type WordFreqWithWeight struct {
Word string
Frequency int
Weight float64
Score float64
}
// SentimentResult 情感分析结果类型
type SentimentResult struct {
Score float64 // 情感得分
Category SentimentType // 情感类别
PositiveCount int // 正面词数量
NegativeCount int // 负面词数量
Description string // 情感描述
}
type SentimentResultAnalyze struct {
gorm.Model
DataTime *time.Time `json:"dataTime" gorm:"index;autoCreateTime"`
SentimentResult
}
// SentimentType 情感类型枚举
type SentimentType int
type HotStrategy struct {
ChgEffect bool `json:"chgEffect"`
Code int `json:"code"`
Data []*HotStrategyData `json:"data"`
Message string `json:"message"`
}
type HotStrategyData struct {
Chg float64 `json:"chg" md:"平均涨幅(%)"`
Code string `json:"code" md:"-"`
HeatValue int `json:"heatValue" md:"热度值"`
Market string `json:"market" md:"-"`
Question string `json:"question" md:"选股策略"`
Rank int `json:"rank" md:"-"`
}
type NtfyNews struct {
Id string `json:"id"`
Time int `json:"time"`
Expires int `json:"expires"`
Event string `json:"event"`
Topic string `json:"topic"`
Title string `json:"title"`
Message string `json:"message"`
Tags []string `json:"tags"`
Icon string `json:"icon"`
}
type THSHotStrategy struct {
Result struct {
Num int `json:"num"`
List []struct {
Author struct {
Avatar string `json:"avatar"`
UserName string `json:"userName"`
UserId int `json:"userId"`
} `json:"author"`
Property struct {
Id int `json:"id"`
Name string `json:"name"`
Query string `json:"query"`
Logic string `json:"logic"`
BuyPosition interface{} `json:"buyPosition"`
Ctime string `json:"ctime"`
Tags []string `json:"tags"`
WinRate string `json:"winRate"`
AnnualYield string `json:"annualYield"`
Type int `json:"type"`
} `json:"property"`
Interaction struct {
CommentNum int `json:"commentNum"`
CollectNum int `json:"collectNum"`
IsCollected bool `json:"isCollected"`
IsSubscribe int `json:"isSubscribe"`
IsPublish int `json:"isPublish"`
Pid int `json:"pid"`
} `json:"interaction"`
} `json:"list"`
} `json:"result"`
}
type StockMoneyDataResp struct {
Rc int `json:"rc"`
Rt int `json:"rt"`
Svr int `json:"svr"`
Lt int `json:"lt"`
Full int `json:"full"`
Dlmkts string `json:"dlmkts"`
Data StockMoneyData `json:"data"`
}
type StockMoneyData struct {
Total int `json:"total"`
Diff []StockMoneyDataDiff `json:"diff"`
}
type StockMoneyDataDiff struct {
F1 int `json:"f1" md:"-"`
F12 string `json:"f12" md:"股票代码"`
F13 int `json:"f13" md:"-"`
F14 string `json:"f14" md:"股票名称"`
F2 float64 `json:"f2" md:"最新价"`
F3 float64 `json:"f3" md:"今日涨跌幅(%)"`
F62 float64 `json:"f62" md:"今日主力净额(元)"`
F184 float64 `json:"f184" md:"今日主力净占比(%)"`
F66 float64 `json:"f66" md:"今日超大单净额(元)"`
F69 float64 `json:"f69" md:"今日超大单净占比(%)"`
F72 float64 `json:"f72" md:"今日大单净额(元)"`
F75 float64 `json:"f75" md:"今日大单净占比(%)"`
F78 float64 `json:"f78" md:"今日中单净额(元)"`
F81 float64 `json:"f81" md:"今日中单净占比(%)"`
F84 float64 `json:"f84" md:"今日小单净额(元)"`
F87 float64 `json:"f87" md:"今日小单净占比(%)"`
F124 int `json:"f124" md:"f124"`
F100 string `json:"f100" md:"所属板块"`
F265 string `json:"f265" md:"板块代码"`
}
type StockConceptInfoResp struct {
Version string `json:"version"`
Result StockConceptInfoResult `json:"result"`
Success bool `json:"success"`
Message string `json:"message"`
Code int `json:"code"`
}
type StockConceptInfoResult struct {
Pages int `json:"pages"`
Data []StockConceptInfo `json:"data"`
Count int `json:"count"`
}
type StockConceptInfo struct {
SECUCODE string `json:"SECUCODE" md:"完整股票代码"`
SECURITYCODE string `json:"SECURITY_CODE" md:"股票代码"`
SECURITYNAMEABBR string `json:"SECURITY_NAME_ABBR" md:"股票名称"`
NEWBOARDCODE string `json:"NEW_BOARD_CODE" md:"板块/概念代码"`
BOARDNAME string `json:"BOARD_NAME" md:"板块/概念名称"`
SELECTEDBOARDREASON string `json:"SELECTED_BOARD_REASON" md:"板块/概念描述"`
ISPRECISE string `json:"IS_PRECISE" md:"-"`
BOARDRANK int `json:"BOARD_RANK" md:"-"`
BOARDYIELD float64 `json:"BOARD_YIELD" md:"板块/概念涨跌幅(%)"`
DERIVEBOARDCODE string `json:"DERIVE_BOARD_CODE" md:"-"`
}
type AiRecommendStocks struct {
gorm.Model
DataTime *time.Time `json:"dataTime" gorm:"index;autoCreateTime"`
ModelName string `json:"modelName" md:"模型名称"`
StockCode string `json:"stockCode" md:"股票代码"`
StockName string `json:"stockName" md:"股票名称"`
BkCode string `json:"bkCode" md:"行业/板块代码"`
BkName string `json:"bkName" md:"行业/板块名称"`
StockPrice string `json:"stockPrice" md:"推荐时股票价格"`
StockClosePrice string `json:"stockClosePrice" md:"推荐时股票收盘价格"`
StockPrePrice string `json:"stockPrePrice" md:"前一交易日股票价格"`
RecommendReason string `json:"recommendReason" md:"推荐理由/驱动因素/逻辑"`
RecommendBuyPrice string `json:"recommendBuyPrice" md:"ai建议买入价"`
RecommendStopProfitPrice string `json:"recommendStopProfitPrice" md:"ai建议止盈价"`
RecommendStopLossPrice string `json:"recommendStopLossPrice" md:"ai建议止损价"`
RiskRemarks string `json:"riskRemarks" md:"风险提示"`
Remarks string `json:"remarks" md:"备注"`
}
func (receiver AiRecommendStocks) TableName() string { return "ai_recommend_stocks" }
type AiRecommendStocksQuery struct {
Page int `form:"page" json:"page"` // 页码
PageSize int `form:"pageSize" json:"pageSize"` // 每页大小
StockCode string `form:"stockCode" json:"stockCode"` // 股票代码筛选
StockName string `form:"stockName" json:"stockName"` // 股票名称筛选
BkCode string `form:"bkCode" json:"bkCode"` // 板块代码筛选
BkName string `form:"bkName" json:"bkName"` // 板块名称筛选
StartDate string `form:"startDate" json:"startDate"` // 开始日期
EndDate string `form:"endDate" json:"endDate"` // 结束日期
}
type AiRecommendStocksPageResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data AiRecommendStocksPageData `json:"data"`
}
type AiRecommendStocksPageData struct {
List []AiRecommendStocks `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
TotalPages int `json:"totalPages"`
}

BIN
build/screenshot/img15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

193
data/dict/user.txt Normal file
View File

@@ -0,0 +1,193 @@
# 补充热点概念与板块Jieba/gse兼容格式
# 权重说明核心热点500-700分事件类400分负权重词汇按需求保留
# 一、负权重低优先级词汇(减少无差别匹配干扰)
公司 -0.1 n
国家 -0.1 n
国际 -0.1 n
会议 -0.1 n
市场 -0.1 n
经济 -0.1 n
技术 -0.1 n
记者 -0.1 n
时间 -0.1 n
项目 -0.1 n
问题 -0.1 n
企业 -0.1 n
财联社 -0.1 n
上涨 -0.1 v
下跌 -0.1 v
期货 -0.1 n
跌幅 -0.1 n
跌超 -0.1 adj
股票 -0.1 n
基金 -0.1 n
电讯 -0.1 n
建筑 -0.1 n
平开 -0.1 n
保险 -0.1 n
行业 -0.1 n
其他 -0.1 n
a股 -0.1 n
港股 -0.1 n
etf -0.1 n
涨幅 -0.1 n
交易所 -0.1 n
证券 -0.1 n
ai -0.1 n
# 二、核心热点概念700分最高优先级
端侧AI 700 n
AI应用 700 n
比特币 700 n
摩尔线程 700 n
摩尔线程概念 700 n
AI算力 700 n
生成式AI 700 n
量子计算 700 n
脑机接口 700 n
6G通信 700 n
人形机器人 700 n
固态电池 700 n
ChatGPT概念 700 n
Web3.0 700 n
元宇宙 700 n
数字孪生 700 n
量子通信 700 n
# 三、重点赛道板块500分高优先级
冰雪旅游 500 n
特高压 500 n
跨境电商 500 n
新能源汽车 500 n
机器人 500 n
具身智能 500 n
油气 500 n
商业航天 500 n
光伏储能 500 n
锂电材料 500 n
半导体设备 500 n
集成电路 500 n
创新药 500 n
CXO 500 n
医疗器械 500 n
数字经济 500 n
数字货币 500 n
区块链 500 n
低空经济 500 n
工业互联网 500 n
物联网 500 n
5G应用 500 n
充电桩 500 n
氢能源 500 n
核聚变 500 n
工业母机 500 n
新材料 500 n
生物制造 500 n
智能网联汽车 500 n
乡村振兴 500 n
国企改革 500 n
央企重组 500 n
跨境金融 500 n
自贸港 500 n
一带一路 500 n
绿色低碳 500 n
碳交易 500 n
数据要素 500 n
数字基建 500 n
东数西算 500 n
国产替代 500 n
信创 500 n
网络安全 500 n
算力网络 500 n
边缘计算 500 n
虚拟现实 500 n
增强现实 500 n
智能穿戴 500 n
智能家居 500 n
车联网 500 n
激光雷达 500 n
氮化镓 500 n
碳化硅 500 n
第三代半导体 500 n
EDA工具 500 n
光刻胶 500 n
芯片设计 500 n
封装测试 500 n
储能电池 500 n
钠离子电池 500 n
氢燃料电池 500 n
光伏组件 500 n
风电设备 500 n
特高压设备 500 n
电力物联网 500 n
智能电网 500 n
轨道交通 500 n
航空航天 500 n
海洋工程 500 n
高端装备 500 n
军工电子 500 n
卫星互联网 500 n
北斗导航 500 n
国产大飞机 500 n
生物医药 500 n
基因测序 500 n
疫苗 500 n
医疗美容 500 n
养老产业 500 n
教育信息化 500 n
体育产业 500 n
文化创意 500 n
旅游复苏 500 n
预制菜 500 n
白酒 500 n
食品饮料 500 n
家电下乡 500 n
房地产复苏 500 n
基建投资 500 n
新型城镇化 500 n
冷链物流 500 n
快递物流 500 n
跨境支付 500 n
金融科技 500 n
消费电子 500 n
元宇宙基建 500 n
数字藏品 500 n
NFT 500 n
绿色电力 500 n
节能降碳 500 n
抽水蓄能 500 n
生物质能 500 n
地热能 500 n
潮汐能 500 n
# 四、事件驱动型概念400分中优先级
俄乌冲突 400 n
中东局势 400 n
美联储加息 400 n
降息预期 400 n
贸易摩擦 400 n
供应链重构 400 n
能源危机 400 n
粮食安全 400 n
疫情复苏 400 n
政策利好 400 n
产业扶持 400 n
技术突破 400 n
并购重组 400 n
IPO提速 400 n
解禁潮 400 n
北向资金流入 400 n
南向资金流入 400 n
主力资金异动 400 n
行业景气度 400 n
业绩预增 400 n
商誉减值 400 n
退市风险 400 n
监管新规 400 n
税收优惠 400 n
补贴政策 400 n
基建刺激 400 n
消费刺激 400 n
新能源补贴 400 n
碳达峰政策 400 n
碳中和目标 400 n

View File

@@ -11,6 +11,7 @@ declare module 'vue' {
About: typeof import('./src/components/about.vue')['default']
AgentChat: typeof import('./src/components/agent-chat.vue')['default']
AgentChat_bk: typeof import('./src/components/agent-chat_bk.vue')['default']
AiRecommendStocksList: typeof import('./src/components/aiRecommendStocksList.vue')['default']
AnalyzeMartket: typeof import('./src/components/AnalyzeMartket.vue')['default']
ClsCalendarTimeLine: typeof import('./src/components/ClsCalendarTimeLine.vue')['default']
EmbeddedUrl: typeof import('./src/components/EmbeddedUrl.vue')['default']
@@ -27,6 +28,8 @@ declare module 'vue' {
MoneyTrend: typeof import('./src/components/moneyTrend.vue')['default']
NewsList: typeof import('./src/components/newsList.vue')['default']
RankTable: typeof import('./src/components/rankTable.vue')['default']
ResearchIndex: typeof import('./src/components/researchIndex.vue')['default']
ResearchReport: typeof import('./src/components/researchReport.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SelectStock: typeof import('./src/components/SelectStock.vue')['default']

View File

@@ -15,9 +15,9 @@ import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,NButton,dateZhCN,z
import {
AlarmOutline,
AnalyticsOutline,
BarChartSharp, Bonfire, BonfireOutline, EaselSharp,
BarChartSharp, Bonfire, BonfireOutline, DiamondOutline, EaselSharp,
ExpandOutline, Flag,
Flame, FlameSharp, InformationOutline,
Flame, FlameSharp, FlaskOutline, InformationOutline,
LogoGithub,
NewspaperOutline,
NewspaperSharp, Notifications,
@@ -30,7 +30,7 @@ import {
} from '@vicons/ionicons5'
import {AnalyzeSentiment, GetConfig, GetGroupList,GetVersionInfo} from "../wailsjs/go/main/App";
import {Dragon, Fire, FirefoxBrowser, Gripfire, Robot} from "@vicons/fa";
import {ReportSearch} from "@vicons/tabler";
import {ReportAnalytics, ReportMoney, ReportSearch} from "@vicons/tabler";
import {LocalFireDepartmentRound} from "@vicons/material";
import {BoxSearch20Regular, CommentNote20Filled} from "@vicons/fluent";
import {FireFilled, FireOutlined, NotificationFilled, StockOutlined} from "@vicons/antd";
@@ -446,6 +446,77 @@ const menuOptions = ref([
show:enableAgent.value,
icon: renderIcon(Robot),
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'research',
query: {
name:"研究中心",
},
},
onClick: () => {
activeKey.value = 'research'
setTimeout(() => {
EventsEmit("changeResearchTab", {ID: 0, name: 'AI分析报告'})
}, 100)
},
},
{default: () => '研究中心'}
),
key: 'research',
icon: renderIcon(FlaskOutline),
children:[
{
label: () =>
h(
RouterLink,
{
to: {
name: 'research',
query: {
name:"AI分析报告",
},
},
onClick: () => {
activeKey.value = 'research'
setTimeout(() => {
EventsEmit("changeResearchTab", {ID: 0, name: 'AI分析报告'})
}, 100)
},
},
{default: () => 'AI分析报告'}
),
key: 'research1',
icon: renderIcon(ReportAnalytics),
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'research',
query: {
name:"股票推荐记录",
},
},
onClick: () => {
activeKey.value = 'research'
setTimeout(() => {
EventsEmit("changeResearchTab", {ID: 1, name: '股票推荐记录'})
}, 100)
},
},
{default: () => '股票推荐记录'}
),
key: 'research2',
icon: renderIcon(DiamondOutline),
}
],
},
{
label: () =>
h(

View File

@@ -75,7 +75,6 @@ function getIndex() {
function handleChart(){
const formatUtil = echarts.format;
AnalyzeSentimentWithFreqWeight("").then((res) => {
//console.log(res)
const treemapchart = echarts.init(chartRef.value);
const gaugeChart=echarts.init(gaugeChartRef.value);
let data = res['frequencies'].map(item => ({

View File

@@ -59,7 +59,7 @@ function getMarketCode(item) {
<template #trigger>
<n-tag type="info" :bordered="false"> {{item.name}} {{item.code}}</n-tag>
</template>
<k-line-chart style="width: 800px" :code="getMarketCode(item)" :chart-height="500" :name="item.name" :k-days="20" :dark-theme="true"></k-line-chart>
<k-line-chart style="width: 800px" :code="getMarketCode(item)" :chart-height="500" :stockName="item.name" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-text></n-td>
<n-td><n-text :type="item.percent>0?'error':'success'">{{item.percent}}%</n-text></n-td>

View File

@@ -4,12 +4,12 @@ import {GetStockKLine} from "../../wailsjs/go/main/App";
import * as echarts from "echarts";
import {onMounted, ref} from "vue";
import _ from "lodash";
const { code,name,darkTheme,kDays ,chartHeight} = defineProps({
const { code,stockName,darkTheme,kDays ,chartHeight} = defineProps({
code: {
type: String,
default: ''
},
name: {
stockName: {
type: String,
default: ''
},
@@ -33,13 +33,15 @@ const downBorderColor = '';
const kLineChartRef = ref(null);
onMounted(() => {
handleKLine(code,name)
handleKLine(code,stockName)
})
function handleKLine(code,name){
GetStockKLine(code,name,365).then(result => {
function handleKLine(code,stockName){
console.log("handleKLine",code,stockName)
const chart = echarts.init(kLineChartRef.value);
chart.showLoading()
GetStockKLine(code,stockName,365).then(result => {
//console.log("GetStockKLine",result)
const chart = echarts.init(kLineChartRef.value);
const categoryData = [];
const values = [];
const volumns=[];
@@ -47,24 +49,28 @@ function handleKLine(code,name){
let resultElement=result[i]
//console.log("resultElement:{}",resultElement)
categoryData.push(resultElement.day)
let flag=resultElement.close>resultElement.open?1:-1
let flag=Number(resultElement.close)>Number(resultElement.open)?1:-1
if(i>0){
flag=Number(resultElement.close)>Number(result[i-1].close)?1:-1
}
values.push([
resultElement.open,
resultElement.close,
resultElement.low,
resultElement.high
Number(resultElement.open),
Number(resultElement.close),
Number(resultElement.low),
Number(resultElement.high)
])
volumns.push([i,resultElement.volume/10000,flag])
volumns.push([i,Number(resultElement.volume)/10000,flag])
}
////console.log("categoryData",categoryData)
////console.log("values",values)
let option = {
title: {
text: name+" "+code,
left: '20px',
text: stockName+" "+categoryData[values.length-1]+" "+values[values.length-1][1]+" "+((values[values.length-1][1]-values[values.length-2][1])/values[values.length-2][1]*100).toFixed(2)+"%",
left: '0px',
textStyle: {
color: darkTheme?'#ccc':'#456'
}
color: Number(values[values.length-1][1])>Number(values[values.length-2][1])?'red':'green',
fontSize: 14
},
},
darkMode: darkTheme,
//backgroundColor: '#1c1c1c',
@@ -150,7 +156,7 @@ function handleKLine(code,name){
left: '8%',
right: '8%',
top: '66%',
height: '15%'
height: '18%'
}
],
xAxis: [
@@ -184,7 +190,11 @@ function handleKLine(code,name){
scale: true,
splitArea: {
show: true
}
},
axisLabel: { show: true },
axisLine: { show: true },
axisTick: { show: true },
splitLine: { show: false }
},
{
scale: true,
@@ -354,10 +364,7 @@ function handleKLine(code,name){
]
};
chart.setOption(option);
chart.on('click',{seriesName:'日K'}, function(params) {
//console.log("click:",params);
});
chart.hideLoading()
})
}
function calculateMA(dayCount,values) {

View File

@@ -168,7 +168,7 @@ function handleEXPLANATION(value, option){
<template #trigger>
<n-button tag="a" text :type="item.CHANGE_RATE>0?'error':'success'" :bordered=false >{{ item.SECURITY_NAME_ABBR }}</n-button>
</template>
<k-line-chart style="width: 800px" :code="item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0]" :chart-height="500" :name="item.SECURITY_NAME_ABBR" :k-days="20" :dark-theme="true"></k-line-chart>
<k-line-chart style="width: 800px" :code="item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0]" :chart-height="500" :stockName="item.SECURITY_NAME_ABBR" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td>

View File

@@ -123,7 +123,7 @@ function getmMarketCode(market,code) {
<template #trigger>
<n-tag type="info" :bordered="false">{{item.codes[0].short_name }}</n-tag>
</template>
<k-line-chart style="width: 800px" :code="getmMarketCode(item.codes[0].market_code,item.codes[0].stock_code)" :chart-height="500" :name="item.codes[0].short_name" :k-days="20" :dark-theme="true"></k-line-chart>
<k-line-chart style="width: 800px" :code="getmMarketCode(item.codes[0].market_code,item.codes[0].stock_code)" :chart-height="500" :stockName="item.codes[0].short_name" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td>

View File

@@ -111,7 +111,7 @@ function handleSearch(value) {
<template #trigger>
<n-tag type="info" :bordered="false">{{item.stockName}}</n-tag>
</template>
<k-line-chart style="width: 800px" :code="getmMarketCode(item.market,item.stockCode)" :chart-height="500" :name="item.stockName" :k-days="20" :dark-theme="true"></k-line-chart>
<k-line-chart style="width: 800px" :code="getmMarketCode(item.market,item.stockCode)" :chart-height="500" :stockName="item.stockName" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td><n-tag type="info" :bordered="false">{{item.indvInduName}}</n-tag></n-td>

View File

@@ -121,10 +121,10 @@ EventsOn("updateVersion",async (msg) => {
<n-space vertical >
<n-image width="100" :src="icon" />
<h1>
<n-badge v-if="!vipLevel" :value="versionInfo" :offset="[50,10]" type="success">
<n-badge v-if="!vipLevel" :value="versionInfo" :offset="[80,10]" type="success">
<n-gradient-text type="info" :size="50" >go-stock</n-gradient-text>
</n-badge>
<n-badge v-if="vipLevel" :value="versionInfo" :offset="[50,10]" type="success">
<n-badge v-if="vipLevel" :value="versionInfo" :offset="[70,10]" type="success">
<n-gradient-text :type="expired?'error':'warning'" :size="50" >go-stock</n-gradient-text><n-tag :bordered="false" size="small" type="warning">VIP{{vipLevel}}</n-tag>
</n-badge>
</h1>
@@ -166,7 +166,7 @@ EventsOn("updateVersion",async (msg) => {
<n-td>赞助 18.8 RMB/<br>赞助 120 RMB/</n-td><n-td>vip1</n-td><n-td>💕 全部功能,软件自动更新(从CDN下载),更新快速便捷AI配置指导提示词参考等</n-td>
</n-tr>
<n-tr>
<n-td>赞助 28.8 RMB/<br>赞助 240 RMB/</n-td><n-td>vip2</n-td><n-td>💕 vip1全部功能,赠送硅基流动AI分析服务💕</n-td>
<n-td>赞助 28.8 RMB/<br>赞助 240 RMB/</n-td><n-td>vip2</n-td><n-td>💕 vip1全部功能,启动时自动同步最近24小时市场资讯(包括外媒简讯) 💕</n-td>
</n-tr>
<n-tr>
<n-td>每月赞助 X RMB</n-td><n-td>vipX</n-td><n-td>🧩 更多计划视go-stock开源项目发展情况而定...(承接GitHub项目README广告推广💖)</n-td>

View File

@@ -0,0 +1,352 @@
<script setup>
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted,onUnmounted, ref,reactive} from 'vue'
import {
GetAiRecommendStocksList,
GetConfig,
GetSponsorInfo,
SaveAsMarkdown,
ShareAnalysis
} from "../../wailsjs/go/main/App";
import {NAvatar, NButton, NEllipsis, NPopover, NText, useMessage, useNotification} from "naive-ui";
import KLineChart from "./KLineChart.vue";
import {format} from "date-fns";
const notify = useNotification()
const vipLevel=ref("");
const vipStartTime=ref("");
const vipEndTime=ref("");
const expired=ref(false)
const isValidVip=ref(false) // 是否是会员
onBeforeMount(()=> {
GetConfig().then(result => {
if (result.darkTheme) {
editorDataRef.darkTheme = true
}
})
GetSponsorInfo().then((res) => {
console.log(res)
vipLevel.value = res.vipLevel;
vipStartTime.value = res.vipStartTime;
vipEndTime.value = res.vipEndTime;
//判断时间是否到期
if (res.vipLevel) {
if (res.vipEndTime < format(new Date(), 'yyyy-MM-dd HH:mm:ss')) {
//notify.warning({content: 'VIP已到期'})
expired.value = true;
}
}else{
//notify.success({content: '未开通VIP'})
}
})
})
onMounted(() => {
query({
page: 1,
pageSize: paginationReactive.pageSize,
order: "desc",
keyword: paginationReactive.keyword,
startDate: paginationReactive.range[0],
endDate: paginationReactive.range[1]
}).then((data) => {
console.log( data)
dataRef.value = data.data
paginationReactive.page = 1
paginationReactive.pageCount = data.pageCount
paginationReactive.itemCount = data.total
loadingRef.value = false
})
})
const message = useMessage()
const mdPreviewRef = ref(null)
const mdEditorRef = ref(null)
const editorDataRef = reactive({
show: false,
loading: false,
darkTheme: false,
chatId: "",
modelName: "",
CreatedAt: "",
stockName: "",
stockCode: "",
question: "",
content: "",
})
const dataRef = ref([])
const loadingRef = ref(true)
// StockClosePrice string `json:"StockClosePrice" md:"推荐时股票收盘价格"`
// StockPrePrice string `json:"stockPrePricePrice" md:"前一交易日股票价格"`
// RecommendReason string `json:"recommendReason" md:"推荐理由/驱动因素/逻辑"`
// RecommendBuyPrice string `json:"recommendBuyPrice" md:"ai建议买入价"`
// RecommendStopProfitPrice string `json:"recommendStopProfitPrice" md:"ai建议止盈价"`
// RecommendStopLossPrice string `json:"recommendStopLossPrice" md:"ai建议止损价"`
// RiskRemarks string `json:"riskRemarks" md:"风险提示"`
// Remarks string `json:"remarks" md:"备注"`
const columnsRef = ref([
{
title: '推荐时间',
key: 'dataTime',
render(row, index) {
//2026-01-14T22:13:27.2693252+08:00 格式化为常用时间格式
return row.CreatedAt.substring(0, 19).replace('T', ' ')
}
},
// {
// title: '模型名称',
// key: 'modelName'
// },
{
title: '股票名称',
key: 'stockName',
render(row, index) {
return h(NText, { type: "info" }, { default: () => row.stockName })
}
},
{
title: '股票代码',
key: 'stockCode'
},
{
title: '推荐时股票价格',
key: 'stockPrice'
},
{
title: '收盘价格',
key: 'stockClosePrice'
},
// {
// title: '前一交易日价格',
// key: 'stockPrePrice',
// show:false,
// },
{
title: 'ai建议买入价',
key: 'recommendBuyPrice'
},
{
title: 'ai建议止盈价',
key: 'recommendStopProfitPrice'
},
{
title: 'ai建议止损价',
key: 'recommendStopLossPrice'
},
{
title: '推荐理由',
key: 'recommendReason',
ellipsis: {
tooltip: isValidVip
}
},
{
title: '风险提示',
key: 'riskRemarks',
ellipsis: {
tooltip: isValidVip
}
},
{
title: '备注',
key: 'remarks',
ellipsis: {
tooltip: isValidVip
}
},
{
title: '操作',
render(row, index) {
return h(
NButton,
{
strong: true,
tertiary: true,
size: 'small',
type: 'warning', // 橙色按钮
style: 'font-size: 14px; padding: 0 10px;', // 稍微大一点的按钮
onClick: () => rowProps(row)
},
{ default: () => '查看详细' }
)
}
},
])
const paginationReactive = reactive({
page: 1,
pageCount: 1,
pageSize: 12,
itemCount: 0,
keyword: "",
range: [new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000), new Date(new Date().getTime() + 24 * 60 * 60 * 1000)],
prefix({ itemCount }) {
return `${itemCount} 条记录`
}
})
const modalDataRef = reactive({
visible: false,
title: "",
content: "",
riskRemarks: "",
stockCode: "",
stockName: "",
remarks: "",
})
const theme = computed(() => {
return editorDataRef.darkTheme ? 'dark' : 'light'
})
function query({
page,
pageSize = 10,
order = 'desc',
keyword = "",
startDate = "",
endDate = ""
}) {
return new Promise((resolve) => {
GetAiRecommendStocksList({
"page": page,
"pageSize": pageSize,
"modelName":keyword,
"stockName":keyword,
"stockCode":keyword,
"startDate": startDate,
"endDate": endDate
}).then((res) => {
const pagedData =res.list
const total = res.total
const pageCount =res.totalPages
resolve({
pageCount,
data: pagedData,
total
})
})
})
}
function handlePageChange(currentPage) {
if (!loadingRef.value) {
loadingRef.value = true
query({
page: currentPage,
pageSize: paginationReactive.pageSize,
order: "desc",
keyword: paginationReactive.keyword,
startDate: formatDate(paginationReactive.range[0]), // Format date to string
endDate: formatDate(paginationReactive.range[1]) // Format date to string
}).then((data) => {
dataRef.value = data.data
paginationReactive.page = currentPage
paginationReactive.pageCount = data.pageCount
paginationReactive.itemCount = data.total
loadingRef.value = false
})
}
}
function handleSearch() {
if (!loadingRef.value) {
loadingRef.value = true
query({
page: 1,
pageSize: paginationReactive.pageSize,
order: "desc",
keyword: paginationReactive.keyword,
startDate: formatDate(paginationReactive.range[0]),
endDate: formatDate(paginationReactive.range[1])
}).then((data) => {
dataRef.value = data.data
paginationReactive.page = 1
paginationReactive.pageCount = data.pageCount
paginationReactive.itemCount = data.total
loadingRef.value = false
})
}
}
function formatDate(dateString) {
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
function getStockCode(stockCode) {
if(stockCode.indexOf( ".")>0){
stockCode=stockCode.split(".")[1]+stockCode.split(".")[0]
}
//转化为小写
stockCode=stockCode.toLowerCase()
return stockCode
}
function rowProps(row) {
return {
style: 'cursor: pointer;',
onClick: () => {
if(vipLevel.value===""|| Number(vipLevel.value) <=0){
notify.warning({content: '未开通VIP或者已经过期'})
return
}
//message.info(row.stockName)
modalDataRef.title = row.stockName
modalDataRef.content = row.recommendReason
modalDataRef.riskRemarks = row.riskRemarks
modalDataRef.stockCode = getStockCode(row.stockCode)
modalDataRef.stockName = row.stockName
modalDataRef.visible = true
modalDataRef.remarks = row.remarks
}
}
}
</script>
<template>
<n-input-group>
<n-date-picker v-model:value="paginationReactive.range" type="datetimerange" style="width: 50%"/>
<n-input clearable placeholder="输入关键词搜索" v-model:value="paginationReactive.keyword"/>
<n-button type="primary" ghost @click="handleSearch" @input="handleSearch">
搜索
</n-button>
</n-input-group>
<n-data-table
remote
:row-props="rowProps"
size="small"
:columns="columnsRef"
:data="dataRef"
:loading="loadingRef"
:pagination="paginationReactive"
:row-key="(rowData)=>rowData.ID"
@update:page="handlePageChange"
flex-height
style="height: calc(100vh - 210px);margin-top: 10px"
/>
<n-modal v-model:show="modalDataRef.visible" :title="modalDataRef.title" preset="card" style="width: 850px;">
<n-gradient-text :size="16" type="warning">{{modalDataRef.remarks}}</n-gradient-text>
<n-card size="small">
<KLineChart style="width: 800px" :code="getStockCode(modalDataRef.stockCode)" :chart-height="500" :stock-name="modalDataRef.stockName" :k-days="30" :dark-theme="editorDataRef.darkTheme"></KLineChart>
</n-card>
<n-card size="small">
<n-text type="info">{{modalDataRef.content}}</n-text>
<n-divider><n-gradient-text type="error">风险提示</n-gradient-text></n-divider>
<n-text type="error">{{modalDataRef.riskRemarks}}</n-text>
</n-card>
</n-modal>
</template>
<style scoped>
</style>

View File

@@ -78,6 +78,7 @@ const indexInterval = ref(null)
const indexIndustryRank = ref(null)
const stockCode= ref('')
const enableTools= ref(true)
const thinkingMode = ref(true)
const treemapRef = ref(null);
let treemapchart =null;
@@ -127,6 +128,9 @@ onBeforeMount(() => {
indexIndustryRank.value = setInterval(() => {
industryRank()
ReFlesh("财联社电报")
ReFlesh("新浪财经")
ReFlesh("外媒")
}, 1000 * 10)
@@ -223,7 +227,7 @@ function reAiSummary() {
aiSummary.value = ""
summaryModal.value = true
loading.value = true
SummaryStockNews(question.value,aiConfigId.value, sysPromptId.value,enableTools.value)
SummaryStockNews(question.value,aiConfigId.value, sysPromptId.value,enableTools.value,thinkingMode.value)
}
function getAiSummary() {
@@ -353,14 +357,14 @@ function ReFlesh(source) {
<AnalyzeMartket :dark-theme="darkTheme" :chart-height="300" :kDays="1" :name="'最近24小时热词'" />
</n-gi>
<n-gi>
<n-grid :cols="httpProxyEnabled?3:2" :y-gap="0">
<n-grid :cols="foreignNewsList.length?3:2" :y-gap="0">
<n-gi>
<news-list :newsList="telegraphList" :header-title="'财联社电报'" @update:message="ReFlesh"></news-list>
</n-gi>
<n-gi>
<news-list :newsList="sinaNewsList" :header-title="'新浪财经'" @update:message="ReFlesh"></news-list>
</n-gi>
<n-gi v-if="httpProxyEnabled">
<n-gi v-if="foreignNewsList.length>0">
<news-list :newsList="foreignNewsList" :header-title="'外媒'" @update:message="ReFlesh"></news-list>
</n-gi>
@@ -407,31 +411,31 @@ function ReFlesh(source) {
</n-grid>
</n-tab-pane>
<n-tab-pane name="上证指数" tab="上证指数">
<k-line-chart code="sh000001" :chart-height="panelHeight" name="上证指数" :k-days="20"
<k-line-chart code="sh000001" :chart-height="panelHeight" stockName="上证指数" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="深证成指" tab="深证成指">
<k-line-chart code="sz399001" :chart-height="panelHeight" name="深证成指" :k-days="20"
<k-line-chart code="sz399001" :chart-height="panelHeight" stockName="深证成指" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="创业板指" tab="创业板指">
<k-line-chart code="sz399006" :chart-height="panelHeight" name="创业板指" :k-days="20"
<k-line-chart code="sz399006" :chart-height="panelHeight" stockName="创业板指" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="恒生指数" tab="恒生指数">
<k-line-chart code="hkHSI" :chart-height="panelHeight" name="恒生指数" :k-days="20"
<k-line-chart code="hkHSI" :chart-height="panelHeight" stockName="恒生指数" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="纳斯达克" tab="纳斯达克">
<k-line-chart code="us.IXIC" :chart-height="panelHeight" name="纳斯达克" :k-days="20"
<k-line-chart code="us.IXIC" :chart-height="panelHeight" stockName="纳斯达克" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="道琼斯" tab="道琼斯">
<k-line-chart code="us.DJI" :chart-height="panelHeight" name="道琼斯" :k-days="20"
<k-line-chart code="us.DJI" :chart-height="panelHeight" stockName="道琼斯" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="标普500" tab="标普500">
<k-line-chart code="us.INX" :chart-height="panelHeight" name="标普500" :k-days="20"
<k-line-chart code="us.INX" :chart-height="panelHeight" stockName="标普500" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
</n-tabs>
@@ -439,59 +443,59 @@ function ReFlesh(source) {
<n-tab-pane name="重大指数" tab="重大指数">
<n-tabs type="segment" animated>
<n-tab-pane name="恒生科技指数" tab="恒生科技指数">
<k-line-chart code="hkHSTECH" :chart-height="panelHeight" name="恒生科技指数" :k-days="20"
<k-line-chart code="hkHSTECH" :chart-height="panelHeight" stockName="恒生科技指数" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="科创50" tab="科创50" >
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
<k-line-chart code="sh000688" :chart-height="panelHeight" stockName="科创50" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="科创芯片" tab="科创芯片" >
<k-line-chart code="sh000685" :chart-height="panelHeight" name="科创芯片" :k-days="20"
<k-line-chart code="sh000685" :chart-height="panelHeight" stockName="科创芯片" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="证券龙头" tab="证券龙头" >
<k-line-chart code="sz399437" :chart-height="panelHeight" name="证券龙头" :k-days="20"
<k-line-chart code="sz399437" :chart-height="panelHeight" stockName="证券龙头" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="高端装备" tab="高端装备" >
<k-line-chart code="sz399437" :chart-height="panelHeight" name="高端装备" :k-days="20"
<k-line-chart code="sz399437" :chart-height="panelHeight" stockName="高端装备" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="中证银行" tab="中证银行">
<k-line-chart code="sz399986" :chart-height="panelHeight" name="中证银行" :k-days="20"
<k-line-chart code="sz399986" :chart-height="panelHeight" stockName="中证银行" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="上证医药" tab="上证医药">
<k-line-chart code="sh000037" :chart-height="panelHeight" name="上证医药" :k-days="20"
<k-line-chart code="sh000037" :chart-height="panelHeight" stockName="上证医药" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="沪深300" tab="沪深300">
<k-line-chart code="sh000300" :chart-height="panelHeight" name="沪深300" :k-days="20"
<k-line-chart code="sh000300" :chart-height="panelHeight" stockName="沪深300" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="上证50" tab="上证50">
<k-line-chart code="sh000016" :chart-height="panelHeight" name="上证50" :k-days="20"
<k-line-chart code="sh000016" :chart-height="panelHeight" stockName="上证50" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="中证A500" tab="中证A500">
<k-line-chart code="sh000510" :chart-height="panelHeight" name="中证A500" :k-days="20"
<k-line-chart code="sh000510" :chart-height="panelHeight" stockName="中证A500" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="中证1000" tab="中证1000">
<k-line-chart code="sh000852" :chart-height="panelHeight" name="中证1000" :k-days="20"
<k-line-chart code="sh000852" :chart-height="panelHeight" stockName="中证1000" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="中证白酒" tab="中证白酒">
<k-line-chart code="sz399997" :chart-height="panelHeight" name="中证白酒" :k-days="20"
<k-line-chart code="sz399997" :chart-height="panelHeight" stockName="中证白酒" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="富时中国三倍做多" tab="富时中国三倍做多">
<k-line-chart code="usYINN.AM" :chart-height="panelHeight" name="富时中国三倍做多" :k-days="20"
<k-line-chart code="usYINN.AM" :chart-height="panelHeight" stockName="富时中国三倍做多" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
<n-tab-pane name="VIX恐慌指数" tab="VIX恐慌指数">
<k-line-chart code="usUVXY.AM" :chart-height="panelHeight" name="VIX恐慌指数" :k-days="20"
<k-line-chart code="usUVXY.AM" :chart-height="panelHeight" stockName="VIX恐慌指数" :k-days="20"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
</n-tabs>
@@ -704,6 +708,16 @@ function ReFlesh(source) {
不启用AI函数工具调用
</template>
</n-switch>
<n-switch v-model:value="thinkingMode" :round="false">
<template #checked>
启用思考模式
</template>
<template #unchecked>
不启用思考模式
</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">

View File

@@ -1,6 +1,7 @@
<script setup>
import {ReFleshTelegraphList} from "../../wailsjs/go/main/App";
import {RefreshCircle, RefreshCircleSharp, RefreshOutline} from "@vicons/ionicons5";
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted,onUnmounted, ref} from 'vue'
const { headerTitle,newsList } = defineProps({
headerTitle: {
@@ -18,6 +19,30 @@ const emits = defineEmits(['update:message'])
const updateMessage = () => {
emits('update:message', headerTitle)
}
// 使用 ref 创建响应式时间数据
const time = ref(new Date())
// 更新时间的函数
const updateTime = () => {
time.value = new Date()
}
let timer = null
// 组件挂载时启动定时器
onMounted(() => {
if (headerTitle === '财联社电报') {
// 每秒更新一次时间
timer = setInterval(updateTime, 1000)
}
})
// 组件卸载时清除定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
</script>
<template>
@@ -25,17 +50,18 @@ const updateMessage = () => {
<template #header>
<n-flex justify="space-between">
<n-tag :bordered="false" size="large" type="success" >{{ headerTitle }}</n-tag>
<n-tag :bordered="false" size="large" type="info" v-if="headerTitle==='财联社电报'"> <n-time :time="time"/></n-tag>
<n-button :bordered="false" @click="updateMessage"><n-icon color="#409EFF" size="25" :component="RefreshCircleSharp"/></n-button>
</n-flex>
</template>
<n-list-item v-for="item in newsList">
<n-list-item v-for="(item,idx) in newsList" :key="item.ID">
<n-space justify="center" v-if="idx!==0 && item.dataTime.substring(0,10) !== newsList[idx-1].dataTime.substring(0,10)">
<n-divider>
{{ item.dataTime.substring(0,10) }}
</n-divider>
</n-space>
<n-space justify="start" >
<!-- <n-text justify="start" :bordered="false" :type="item.isRed?'error':'info'" style="overflow-wrap: break-word;">-->
<!-- <n-tag size="small" :type="item.isRed?'error':'warning'" :bordered="false"> {{ item.time }}</n-tag>-->
<!-- <n-text size="small" v-if="item.title" type="warning" :bordered="false">{{ item.title }}&nbsp;&nbsp;</n-text>-->
<!-- <n-text style="overflow-wrap: break-word;word-break: break-all; word-wrap: break-word;" :type="item.isRed?'error':'info'">{{ item.content }}</n-text>-->
<!-- </n-text>-->
<n-collapse v-if="item.title" arrow-placement="right">
<n-collapse v-if="item.title" arrow-placement="right" >
<n-collapse-item :name="item.title">
<template #header>
<n-tag size="small" :type="item.isRed?'error':'warning'" :bordered="false"> {{ item.time }}</n-tag>

View File

@@ -72,7 +72,7 @@ function GetMoneyRankSinaData(){
<template #trigger>
<n-button tag="a" text :type="item.changeratio>0?'error':'success'" :bordered=false >{{ item.name }}</n-button>
</template>
<k-line-chart style="width: 800px" :code="item.symbol" :chart-height="500" :name="item.name" :k-days="20" :dark-theme="true"></k-line-chart>
<k-line-chart style="width: 800px" :code="item.symbol" :chart-height="500" :stockName="item.name" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td><n-text :type="item.changeratio>0?'error':'success'">{{item.trade}}</n-text></n-td>

View File

@@ -0,0 +1,49 @@
<script setup>
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted,onUnmounted, ref,reactive} from 'vue'
import {GetAIResponseResultList} from "../../wailsjs/go/main/App";
import {NButton, NEllipsis, NText} from "naive-ui";
import ResearchReport from "./researchReport.vue";
import AiRecommendStocksList from "./aiRecommendStocksList.vue";
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
import {useRoute} from 'vue-router'
const nowTab = ref("AI分析报告")
const route = useRoute()
onBeforeMount(() => {
nowTab.value = route.query.name
})
onBeforeUnmount(() => {
EventsOff("changeResearchTab")
})
onUnmounted(() => {
});
EventsOn("changeResearchTab", async (msg) => {
console.log("changeResearchTab", msg)
updateTab(msg.name)
})
function updateTab(name) {
nowTab.value = name
}
</script>
<template>
<n-card>
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab" style="--wails-draggable:no-drag">
<n-tab-pane name="AI分析报告">
<ResearchReport/>
</n-tab-pane>
<n-tab-pane name="股票推荐记录">
<AiRecommendStocksList/>
</n-tab-pane>
</n-tabs>
</n-card>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,292 @@
<script setup>
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted,onUnmounted, ref,reactive} from 'vue'
import {GetAIResponseResultList, GetConfig, SaveAsMarkdown, ShareAnalysis} from "../../wailsjs/go/main/App";
import {NAvatar, NButton, NEllipsis, NText, useMessage} from "naive-ui";
import {MdEditor, MdPreview} from 'md-editor-v3';
onBeforeMount(()=> {
GetConfig().then(result => {
if (result.darkTheme) {
editorDataRef.darkTheme = true
}
})
})
onMounted(() => {
query({
page: 1,
pageSize: paginationReactive.pageSize,
order: "desc",
keyword: paginationReactive.keyword,
startDate: paginationReactive.range[0],
endDate: paginationReactive.range[1]
}).then((data) => {
console.log( data)
dataRef.value = data.data
paginationReactive.page = 1
paginationReactive.pageCount = data.pageCount
paginationReactive.itemCount = data.total
loadingRef.value = false
})
})
const message = useMessage()
const mdPreviewRef = ref(null)
const mdEditorRef = ref(null)
const editorDataRef = reactive({
show: false,
loading: false,
darkTheme: false,
chatId: "",
modelName: "",
CreatedAt: "",
stockName: "",
stockCode: "",
question: "",
content: "",
})
const dataRef = ref([])
const loadingRef = ref(true)
const columnsRef = ref([
{
title: '分析时间',
key: 'CreatedAt',
render(row, index) {
//2026-01-14T22:13:27.2693252+08:00 格式化为常用时间格式
return row.CreatedAt.substring(0, 19).replace('T', ' ')
}
},
{
title: '模型名称',
key: 'modelName'
},
{
title: '分析对象',
key: 'stockName'
},
{
title: '提示词',
key: 'question',
render(row, index) {
return h(NEllipsis, { tooltip: true ,style: "max-width: 240px;"}, {default: () => h(NText,{type: "info"},{default: () => row.question}),})
}
},
{
title: '操作',
render(row, index) {
return h(
NButton,
{
strong: true,
tertiary: true,
size: 'small',
type: 'warning', // 橙色按钮
style: 'font-size: 14px; padding: 0 10px;', // 稍微大一点的按钮
onClick: () => showReport(row)
},
{ default: () => '查看分析报告' }
)
}
},
])
const paginationReactive = reactive({
page: 1,
pageCount: 1,
pageSize: 12,
itemCount: 0,
keyword: "",
range: [new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000), new Date(new Date().getTime() + 24 * 60 * 60 * 1000)],
prefix({ itemCount }) {
return `${itemCount} 条记录`
}
})
const theme = computed(() => {
return editorDataRef.darkTheme ? 'dark' : 'light'
})
function showReport(row) {
editorDataRef.show = true
editorDataRef.chatId = row.chatId
editorDataRef.modelName = row.modelName
editorDataRef.CreatedAt = row.CreatedAt.substring(0, 19).replace('T', ' ')
editorDataRef.stockName = row.stockName
editorDataRef.stockCode = row.stockCode
editorDataRef.question = row.question
editorDataRef.content = row.content
editorDataRef.loading = false
}
function query({
page,
pageSize = 10,
order = 'desc',
keyword = "",
startDate = "",
endDate = ""
}) {
return new Promise((resolve) => {
GetAIResponseResultList({
"page": page,
"pageSize": pageSize,
"modelName":keyword,
"question":keyword,
"stockName":keyword,
"stockCode":keyword,
"startDate":startDate,
"endDate":endDate
}).then((res) => {
const pagedData =res.list
const total = res.total
const pageCount =res.totalPages
resolve({
pageCount,
data: pagedData,
total
})
})
})
}
function handlePageChange(currentPage) {
if (!loadingRef.value) {
loadingRef.value = true
query({
page: currentPage,
pageSize: paginationReactive.pageSize,
order: "desc",
keyword: paginationReactive.keyword,
startDate: formatDate(paginationReactive.range[0]),
endDate: formatDate(paginationReactive.range[1])
}).then((data) => {
dataRef.value = data.data
paginationReactive.page = currentPage
paginationReactive.pageCount = data.pageCount
paginationReactive.itemCount = data.total
loadingRef.value = false
})
}
}
function handleSearch() {
if (!loadingRef.value) {
loadingRef.value = true
query({
page: 1,
pageSize: paginationReactive.pageSize,
order: "desc",
keyword: paginationReactive.keyword,
startDate: formatDate(paginationReactive.range[0]),
endDate: formatDate(paginationReactive.range[1])
}).then((data) => {
dataRef.value = data.data
paginationReactive.page = 1
paginationReactive.pageCount = data.pageCount
paginationReactive.itemCount = data.total
loadingRef.value = false
})
}
}
function share(code, name) {
ShareAnalysis(code, name).then(msg => {
//message.info(msg)
notify.info({
avatar: () =>
h(NAvatar, {
size: 'small',
round: false,
src: icon.value
}),
title: '分享到社区',
duration: 1000 * 30,
content: () => {
return h('div', {
style: {
'text-align': 'left',
'font-size': '14px',
}
}, {default: () => msg})
},
})
})
}
function saveAsMarkdown(code,name) {
SaveAsMarkdown(code, name).then(result => {
if(result !== ""){
message.success(result)
}
})
}
async function copyToClipboard() {
try {
await navigator.clipboard.writeText(editorDataRef.content);
message.success('分析结果已复制到剪切板');
} catch (err) {
message.error('复制失败: ' + err);
}
}
function formatDate(dateString) {
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
</script>
<template>
<n-input-group>
<n-date-picker v-model:value="paginationReactive.range" type="datetimerange" style="width: 50%"/>
<n-input clearable placeholder="输入关键词搜索" v-model:value="paginationReactive.keyword"/>
<n-button type="primary" ghost @click="handleSearch" @input="handleSearch">
搜索
</n-button>
</n-input-group>
<n-data-table
remote
size="small"
:columns="columnsRef"
:data="dataRef"
:loading="loadingRef"
:pagination="paginationReactive"
:row-key="(rowData)=>rowData.ID"
@update:page="handlePageChange"
flex-height
style="height: calc(100vh - 210px);margin-top: 10px"
/>
<n-modal transform-origin="center" v-model:show="editorDataRef.show" preset="card" style="width: 800px;"
:title="'['+editorDataRef.stockName+']AI分析'">
<n-spin size="small" :show="editorDataRef.loading">
<MdPreview ref="mdPreviewRef" style="height: 540px;text-align: left"
:modelValue="editorDataRef.content" :theme="theme"/>
</n-spin>
<template #footer>
<n-flex justify="space-between" ref="tipsRef">
<n-text type="info" v-if="editorDataRef.chatId">
<n-tag v-if="editorDataRef.modelName" type="warning" round :title="editorDataRef.chatId" :bordered="false">
{{ editorDataRef.modelName }}
</n-tag>
{{ editorDataRef.CreatedAt }}
</n-text>
<n-text type="error">*AI分析结果仅供参考请以实际行情为准投资需谨慎风险自担</n-text>
</n-flex>
</template>
<template #action>
<n-flex justify="right">
<n-button size="tiny" type="success" @click="copyToClipboard">复制到剪切板</n-button>
<n-button size="tiny" type="primary" @click="saveAsMarkdown(editorDataRef.stockCode,editorDataRef.stockName)">保存为Markdown文件</n-button>
<n-button size="tiny" type="error" @click="share(editorDataRef.stockCode,editorDataRef.stockName)">分享到项目社区</n-button>
</n-flex>
</template>
</n-modal>
</template>
<style scoped>
</style>

View File

@@ -34,6 +34,8 @@ const formValue = ref({
questionTemplate: "{{stockName}}分析和总结",
crawlTimeOut: 30,
kDays: 30,
httpProxy:"",
httpProxyEnabled:false,
},
enableDanmu: false,
browserPath: '',
@@ -57,8 +59,10 @@ function addAiConfig() {
apiKey: '',
modelName: 'deepseek-chat',
temperature: 0.1,
maxTokens: 1024,
maxTokens: 4096,
timeOut: 60,
httpProxy:"",
httpProxyEnabled:false,
}));
}
@@ -92,6 +96,8 @@ onMounted(() => {
questionTemplate: res.questionTemplate ? res.questionTemplate : '{{stockName}}分析和总结',
crawlTimeOut: res.crawlTimeOut,
kDays: res.kDays,
httpProxy:"",
httpProxyEnabled:false,
}
@@ -388,14 +394,14 @@ function deletePrompt(ID) {
</n-form-item-gi>
<n-form-item-gi :span="4" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多"
label="日K线数据(天)" path="openAI.kDays">
<n-input-number min="30" step="1" max="365" v-model:value="formValue.openAI.kDays"/>
<n-input-number min="30" step="1" max="60" v-model:value="formValue.openAI.kDays"/>
</n-form-item-gi>
<n-form-item-gi :span="2" label="http代理" path="httpProxyEnabled">
<n-form-item-gi :span="2" label="爬虫http代理" path="httpProxyEnabled">
<n-switch v-model:value="formValue.httpProxyEnabled"/>
</n-form-item-gi>
<n-form-item-gi :span="10" v-if="formValue.httpProxyEnabled" title="http代理地址"
label="http代理地址" path="httpProxy">
<n-input type="text" placeholder="http代理地址" v-model:value="formValue.httpProxy" clearable/>
<n-input type="text" placeholder="爬虫http代理地址" v-model:value="formValue.httpProxy" clearable/>
</n-form-item-gi>
@@ -452,6 +458,12 @@ function deletePrompt(ID) {
<n-form-item-gi :span="5" label="Timeout(秒)" :path="`openAI.aiConfigs[${index}].timeOut`">
<n-input-number min="60" step="1" placeholder="超时(秒)" v-model:value="aiConfig.timeOut"/>
</n-form-item-gi>
<n-form-item-gi :span="12" label="http代理" :path="`openAI.aiConfigs[${index}].httpProxyEnabled`">
<n-switch v-model:value="aiConfig.httpProxyEnabled"/>
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="aiConfig.httpProxyEnabled" title="http代理地址" :path="`openAI.aiConfigs[${index}].httpProxy`">
<n-input type="text" placeholder="http代理地址" v-model:value="aiConfig.httpProxy" clearable/>
</n-form-item-gi>
</n-grid>
</n-card>
<n-button type="primary" dashed @click="addAiConfig" style="width: 100%;">+ 添加AI配置</n-button>

View File

@@ -110,6 +110,7 @@ const modalShow4 = ref(false)
const modalShow5 = ref(false)
const addBTN = ref(true)
const enableTools = ref(false)
const thinkingMode = ref(false)
const formModel = ref({
name: "",
code: "",
@@ -1580,7 +1581,7 @@ function aiReCheckStock(stock, stockCode) {
//
//message.info("sysPromptId:"+data.sysPromptId)
NewChatStream(stock, stockCode, data.question, data.aiConfigId, data.sysPromptId, enableTools.value)
NewChatStream(stock, stockCode, data.question, data.aiConfigId, data.sysPromptId, enableTools.value,thinkingMode.value)
}
function aiCheckStock(stock, stockCode) {
@@ -2353,6 +2354,14 @@ function searchStockReport(stockCode) {
不启用AI函数工具调用
</template>
</n-switch>
<n-switch v-model:value="thinkingMode" :round="false">
<template #checked>
启用思考模式
</template>
<template #unchecked>
不启用思考模式
</template>
</n-switch>
<n-gradient-text type="error" style="margin-left: 10px">
*AI函数工具调用可以增强AI获取数据的能力,但会消耗更多tokens
</n-gradient-text>

View File

@@ -6,6 +6,7 @@ import aboutView from "../components/about.vue";
import fundView from "../components/fund.vue";
import marketView from "../components/market.vue";
import agentChat from "../components/agent-chat.vue"
import research from "../components/researchIndex.vue";
const routes = [
{ path: '/', component: stockView,name: 'stock'},
@@ -14,6 +15,8 @@ const routes = [
{ path: '/about', component: aboutView,name: 'about' },
{ path: '/market', component: marketView,name: 'market' },
{ path: '/agent', component: agentChat,name: 'agent' },
{ path: '/research', component: research,name: 'research' },
]
const router = createRouter({

View File

@@ -12,10 +12,12 @@ export function AddPrompt(arg1:models.Prompt):Promise<string>;
export function AddStockGroup(arg1:number,arg2:string):Promise<string>;
export function AnalyzeSentiment(arg1:string):Promise<data.SentimentResult>;
export function AnalyzeSentiment(arg1:string):Promise<models.SentimentResult>;
export function AnalyzeSentimentWithFreqWeight(arg1:string):Promise<Record<string, any>>;
export function BatchDeleteAIResponseResult(arg1:Array<number>):Promise<string>;
export function ChatWithAgent(arg1:string,arg2:number,arg3:any):Promise<void>;
export function CheckSponsorCode(arg1:string):Promise<Record<string, any>>;
@@ -28,6 +30,8 @@ export function ClsCalendar():Promise<Array<any>>;
export function DelPrompt(arg1:number):Promise<string>;
export function DeleteAIResponseResult(arg1:string):Promise<string>;
export function EMDictCode(arg1:string):Promise<Array<any>>;
export function ExportConfig():Promise<string>;
@@ -38,8 +42,12 @@ export function FollowFund(arg1:string):Promise<string>;
export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult>;
export function GetAIResponseResultList(arg1:models.AIResponseResultQuery):Promise<models.AIResponseResultPageData>;
export function GetAiConfigs():Promise<Array<data.AIConfig>>;
export function GetAiRecommendStocksList(arg1:models.AiRecommendStocksQuery):Promise<models.AiRecommendStocksPageData>;
export function GetConfig():Promise<data.SettingConfig>;
export function GetFollowList(arg1:number):Promise<any>;
@@ -96,7 +104,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:number,arg5:any,arg6:boolean):Promise<void>;
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:number,arg5:any,arg6:boolean,arg7:boolean):Promise<void>;
export function NewsPush(arg1:any):Promise<void>;
@@ -136,7 +144,7 @@ export function StockNotice(arg1:string):Promise<Array<any>>;
export function StockResearchReport(arg1:string):Promise<Array<any>>;
export function SummaryStockNews(arg1:string,arg2:number,arg3:any,arg4:boolean):Promise<void>;
export function SummaryStockNews(arg1:string,arg2:number,arg3:any,arg4:boolean,arg5:boolean):Promise<void>;
export function UnFollow(arg1:string):Promise<string>;

View File

@@ -26,6 +26,10 @@ export function AnalyzeSentimentWithFreqWeight(arg1) {
return window['go']['main']['App']['AnalyzeSentimentWithFreqWeight'](arg1);
}
export function BatchDeleteAIResponseResult(arg1) {
return window['go']['main']['App']['BatchDeleteAIResponseResult'](arg1);
}
export function ChatWithAgent(arg1, arg2, arg3) {
return window['go']['main']['App']['ChatWithAgent'](arg1, arg2, arg3);
}
@@ -50,6 +54,10 @@ export function DelPrompt(arg1) {
return window['go']['main']['App']['DelPrompt'](arg1);
}
export function DeleteAIResponseResult(arg1) {
return window['go']['main']['App']['DeleteAIResponseResult'](arg1);
}
export function EMDictCode(arg1) {
return window['go']['main']['App']['EMDictCode'](arg1);
}
@@ -70,10 +78,18 @@ export function GetAIResponseResult(arg1) {
return window['go']['main']['App']['GetAIResponseResult'](arg1);
}
export function GetAIResponseResultList(arg1) {
return window['go']['main']['App']['GetAIResponseResultList'](arg1);
}
export function GetAiConfigs() {
return window['go']['main']['App']['GetAiConfigs']();
}
export function GetAiRecommendStocksList(arg1) {
return window['go']['main']['App']['GetAiRecommendStocksList'](arg1);
}
export function GetConfig() {
return window['go']['main']['App']['GetConfig']();
}
@@ -186,8 +202,8 @@ export function LongTigerRank(arg1) {
return window['go']['main']['App']['LongTigerRank'](arg1);
}
export function NewChatStream(arg1, arg2, arg3, arg4, arg5, arg6) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4, arg5, arg6);
export function NewChatStream(arg1, arg2, arg3, arg4, arg5, arg6, arg7) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4, arg5, arg6, arg7);
}
export function NewsPush(arg1) {
@@ -266,8 +282,8 @@ export function StockResearchReport(arg1) {
return window['go']['main']['App']['StockResearchReport'](arg1);
}
export function SummaryStockNews(arg1, arg2, arg3, arg4) {
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2, arg3, arg4);
export function SummaryStockNews(arg1, arg2, arg3, arg4, arg5) {
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2, arg3, arg4, arg5);
}
export function UnFollow(arg1) {

View File

@@ -13,6 +13,8 @@ export namespace data {
maxTokens: number;
temperature: number;
timeOut: number;
httpProxy: string;
httpProxyEnabled: boolean;
static createFrom(source: any = {}) {
return new AIConfig(source);
@@ -30,6 +32,8 @@ export namespace data {
this.maxTokens = source["maxTokens"];
this.temperature = source["temperature"];
this.timeOut = source["timeOut"];
this.httpProxy = source["httpProxy"];
this.httpProxyEnabled = source["httpProxyEnabled"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@@ -342,26 +346,6 @@ export namespace data {
export class SentimentResult {
Score: number;
Category: number;
PositiveCount: number;
NegativeCount: number;
Description: string;
static createFrom(source: any = {}) {
return new SentimentResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.Score = source["Score"];
this.Category = source["Category"];
this.PositiveCount = source["PositiveCount"];
this.NegativeCount = source["NegativeCount"];
this.Description = source["Description"];
}
}
export class SettingConfig {
ID: number;
// Go type: time
@@ -727,6 +711,206 @@ export namespace models {
return a;
}
}
export class AIResponseResultPageData {
list: AIResponseResult[];
total: number;
page: number;
pageSize: number;
totalPages: number;
static createFrom(source: any = {}) {
return new AIResponseResultPageData(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.list = this.convertValues(source["list"], AIResponseResult);
this.total = source["total"];
this.page = source["page"];
this.pageSize = source["pageSize"];
this.totalPages = source["totalPages"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class AIResponseResultQuery {
page: number;
pageSize: number;
chatId: string;
modelName: string;
stockCode: string;
stockName: string;
question: string;
startDate: string;
endDate: string;
static createFrom(source: any = {}) {
return new AIResponseResultQuery(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.page = source["page"];
this.pageSize = source["pageSize"];
this.chatId = source["chatId"];
this.modelName = source["modelName"];
this.stockCode = source["stockCode"];
this.stockName = source["stockName"];
this.question = source["question"];
this.startDate = source["startDate"];
this.endDate = source["endDate"];
}
}
export class AiRecommendStocks {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
// Go type: time
dataTime?: any;
modelName: string;
stockCode: string;
stockName: string;
bkCode: string;
bkName: string;
stockPrice: string;
stockClosePrice: string;
stockPrePrice: string;
recommendReason: string;
recommendBuyPrice: string;
recommendStopProfitPrice: string;
recommendStopLossPrice: string;
riskRemarks: string;
remarks: string;
static createFrom(source: any = {}) {
return new AiRecommendStocks(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.dataTime = this.convertValues(source["dataTime"], null);
this.modelName = source["modelName"];
this.stockCode = source["stockCode"];
this.stockName = source["stockName"];
this.bkCode = source["bkCode"];
this.bkName = source["bkName"];
this.stockPrice = source["stockPrice"];
this.stockClosePrice = source["stockClosePrice"];
this.stockPrePrice = source["stockPrePrice"];
this.recommendReason = source["recommendReason"];
this.recommendBuyPrice = source["recommendBuyPrice"];
this.recommendStopProfitPrice = source["recommendStopProfitPrice"];
this.recommendStopLossPrice = source["recommendStopLossPrice"];
this.riskRemarks = source["riskRemarks"];
this.remarks = source["remarks"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class AiRecommendStocksPageData {
list: AiRecommendStocks[];
total: number;
page: number;
pageSize: number;
totalPages: number;
static createFrom(source: any = {}) {
return new AiRecommendStocksPageData(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.list = this.convertValues(source["list"], AiRecommendStocks);
this.total = source["total"];
this.page = source["page"];
this.pageSize = source["pageSize"];
this.totalPages = source["totalPages"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class AiRecommendStocksQuery {
page: number;
pageSize: number;
stockCode: string;
stockName: string;
bkCode: string;
bkName: string;
startDate: string;
endDate: string;
static createFrom(source: any = {}) {
return new AiRecommendStocksQuery(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.page = source["page"];
this.pageSize = source["pageSize"];
this.stockCode = source["stockCode"];
this.stockName = source["stockName"];
this.bkCode = source["bkCode"];
this.bkName = source["bkName"];
this.startDate = source["startDate"];
this.endDate = source["endDate"];
}
}
export class Prompt {
ID: number;
name: string;
@@ -745,6 +929,26 @@ export namespace models {
this.type = source["type"];
}
}
export class SentimentResult {
Score: number;
Category: number;
PositiveCount: number;
NegativeCount: number;
Description: string;
static createFrom(source: any = {}) {
return new SentimentResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.Score = source["Score"];
this.Category = source["Category"];
this.PositiveCount = source["PositiveCount"];
this.NegativeCount = source["NegativeCount"];
this.Description = source["Description"];
}
}
export class VersionInfo {
ID: number;
// Go type: time

4
go.mod
View File

@@ -5,7 +5,7 @@ go 1.25.0
require (
github.com/PuerkitoBio/goquery v1.11.0
github.com/chromedp/chromedp v0.14.2
github.com/cloudwego/eino v0.7.3
github.com/cloudwego/eino v0.7.9
github.com/cloudwego/eino-ext/components/model/ark v0.1.52
github.com/cloudwego/eino-ext/components/model/deepseek v0.1.0
github.com/cloudwego/eino-ext/components/model/openai v0.1.5
@@ -25,6 +25,7 @@ require (
github.com/tidwall/gjson v1.18.0
github.com/wailsapp/wails/v2 v2.11.0
go.uber.org/zap v1.27.1
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
golang.org/x/net v0.47.0
golang.org/x/sys v0.38.0
golang.org/x/text v0.31.0
@@ -120,7 +121,6 @@ require (
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

4
go.sum
View File

@@ -118,8 +118,8 @@ github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5P
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/eino v0.7.3 h1:+byYvxX3d9C12XfSyXBH2blZlReTuqcPPbPqsdNiYGU=
github.com/cloudwego/eino v0.7.3/go.mod h1:nA8Vacmuqv3pqKBQbTWENBLQ8MmGmPt/WqiyLeB8ohQ=
github.com/cloudwego/eino v0.7.9 h1:wrkVFKa/gqe5b7EUl/hyKVXxwcAltI0OdOzABmDS5IE=
github.com/cloudwego/eino v0.7.9/go.mod h1:nA8Vacmuqv3pqKBQbTWENBLQ8MmGmPt/WqiyLeB8ohQ=
github.com/cloudwego/eino-ext/components/model/ark v0.1.52 h1:/qqHpgvS5hoC7Z2U0JOFJL75q/odMlR26VEsoV1bjo0=
github.com/cloudwego/eino-ext/components/model/ark v0.1.52/go.mod h1:dC4wNeUdnjo4s/1r+YG7fMQcnfQ3bOFWw8Penh86vOI=
github.com/cloudwego/eino-ext/components/model/deepseek v0.1.0 h1:LutIVpQaqXaXNhn3RkSB0dWyBldQ0oxq2pecyW4jqyU=

11
main.go
View File

@@ -152,9 +152,9 @@ func main() {
BackgroundColour: backgroundColour,
Assets: assets,
Menu: AppMenu,
Logger: nil,
Logger: logger.NewFileLogger("./logs/wails.log"),
LogLevel: logger.DEBUG,
LogLevelProduction: logger.ERROR,
LogLevelProduction: logger.INFO,
OnStartup: app.startup,
OnDomReady: app.domReady,
OnBeforeClose: app.beforeClose,
@@ -189,7 +189,7 @@ func main() {
WindowIsTranslucent: true,
About: &mac.AboutInfo{
Title: "go-stock",
Message: "",
Message: "go-stockAI赋能股票分析✨ ",
Icon: icon,
},
},
@@ -241,8 +241,11 @@ func AutoMigrate() {
db.Dao.AutoMigrate(&models.LongTigerRankData{})
db.Dao.AutoMigrate(&data.AIConfig{})
db.Dao.AutoMigrate(&models.BKDict{})
db.Dao.AutoMigrate(&models.WordAnalyze{})
db.Dao.AutoMigrate(&models.SentimentResultAnalyze{})
db.Dao.AutoMigrate(&models.AiRecommendStocks{})
updateMultipleModel()
//updateMultipleModel()
}
func initStockDataUS(ctx context.Context) {