Compare commits

...

487 Commits

Author SHA1 Message Date
SparkMemory
6116e9287a
Merge pull request #84 from ArvinLovegood/dev
合并
2025-07-01 08:46:43 +08:00
ArvinLovegood
3e16574faa docs(README): 更新重大功能开发计划
- 新增股票分析知识库功能,状态为施工中
- 新增Ai智能选股功能,状态为施工中,计划在下半年重点开发
2025-06-30 16:51:07 +08:00
ArvinLovegood
482472af4e feat(frontend):添加指标选股功能
- 在 App.vue 中添加指标选股相关路由和菜单项
- 新增 SelectStock 组件实现选股功能
- 在 backend 中调整搜索股票接口的分页参数
2025-06-30 16:27:15 +08:00
ArvinLovegood
bdc3689ac8 fix(backend):修复雪球热门股票接口
- 新增请求获取 cookies
- 使用 cookies 进行后续请求
- 优化请求头设置
2025-06-30 11:07:14 +08:00
ArvinLovegood
e8ebb577b2 test: 更新测试代码并优化日志输出
- 在 market_news_api.go 中更新了 XUEQIUHotStock 的日志输出
- 在 search_stock_api.go 中注释掉了日志输出语句
- 修改了 search_stock_api_test.go 中的测试用例和日志输出格式
2025-06-30 10:45:41 +08:00
sparkmemory
71f8265bc2 feat(app): 添加股票搜索功能并优化测试用例
- 在 App 结构中添加 SearchStock 方法,用于股票搜索
- 更新测试用例,增加对搜索结果 columns 的打印
- 使用分号分隔多个搜索条件,提高搜索灵活性
2025-06-29 18:11:52 +08:00
ArvinLovegood
43063fa7fb feat(data): 添加搜索股票 API功能
- 实现了搜索股票 API 的请求和解析功能
- 添加了搜索股票的测试用例
2025-06-29 17:31:29 +08:00
ArvinLovegood
86f041b4d6 feat(frontend):添加财经日历和重大事件时间轴功能
- 在 App.d.ts 和 App.js 中添加了 ClsCalendar 和 InvestCalendarTimeLine 函数
- 在 app_common.go 中实现了对应的后端逻辑
- 新增了 InvestCalendarTimeLine 和 ClsCalendarTimeLine组件用于展示数据
- 更新了 market.vue 中的 tabs,添加了新功能的页面
2025-06-27 17:46:50 +08:00
ArvinLovegood
0ce7e8e7a7 docs(README):添加优云智算平台信息
- 在 README.md 中添加优云智算平台信息,提供免费 GPU 资源和海量源项目镜像
2025-06-26 13:25:15 +08:00
ArvinLovegood
bbab60e2ad docs(README):添加优云智算平台信息
- 在 README.md 中添加优云智算平台信息,提供免费 GPU 资源和海量源项目镜像
- 在 stock_data_api.go 中增加关注股票数量的限制,最多只能关注 63 只股票
2025-06-26 13:19:07 +08:00
ArvinLovegood
1fbd564bff build(frontend):更新Node.js版本并迁移图标库
- 将 Node.js 版本从 18.x 升级到 20.x
- 从 package.json 中移除 @vicons/ionicons5 依赖
- 在 devDependencies 中添加多个 @vicons 开头的图标库
- 更新 package-lock.json 和相关文件以反映这些更改
2025-06-25 14:10:26 +08:00
ArvinLovegood
f0ad50303e feat(frontend):优化热门股票和话题组件
- 更新热门股票列表,增加更多图标和数据字段
- 改进热门话题组件,添加点击事件和额外信息展示
- 调整股票搜索功能,使用居中弹窗打开链接
- 更新 App.vue 中的图标和菜单项
- 修改后端 HotStock 函数,增加返回数据量
2025-06-25 13:52:31 +08:00
ArvinLovegood
55839d3329 feat(frontend):优化热门股票和话题组件
- 更新热门股票列表,增加更多图标和数据字段
- 改进热门话题组件,添加点击事件和额外信息展示
- 调整股票搜索功能,使用居中弹窗打开链接
- 更新 App.vue 中的图标和菜单项
- 修改后端 HotStock 函数,增加返回数据量
2025-06-25 13:37:55 +08:00
ArvinLovegood
3f4cbca4a7 docs(README):添加热门股票、事件和话题功能,更新开发者列表
- 在 README.md 文件的更新日志中添加了 2025.06.25 的更新内容- 新增了热门股票、事件和话题功能
2025-06-25 10:34:57 +08:00
SparkMemory
6e3b9ff1f9
fix(stock): 修复昨天因为美股逻辑导致A股关注错误(CodeNoobLH/dev)
fix(stock): 修复昨天因为美股逻辑导致A股关注错误
2025-06-25 10:25:47 +08:00
浓睡不消残酒
0e45866421 fix(stock): 优化股票代码处理逻辑
- 在关注股票时,仅当股票代码以 "us" 开头时,才将其转换为 "gb_" 前缀的格式
2025-06-25 10:21:40 +08:00
ArvinLovegood
e0225c4158 docs(README): 添加热门股票、事件和话题功能
- 在 README.md 文件的更新日志中添加了 2025.06.25 的更新内容- 新增了热门股票、事件和话题功能
2025-06-25 09:43:04 +08:00
ArvinLovegood
2f6c17fb2a feat(frontend):添加热门股票、事件和话题功能
- 在 App.d.ts 和 App.js 中添加了 HotEvent、HotStock 和 HotTopic 函数
- 在 app_common.go 中实现了相关功能的后端逻辑
- 新增 HotEvents、HotStockList 和 HotTopics 组件用于前端展示
- 更新 market.vue以包含新的热门股票和话题功能
- 在 KLineChart.vue 中添加了代码和名称的显示
2025-06-25 09:41:16 +08:00
ArvinLovegood
22b4fcdffb Merge remote-tracking branch 'origin/dev' into dev 2025-06-25 09:40:35 +08:00
ArvinLovegood
7dd10d443e feat(frontend):添加热门股票、事件和话题功能
- 在 App.d.ts 和 App.js 中添加了 HotEvent、HotStock 和 HotTopic 函数
- 在 app_common.go 中实现了相关功能的后端逻辑
- 新增 HotEvents、HotStockList 和 HotTopics 组件用于前端展示
- 更新 market.vue以包含新的热门股票和话题功能
- 在 KLineChart.vue 中添加了代码和名称的显示
2025-06-25 09:40:04 +08:00
SparkMemory
5b5590ebd7
Merge pull request #81 from CodeNoobLH/master
修复美股展示和排序问题
2025-06-24 18:26:08 +08:00
浓睡不消残酒
be02343d68 修复前端关注美股后不会展示的问题
修复前端美股默认排序靠前问题
修复后端美股无法排序问题
2025-06-24 18:11:28 +08:00
SparkMemory
942d249671
Merge pull request #80 from CodeNoobLH/master
修复股票排序后,前端股票数量异常问题
2025-06-23 18:26:12 +08:00
浓睡不消残酒
9f2719cdbc 修改排序前端代码 2025-06-23 18:09:43 +08:00
浓睡不消残酒
0343a95a21 Merge branch 'master' of https://github.com/CodeNoobLH/go-stock 2025-06-23 16:42:22 +08:00
浓睡不消残酒
9337084ebf 修改排序后端代码 2025-06-23 16:37:20 +08:00
浓睡不消残酒
18834d9281
Merge branch 'ArvinLovegood:master' into master 2025-06-23 16:19:48 +08:00
SparkMemory
9e06136983
Merge pull request #79 from ArvinLovegood/dev
移除jieba依赖
2025-06-21 15:27:56 +08:00
ArvinLovegood
30a3d1d9ef Merge remote-tracking branch 'origin/dev' into dev 2025-06-21 15:21:54 +08:00
ArvinLovegood
5b6de9f9f6 build(deps): 从 go.mod 中移除 github.com/yanyiwu/gojieba
移除了 go.mod 和 go.sum 文件中不再使用的 github.com/yanyiwu/gojieba 依赖。
2025-06-21 15:21:35 +08:00
SparkMemory
65d737c695
Merge pull request #78 from ArvinLovegood/dev
合并
2025-06-21 15:19:30 +08:00
SparkMemory
af73691b22
Merge pull request #77 from ArvinLovegood/master
合并稳定版
2025-06-21 15:17:38 +08:00
ArvinLovegood
b2c12cffbb feat(backend):使用gse替代gojieba进行分词
- 移除 gojieba 依赖,减少二进制文件大小
- 添加 gse 依赖,支持更高效的分词处理
- 更新 splitWords 函数,使用 gse 进行中英文分词
- 在包初始化时加载 gse 默认词典
2025-06-21 13:49:30 +08:00
ArvinLovegood
a936dc6371 feat(frontend):个股卡片中添加按钮,可以直接跳转到个股研报和公司公告页面,查询对应个股的研报或公告
- 在 market.vue 中添加个股研报和公司公告组件
- 在 stock.vue 中增加研报和公告的搜索功能
- 修改 StockNoticeList 和 StockResearchReportList 组件,支持接收 stockCode 参数
- 在 backend 中添加 TradingView 新闻 API 接口
2025-06-20 18:43:11 +08:00
ArvinLovegood
f6d217e4fd feat(analyze): 添加情感分析功能并优化新闻推送通知
- 在 App.vue 中添加情感分析相关的导入和使用
- 在 app_common.go 中实现 AnalyzeSentiment 方法- 在 market_news_api.go 和 models.go 中集成情感分析结果
- 更新前端通知显示,根据情感分析结果调整通知类型和样式
- 在 go.mod 中添加 gojieba 依赖用于情感分析
2025-06-20 11:33:38 +08:00
ArvinLovegood
378b669827 feat(market):添加行业研究功能
- 在 App.vue 中添加行业研究选项
- 在 market.vue 中实现行业研究页面布局
- 新增 IndustryResearchReportList 组件用于显示行业研究列表
- 在 app_common.go 中添加相关 API 接口
- 在 market_news_api.go 中实现行业研究数据获取逻辑
- 更新 README.md,添加行业研究功能说明
2025-06-18 18:34:15 +08:00
ArvinLovegood
0d3fd47552 feat(market):添加行业研究功能
- 在 App.vue 中添加行业研究选项
- 在 market.vue 中实现行业研究页面布局
- 新增 IndustryResearchReportList 组件用于显示行业研究列表
- 在 app_common.go 中添加相关 API 接口
- 在 market_news_api.go 中实现行业研究数据获取逻辑
- 更新 README.md,添加行业研究功能说明
2025-06-18 18:33:20 +08:00
ArvinLovegood
a2fee361e7 feat(frontend):实时市场资讯信息提醒功能
- 新增 NewsPush 函数用于推送市场资讯
- 在 App.vue 中添加新闻推送的事件监听
- 在 settings 中增加启用新闻推送的选项
- 修改 README.md,添加实时市场资讯信息提醒的更新说明
2025-06-18 14:23:32 +08:00
ArvinLovegood
1ef950b961 docs: 注释掉 README 中的 GitCode 星星徽章
- 在 README.md 文件中,将 GitCode 的星星徽章图片链接用注释标记包围
-这样做可能是为了暂时移除或隐藏该徽章,而不直接删除代码行
2025-06-18 10:29:39 +08:00
ArvinLovegood
934b4608b7 docs(README): 更新内置股票基础数据
- 在 README.md 文件中的更新日志部分添加了新的更新记录
- 新增了"2025.06.18 更新内置股票基础数据"的更新记录条目
2025-06-18 10:11:54 +08:00
ArvinLovegood
68e7b6a68c refactor(data):更新内置股票基础数据
- 更新内置股票基础数据
2025-06-18 09:54:15 +08:00
ArvinLovegood
700572567e refactor(backend):移除市场新闻 API 的来源参数
- 将 NewMarketNewsApi().GetNewsList("新浪财经", 100) 调用中的来源参数修改为空字符串
- 此修改可能会影响市场新闻的获取结果,但具体影响需要进一步测试
2025-06-17 15:54:49 +08:00
ArvinLovegood
c9ade36844 refactor(frontend):重构龙虎榜功能
- 将龙虎榜相关代码从 market.vue 中抽离,创建独立的 LongTigerRankList 组件
-优化龙虎榜数据获取逻辑,增加对历史数据的递归查询
- 改进用户界面,保留原有的筛选和排序功能
- 删除 market.vue 中的冗余代码,提高代码可读性和维护性
2025-06-17 14:08:17 +08:00
ArvinLovegood
0a2491d725 feat(frontend):为股票公告列表添加走势图和资金趋势图
- 在 StockNoticeList 组件中添加 KLineChart 和 MoneyTrend 组件
- 实现股票代码和名称的悬停显示功能- 添加资金趋势图和 K线图的渲染
- 优化股票代码显示格式
2025-06-17 09:49:43 +08:00
ArvinLovegood
9d8af191c5 docs(README):更新公司公告信息搜索/查看功能
- 在更新日志中添加了"2025.06.15 添加公司公告信息搜索/查看功能"的记录
- 此更新增加了对公司公告信息进行搜索和查看的功能,进一步丰富了应用的资讯获取渠道
2025-06-16 18:16:20 +08:00
ArvinLovegood
6382be6b19 ci: 更新 GitHub Actions 触发条件
- 移除对 master 分支的监听
- 取消注释并启用对 '*-release' 标签的监听
2025-06-16 17:54:25 +08:00
SparkMemory
0cafcb9cd4
feat(market):添加公司公告功能
feat(market):添加公司公告功能

- 在市场页面添加公司公告选项卡
- 实现公司公告数据接口和组件
- 优化市场页面布局和功能
2025-06-16 17:42:34 +08:00
ArvinLovegood
21c7f5390c feat(market):添加公司公告功能
- 在市场页面添加公司公告选项卡
- 实现公司公告数据接口和组件
- 优化市场页面布局和功能
2025-06-16 17:40:53 +08:00
ArvinLovegood
02db6c2e87 feat(market):添加公司公告功能
- 在市场页面添加公司公告选项卡
- 实现公司公告数据接口和组件
- 优化市场页面布局和功能
2025-06-16 17:40:35 +08:00
ArvinLovegood
2811786bfd ci: 注释掉 tag 触发条件
- 注释掉了 GitHub Actions 工作流中的 tags配置
- 这将阻止任何新标签触发该工作流- 可能是为了控制工作流的触发条件,避免不必要的自动构建
2025-06-16 15:02:26 +08:00
SparkMemory
9aa2c4095a
feat(个股研报):增加个股研报搜索功能
feat(研报):增加个股研报搜索功能
2025-06-16 15:01:21 +08:00
ArvinLovegood
ad9bea4c24 feat(研报):增加个股研报搜索功能
- 修改 App.d.ts 和 App.js,为 StockResearchReport 函数添加股票代码参数
- 更新 app_common.go,将 StockResearchReport 方法改为接收股票代码参数
- 修改 market_news_api.go,实现根据股票代码查询研报的逻辑
- 更新 market_news_api_test.go,添加针对具体股票代码的测试用例
- 在前端 StockResearchReportList 组件中增加股票代码搜索功能
2025-06-16 14:45:59 +08:00
SparkMemory
4f8d84b8a0
Merge pull request #72 from ArvinLovegood/dev
ci:优化 GitHub Actions 工作流触发条件
2025-06-16 13:20:50 +08:00
ArvinLovegood
e238700333 ci:优化 GitHub Actions 工作流触发条件
- 添加 master 分支的 push 事件触发
- 保留标签触发条件
2025-06-16 13:18:59 +08:00
浓睡不消残酒
6bdff0a0f3 Merge remote-tracking branch 'origin/master' 2025-06-16 10:24:37 +08:00
浓睡不消残酒
c7655d2adf refactor(backend): 优化股票排序功能
- 重构了 SetStockSort 函数,增加了事务处理和错误处理
- 添加了对新排序位置是否被占用的检查
- 实现了向前和向后移动排序时对其他记录的影响
- 优化了数据库查询和更新操作,提高了代码的健壮性和性能
2025-06-16 10:17:17 +08:00
ArvinLovegood
8996ddf986 build:更新前端依赖
- 更新了 frontend/package.json.md5 文件- 可能涉及前端项目的依赖更新或调整
2025-06-15 17:57:03 +08:00
SparkMemory
329936568f
Merge pull request #71 from ArvinLovegood/dependabot/npm_and_yarn/frontend/multi-a91bf2f4f6
build(deps): bump esbuild and vite in /frontend
2025-06-15 17:42:34 +08:00
dependabot[bot]
0d85e24595
build(deps): bump esbuild and vite in /frontend
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.5 and updates ancestor dependency [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies need to be updated together.


Updates `esbuild` from 0.21.5 to 0.25.5
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.5...v0.25.5)

Updates `vite` from 5.4.19 to 6.3.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.5/packages/vite)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.25.5
  dependency-type: indirect
- dependency-name: vite
  dependency-version: 6.3.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-15 09:41:49 +00:00
SparkMemory
b266281bbd
Merge pull request #70 from ArvinLovegood/dependabot/npm_and_yarn/frontend/vite-5.4.19
build(deps-dev): bump vite from 5.4.14 to 5.4.19 in /frontend
2025-06-15 17:40:07 +08:00
dependabot[bot]
ace3ff7302
build(deps-dev): bump vite from 5.4.14 to 5.4.19 in /frontend
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.14 to 5.4.19.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.19/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.19/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.19
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-15 09:37:17 +00:00
SparkMemory
60b7cdc761
Merge pull request #69 from ArvinLovegood/dependabot/go_modules/golang.org/x/net-0.38.0
build(deps): bump golang.org/x/net from 0.35.0 to 0.38.0
2025-06-15 17:34:19 +08:00
dependabot[bot]
3cc597d361
build(deps): bump golang.org/x/net from 0.35.0 to 0.38.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.35.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.35.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-15 09:33:55 +00:00
SparkMemory
68bcfc679a
Merge pull request #68 from ArvinLovegood/dependabot/go_modules/golang.org/x/crypto-0.35.0
build(deps): bump golang.org/x/crypto from 0.33.0 to 0.35.0
2025-06-15 17:32:23 +08:00
dependabot[bot]
78f7808f1b
build(deps): bump golang.org/x/crypto from 0.33.0 to 0.35.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.33.0 to 0.35.0.
- [Commits](https://github.com/golang/crypto/compare/v0.33.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.35.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-15 09:13:25 +00:00
ArvinLovegood
d6c3a6b98b feat(frontend):添加个股研报到弹出菜单
- 在市场标签列表中新增了个股研报标签
- 设置了个股研报的路由和点击事件处理
- 使用了 RouterLink 组件实现导航
2025-06-15 17:06:03 +08:00
SparkMemory
3b25aa79bb
Merge pull request #67 from CodeNoobLH/master
修复了关注股票后点击成本没有效果的bug
2025-06-13 21:46:07 +08:00
浓睡不消残酒
e49545a581
Merge branch 'ArvinLovegood:master' into master 2025-06-13 18:29:30 +08:00
浓睡不消残酒
1185af5a87 feat(frontend): 更新关注列表并优化关注功能
- 修复了关注后不能点击成本的bug
2025-06-13 17:00:54 +08:00
ArvinLovegood
152a6335d8 feat(frontend):添加个股研报到弹出菜单
- 在市场标签列表中新增了个股研报标签
- 设置了个股研报的路由和点击事件处理
- 使用了 RouterLink 组件实现导航
2025-06-13 17:00:04 +08:00
ArvinLovegood
338e371190 feat(frontend):添加个股研报功能
- 在前端新增 StockResearchReportList 组件,用于显示个股研报列表
- 在后端新增 StockResearchReport 接口和实现,获取个股研报数据- 在 App.d.ts 和 App.js 中添加相关函数声明和实现- 在 market.vue 中集成新增的个股研报功能
2025-06-13 15:48:01 +08:00
ArvinLovegood
3ffcaa0374 feat(frontend):添加个股研报功能
- 在前端新增 StockResearchReportList 组件,用于显示个股研报列表
- 在后端新增 StockResearchReport 接口和实现,获取个股研报数据- 在 App.d.ts 和 App.js 中添加相关函数声明和实现- 在 market.vue 中集成新增的个股研报功能
2025-06-13 15:37:41 +08:00
ArvinLovegood
ed9d9cde77 docs(README): 更新 QQ 交流群描述
- 在 README.md 文件中更新了 QQ 交流群的描述信息
- 说明群聊已满,但会定期清理,随缘入群
2025-06-13 13:36:44 +08:00
ArvinLovegood
673d446b05 feat(market):增加龙虎榜上榜原因筛选功能并优化数据处理
- 在前端市场组件中添加龙虎榜上榜原因筛选功能
- 实现后台数据存储优化,避免重复插入相同数据
- 为 LongTigerRankData 模型添加索引,提高查询效率
2025-06-12 17:32:58 +08:00
ArvinLovegood
e2e0ef2aad docs(README): 更新龙虎榜功能和近期优化
- 添加龙虎榜功能,新增行业排名分类
- 优化股票分时图显示
- 修复财联社电报获取问题
- 优化资金趋势图表组件
- 重构应用加载和数据初始化逻辑
- 添加股票资金趋势功能,增加主力当日净流入数据并优化展示效果- 添加个股资金流向功能
- 排行榜增加股票行情K线图弹窗
- 添加行业排名功能
- 优化分时图的展示
- 补全港股/美股基础数据,优化港股股价延迟问题和初始化逻辑
2025-06-12 15:47:29 +08:00
ArvinLovegood
a8ecbf9329 feat(frontend):添加龙虎榜功能
- 在前端 App.vue 中添加龙虎榜相关路由和图标
- 实现龙虎榜数据获取和展示功能
- 添加龙虎榜数据模型和 API 接口
- 更新后端 MarketNewsApi 类,增加 LongTiger 方法获取龙虎榜数据
2025-06-12 15:38:42 +08:00
ArvinLovegood
9eded54d8d refactor(frontend):优化股票分时图显示
- 调整股票价格显示范围,增加百分比浮动
- 在模态框标题中添加股票涨跌百分比
2025-05-30 11:31:51 +08:00
ArvinLovegood
c1d458e5cf refactor(frontend):优化股票分时图显示
- 调整股票价格显示范围,增加百分比浮动
- 在模态框标题中添加股票涨跌百分比
2025-05-30 11:25:17 +08:00
ArvinLovegood
7158e405a6 refactor(frontend):优化股票分时图显示
- 调整股票价格显示范围,增加百分比浮动
- 在模态框标题中添加股票涨跌百分比
2025-05-30 10:56:42 +08:00
ArvinLovegood
d993a5525f feat(market): 添加证监会行业资金排名(净流入)板块
- 在市场组件中增加了一个新的标签页"证监会行业资金排名(净流入)"
- 使用 industryMoneyRank组件来展示该排名,传入不同的分类参数 fenlei='2'- 保持与其他行业排名相同的展示逻辑和样式
2025-05-27 14:45:38 +08:00
ArvinLovegood
6af6d989ba feat(frontend):新增行业资金排名功能
- 在市场页面添加行业资金排名和概念板块资金排名两个新标签页
- 实现行业和概念板块的资金流向数据展示
- 新增 industryMoneyRank组件用于显示资金排名数据
- 更新后端 API 接口,支持按不同排序方式获取行业资金排名数据
2025-05-27 14:39:26 +08:00
ArvinLovegood
0b3acd9adc ci: 更新 GitHub Actions 工作流触发条件
- 将标签匹配模式从 '*' 改为 '*-release'
- 仅匹配以 '-release' 结尾的标签,限制发布次数
2025-05-21 10:15:17 +08:00
ArvinLovegood
013de869f4 feat(backend):新增 top stocks 排行榜功能并更新相关模块
- 在 MarketNewsApi 中添加 TopStocksRankingList 方法,实现 top stocks 排行榜数据的获取和解析
- 更新 App.vue 中的 content 文本,增加未经授权禁止商业用途的声明- 在 market_news_api_test.go 中添加 TopStocksRankingList 的测试用例
2025-05-21 09:59:38 +08:00
ArvinLovegood
1b67e20932 refactor(backend/data):修复财联社电报获取问题
- 修改 market_news_api.go 中的 GoQuery 选择器,从 ".telegraph-list"改为 ".telegraph-content-box"- 更新 openai_api_test.go 中的 TestGetTopNewsList 函数,增加测试日志输出
2025-05-20 10:46:30 +08:00
ArvinLovegood
8b510bce94 refactor(frontend):优化资金趋势图表组件
- 优化图表展示效果,增加累计净流入和股价的显示
- 调整图表样式,增加暗黑主题支持
- 优化数据处理逻辑,提高图表准确性
-调整模态框样式,移除不必要的属性
2025-05-16 17:26:34 +08:00
ArvinLovegood
71676eead4 feat(moneyTrend):资金趋势图表增加主力当日净流入数据并优化展示效果
- 在资金趋势图表中添加主力当日净流入数据
- 优化图表颜色和样式,增加最大值和最小值标记
- 添加平均值参考线
- 调整轴线样式,提高可读性
- 后端接口增加数据天数至360天
2025-05-15 21:28:59 +08:00
ArvinLovegood
2a274db7ae feat(frontend):添加股票资金趋势功能
- 在前端添加了股票资金趋势页面组件
- 在后端实现了获取股票资金趋势数据的接口
- 优化了前端界面布局,增加了资金趋势按钮
2025-05-15 18:36:53 +08:00
ArvinLovegood
4fd5cbf8e6 refactor(app):重构应用加载和数据初始化逻辑(小白福音)
- 在 domReady 函数中添加股票数据初始化逻辑
- 更新前端 App.vue以显示加载信息
- 修改后端 initStockData 函数,添加上下文和加载消息
- 优化市场数据定时刷新逻辑
- 修复 AI 响应结果获取方式
2025-05-15 14:29:08 +08:00
ArvinLovegood
d7b17b2561 refactor(app):重构应用加载和数据初始化逻辑(小白福音)
- 在 domReady 函数中添加股票数据初始化逻辑
- 更新前端 App.vue以显示加载信息
- 修改后端 initStockData 函数,添加上下文和加载消息
- 优化市场数据定时刷新逻辑
- 修复 AI 响应结果获取方式
2025-05-15 14:13:42 +08:00
ArvinLovegood
ad92c41d08 feat(rankTable):排行榜增加股票行情K线图弹窗
- 在排名表格中,将股票名称单元格改为可触发 popover 的按钮
- 在 popover 中显示股票的 K 线图
- 引入 KLineChart 组件用于渲染 K线图
- 优化表格展示效果,调整涨跌幅和成交额的显示方式
2025-05-14 15:29:06 +08:00
ArvinLovegood
47dbbb8813 feat(frontend):添加个股资金流向功能
- 在 App.vue 中添加个股资金流向相关路由和菜单项
- 新增 RankTable 组件用于展示排名数据
- 在 market.vue 中集成 RankTable 组件,实现资金流向排名展示
- 在后端添加 GetIndustryMoneyRankSina 和 GetMoneyRankSina接口
- 更新前端 App.d.ts、App.js 和后端 app.go 以支持新功能
2025-05-14 12:04:32 +08:00
ArvinLovegood
ae9f4073dc feat(market):添加行业排名功能
- 在市场行情模块中增加行业排名标签页
- 实现行业排名数据的获取和展示- 添加行业排名相关的图标和交互
- 优化市场行情模块的结构和样式
2025-05-13 23:11:36 +08:00
ArvinLovegood
c7e37e039e feat(frontend):添加股票分组菜单功能并优化路由
- 在 App.vue 中添加股票分组列表,动态生成分组选项
- 更新路由配置,使用 createWebHistory替代 createWebHashHistory
- 在 stock.vue 中添加分组切换逻辑,支持通过路由和事件切换分组
2025-05-09 23:43:51 +08:00
ArvinLovegood
99b6586c77 feat(stock):添加A股盘口数据解析和展示功能
- 在 stock.vue 中添加盘口数据展示组件
- 在 stock_data_api.go 中增加 A 股盘口数据解析逻辑
- 优化数据库自动迁移逻辑,提取到单独的函数中
- 更新测试用例以覆盖新的盘口数据解析功能
2025-05-09 11:52:52 +08:00
ArvinLovegood
7e24424ea0 feat(stock):添加A股盘口数据解析和展示功能
- 在 stock.vue 中添加盘口数据展示组件
- 在 stock_data_api.go 中增加 A 股盘口数据解析逻辑
- 优化数据库自动迁移逻辑,提取到单独的函数中
- 更新测试用例以覆盖新的盘口数据解析功能
2025-05-09 11:44:13 +08:00
ArvinLovegood
58d93c76f6 feat(stock):优化分时图展示效果
- 重新设计分时图布局和样式,增加更多图表元素
- 添加开盘价、收盘价等关键信息显示
- 实现分时图自动刷新功能
- 优化模态框样式,调整图表尺寸
- 重构相关函数,提高代码可维护性
2025-05-08 18:31:20 +08:00
ArvinLovegood
df989b706b docs(README): 更新分时图展示优化及版本日志
- 优化分时图的展示效果
- 在更新日志中添加 2025.05.07 版本的改动说明
2025-05-07 16:42:43 +08:00
ArvinLovegood
cf537ca695 feat(stock):优化股票分时图表展示
- 新增 GetStockMinutePriceLineData 函数获取股票分时数据
- 在前端实现分时数据图表展示
- 后端增加 GetStockMinutePriceData接口获取分时数据
- 更新数据库模型,添加 MinuteData 结构体
2025-05-07 16:17:33 +08:00
ArvinLovegood
11a1a47eca feat(data):补全港股/美股基础数据,优化初始化逻辑
- 美股和港股数据初始化时增加总数检查,避免重复插入
- 优化数据插入逻辑,减少不必要的查询操作
-港股数据初始化逻辑调整,解决数据延迟问题
2025-04-29 15:34:06 +08:00
ArvinLovegood
338064e536 feat(backend/data):添加腾讯股票数据接口支持
- 新增腾讯股票数据接口 URL
- 实现腾讯股票数据解析逻辑,支持港股和 A 股
- 更新 GetStockCodeRealTimeData 方法,支持腾讯股票数据
- 添加腾讯股票数据解析单元测试
2025-04-29 15:06:21 +08:00
ArvinLovegood
8ba26b6250 feat(data):补全港股和美股基础信息
- 新增了从多个数据源获取港股和美股信息的方法
- 实现了对获取数据的解析和存储
- 添加了相关的测试函数
2025-04-29 10:52:00 +08:00
ArvinLovegood
54138ff61e feat(frontend):添加市场资讯手动刷新功能
- 在 App.d.ts 中添加 ReFleshTelegraphList 函数声明
- 在 app.go 中实现 ReFleshTelegraphList 方法,用于刷新电报列表- 在 App.js 中添加 ReFleshTelegraphList 函数的前端调用接口
- 在 market.vue 中添加 ReFlesh 函数,用于调用刷新接口并更新数据
- 在 newsList.vue 中添加刷新按钮和相关事件处理,支持手动刷新功能
2025-04-28 12:17:27 +08:00
ArvinLovegood
d8d5091709 docs: 更新项目名称描述
- 将"基于大预言模型的AI赋能股票分析工具"修改为"基于大语言模型的AI赋能股票分析工具"
- 此更新更准确地描述了项目的功能和目标
2025-04-27 14:59:59 +08:00
ArvinLovegood
7f204ee80d docs: 更新项目 README
- 修改项目标题为"基于大预言模型的AI赋能股票分析工具"
- 移除了对 Wails 和 NaiveUI 的提及
2025-04-27 14:58:38 +08:00
ArvinLovegood
4a367b6027 docs(README): 更新 QQ 交流群链接
- 添加新的 QQ交流群 2 链接
- 保留原 QQ 交流群链接,并注明已满
- 优化群链接格式,提高可读性
2025-04-27 14:33:36 +08:00
ArvinLovegood
e615fc4108 docs(README): 更新 QQ 交流群链接
- 添加新的 QQ交流群 2 链接
- 保留原 QQ 交流群链接,并注明已满
- 优化群链接格式,提高可读性
2025-04-27 14:33:06 +08:00
ArvinLovegood
2b982f924e feat(market):添加VIX恐慌指数
- 在市场组件中新增 VIX 恐慌指数选项卡
- 使用代码 usUVXY.AM 获取 VIX 恐慌指数数据
- 设置图表显示参数,包括面板高度、名称、K 线天数和暗黑主题
2025-04-27 14:21:20 +08:00
ArvinLovegood
24e24f8236 refactor(frontend):修正美洲地区名称翻译
- 将美国地区的翻译从 "美国" 修改为 "美洲"
- 此修改提高了地区名称的准确性和一致性
2025-04-27 10:49:12 +08:00
ArvinLovegood
e77c23e42a docs(README): 更新市场资讯支持AI分析和总结
- 新增AI分析和总结功能,让AI帮助用户读取市场信息
- 添加相关截图展示新功能
- 调整现有内容格式,优化列表符号使用
2025-04-25 18:14:21 +08:00
ArvinLovegood
ef6228922e ci:更新Go版本并修复市场资讯保存功能
-将 Go 版本从 1.23 升级到1.24
- 修复市场资讯保存功能,更新 SaveAsMarkdown 调用参数
2025-04-25 17:55:24 +08:00
ArvinLovegood
c4caea5be8 feat(frontend):添加AI市场资讯总结功能
- 在市场组件中增加 AI 总结按钮和模态框
- 实现 SummaryStockNews 函数用于获取 AI 总结
- 添加 GetNewsList 方法获取市场新闻列表
- 优化市场资讯的展示和交互
2025-04-25 17:03:52 +08:00
ArvinLovegood
3535ba57ab feat(frontend):添加AI市场资讯总结功能
- 在市场组件中增加 AI 总结按钮和模态框
- 实现 SummaryStockNews 函数用于获取 AI 总结
- 添加 GetNewsList 方法获取市场新闻列表
- 优化市场资讯的展示和交互
2025-04-25 16:33:14 +08:00
ArvinLovegood
cedff896bb refactor(frontend):调整K线图标记点样式
- 注释掉 KLineChart.vue 中的 markPoint.symbol属性,以修改标记点的显示方式
- 在 main.go 中:
- 移除初始化股票数据的冗余注释代码 - 保留获取屏幕分辨率的代码并修复缩进
  -调整应用程序窗口的最小高度
2025-04-24 22:19:58 +08:00
ArvinLovegood
ffc212abc3 feat(README): 新增市场行情模块说明和截图
- 在更新日志和重大更新部分添加市场行情模块的相关信息
- 插入市场行情模块的截图链接
- 优化README内容,增强项目展示效果
2025-04-24 17:50:55 +08:00
ArvinLovegood
7bacbe0d89 feat(frontend):重构市场资讯页面并添加全球股指功能
- 重构市场资讯页面布局,增加多个新闻源和全球股指信息- 新增 GetStockCommonKLine、GetTelegraphList 和 GlobalStockIndexes 等接口
- 实现全球股指数据的获取和展示
- 优化市场资讯的获取和更新逻辑
- 调整 K 线图组件的参数和样式
2025-04-24 17:30:54 +08:00
ArvinLovegood
6be23d6abc feat(frontend):添加市场资讯功能
- 新增市场资讯页面,用于展示财经新闻
- 实现电报列表获取和实时更新功能
- 添加新闻标签和股票标签显示
- 优化新闻列表展示样式
2025-04-23 16:39:54 +08:00
ArvinLovegood
3a74e0ed98 refactor(frontend):优化股票K线组件的样式和功能
-移除了多余的 console.log 语句
- 调整了图表的样式,包括颜色、背景等
- 优化了鼠标悬停时的提示信息显示
-调整了均线的透明度
- 优化了分组列表的加载逻辑
2025-04-22 18:03:20 +08:00
ArvinLovegood
4b0b3c0491 refactor(frontend):调整K线成交量颜色
- 调整了 stock.vue 文件中 dimension 属性的 pieces 数组顺序
- 将下降颜色(downColor)对应的值改为 -1,上升颜色(upColor)对应的值改为 1
2025-04-22 13:04:45 +08:00
ArvinLovegood
2bd63cf2f4 feat(frontend):优化股票K线图功能
- 在 App.d.ts 中添加 GetStockKLine 函数声明
- 在 app.go 中实现 GetStockKLine 方法- 在 App.js 中添加 GetStockKLine 函数导出
- 在 package.json 中添加 echarts依赖
- 在 stock.vue 中实现 K 线图展示功能- 优化 K 线图数据处理和图表配置
2025-04-22 11:56:35 +08:00
ArvinLovegood
71d8822d15 feat(frontend):优化股票K线图功能
- 在 App.d.ts 中添加 GetStockKLine 函数声明
- 在 app.go 中实现 GetStockKLine 方法- 在 App.js 中添加 GetStockKLine 函数导出
- 在 package.json 中添加 echarts依赖
- 在 stock.vue 中实现 K 线图展示功能- 优化 K 线图数据处理和图表配置
2025-04-22 11:43:31 +08:00
ArvinLovegood
db3594af77 feat(stock):支持港股和美股的K线数据获取
- 修改了 openai_api.go 中的股票代码处理逻辑,增加了对港股和美股的支持
- 新增了 StockDataApi 类中的 GetHK_KLineData 方法,用于获取港股和美股的 K 线数据
- 更新了前端 stock.vue 组件的样式
-增加了 GetHK_KLineData 方法的单元测试
2025-04-21 18:39:20 +08:00
ArvinLovegood
c7d728e613 refactor(backend):修正K线数据接口参数
- 将 URL 中的 `days` 参数改为 `datalen` 参数- 适应 Sina API 的变更,确保正确获取 K 线数据
2025-04-18 18:22:45 +08:00
Lovegood
3ca2eed575
Merge pull request #65 from Exisfar/exisfar
更正了 未开盘时今日盈亏 计算存在的问题
2025-04-13 18:30:09 +08:00
Exisfar
8cd55034c3 更正了 未开盘时今日盈亏 计算存在的问题 2025-04-12 23:07:08 +08:00
ArvinLovegood
344c43cbf1 ci:移除 windows/arm64 平台的构建
- 删除了 GitHub Actions 工作流中针对 windows/arm64 平台的构建任务
- 保留了 windows/amd64 平台的构建
- 注释掉了 linux/amd64 平台的构建
2025-04-10 18:03:21 +08:00
ArvinLovegood
8c49b00057 ci:添加windows/arm64平台
- 在 GitHub Actions 工作流程中增加了 windows/arm64 平台的生成任务
- 新增 go-stock-windows-arm64.exe 可执行文件
2025-04-10 17:56:04 +08:00
ArvinLovegood
51cc21107a feat(data):从雪球接口获取财务数据并优化表格解析
- 新增 GetFinancialReportsByXUEQIU 函数,用于从雪球获取财务报告
- 优化 GetTableMarkdown 函数,改进表格解析逻辑
- 更新测试用例,验证新接口的正确性- 重构原有 GetFinancialReports函数,提高代码可维护性
2025-04-08 17:09:48 +08:00
ArvinLovegood
ece40d1fc0 feat(data): 添加雪球接口获取财务数据并优化表格解析
- 新增 GetFinancialReportsByXUEQIU 函数,用于从雪球获取财务报告
- 优化 GetTableMarkdown 函数,改进表格解析逻辑
- 更新测试用例,验证新接口的正确性- 重构原有 GetFinancialReports函数,提高代码可维护性
2025-04-08 17:06:10 +08:00
ArvinLovegood
1a3c8b4fae feat(config):添加基金功能启用配置
- 在配置文件中增加 enableFund 字段,用于控制是否启用基金功能
- 根据配置决定是否启动基金监控和数据获取任务
- 更新前端界面,在设置页面添加基金功能启用开关
- 优化代码结构,提高可维护性和可读性
2025-04-07 14:03:17 +08:00
ArvinLovegood
09d3a16841 feat(config):添加基金功能启用配置
- 在配置文件中增加 enableFund 字段,用于控制是否启用基金功能
- 根据配置决定是否启动基金监控和数据获取任务
- 更新前端界面,在设置页面添加基金功能启用开关
- 优化代码结构,提高可维护性和可读性
2025-04-07 13:44:47 +08:00
ArvinLovegood
65bc8cde47 fix(stock):修复股票代码错误导致的重复问题
- 在前端 stock 组件中增加了对股票代码是否已存在于 stocks.value 中的检查
- 如果股票代码不存在,则删除对应的 result.key
- 在后端 app.go 中增加了对股票价格变化的判断,只有在价格变化时才发送事件
2025-04-07 12:25:40 +08:00
ArvinLovegood
b45d5dc762 refactor(frontend):优化滚动效果并更新股票分组列表
- 修复滚动到指定元素时的 smooth behavior
- 在添加股票分组后更新分组列表- 调整股票卡片的 ID 和数据属性
- 优化网格布局的间距设置
2025-04-03 17:55:09 +08:00
ArvinLovegood
512f9a0757 feat(stock):添加股票分组功能
- 新增股票分组相关接口和页面
- 实现分组添加、删除和股票移除功能
- 优化股票列表展示,支持按分组筛选
- 添加分组相关数据结构和 API
2025-04-03 17:21:07 +08:00
ArvinLovegood
9e5650617b refactor(settings):调整浏览器池大小默认值
-将 BrowserPoolSize 的默认值从 3 修改为 1
- 确保在设置值小于等于 0 时,使用新的默认值 1
2025-04-02 14:04:56 +08:00
ArvinLovegood
bac10a2a04 refactor(app):重构主程序和优化股票查询功能
- 重构主程序循环,使用 goroutine 启动 systray.Run
- 注释掉 onExit 函数中的退出操作
- 优化股票查询功能,增加实时数据获取和处理
- 改进模板替换逻辑,支持多种格式
2025-04-02 13:46:13 +08:00
ArvinLovegood
65060a91ce docs(README): 更新 Tushare 注册说明
- 在 README.md 中更新了 Tushare 大数据开放社区的注册说明
- 新增提示:Tushare只需要 120 积分,注册完成后补充个人资料即可获得 120 积分
2025-04-02 11:55:47 +08:00
ArvinLovegood
2ae3893325 feat(data):替换A股K线数据源(不再强制依赖Tushare)
- 新增 GetKLineData 方法,用于获取指定股票的 K线数据
- 实现了将 JSON 数据转换为 Markdown 表格的函数 JSONToMarkdownTable- 在 NewChatStream 中添加了对 A 股 K线数据的获取和展示逻辑- 增加了相关测试用例
2025-04-02 11:42:06 +08:00
ArvinLovegood
fdaa80777d refactor(data):重构股票价格信息获取功能
- 更新 SearchStockPriceInfo 函数签名,增加 stockName 参数
- 优化股票价格信息的爬取逻辑,支持不同市场类型的股票
- 调整输出格式,增加股票名称和时间信息
- 添加日志记录,方便调试和监控
2025-04-02 09:24:24 +08:00
ArvinLovegood
5de74f220f docs(README):优化部分设置选项,避免重启软件
- 在 README.md 中添加了新的更新日志条目,说明对设置选项进行了优化
- 此更新提高了软件的用户体验,减少了重启软件的需要
2025-04-01 17:59:02 +08:00
ArvinLovegood
c5065b0504 feat(frontend):实现暗黑主题切换即时生效不需要重启
- 在 about、fund、settings 和 stock 组件中添加 onBeforeUnmount 钩子,用于销毁消息实例
- 在 app.go 中添加 updateSettings 事件处理,根据配置切换暗黑主题
-优化 settings 组件,保存配置后发送 updateSettings 事件
-调整 stock 组件中 n-card 的属性,移除冗余代码
2025-04-01 17:14:28 +08:00
ArvinLovegood
9ebb246e5c refactor:调整日志级别并优化代码
-将数据库日志级别从 Info降低到 Warn,减少不必要的日志输出
- 注释掉股票数据存在时的 Info 级别日志,降低日志冗余
2025-04-01 16:12:11 +08:00
ArvinLovegood
5096bfac68 feat(core): 用 cron 替代 ticker 实现定时任务
- 使用 cron 库替换原有的 ticker 实现,提高定时任务的准确性和灵活性
- 新增 cronEntrys 字典用于管理定时任务,便于更新和删除
- 修改数据刷新间隔的设置方式,支持动态更新
- 优化股票监控和新闻刷新的定时任务执行逻辑
2025-04-01 16:07:55 +08:00
ArvinLovegood
63e898bef8 refactor(frontend):优化股票排序
- 在 stock.vue 中引入 lodash 的 keys 和 pad 函数
-优化排序逻辑,使用 lodash 的 keys 函数替代 Object.keys
-移除不必要的 padZero 函数,简化 GetSortKey 的实现
- 在 package.json 中添加 lodash 依赖
2025-04-01 14:00:46 +08:00
ArvinLovegood
7af3fe72d5 docs(README): 更新数据爬取优化的说明
- 在更新日志中添加了"2025.03.31优化数据爬取"的条目
- 说明了对数据爬取功能进行了优化和改进
2025-04-01 11:51:21 +08:00
ArvinLovegood
3402f0d296 feat(data):实现浏览器实例池化
- 新增 BrowserPool 结构和相关方法,用于管理和复用浏览器实例
- 在 CrawlerApi 中集成浏览器池,使用 FetchPage 方法获取页面内容
-优化了配置获取方式,统一使用 GetConfig() 函数
-修复了一些代码中的小问题,如错误处理和日志记录
2025-03-31 23:08:09 +08:00
ArvinLovegood
51aae0539c refactor(backend):优化日志输出和接口调用
- 移除不必要的日志输出,减少日志噪音
- 优化 OpenAI API 调用逻辑,改进消息构建方式
- 注释掉部分不必要的代码,提高代码可读性
- 更新 README 中的 DeepSeek 相关信息
2025-03-31 16:39:13 +08:00
ArvinLovegood
7b625e2e80 feat(backend):AI分析添加大盘指数信息
- 新增 getZSInfo 函数,用于获取指定股票代码的大盘指数信息
- 在处理用户问题时添加大盘指数信息查询功能
- 优化了代码结构,提高了可维护性
2025-03-31 14:49:44 +08:00
ArvinLovegood
f1e40e7d3b refactor(data):重构财务数据爬取功能
- 移除雪球爬虫测试,改为 sina 和 eastmoney 测试
- 新增eastmoney财务数据爬取支持
- 优化openai_api.go中的财务报告获取逻辑
- 使用通用爬虫API替代chromedp实现
2025-03-31 14:05:04 +08:00
ArvinLovegood
5f8556cc3d refactor(stock):重构股票价格数据爬取功能
- 移除了不必要的 chromedp Cancel 调用
- 新增了对雪球网的爬虫测试用例
- 修改了股票价格信息的爬取逻辑,使用新浪财经作为数据源
- 优化了爬取结果的 Markdown 格式输出
- 删除了未使用的 validator包引用
2025-03-31 12:33:56 +08:00
ArvinLovegood
34e2de07fb feat(systray):替换系统托盘库并优化相关功能
- 使用 energye/systray 替换 getlantern/systray
- 优化系统托盘创建和菜单项处理逻辑
- 移除冗余的事件监听代码
- 更新 go.mod 和 go.sum 文件以反映库依赖变更
2025-03-31 10:32:09 +08:00
ArvinLovegood
b186a17a81 feat(cron):设置cron时,cron任务实时生效,避免重启
- 新增 AddCronTask 函数用于添加 cron 任务
- 在 App 结构中添加 cronEntrys 字典用于管理 cron 任务 ID- 优化 SetStockAICron 函数,支持更新和删除 cron 任务
- 新增 GetFollowedStockByStockCode 函数用于获取关注的股票信息
- 更新前端 API 接口,添加 AddCronTask 方法
2025-03-30 15:10:55 +08:00
ArvinLovegood
95c3909dc9 docs(README): 添加 AI 自动定时分析功能更新日志
- 在 README.md 文件的更新日志部分添加了 2025.03.30 的 AI 自动定时分析功能
-此功能的添加标志着 AI 分析工具的进一步智能化和自动化
2025-03-30 09:59:04 +08:00
ArvinLovegood
54b0c7ccb3 feat(stock):添加股票自动分析功能
- 在 App 结构中添加 cron 实例,用于定时任务调度
- 新增 SetStockAICron 函数,用于设置股票自动分析的 cron 表达式- 在前端 stock 组件中添加 cron 字段,允许用户输入定时任务规则
- 在后端 StockDataApi 中添加 SetStockAICron 方法,用于更新数据库中的 cron 信息
- 修改前端保存逻辑,当用户设置 cron 时,调用 SetStockAICron接口保存
2025-03-30 08:58:45 +08:00
ArvinLovegood
e44bc55301 docs(README): 更新 AI 分析和 Markdown 文件保存功能
- 新增多提示词模板管理功能,AI 分析时可选择不同提示词模板
- AI 分析结果保存为 Markdown 文件时,支持选择保存位置目录
2025-03-29 21:35:57 +08:00
ArvinLovegood
fd3046b2c3 feat(prompt):添加prompt模板管理功能
- 新增 PromptTemplate 模型和相关 API
- 实现 prompt 模板的添加、删除和查询功能
- 在前端添加 prompt 管理界面
- 修改聊天流 API,支持使用自定义 prompt
2025-03-29 21:31:06 +08:00
ArvinLovegood
2b41dc11c1 featend(front):保存AI分析结果为Markdown文件时可以选择保存目录
- 在 App.d.ts 中添加 SaveAsMarkdown 函数声明
- 在 app.go 中实现 SaveAsMarkdown 方法,用于保存分析结果
- 在 App.js 中添加 SaveAsMarkdown 函数的 JavaScript 调用接口- 在 stock.vue 中添加保存为 Markdown 的功能按钮,并实现相关逻辑
2025-03-28 22:20:30 +08:00
ArvinLovegood
076dc4f9ef fix(backend/data): 修复爬虫任务取消后未关闭 ctx 的问题
- 在 crawler_api.go 文件中的多个函数中添加了 chromedp.Cancel(ctx) 调用
- 确保在任务取消时能够正确关闭 ctx,避免资源泄露
2025-03-28 21:31:35 +08:00
ArvinLovegood
1a728672c8 feat(frontend):优化弹幕显示效果并支持暗黑主题
- 在 fund.vue 和 stock.vue组件中,使用 useSlot 属性自定义弹幕样式
- 添加暗黑主题支持,根据用户设置动态调整主题
- 修改 AI 分析结果的 Markdown 编辑器和预览主题,使其支持动态主题切换
2025-03-26 17:05:31 +08:00
ArvinLovegood
c8178a6c5f feat(settings):设置界面添加主题切换功能
- 在 Settings 模型中添加 darkTheme 字段
- 在前端 App.vue 中实现暗黑主题切换
- 更新设置界面,增加暗黑主题开关
- 调整股票卡片样式,支持暗黑主题
- 优化 HTML 和 CSS 样式以适应暗黑主题
2025-03-26 15:29:08 +08:00
ArvinLovegood
9d546fd214 refactor:调整最小高度以适应16:9的宽高比
- 将 MinHeight 从 768 修改为 800
- 保持 MinWidth 为 1456,与修改后的 MinHeight 形成 16:9 的宽高比
2025-03-24 10:33:01 +08:00
ArvinLovegood
d467adbdec style(frontend):调整界面拖动元素
- 在 App.vue 中为底部菜单栏添加 --wails-draggable:drag 样式
- 在 index.html 中移除 body 标签中的 --wails-draggable:drag 样式
2025-03-24 09:12:18 +08:00
ArvinLovegood
c08776d028 style(frontend):优化基金和股票组件的样式
- 调整了基金和股票组件中弹出框的位置
- 修改了关注按钮和发送弹幕按钮的样式
- 注释掉了主窗口分辨率自动获取的代码
- 调整了主窗口的默认尺寸设置
2025-03-22 20:38:27 +08:00
ArvinLovegood
c3c770b2ed refactor(app):临时移除屏幕分辨率动态获取
- 注释掉了使用 syscall 动态加载 user32.dll 和 GetSystemMetrics函数的代码
- 固定返回屏幕分辨率为 1366x768
- 解除了 stock_data_api_test.go 中的 db.Init 注释
2025-03-21 20:53:13 +08:00
ArvinLovegood
63a05954f8 feat(app):添加单实例锁和应用拖动/退出优化
- 在 main.go 中添加了 SingleInstanceLock 配置,确保只有一个应用实例运行- 在 App 结构中添加了 OnSecondInstanceLaunch 函数,用于处理第二次启动时的通知
- 优化了应用退出流程,确保 systray 正确退出
- 调整了窗口默认大小和最小宽度
2025-03-21 17:53:32 +08:00
ArvinLovegood
98c81107fc docs(README): 移除 AI 大模型设置说明链接
- 从 README.md 中删除了 AI 大模型设置说明的链接- 保留了公众号二维码和 QQ 交流群链接
2025-03-21 08:54:52 +08:00
ArvinLovegood
fb862564e1 docs(README): 更新功能开发计划和推广链接
- 更新重大功能开发计划,增加ETF支持的当前状态
- 移除AI大模型设置说明和野草云推广链接
2025-03-21 08:54:16 +08:00
ArvinLovegood
c0bad34e36 docs(README): 更新功能开发计划和推广链接
- 更新重大功能开发计划,增加ETF支持的当前状态
- 移除AI大模型设置说明和野草云推广链接
2025-03-21 08:47:42 +08:00
ArvinLovegood
f7a2681157 feat(settings):增加滚动快讯配置选项
- 在 Settings 结构中添加 EnableNews 字段
- 前端增加滚动快讯配置开关
- 后端逻辑中根据配置决定是否显示滚动快讯
2025-03-20 23:16:37 +08:00
ArvinLovegood
ee5c47f2dc refactor(frontend):优化OpenAI设置界面布局和关注股票时的排序逻辑
- 调整了 OpenAI 设置界面的表单项布局,将 span 属性从 22 改为11
- 修改了模型用户 Prompt 输入框的行数,从 2 行调整为 5 行
- 增加了关注股票时的排序逻辑,获取最大排序值并加 1
2025-03-20 14:46:04 +08:00
ArvinLovegood
c28151320c refactor(frontend):优化页面布局和滚动条
- 在 App.vue 中添加全局滚动条样式- 调整 fund.vue 和 stock.vue 中固定元素的位置和宽度
- 在 App.vue 中使用 n-scrollbar 组件包裹 RouterView
2025-03-20 10:03:58 +08:00
ArvinLovegood
e5c4076278 test:注释掉测试函数中的数据库初始化代码
- 在 openai_api_test.go 和 stock_data_api_test.go 文件中
- 注释掉了 TestNewDeepSeekOpenAiConfig 和 TestSearchGuShiTongStockInfo 函数中的 db.Init 调用
- 这可能是为了在不依赖数据库的情况下进行测试,提高测试的独立性和可维护性
2025-03-19 15:54:51 +08:00
ArvinLovegood
8673796919 fix(stock):修复北交所股票AI分析时,股价获取失败的问题
- 在 SearchStockPriceInfo 函数中增加了对北交所股票代码的处理逻辑
- 更新了测试用例,添加了北交所股票的测试
- 调整了前端组件的样式
2025-03-19 14:11:39 +08:00
ArvinLovegood
b4c513a585 feat(frontend):增加已关注股票和基金的快速定位功能,闪烁显示效果
- 在 fund.vue 和 stock.vue 中添加了闪烁边框效果,用于突出显示选中的股票和基金- 实现了滚动到指定元素并添加闪烁效果的 blinkBorder函数
- 在选择股票和基金时调用该函数,以达到视觉提示的效果
- 更新了 CSS 样式,添加了 .blink-border 类以实现闪烁动画
2025-03-17 13:11:36 +08:00
ArvinLovegood
f48aa837a9 fix(stock-data):修复无法取消关注美股的问题
- 在取消关注、设置成本价和体积、设置报警值和排序等操作中,将股票代码转换为小写
- 确保在数据库查询中使用统一的小写股票代码,避免因大小写差异导致的查询错误
2025-03-17 10:36:42 +08:00
ArvinLovegood
e347f6080c refactor:注释掉启动时Edge浏览器检查代码
- 注释掉了检查 Edge 浏览器是否安装的代码块
- 这可能会影响 AI 分析功能的使用
2025-03-15 11:58:09 +08:00
ArvinLovegood
b371555d37 docs(README): 更新 README.md 文件内容
- 新增 2025.03.15 版本更新日志,增加自定义浏览器路径配置功能
- 新增 2025.03.14 版本更新日志,优化编译构建减少程序文件大小
- 调整表格格式,确保对齐正确
2025-03-15 10:25:08 +08:00
ArvinLovegood
4c3fa36d4f feat(settings):添加浏览器路径配置并优化爬虫功能
- 在前端和后端的设置中添加浏览器路径配置项
- 修改爬虫相关函数,使用配置的浏览器路径替代自动检测
- 优化日志输出,统一使用"BrowserPath"字段
- 重构部分代码,提高可维护性
2025-03-15 10:20:26 +08:00
ArvinLovegood
1d4ede336c build:更新wails构建动作版本
- 将 ArvinLovegood/wails-build-action 版本从 v3.0 升级到 v3.4
- 通过升级构建工具版本来提高构建效率和可靠性
2025-03-14 21:29:56 +08:00
ArvinLovegood
740f8ef022 build:更新wails构建动作版本
- 将 ArvinLovegood/wails-build-action 版本从 v3.0 升级到 v3.2
- 通过升级构建工具版本来提高构建效率和可靠性
2025-03-14 18:34:06 +08:00
ArvinLovegood
646420d672 build:更新wails构建动作版本
- 将 ArvinLovegood/wails-build-action 版本从 v3.0 升级到 v3.1
- 通过升级构建工具版本来提高构建效率和可靠性
2025-03-14 18:25:58 +08:00
ArvinLovegood
55f7f246b0 build:更新wails构建动作版本
- 将 ArvinLovegood/wails-build-action 版本从 v2.8 升级到 v2.9
- 此更新可能包含新的功能、修复和性能改进
2025-03-14 18:14:01 +08:00
ArvinLovegood
0b4c9f9ae2 build:更新wails构建动作版本
- 将 ArvinLovegood/wails-build-action 版本从 v2.8 升级到 v2.9
- 此更新可能包含新的功能、修复和性能改进
2025-03-14 18:02:08 +08:00
ArvinLovegood
f6eaa11d65 fix(main):修复美股信息更新逻辑
- 将 return 语句替换为 continue,以修复在发现已有数据时提前退出循环的问题
- 优化了股票信息的更新逻辑,确保在数据库已存在记录时跳过当前迭代
2025-03-14 17:35:02 +08:00
ArvinLovegood
11f0b66360 fix(main):修复美股信息更新逻辑
- 将 return 语句替换为 continue,以修复在发现已有数据时提前退出循环的问题
- 优化了股票信息的更新逻辑,确保在数据库已存在记录时跳过当前迭代
2025-03-14 15:44:04 +08:00
ArvinLovegood
f0e5dbe278 feat(stock):添加时间戳到AI分析结果文件名
- 在 AI 分析结果文件名中加入时间戳,格式为:股票名称[股票代码]-时间ai-analysis-result.md
- 这样可以区分不同时间生成的分析结果,提高文件的可识别性和组织性
2025-03-14 13:57:16 +08:00
ArvinLovegood
e260e3fc71 refactor(frontend):优化基金组件布局和样式
- 调整净值信息展示布局,使用 Flex 布局优化排版
- 替换标签组件为文本组件,提升用户体验
- 调整标签样式,增加间距和对齐方式
- 优化关注和取消关注按钮样式
2025-03-12 18:02:21 +08:00
ArvinLovegood
c64f865216 feat(app):非交易时间不发送推送通知
- 在 SendDingDingMessageByType函数中添加了对 A股、港股和美股非交易时间的判断
- 当股票代码以特定前缀开头且不在交易时间时,返回相应的提示信息
- 优化了对不同市场股票代码的判断逻辑,提高了代码的可读性和可维护性
2025-03-11 21:05:37 +08:00
ArvinLovegood
3217338966 refactor(stock): 注释掉无用的 ticker 相关代码
- 注释掉了 stock.vue 文件中的 ticker 相关代码
- 包括 ticker 的初始化、使用和清除代码
- 此修改可能是为了优化性能或避免不必要的操作
2025-03-11 11:49:29 +08:00
ArvinLovegood
5d6ecdc21b docs(README): 添加基金估值和净值监控查看功能
- 在更新日志中新增"2025.03.09基金估值和净值监控查看"项
- 此更新增加了对基金估值和净值的监控查看功能,有助于用户更好地了解基金表现
2025-03-11 09:06:59 +08:00
ArvinLovegood
6beaf9007c featend(front):添加二维码图片并更新基金模型
- 在 about.vue 组件中添加了二维码图片- 在 models.ts 文件中更新了 FundModel 结构,添加 netEstimatedRate 字段
2025-03-11 08:52:28 +08:00
ArvinLovegood
1925ffda31 featend(front):添加二维码图片并更新基金模型
- 在 about.vue 组件中添加了二维码图片- 在 models.ts 文件中更新了 FundModel 结构,添加 netEstimatedRate 字段
2025-03-11 08:52:15 +08:00
ArvinLovegood
217c4975c4 docs(README): 添加公众号二维码图片
- 在 README.md 文件中插入公众号二维码图片链接
- 二维码图片指向微信公众号,便于用户扫描关注
2025-03-11 08:43:03 +08:00
ArvinLovegood
4dd474c0fb docs(README): 添加 AI 大模型设置说明链接
在 README.md 文件的 "推广链接"部分添加了 AI 大模型设置说明的微信文章链接,用于提供 AI 大模型的相关配置信息。
2025-03-10 23:31:39 +08:00
ArvinLovegood
78150bcecd Merge branch 'master' of https://github.com/ArvinLovegood/go-stock 2025-03-10 22:30:12 +08:00
ArvinLovegood
2e7c9514b4 docs(README): 添加 AI 大模型设置说明链接
在 README.md 文件的 "推广链接"部分添加了 AI 大模型设置说明的微信文章链接,用于提供 AI 大模型的相关配置信息。
2025-03-10 22:23:43 +08:00
sparkmemory
ba862ff586 feat(stock_data):支持北交所股票数据解析
- 在股票数据解析条件中增加了北交所 (bj)
2025-03-10 16:38:25 +08:00
sparkmemory
7d58082525 feat(frontend):更新关于页面并增加社区链接
- 在 about.vue 中添加了项目社区和 QQ交流群的链接
- 调整了投资风险警示的文案,使其更加醒目
2025-03-10 15:58:52 +08:00
sparkmemory
4f96b0a784 feat(fund):增加基金估算净值涨跌幅并优化净值显示
- 在基金数据结构中添加 NetEstimatedRate 字段,用于表示估算净值涨跌幅
- 在获取关注基金列表时,计算并填充 NetEstimatedRate 值- 前端组件中增加估算净值涨跌幅的显示
- 优化单位净值和估算净值的显示格式
2025-03-10 14:59:52 +08:00
ArvinLovegood
db43da6577 feat(fund):添加基金监控和查询功能
- 新增基金数据 API,实现基金信息爬取和数据库操作
- 添加基金监控逻辑,定期更新基金净值信息
- 实现基金列表查询、关注和取消关注功能
- 新增基金相关前端组件,展示基金信息和操作
2025-03-09 19:32:08 +08:00
ArvinLovegood
9a6e210bae feat(fund):添加基金监控和查询功能
- 新增基金数据 API,实现基金信息爬取和数据库操作
- 添加基金监控逻辑,定期更新基金净值信息
- 实现基金列表查询、关注和取消关注功能
- 新增基金相关前端组件,展示基金信息和操作
2025-03-09 16:35:53 +08:00
ArvinLovegood
1b31ff04df docs(README): 添加 QQ 交流群链接
在 README.md 文件中添加了 go-stock 交流群的 QQ 群聊链接,便于用户加入群组进行交流和讨论。
2025-03-07 10:19:02 +08:00
ArvinLovegood
ebdd0d701e docs(README): 添加 QQ 交流群链接
在 README.md 文件中添加了 go-stock 交流群的 QQ 群聊链接,便于用户加入群组进行交流和讨论。
2025-03-07 10:16:29 +08:00
ArvinLovegood
102d6bbcdb docs(README): 添加项目社区分享 2025-03-06 16:37:07 +08:00
ArvinLovegood
74746fc2c2 refactor(frontend):优化股票分享功能的用户体验
- 移除了原有的 message.info 弹窗- 添加了 notify.info 通知,包含自定义的头像、标题和内容
- 通知内容采用左侧对齐和较大的字体
- 通知持续时间延长至 30 秒
2025-03-06 15:49:52 +08:00
ArvinLovegood
6f6884c18a feat(frontend):添加股票分析分享功能
- 在 App.d.ts 中添加 ShareAnalysis 函数声明
- 在 app.go 中实现 ShareAnalysis 方法,用于获取股票分析结果并上传
- 在 App.js 中添加 ShareAnalysis 函数的 JavaScript 调用接口
- 在 stock.vue 中添加分享按钮和相关逻辑,实现股票分析结果的分享功能
2025-03-06 15:27:53 +08:00
ArvinLovegood
ec7534ff2c perf(stock):优化盘前盘后标签显示逻辑
- 修改了盘前盘后标签的显示条件,仅在盘前盘后值大于 0 时显示
- 这个改动可以避免在盘前盘后值为 0时不必要的标签显示,提高界面的可读性和性能
2025-03-05 22:52:31 +08:00
ArvinLovegood
db270779e6 fix(stock-data):修复美股数据解析异常
- 增加日志输出,记录股票数据解析完成后的 parts 长度- 调整昨日收盘价的解析逻辑,兼容不同长度的 parts
- 优化代码结构,提高可读性和可维护性
2025-03-05 18:12:04 +08:00
ArvinLovegood
09ae4c542b docs(README): 更新推广链接描述
- 移除了推广链接中的"免备案"字样,以符合法律法规要求
- 保留了野草云的链接和基本描述,确保信息准确无误
2025-03-05 13:54:29 +08:00
ArvinLovegood
5b1a9c6d4d docs(README): 更新推广链接描述
- 移除了推广链接中的"免备案"字样,以符合法律法规要求
- 保留了野草云的链接和基本描述,确保信息准确无误
2025-03-05 10:45:27 +08:00
ArvinLovegood
1c0596587f docs(README):添加推广链接
- 在 README.md 文件中新增了推广链接部分
- 添加了野草云的推广链接,提供高速、稳定的免备案香港/美国VPS服务器
2025-03-05 10:22:06 +08:00
ArvinLovegood
8e7b7bd4e1 docs(README):添加推广链接
- 在 README.md 文件中新增了推广链接部分
- 添加了野草云的推广链接,提供高速、稳定的免备案香港/美国VPS服务器
2025-03-05 10:19:34 +08:00
ArvinLovegood
48c5f5cc4c docs(README):添加推广链接
- 在 README.md 文件中新增了推广链接部分
- 添加了野草云的推广链接,提供高速、稳定的免备案香港/美国VPS服务器
2025-03-05 10:17:04 +08:00
ArvinLovegood
7417caa778 fix(stock_data):修复股票代码实时数据获取时的异常字符问题
- 在获取昨日收盘价时,使用 strutil.ReplaceWithMap函数去除引号
- 移除了测试代码中不必要的 JSON 序列化和数据库操作
- 在测试函数中添加了新的股票代码参数
2025-03-05 09:21:10 +08:00
ArvinLovegood
0864806770 feat(stock):添加盘前盘后涨跌幅功能
- 在 StockData 结构中添加盘前盘后涨跌幅字段
- 修改 ParseFullSingleStockData 函数,解析盘前盘后涨跌幅数据
- 更新前端模型和组件,显示盘前盘后涨跌幅信息
- 调整弹幕位置,优化界面布局
2025-03-04 23:21:03 +08:00
ArvinLovegood
adcde5efcc feat(app):增加盘前盘后股票数据更新功能
- 新增盘前(4:00 AM到9:30 AM)和盘后(4:00 PM到8:00 PM)的判断逻辑
- 在不同时间段内更新股票数据
- 优化了股票代码的判断条件,区分不同市场的交易时间
- 移除了不必要的日志输出,提高了代码可读性
2025-03-03 21:52:11 +08:00
ArvinLovegood
ce91b2e532 refactor(frontend):重构前端关注列表和美股股票价格更新逻辑
(当前美股功能处于 测试阶段,可能不稳定。)
- 修改 GetFollowList 接口返回类型为 any
- 优化关注列表中美国股票代码处理逻辑
- 增加股票价格更新时的盘前盘后信息
- 调整股票数据解析和处理逻辑
2025-03-01 12:42:00 +08:00
ArvinLovegood
826a29cd8c feat(frontend): 更新软件功能描述和市场支持信息
- 在 about.vue 和 README.md 中添加了对支持的市场范围(A股、港股、美股)的说明
- 提到了未来计划增加基金和 ETF 的支持
- 更新了 AI平台和模型的支持列表
2025-02-28 17:55:04 +08:00
ArvinLovegood
b2b0300aa1 feat(data):增加对美股数据的支持
- 新增 getUSStockPriceInfo 函数用于获取美股实时行情信息
- 修改 SearchStockPriceInfo 函数,支持美股代码查询
- 更新 Tushare 数据接口,增加对美股每日数据的支持
- 优化股票代码处理逻辑,兼容不同市场代码格式
2025-02-28 17:38:48 +08:00
ArvinLovegood
dbc25ca582 feat(README): 更新功能状态和版本日志
- 将美股支持状态从 🚧 修改为 
- 添加 2025.02.28 美股数据支持到更新日志
2025-02-28 16:45:18 +08:00
ArvinLovegood
40a4e58276 chore: 更新 .gitignore 文件
- 移除 frontend/package.json.md5 文件的忽略规则
- 添加 /build/us.json 文件到忽略列表
2025-02-28 16:38:15 +08:00
ArvinLovegood
2c2d689f53 feat(stock):添加美国股票基本信息初始化
- 增加美国股票基本信息的 JSON 文件
- 实现 initStockDataUS 函数用于初始化美国股票数据
- 在主程序中添加美国股票数据初始化的逻辑
2025-02-28 16:35:43 +08:00
ArvinLovegood
fdca30ce3a feat(stock):添加美股数据支持
- 新增 StockInfoUS 模型用于存储美股信息
- 实现 IsUSTradingTime 函数判断美股交易时间
- 修改 MonitorStockPrices 函数以支持美股数据
- 更新前端股票组件以适配美股数据
- 优化后端 API 以支持美股实时数据获取和解析
2025-02-28 16:30:48 +08:00
ArvinLovegood
7b3bad4102 feat(frontend):添加软件更新检查功能
- 在 about.vue 中添加检查更新按钮和相关逻辑
- 在 App.d.ts 和 App.js 中添加 CheckUpdate 函数声明
- 在 app.go 中实现 CheckUpdate 方法,检查 GitHub 上的最新版本- 更新 go.mod 中的依赖版本
2025-02-27 21:23:25 +08:00
Lovegood
531b01bca3
Update issue templates 2025-02-27 14:07:24 +08:00
ArvinLovegood
645c6979a4 docs: 添加 Pull Request 模板
添加了 .github/pull_request_template.md 文件,用于规范 Pull Request 的提交信息。模板包含了以下内容:
- PR 概述
- 相关问题
- 改动内容详细说明(代码修改、新增功能、删除内容)
-测试情况(单元测试、集成测试)
- 注意事项
- 其他补充说明

此模板有助于提高 PR 的质量和可审查性,确保开发者在提交 PR 时提供足够的信息。
2025-02-27 13:59:09 +08:00
ArvinLovegood
5c94b40e4d docs(README): 更新大模型聚合平台信息并添加火山方舟链接
- 在大模型聚合平台部分添加了火山方舟的注册链接
- 更新了硅基流动的注册链接
- 增加了火山方舟的相关描述
2025-02-27 09:07:52 +08:00
ArvinLovegood
83603a12a7 feat(frontend):设置页面添加弹幕功能开关
(今天看见某位朋友在弹幕中说,关掉弹幕。那就如你所愿,你可以自己决定是否显示弹幕了😎)
- 在设置页面添加弹幕功能开关
- 调整数据刷新间隔和启动时更新信息的布局
- 在股票页面实现弹幕功能,根据设置开关控制是否显示弹幕
- 调整应用窗口高度比例
- 优化 OpenAI API 请求时的 URL 处理
2025-02-26 22:19:44 +08:00
ArvinLovegood
2aba86e424 feat(frontend):设置页面添加弹幕功能开关
(今天看见某位朋友在弹幕中说,关掉弹幕。那就如你所愿,你可以自己决定是否显示弹幕了😎)
- 在设置页面添加弹幕功能开关
- 调整数据刷新间隔和启动时更新信息的布局
- 在股票页面实现弹幕功能,根据设置开关控制是否显示弹幕
- 调整应用窗口高度比例
- 优化 OpenAI API 请求时的 URL 处理
2025-02-26 22:17:17 +08:00
ArvinLovegood
8a7e0140eb refactor(gui):调整应用窗口宽度比例
- 将应用窗口宽度从屏幕宽度的2/3 调整为 4/5
- 此修改旨在优化用户界面布局,提供更好的视觉体验
2025-02-25 22:12:27 +08:00
ArvinLovegood
797a35eaa5 feat(stock-data):添加屏幕分辨率适配,动态调整应用窗口大小
- 新增 GetRealTimeStockPriceInfo 函数,用于获取指定股票的实时价格和时间
- 优化爬虫配置,提高数据抓取效率
- 添加屏幕分辨率适配,动态调整应用窗口大小
- 修复部分股票代码格式问题,确保数据准确性
2025-02-25 22:07:33 +08:00
ArvinLovegood
1763435aa1 docs(README): 更新港股支持状态
- 在 README.md 文件中更新了港股支持的状态
- 添加说明,目前港股数据支持有延迟
2025-02-24 12:16:56 +08:00
ArvinLovegood
7952c1fceb feat(hk):更新港股数据支持并添加新功能
- 更新 README.md,添加 ETF 和美股支持计划
- 修改 stock.vue,增加弹幕相关图标和功能
- 更新 stock_data_api.go,添加股票价格时间信息
- 修改 stock_data_api_test.go,更新测试用例
2025-02-24 12:15:42 +08:00
ArvinLovegood
fbb8b00315 feat(app): 增加港股交易时间判断并更新相关功能
- 在 app.go 中添加 IsHKTradingTime 函数,用于判断当前时间是否在港股交易时间内
- 更新股票监控逻辑,使其在港股交易时间也能正常运行
- 在前端 stock 组件中添加股票代码标签,并根据股票代码动态显示货币符号
- 新增 app_test.go 文件,添加 IsHKTradingTime函数的单元测试
2025-02-24 10:12:23 +08:00
ArvinLovegood
2bf7d1e31f docs(README):添加弹幕功能更新日志
- 在 README.md 文件的更新日志部分添加了弹幕功能的记录
-弹幕功能使得盯盘不再孤单,增加了互动性
2025-02-24 09:07:45 +08:00
ArvinLovegood
cb2bc61c6f style(frontend):优化弹幕组件布局和交互
- 在 vue-danmaku 组件中添加 pointer-events: none 样式,确保弹幕不影响事件
-优化股票卡片的鼠标悬停和移出效果
2025-02-23 23:29:54 +08:00
ArvinLovegood
b3f23fc4db feat(frontend):添加弹幕功能并优化股票组件
- 在 stock.vue 中集成 vue3-danmaku 弹幕组件
- 实现 WebSocket 连接以接收实时弹幕消息
- 添加发送弹幕功能
- 优化股票搜索和显示逻辑
- 更新 App.vue 中的导入信息
- 在 package.json 中添加 vue3-danmaku 依赖
2025-02-23 22:02:20 +08:00
ArvinLovegood
67bd9e7996 Merge remote-tracking branch 'origin/master' 2025-02-23 21:58:18 +08:00
ArvinLovegood
4b9ae00452 feat(frontend):添加弹幕功能并优化股票组件
- 在 stock.vue 中集成 vue3-danmaku 弹幕组件
- 实现 WebSocket 连接以接收实时弹幕消息
- 添加发送弹幕功能
- 优化股票搜索和显示逻辑
- 更新 App.vue 中的导入信息
- 在 package.json 中添加 vue3-danmaku 依赖
2025-02-23 21:58:01 +08:00
sparkmemory
4baaefc8c5 fix(backend/data):修复香港股票数据解析并优化日期时间格式
- 在 ParseHKStockData函数中,增加 ";" 字符的分割,以解决数据格式问题- 优化日期和时间的解析,将日期格式从 "/" 改为 "-",时间格式进行相应调整
- 注释掉测试文件中的一个测试调用,可能是为了临时跳过该测试
2025-02-23 18:29:23 +08:00
ArvinLovegood
a6f17c632e feat(stock):添加香港股票数据支持
- 新增 StockInfoHK模型用于存储香港股票基本信息- 实现香港股票数据的爬取和解析功能
- 更新数据库初始化逻辑,支持香港股票数据导入
- 修改股票价格信息获取接口,支持香港股票
- 优化股票数据解析逻辑,适配香港股票数据格式
2025-02-22 21:47:05 +08:00
ArvinLovegood
4c249f0806 feat(wails.json): 更新软件备注信息
- 在 comments 字段中添加了更多支持的 AI 平台和模型信息
- 补充了软件的 GitHub 发行地址
2025-02-21 16:16:51 +08:00
ArvinLovegood
825014e370 fix(stock):修复AI重新检测库存时保留问题文本
- 移除了 aiReCheckStock函数中清除 question 字段的代码行
- 确保在重新检测库存时,之前的问题文本得以保留
2025-02-21 14:45:46 +08:00
ArvinLovegood
c91466a023 fix(stock):修复AI重新检测股票时保留问题文本的bug
- 在 aiReCheckStock函数中添加了清空 question 字段的逻辑
- 确保在重新检测股票时,不会保留上一次的问题文本
2025-02-21 14:38:01 +08:00
ArvinLovegood
92c61e4c26 fix(stock): 修复 AI 重新检测股票时保留问题文本的 bug
- 在 aiReCheckStock函数中添加了清空 question 字段的逻辑
- 确保在重新检测股票时,不会保留上一次的问题文本
2025-02-21 14:37:10 +08:00
ArvinLovegood
b34d2d8d76 docs(README): 更新 Gitee star 徽章链接
将 Gitee star 徽章的图片链接从自定义 URL 更改为官方提供的主题链接,以确保更好的兼容性和准确性。
2025-02-21 12:34:46 +08:00
ArvinLovegood
c287a82211 docs(README): 更新 Gitee star 徽章链接
-将 Gitee star 徽章的链接地址从 Hamm.cn 更改为 Gitee.com
- 优化 README 文档中的徽章展示
2025-02-21 12:08:40 +08:00
ArvinLovegood
1144ac34a7 docs(README): 添加 Gitee Star 徽章
在 README.md 文件中添加了 Gitee平台的 Star 徽章,以增加项目在不同平台上的可见性和互动性。
2025-02-21 12:07:00 +08:00
ArvinLovegood
1b66f0c0d8 docs(README): 添加项目徽章并优化格式
- 在 README.md 中添加了 GitHub Release 和星标徽章
- 优化了文档格式,包括调整图片链接格式和增加空行
2025-02-21 11:53:51 +08:00
ArvinLovegood
e597d3b484 docs: 更新README.md中的功能开发计划
在README.md中新增港股支持功能的状态为🚧,并调整了多轮对话和自定义AI分析提问模板的备注格式,使其更加清晰易读。
2025-02-20 18:05:27 +08:00
spark
cdc4b43925 refactor(data):调整KDays最小值为120天
- 在 openai_api.go 和 settings_api.go 文件中,将 KDays 的最小值从 30 天调整为 120 天
- 这个改动可能会影响到数据爬取和设置的相关功能
2025-02-19 20:49:50 +08:00
spark
0ff14fc01c refactor(data):优化数据处理和格式化
- 修改 OpenAI 消息内容格式,增加日期信息
- 重置股票指数和基本信息的 ID 为 0,以确保正确插入数据库
2025-02-19 20:37:56 +08:00
spark
5ccbbb6bb5 docs(frontend): 更新关于页面信息和使用说明
- 增加支持的 AI 平台和模型列表
- 添加 AI 分析股票结果的免责声明
- 修改联系方式备注说明,提高沟通效率
- 更新商业授权和定制开发的联系方式
- 优化页面布局和内容结构
2025-02-19 13:06:57 +08:00
spark
ec4a8659eb build(frontend):更新项目依赖并添加新功能支持:分析结果导出word文件
- 添加 @types/file-saver、@vavt/cm-extension、@vavt/v3-extension 和 file-saver依赖
- 更新 md-editor-v3 依赖至 5.2.3 版本
- 添加 html-docx-js-typescript 依赖
2025-02-19 12:23:55 +08:00
spark
34ac6755a9 refactor(backend):重构数据处理和前端AI分析结果展示
- 新增 Resp 结构体用于统一响应格式- 优化 OpenAI API 流数据处理逻辑,解析并展示具体错误信息
- 更新前端 stock组件,改进 AI 分析结果的接收和展示
- 调整代码格式,提高可读性
2025-02-18 14:19:40 +08:00
spark
e21ba1b800 feat(frontend/backend):添加日K线数据天数设置功能
- 在前端设置页面添加日 K 线数据天数配置选项
- 在后端 OpenAI 配置中添加 KDays 字段
- 调整股票数据分析时的历史数据时间范围
2025-02-18 12:32:34 +08:00
spark
17a234f679 修复敏感词问题导致deepseek无法分析 2025-02-17 21:53:50 +08:00
spark
d504dc6d13 docs(frontend): 更新长期技术支持描述
- 在 about.vue 和 README.md 中将"长期技术支持(不限次数,新功能优先体验)"修改为"长期技术支持(不限次数,新功能优先体验等)"
- 此修改旨在更准确地描述长期技术支持的内容,增加"等"字暗示可能还有其他权益
2025-02-17 17:57:44 +08:00
spark
0b749d1699 docs(frontend): 更新技术支持方式和费用
- 移除了邮件支持方式
- 更新了长期技术支持的描述和费用
- 统一了前端组件和 README 中的技术支持信息
2025-02-17 17:55:49 +08:00
spark
4e9a24c8f2 feat(README): 更新项目功能描述
- 在 README.md 中增加了对市场整体和个股情绪分析、K线技术指标分析等功能的描述
2025-02-17 17:39:19 +08:00
spark
c81b1a730d feat(data):添加tushare数据接口并优化股票代码转换功能(设置好提问模板后可进行K线分析功能)
- 新增 TushareApi 结构体和 GetDaily 方法,用于获取 A 股日线行情数据
- 在 openai_api.go 中添加获取股票日 K线数据的协程
- 在 utils.go 中添加股票代码与 tushare 代码相互转换的函数
- 更新相关测试文件以支持新功能
2025-02-17 17:33:17 +08:00
spark
8d3cd7b151 feat:完成多轮对话功能开发
- 将多轮对话功能的状态从 🚧 更新为 
- 在更新日志中添加 2025.02.16 版本信息,说明 AI 分析后可继续对话提问
- 新增 v2025.2.16.1-alpha 版本号
2025-02-17 12:13:16 +08:00
spark
5ee1ae4a32 feat(frontend):优化AI聊天功能并添加新功能
- 新增用户自定义问题输入功能
- 优化 AI回答的展示逻辑
- 添加错误处理和提示
- 更新后端接口以支持新功能
2025-02-16 21:56:07 +08:00
spark
dab51f7a70 feat(backend):添加持仓成本价(costPrice)变量到用户提问问题模板
- 在 openai_api.go 文件中,增加了对持仓成本价的处理
- 通过查询数据库获取股票的持仓成本价,并加入到替换模板中
- 更新了问题模板的替换逻辑,支持新的成本价变量
2025-02-15 15:30:46 +08:00
spark
a20d4e721d feat(data):优化数据处理和模型结果展示(ps:今天白天太忙了,更新内容较少)
- 修改文本处理方法,提高消息内容的可读性
- 在 AIResponseResult模型中添加 modelName 字段
- 更新前端组件,展示模型名称信息
- 优化数据库查询,提高响应速度
2025-02-14 22:39:57 +08:00
spark
f4da21d645 feat(backend):添加股市通资讯爬取功能
- 新增 SearchGuShiTongStockInfo函数,用于爬取百度股市通的股票资讯
- 修改 OpenAI_API 函数,增加股市通资讯的爬取
- 添加 RemoveAllNonDigitChar 函数,用于去除所有非数字字符
2025-02-13 17:56:22 +08:00
spark
fc37440f6b docs(frontend):更新关于页面中的技术支持说明
- 在 about.vue 文件中,增加了向开源社区请求帮助的建议
- 修改了对一对一技术支持的描述,强调在确实需要时再进行赞助和联系
2025-02-13 14:59:40 +08:00
spark
d7b47a7010 docs(frontend): 更新关于页面的联系方式说明
- 在 about.vue 文件中,增加了对添加微信或 QQ 时的备注说明
- 添加了技术支持的链接,提高了可点击性
- 优化了页面布局,增加了视觉区分度
2025-02-13 14:56:37 +08:00
spark
85d71ae58e docs: 更新 issue 模板 2025-02-13 14:40:59 +08:00
spark
23dc25f642 docs: 更新 Bug 报告模板为中文版本
- 将 Bug 报告模板的英文内容翻译为中文
- 优化模板结构,增加更多详细分类和说明
- 添加复现步骤、频率、错误日志等新字段
- 引入截图或视频上传建议
- 增加可能的原因分析和补充说明部分
2025-02-13 14:34:13 +08:00
spark
467bbd8923 refactor(data):重构数据爬取功能
- 新增 CrawlerApi 结构体和相关方法,实现通用的爬虫功能
- 优化了 openai_api 和 stock_data_api 中的爬虫逻辑
- 添加了 RemoveAllBlankChar函数,用于移除字符串中的空白字符
- 更新了前端 stock组件中的警告提示
2025-02-13 14:16:56 +08:00
spark
6be5c0fa05 refactor(stock_data):优化股票信息搜索功能
- 修改 SearchStockInfo 函数,增加对不同消息类型的处理
- 更新页面等待逻辑,根据消息类型选择不同的选择器
- 调整测试函数,增加时间参数
2025-02-12 22:48:17 +08:00
spark
4fac915778 refactor(stock_data):优化股票信息搜索功能
- 修改 SearchStockInfo 函数,增加对不同消息类型的处理
- 更新页面等待逻辑,根据消息类型选择不同的选择器
- 调整测试函数,增加时间参数
2025-02-12 21:52:27 +08:00
spark
d27bcbd334 feat(backend):添加资讯采集超时设置并优化相关功能
- 在 OpenAi 结构中添加 CrawlTimeOut 字段,用于设置资讯采集超时时间
- 修改相关函数以支持新的超时设置,包括 GetFinancialReports、GetTelegraphList、GetTopNewsList等
- 在前端设置页面添加 Crawler Timeout 设置项
- 优化浏览器检查逻辑,优先检查 Chrome 浏览器
2025-02-12 17:03:25 +08:00
spark
d46872ffbd docs(README):可配置的提问模板
- 更新可配置提问模板的版本号为 v2025.2.12.5-alpha
- 调整更新日志和功能开发计划的格式
- 更新功能开发计划中的状态标识
2025-02-12 15:23:28 +08:00
spark
29da37739d docs(README): 可配置的提问模板,更新功能开发计划和版本发布说明
- 更新可配置提问模板的版本号为 v2025.2.12.5-alpha
- 调整更新日志和功能开发计划的格式
- 更新功能开发计划中的状态标识
2025-02-12 15:14:44 +08:00
spark
d7584bc4de docs(README): 更新功能开发计划和发布新版本
- 新增可配置的提问模板功能
- 更新功能开发计划,将自定义AI分析提问模板状态标记为已完成
- 调整不再强制依赖Chrome浏览器状态为已完成
- 优化README结构,增加更新日志部分
2025-02-12 14:56:27 +08:00
spark
1f78cc3589 feat(frontend/backend):增加自定义用户提问模板功能
- 在 Settings 模型中添加 questionTemplate 字段
- 在 OpenAi 结构体中添加 QuestionTemplate 字段
- 更新前端设置组件,增加用户 prompt 配置选项
- 修改后端 API调用,支持使用自定义用户 prompt
2025-02-12 14:47:50 +08:00
spark
a3b718c149 refactor(frontend):移除AI分析结果时间的判断逻辑
移除了 stock.vue 组件中 AI 分析结果下方提示文字的时间判断逻辑。现在,无论是否有分析时间,提示文字将始终显示,以确保用户在任何情况下都能看到投资风险提示。
2025-02-12 12:57:02 +08:00
spark
37e63538e2 refactor(data):添加chromedriver路径日志输出
- 在 GetFinancialReports、SearchStockPriceInfo 和 SearchStockInfo 函数中添加了 chromedriver 路径的日志输出
- 有助于调试和验证 chromedriver 的正确路径,确保自动化任务顺利进行
2025-02-12 12:53:15 +08:00
spark
70ee9df22a fix(data):优化Edge浏览器调用逻辑修复AI分析使用BUG
- 在 Windows 系统上动态检查 Edge 浏览器安装情况
- 根据系统环境选择合适的 Edge 可执行文件路径
- 优化了 openai_api 和 stock_data_api 中的 Edge 调用逻辑
2025-02-12 12:17:51 +08:00
spark
b764c978f1 docs(README): 添加关于版权和技术支持的声明
- 增加了本软件基于开源技术构建的说明
- 提供了技术支持的联系方式和赞助费用
- 添加了长期技术支持的选项和费用说明
2025-02-12 11:41:06 +08:00
spark
fc8dbb919c docs(README): 添加关于版权和技术支持的声明
- 增加了本软件基于开源技术构建的说明
- 提供了技术支持的联系方式和赞助费用
- 添加了长期技术支持的选项和费用说明
2025-02-12 11:26:18 +08:00
spark
02e3d1df11 feat(frontend):添加关于版权和技术支持申明
- 在关于页面中增加了关于版权和技术支持的申明内容
- 添加了技术支持的联系方式和赞助费用说明
-优化了页面布局,移除了不必要的组件嵌套
2025-02-12 11:14:33 +08:00
spark
e074ab2c39 feat(frontend): 添加支付宝和微信支付二维码
- 在 about.vue 中添加了支付宝和微信支付的二维码图片
- 在 VersionInfo 模型中增加了 Alipay 和 Wxpay 字段
- 更新了后端和前端的相关代码,支持支付二维码的获取和显示
- 在 stock.vue 中添加了 AI 分析结果的免责声明
2025-02-12 10:51:24 +08:00
spark
e2e5a063e7 docs(README): 添加 AnythingLLM 到大模型平台列表
在 README.md 文件中的大模型平台对比表格中添加了 AnythingLLM。
2025-02-12 10:17:04 +08:00
spark
c4c2bea73d docs: 更新 README.md 中的项目描述
- 移除了自选股行情实时监控的描述
-简化了项目介绍,突出了基于 Wails 和 NaiveUI 构建的 AI 赋能股票分析工具的核心信息
2025-02-12 09:51:29 +08:00
spark
bfd9515387 docs: 更新 README.md 标题
在 README.md 文件中,为项目名称 "go-stock" 添加了前缀,使其更加显眼。这一修改旨在提高项目名称的可见性,以便于访客快速识别项目的主题。
2025-02-12 09:50:20 +08:00
spark
7029d75790 docs(README): 更新功能开发表格中的描述
- 将"自定义提问模板"改为"自定义AI分析提问模板"
- 将"可配置的提问"改为"可配置的提问模板"
2025-02-12 09:41:39 +08:00
spark
13d1c75b76 feat: 不再强制安装Chrome浏览器
- 默认使用Edge浏览器抓取新闻资讯
- 提高了系统的灵活性和用户体验
2025-02-12 09:40:34 +08:00
spark
aaf53f651a docs: 修改功能开发标题
- 将"计划开发"更改为"功能开发",以更准确地描述功能列表的性质
- 优化文档结构,提高可读性和清晰度
2025-02-12 09:38:43 +08:00
spark
dad9ece712 docs(README): 添加计划开发功能列表
- 在 README.md 中新增了"计划开发"章节
- 增加了自定义提问模板和多轮对话两个即将开发的功能
- 为每个功能添加了状态和备注信息
2025-02-12 09:37:59 +08:00
spark
6e17e89961 docs(README): 添加计划开发功能列表
- 在 README.md 中新增了"计划开发"章节
- 增加了自定义提问模板和多轮对话两个即将开发的功能
- 为每个功能添加了状态和备注信息
2025-02-12 09:36:09 +08:00
spark
fae5a5fb6a docs(README): 更新项目文档和截图
- 添加项目简介、大模型支持情况等新内容
- 更新功能截图
- 调整文档结构,优化标题层级- 移除部分冗余信息,提高可读性
2025-02-12 09:29:39 +08:00
spark
96f2898111 docs: 更新 README 状态部分,添加 Star History 图表
- 在 README.md 文件的"状态"部分,添加了 Star History 图表的嵌入代码
- 该图表展示了项目星星随时间的增长情况,为潜在贡献者提供了项目热度的可视化信息
2025-02-12 08:50:24 +08:00
spark
e24965393b feat(browser):使用Edge替代Chrome执行AI分析时的依赖
- 新增 checkEdgeOnWindows 函数以检查 Edge 浏览器安装情况
- 修改 AI 分析相关功能,使用 Edge 浏览器代替 Chrome
- 更新相关日志和错误处理
2025-02-11 21:33:11 +08:00
spark
7e5d135483 feat(stock):保存分析结果为Markdown文件
- 新增 saveAsMarkdown 函数,用于将分析结果保存为 Markdown 文件
- 更新 saveAsImage 函数,添加股票名称和代码到文件名
- 在股票分析结果页面添加保存为 Markdown 文件的按钮
2025-02-11 17:59:50 +08:00
spark
c8827da35a feat(frontend):添加AI分析结果保存和复制功能
- 在 stock.vue 组件中添加了保存分析结果为图片和复制到剪切板的功能
- 引入了 html2canvas 库用于截图
- 优化了 AI 分析结果弹窗的布局
2025-02-11 17:53:06 +08:00
spark
268bc8b1f6 docs(README):添加Tushare数据社区注册链接
- 在 README.md 中添加了 Tushare大数据开放社区的注册链接- Tushare 提供免费的各类金融数据,助力行业和量化研究
2025-02-11 17:25:23 +08:00
spark
2869b37053 docs(README):添加Tushare数据社区注册链接
- 在 README.md 中添加了 Tushare大数据开放社区的注册链接- Tushare 提供免费的各类金融数据,助力行业和量化研究
2025-02-11 17:23:18 +08:00
spark
b237341fda docs: 添加项目状态图表
- 在 README.md 中插入 Repobeats analytics 图像,展示项目活动状态
2025-02-11 15:23:54 +08:00
spark
957de8ad8b feat(backend):添加获取新闻资讯功能
- 新增 GetTopNewsList 函数,用于获取新闻资讯
- 在处理用户消息时,添加获取新闻资讯的逻辑
- 当获取新闻资讯失败时,发送警告消息
2025-02-11 15:10:29 +08:00
spark
e622b7d86e refactor(frontend):注释掉错误消息弹窗
- 在 settings.vue 文件中注释掉了错误消息弹窗的代码行
-这个修改可能会影响错误处理的用户界面展示
2025-02-11 13:56:39 +08:00
spark
95f9f1840f refactor(data):重构OpenAi结构体并添加上下文对象
- 在 OpenAi 结构体中添加 ctx 字段,用于传递上下文对象
- 更新 NewDeepSeekOpenAi 函数签名,现在需要传入 context.Context 参数
- 在获取股票信息失败时,除了在控制台输出错误信息外,还通过 runtime.EventsEmit 发送警告消息到前端
- 优化错误信息的显示格式,添加警告图标
2025-02-11 13:34:14 +08:00
spark
267f6f638f refactor(data):重构OpenAi结构体并添加上下文对象
- 在 OpenAi 结构体中添加 ctx 字段,用于传递上下文对象
- 更新 NewDeepSeekOpenAi 函数签名,现在需要传入 context.Context 参数
- 在获取股票信息失败时,除了在控制台输出错误信息外,还通过 runtime.EventsEmit 发送警告消息到前端
- 优化错误信息的显示格式,添加警告图标
2025-02-11 12:52:32 +08:00
spark
b459abb35d feat(data):为数据获取失败时添加错误反馈并设置超时
- 在获取股票价格、财报、市场资讯、股票资讯和电报资讯失败时,向用户发送错误信息
- 为 GetFinancialReports、SearchStockPriceInfo 和 SearchStockInfo 函数添加 30 秒超时设置
2025-02-11 12:29:49 +08:00
spark
d79bdc8bc1 feat(frontend):更新页面标题和优化错误处理
- 更新页面标题为 "go-stock:AI赋能股票分析"
- 改进全局错误处理,增加错误信息的控制台输出
- 优化设置组件中的错误提示和表单重置逻辑
2025-02-11 09:34:40 +08:00
spark
863e88c579 refactor(frontend):优化错误捕获和日志记录功能
- 修改 App.vue、settings.vue 和 stock.vue 中的 window.onerror 函数,增加页面标识和友好的错误提示
- 优化 openai_api.go 中的错误捕获,增加详细的日志记录
- 统一错误消息参数,提高错误信息的准确性和可读性
2025-02-10 18:06:49 +08:00
spark
853f6b180e refactor(frontend):优化错误捕获和日志记录功能
- 修改 App.vue、settings.vue 和 stock.vue 中的 window.onerror 函数,增加页面标识和友好的错误提示
- 优化 openai_api.go 中的错误捕获,增加详细的日志记录
- 统一错误消息参数,提高错误信息的准确性和可读性
2025-02-10 18:06:31 +08:00
spark
b4c55ce233 feat(frontend):增加前端错误捕获和后端panic处理
- 在前端 App.vue、settings.vue 和 stock.vue 中添加 window.onerror 事件处理器,捕获前端错误并发送给后端
- 在后端 app.go 和 openai_api.go 中添加 panic 处理逻辑,捕获并记录 panic错误
- 在 main.go 中添加 PanicHandler 函数,用于捕获和处理全局 panic
2025-02-10 17:46:42 +08:00
spark
22111411c3 ci:更新GitHubActions工作流
- 修改了工作流中的构建步骤名称,从 "Build wails" 改为 "Build wails x go-stock"
- 这个更改更准确地描述了构建过程,即使用 wails 构建 go-stock 项目
2025-02-10 14:12:38 +08:00
spark
ddfc7c1216 feat(app):添加谷歌浏览器检查并发送警告消息
- 在应用启动时检查谷歌浏览器是否安装
- 如果未安装,发送警告消息提醒用户
- 新增 checkChromeOnWindows 函数用于检查浏览器
- 在前端添加警告消息的事件监听
2025-02-10 13:59:23 +08:00
spark
908086c0c0 feat(app):添加谷歌浏览器检查并发送警告消息
- 在应用启动时检查谷歌浏览器是否安装
- 如果未安装,发送警告消息提醒用户
- 新增 checkChromeOnWindows 函数用于检查浏览器
- 在前端添加警告消息的事件监听
2025-02-10 13:50:51 +08:00
spark
2c0e2ec698 build(frontend): 升级Vite 版本
- 将 Vite 版本从 5.4.6 升级到 5.4.12- 更新 package.json 和 package-lock.json 中的 Vite 依赖
- 更新 package.json.md5 校验值
2025-02-10 13:25:12 +08:00
spark
cb4690bf88 docs(README): 添加软件下载安装说明
- 在 README.md 中新增了下载安装部分
- 提供了安装版和绿色版两种版本的下载链接
- 优化了文档结构,方便用户快速找到下载信息
2025-02-10 13:16:35 +08:00
spark
100fb3e2a9 docs(README): 添加软件下载安装说明
- 在 README.md 中新增了下载安装部分
- 提供了安装版和绿色版两种版本的下载链接
- 优化了文档结构,方便用户快速找到下载信息
2025-02-10 13:14:34 +08:00
Lovegood
1bb13866bc
Update README.md 2025-02-10 13:03:08 +08:00
spark
05aaf82849 docs: 更新 README 中的 star 请求样式
- 在 README.md 文件中,将"请给我一个 star"的部分添加了颜色样式
- 使用 span 标签为文本添加蓝色背景和红色"star"字- 优化了视觉效果,使请求更醒目
2025-02-10 13:00:55 +08:00
spark
255a214554 docs(README):添加项目星标请求
在 README.md 文件开头加入了一行文本,请求感兴趣的用户给项目星标。这有助于增加项目的 visibility 和吸引更多的贡献者。
2025-02-10 12:50:58 +08:00
spark
5d0de949a8 docs(README):添加项目星标请求
在 README.md 文件开头加入了一行文本,请求感兴趣的用户给项目星标。这有助于增加项目的 visibility 和吸引更多的贡献者。
2025-02-10 12:49:38 +08:00
Lovegood
03229fa46b
Create SECURITY.md 2025-02-10 12:29:51 +08:00
spark
b4cdd2d28b docs:更新行为准则的中文翻译版本
- 将中文翻译版本从代码文件中移除
- 在文件顶部添加完整的中文翻译内容
- 更新文件结构,使中文版本更加突出和易于阅读
2025-02-10 12:17:56 +08:00
spark
ef838af18b feat(frontend):设zhi默认prompt
- 在 settings.vue 中添加了 AI 投资顾问的默认角色设定
- 设定了 AI 投资顾问的核心能力和服务范围
- 定义了 AI 投资顾问的交互风格和表达方式- 优化了 prompt 的默认值,确保 AI 生成的内容符合预期
2025-02-10 12:16:54 +08:00
Lovegood
62defa1ebc
Update LICENSE 2025-02-10 12:12:26 +08:00
Lovegood
3dd9790015
Create CONTRIBUTING.md 2025-02-10 12:07:35 +08:00
Lovegood
c25304d28b
Create CODE_OF_CONDUCT.md 2025-02-10 12:00:54 +08:00
spark
222ba03841 docs(README):更新设置界面截图
- 将 README.md 中的 img_12.png 图片引用从旧路径修改为新路径- 旧路径: build/screenshot/img_12.png
- 新路径: build/screenshot/img_4.png
2025-02-10 10:01:23 +08:00
spark
3f73a5a521 feat(frontend):添加设置导出导入功能
- 在 App.d.ts 中添加 ExportConfig 函数声明
- 在 app.go 中实现 ExportConfig 方法,用于导出配置文件
- 在 App.js 中添加 ExportConfig 函数的 JavaScript 调用接口
- 在 settings.vue 中添加导出和导入配置的功能按钮,并实现相关逻辑
- 在 settings_api.go 中添加 Export 方法,用于生成配置文件的 JSON 字符串
2025-02-10 09:47:10 +08:00
spark
3a3e0b0543 feat(frontend):添加设置导出导入功能
- 在 App.d.ts 中添加 ExportConfig 函数声明
- 在 app.go 中实现 ExportConfig 方法,用于导出配置文件
- 在 App.js 中添加 ExportConfig 函数的 JavaScript 调用接口
- 在 settings.vue 中添加导出和导入配置的功能按钮,并实现相关逻辑
- 在 settings_api.go 中添加 Export 方法,用于生成配置文件的 JSON 字符串
2025-02-10 09:45:51 +08:00
spark
0006501cc8 fix(data):优化数据获取流程并添加错误日志
- 在获取股票价格、财报、市场资讯等数据时,增加了空值判断并记录错误日志
-优化了数据获取流程,提高了代码的健壮性和可维护性- 在 chromedp 上下文中添加了日志记录,便于调试和排查问题
2025-02-09 20:58:45 +08:00
spark
c5fbe5fdae Merge branch 'master' of https://github.com/ArvinLovegood/go-stock 2025-02-09 20:18:42 +08:00
spark
66d85cf0a2 fix(backend):修复chromedp未取消导致的资源泄漏问题
- 在 openai_api.go 和 stock_data_api.go 中添加了对 chromedp.Cancel 的调用
- 确保在请求完成后正确取消 chromedp 的执行上下文,释放资源
2025-02-09 20:18:05 +08:00
spark
24145894b6 refactor(app):优化GetStockInfos函数,避免闪退
- 移除错误处理,因为调用方可能不需要错误信息
- 调整变量初始化顺序,提高代码可读性
- 简化错误处理逻辑,忽略错误并返回空值
2025-02-09 19:20:46 +08:00
Lovegood
75f680a298
Update issue templates 2025-02-09 17:16:59 +08:00
spark
2658f207dc feat(frontend):使用内嵌应用图标
- 使用内嵌应用图标替换URL图标
- 添加 GetVersionInfo 函数调用,用于获取版本信息
2025-02-09 16:26:20 +08:00
spark
626f99f0d1 refactor(frontend):重构关于页面布局
- 使用 n-divider组件替代 h1 标题,提高页面美观度
- 移除多余的 n-card 嵌套,简化页面结构
- 注释掉多余的 h1 标题,优化代码可读性
2025-02-09 16:17:12 +08:00
spark
4d541e81a2 feat(frontend):添加鸣谢部分
- 增加了捐赠者、开发者和开源项目的鸣谢列表
- 优化了关于页面的布局,使鸣谢内容更加突出
- 添加了外部链接,方便用户访问相关开源项目
2025-02-09 16:04:45 +08:00
spark
6dfe3fd135 feat(frontend):添加鸣谢部分
- 增加了捐赠者、开发者和开源项目的鸣谢列表
- 优化了关于页面的布局,使鸣谢内容更加突出
- 添加了外部链接,方便用户访问相关开源项目
2025-02-09 16:01:47 +08:00
spark
915e12eab3 feat(frontend):丰富关于页面内容并优化布局
- 在 GitHub 链接旁边添加 Issues 和 Releases 链接
- 在邮箱下方添加 QQ 和微信联系方式- 使用 n-divider 组件进行垂直分割,提高可读性
2025-02-09 15:40:04 +08:00
spark
bcfcbfeef0 fix(stock):优化股票关注功能
- 增加股票代码有效性验证
- 改进关注失败时的错误处理和用户提示
- 修复可能的 nil pointer dereference 问题
2025-02-09 15:29:04 +08:00
spark
1dc731de1e refactor(frontend):重构关于页面并添加作者信息
- 更新了 about.vue 页面布局和内容- 添加了作者信息和邮箱链接
- 移除了更新说明部分
- 调整了软件描述的样式和内容
2025-02-08 17:50:13 +08:00
spark
a580f9254a refactor(frontend):重构关于页面并添加作者信息
- 更新了 about.vue 页面布局和内容- 添加了作者信息和邮箱链接
- 移除了更新说明部分
- 调整了软件描述的样式和内容
2025-02-08 17:44:39 +08:00
spark
9b080bbb45 refactor(frontend):重构关于页面并添加作者信息
- 更新了 about.vue 页面布局和内容- 添加了作者信息和邮箱链接
- 移除了更新说明部分
- 调整了软件描述的样式和内容
2025-02-08 17:26:10 +08:00
spark
86183f4585 build:更新Wails构建动作版本 2025-02-08 16:48:12 +08:00
spark
97ab29259a ci:精简 commit message 输出
- 修改了获取 commit message 的 git 命令,仅保留 commit 主题行
-移除了不必要的信息,如作者和日期
- 优化了输出格式,提高了可读性
2025-02-08 16:30:54 +08:00
spark
91f3e66239 ci: 更新获取 commit message 的命令
- 修复了获取 commit message 时的语法错误
- 使用 PowerShell 兼容的命令格式
2025-02-08 16:06:59 +08:00
Lovegood
713b25d2db
Update main.yml 2025-02-08 15:46:58 +08:00
spark
d0b65e7063 ci: 更新获取 commit message 的命令
- 修复了获取 commit message 时的语法错误
- 使用 PowerShell 兼容的命令格式
2025-02-08 15:34:40 +08:00
spark
f062306158 build: 更新 Wails 构建动作版本
- 将 ArvinLovegood/wails-build-action 版本从 v2.5 升级到 v2.6
- 保持其他配置不变,仅更新动作版本
2025-02-08 15:25:35 +08:00
spark
ae7b617e83 feat(frontend): 添加关于软件页面并实现版本信息动态获取
- 新增 about.vue 组件,包含软件介绍、更新说明和作者信息
- 添加 GetVersionInfo 函数,用于获取版本信息
- 在 App.vue 中添加关于软件的菜单项
- 在 router.js 中添加关于软件的路由
- 优化页面布局和样式
2025-02-08 15:05:52 +08:00
spark
1035f2a800 feat(frontend): 添加关于软件页面
- 在 App.vue 中添加关于软件的菜单项
- 在 router.js 中添加关于软件的路由- 新增 about.vue 组件,包含软件介绍和作者信息
2025-02-08 12:20:40 +08:00
spark
cb28b18541 docs(README): 更新 AI 分析股票功能截图
- 将 AI 分析股票功能的截图从 img_10.png 修改为 img.png
- 更新 README.md 中的相关图片引用
2025-02-08 11:39:39 +08:00
spark
9d42eb2729 docs(README): 更新项目介绍和赞助信息 2025-02-08 11:24:56 +08:00
spark
7b93d4d8ca feat(data): 添加 AIResponseResult模型并实现相关功能
感谢 @gnim2600 的建议!

- 新增 AIResponseResult 模型用于保存 AI 分析结果
- 实现 SaveAIResponseResult 和 GetAIResponseResult 函数
- 在前端添加 AI 分析功能,包括保存和获取分析结果
-优化 AI 分析界面,增加分析时间显示和再次分析按钮
2025-02-08 11:13:17 +08:00
spark
3e13ef007b feat(openai): 添加 OpenAI API 超时设置并调整相关功能
感谢@gnim2600 @XXXiaohuayanGGG 两位提供的帮助和建议
- 在前端和后端添加 OpenAI API 超时设置选项
- 更新 AI 诊断股票功能,支持自定义超时时间
- 优化设置界面布局,提高用户体验
- 为 AI 分析结果添加居中显示样式
2025-02-08 09:14:09 +08:00
spark
6ff1b68f1b fix:修复 GitHub 时间转换错误
- 移除了 getTimezoneOffset() * 60 * 1000 的计算
-现在直接使用 utcDate.getTime() 获取时间戳
2025-02-07 11:21:05 +08:00
spark
a6547db195 docs(README): 添加版本信息提示截图
- 在 README.md 中新增了版本信息提示部分
- 添加了对应的截图 img_11.png
2025-02-07 11:03:01 +08:00
spark
567414a136 feat(update): 增加新版本详细信息和发布时间
- 获取并显示新版本的 Tag 和 Commit 信息
- 将 UTC 时间转换为本地时间并显示
- 在通知中添加新版本详细信息和发布时间
- 优化股票卡片样式,增加鼠标悬停效果
2025-02-07 10:49:55 +08:00
Lovegood
34dc38a95f
Merge pull request #3 from 2lovecode/feature-support-macos
feat(macos):support macos
2025-02-06 20:12:38 +08:00
2lovecode
6d2ab3ef41 feat(macos):support macos 2025-02-06 18:03:06 +08:00
spark
e55506705e feat(update): 添加软件更新检查功能
- 在应用启动时检查 GitHub 上的最新版本
- 如果发现新版本,通过通知提示用户更新
- 新增 GitHubReleaseVersion模型用于解析版本信息
- 在前端添加更新通知的展示逻辑
2025-02-06 16:19:11 +08:00
spark
322e87efbd ci:为 GitHub Actions 添加 build-tags 参数
在 GitHub Actions 的构建配置中添加了 build-tags 参数,使其等于当前的引用名称(github.ref_name)。这允许我们在构建过程中使用特定的标记。

- 修改了 .github/workflows/main.yml 文件- 在 build-platform 部分添加了 build-tags 参数
- 参数值设置为当前引用名称,增加了构建的灵活性和可追溯性
2025-02-06 15:01:36 +08:00
spark
1628381295 feat(app): 添加版本信息,为更新推送做准备
- 在应用启动时打印版本号
2025-02-06 14:53:07 +08:00
spark
8afc26badb test:移除雪球和硅流 API 调用相关代码 2025-02-05 16:44:00 +08:00
spark
d5db2ef879 feat(backend): 添加获取财务报告功能并优化聊天流
- 新增 GetFinancialReports 函数,用于抓取股票财务报告信息
- 优化 NewChatStream 函数,增加财务报告信息到聊天流中
- 更新测试用例,使用北京文化(sz000802)作为示例股票- 添加 TestGetFinancialReports 和 TestXUEQIU 测试函数
2025-02-05 16:25:24 +08:00
spark
509cd2dbca refactor(backend): 调整 openai_api.go 中的资源关闭逻辑
- 将 resp.RawBody().Close() 调用移动到 if err != nil块之后
- 确保在发生错误时也能正确关闭网络连接
- 优化了代码结构,提高了资源管理的可靠性
2025-02-04 20:02:21 +08:00
spark
3de2ad3cdc refactor(backend): 重构 OpenAI 和股票数据 API
-优化了 OpenAI API 的调用逻辑,提高了错误处理和数据处理的能力
- 改进了股票数据 API 的数据抓取和处理方式
- 移除了测试代码中冗余的部分,提高了代码可读性和维护性
2025-02-04 19:45:22 +08:00
spark
b00bddcdec refactor(stock-data): 重构股票数据获取逻辑
- 移除了不必要的并发请求,简化了代码结构
- 新增 FetchPrice 函数,用于获取股票价格信息
- 优化 SearchStockInfo 函数,提高了搜索效率和准确性
- 新增 SearchStockInfoByCode 函数,用于根据股票代码获取相关信息- 修复了一些潜在的错误和性能问题
2025-02-04 18:12:08 +08:00
spark
64b37b687c refactor(data): 优化 OpenAI API 客户端配置并改进流数据处理
- 将请求超时时间从 30秒增加到 60 秒
- 修正流数据的前缀检查,从 "chat data: " 改为 "data: "- 增加对 reasoning_content 的处理逻辑
- 优化数据处理流程,提高错误处理能力
2025-02-04 15:12:15 +08:00
spark
e81319bb4f docs(README): 添加赞助信息在 README.md 中添加了赞助信息部分,提供了支付宝和微信支付的二维码图片链接,鼓励对项目有帮助的用户进行赞助。 2025-02-04 07:31:44 +08:00
spark
7bc219d1a5 refactor(frontend): 优化 OpenAI 设置界面文案
- 将"自定义Prompt"标签修改为"自定义系统Prompt"
- 更新输入框占位符为"请输入系统prompt"
2025-02-03 22:03:16 +08:00
spark
0f2f58e6b8 docs: 更新 README 中的设置截图
- 将 README.md 中的 img_11.png 替换为 img_12.png
- 优化设置界面的视觉效果
2025-02-03 21:53:31 +08:00
spark
2dc0b95b45 docs: 更新 README 中的设置截图
- 将 README.md 中的 img_11.png 替换为 img_12.png
- 优化设置界面的视觉效果
2025-02-03 14:01:20 +08:00
spark
869eced99e refactor(backend): 优化 API 客户端配置并调整日志输出
- 为 OpenAI API 客户端添加重试次数和超时设置
- 修改 OpenAI API 客户端初始化,设置基础 URL
- 优化 OpenAI API 响应数据的处理逻辑
- 为 stock_data API 客户端添加重试次数设置
- 在 stock_data API 中添加日志和错误处理
2025-02-03 13:50:13 +08:00
spark
f5aa70bf61 feat(main): 调整窗口最大宽度和高度并启用默认上下文菜单
- 将窗口最大宽度从 1280调整为 1920
- 启用默认上下文菜单
2025-02-01 13:18:43 +08:00
spark
71289d1408 feat(openai): 添加自定义 prompt 功能
- 更新前端设置组件,增加自定义 prompt 输入框
- 更新后端设置 API,支持保存和读取 prompt 配置
2025-02-01 11:32:38 +08:00
spark
f6297d224c refactor(frontend): 优化AI股票分析用户体验
- 在股票检测过程中添加 loading 状态
2025-01-31 16:26:36 +08:00
spark
0bfa50e2b6 feat(frontend): 优化 AI 分析功能
- 添加 loading 状态和DONE消息处理
- 改进消息提示和销毁逻辑
- 优化 AI 分析结果的展示
- 调整 API测试日志输出
2025-01-31 15:50:47 +08:00
spark
0d182b923f build: 更新 wails 构建动作
- 将 dAppServer/wails-build-action 替换为 ArvinLovegood/wails-build-action
- 指定使用2.3 版本的 wails 构建动作
2025-01-31 13:46:29 +08:00
spark
7f19d00a23 build: 更新 wails 构建动作
- 将 dAppServer/wails-build-action 替换为 ArvinLovegood/wails-build-action
- 指定使用2.3 版本的 wails 构建动作
2025-01-31 13:44:49 +08:00
spark
f514a0083d build: 更新 wails 构建动作引用
- 将 dAppServer/wails-build-action@latest替换为 dAppServer/wails-build-action
- 此修改旨在解决特定版本冲突问题
2025-01-31 13:40:51 +08:00
spark
7fbe178c78 build: 更新 Wails 构建动作版本
- 将 dAppServer/wails-build-action 版本从 v2.2 修改为 latest
- 此更改确保使用最新版本的 Wails 构建动作,可能包含未记录的最新功能和修复
2025-01-31 13:38:13 +08:00
spark
40fe30ce2f refactor(data): 重构股票数据获取方法
- 优化了股票价格信息的获取流程,增加了对关键元素的等待和检查
- 改进了搜索结果页面的处理,使用更准确的选择器进行等待
- 删除了不必要的测试函数,整合了相关的测试用例
2025-01-31 13:02:04 +08:00
spark
1751be729b feat(frontend): 添加 Tushare 接口 token 配置功能
- 在前端设置页面增加 Tushare api token 输入框
- 在后端 Settings 结构体中添加 TushareToken 字段- 更新相关 API 调用,使用配置的 TushareToken
2025-01-26 21:41:48 +08:00
spark
d82ace220a ci: 注释掉 Linux 构建
- 不在支持Linux平台,专注Windows平台
- 在 GitHub Actions 配置文件中注释掉了 Linux构建相关配置
- 此修改将阻止在 Ubuntu 系统上进行 go-stock-linux-amd64 的构建
2025-01-26 11:53:00 +08:00
spark
9c51ecde2f ci: 更新作者邮箱地址
- 将作者邮箱从 "wzl@huazx.cn" 修改为 "sparkmemory@163.com"
2025-01-26 11:50:58 +08:00
spark
d3cf202c88 feat(frontend): 增加 AI 赋能股票分析功能
- 在 package.json 中添加 AI 赋能股票分析相关关键词
- 更新 settings.vue 中的 openAI 配置项,优化输入框类型和样式
- 在 wails.json 中添加 AI 赋能分析股票的说明
2025-01-26 11:44:50 +08:00
Lovegood
847cacc71e
Update README.md 2025-01-24 10:58:10 +08:00
Lovegood
23149b8a28
Update README.md 2025-01-24 10:57:29 +08:00
Lovegood
698f496a3c
Update README.md 2025-01-24 10:57:09 +08:00
Lovegood
8ac97f43ff
Update README.md 2025-01-24 10:55:33 +08:00
Lovegood
0abf7c9e5a
Update README.md 2025-01-24 10:54:00 +08:00
spark
a55920f445 feat(backend): 添加电报新闻功能
- 新增 GetTelegraphList 函数,用于获取电报新闻列表
- 在处理用户消息时,添加了获取电报新闻的协程
- 优化了消息处理流程,增加了电报新闻的回复
2025-01-23 17:29:18 +08:00
spark
775635a48c feat(backend): 添加通用聊天流功能并优化系统托盘事件处理
- 在 openai_api.go 中添加 NewCommonChatStream 函数,实现通用聊天流功能
- 修改 systray.Run 调用,使用 goroutine 异步执行 onReady 和 onExit 函数- 更新 stock.vue 中的 search函数,增加对多个股票信息页面的支持
2025-01-23 17:07:33 +08:00
spark
5bc7cfab0a feat(app): 更新股票信息显示和隐藏功能
- 在股票信息更新时,如果总价格不为0,设置系统托盘提示信息- 修复了显示和隐藏应用程序的功能
- 优化了股票数据 API 的请求 URL
- 替换 ioutil 包为 io 包,以适应 Go 1.16 及以上版本
2025-01-23 11:18:11 +08:00
spark
e3e06d342b feat(backend): 添加股票价格信息查询功能
- 新增 SearchStockPriceInfo 函数,用于查询股票价格信息
- 更新 NewChatStream 函数,增加股票代码参数- 在前端添加股票代码参数传递
- 优化后端接口测试用例
2025-01-22 17:00:14 +08:00
spark
16e187b96c docs: 更新 README 中的设置截图
- 将 README.md 中的 img.png 文件引用从根目录改为 build/screenshot 目录
- 更新设置截图,使用最新的 img_11.png 替换旧的 img.png
2025-01-22 15:41:47 +08:00
spark
399513cf14 feat(backend): 添加股票信息搜索功能并优化 OpenAI API调用
- 新增 SearchStockInfo 函数,用于搜索指定股票的相关信息
- 优化 OpenAI API 调用,使用搜索到的股票信息作为上下文- 更新 go.mod 和 go.sum 文件,添加 chromedp 等依赖
2025-01-22 15:15:41 +08:00
spark
3f024faf82 feat(frontend): 添加 AI 分析开关功能
- 在全局配置中获取 openAiEnable 状态
- 根据 openAiEnable 状态控制 AI 分析按钮的显示- 优化了组件的初始化逻辑,确保配置信息及时加载
2025-01-22 12:21:43 +08:00
spark
dadfe1cf54 feat(frontend): 实现 AI聊天流功能
- 新增 NewChatStream 函数,用于接收实时聊天流数据
- 在 App 组件中添加 NewChatStream 方法处理聊天流
- 修改前端 Stock 组件,支持实时显示 AI 聊天流结果
- 优化后端 OpenAi 结构,增加 NewChatStream 方法获取流式响应
2025-01-22 12:02:33 +08:00
spark
9cd6761778 feat(backend): 移除 OpenAI API 中的 Markdown 输出
- 删除了 OpenAI API 请求中的 Markdown 输出要求
- 注释掉了日志记录响应内容的代码行- 在 README 中添加了关于 AI 股票分析功能的重大更新说明
2025-01-17 15:33:04 +08:00
spark
1d58d6b224 feat(backend): 移除 OpenAI API 中的 Markdown 输出
- 删除了 OpenAI API 请求中的 Markdown 输出要求
- 注释掉了日志记录响应内容的代码行- 在 README 中添加了关于 AI 股票分析功能的重大更新说明
2025-01-17 15:29:57 +08:00
spark
db4a2b5fa9 feat(backend): 更新 OpenAI API 调用以支持 Markdown 输出
- 在请求内容中添加了对 Markdown 输出的要求
- 此更改将提高回复的可读性和格式化效果
2025-01-17 15:21:17 +08:00
spark
af3f2b03dc style(frontend): 优化 AI 分析结果弹窗样式
-调整弹窗高度为480px,增加垂直空间
- 设置 Markdown 预览区域高度为 380px,确保内容显示完整
-移除不必要的 previewTheme 属性,直接使用 theme 属性
2025-01-17 15:19:46 +08:00
spark
ccbb835c83 feat(frontend): 集成 OpenAI 聊天功能- 新增 NewChat 函数,用于与 OpenAI 进行聊天
- 在 App.d.ts 和 App.js 中添加 NewChat 方法的声明和实现
- 在 models.ts 中添加 OpenAI 相关的配置项
- 在 package.json 中添加 md-editor-v3 依赖,可能用于富文本编辑
2025-01-17 14:36:13 +08:00
spark
97b5faee4a refactor(app): 优化系统托盘相关代码
- 移除了 systray.Run 函数的冗余 goroutine
- 删除了多余的空行,提高了代码可读性
2025-01-15 13:01:12 +08:00
spark
c9a8192d60 style: 修改 .gitignore 文件中的路径格式
- 移除了 .gitignore 文件中路径开头的 ./
- 使路径格式更加规范和一致
2025-01-15 10:34:42 +08:00
spark
1af138312a chore: 更新 .gitignore 文件
- 移除对特定可执行文件的忽略规则
- 使用通配符忽略 build/bin 目录下的所有文件
- 删除 frontend/package.json.md5 忽略规则
2025-01-15 10:32:01 +08:00
spark
272c990248 build: 更新 .gitignore 文件- 添加 frontend/package.json.md5到忽略列表,避免无关文件影响版本控制 2025-01-15 10:29:31 +08:00
spark
2907285915 build: 更新 .gitignore 文件- 添加 frontend/package.json.md5到忽略列表,避免无关文件影响版本控制 2025-01-15 10:28:40 +08:00
sparkmemory
9f7b7b8a64 feat(app): 启动时添加股票价格监控
- 在 app.go 中添加了 MonitorStockPrices 函数的异步调用
- 修改了前端 App.vue 中的跑马灯效果,包括速度、样式和布局调整
- 更新了 package.json 的 MD5 哈希值
2025-01-14 23:42:43 +08:00
spark
02bfe4758e refactor(app): 调整系统托盘创建逻辑并更新应用配置
- 将系统托盘创建逻辑从 main.go 移动到 app.go 中的 startup 方法- 更新应用配置,添加生产环境日志级别配置
- 移除 main.go 中的冗余注释
2025-01-14 21:03:35 +08:00
spark
6483243d2a feat(stock): 添加电报资讯功能
- 在后端增加电报资讯抓取功能,定时刷新并发送到前端
- 在前端添加电报资讯显示组件,滚动显示最新资讯
- 更新 go.mod 和 go.sum 文件,添加相关依赖
2025-01-14 13:13:50 +08:00
spark
1ea534b3c0 refactor(app): 重构应用启动和托盘功能
- 移除 App.startup 中的系统托盘创建逻辑
- 在 main.go 中添加系统托盘创建逻辑- 更新前端 App.vue,添加实时盈亏显示和相关事件监听- 调整 stock.vue,引入通知功能
2025-01-14 11:31:15 +08:00
spark
2fcd89ab97 style(frontend): 优化页面元素的样式和布局
-调整了 App.vue 中底部菜单栏的 z-index 值- 在 settings.vue 中为数据刷新间隔输入框添加了单位提示- 优化了 stock.vue 中搜索框的布局和样式
2025-01-13 15:23:51 +08:00
spark
b5c44870fe docs: 更新 README 中的屏幕截图链接
- 更新了多个屏幕截图的文件名
- 调整了部分截图的展示顺序- 修正了一个截图链接的路径
2025-01-13 12:30:01 +08:00
spark
54f0a0b585 docs: 更新 README 中的屏幕截图链接
- 更新了多个屏幕截图的文件名
- 调整了部分截图的展示顺序- 修正了一个截图链接的路径
2025-01-13 12:18:27 +08:00
spark
ab6f400930 docs: 更新 README 中的屏幕截图链接
- 更新了多个屏幕截图的文件名
- 调整了部分截图的展示顺序- 修正了一个截图链接的路径
2025-01-13 12:18:08 +08:00
spark
a376d1d92c feat(settings): 添加基础设置功能- 在数据库中增加更新基础信息和刷新间隔的配置项
- 实现根据配置定时更新数据的功能
- 添加启动时更新基础信息的逻辑
- 更新前端设置界面,增加基础设置选项
2025-01-13 12:07:35 +08:00
spark
a653ef9fa8 feat(frontend): 优化 App.vue 中的全屏和隐藏功能- 为全屏和隐藏按钮添加了 title 属性,提升用户体验
- 优化了全屏切换逻辑,现在支持键盘快捷键 Ctrl+F 和 Esc
- 调整了全屏按钮的显示文本,根据当前状态动态变化
- 移除了不必要的 console.log 语句,简化了代码
2025-01-13 10:52:34 +08:00
sparkmemory
ce29514b54 feat(settings): 优化配置更新逻辑
- 增加对配置存在的检查,如果存在则更新,不存在则创建默认配置
- 添加日志记录,当配置不存在时创建默认配置的情况
- 通过 Where 子句指定 ID 进行更新,提高更新操作的准确性
2025-01-12 20:47:49 +08:00
sparkmemory
1fd149bbd5 feat(frontend): 添加窗口移动功能并优化错误处理
- 在 App.vue 中添加移动窗口功能
- 优化全屏切换逻辑
- 在 stock_data_api.go 中改进错误处理
- 移除 app.go 中的冗余日志
2025-01-11 22:54:07 +08:00
sparkmemory
9dc8fa97df feat(settings): 添加推送设置功能- 新增本地推送和钉钉推送的配置选项
- 实现配置的保存和读取功能- 添加测试通知按钮
-优化股票信息的显示格式
2025-01-11 14:16:28 +08:00
sparkmemory
7c52cd1d26 style(frontend): 优化页面布局和底部菜单样式
-调整 RouterView 的样式,增加底部填充
- 修改底部菜单的布局方式,从 sticky 改为 fixed,并设置宽度为 100%
- 更新 App.vue 文件中的相关代码
2025-01-10 20:24:12 +08:00
spark
338ce91ffd feat(frontend): 实现基本路由功能并添加设置页面
- 在 App.vue 中集成 vue-router
- 新增 router.js 文件配置路由- 添加设置页面组件和路由- 更新菜单选项,使用 RouterLink 替代普通链接
2025-01-10 18:23:12 +08:00
spark
6e5f57d62e feat(stock): 在股票卡片中添加取消关注按钮
- 在股票卡片的 header-extra 区域添加了一个取消关注按钮
- 按钮点击时调用 removeMonitor 方法,传入股票代码、名称和 key
- 移除了原有的可关闭图标
2025-01-10 15:46:43 +08:00
spark
b1a0e9575b feat(frontend): 优化用户界面和功能
- 添加全屏切换功能
- 实现窗口隐藏和退出功能
- 新增设置菜单
- 优化股票信息展示界面
- 调整窗口大小和布局
2025-01-10 15:32:22 +08:00
spark
88fb3ce94c refactor(linux): 移除未使用的 time 包导入
- 删除了 app_linux.go 文件中未使用的 time 包导入
-此修改提高了代码的整洁度和可维护性
2025-01-09 15:51:07 +08:00
spark
60d8efc158 refactor(linux): 移除未使用的 time 包导入
- 删除了 app_linux.go 文件中未使用的 time 包导入
-此修改提高了代码的整洁度和可维护性
2025-01-09 15:47:41 +08:00
spark
a41ab5499a refactor: 为 app_linux.go 添加 time 包导入
- 在 app_linux.go 文件中导入了 time 包
- 此修改可能为后续功能使用时间相关函数做准备
2025-01-09 15:42:47 +08:00
spark
a54f769ea2 feat(app_linux): 增加股票排序和消息发送功能
- 添加 SetStockSort 方法,用于设置股票排序
- 新增 SendDingDingMessageByType 方法,根据消息类型发送钉钉消息- 实现 GenNotificationMsg 方法,生成通知消息内容
- 添加 getMsgTypeTTL 和 getMsgTypeName 方法,用于获取消息类型的 TTL 和名称
- 优化 Greet 方法,处理返回的股票数据
2025-01-09 15:42:02 +08:00
spark
9a46788339 feat(app_linux): 增加股票排序和消息发送功能
- 添加 SetStockSort 方法,用于设置股票排序
- 新增 SendDingDingMessageByType 方法,根据消息类型发送钉钉消息- 实现 GenNotificationMsg 方法,生成通知消息内容
- 添加 getMsgTypeTTL 和 getMsgTypeName 方法,用于获取消息类型的 TTL 和名称
- 优化 Greet 方法,处理返回的股票数据
2025-01-09 15:37:20 +08:00
spark
def92ad722 fix(stock): 修正股票排序键的使用
- 将 result.Sort 修改为 result.sort,以匹配正确的属性名称
- 更新 GetSortKey 函数调用,使用正确的属性名称
2025-01-09 14:54:35 +08:00
spark
7e27996f17 feat(backend): 优化股票数据获取逻辑
- 修改 GetStockCodeRealTimeData 方法,支持批量获取多个股票代码的实时数据
- 新增 GetStockInfos 函数,用于获取关注股票的实时信息- 重构 getStockInfo 函数,提高代码复用性
- 优化数据处理逻辑,提高程序运行效率
2025-01-09 14:45:25 +08:00
spark
ad428f83f8 refactor(stock): 重构股票数据处理逻辑
- 移除定时更新标题的代码
- 优化股票数据获取和处理流程
- 增加更多股票相关信息的计算和展示
- 调整前端组件以适应新的数据结构
- 修复了一些潜在的数值计算问题
2025-01-09 13:31:18 +08:00
spark
d3c6c1d570 feat(app): 优化股票监控和交易时间判断
- 添加了判断是否为交易日和交易时间的函数
- 修改了股票价格更新逻辑,只在交易时间内进行监控
- 优化了股票价格显示,增加了上次当前价格字段
- 更新了前端组件,支持显示股票价格变化动画
2025-01-08 15:28:08 +08:00
spark
1554d3309d feat(backend): 实现股票价格实时监控功能
- 在 App 结构中添加定时更新股票价格的逻辑
- 实现 MonitorStockPrices 函数,用于更新关注股票的价格
- 在前端添加股票价格更新的事件处理
- 优化股票数据的获取和处理逻辑
2025-01-08 14:17:11 +08:00
spark
e7560f3e9b feat(backend): 添加 Windows 系统消息提醒功能
- 新增 AlertWindowsApi 结构体和 SendNotification 方法,用于发送 Windows 系统通知
- 实现 SendDingDingMessageByType 方法,支持根据不同消息类型发送通知
- 添加消息类型 TTL 和名称映射,优化消息发送逻辑
- 更新前端接口,增加 SendDingDingMessageByType 方法调用- 引入 go-toast 库,用于 Windows 系统通知
2025-01-08 10:57:17 +08:00
spark
daa29b37a5 feat(stock): 添加股票排序功能- 新增 SetStockSort 函数用于设置股票排序
- 在前端增加股票排序的输入和显示逻辑
- 修改后端数据库,增加股票排序字段
- 优化股票列表的渲染,支持按排序值进行排序
2025-01-07 13:29:16 +08:00
spark
9a41560bee refactor(frontend): 优化股票组件功能和布局
-调整了固定按钮的位置和样式
- 优化了股票搜索和添加功能的布局
- 移除了不必要的控制台日志输出- 调整了事件处理
2025-01-07 11:11:07 +08:00
spark
975ad611df build(frontend): 升级 naive-ui 至 2.41.0 版本
- 在 package.json 和 package-lock.json 中更新 naive-ui 版本
- 更新 package.json.md5 校验值
2025-01-07 09:54:37 +08:00
spark
b764770729 refactor(app): 重构应用控制逻辑并修正部分功能
-移除了 App.shutdown 方法中的 runtime.Quit 调用
- 将 runtime.Show 替换为 runtime.WindowShow
- 在全屏菜单项中移除了 MenuItem.Hide 调用
- 将 runtime.Hide替换为 runtime.WindowHide
2025-01-07 09:42:20 +08:00
spark
88aa793774 feat(app): 优化应用隐藏功能并添加错误日志
- 注释掉隐藏应用程序的代码,暂时禁用此功能
- 添加对话框错误日志记录,提高错误追踪能力
- 在 shutdown 函数中添加 runtime.Quit 调用,确保应用正确退出
-优化股票组件中的报警逻辑,增加对当前价格的判断
2025-01-07 09:27:24 +08:00
spark
180bec8866 docs: 更新 README.md 文件 2025-01-06 18:07:04 +08:00
spark
420d5b60f1 docs(README): 更新截图并添加钉钉报警通知说明
- 更新成本仓位设置截图
- 添加钉钉报警通知功能截图及说明
2025-01-06 17:59:06 +08:00
spark
af1bc685a7 build(ci): 更新 GitHub Actions 构建配置
- 为 Windows平台构建产物命名为 go-stock-windows-amd64.exe
-为 Linux 平台构建产物命名为 go-stock-linux-amd64- 通过明确指定构建输出名称,提高构建结果的可识别性和一致性
2025-01-06 17:33:54 +08:00
spark
b0922b0878 ci: 更新 GitHub Actions 构建矩阵
- 为 Windows 和 Linux 构建产物添加平台名称后缀
- 保持原有构建配置不变
2025-01-06 17:18:24 +08:00
spark
200a160acf refactor(app): 优化系统托盘和菜单相关代码
- 在 FileMenu 中添加了隐藏到托盘区的功能,仅在 Windows 平台上显示- 优化了代码结构,提高了可读性和可维护性
2025-01-06 16:56:32 +08:00
spark
9fae9fc034 feat: 添加 Linux 平台支持
- 新增 app_linux.go 文件,实现 Linux 平台下的应用逻辑
- 添加缓存功能,用于限制钉钉消息发送频率- 实现股票数据相关功能,包括获取股票列表、关注股票等
- 添加应用启动、关闭等生命周期方法
2025-01-06 16:44:22 +08:00
spark
9a3393bfc3 feat(app): 为 Windows 系统添加系统托盘功能并支持 Linux
- 在 app.go 中添加了对 Windows 操作系统的判断- 仅在 Windows 系统上创建系统托盘
- 更新 GitHub Actions 工作流,添加 Linux 平台的构建
2025-01-06 16:30:03 +08:00
spark
64270d5df2 ci: 更新 Windows 构建输出文件名
- 将 Windows构建输出文件名从 'go-stock' 改为 'go-stock.exe'
- 确保在 Windows 平台上生成可执行文件
2025-01-06 15:19:32 +08:00
spark
e808ca47b6 refactor(app): 注释掉退出相关的代码
- 在 App.d.ts 中注释掉了 systray.Quit() 调用
- 在 App.js 中注释掉了 FileMenu 中的退出选项
- 更新了 models.ts 的 MD5 校验值
2025-01-06 15:18:33 +08:00
sparkmemory
1b3c043ce6 feat(stock): 增加股价提醒功能并优化报警逻辑
- 在 SetAlarmChangePercent 函数中添加 alarmPrice 参数
- 在前端添加股价提醒输入框
- 修改报警逻辑,支持同时根据涨跌幅和股价进行提醒
- 更新数据库模型,添加 AlarmPrice 字段
2025-01-04 20:54:04 +08:00
sparkmemory
04446d7521 refactor(app): 优化菜单项创建顺序和股票收益计算逻辑- 调整 systray 菜单项创建顺序,将"退出"菜单项放在最后- 修正股票收益计算逻辑,确保正确处理负数情况 2025-01-04 19:11:58 +08:00
spark
2306a8e225 ui(systray): 修改系统托盘菜单项名称
-将"隐藏应用程序"菜单项修改为"隐藏"
- 优化菜单项名称,使其更加简洁明了
2025-01-04 14:42:55 +08:00
sparkmemory
46aff404d4 update 2025-01-03 23:19:57 +08:00
sparkmemory
1e5d9bc469 Merge branch 'master' of https://github.com/ArvinLovegood/go-stock
# Conflicts:
#	stock_basic.json
2025-01-03 23:03:40 +08:00
spark
98844ce717 feat(app): 添加系统托盘功能
- 使用 systray 库创建系统托盘图标和菜单- 添加退出、显示和隐藏应用程序的菜单项
- 实现托盘图标初始化和清理逻辑
- 更新 go.mod 和 go.sum 文件,添加相关依赖
2025-01-03 22:54:39 +08:00
spark
2bd91c6555 refactor(frontend): 调整股票数量输入的最小步长为 100
- 在 Stock 组件中的股票数量输入框中添加 step 属性
-将 step 属性设置为 100,以符合业务需求
2025-01-03 17:02:35 +08:00
spark
2166b0a39b feat(stock): 优化股票对话框输入项
- 为成本、数量和涨跌报警值添加单位后缀
- 优化表单项的标签文案
- 调整输入框样式,增加单位后缀
2025-01-03 16:58:47 +08:00
spark
2f2b19f5d7 feat(app): 添加钉钉消息发送功能和股票涨跌报警
- 新增 SendDingDingMessage 和 SetAlarmChangePercent 函数- 实现钉钉消息发送和股票涨跌报警逻辑
- 更新前端界面,增加报警值设置和消息发送功能
- 新增 DingDingAPI 结构体和相关方法
2025-01-03 16:43:32 +08:00
spark
685a7d23b2 feat(stock_data_api): 搜索股票时包含深交所指数
- 将指数市场范围从上交所(SSE)扩展到包括深交所(SZSE)
- 优化了股票和指数的搜索逻辑,提高搜索结果的全面性
2025-01-03 13:17:19 +08:00
spark
5f1eaf02c4 feat(frontend): 扩展股票搜索框占位符文本
- 将搜索框的占位符文本从"请输入股票名称或者代码"修改为"请输入股票/指数名称或者代码"
- 这个修改使得用户更加清晰地知道可以在搜索框中输入股票或指数的名称或代码
2025-01-03 13:05:43 +08:00
spark
116dae19cf feat(stock): 搜索股票时增加指数匹配
- 在搜索股票时增加对上交所指数的匹配
- 优化股票代码输入逻辑,增加空值判断
-调整关注股票功能,避免重复关注
- 修改分时图数据的更新频率为 3.5 秒一次
2025-01-03 13:04:29 +08:00
spark
a35b42f831 refactor(backend): 移除股票数据 API 中的冗余代码
- 删除了 GetIndexBasic 和 GetStockBaseInfo 方法中的冗余代码
- 移除了不必要的文件写入操作和注释掉的代码
- 优化了代码结构,提高了代码的可读性和维护性
2025-01-03 09:57:19 +08:00
spark
513cd69e3e build: 更新 .gitignore 文件以忽略构建输出
- 修改 .gitignore 文件,更新对构建输出文件和目录的忽略规则
-保留了对 .idea 目录和 data/*.db 文件的忽略- 更新了对 build 目录下可执行文件的忽略规则
2025-01-03 09:54:27 +08:00
spark
0ce01bcdf0 build: 更新 .gitignore 文件以忽略构建输出
- 修改 .gitignore 文件,更新对构建输出文件和目录的忽略规则
-保留了对 .idea 目录和 data/*.db 文件的忽略- 更新了对 build 目录下可执行文件的忽略规则
2025-01-03 09:51:45 +08:00
spark
afe5474264 refactor(stock): 优化股票组件和数据更新逻辑
- 修改股票列表显示格式,将代码和名称之间的连接符改为短横线- 调整股票数据更新频率,从 3 秒改为 1 秒
- 修复当前价格为零时的显示问题,使用卖一报价替代
- 优化数据库更新操作,添加 ts_code 条件以确保更新正确性
2025-01-03 09:48:21 +08:00
spark
f35847823b feat(data): 添加指数信息获取功能
- 在 StockDataApi 中新增 GetIndexBasic 方法,用于获取指数信息
- 在数据库中添加 index_basic 表并进行自动迁移- 优化 GetStockBaseInfo 方法,使用 map 结构处理字段- 增加 GetIndexBasic 的单元测试
2025-01-02 17:52:25 +08:00
spark
15120c98da feat(frontend): 优化股票代码输入和搜索功能
- 改进股票列表显示格式,增加连字符分隔
- 添加支持直接输入股票代码进行搜索的功能
- 优化股票选择逻辑,支持不同格式的股票代码
-增加调试日志输出,便于问题排查
2025-01-02 15:39:39 +08:00
sparkmemory
6903abd6f8 update 2024-12-29 07:50:12 +08:00
spark
f9a0c8d94e 样式修改,新增菜单工具栏 2024-12-28 14:41:45 +08:00
spark
0c04272153 样式修改,新增菜单工具栏 2024-12-25 13:13:23 +08:00
sparkmemory
cf7e8415e6 update 2024-12-23 11:26:24 +08:00
sparkmemory
1ab6875790 update 2024-12-23 11:25:33 +08:00
sparkmemory
cc40da8371 update 2024-12-23 11:21:03 +08:00
sparkmemory
29158f51af update 2024-12-23 07:34:58 +08:00
spark
9665462ae5 样式修改 2024-12-20 21:02:37 +08:00
spark
71e3953cd8 添加缩略图 2024-12-20 15:06:09 +08:00
spark
bedeaad1f2 添加按钮位置修改 2024-12-20 14:37:38 +08:00
spark
4f05820e2e 优化首次启动速度 2024-12-20 14:34:24 +08:00
spark
abf05aabd8 退出程序添加提示 2024-12-20 14:33:54 +08:00
spark
9016289e96 样式调整 2024-12-20 11:07:46 +08:00
spark
bab53711b7 添加全屏按钮 2024-12-20 11:01:56 +08:00
spark
de67f07f41 修改为深色主题 2024-12-20 10:01:56 +08:00
spark
869223cfb9 修改为深色主题 2024-12-20 10:00:46 +08:00
spark
d240239fcc 窗口标题显示当前时间 2024-12-19 20:59:01 +08:00
spark
6b62385cad 分时图和K线图展示 2024-12-19 13:48:02 +08:00
113 changed files with 271893 additions and 779 deletions

54
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,54 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
# Bug 报告
## 基本信息
### Bug 描述
<请用简洁明了的语言概括 Bug 的核心问题例如登录页面输入错误密码后无提示信息>
### 软件版本信息
<说明你所使用的软件版本在关于界面中可以找到>
### 运行操作系统和环境
- **操作系统**<例如 Windows 10macOS 12.6Ubuntu 22.04 >
- **浏览器(如果是网页应用)**< Chrome 108Firefox 107 同时说明浏览器的版本和是否使用了特殊的插件>
- **其他相关环境信息**<例如运行项目的服务器配置数据库版本等>
## Bug 描述
### 预期行为
<详细描述你认为在正常情况下系统应该呈现的行为例如当用户在登录页面输入错误密码时页面应弹出提示框显示密码错误请重新输入>
### 实际行为
<准确描述实际发生的情况可以包括错误信息页面显示异常功能无法正常使用等具体表现例如当输入错误密码后页面没有任何提示也没有重新聚焦到密码输入框登录按钮依然可点击>
### 复现步骤
<提供详细的步骤让开发者能够按照这些步骤重现 Bug步骤要尽量清晰具体例如
1. 打开项目的登录页面URL[具体登录页面 URL])。
2. 在用户名输入框输入已注册的用户名。
3. 在密码输入框输入错误的密码。
4. 点击登录按钮。>
### 频率
<说明 Bug 出现的频率例如每次都会出现偶尔出现 10% 的概率>
## 相关信息
### 错误日志
<如果有错误日志或控制台输出信息请提供完整的内容可以使用代码块来展示例如>
### 截图或视频
<如果 Bug 涉及页面显示问题或操作流程异常附上相关的截图或录屏视频会非常有帮助可以直接上传截图文件或者提供视频的链接>
### 可能的原因分析(可选)
<如果你对 Bug 产生的原因有一些初步的猜测或分析可以在这里简要说明这有助于开发者更快地定位问题但不是必需的>
## 补充说明
<如果有其他与 Bug 相关但不属于上述分类的信息可以在这里进行补充例如之前是否进行过特定的配置更改是否与其他功能存在关联等>

10
.github/ISSUE_TEMPLATE/custom.md vendored Normal file
View File

@ -0,0 +1,10 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

48
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,48 @@
# Pull Request 信息
## 本次 PR 概述
请简要描述这个 Pull Request 做了什么改动。例如:
- 修复了某个特定功能的 bug
- 实现了一个新的功能特性
- 对代码进行了优化,提升了性能
## 相关问题
如果这个 PR 是为了解决某个 Issue请在此处关联对应的 Issue 编号,格式为 `Fixes #<issue-number>`。例如:
Fixes #123
## 改动内容详细说明
### 代码修改
- 列出主要修改的文件和修改点。例如:
- `app_linux.go`
- 修改了函数 `GetStockList` 的逻辑,从使用 `for` 循环改为 `sum` 函数,提升了计算效率。
- `app_test.go`
- 新增了针对 `GetStockList` 函数的单元测试,确保修改后的逻辑正确。
### 新增功能
如果有新增功能,请详细描述该功能的使用方法和特点。例如:
- 新增了一个用户认证模块,支持使用用户名和密码进行登录。使用方法如下:
- 调用 `authenticate_user(username, password)` 函数进行认证。
- 若认证成功,返回 `True`;否则返回 `False`
### 删除内容
如果有删除的代码或文件,请说明删除的原因。例如:
- 删除了 `app_test.go` 文件,因为该模块的功能已经被新的模块替代,不再需要。
## 测试情况
### 单元测试
- 列出运行的单元测试以及测试结果。例如:
- 运行了 `app_test.go` 进行单元测试,所有测试用例均通过。
- 测试覆盖率达到了 90%。
### 集成测试
如果进行了集成测试,请描述测试环境和测试结果。例如:
- 在本地开发环境Wails CLI v2.10.1 node v18.19.1 )中进行了集成测试,功能正常。
- 在 CI/CD 环境中也进行了测试,所有步骤均通过。
## 注意事项
- 提醒其他开发者在审查代码时需要注意的地方。例如:
- 本次修改涉及到数据库表结构的变更,请确保在部署前进行数据库迁移。
- 新增的功能依赖于第三方库 `requests`,请确保在环境中安装该库。
## 其他补充说明
- 可以在这里提供任何其他需要说明的信息,例如设计文档的链接、相关讨论的记录等。

View File

@ -4,7 +4,7 @@ on:
push:
tags:
# Match any new tag
- '*'
- '*-release'
env:
# Necessary for most environments as build failure can occur due to OOM issues
@ -17,9 +17,12 @@ jobs:
fail-fast: false
matrix:
build:
- name: 'go-stock'
- name: 'go-stock-windows-amd64.exe'
platform: 'windows/amd64'
os: 'windows-latest'
# - name: 'go-stock-linux-amd64'
# platform: 'linux/amd64'
# os: 'ubuntu-latest'
runs-on: ${{ matrix.build.os }}
steps:
@ -28,11 +31,20 @@ jobs:
with:
submodules: recursive
- name: Build wails
uses: dAppServer/wails-build-action@v2.2
- name: Get commit message
id: get_commit_message
run: |
$commit_message = & git log -1 --pretty=format:"%s"
echo "::set-output name=commit_message::$commit_message"
- name: Build wails x go-stock
uses: ArvinLovegood/wails-build-action@v3.4
id: build
with:
build-name: ${{ matrix.build.name }}
build-platform: ${{ matrix.build.platform }}
package: true
go-version: '1.23'
go-version: '1.24'
build-tags: ${{ github.ref_name }}
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
node-version: '20.x'

8
.gitignore vendored
View File

@ -106,6 +106,8 @@ dist
.DS_Store
.idea/
data/*.db
build/*.exe
/build/bin/go-stock-dev.exe
/data/*.db
/build/*.exe
/build/bin/*
frontend/package.json.md5
/build/us.json

217
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,217 @@
# Contributor Covenant 行为准则
## 我们的承诺
我们作为项目的成员、贡献者和领导者,承诺为每一个人营造一个无骚扰的社区参与环境,无论年龄、体型、可见或不可见的残疾状况、种族、身体特征、性别认同与表达、经验水平、教育背景、社会经济地位、国籍、个人外貌、种族、宗教信仰或性取向如何。
我们承诺以有助于建立一个开放、友好、多元、包容和健康的社区的方式行事和互动。
## 我们的准则
有助于为我们的社区营造积极环境的行为示例包括:
- 对他人展现出同理心和善意
- 尊重不同的意见、观点和经验
- 给予并欣然接受建设性的反馈
- 对自己的错误负责,向受影响的人道歉,并从经验中学习
- 不仅关注个人利益,更着眼于整个社区的利益
不可接受的行为示例包括:
- 使用性暗示的语言或图像,以及任何形式的性关注或挑逗
- 恶意挑衅、侮辱性或贬低性的评论,以及个人或政治攻击
- 公开或私下的骚扰行为
- 在未经明确许可的情况下公布他人的私人信息,如实际地址或电子邮件地址
- 在专业环境中被合理认为不适当的其他行为
## 执行责任
社区领导者有责任阐明和执行我们可接受行为的标准,并将针对任何他们认为不适当、具有威胁性、冒犯性或有害的行为采取适当和公平的纠正措施。
社区领导者有权且有责任移除、编辑或拒绝不符合本行为准则的评论、提交的代码、代码修改、维基编辑、问题报告和其他贡献,并在适当时说明进行管理决策的原因。
## 适用范围
本行为准则适用于所有社区空间,并且当个人在公共场合正式代表社区时也同样适用。代表我们社区的示例包括使用官方电子邮件地址、通过官方社交媒体账户发布内容,或在线上或线下活动中担任指定代表。
## 执行
若发生滥用、骚扰或其他不可接受的行为,可向负责执行的社区领导者报告,邮箱地址为 [sparkmemory@163.com]。所有投诉都将得到及时、公正的审查和调查。
所有社区领导者都有义务尊重任何事件报告者的隐私和安全。
### 执行指南
社区领导者将遵循以下社区影响指南来确定对任何他们认为违反本行为准则的行为的后果:
#### 1. 纠正
**社区影响**:使用不适当的语言或其他被认为在社区中不专业或不受欢迎的行为。
**后果**:社区领导者发出私下的书面警告,阐明违规行为的性质,并解释为什么该行为不适当。可能会要求公开道歉。
#### 2. 警告
**社区影响**:通过单次事件或一系列行为构成的违规。
**后果**:发出警告并说明持续此类行为的后果。在指定的时间段内,禁止与相关人员进行互动,包括主动与执行本行为准则的人员进行互动。这包括避免在社区空间以及社交媒体等外部渠道进行互动。违反这些规定可能会导致临时或永久禁令。
#### 3. 临时禁令
**社区影响**:严重违反社区标准,包括持续的不当行为。
**后果**:在指定的时间段内,禁止与社区进行任何形式的互动或公开交流。在此期间,禁止与相关人员进行任何公开或私下的互动,包括主动与执行本行为准则的人员进行互动。违反这些规定可能会导致永久禁令。
#### 4. 永久禁令
**社区影响**:表现出违反社区标准的行为模式,包括持续的不当行为、骚扰个人,或对某类人群进行攻击或贬低。
**后果**:永久禁止在社区内进行任何形式的公开互动。
## 版权声明
本行为准则改编自 [Contributor Covenant][主页] 2.1 版本,可在 [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1] 查看。
社区影响指南的灵感来自 [Mozilla 的行为准则执行阶梯][Mozilla CoC]。
有关本行为准则常见问题的解答,请参阅常见问题解答页面 [https://www.contributor-covenant.org/faq][FAQ]。该准则有多种语言的翻译版本,可在 [https://www.contributor-covenant.org/translations][翻译] 查看。
[主页]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[翻译]: https://www.contributor-covenant.org/translations
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at [sparkmemory@163.com].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
### Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
#### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
#### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
#### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
#### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

79
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,79 @@
# Contributing to [go-stock]
感谢你对 [go-stock] 项目的兴趣并愿意贡献代码!本指南将帮助你了解如何为这个项目做出贡献。
## 行为准则
在参与这个项目时,请遵守我们的 [行为准则](./CODE_OF_CONDUCT.md)。我们致力于为所有贡献者提供一个友好、包容和尊重的环境。
## 贡献类型
### 报告问题
如果你发现了一个 bug、有功能请求或者对项目有任何建议请在项目的 [GitHub Issues](https://github.com/ArvinLovegood/go-stock/issues) 中创建一个新的 issue。在创建 issue 时,请提供尽可能多的信息,包括:
- **问题描述**:清晰地描述你遇到的问题或建议的功能。
- **重现步骤**:如果是 bug请提供重现该问题的具体步骤。
- **环境信息**:例如操作系统、编程语言版本等。
- **相关日志或错误信息**:如果有的话,请附上相关的日志或错误信息。
### 提交代码
我们欢迎各种类型的代码贡献,包括修复 bug、添加新功能、改进文档等。请按照以下步骤提交你的代码
#### 1. Fork 项目
在 GitHub 上点击项目页面的 “Fork” 按钮,将项目复制到你自己的 GitHub 账户下。
#### 2. 克隆项目到本地
使用以下命令将你 fork 的项目克隆到本地:
```bash
git clone https://github.com/ArvinLovegood/go-stock.git
cd go-stock
```
#### 3. 创建新分支
在开始编写代码之前,创建一个新的分支来包含你的更改。建议使用一个描述性的分支名称,例如 `fix-bug-123``add-new-feature`
```bash
git checkout -b 新分支名称
```
#### 4. 编写代码
在新分支上进行你的代码更改。请确保你的代码遵循项目的编码风格和规范。
#### 5. 测试代码
在提交代码之前,请确保你的更改通过了项目的测试。如果项目没有测试,请考虑添加适当的测试。
#### 6. 提交更改
将你的更改提交到本地仓库,并提供一个清晰、简洁的提交信息。
```bash
git add.
git commit -m "描述你的更改,例如:修复了 #123 号 bug"
```
#### 7. 同步上游仓库
在推送代码之前,确保你的分支与上游仓库(原始项目)保持同步。
```bash
git remote add upstream https://github.com/ArvinLovegood/go-stock.git
git fetch upstream
git rebase upstream/main
```
#### 8. 推送更改
将你的更改推送到你 fork 的 GitHub 仓库。
```bash
git push origin 新分支名称
```
#### 9. 创建 Pull Request
在 GitHub 上,导航到你 fork 的项目页面,点击 “New pull request” 按钮。选择你刚刚推送的分支,并提供一个清晰的描述,说明你的更改内容和目的。然后提交 pull request。
### 改进文档
良好的文档对于项目的成功至关重要。如果你发现文档中有错误、不清楚的地方或者有可以改进的地方,请提交一个 issue 或者直接修改文档并提交 pull request。
## 代码风格和规范
请遵循项目的代码风格和规范。如果项目中没有明确的规范,请参考以下通用准则:
- **代码格式**:使用一致的缩进、空格和换行符。
- **注释**:添加适当的注释来解释代码的功能和逻辑。
- **命名规范**:使用有意义的变量名、函数名和类名。
## 许可证
通过贡献代码,你同意你的贡献将根据项目的 [许可证](./LICENSE) 进行分发。
再次感谢你对项目的贡献!如果你有任何问题或需要帮助,请随时在 issue 中提问。

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright [2025] [sparkmemory@163.com]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

168
README.md
View File

@ -1,31 +1,163 @@
# README
# go-stock : 基于大语言模型的AI赋能股票分析工具
## ![go-stock](./build/appicon.png)
![GitHub Release](https://img.shields.io/github/v/release/ArvinLovegood/go-stock?link=https%3A%2F%2Fgithub.com%2FArvinLovegood%2Fgo-stock%2Freleases&link=https%3A%2F%2Fgithub.com%2FArvinLovegood%2Fgo-stock%2Freleases)
[![GitHub Repo stars](https://img.shields.io/github/stars/ArvinLovegood/go-stock?link=https%3A%2F%2Fgithub.com%2FArvinLovegood%2Fgo-stock)](https://github.com/ArvinLovegood/go-stock)
[![star](https://gitee.com/arvinlovegood_admin/go-stock/badge/star.svg?theme=dark)](https://gitee.com/arvinlovegood_admin/go-stock)
![Wails and NaiveUI](./build/appicon.png)
[//]: # ([![star]&#40;https://gitcode.com/ArvinLovegood/go-stock/star/badge.svg&#41;]&#40;https://gitcode.com/ArvinLovegood/go-stock&#41;)
### 🌟公众号
![扫码_搜索联合传播样式-白色版.png](build/screenshot/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88.png)
### 📈 交流群
- QQ交流群2[点击链接加入群聊【go-stock交流群2】892666282](https://qm.qq.com/q/5mYiy6Yxh0)
- QQ交流群[点击链接加入群聊【go-stock交流群】491605333(已满会定期清理,随缘入群)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333)
### ✨ 简介
- 本项目基于Wails和NaiveUI开发结合AI大模型构建的股票分析工具。
- 目前已支持A股港股美股未来计划加入基金ETF等支持。
- 支持市场整体/个股情绪分析K线技术指标分析等功能。
- 本项目仅供娱乐不喜勿喷AI分析股票结果仅供学习研究投资有风险请谨慎使用。
- 开发环境主要基于Windows10+,其他平台未测试或功能受限。
### 📦 立即体验
- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases)
- 绿色版:[go-stock-windows-amd64.exe](https://github.com/ArvinLovegood/go-stock/releases)
## About
### 💬 支持大模型/平台
| 模型 | 状态 | 备注 |
| --- | --- |---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
| [AnythingLLM](https://anythingllm.com/) | ✅ | 本地知识库 |
| [DeepSeek](https://www.deepseek.com/) | ✅ | deepseek-reasoner,deepseek-chat |
| [大模型聚合平台](https://cloud.siliconflow.cn/i/foufCerk) | ✅ | 如:[硅基流动](https://cloud.siliconflow.cn/i/foufCerk)[火山方舟](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ) ,[优云智算](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock) |
A China stock data viewer build by [Wails](https://wails.io/) with [NavieUI](https://www.naiveui.com/).
A股数据可视化工具基于Wails和NaiveUI。
### <span style="color: #568DF4;">各位亲爱的朋友们,如果您对这个项目感兴趣,请先给我一个<i style="color: #EA2626;">star</i>吧,谢谢!</span>💕
- 优云智算by UCloud万卡规模4090免费用10小时新人注册另增50万tokens海量热门源项目镜像一键部署[注册链接](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock)
- 经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定注册即送2000万Tokens[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
- 火山方舟每个模型注册即送50万tokens[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
- Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究(注意Tushare只需要120积分即可注册完成个人资料补充即可得120积分)[注册链接](https://tushare.pro/register?reg=701944)
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
- 欢迎大家提出宝贵的建议欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧)。💕
## Prerequisites
INSTALL [GO](https://golang.org) AND [Wails](https://wails.io/)
## Running the Application in Developer Mode
The easiest way is to use the Wails CLI: `wails dev`
## 🧩 重大功能开发计划
| 功能说明 | 状态 | 备注 |
|-----------------|----|----------------------------------------------------------------------------------------------------------|
| 股票分析知识库 | 🚧 | 未来计划 |
| Ai智能选股 | 🚧 | Ai智能选股功能开发中(下半年重点开发计划) |
| ETF支持 | 🚧 | ETF数据支持 (目前可以查看净值和估值) |
| 美股支持 | ✅ | 美股数据支持 |
| 港股支持 | ✅ | 港股数据支持 |
| 多轮对话 | ✅ | AI分析后可继续对话提问 |
| 自定义AI分析提问模板 | ✅ | 可配置的提问模板 [v2025.2.12.7-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.12.7-alpha) |
| 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 |
This should hot refresh when making changes the Frontend and rebuild when making changes in the Go.
## 👀 更新日志
### 2025.06.30 添加指标选股功能
### 2025.06.27 添加财经日历和重大事件时间轴功能
### 2025.06.25 添加热门股票、事件和话题功能
### 2025.06.18 更新内置股票基础数据,软件内实时市场资讯信息提醒,添加行业研究功能
### 2025.06.15 添加公司公告信息搜索/查看功能
### 2025.06.15 添加个股研报到弹出菜单
### 2025.06.13 添加个股研报功能
### 2025.06.12 添加龙虎榜功能,新增行业排名分类
### 2025.05.30 优化股票分时图显示
### 2025.05.20 修复财联社电报获取问题
### 2025.05.16 优化资金趋势图表组件
### 2025.05.15 重构应用加载和数据初始化逻辑,添加股票资金趋势功能,资金趋势图表增加主力当日净流入数据并优化展示效果
### 2025.05.14 添加个股资金流向功能排行榜增加股票行情K线图弹窗
### 2025.05.13 添加行业排名功能
### 2025.05.09 添加A股盘口数据解析和展示功能
### 2025.05.07 优化分时图的展示
### 2025.04.29 补全港股/美股基础数据,优化港股股价延迟问题,优化初始化逻辑
### 2025.04.25 市场资讯支持AI分析和总结让AI帮你读市场
### 2025.04.24 新增市场行情模块:即时掌握全球市场行情资讯/动态从此再也不用偷摸去各大财经网站啦。go-stock一键帮你搞定
### 2025.04.22 优化K线图展示支持拉伸放大看得更舒服啦
### 2025.04.21 港股美股K线数据获取优化
### 2025.04.01 优化部分设置选项,避免重启软件
### 2025.03.31 优化数据爬取
### 2025.03.30 AI自动定时分析功能
### 2025.03.29 多提示词模板管理AI分析时支持选择不同提示词模板
### 2025.03.28 AI分析结果保存为markdown文件时支持保存位置目录选择
### 2025.03.15 自定义爬虫使用的浏览器路径配置
### 2025.03.14 优化编译构建,大幅减少编译后的程序文件大小
### 2025.03.09 基金估值和净值监控查看
### 2025.03.06 项目社区分享功能
### 2025.02.28 美股数据支持
### 2025.02.23 弹幕功能,盯盘不再孤单,无聊划个水!😎
### 2025.02.22 港股数据支持(目前有延迟)
### 2025.02.16 AI分析后可继续对话提问
- [v2025.2.16.1-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.16.1-alpha)
### 2025.02.12 可配置的提问模板
- [v2025.2.12.7-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.12.7-alpha)
## 🦄 重大更新
### BIG NEWS !!! 重大更新!!!
- 2025.04.25 市场资讯支持AI分析和总结让AI帮你读市场
![img.png](img.png)
- 2025.04.24 新增市场行情模块:即时掌握全球市场行情资讯/动态从此再也不用偷摸去各大财经网站啦。go-stock一键帮你搞定
![img.png](build/screenshot/img13.png)
![img_13.png](build/screenshot/img_13.png)
- ![img_14.png](build/screenshot/img_14.png)
- 2025.01.17 新增AI大模型分析股票功能
![img_5.png](build/screenshot/img.png)
## 📸 功能截图
![img_1.png](build/screenshot/img_6.png)
### 设置
![img_12.png](build/screenshot/img_4.png)
### 成本设置
![img.png](build/screenshot/img_7.png)
### 日K
![img_12.png](build/screenshot/img_12.png)
### 分时
![img_3.png](build/screenshot/img_9.png)
### 钉钉报警通知
![img_4.png](build/screenshot/img_5.png)
### AI分析股票
![img_5.png](build/screenshot/img.png)
### 版本信息提示
![img_11.png](build/screenshot/img_11.png)
## 💕 感谢以下项目
- [NaiveUI](https://www.naiveui.com/)
- [Wails](https://wails.io/)
- [Vue](https://vuejs.org/)
- [Vite](https://vitejs.dev/)
- [Tushare](https://tushare.pro/register?reg=701944)
## 😘 赞助我
### 都划到这了,如果我的项目对您有帮助,请赞助我吧!😊😊😊
| 支付宝 | 微信 |
|-----|-----|
| ![alipay.jpg](build/screenshot/alipay.jpg) | ![wxpay.jpg](build/screenshot/wxpay.jpg) |
## ⭐ Star History
[![Star History Chart](https://api.star-history.com/svg?repos=ArvinLovegood/go-stock&type=Date)](https://star-history.com/#ArvinLovegood/go-stock&Date)
## 🤖 状态
![Alt](https://repobeats.axiom.co/api/embed/40b07d415a42c2264a18c4fe1b6f182ff1470687.svg "Repobeats analytics image")
## 🐳 关于技术支持申明
- 本软件基于开源技术构建使用Wails、NaiveUI、Vue、AI大模型等开源项目。 技术上如有问题,可以先向对应的开源社区请求帮助。
- 开源不易,本人精力和时间有限,如需一对一技术支持,请先赞助。联系微信(备注 技术支持)ArvinLovegood
<img src="./build/wx.jpg" width="301px" height="402px" alt="ArvinLovegood">
| 技术支持方式 | 赞助(元) |
|:--------------------------------|:-----:|
| 加 QQ506808970微信ArvinLovegood | 100/次 |
| 长期技术支持(不限次数,新功能优先体验等) | 5000 |
## Building the Application for Production
You can build you Application with: `wails build`
## License
[Apache License 2.0](LICENSE)
## Credits
[NaiveUI](https://www.naiveui.com/)
[Wails](https://wails.io/)
[Vue](https://vuejs.org/)
[Vite](https://vitejs.dev/)

56
SECURITY.md Normal file
View File

@ -0,0 +1,56 @@
# 安全策略
## 1. 受支持的版本
以下是 [go-stock] 项目当前接受安全更新支持的版本:
| 版本号 | 是否支持 |
| ------- | ------------------ |
| [v年.月.日.版本号] | :white_check_mark: |
| [v较旧的年.月.日.版本号] | :x: |
请注意,通常只有最新的主要或次要版本会积极维护安全更新,较旧版本可能不会收到安全补丁。
## 2. 报告安全漏洞
### 2.1 报告方式
我们非常重视安全漏洞问题。如果您在我们的项目中发现了安全漏洞,请通过以下方式向我们报告:
- **私下披露**:请发送电子邮件至 [sparkmemory@163.com]。在邮件中,请包含以下详细信息:
- 对漏洞的详细描述,包括如何复现该漏洞。
- 受影响的项目版本。
- 该漏洞可能造成的任何影响或风险。
- 如果可能,请提供建议的修复或缓解策略。
### 2.2 响应时间线
- **首次确认**:我们将在收到报告后的 [7] 个工作日内确认收到您的报告。
- **调查与进度更新**:我们将对报告的漏洞进行全面调查,并在 [7] 个工作日内向您提供调查进度更新。
- **补丁发布**:一旦修复方案开发完成,我们将尽快发布补丁。发布补丁的时间可能会因漏洞的复杂程度而有所不同。
### 2.3 保密承诺
我们深知安全漏洞报告保密的重要性。我们将对所有报告内容严格保密,未经您的许可,不会披露您的身份或漏洞的具体细节,除非法律有相关要求。
## 3. 安全更新与沟通
### 3.1 补丁发布
当发现并修复安全漏洞后,我们会为受支持的项目版本发布补丁。补丁将在项目的官方 GitHub 仓库上提供。
### 3.2 安全公告
我们会在项目的 GitHub 安全公告页面发布安全公告。这些公告将详细说明漏洞情况、受影响的版本以及缓解或修复问题的步骤。
### 3.3 沟通渠道
- **GitHub**:所有关于安全更新和公告的官方通知将发布在项目的 GitHub 仓库上。
- **电子邮件**:如果您订阅了项目的安全通知,您将收到有关重要安全更新的电子邮件通知。
## 4. 第三方依赖
我们会定期审查和更新项目中使用的第三方依赖,以确保其安全性。然而,第三方组件的安全性也依赖于其各自的维护者。如果您发现与第三方依赖相关的安全问题,请同时向相应的维护者报告并告知我们。
## 5. 安全最佳实践
我们鼓励项目的所有贡献者和用户遵循以下安全最佳实践:
- 及时更新开发和生产环境,安装最新的安全补丁。
- 使用强大的身份验证和授权机制。
- 避免在代码中硬编码凭证信息。
- 定期审查代码,排查潜在的安全漏洞。
## 6. 联系信息
如果您对 [go-stock] 项目的安全策略有任何疑问或担忧,请通过 [sparkmemory@163.com] 联系我们。

1105
app.go

File diff suppressed because it is too large Load Diff

61
app_common.go Normal file
View File

@ -0,0 +1,61 @@
package main
import (
"go-stock/backend/data"
"go-stock/backend/models"
)
// @Author spark
// @Date 2025/6/8 20:45
// @Desc
//-----------------------------------------------------------------------------------
func (a *App) LongTigerRank(date string) *[]models.LongTigerRankData {
return data.NewMarketNewsApi().LongTiger(date)
}
func (a *App) StockResearchReport(stockCode string) []any {
return data.NewMarketNewsApi().StockResearchReport(stockCode, 7)
}
func (a *App) StockNotice(stockCode string) []any {
return data.NewMarketNewsApi().StockNotice(stockCode)
}
func (a *App) IndustryResearchReport(industryCode string) []any {
return data.NewMarketNewsApi().IndustryResearchReport(industryCode, 7)
}
func (a App) EMDictCode(code string) []any {
return data.NewMarketNewsApi().EMDictCode(code, a.cache)
}
func (a App) AnalyzeSentiment(text string) data.SentimentResult {
return data.AnalyzeSentiment(text)
}
func (a App) HotStock(marketType string) *[]models.HotItem {
return data.NewMarketNewsApi().XUEQIUHotStock(100, marketType)
}
func (a App) HotEvent(size int) *[]models.HotEvent {
if size <= 0 {
size = 10
}
return data.NewMarketNewsApi().HotEvent(size)
}
func (a App) HotTopic(size int) []any {
if size <= 0 {
size = 10
}
return data.NewMarketNewsApi().HotTopic(size)
}
func (a App) InvestCalendarTimeLine(yearMonth string) []any {
return data.NewMarketNewsApi().InvestCalendar(yearMonth)
}
func (a App) ClsCalendar() []any {
return data.NewMarketNewsApi().ClsCalendar()
}
func (a App) SearchStock(words string) map[string]any {
return data.NewSearchStockApi(words).SearchStock()
}

460
app_darwin.go Normal file
View File

@ -0,0 +1,460 @@
//go:build darwin
// +build darwin
package main
import (
"context"
"fmt"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/duke-git/lancet/v2/slice"
"github.com/go-resty/resty/v2"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
cache *freecache.Cache
}
// NewApp creates a new App application struct
func NewApp() *App {
cacheSize := 512 * 1024
cache := freecache.NewCache(cacheSize)
return &App{
cache: cache,
}
}
// startup is called at application startup
func (a *App) startup(ctx context.Context) {
logger.SugaredLogger.Infof("Version:%s", Version)
// Perform your setup here
a.ctx = ctx
// TODO 创建系统托盘
}
func checkUpdate(a *App) {
releaseVersion := &models.GitHubReleaseVersion{}
_, err := resty.New().R().
SetResult(releaseVersion).
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
if err != nil {
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
return
}
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
if releaseVersion.TagName != Version {
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
}
}
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
// Add your action here
//定时更新数据
go func() {
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
interval := config.RefreshInterval
if interval <= 0 {
interval = 1
}
ticker := time.NewTicker(time.Second * time.Duration(interval))
defer ticker.Stop()
for range ticker.C {
if isTradingTime(time.Now()) {
MonitorStockPrices(a)
}
}
}()
go func() {
ticker := time.NewTicker(time.Second * time.Duration(60))
defer ticker.Stop()
for range ticker.C {
telegraph := refreshTelegraphList()
if telegraph != nil {
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
}
}
}()
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
go MonitorStockPrices(a)
//检查新版本
go func() {
checkUpdate(a)
}()
}
func refreshTelegraphList() *[]string {
url := "https://www.cls.cn/telegraph"
response, err := resty.New().R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
if err != nil {
return &[]string{}
}
//logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
if err != nil {
return &[]string{}
}
var telegraph []string
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
//logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, selection.Text())
})
return &telegraph
}
// isTradingDay 判断是否是交易日
func isTradingDay(date time.Time) bool {
weekday := date.Weekday()
// 判断是否是周末
if weekday == time.Saturday || weekday == time.Sunday {
return false
}
// 这里可以添加具体的节假日判断逻辑
// 例如:判断是否是春节、国庆节等
return true
}
// isTradingTime 判断是否是交易时间
func isTradingTime(date time.Time) bool {
if !isTradingDay(date) {
return false
}
hour, minute, _ := date.Clock()
// 判断是否在9:15到11:30之间
if (hour == 9 && minute >= 15) || (hour == 10) || (hour == 11 && minute <= 30) {
return true
}
// 判断是否在13:00到15:00之间
if (hour == 13) || (hour == 14) || (hour == 15 && minute <= 0) {
return true
}
return false
}
func MonitorStockPrices(a *App) {
dest := &[]data.FollowedStock{}
db.Dao.Model(&data.FollowedStock{}).Find(dest)
total := float64(0)
//for _, follow := range *dest {
// stockData := getStockInfo(follow)
// total += stockData.ProfitAmountToday
// price, _ := convertor.ToFloat(stockData.Price)
// if stockData.PrePrice != price {
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
// }
//}
stockInfos := GetStockInfos(*dest...)
for _, stockInfo := range *stockInfos {
total += stockInfo.ProfitAmountToday
price, _ := convertor.ToFloat(stockInfo.Price)
if stockInfo.PrePrice != price {
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
}
}
if total != 0 {
// title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
// systray.SetTooltip(title)
}
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
//runtime.WindowSetTitle(a.ctx, title)
}
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
stockCodes := make([]string, 0)
for _, follow := range follows {
stockCodes = append(stockCodes, follow.StockCode)
}
stockData, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
if err != nil {
logger.SugaredLogger.Errorf("get stock code real time data error:%s", err.Error())
return nil
}
stockInfos := make([]data.StockInfo, 0)
for _, info := range *stockData {
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
return follow.StockCode == info.Code
})
if ok {
addStockFollowData(v, &info)
stockInfos = append(stockInfos, info)
}
}
return &stockInfos
}
func getStockInfo(follow data.FollowedStock) *data.StockInfo {
stockCode := follow.StockCode
stockDatas, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
if err != nil || len(*stockDatas) == 0 {
return &data.StockInfo{}
}
stockData := (*stockDatas)[0]
addStockFollowData(follow, &stockData)
return &stockData
}
func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
stockData.PrePrice = follow.Price //上次当前价格
stockData.Sort = follow.Sort
stockData.CostPrice = follow.CostPrice //成本价
stockData.CostVolume = follow.Volume //成本量
stockData.AlarmChangePercent = follow.AlarmChangePercent
stockData.AlarmPrice = follow.AlarmPrice
//当前价格
price, _ := convertor.ToFloat(stockData.Price)
//当前价格为0 时 使用卖一价格作为当前价格
if price == 0 {
price, _ = convertor.ToFloat(stockData.A1P)
}
//当前价格依然为0 时 使用买一报价作为当前价格
if price == 0 {
price, _ = convertor.ToFloat(stockData.B1P)
}
//昨日收盘价
preClosePrice, _ := convertor.ToFloat(stockData.PreClose)
//当前价格依然为0 时 使用昨日收盘价为当前价格
if price == 0 {
price = preClosePrice
}
//今日最高价
highPrice, _ := convertor.ToFloat(stockData.High)
if highPrice == 0 {
highPrice, _ = convertor.ToFloat(stockData.Open)
}
//今日最低价
lowPrice, _ := convertor.ToFloat(stockData.Low)
if lowPrice == 0 {
lowPrice, _ = convertor.ToFloat(stockData.Open)
}
//开盘价
//openPrice, _ := convertor.ToFloat(stockData.Open)
if price > 0 {
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
}
if highPrice > 0 {
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
}
if lowPrice > 0 {
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
}
if follow.CostPrice > 0 && follow.Volume > 0 {
if price > 0 {
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(price-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((price-follow.CostPrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = mathutil.RoundToFloat((price-preClosePrice)*float64(follow.Volume), 2)
} else {
//未开盘时当前价格为昨日收盘价
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
// 未开盘时,今日盈亏为 0
stockData.ProfitAmountToday = 0
}
}
//logger.SugaredLogger.Debugf("stockData:%+v", stockData)
if follow.Price != price && price > 0 {
go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{
"price": price,
})
}
}
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog,
Title: "go-stock",
Message: "确定关闭吗?",
Buttons: []string{"确定"},
Icon: icon,
CancelButton: "取消",
})
if err != nil {
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
return false
}
logger.SugaredLogger.Debugf("dialog:%s", dialog)
if dialog == "No" {
return true
}
return false
}
// shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) {
// Perform your teardown here
// systray.Quit()
}
// Greet returns a greeting for the given name
func (a *App) Greet(stockCode string) *data.StockInfo {
//stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
follow := &data.FollowedStock{
StockCode: stockCode,
}
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
stockInfo := getStockInfo(*follow)
return stockInfo
}
func (a *App) Follow(stockCode string) string {
return data.NewStockDataApi().Follow(stockCode)
}
func (a *App) UnFollow(stockCode string) string {
return data.NewStockDataApi().UnFollow(stockCode)
}
func (a *App) GetFollowList() []data.FollowedStock {
return data.NewStockDataApi().GetFollowList()
}
func (a *App) GetStockList(key string) []data.StockBasic {
return data.NewStockDataApi().GetStockList(key)
}
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
}
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
}
func (a *App) SetStockSort(sort int64, stockCode string) {
data.NewStockDataApi().SetStockSort(sort, stockCode)
}
func (a *App) SendDingDingMessage(message string, stockCode string) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
return data.NewDingDingAPI().SendDingDingMessage(message)
}
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
stockInfo := &data.StockInfo{}
db.Dao.Model(stockInfo).Where("code = ?", stockCode).First(stockInfo)
go data.NewAlertWindowsApi("go-stock消息通知", getMsgTypeName(msgType), GenNotificationMsg(stockInfo), "").SendNotification()
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) NewChat(stock string) string {
return data.NewDeepSeekOpenAi().NewChat(stock)
}
func (a *App) NewChatStream(stock, stockCode string) {
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
for msg := range msgs {
runtime.EventsEmit(a.ctx, "newChatStream", msg)
}
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
}
func GenNotificationMsg(stockInfo *data.StockInfo) string {
Price, err := convertor.ToFloat(stockInfo.Price)
if err != nil {
Price = 0
}
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
if err != nil {
PreClose = 0
}
var RF float64
if PreClose > 0 {
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
}
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
}
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
func getMsgTypeTTL(msgType int) int {
switch msgType {
case 1:
return 60 * 5
case 2:
return 60 * 30
case 3:
return 60 * 30
default:
return 60 * 5
}
}
func getMsgTypeName(msgType int) string {
switch msgType {
case 1:
return "涨跌报警"
case 2:
return "股价报警"
case 3:
return "成本价报警"
default:
return "未知类型"
}
}
func (a *App) UpdateConfig(settings *data.Settings) string {
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
return data.NewSettingsApi(settings).UpdateConfig()
}
func (a *App) GetConfig() *data.Settings {
return data.NewSettingsApi(&data.Settings{}).GetConfig()
}

193
app_linux.go Normal file
View File

@ -0,0 +1,193 @@
//go:build linux
// +build linux
package main
import (
"context"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/logger"
)
// App struct
type App struct {
ctx context.Context
cache *freecache.Cache
}
// NewApp creates a new App application struct
func NewApp() *App {
cacheSize := 512 * 1024
cache := freecache.NewCache(cacheSize)
return &App{
cache: cache,
}
}
// startup is called at application startup
func (a *App) startup(ctx context.Context) {
// Perform your setup here
a.ctx = ctx
}
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
// Add your action here
//ticker := time.NewTicker(time.Second)
//defer ticker.Stop()
////定时更新数据
//go func() {
// for range ticker.C {
// runtime.WindowSetTitle(ctx, "go-stock "+time.Now().Format("2006-01-02 15:04:05"))
// }
//}()
}
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog,
Title: "go-stock",
Message: "确定关闭吗?",
Buttons: []string{"确定"},
Icon: icon,
CancelButton: "取消",
})
if err != nil {
return false
}
logger.SugaredLogger.Debugf("dialog:%s", dialog)
if dialog == "No" {
return true
}
return false
}
// shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) {
// Perform your teardown here
}
// Greet returns a greeting for the given name
func (a *App) Greet(name string) *data.StockInfo {
stockDatas, _ := data.NewStockDataApi().GetStockCodeRealTimeData(name)
stockData := (*stockDatas)[0]
return &stockData
}
func (a *App) Follow(stockCode string) string {
return data.NewStockDataApi().Follow(stockCode)
}
func (a *App) UnFollow(stockCode string) string {
return data.NewStockDataApi().UnFollow(stockCode)
}
func (a *App) GetFollowList() []data.FollowedStock {
return data.NewStockDataApi().GetFollowList()
}
func (a *App) GetStockList(key string) []data.StockBasic {
return data.NewStockDataApi().GetStockList(key)
}
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
}
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
}
func (a *App) SendDingDingMessage(message string, stockCode string) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) SetStockSort(sort int64, stockCode string) {
data.NewStockDataApi().SetStockSort(sort, stockCode)
}
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
if err != nil {
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
return ""
}
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func GenNotificationMsg(stockInfo *data.StockInfo) string {
Price, err := convertor.ToFloat(stockInfo.Price)
if err != nil {
Price = 0
}
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
if err != nil {
PreClose = 0
}
var RF float64
if PreClose > 0 {
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
}
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
}
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
func getMsgTypeTTL(msgType int) int {
switch msgType {
case 1:
return 60 * 5
case 2:
return 60 * 30
case 3:
return 60 * 30
default:
return 60 * 5
}
}
func getMsgTypeName(msgType int) string {
switch msgType {
case 1:
return "涨跌报警"
case 2:
return "股价报警"
case 3:
return "成本价报警"
default:
return "未知类型"
}
}
func (a *App) UpdateConfig(settings *data.Settings) string {
return data.NewSettingsApi(settings).UpdateConfig()
}
func (a *App) GetConfig() *data.Settings {
return data.NewSettingsApi(&data.Settings{}).GetConfig()
}

25
app_test.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"go-stock/backend/logger"
"testing"
"time"
)
// @Author spark
// @Date 2025/2/24 9:35
// @Desc
// -----------------------------------------------------------------------------------
func TestIsHKTradingTime(t *testing.T) {
f := IsHKTradingTime(time.Now())
t.Log(f)
}
func TestIsUSTradingTime(t *testing.T) {
date := time.Now()
hour, minute, _ := date.Clock()
logger.SugaredLogger.Infof("当前时间: %d:%d", hour, minute)
t.Log(IsUSTradingTime(time.Now()))
}

View File

@ -0,0 +1,52 @@
//go:build darwin
// +build darwin
package data
import (
"fmt"
"go-stock/backend/logger"
"os/exec"
)
// AlertWindowsApi @Author 2lovecode
// @Date 2025/02/06 17:50
// @Desc
// -----------------------------------------------------------------------------------
type AlertWindowsApi struct {
AppID string
// 窗口标题
Title string
// 窗口内容
Content string
// 窗口图标
Icon string
}
func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string) *AlertWindowsApi {
return &AlertWindowsApi{
AppID: AppID,
Title: Title,
Content: Content,
Icon: Icon,
}
}
func (a AlertWindowsApi) SendNotification() bool {
if GetConfig().LocalPushEnable == false {
logger.SugaredLogger.Error("本地推送未开启")
return false
}
script := fmt.Sprintf(`display notification "%s" with title "%s"`, a.Content, a.Title)
cmd := exec.Command("osascript", "-e", script)
err := cmd.Run()
if err != nil {
logger.SugaredLogger.Error(err)
return false
}
return true
}

View File

@ -0,0 +1,32 @@
//go:build darwin
// +build darwin
package data
import (
"go-stock/backend/logger"
"testing"
"github.com/go-toast/toast"
)
// @Author 2lovecode
// @Date 2025/02/06 17:50
// @Desc
// -----------------------------------------------------------------------------------
func TestAlert(t *testing.T) {
notification := toast.Notification{
AppID: "go-stock",
Title: "Hello, World!",
Message: "This is a toast notification.",
Icon: "../../build/appicon.png",
Duration: "short",
Audio: toast.Default,
}
err := notification.Push()
if err != nil {
logger.SugaredLogger.Error(err)
return
}
}

View File

@ -0,0 +1,53 @@
//go:build windows
package data
import (
"github.com/go-toast/toast"
"go-stock/backend/logger"
)
// AlertWindowsApi @Author spark
// @Date 2025/1/8 9:40
// @Desc
// -----------------------------------------------------------------------------------
type AlertWindowsApi struct {
AppID string
// 窗口标题
Title string
// 窗口内容
Content string
// 窗口图标
Icon string
}
func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string) *AlertWindowsApi {
return &AlertWindowsApi{
AppID: AppID,
Title: Title,
Content: Content,
Icon: Icon,
}
}
func (a AlertWindowsApi) SendNotification() bool {
if GetConfig().LocalPushEnable == false {
logger.SugaredLogger.Error("本地推送未开启")
return false
}
notification := toast.Notification{
AppID: a.AppID,
Title: a.Title,
Message: a.Content,
Icon: a.Icon,
Duration: "short",
Audio: toast.Default,
}
err := notification.Push()
if err != nil {
logger.SugaredLogger.Error(err)
return false
}
return true
}

View File

@ -0,0 +1,30 @@
//go:build windows
package data
import (
"github.com/go-toast/toast"
"go-stock/backend/logger"
"testing"
)
// @Author spark
// @Date 2025/1/8 9:40
// @Desc
//-----------------------------------------------------------------------------------
func TestAlert(t *testing.T) {
notification := toast.Notification{
AppID: "go-stock",
Title: "Hello, World!",
Message: "This is a toast notification.",
Icon: "../../build/appicon.png",
Duration: "short",
Audio: toast.Default,
}
err := notification.Push()
if err != nil {
logger.SugaredLogger.Error(err)
return
}
}

237
backend/data/crawler_api.go Normal file
View File

@ -0,0 +1,237 @@
package data
import (
"context"
"github.com/chromedp/chromedp"
"go-stock/backend/logger"
"time"
)
// @Author spark
// @Date 2025/2/13 9:25
// @Desc
// -----------------------------------------------------------------------------------
type CrawlerApi struct {
crawlerCtx context.Context
crawlerBaseInfo CrawlerBaseInfo
pool *BrowserPool
}
func (c *CrawlerApi) NewTimeOutCrawler(timeout int, crawlerBaseInfo CrawlerBaseInfo) CrawlerApi {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
return c.NewCrawler(ctx, crawlerBaseInfo)
}
func (c *CrawlerApi) NewCrawler(ctx context.Context, crawlerBaseInfo CrawlerBaseInfo) CrawlerApi {
return CrawlerApi{
crawlerCtx: ctx,
crawlerBaseInfo: crawlerBaseInfo,
pool: NewBrowserPool(GetConfig().BrowserPoolSize),
}
}
func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bool) {
page, err := c.pool.FetchPage(url, waitVisible)
if err != nil {
return "", false
}
return page, true
}
func (c *CrawlerApi) GetHtml_old(url, waitVisible string, headless bool) (string, bool) {
htmlContent := ""
path := GetConfig().BrowserPath
//logger.SugaredLogger.Infof("Browser path:%s", path)
if path != "" {
pctx, pcancel := chromedp.NewExecAllocator(
c.crawlerCtx,
chromedp.ExecPath(path),
chromedp.Flag("headless", headless),
chromedp.Flag("blink-settings", "imagesEnabled=false"),
chromedp.Flag("disable-javascript", false),
chromedp.Flag("disable-gpu", true),
chromedp.UserAgent(c.crawlerBaseInfo.Headers["User-Agent"]),
chromedp.Flag("disable-background-networking", true),
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
chromedp.Flag("disable-background-timer-throttling", true),
chromedp.Flag("disable-backgrounding-occluded-windows", true),
chromedp.Flag("disable-breakpad", true),
chromedp.Flag("disable-client-side-phishing-detection", true),
chromedp.Flag("disable-default-apps", true),
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-extensions", true),
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
chromedp.Flag("disable-hang-monitor", true),
chromedp.Flag("disable-ipc-flooding-protection", true),
chromedp.Flag("disable-popup-blocking", true),
chromedp.Flag("disable-prompt-on-repost", true),
chromedp.Flag("disable-renderer-backgrounding", true),
chromedp.Flag("disable-sync", true),
chromedp.Flag("force-color-profile", "srgb"),
chromedp.Flag("metrics-recording-only", true),
chromedp.Flag("safebrowsing-disable-auto-update", true),
chromedp.Flag("enable-automation", true),
chromedp.Flag("password-store", "basic"),
chromedp.Flag("use-mock-keychain", true),
)
defer pcancel()
ctx, cancel := chromedp.NewContext(pctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
defer cancel()
//defer chromedp.Cancel(ctx)
err := chromedp.Run(ctx, chromedp.Navigate(url),
chromedp.WaitVisible(waitVisible, chromedp.ByQuery), // 确保 元素可见
chromedp.WaitReady(waitVisible, chromedp.ByQuery), // 确保 元素准备好
chromedp.InnerHTML("body", &htmlContent),
)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "", false
}
} else {
ctx, cancel := chromedp.NewContext(c.crawlerCtx, chromedp.WithLogf(logger.SugaredLogger.Infof))
defer cancel()
//defer chromedp.Cancel(ctx)
err := chromedp.Run(ctx, chromedp.Navigate(url), chromedp.WaitVisible("body"), chromedp.InnerHTML("body", &htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "", false
}
}
return htmlContent, true
}
func (c *CrawlerApi) GetHtmlWithNoCancel(url, waitVisible string, headless bool) (html string, ok bool, parent context.CancelFunc, child context.CancelFunc) {
htmlContent := ""
path := GetConfig().BrowserPath
//logger.SugaredLogger.Infof("BrowserPath :%s", path)
var parentCancel context.CancelFunc
var childCancel context.CancelFunc
var pctx context.Context
var cctx context.Context
if path != "" {
pctx, parentCancel = chromedp.NewExecAllocator(
c.crawlerCtx,
chromedp.ExecPath(path),
chromedp.Flag("headless", headless),
chromedp.Flag("blink-settings", "imagesEnabled=false"),
chromedp.Flag("disable-javascript", false),
chromedp.Flag("disable-gpu", true),
chromedp.UserAgent(c.crawlerBaseInfo.Headers["User-Agent"]),
chromedp.Flag("disable-background-networking", true),
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
chromedp.Flag("disable-background-timer-throttling", true),
chromedp.Flag("disable-backgrounding-occluded-windows", true),
chromedp.Flag("disable-breakpad", true),
chromedp.Flag("disable-client-side-phishing-detection", true),
chromedp.Flag("disable-default-apps", true),
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-extensions", true),
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
chromedp.Flag("disable-hang-monitor", true),
chromedp.Flag("disable-ipc-flooding-protection", true),
chromedp.Flag("disable-popup-blocking", true),
chromedp.Flag("disable-prompt-on-repost", true),
chromedp.Flag("disable-renderer-backgrounding", true),
chromedp.Flag("disable-sync", true),
chromedp.Flag("force-color-profile", "srgb"),
chromedp.Flag("metrics-recording-only", true),
chromedp.Flag("safebrowsing-disable-auto-update", true),
chromedp.Flag("enable-automation", true),
chromedp.Flag("password-store", "basic"),
chromedp.Flag("use-mock-keychain", true),
)
//defer pcancel()
cctx, childCancel = chromedp.NewContext(pctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
//defer cancel()
err := chromedp.Run(cctx, chromedp.Navigate(url),
chromedp.WaitVisible(waitVisible, chromedp.ByQuery), // 确保 元素可见
chromedp.WaitReady(waitVisible, chromedp.ByQuery), // 确保 元素准备好
chromedp.InnerHTML("body", &htmlContent),
)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "", false, parentCancel, childCancel
}
} else {
cctx, childCancel = chromedp.NewContext(c.crawlerCtx, chromedp.WithLogf(logger.SugaredLogger.Infof))
//defer cancel()
err := chromedp.Run(cctx, chromedp.Navigate(url), chromedp.WaitVisible("body"), chromedp.InnerHTML("body", &htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "", false, parentCancel, childCancel
}
}
return htmlContent, true, parentCancel, childCancel
}
func (c *CrawlerApi) GetHtmlWithActions(actions *[]chromedp.Action, headless bool) (string, bool) {
htmlContent := ""
*actions = append(*actions, chromedp.InnerHTML("body", &htmlContent))
path := GetConfig().BrowserPath
//logger.SugaredLogger.Infof("GetHtmlWithActions path:%s", path)
if path != "" {
pctx, pcancel := chromedp.NewExecAllocator(
c.crawlerCtx,
chromedp.ExecPath(path),
chromedp.Flag("headless", headless),
chromedp.Flag("blink-settings", "imagesEnabled=false"),
chromedp.Flag("disable-javascript", false),
chromedp.Flag("disable-gpu", true),
chromedp.UserAgent(c.crawlerBaseInfo.Headers["User-Agent"]),
chromedp.Flag("disable-background-networking", true),
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
chromedp.Flag("disable-background-timer-throttling", true),
chromedp.Flag("disable-backgrounding-occluded-windows", true),
chromedp.Flag("disable-breakpad", true),
chromedp.Flag("disable-client-side-phishing-detection", true),
chromedp.Flag("disable-default-apps", true),
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-extensions", true),
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
chromedp.Flag("disable-hang-monitor", true),
chromedp.Flag("disable-ipc-flooding-protection", true),
chromedp.Flag("disable-popup-blocking", true),
chromedp.Flag("disable-prompt-on-repost", true),
chromedp.Flag("disable-renderer-backgrounding", true),
chromedp.Flag("disable-sync", true),
chromedp.Flag("force-color-profile", "srgb"),
chromedp.Flag("metrics-recording-only", true),
chromedp.Flag("safebrowsing-disable-auto-update", true),
chromedp.Flag("enable-automation", true),
chromedp.Flag("password-store", "basic"),
chromedp.Flag("use-mock-keychain", true),
)
defer pcancel()
ctx, cancel := chromedp.NewContext(pctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
defer cancel()
//defer chromedp.Cancel(ctx)
err := chromedp.Run(ctx, *actions...)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "", false
}
} else {
ctx, cancel := chromedp.NewContext(c.crawlerCtx, chromedp.WithLogf(logger.SugaredLogger.Infof))
defer cancel()
//defer chromedp.Cancel(ctx)
err := chromedp.Run(ctx, *actions...)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "", false
}
}
return htmlContent, true
}
type CrawlerBaseInfo struct {
Name string `json:"name"`
Description string `json:"description"`
BaseUrl string `json:"base_url"`
Headers map[string]string `json:"headers"`
}

View File

@ -0,0 +1,371 @@
package data
import (
"context"
"encoding/json"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/duke-git/lancet/v2/strutil"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"os"
"strings"
"testing"
"time"
"github.com/chromedp/chromedp"
"github.com/stretchr/testify/assert"
)
func TestNewTimeOutGuShiTongCrawler(t *testing.T) {
crawlerAPI := CrawlerApi{}
timeout := 10
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://gushitong.baidu.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
result := crawlerAPI.NewTimeOutCrawler(timeout, crawlerBaseInfo)
assert.NotNil(t, result.crawlerCtx)
assert.Equal(t, crawlerBaseInfo, result.crawlerBaseInfo)
}
func TestNewGuShiTongCrawler(t *testing.T) {
crawlerAPI := CrawlerApi{}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://gushitong.baidu.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
result := crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
assert.Equal(t, ctx, result.crawlerCtx)
assert.Equal(t, crawlerBaseInfo, result.crawlerBaseInfo)
}
func TestGetHtml(t *testing.T) {
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://gushitong.baidu.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
url := "https://www.cls.cn/searchPage?type=depth&keyword=%E6%96%B0%E5%B8%8C%E6%9C%9B"
waitVisible := ".search-telegraph-list,.subject-interest-list"
//url = "https://gushitong.baidu.com/stock/ab-600745"
//waitVisible = "div.news-item"
htmlContent, success := crawlerAPI.GetHtml(url, waitVisible, true)
if success {
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
var messages []string
document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
messages = append(messages, text)
logger.SugaredLogger.Infof("搜索到消息-%s: %s", "", text)
})
}
//logger.SugaredLogger.Infof("htmlContent:%s", htmlContent)
}
func TestGetHtmlWithActions(t *testing.T) {
crawlerAPI := CrawlerApi{}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, CrawlerBaseInfo{
Name: "百度股市通",
Description: "Test Crawler Description",
BaseUrl: "https://gushitong.baidu.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
})
actions := []chromedp.Action{
chromedp.Navigate("https://gushitong.baidu.com/stock/ab-600745"),
chromedp.WaitVisible("div.cos-tab"),
chromedp.Click(".header div.cos-tab:nth-child(6)", chromedp.ByQuery),
chromedp.ScrollIntoView("div.finance-container >div.row:nth-child(3)"),
chromedp.WaitVisible("div.cos-tabs-header-container"),
chromedp.Click(".page-content .cos-tabs-header-container .cos-tabs-header .cos-tab:nth-child(1)", chromedp.ByQuery),
chromedp.WaitVisible(".page-content .finance-container .report-col-content", chromedp.ByQuery),
chromedp.Click(".page-content .cos-tabs-header-container .cos-tabs-header .cos-tab:nth-child(4)", chromedp.ByQuery),
chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight);`, nil),
chromedp.Sleep(1 * time.Second),
}
htmlContent, success := crawlerAPI.GetHtmlWithActions(&actions, false)
if success {
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
var messages []string
document.Find("div.report-table-list-container,div.report-row").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveWhiteSpace(selection.Text(), false)
messages = append(messages, text)
logger.SugaredLogger.Infof("搜索到消息-%s: %s", "", text)
})
logger.SugaredLogger.Infof("messages:%d", len(messages))
}
//logger.SugaredLogger.Infof("htmlContent:%s", htmlContent)
}
func TestHk(t *testing.T) {
//https://stock.finance.sina.com.cn/hkstock/quotes/00001.html
db.Init("../../data/stock.db")
hks := &[]models.StockInfoHK{}
db.Dao.Model(&models.StockInfoHK{}).Limit(1).Find(hks)
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://stock.finance.sina.com.cn",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
for _, hk := range *hks {
logger.SugaredLogger.Infof("hk: %+v", hk)
url := fmt.Sprintf("https://stock.finance.sina.com.cn/hkstock/quotes/%s.html", strings.ReplaceAll(hk.Code, ".HK", ""))
htmlContent, ok := crawlerAPI.GetHtml(url, "#stock_cname", true)
if !ok {
continue
}
//logger.SugaredLogger.Infof("htmlContent: %s", htmlContent)
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
document.Find("#stock_cname").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Infof("股票名称-:%s", text)
})
document.Find("#mts_stock_hk_price").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Infof("股票名称-现价: %s", text)
})
document.Find(".deta_hqContainer >.deta03 li").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveNonPrintable(selection.Text())
logger.SugaredLogger.Infof("股票名称-%s: %s", "", text)
})
}
}
func TestUpdateUSName(t *testing.T) {
db.Init("../../data/stock.db")
us := &[]models.StockInfoUS{}
db.Dao.Model(&models.StockInfoUS{}).Where("name = ?", "").Order("RANDOM()").Find(us)
for _, us := range *us {
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://stock.finance.sina.com.cn",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
url := fmt.Sprintf("https://stock.finance.sina.com.cn/usstock/quotes/%s.html", us.Code[:len(us.Code)-3])
logger.SugaredLogger.Infof("url: %s", url)
//waitVisible := "span.quote_title_name"
waitVisible := "div.hq_title > h1"
htmlContent, ok := crawlerAPI.GetHtml(url, waitVisible, true)
if !ok {
continue
}
//logger.SugaredLogger.Infof("htmlContent: %s", htmlContent)
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
name := ""
document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) {
name = strutil.RemoveNonPrintable(selection.Text())
name = strutil.SplitAndTrim(name, " ", "")[0]
logger.SugaredLogger.Infof("股票名称-:%s", name)
})
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", us.Code).Updates(map[string]interface{}{
"name": name,
"full_name": name,
})
}
}
func TestUS(t *testing.T) {
db.Init("../../data/stock.db")
bytes, err := os.ReadFile("../../build/us.json")
if err != nil {
return
}
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://quote.eastmoney.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
tick := &Tick{}
json.Unmarshal(bytes, &tick)
for i, datum := range tick.Data {
logger.SugaredLogger.Infof("datum: %d, %+v", i, datum)
name := ""
//https://quote.eastmoney.com/us/AAPL.html
//https://stock.finance.sina.com.cn/usstock/quotes/goog.html
//url := fmt.Sprintf("https://stock.finance.sina.com.cn/usstock/quotes/%s.html", strings.ReplaceAll(datum.C, ".US", ""))
////waitVisible := "span.quote_title_name"
//waitVisible := "div.hq_title > h1"
//
//htmlContent, ok := crawlerAPI.GetHtml(url, waitVisible, true)
//
//if !ok {
// continue
//}
////logger.SugaredLogger.Infof("htmlContent: %s", htmlContent)
//document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
//if err != nil {
// logger.SugaredLogger.Error(err.Error())
//}
//document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) {
// name = strutil.RemoveNonPrintable(selection.Text())
// name = strutil.SplitAndTrim(name, " ", "")[0]
// logger.SugaredLogger.Infof("股票名称-:%s", name)
//})
us := &models.StockInfoUS{
Code: datum.C + ".US",
EName: datum.N,
FullName: datum.N,
Name: name,
Exchange: datum.E,
Type: datum.T,
}
db.Dao.Create(us)
}
}
func TestUSSINA(t *testing.T) {
//https://finance.sina.com.cn/stock/usstock/sector.shtml#cm
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://quote.eastmoney.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
html, ok := crawlerAPI.GetHtml("https://finance.sina.com.cn/stock/usstock/sector.shtml#cm", "div#data", false)
if !ok {
return
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
document.Find("div#data > table >tbody >tr").Each(func(i int, selection *goquery.Selection) {
tr := selection.Text()
logger.SugaredLogger.Infof("tr: %s", tr)
})
}
func TestSina(t *testing.T) {
db.Init("../../data/stock.db")
url := "https://finance.sina.com.cn/realstock/company/sz002906/nc.shtml"
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://finance.sina.com.cn",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
html, ok := crawlerAPI.GetHtml(url, "div#hqDetails table", true)
if !ok {
return
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
//price
price := strutil.RemoveWhiteSpace(document.Find("div#price").First().Text(), false)
hqTime := strutil.RemoveWhiteSpace(document.Find("div#hqTime").First().Text(), false)
var markdown strings.Builder
markdown.WriteString("\n ## 当前股票数据:\n")
markdown.WriteString(fmt.Sprintf("### 当前股价:%s 时间:%s\n", price, hqTime))
GetTableMarkdown(document, "div#hqDetails table", &markdown)
}
func TestDC(t *testing.T) {
url := "https://emweb.securities.eastmoney.com/pc_hsf10/pages/index.html?type=web&code=sh600745#/cwfx"
db.Init("../../data/stock.db")
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://emweb.securities.eastmoney.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
var markdown strings.Builder
markdown.WriteString("\n ## 财务数据:\n")
html, ok := crawlerAPI.GetHtml(url, "div.report_table table", false)
if !ok {
return
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
GetTableMarkdown(document, "div.report_table table", &markdown)
}
type Tick struct {
Code int `json:"code"`
Status string `json:"status"`
Data []struct {
C string `json:"c"`
N string `json:"n"`
T string `json:"t"`
E string `json:"e"`
} `json:"data"`
}

View File

@ -0,0 +1,85 @@
package data
import (
"github.com/go-resty/resty/v2"
"go-stock/backend/logger"
)
// @Author spark
// @Date 2025/1/3 13:53
// @Desc
//-----------------------------------------------------------------------------------
type DingDingAPI struct {
client *resty.Client
}
func NewDingDingAPI() *DingDingAPI {
return &DingDingAPI{
client: resty.New(),
}
}
func (DingDingAPI) SendDingDingMessage(message string) string {
if GetConfig().DingPushEnable == false {
//logger.SugaredLogger.Info("钉钉推送未开启")
return "钉钉推送未开启"
}
// 发送钉钉消息
resp, err := resty.New().R().
SetHeader("Content-Type", "application/json").
SetBody(message).
Post(getApiURL())
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "发送钉钉消息失败"
}
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
return "发送钉钉消息成功"
}
func GetConfig() *Settings {
return NewSettingsApi(&Settings{}).GetConfig()
}
func getApiURL() string {
return GetConfig().DingRobot
}
func (DingDingAPI) SendToDingDing(title, message string) string {
// 发送钉钉消息
resp, err := resty.New().R().
SetHeader("Content-Type", "application/json").
SetBody(&Message{
Msgtype: "markdown",
Markdown: Markdown{
Title: "go-stock " + title,
Text: message,
},
At: At{
IsAtAll: true,
},
}).
Post(getApiURL())
if err != nil {
logger.SugaredLogger.Error(err.Error())
return "发送钉钉消息失败"
}
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
return "发送钉钉消息成功"
}
type Message struct {
Msgtype string `json:"msgtype"`
Markdown Markdown `json:"markdown"`
At At `json:"at"`
}
type Markdown struct {
Title string `json:"title"`
Text string `json:"text"`
}
type At struct {
AtMobiles []string `json:"atMobiles"`
AtUserIds []string `json:"atUserIds"`
IsAtAll bool `json:"isAtAll"`
}

View File

@ -0,0 +1,32 @@
package data
import (
"github.com/go-resty/resty/v2"
"testing"
)
// @Author spark
// @Date 2025/1/3 13:53
// @Desc
//-----------------------------------------------------------------------------------
func TestRobot(t *testing.T) {
dingdingRobotUrl := "XXX"
resp, err := resty.New().R().
SetHeader("Content-Type", "application/json").
SetBody(`{
"msgtype": "markdown",
"markdown": {
"title":"go-stock",
"text": "#### 杭州天气 @150XXXXXXXX \n > 9度西北风1级空气良89相对温度73%\n > ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n > ###### 10点20分发布 [天气](https://www.dingtalk.com) \n"
},
"at": {
"isAtAll": true
}
}`).
Post(dingdingRobotUrl)
if err != nil {
t.Error(err)
}
t.Log(resp.String())
}

View File

@ -0,0 +1,401 @@
package data
import (
"context"
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/mathutil"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"go-stock/backend/db"
"go-stock/backend/logger"
"strconv"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"gorm.io/gorm"
)
type FundApi struct {
client *resty.Client
config *Settings
}
func NewFundApi() *FundApi {
return &FundApi{
client: resty.New(),
config: GetConfig(),
}
}
type FollowedFund struct {
gorm.Model
Code string `json:"code" gorm:"index"` // 基金代码
Name string `json:"name"` // 基金简称
NetUnitValue *float64 `json:"netUnitValue"` // 单位净值
NetUnitValueDate string `json:"netUnitValueDate"` // 单位净值日期
NetEstimatedUnit *float64 `json:"netEstimatedUnit"` // 估算单位净值
NetEstimatedTime string `json:"netEstimatedUnitTime"` // 估算单位净值日期
NetAccumulated *float64 `json:"netAccumulated"` // 累计净值
//计算值
NetEstimatedRate *float64 `json:"netEstimatedRate"` // 估算单位净值涨跌幅
FundBasic FundBasic `json:"fundBasic" gorm:"foreignKey:Code;references:Code"`
}
func (FollowedFund) TableName() string {
return "followed_fund"
}
// FundBasic 基金基本信息结构体
type FundBasic struct {
gorm.Model
Code string `json:"code" gorm:"index"` // 基金代码
Name string `json:"name"` // 基金简称
FullName string `json:"fullName"` // 基金全称
Type string `json:"type"` // 基金类型
Establishment string `json:"establishment"` // 成立日期
Scale string `json:"scale"` // 最新规模(亿元)
Company string `json:"company"` // 基金管理人
Manager string `json:"manager"` // 基金经理
Rating string `json:"rating"` //基金评级
TrackingTarget string `json:"trackingTarget"` //跟踪标的
NetUnitValue *float64 `json:"netUnitValue"` // 单位净值
NetUnitValueDate string `json:"netUnitValueDate"` // 单位净值日期
NetEstimatedUnit *float64 `json:"netEstimatedUnit"` // 估算单位净值
NetEstimatedTime string `json:"netEstimatedUnitTime"` // 估算单位净值日期
NetAccumulated *float64 `json:"netAccumulated"` // 累计净值
//净值涨跌幅: 近1月,近3月,近6月,近1年,近3年,近5年,今年来,成立来
NetGrowth1 *float64 `json:"netGrowth1"` //近1月
NetGrowth3 *float64 `json:"netGrowth3"` //近3月
NetGrowth6 *float64 `json:"netGrowth6"` //近6月
NetGrowth12 *float64 `json:"netGrowth12"` //近1年
NetGrowth36 *float64 `json:"netGrowth36"` //近3年
NetGrowth60 *float64 `json:"netGrowth60"` //近5年
NetGrowthYTD *float64 `json:"netGrowthYTD"` //今年来
NetGrowthAll *float64 `json:"netGrowthAll"` //成立来
}
func (FundBasic) TableName() string {
return "fund_basic"
}
// CrawlFundBasic 爬取基金基本信息
func (f *FundApi) CrawlFundBasic(fundCode string) (*FundBasic, error) {
defer func() {
if r := recover(); r != nil {
logger.SugaredLogger.Errorf("CrawlFundBasic panic: %v", r)
}
}()
crawler := CrawlerApi{
crawlerBaseInfo: CrawlerBaseInfo{
Name: "天天基金",
BaseUrl: "http://fund.eastmoney.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"},
},
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(f.config.CrawlTimeOut)*time.Second)
defer cancel()
crawler = crawler.NewCrawler(ctx, crawler.crawlerBaseInfo)
url := fmt.Sprintf("%s/%s.html", crawler.crawlerBaseInfo.BaseUrl, fundCode)
//logger.SugaredLogger.Infof("CrawlFundBasic url:%s", url)
// 使用现有爬虫框架解析页面
htmlContent, ok := crawler.GetHtml(url, ".merchandiseDetail", true)
if !ok {
return nil, fmt.Errorf("页面解析失败")
}
fund := &FundBasic{Code: fundCode}
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
return nil, err
}
// 解析基础信息
name := doc.Find(".merchandiseDetail .fundDetail-tit").First().Text()
fund.Name = strings.TrimSpace(strutil.ReplaceWithMap(name, map[string]string{"查看相关ETF>": ""}))
//logger.SugaredLogger.Infof("基金名称:%s", fund.Name)
doc.Find(".infoOfFund table td ").Each(func(i int, s *goquery.Selection) {
text := strutil.RemoveWhiteSpace(s.Text(), true)
//logger.SugaredLogger.Infof("基金信息:%+v", text)
defer func() {
if r := recover(); r != nil {
//logger.SugaredLogger.Errorf("panic: %v", r)
}
}()
splitEx := strutil.SplitEx(text, "", true)
if strutil.ContainsAny(text, []string{"基金类型", "类型"}) {
fund.Type = splitEx[1]
}
if strutil.ContainsAny(text, []string{"成立日期", "成立日"}) {
fund.Establishment = splitEx[1]
}
if strutil.ContainsAny(text, []string{"基金规模", "规模"}) {
fund.Scale = splitEx[1]
}
if strutil.ContainsAny(text, []string{"管理人", "基金公司"}) {
fund.Company = splitEx[1]
}
if strutil.ContainsAny(text, []string{"基金经理", "经理人"}) {
fund.Manager = splitEx[1]
}
if strutil.ContainsAny(text, []string{"基金评级", "评级"}) {
fund.Rating = splitEx[1]
}
if strutil.ContainsAny(text, []string{"跟踪标的", "标的"}) {
fund.TrackingTarget = splitEx[1]
}
})
//获取基金净值涨跌幅信息
doc.Find(".dataOfFund dl > dd").Each(func(i int, s *goquery.Selection) {
text := strutil.RemoveWhiteSpace(s.Text(), true)
//logger.SugaredLogger.Infof("净值涨跌幅信息:%+v", text)
defer func() {
if r := recover(); r != nil {
//logger.SugaredLogger.Errorf("panic: %v", r)
}
}()
splitEx := strutil.SplitAndTrim(text, "", "%")
toFloat, err1 := convertor.ToFloat(splitEx[1])
if err1 != nil {
//logger.SugaredLogger.Errorf("转换失败:%+v", err)
return
}
//logger.SugaredLogger.Infof("净值涨跌幅信息:%+v", toFloat)
if strutil.ContainsAny(text, []string{"近1月"}) {
fund.NetGrowth1 = &toFloat
}
if strutil.ContainsAny(text, []string{"近3月"}) {
fund.NetGrowth3 = &toFloat
}
if strutil.ContainsAny(text, []string{"近6月"}) {
fund.NetGrowth6 = &toFloat
}
if strutil.ContainsAny(text, []string{"近1年"}) {
fund.NetGrowth12 = &toFloat
}
if strutil.ContainsAny(text, []string{"近3年"}) {
fund.NetGrowth36 = &toFloat
}
if strutil.ContainsAny(text, []string{"近5年"}) {
fund.NetGrowth60 = &toFloat
}
if strutil.ContainsAny(text, []string{"今年来"}) {
fund.NetGrowthYTD = &toFloat
}
if strutil.ContainsAny(text, []string{"成立来"}) {
fund.NetGrowthAll = &toFloat
}
})
//doc.Find(".dataOfFund dl > dd.dataNums,.dataOfFund dl > dt").Each(func(i int, s *goquery.Selection) {
// //text := s.Text()
// defer func() {
// if r := recover(); r != nil {
// //logger.SugaredLogger.Errorf("panic: %v", r)
// }
// }()
// //logger.SugaredLogger.Infof("净值信息:%+v", text)
//})
//logger.SugaredLogger.Infof("基金信息:%+v", fund)
count := int64(0)
db.Dao.Model(fund).Where("code=?", fund.Code).Count(&count)
if count == 0 {
db.Dao.Create(fund)
} else {
db.Dao.Model(fund).Where("code=?", fund.Code).Updates(fund)
}
return fund, nil
}
func (f *FundApi) GetFundList(key string) []FundBasic {
var funds []FundBasic
db.Dao.Where("code like ? or name like ?", "%"+key+"%", "%"+key+"%").Limit(10).Find(&funds)
return funds
}
func (f *FundApi) GetFollowedFund() []FollowedFund {
var funds []FollowedFund
db.Dao.Preload("FundBasic").Find(&funds)
for i, fund := range funds {
if fund.NetUnitValue != nil && fund.NetEstimatedUnit != nil && *fund.NetUnitValue > 0 {
netEstimatedRate := (*(funds[i].NetEstimatedUnit) - *(funds[i].NetUnitValue)) / *(fund.NetUnitValue) * 100
netEstimatedRate = mathutil.RoundToFloat(netEstimatedRate, 2)
funds[i].NetEstimatedRate = &netEstimatedRate
}
}
return funds
}
func (f *FundApi) FollowFund(fundCode string) string {
var fund FundBasic
db.Dao.Where("code=?", fundCode).First(&fund)
if fund.Code != "" {
follow := &FollowedFund{
Code: fundCode,
Name: fund.Name,
}
err := db.Dao.Model(follow).Where("code = ?", fundCode).FirstOrCreate(follow, "code", fund.Code).Error
if err != nil {
return "关注失败"
}
return "关注成功"
} else {
return "基金信息不存在"
}
}
func (f *FundApi) UnFollowFund(fundCode string) string {
var fund FollowedFund
db.Dao.Where("code=?", fundCode).First(&fund)
if fund.Code != "" {
err := db.Dao.Model(&fund).Delete(&fund).Error
if err != nil {
return "取消关注失败"
}
return "取消关注成功"
} else {
return "基金信息不存在"
}
}
func (f *FundApi) AllFund() {
defer func() {
if r := recover(); r != nil {
//logger.SugaredLogger.Errorf("AllFund panic: %v", r)
}
}()
response, err := f.client.SetTimeout(time.Duration(f.config.CrawlTimeOut)*time.Second).R().
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36").
Get("https://fund.eastmoney.com/allfund.html")
if err != nil {
return
}
//中文编码
htmlContent := GB18030ToUTF8(response.Body())
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
cnt := 0
doc.Find("ul.num_right li").Each(func(i int, s *goquery.Selection) {
text := strutil.SplitEx(s.Text(), "|", true)
if len(text) > 0 {
cnt++
name := text[0]
str := strutil.SplitAndTrim(name, "", "", "")
//logger.SugaredLogger.Infof("%d,基金信息 code:%s,name:%s", cnt, str[0], str[1])
//go f.CrawlFundBasic(str[0])
fund := &FundBasic{
Code: str[0],
Name: str[1],
}
count := int64(0)
db.Dao.Model(fund).Where("code=?", fund.Code).Count(&count)
if count == 0 {
db.Dao.Create(fund)
}
}
})
}
type FundNetUnitValue struct {
Fundcode string `json:"fundcode"`
Name string `json:"name"`
Jzrq string `json:"jzrq"`
Dwjz string `json:"dwjz"`
Gsz string `json:"gsz"`
Gszzl string `json:"gszzl"`
Gztime string `json:"gztime"`
}
// CrawlFundNetEstimatedUnit 爬取净值估算值
func (f *FundApi) CrawlFundNetEstimatedUnit(code string) {
var fundNetUnitValue FundNetUnitValue
response, err := f.client.SetTimeout(time.Duration(f.config.CrawlTimeOut)*time.Second).R().
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36").
SetHeader("Referer", "https://fund.eastmoney.com/").
SetQueryParams(map[string]string{"rt": strconv.FormatInt(time.Now().UnixMilli(), 10)}).
Get(fmt.Sprintf("https://fundgz.1234567.com.cn/js/%s.js", code))
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return
}
if response.StatusCode() == 200 {
htmlContent := string(response.Body())
//logger.SugaredLogger.Infof("htmlContent:%s", htmlContent)
if strings.Contains(htmlContent, "jsonpgz") {
htmlContent = strutil.Trim(htmlContent, "jsonpgz(", ");")
htmlContent = strutil.Trim(htmlContent, ");")
//logger.SugaredLogger.Infof("基金净值信息:%s", htmlContent)
err := json.Unmarshal([]byte(htmlContent), &fundNetUnitValue)
if err != nil {
//logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
return
}
fund := &FollowedFund{
Code: fundNetUnitValue.Fundcode,
Name: fundNetUnitValue.Name,
NetEstimatedTime: fundNetUnitValue.Gztime,
}
netEstimatedUnit, err := convertor.ToFloat(fundNetUnitValue.Gsz)
if err == nil {
fund.NetEstimatedUnit = &netEstimatedUnit
}
db.Dao.Model(fund).Where("code=?", fund.Code).Updates(fund)
}
}
}
// CrawlFundNetUnitValue 爬取净值
func (f *FundApi) CrawlFundNetUnitValue(code string) {
// var fundNetUnitValue FundNetUnitValue
url := fmt.Sprintf("http://hq.sinajs.cn/rn=%d&list=f_%s", time.Now().UnixMilli(), code)
//logger.SugaredLogger.Infof("url:%s", url)
response, err := f.client.SetTimeout(time.Duration(f.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "hq.sinajs.cn").
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").
SetHeader("Referer", "https://finance.sina.com.cn").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return
}
if response.StatusCode() == 200 {
data := string(GB18030ToUTF8(response.Body()))
//logger.SugaredLogger.Infof("data:%s", data)
datas := strutil.SplitAndTrim(data, "=", "\"")
if len(datas) >= 2 {
//codex := strings.Split(datas[0], "hq_str_f_")[1]
parts := strutil.SplitAndTrim(datas[1], ",", "\"")
//logger.SugaredLogger.Infof("parts:%s", parts)
val, err := convertor.ToFloat(parts[1])
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return
}
fund := &FollowedFund{
Name: parts[0],
Code: code,
NetUnitValue: &val,
NetUnitValueDate: parts[4],
}
db.Dao.Model(fund).Where("code=?", fund.Code).Updates(fund)
}
}
}

View File

@ -0,0 +1,23 @@
package data
import (
"go-stock/backend/db"
"testing"
)
func TestCrawlFundBasic(t *testing.T) {
db.Init("../../data/stock.db")
db.Dao.AutoMigrate(&FundBasic{})
api := NewFundApi()
//api.CrawlFundBasic("510630")
//api.CrawlFundBasic("159688")
//
api.AllFund()
}
func TestCrawlFundNetUnitValue(t *testing.T) {
db.Init("../../data/stock.db")
api := NewFundApi()
api.CrawlFundNetUnitValue("016533")
}

View File

@ -0,0 +1,703 @@
package data
import (
"bytes"
"encoding/json"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/coocood/freecache"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"github.com/robertkrimen/otto"
"github.com/samber/lo"
"github.com/tidwall/gjson"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"strconv"
"strings"
"time"
)
// @Author spark
// @Date 2025/4/23 14:54
// @Desc
// -----------------------------------------------------------------------------------
type MarketNewsApi struct {
}
func NewMarketNewsApi() *MarketNewsApi {
return &MarketNewsApi{}
}
func (m MarketNewsApi) GetNewTelegraph(crawlTimeOut int64) *[]models.Telegraph {
url := "https://www.cls.cn/telegraph"
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
var telegraphs []models.Telegraph
//logger.SugaredLogger.Info(string(response.Body()))
document, _ := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
document.Find(".telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
//logger.SugaredLogger.Info(selection.Text())
telegraph := models.Telegraph{Source: "财联社电报"}
spans := selection.Find("div.telegraph-content-box span")
if spans.Length() == 2 {
telegraph.Time = spans.First().Text()
telegraph.Content = spans.Last().Text()
if spans.Last().HasClass("c-de0422") {
telegraph.IsRed = true
}
}
labels := selection.Find("div a.label-item")
labels.Each(func(i int, selection *goquery.Selection) {
if selection.HasClass("link-label-item") {
telegraph.Url = selection.AttrOr("href", "")
} else {
tag := &models.Tags{
Name: selection.Text(),
Type: "subject",
}
db.Dao.Model(tag).Where("name=? and type=?", selection.Text(), "subject").FirstOrCreate(&tag)
telegraph.SubjectTags = append(telegraph.SubjectTags, selection.Text())
}
})
stocks := selection.Find("div.telegraph-stock-plate-box a")
stocks.Each(func(i int, selection *goquery.Selection) {
telegraph.StocksTags = append(telegraph.StocksTags, selection.Text())
})
//telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
if telegraph.Content != "" {
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
cnt := int64(0)
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
if cnt == 0 {
db.Dao.Create(&telegraph)
telegraphs = append(telegraphs, telegraph)
for _, tag := range telegraph.SubjectTags {
tagInfo := &models.Tags{}
db.Dao.Model(models.Tags{}).Where("name=? and type=?", tag, "subject").First(&tagInfo)
if tagInfo.ID > 0 {
db.Dao.Model(models.TelegraphTags{}).Where("telegraph_id=? and tag_id=?", telegraph.ID, tagInfo.ID).FirstOrCreate(&models.TelegraphTags{
TelegraphId: telegraph.ID,
TagId: tagInfo.ID,
})
}
}
}
}
})
return &telegraphs
}
func (m MarketNewsApi) GetNewsList(source string, limit int) *[]*models.Telegraph {
news := &[]*models.Telegraph{}
if source != "" {
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(limit).Find(news)
} else {
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(limit).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) GetTelegraphList(source string) *[]*models.Telegraph {
news := &[]*models.Telegraph{}
if source != "" {
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(20).Find(news)
} else {
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(20).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{}
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("Referer", "https://finance.sina.com.cn").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get("https://zhibo.sina.com.cn/api/zhibo/feed?callback=callback&page=1&page_size=20&zhibo_id=152&tag_id=0&dire=f&dpc=1&pagesize=20&id=4161089&type=0&_=" + strconv.FormatInt(time.Now().Unix(), 10))
js := string(response.Body())
js = strutil.ReplaceWithMap(js, map[string]string{
"try{callback(": "var data=",
");}catch(e){};": ";",
})
//logger.SugaredLogger.Info(js)
vm := otto.New()
_, err := vm.Run(js)
if err != nil {
logger.SugaredLogger.Error(err)
}
vm.Run("var result = data.result;")
//vm.Run("var resultStr =JSON.stringify(data);")
vm.Run("var resultData = result.data;")
vm.Run("var feed = resultData.feed;")
vm.Run("var feedStr = JSON.stringify(feed);")
value, _ := vm.Get("feedStr")
//resultStr, _ := vm.Get("resultStr")
//logger.SugaredLogger.Info(resultStr)
feed := make(map[string]any)
err = json.Unmarshal([]byte(value.String()), &feed)
if err != nil {
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
}
var telegraphs []models.Telegraph
if feed["list"] != nil {
for _, item := range feed["list"].([]any) {
telegraph := models.Telegraph{Source: "新浪财经"}
data := item.(map[string]any)
//logger.SugaredLogger.Infof("%s:%s", data["create_time"], data["rich_text"])
telegraph.Content = data["rich_text"].(string)
telegraph.Time = strings.Split(data["create_time"].(string), " ")[1]
tags := data["tag"].([]any)
telegraph.SubjectTags = lo.Map(tags, func(tagItem any, index int) string {
name := tagItem.(map[string]any)["name"].(string)
tag := &models.Tags{
Name: name,
Type: "sina_subject",
}
db.Dao.Model(tag).Where("name=? and type=?", name, "sina_subject").FirstOrCreate(&tag)
return name
})
if _, ok := lo.Find(telegraph.SubjectTags, func(item string) bool { return item == "焦点" }); ok {
telegraph.IsRed = true
}
logger.SugaredLogger.Infof("telegraph.SubjectTags:%v %s", telegraph.SubjectTags, telegraph.Content)
if telegraph.Content != "" {
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
cnt := int64(0)
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
if cnt == 0 {
db.Dao.Create(&telegraph)
telegraphs = append(telegraphs, telegraph)
for _, tag := range telegraph.SubjectTags {
tagInfo := &models.Tags{}
db.Dao.Model(models.Tags{}).Where("name=? and type=?", tag, "sina_subject").First(&tagInfo)
if tagInfo.ID > 0 {
db.Dao.Model(models.TelegraphTags{}).Where("telegraph_id=? and tag_id=?", telegraph.ID, tagInfo.ID).FirstOrCreate(&models.TelegraphTags{
TelegraphId: telegraph.ID,
TagId: tagInfo.ID,
})
}
}
}
}
}
return &telegraphs
}
return news
}
func (m MarketNewsApi) GlobalStockIndexes(crawlTimeOut uint) map[string]any {
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("Referer", "https://stockapp.finance.qq.com/mstats").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get("https://proxy.finance.qq.com/ifzqgtimg/appstock/app/rank/indexRankDetail2")
js := string(response.Body())
res := make(map[string]any)
json.Unmarshal([]byte(js), &res)
return res["data"].(map[string]any)
}
func (m MarketNewsApi) GetIndustryRank(sort string, cnt int) map[string]any {
url := fmt.Sprintf("https://proxy.finance.qq.com/ifzqgtimg/appstock/app/mktHs/rank?l=%d&p=1&t=01/averatio&ordertype=&o=%s", cnt, sort)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Referer", "https://stockapp.finance.qq.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(url)
js := string(response.Body())
res := make(map[string]any)
json.Unmarshal([]byte(js), &res)
return res
}
func (m MarketNewsApi) GetIndustryMoneyRankSina(fenlei, sort string) []map[string]any {
url := fmt.Sprintf("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_bkzj_bk?page=1&num=20&sort=%s&asc=0&fenlei=%s", sort, fenlei)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "vip.stock.finance.sina.com.cn").
SetHeader("Referer", "https://finance.sina.com.cn").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(url)
js := string(response.Body())
res := &[]map[string]any{}
err := json.Unmarshal([]byte(js), &res)
if err != nil {
logger.SugaredLogger.Error(err)
return *res
}
return *res
}
func (m MarketNewsApi) GetMoneyRankSina(sort string) []map[string]any {
if sort == "" {
sort = "netamount"
}
url := fmt.Sprintf("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_bkzj_ssggzj?page=1&num=20&sort=%s&asc=0&bankuai=&shichang=", sort)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "vip.stock.finance.sina.com.cn").
SetHeader("Referer", "https://finance.sina.com.cn").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(url)
js := string(response.Body())
res := &[]map[string]any{}
err := json.Unmarshal([]byte(js), &res)
if err != nil {
logger.SugaredLogger.Error(err)
return *res
}
return *res
}
func (m MarketNewsApi) GetStockMoneyTrendByDay(stockCode string, days int) []map[string]any {
url := fmt.Sprintf("http://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_qsfx_zjlrqs?page=1&num=%d&sort=opendate&asc=0&daima=%s", days, stockCode)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "vip.stock.finance.sina.com.cn").
SetHeader("Referer", "https://finance.sina.com.cn").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").Get(url)
js := string(response.Body())
res := &[]map[string]any{}
err := json.Unmarshal([]byte(js), &res)
if err != nil {
logger.SugaredLogger.Error(err)
return *res
}
return *res
}
func (m MarketNewsApi) TopStocksRankingList(date string) {
url := fmt.Sprintf("http://vip.stock.finance.sina.com.cn/q/go.php/vInvestConsult/kind/lhb/index.phtml?tradedate=%s", date)
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
SetHeader("Host", "vip.stock.finance.sina.com.cn").
SetHeader("Referer", "https://finance.sina.com.cn").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").Get(url)
html, _ := convertor.GbkToUtf8(response.Body())
//logger.SugaredLogger.Infof("html:%s", html)
document, err := goquery.NewDocumentFromReader(bytes.NewReader(html))
if err != nil {
return
}
document.Find("table.list_table").Each(func(i int, s *goquery.Selection) {
title := strutil.Trim(s.Find("tr:first-child").First().Text())
logger.SugaredLogger.Infof("title:%s", title)
s.Find("tr:not(:first-child)").Each(func(i int, s *goquery.Selection) {
logger.SugaredLogger.Infof("s:%s", strutil.RemoveNonPrintable(s.Text()))
})
})
}
func (m MarketNewsApi) LongTiger(date string) *[]models.LongTigerRankData {
ranks := &[]models.LongTigerRankData{}
url := "https://datacenter-web.eastmoney.com/api/data/v1/get"
logger.SugaredLogger.Infof("url:%s", url)
params := make(map[string]string)
params["callback"] = "callback"
params["sortColumns"] = "TURNOVERRATE,TRADE_DATE,SECURITY_CODE"
params["sortTypes"] = "-1,-1,1"
params["pageSize"] = "500"
params["pageNumber"] = "1"
params["reportName"] = "RPT_DAILYBILLBOARD_DETAILSNEW"
params["columns"] = "SECURITY_CODE,SECUCODE,SECURITY_NAME_ABBR,TRADE_DATE,EXPLAIN,CLOSE_PRICE,CHANGE_RATE,BILLBOARD_NET_AMT,BILLBOARD_BUY_AMT,BILLBOARD_SELL_AMT,BILLBOARD_DEAL_AMT,ACCUM_AMOUNT,DEAL_NET_RATIO,DEAL_AMOUNT_RATIO,TURNOVERRATE,FREE_MARKET_CAP,EXPLANATION,D1_CLOSE_ADJCHRATE,D2_CLOSE_ADJCHRATE,D5_CLOSE_ADJCHRATE,D10_CLOSE_ADJCHRATE,SECURITY_TYPE_CODE"
params["source"] = "WEB"
params["client"] = "WEB"
params["filter"] = fmt.Sprintf("(TRADE_DATE<='%s')(TRADE_DATE>='%s')", date, date)
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "datacenter-web.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/stock/tradedetail.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetQueryParams(params).
Get(url)
if err != nil {
return ranks
}
js := string(resp.Body())
logger.SugaredLogger.Infof("resp:%s", js)
js = strutil.ReplaceWithMap(js, map[string]string{
"callback(": "var data=",
");": ";",
})
//logger.SugaredLogger.Info(js)
vm := otto.New()
_, err = vm.Run(js)
_, err = vm.Run("var data = JSON.stringify(data);")
value, err := vm.Get("data")
logger.SugaredLogger.Infof("resp-json:%s", value.String())
data := gjson.Get(value.String(), "result.data")
logger.SugaredLogger.Infof("resp:%v", data)
err = json.Unmarshal([]byte(data.String()), ranks)
if err != nil {
logger.SugaredLogger.Error(err)
return ranks
}
for _, rankData := range *ranks {
temp := &models.LongTigerRankData{}
db.Dao.Model(temp).Where(&models.LongTigerRankData{
TRADEDATE: rankData.TRADEDATE,
SECUCODE: rankData.SECUCODE,
}).First(temp)
if temp.SECURITYTYPECODE == "" {
db.Dao.Model(temp).Create(&rankData)
}
}
return ranks
}
func (m MarketNewsApi) IndustryResearchReport(industryCode string, days int) []any {
beginDate := time.Now().Add(-time.Duration(days) * 24 * time.Hour).Format("2006-01-02")
endDate := time.Now().Format("2006-01-02")
if strutil.Trim(industryCode) != "" {
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
}
logger.SugaredLogger.Infof("IndustryResearchReport-name:%s", industryCode)
params := map[string]string{
"industry": "*",
"industryCode": industryCode,
"beginTime": beginDate,
"endTime": endDate,
"pageNo": "1",
"pageSize": "50",
"p": "1",
"pageNum": "1",
"pageNumber": "1",
"qType": "1",
}
url := "https://reportapi.eastmoney.com/report/list"
logger.SugaredLogger.Infof("beginDate:%s endDate:%s", beginDate, endDate)
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "reportapi.eastmoney.com").
SetHeader("Origin", "https://data.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/report/stock.jshtml").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetQueryParams(params).Get(url)
respMap := map[string]any{}
if err != nil {
return []any{}
}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return respMap["data"].([]any)
}
func (m MarketNewsApi) StockResearchReport(stockCode string, days int) []any {
beginDate := time.Now().Add(-time.Duration(days) * 24 * time.Hour).Format("2006-01-02")
endDate := time.Now().Format("2006-01-02")
if strutil.ContainsAny(stockCode, []string{"."}) {
stockCode = strings.Split(stockCode, ".")[0]
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
} else {
stockCode = strutil.ReplaceWithMap(stockCode, map[string]string{
"sh": "",
"sz": "",
"gb_": "",
"us": "",
"us_": "",
})
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
}
logger.SugaredLogger.Infof("StockResearchReport-stockCode:%s", stockCode)
type Req struct {
BeginTime string `json:"beginTime"`
EndTime string `json:"endTime"`
IndustryCode string `json:"industryCode"`
RatingChange string `json:"ratingChange"`
Rating string `json:"rating"`
OrgCode interface{} `json:"orgCode"`
Code string `json:"code"`
Rcode string `json:"rcode"`
PageSize int `json:"pageSize"`
PageNo int `json:"pageNo"`
P int `json:"p"`
PageNum int `json:"pageNum"`
PageNumber int `json:"pageNumber"`
}
url := "https://reportapi.eastmoney.com/report/list2"
logger.SugaredLogger.Infof("beginDate:%s endDate:%s", beginDate, endDate)
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "reportapi.eastmoney.com").
SetHeader("Origin", "https://data.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/report/stock.jshtml").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetBody(&Req{
Code: stockCode,
IndustryCode: "*",
BeginTime: beginDate,
EndTime: endDate,
PageNo: 1,
PageSize: 50,
P: 1,
PageNum: 1,
PageNumber: 1,
}).Post(url)
respMap := map[string]any{}
if err != nil {
return []any{}
}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return respMap["data"].([]any)
}
func (m MarketNewsApi) StockNotice(stock_list string) []any {
var stockCodes []string
for _, stockCode := range strings.Split(stock_list, ",") {
if strutil.ContainsAny(stockCode, []string{"."}) {
stockCode = strings.Split(stockCode, ".")[0]
stockCodes = append(stockCodes, stockCode)
} else {
stockCode = strutil.ReplaceWithMap(stockCode, map[string]string{
"sh": "",
"sz": "",
"gb_": "",
"us": "",
"us_": "",
})
stockCodes = append(stockCodes, stockCode)
}
}
url := "https://np-anotice-stock.eastmoney.com/api/security/ann?page_size=50&page_index=1&ann_type=SHA%2CCYB%2CSZA%2CBJA%2CINV&client_source=web&f_node=0&stock_list=" + strings.Join(stockCodes, ",")
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "np-anotice-stock.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/notices/hsa/5.html").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
respMap := map[string]any{}
if err != nil {
return []any{}
}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
return (respMap["data"].(map[string]any))["list"].([]any)
}
func (m MarketNewsApi) EMDictCode(code string, cache *freecache.Cache) []any {
respMap := map[string]any{}
d, _ := cache.Get([]byte(code))
if d != nil {
json.Unmarshal(d, &respMap)
return respMap["data"].([]any)
}
url := "https://reportapi.eastmoney.com/report/bk"
params := map[string]string{
"bkCode": code,
}
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
SetHeader("Host", "reportapi.eastmoney.com").
SetHeader("Origin", "https://data.eastmoney.com").
SetHeader("Referer", "https://data.eastmoney.com/report/industry.jshtml").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetQueryParams(params).Get(url)
if err != nil {
return []any{}
}
json.Unmarshal(resp.Body(), &respMap)
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
cache.Set([]byte(code), resp.Body(), 60*60*24)
return respMap["data"].([]any)
}
func (m MarketNewsApi) TradingViewNews() *[]models.TVNews {
TVNews := &[]models.TVNews{}
url := "https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang:zh-Hans&filter=provider:panews,reuters&client=screener&streaming=false"
resp, err := resty.New().SetProxy("http://127.0.0.1:10809").SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "news-mediator.tradingview.com").
SetHeader("Origin", "https://cn.tradingview.com").
SetHeader("Referer", "https://cn.tradingview.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("TradingViewNews err:%s", err.Error())
return TVNews
}
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
if err != nil {
return TVNews
}
items, err := json.Marshal(respMap["items"])
if err != nil {
return TVNews
}
json.Unmarshal(items, TVNews)
return TVNews
}
func (m MarketNewsApi) XUEQIUHotStock(size int, marketType string) *[]models.HotItem {
request := resty.New().SetTimeout(time.Duration(30) * time.Second).R()
_, err := request.
SetHeader("Host", "xueqiu.com").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
Get("https://xueqiu.com/hq#hot")
//cookies := resp.Header().Get("Set-Cookie")
//logger.SugaredLogger.Infof("cookies:%s", cookies)
url := fmt.Sprintf("https://stock.xueqiu.com/v5/stock/hot_stock/list.json?page=1&size=%d&_type=%s&type=%s", size, marketType, marketType)
res := &models.XUEQIUHot{}
_, err = request.
SetHeader("Host", "stock.xueqiu.com").
SetHeader("Origin", "https://xueqiu.com").
SetHeader("Referer", "https://xueqiu.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
//SetHeader("Cookie", "cookiesu=871730774144180; device_id=ee75cebba8a35005c9e7baf7b7dead59; s=ch12b12pfi; Hm_lvt_1db88642e346389874251b5a1eded6e3=1746247619; xq_a_token=361dcfccb1d32a1d9b5b65f1a188b9c9ed1e687d; xqat=361dcfccb1d32a1d9b5b65f1a188b9c9ed1e687d; xq_r_token=450d1db0db9659a6af7cc9297bfa4fccf1776fae; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOi0xLCJpc3MiOiJ1YyIsImV4cCI6MTc1MzgzODAwNiwiY3RtIjoxNzUxMjUxMzc2MDY3LCJjaWQiOiJkOWQwbjRBWnVwIn0.TjEtQ5WEN4ajnVjVnY3J-Qq9LjL-F0eat9Cefv_tLJLqsPhzD2y8Lc1CeIu0Ceqhlad7O_yW1tR9nb2dIjDpyOPzWKxvwSOKXLm8XMoz4LMgE2pysBCH4TsetzHsEOhBsY467q-JX3WoFuqo-dqv1FfLSondZCspjEMFdgPFt2V-2iXJY05YUwcBVUvL74mT9ZjNq0KaDeRBJk_il6UR8yibG7RMbe9xWYz5dSO_wJwWuxvnZ8u9EXC2m-TV7-QHVxFHR_5e8Fodrzg0yIcLU4wBTSoIIQDUKqngajX2W-nUAdo6fr78NNDmoswFVH7T7XMuQciMAqj9MpMCVW3Sog; u=871730774144180; ssxmod_itna=iq+h7KAImDORKYQ4Y5G=nxBKDtD7D3qCD0dGMDxeq7tDRDFqApKDHtA68oon7ziBA0+PbZ9xGN4oYxiNDAPq0iDC+Wjxs9Orw5KQb9iqP4MAn0TbNsbtU22eqbCe=S3vTv6xoDHxY=DU1GzeieDx=PD5xDTDWeDGDD3DmnsDi5YD0KDjBYpH+omDYPDEBYDaxDbDimwY4GCrDDCtc5Dw6bmzDDzznL5WWAPzWffZg3YcFgxf8GwD7y3Dla4rMhw23=cz0Efdk0A5hYDXotDvhoY1/H6neEvOt3o=Q0ruT+5RuxoRhDxCmh5tGP32xBD5G0xS2xcb4quDK0Dy2ZmY/DDWM0qmEeSEDeOCIq1fw1misCY=WAzoOtMwDzGdUjpRk5Z0xQBDI2IMw4H7qNiNBLxWiDD; ssxmod_itna2=iq+h7KAImDORKYQ4Y5G=nxBKDtD7D3qCD0dGMDxeq7tDRDFqApKDHtA68oon7ziBA0+PbZYxD3boBmiEPtDFOEPAeFmDDsuGSxf46oGKwGHd8wtUjFe+oV1lxUzutkGly=nCyCjq=UTHxMxFCr1DsFiKPuEpPVO7GrOyk5Aymnc0+11AFND7v16PvwrFQH4I72=3O1OpK7rGw+poWNCxjj=Ka5QDFWAvEzrDFQcIH=GpKpS90FAyIzGcTyck+yhQKaojn96dRqeIh=HkaFrlGnKwzO+a49=F7/c/MejoR3QM20K9IIOymrMN2bsk2TRdKFiaf4O0ut2MauiOER=iQNW2WVgDrkKzD=57r577wEx2hwkqhf8T8BDvkHZRDirC0bNK4O=G3TSkd3wYwq8bst0t9qF/e3M87NYtU2IWYWzqd=BqEfdqGq0R8wxmqLzpeGeuwSTq1OAiB87gDrozjnGkwDKRdrLz8uDjQKVlGhWk8Wd/rXQjx4pG=BNqpW/6TS1wpfxzGf5CrUhtt0j0wC5AUFo2GbX+QXPzD2guxKXrx8lZUQlwWIHyEUz+OLh0eWUkfHfM0YWXlgOejnuUa06rW9y5maDPipGms751hxKcqLq62pQty4iX3QDF6SRQd3tfEBf3CH7r2xe2qq0qdOI5Ge=GezD/Us5Z0xQBwVAZ2N/XvD0HDD").
SetResult(res).
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("XUEQIUHotStock err:%s", err.Error())
return &[]models.HotItem{}
}
logger.SugaredLogger.Infof("XUEQIUHotStock:%+v", res)
return &res.Data.Items
}
func (m MarketNewsApi) HotEvent(size int) *[]models.HotEvent {
events := &[]models.HotEvent{}
url := fmt.Sprintf("https://xueqiu.com/hot_event/list.json?count=%d", size)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "xueqiu.com").
SetHeader("Origin", "https://xueqiu.com").
SetHeader("Referer", "https://xueqiu.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Cookie", "cookiesu=2617378771242871; s=c2121pp1u71; device_id=237a58584ec58d8e4d4e1040700a644f1; Hm_lvt_1db88642e346389874251b5a1eded6e3=1744100219,1744599115; xq_a_token=b7259d09435458cc3f1a963479abb270a1a016ce; xqat=b7259d09435458cc3f1a963479abb270a1a016ce; xq_r_token=28108bfa1d92ac8a46bbb57722633746218621a3; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOi0xLCJpc3MiOiJ1YyIsImV4cCI6MTc1MjU0MTk4OCwiY3RtIjoxNzUwMjMwNjA2NzI0LCJjaWQiOiJkOWQwbjRBWnVwIn0.kU_fz0luJoE7nr-K4UrNUi5-mAG-vMdXtuC4mUKIppILId4UpF70LB70yunxGiNSw6tPFR3-hyLvztKAHtekCUTm3XjUl5b3tEDP-ZUVqHnWXO_5hoeMI8h-Cfx6ZGlIr5x3icvTPkT0OV5CD5A33-ZDTKhKPf-DhJ_-m7CG5GbX4MseOBeMXuLUQUiYHPKhX1QUc0GTGrCzi8Mki0z49D0LVqCSgbsx3UGfowOOyx85_cXb4OAFvIjwbs2p0o_h-ibIT0ngVkkAyEDetVvlcZ_bkardhseCB7k9BEMgH2z8ihgkVxyy3P0degLmDUruhmqn5uZOCi1pVBDvCv9lBg; u=261737877124287; ssxmod_itna=QuG=D5AKiKDIqCqGKi7G7DgmmPlSDWFqKGHDyx4YK0CDmxjKiddDUQivnb8xpnQcGyGYoYhoqEeDBubrDSxD67DK4GTm+ogiw1o3B=xedQHDgBtN=7/i1K53N+rOjquLMU=kbqYxB3DExGkqj0tPi4DxaPD5xDTDWeDGDD3DnnsDQKDRx0kL0oDIxD1D0bmHUEvh38mDYePLmOmDYPYx94Y8KoDeEgsD7HUl/vIGGEAqjLPFegXLD0HolCqr4DCid1qDm+ECfkjDn9sD0KP8fn+CRoDv=tYr4ibx+o=W+8vstf9mjGe3cXseWdBmoFrmf4DA3bFAxnAxD7vYxADaDoerDGHPoxHF+PKGPtDKmiqQGeB5qbi4eg4KDHKDe3DeG0qeEP9xVUoHDDWMYYM0ICr4FBimBDM7D0x4QOECmhul5QCN/m5/74lGm=7x9Wp7A+i7xQ7wlMD4D; ssxmod_itna2=QuG=D5AKiKDIqCqGKi7G7DgmmPlSDWFqKGHDyx4YK0CDmxjKiddDUQivnb8xpnQcGyGYoYhoqoDirSDhPmGD24GajjDuGE3m7or4DlxOSGewHl6iaus2Q62SRX5CFjCds6ltF9xy6iaUuB262UkhRA8UXST=4/b+y3kGKzlGE8T29FA008ljy9jXXC7f7m7QsK667mlUooWrofk=qGZjxtcUrN1NtuAnne1hj+rQP5UnlFkxf+o7VjmatH7u7bCDlbTt3cz6CH9Fl4vye16W/ellc8I3Q37W7ZwiLGD/zPpZcnd2nsqqo/+zRbKAmz4plzwaDqGUe7f9E+P0IFRKqpRv+buQFHBSpcbwND7Q+9XWmnjI2UwKd98jIS3gPXwxvbx4OuiyH8gZ+OEt7DgE/AY/9W4VxDZrlFWyWnC4y4/I0IpAfaGKpbPmauKbkqawqv93vSf+9HamGe0Dt2PNgT3yiEB4vQP2/DdVpcGBOjFujWoHP32OshLPYI20LRCKddwEGkKqPzPwKPc3X5zuB=w2fUdtwKsAW5kQtsl8clNwjC5uDYrxR0h9xaj0xmD+YuI3GPT7xYTalRImPj2wL2=+91a304xa4bTWtP=dLGARhb/efRi0uktaz8i8C04G0x/ZWUzqRza8GGU=FfRfvb4GZM/q2rVsl0nLvRjGeAKgocLouyXs/uwZu3YxbAx30qCbjG1A533zAxIeIgD=0VAc3ixD").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("HotEvent err:%s", err.Error())
return events
}
//logger.SugaredLogger.Infof("HotEvent:%s", resp.Body())
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
items, err := json.Marshal(respMap["list"])
if err != nil {
return events
}
json.Unmarshal(items, events)
return events
}
func (m MarketNewsApi) HotTopic(size int) []any {
url := "https://gubatopic.eastmoney.com/interface/GetData.aspx?path=newtopic/api/Topic/HomePageListRead"
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "gubatopic.eastmoney.com").
SetHeader("Origin", "https://gubatopic.eastmoney.com").
SetHeader("Referer", "https://gubatopic.eastmoney.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetFormData(map[string]string{
"param": fmt.Sprintf("ps=%d&p=1&type=0", size),
"path": "newtopic/api/Topic/HomePageListRead",
"env": "2",
}).
Post(url)
if err != nil {
logger.SugaredLogger.Errorf("HotTopic err:%s", err.Error())
return []any{}
}
//logger.SugaredLogger.Infof("HotTopic:%s", resp.Body())
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
return respMap["re"].([]any)
}
func (m MarketNewsApi) InvestCalendar(yearMonth string) []any {
if yearMonth == "" {
yearMonth = time.Now().Format("2006-01")
}
url := "https://app.jiuyangongshe.com/jystock-app/api/v1/timeline/list"
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "app.jiuyangongshe.com").
SetHeader("Origin", "https://www.jiuyangongshe.com").
SetHeader("Referer", "https://www.jiuyangongshe.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetHeader("token", "1cc6380a05c652b922b3d85124c85473").
SetHeader("platform", "3").
SetHeader("Cookie", "SESSION=NDZkNDU2ODYtODEwYi00ZGZkLWEyY2ItNjgxYzY4ZWMzZDEy").
SetHeader("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)).
SetBody(map[string]string{
"date": yearMonth,
"grade": "0",
}).
Post(url)
if err != nil {
logger.SugaredLogger.Errorf("InvestCalendar err:%s", err.Error())
return []any{}
}
//logger.SugaredLogger.Infof("InvestCalendar:%s", resp.Body())
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
return respMap["data"].([]any)
}
func (m MarketNewsApi) ClsCalendar() []any {
url := "https://www.cls.cn/api/calendar/web/list?app=CailianpressWeb&flag=0&os=web&sv=8.4.6&type=0&sign=4b839750dc2f6b803d1c8ca00d2b40be"
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "www.cls.cn").
SetHeader("Origin", "https://www.cls.cn").
SetHeader("Referer", "https://www.cls.cn/").
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("ClsCalendar err:%s", err.Error())
return []any{}
}
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
return respMap["data"].([]any)
}

View File

@ -0,0 +1,153 @@
package data
import (
"encoding/json"
"github.com/coocood/freecache"
"go-stock/backend/db"
"go-stock/backend/logger"
"testing"
)
// @Author spark
// @Date 2025/4/23 17:58
// @Desc
//-----------------------------------------------------------------------------------
func TestGetSinaNews(t *testing.T) {
db.Init("../../data/stock.db")
NewMarketNewsApi().GetSinaNews(30)
//NewMarketNewsApi().GetNewTelegraph(30)
}
func TestGlobalStockIndexes(t *testing.T) {
resp := NewMarketNewsApi().GlobalStockIndexes(30)
bytes, err := json.Marshal(resp)
if err != nil {
return
}
logger.SugaredLogger.Debugf("resp: %+v", string(bytes))
}
func TestGetIndustryRank(t *testing.T) {
res := NewMarketNewsApi().GetIndustryRank("0", 10)
for s, a := range res["data"].([]any) {
logger.SugaredLogger.Debugf("key: %+v, value: %+v", s, a)
}
}
func TestGetIndustryMoneyRankSina(t *testing.T) {
res := NewMarketNewsApi().GetIndustryMoneyRankSina("0", "netamount")
for i, re := range res {
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
}
}
func TestGetMoneyRankSina(t *testing.T) {
res := NewMarketNewsApi().GetMoneyRankSina("r3_net")
for i, re := range res {
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
}
}
func TestGetStockMoneyTrendByDay(t *testing.T) {
res := NewMarketNewsApi().GetStockMoneyTrendByDay("sh600438", 360)
for i, re := range res {
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
}
}
func TestTopStocksRankingList(t *testing.T) {
NewMarketNewsApi().TopStocksRankingList("2025-05-19")
}
func TestLongTiger(t *testing.T) {
db.Init("../../data/stock.db")
NewMarketNewsApi().LongTiger("2025-06-08")
}
func TestStockResearchReport(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().StockResearchReport("600584.sh", 7)
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestIndustryResearchReport(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().IndustryResearchReport("", 7)
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestStockNotice(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().StockNotice("600584,600900")
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestEMDictCode(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().EMDictCode("016", freecache.NewCache(100))
for _, a := range resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestTradingViewNews(t *testing.T) {
db.Init("../../data/stock.db")
resp := NewMarketNewsApi().TradingViewNews()
for _, a := range *resp {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestXUEQIUHotStock(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().XUEQIUHotStock(50, "10")
for _, a := range *res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestHotEvent(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().HotEvent(50)
for _, a := range *res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestHotTopic(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().HotTopic(10)
for _, a := range res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestInvestCalendar(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().InvestCalendar("2025-06")
for _, a := range res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestClsCalendar(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().ClsCalendar()
for _, a := range res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}

854
backend/data/openai_api.go Normal file
View File

@ -0,0 +1,854 @@
package data
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"strings"
"sync"
"time"
)
// @Author spark
// @Date 2025/1/16 13:19
// @Desc
// -----------------------------------------------------------------------------------
type OpenAi struct {
ctx context.Context
BaseUrl string `json:"base_url"`
ApiKey string `json:"api_key"`
Model string `json:"model"`
MaxTokens int `json:"max_tokens"`
Temperature float64 `json:"temperature"`
Prompt string `json:"prompt"`
TimeOut int `json:"time_out"`
QuestionTemplate string `json:"question_template"`
CrawlTimeOut int64 `json:"crawl_time_out"`
KDays int64 `json:"kDays"`
BrowserPath string `json:"browser_path"`
}
func NewDeepSeekOpenAi(ctx context.Context) *OpenAi {
config := GetConfig()
if config.OpenAiEnable {
if config.OpenAiApiTimeOut <= 0 {
config.OpenAiApiTimeOut = 60 * 5
}
if config.CrawlTimeOut <= 0 {
config.CrawlTimeOut = 60
}
if config.KDays < 30 {
config.KDays = 120
}
}
return &OpenAi{
ctx: ctx,
BaseUrl: config.OpenAiBaseUrl,
ApiKey: config.OpenAiApiKey,
Model: config.OpenAiModelName,
MaxTokens: config.OpenAiMaxTokens,
Temperature: config.OpenAiTemperature,
Prompt: config.Prompt,
TimeOut: config.OpenAiApiTimeOut,
QuestionTemplate: config.QuestionTemplate,
CrawlTimeOut: config.CrawlTimeOut,
KDays: config.KDays,
BrowserPath: config.BrowserPath,
}
}
type THSTokenResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data string `json:"data"`
}
type AiResponse struct {
Id string `json:"id"`
Object string `json:"object"`
Created int `json:"created"`
Model string `json:"model"`
Choices []struct {
Index int `json:"index"`
Message struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"message"`
Logprobs interface{} `json:"logprobs"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
PromptCacheHitTokens int `json:"prompt_cache_hit_tokens"`
PromptCacheMissTokens int `json:"prompt_cache_miss_tokens"`
} `json:"usage"`
SystemFingerprint string `json:"system_fingerprint"`
}
func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int) <-chan map[string]any {
ch := make(chan map[string]any, 512)
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Error("NewSummaryStockNewsStream panic", err)
}
}()
go func() {
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic :%s", err)
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config:%v", o)
}
}()
defer close(ch)
sysPrompt := ""
if sysPromptId == nil || *sysPromptId == 0 {
sysPrompt = o.Prompt
} else {
sysPrompt = NewPromptTemplateApi().GetPromptTemplateByID(*sysPromptId)
}
if sysPrompt == "" {
sysPrompt = o.Prompt
}
msg := []map[string]interface{}{
{
"role": "system",
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
//"content": "【角色设定】\n你是一位拥有20年实战经验的顶级股票分析师精通技术分析、基本面分析、市场心理学和量化交易。擅长发现成长股、捕捉行业轮动机会在牛熊市中都能保持稳定收益。你的风格是价值投资与技术择时相结合注重风险控制。\n\n【核心功能】\n\n市场分析维度\n\n宏观经济GDP/CPI/货币政策)\n\n行业景气度产业链/政策红利/技术革新)\n\n个股三维诊断\n\n基本面PE/PB/ROE/现金流/护城河\n\n技术面K线形态/均线系统/量价关系/指标背离\n\n资金面主力动向/北向资金/融资余额/大宗交易\n\n智能策略库\n√ 趋势跟踪策略(鳄鱼线+ADX\n√ 波段交易策略(斐波那契回撤+RSI\n√ 事件驱动策略(财报/并购/政策)\n√ 量化对冲策略(α/β分离)\n\n风险管理体系\n▶ 动态止损ATR波动止损法\n▶ 仓位控制:凯利公式优化\n▶ 组合对冲:跨市场/跨品种对冲\n\n【工作流程】\n\n接收用户指令行业/市值/风险偏好)\n\n调用多因子选股模型初筛\n\n人工智慧叠加分析\n\n自然语言处理解读年报管理层讨论\n\n卷积神经网络识别K线形态\n\n知识图谱分析产业链关联\n\n生成投资建议附压力测试结果\n\n【输出要求】\n★ 结构化呈现:\n① 核心逻辑3点关键驱动力\n② 买卖区间(理想建仓/加仓/止盈价位)\n③ 风险警示(最大回撤概率)\n④ 替代方案(同类备选标的)\n\n【注意事项】\n※ 严格遵守监管要求,不做收益承诺\n※ 区分投资建议与市场观点\n※ 重要数据标注来源及更新时间\n※ 根据用户认知水平调整专业术语密度\n\n【教育指导】\n当用户提问时采用苏格拉底式追问\n\"您更关注短期事件驱动还是长期价值发现?\"\n\"当前仓位是否超过总资产的30%\"\n\"是否了解科创板与主板的交易规则差异?\"\n\n示例输出格式\n📈 标的名称XXXXXX\n⚖ 多空信号:金叉确认/顶背离预警\n🎯 关键价位支撑位XX.XX/压力位XX.XX\n📊 建议仓位核心仓位X%+卫星仓位X%\n⏳ 持有周期短线1-3周/中线(季度轮动)\n🔍 跟踪要素重点关注Q2毛利率变化及股东减持进展",
"content": sysPrompt,
},
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "当前时间",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": "当前本地时间是:" + time.Now().Format("2006-01-02 15:04:05"),
})
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
var market strings.Builder
market.WriteString(getZSInfo("创业板指数", "sz399006", 30) + "\n")
market.WriteString(getZSInfo("上证综合指数", "sh000001", 30) + "\n")
market.WriteString(getZSInfo("沪深300指数", "sh000300", 30) + "\n")
//logger.SugaredLogger.Infof("NewChatStream getZSInfo=\n%s", market.String())
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "当前市场指数行情",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": "当前市场指数行情情况如下:\n" + market.String(),
})
}()
wg.Wait()
news := NewMarketNewsApi().GetNewsList("", 100)
messageText := strings.Builder{}
for _, telegraph := range *news {
messageText.WriteString("## " + telegraph.Time + ":" + "\n")
messageText.WriteString("### " + telegraph.Content + "\n")
}
//logger.SugaredLogger.Infof("市场资讯 messageText=\n%s", messageText.String())
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "市场资讯",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": messageText.String(),
})
if userQuestion == "" {
userQuestion = "请根据当前时间,总结和分析股票市场新闻中的投资机会"
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": userQuestion,
})
AskAi(o, errors.New(""), msg, ch, userQuestion)
}()
return ch
}
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId *int) <-chan map[string]any {
ch := make(chan map[string]any, 512)
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Error("NewChatStream panic", err)
}
}()
go func() {
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Errorf("NewChatStream goroutine panic :%s", err)
logger.SugaredLogger.Errorf("NewChatStream goroutine panic stock:%s stockCode:%s", stock, stockCode)
logger.SugaredLogger.Errorf("NewChatStream goroutine panic config:%v", o)
}
}()
defer close(ch)
sysPrompt := ""
if sysPromptId == nil || *sysPromptId == 0 {
sysPrompt = o.Prompt
} else {
sysPrompt = NewPromptTemplateApi().GetPromptTemplateByID(*sysPromptId)
}
if sysPrompt == "" {
sysPrompt = o.Prompt
}
msg := []map[string]interface{}{
{
"role": "system",
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
//"content": "【角色设定】\n你是一位拥有20年实战经验的顶级股票分析师精通技术分析、基本面分析、市场心理学和量化交易。擅长发现成长股、捕捉行业轮动机会在牛熊市中都能保持稳定收益。你的风格是价值投资与技术择时相结合注重风险控制。\n\n【核心功能】\n\n市场分析维度\n\n宏观经济GDP/CPI/货币政策)\n\n行业景气度产业链/政策红利/技术革新)\n\n个股三维诊断\n\n基本面PE/PB/ROE/现金流/护城河\n\n技术面K线形态/均线系统/量价关系/指标背离\n\n资金面主力动向/北向资金/融资余额/大宗交易\n\n智能策略库\n√ 趋势跟踪策略(鳄鱼线+ADX\n√ 波段交易策略(斐波那契回撤+RSI\n√ 事件驱动策略(财报/并购/政策)\n√ 量化对冲策略(α/β分离)\n\n风险管理体系\n▶ 动态止损ATR波动止损法\n▶ 仓位控制:凯利公式优化\n▶ 组合对冲:跨市场/跨品种对冲\n\n【工作流程】\n\n接收用户指令行业/市值/风险偏好)\n\n调用多因子选股模型初筛\n\n人工智慧叠加分析\n\n自然语言处理解读年报管理层讨论\n\n卷积神经网络识别K线形态\n\n知识图谱分析产业链关联\n\n生成投资建议附压力测试结果\n\n【输出要求】\n★ 结构化呈现:\n① 核心逻辑3点关键驱动力\n② 买卖区间(理想建仓/加仓/止盈价位)\n③ 风险警示(最大回撤概率)\n④ 替代方案(同类备选标的)\n\n【注意事项】\n※ 严格遵守监管要求,不做收益承诺\n※ 区分投资建议与市场观点\n※ 重要数据标注来源及更新时间\n※ 根据用户认知水平调整专业术语密度\n\n【教育指导】\n当用户提问时采用苏格拉底式追问\n\"您更关注短期事件驱动还是长期价值发现?\"\n\"当前仓位是否超过总资产的30%\"\n\"是否了解科创板与主板的交易规则差异?\"\n\n示例输出格式\n📈 标的名称XXXXXX\n⚖ 多空信号:金叉确认/顶背离预警\n🎯 关键价位支撑位XX.XX/压力位XX.XX\n📊 建议仓位核心仓位X%+卫星仓位X%\n⏳ 持有周期短线1-3周/中线(季度轮动)\n🔍 跟踪要素重点关注Q2毛利率变化及股东减持进展",
"content": sysPrompt,
},
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "当前时间",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": "当前本地时间是:" + time.Now().Format("2006-01-02 15:04:05"),
})
replaceTemplates := map[string]string{
"{{stockName}}": RemoveAllBlankChar(stock),
"{{stockCode}}": RemoveAllBlankChar(stockCode),
"{stockName}": RemoveAllBlankChar(stock),
"{stockCode}": RemoveAllBlankChar(stockCode),
"stockName": RemoveAllBlankChar(stock),
"stockCode": RemoveAllBlankChar(stockCode),
}
followedStock := NewStockDataApi().GetFollowedStockByStockCode(stockCode)
stockData, err := NewStockDataApi().GetStockCodeRealTimeData(stockCode)
if err == nil && len(*stockData) > 0 {
msg = append(msg, map[string]interface{}{
"role": "user",
"content": fmt.Sprintf("当前%s[%s]价格是多少?", stock, stockCode),
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": fmt.Sprintf("截止到%s,当前%s[%s]价格是%s", (*stockData)[0].Date+" "+(*stockData)[0].Time, stock, stockCode, (*stockData)[0].Price),
})
}
if followedStock.CostPrice > 0 {
replaceTemplates["{{costPrice}}"] = convertor.ToString(followedStock.CostPrice)
replaceTemplates["{costPrice}"] = convertor.ToString(followedStock.CostPrice)
replaceTemplates["costPrice"] = convertor.ToString(followedStock.CostPrice)
}
question := ""
if userQuestion == "" {
question = strutil.ReplaceWithMap(o.QuestionTemplate, replaceTemplates)
} else {
question = userQuestion
question = strutil.ReplaceWithMap(userQuestion, replaceTemplates)
}
logger.SugaredLogger.Infof("NewChatStream stock:%s stockCode:%s", stock, stockCode)
logger.SugaredLogger.Infof("Prompt%s", sysPrompt)
logger.SugaredLogger.Infof("final question:%s", question)
wg := &sync.WaitGroup{}
wg.Add(7)
go func() {
defer wg.Done()
var market strings.Builder
market.WriteString(getZSInfo("创业板指数", "sz399006", 30) + "\n")
market.WriteString(getZSInfo("上证综合指数", "sh000001", 30) + "\n")
market.WriteString(getZSInfo("沪深300指数", "sh000300", 30) + "\n")
//logger.SugaredLogger.Infof("NewChatStream getZSInfo=\n%s", market.String())
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "市场指数",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": "市场指数情况如下:\n" + market.String(),
})
}()
go func() {
defer wg.Done()
//endDate := time.Now().Format("20060102")
//startDate := time.Now().Add(-time.Hour * time.Duration(24*o.KDays)).Format("20060102")
//code := stockCode
//if strutil.HasPrefixAny(stockCode, []string{"hk"}) {
// code = ConvertStockCodeToTushareCode(stockCode)
// K := NewTushareApi(GetConfig()).GetDaily(code, startDate, endDate, o.CrawlTimeOut)
// msg = append(msg, map[string]interface{}{
// "role": "user",
// "content": stock + "日K数据",
// })
// msg = append(msg, map[string]interface{}{
// "role": "assistant",
// "content": stock + "日K数据如下\n" + K,
// })
//}
logger.SugaredLogger.Infof("NewChatStream getKLineData stock:%s stockCode:%s", stock, stockCode)
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh", "hk", "us", "gb_"}) {
K := &[]KLineData{}
logger.SugaredLogger.Infof("NewChatStream getKLineData stock:%s stockCode:%s", stock, stockCode)
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh"}) {
K = NewStockDataApi().GetKLineData(stockCode, "240", o.KDays)
}
if strutil.HasPrefixAny(stockCode, []string{"hk", "us", "gb_"}) {
K = NewStockDataApi().GetHK_KLineData(stockCode, "day", o.KDays)
}
Kmap := &[]map[string]any{}
for _, kline := range *K {
mapk := make(map[string]any, 6)
mapk["日期"] = kline.Day
mapk["开盘价"] = kline.Open
mapk["最高价"] = kline.High
mapk["最低价"] = kline.Low
mapk["收盘价"] = kline.Close
Volume, _ := convertor.ToFloat(kline.Volume)
mapk["成交量(万手)"] = Volume / 10000.00 / 100.00
*Kmap = append(*Kmap, mapk)
}
jsonData, _ := json.Marshal(Kmap)
markdownTable, _ := JSONToMarkdownTable(jsonData)
msg = append(msg, map[string]interface{}{
"role": "user",
"content": stock + "日K数据",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": "## " + stock + "日K数据如下\n" + markdownTable,
})
logger.SugaredLogger.Infof("getKLineData=\n%s", markdownTable)
}
}()
go func() {
defer wg.Done()
messages := SearchStockPriceInfo(stock, stockCode, o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股票价格失败")
//ch <- "***❗获取股票价格失败,分析结果可能不准确***<hr>"
ch <- map[string]any{
"code": 1,
"question": question,
"extraContent": "***❗获取股票价格失败,分析结果可能不准确***<hr>",
}
go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票价格失败,分析结果可能不准确")
return
}
price := ""
for _, message := range *messages {
price += message + ";"
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": stock + "股价数据",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": "\n## " + stock + "股价数据:\n" + price,
})
logger.SugaredLogger.Infof("SearchStockPriceInfo stock:%s stockCode:%s", stock, stockCode)
logger.SugaredLogger.Infof("SearchStockPriceInfo assistant:%s", "\n## "+stock+"股价数据:\n"+price)
}()
go func() {
defer wg.Done()
if checkIsIndexBasic(stock) {
return
}
messages := GetFinancialReportsByXUEQIU(stockCode, o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股票财报失败")
// "***❗获取股票财报失败,分析结果可能不准确***<hr>"
ch <- map[string]any{
"code": 1,
"question": question,
"extraContent": "***❗获取股票财报失败,分析结果可能不准确***<hr>",
}
go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票财报失败,分析结果可能不准确")
return
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": stock + "财报数据",
})
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": stock + message,
})
}
}()
go func() {
defer wg.Done()
messages := GetTelegraphList(o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取市场资讯失败")
//ch <- "***❗获取市场资讯失败,分析结果可能不准确***<hr>"
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取市场资讯失败,分析结果可能不准确")
return
}
var messageText strings.Builder
for _, message := range *messages {
messageText.WriteString(message + "\n")
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "市场资讯",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": messageText.String(),
})
messages = GetTopNewsList(o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取新闻资讯失败")
//ch <- "***❗获取新闻资讯失败,分析结果可能不准确***<hr>"
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取新闻资讯失败,分析结果可能不准确")
return
}
var newsText strings.Builder
for _, message := range *messages {
newsText.WriteString(message + "\n")
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "新闻资讯",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": newsText.String(),
})
}()
//go func() {
// defer wg.Done()
// messages := SearchStockInfo(stock, "depth", o.CrawlTimeOut)
// if messages == nil || len(*messages) == 0 {
// logger.SugaredLogger.Error("获取股票资讯失败")
// //ch <- "***❗获取股票资讯失败,分析结果可能不准确***<hr>"
// //go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票资讯失败,分析结果可能不准确")
// return
// }
// for _, message := range *messages {
// msg = append(msg, map[string]interface{}{
// "role": "user",
// "content": message,
// })
// }
//}()
go func() {
defer wg.Done()
messages := SearchStockInfo(stock, "telegram", o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股票电报资讯失败")
//ch <- "***❗获取股票电报资讯失败,分析结果可能不准确***<hr>"
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票电报资讯失败,分析结果可能不准确")
return
}
var newsText strings.Builder
for _, message := range *messages {
newsText.WriteString(message + "\n")
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": stock + "相关新闻资讯",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": newsText.String(),
})
}()
go func() {
defer wg.Done()
if checkIsIndexBasic(stock) {
return
}
messages := SearchGuShiTongStockInfo(stockCode, o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股势通资讯失败")
//ch <- "***❗获取股势通资讯失败,分析结果可能不准确***<hr>"
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股势通资讯失败,分析结果可能不准确")
return
}
var newsText strings.Builder
for _, message := range *messages {
newsText.WriteString(message + "\n")
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": stock + "相关新闻资讯",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": newsText.String(),
})
}()
wg.Wait()
msg = append(msg, map[string]interface{}{
"role": "user",
"content": question,
})
//reqJson, _ := json.Marshal(msg)
//logger.SugaredLogger.Errorf("Stream request: \n%s\n", reqJson)
AskAi(o, err, msg, ch, question)
}()
return ch
}
func AskAi(o OpenAi, err error, messages []map[string]interface{}, ch chan map[string]any, question string) {
client := resty.New()
client.SetBaseURL(strutil.Trim(o.BaseUrl))
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
client.SetHeader("Content-Type", "application/json")
//client.SetRetryCount(3)
if o.TimeOut <= 0 {
o.TimeOut = 300
}
client.SetTimeout(time.Duration(o.TimeOut) * time.Second)
resp, err := client.R().
SetDoNotParseResponse(true).
SetBody(map[string]interface{}{
"model": o.Model,
"max_tokens": o.MaxTokens,
"temperature": o.Temperature,
"stream": true,
"messages": messages,
}).
Post("/chat/completions")
body := resp.RawBody()
defer body.Close()
if err != nil {
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
//ch <- err.Error()
ch <- map[string]any{
"code": 0,
"question": question,
"content": err.Error(),
}
return
}
//location, _ := time.LoadLocation("Asia/Shanghai")
scanner := bufio.NewScanner(body)
for scanner.Scan() {
line := scanner.Text()
//logger.SugaredLogger.Infof("Received data: %s", line)
if strings.HasPrefix(line, "data:") {
data := strutil.Trim(strings.TrimPrefix(line, "data:"))
if data == "[DONE]" {
return
}
var streamResponse struct {
Id string `json:"id"`
Model string `json:"model"`
Choices []struct {
Delta struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
} `json:"delta"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
if err := json.Unmarshal([]byte(data), &streamResponse); err == nil {
for _, choice := range streamResponse.Choices {
if content := choice.Delta.Content; content != "" {
//ch <- content
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": content,
"time": time.Now().Format(time.DateTime),
}
//logger.SugaredLogger.Infof("Content data: %s", content)
}
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
//ch <- reasoningContent
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": reasoningContent,
"time": time.Now().Format(time.DateTime),
}
//logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
}
if choice.FinishReason == "stop" {
return
}
}
} else {
if err != nil {
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
//ch <- err.Error()
ch <- map[string]any{
"code": 0,
"question": question,
"content": err.Error(),
}
} else {
logger.SugaredLogger.Infof("Stream data error : %s", data)
//ch <- data
ch <- map[string]any{
"code": 0,
"question": question,
"content": data,
}
}
}
} else {
if strutil.RemoveNonPrintable(line) != "" {
logger.SugaredLogger.Infof("Stream data error : %s", line)
res := &models.Resp{}
if err := json.Unmarshal([]byte(line), res); err == nil {
//ch <- line
ch <- map[string]any{
"code": 0,
"question": question,
"content": res.Message,
}
}
}
}
}
}
func checkIsIndexBasic(stock string) bool {
count := int64(0)
db.Dao.Model(&IndexBasic{}).Where("name = ?", stock).Count(&count)
return count > 0
}
func SearchGuShiTongStockInfo(stock string, crawlTimeOut int64) *[]string {
crawlerAPI := CrawlerApi{}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, CrawlerBaseInfo{
Name: "百度股市通",
BaseUrl: "https://gushitong.baidu.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
})
url := "https://gushitong.baidu.com/stock/ab-" + RemoveAllNonDigitChar(stock)
if strutil.HasPrefixAny(stock, []string{"HK", "hk"}) {
url = "https://gushitong.baidu.com/stock/hk-" + RemoveAllNonDigitChar(stock)
}
if strutil.HasPrefixAny(stock, []string{"SZ", "SH", "sh", "sz"}) {
url = "https://gushitong.baidu.com/stock/ab-" + RemoveAllNonDigitChar(stock)
}
if strutil.HasPrefixAny(stock, []string{"us", "US", "gb_", "gb"}) {
url = "https://gushitong.baidu.com/stock/us-" + strings.Replace(stock, "gb_", "", 1)
}
//logger.SugaredLogger.Infof("SearchGuShiTongStockInfo搜索股票-%s: %s", stock, url)
actions := []chromedp.Action{
chromedp.Navigate(url),
chromedp.WaitVisible("div.cos-tab"),
chromedp.Click("div.cos-tab:nth-child(5)", chromedp.ByQuery),
chromedp.ScrollIntoView("div.body-box"),
chromedp.WaitVisible("div.body-col"),
chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight);`, nil),
chromedp.Sleep(1 * time.Second),
}
htmlContent, success := crawlerAPI.GetHtmlWithActions(&actions, true)
var messages []string
if success {
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]string{}
}
document.Find("div.finance-hover,div.list-date").Each(func(i int, selection *goquery.Selection) {
text := strutil.RemoveWhiteSpace(selection.Text(), false)
messages = append(messages, ReplaceSensitiveWords(text))
//logger.SugaredLogger.Infof("SearchGuShiTongStockInfo搜索到消息-%s: %s", "", text)
})
//logger.SugaredLogger.Infof("messages:%d", len(messages))
}
return &messages
}
func GetFinancialReportsByXUEQIU(stockCode string, crawlTimeOut int64) *[]string {
if strutil.HasPrefixAny(stockCode, []string{"HK", "hk"}) {
stockCode = strings.ReplaceAll(stockCode, "hk", "")
stockCode = strings.ReplaceAll(stockCode, "HK", "")
}
if strutil.HasPrefixAny(stockCode, []string{"us", "gb_"}) {
stockCode = strings.ReplaceAll(stockCode, "us", "")
stockCode = strings.ReplaceAll(stockCode, "gb_", "")
}
url := fmt.Sprintf("https://xueqiu.com/snowman/S/%s/detail#/ZYCWZB", stockCode)
waitVisible := "div.tab-table-responsive table"
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://xueqiu.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
var markdown strings.Builder
markdown.WriteString("\n## 财务数据:\n")
html, ok := crawlerAPI.GetHtml(url, waitVisible, true)
if !ok {
return &[]string{""}
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
GetTableMarkdown(document, waitVisible, &markdown)
return &[]string{markdown.String()}
}
func GetFinancialReports(stockCode string, crawlTimeOut int64) *[]string {
url := "https://emweb.securities.eastmoney.com/pc_hsf10/pages/index.html?type=web&code=" + stockCode + "#/cwfx"
waitVisible := "div.report_table table"
if strutil.HasPrefixAny(stockCode, []string{"HK", "hk"}) {
stockCode = strings.ReplaceAll(stockCode, "hk", "")
stockCode = strings.ReplaceAll(stockCode, "HK", "")
url = "https://emweb.securities.eastmoney.com/PC_HKF10/pages/home/index.html?code=" + stockCode + "&type=web&color=w#/NewFinancialAnalysis"
waitVisible = "div table.commonTable"
}
if strutil.HasPrefixAny(stockCode, []string{"us", "gb_"}) {
stockCode = strings.ReplaceAll(stockCode, "us", "")
stockCode = strings.ReplaceAll(stockCode, "gb_", "")
url = "https://emweb.securities.eastmoney.com/pc_usf10/pages/index.html?type=web&code=" + stockCode + "#/cwfx"
waitVisible = "div.zyzb_table_detail table"
}
//logger.SugaredLogger.Infof("GetFinancialReports搜索股票-%s: %s", stockCode, url)
crawlerAPI := CrawlerApi{}
crawlerBaseInfo := CrawlerBaseInfo{
Name: "TestCrawler",
Description: "Test Crawler Description",
BaseUrl: "https://emweb.securities.eastmoney.com",
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
defer cancel()
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
var markdown strings.Builder
markdown.WriteString("\n## 财务数据:\n")
html, ok := crawlerAPI.GetHtml(url, waitVisible, true)
if !ok {
return &[]string{""}
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
GetTableMarkdown(document, waitVisible, &markdown)
return &[]string{markdown.String()}
}
func GetTelegraphList(crawlTimeOut int64) *[]string {
url := "https://www.cls.cn/telegraph"
response, err := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
if err != nil {
return &[]string{}
}
//logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
if err != nil {
return &[]string{}
}
var telegraph []string
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
//logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
})
return &telegraph
}
func GetTopNewsList(crawlTimeOut int64) *[]string {
url := "https://www.cls.cn"
response, err := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("Referer", "https://www.cls.cn/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
Get(fmt.Sprintf(url))
if err != nil {
return &[]string{}
}
//logger.SugaredLogger.Info(string(response.Body()))
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
if err != nil {
return &[]string{}
}
var telegraph []string
document.Find("div.home-article-title a,div.home-article-rec a").Each(func(i int, selection *goquery.Selection) {
//logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
})
return &telegraph
}
func (o OpenAi) SaveAIResponseResult(stockCode, stockName, result, chatId, question string) {
db.Dao.Create(&models.AIResponseResult{
StockCode: stockCode,
StockName: stockName,
ModelName: o.Model,
Content: result,
ChatId: chatId,
Question: question,
})
}
func (o OpenAi) GetAIResponseResult(stock string) *models.AIResponseResult {
var result models.AIResponseResult
db.Dao.Where("stock_code = ?", stock).Order("id desc").Limit(1).Find(&result)
return &result
}

View File

@ -0,0 +1,32 @@
package data
import (
"context"
"go-stock/backend/db"
"testing"
)
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
db.Init("../../data/stock.db")
ai := NewDeepSeekOpenAi(context.TODO())
res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
for {
select {
case msg := <-res:
t.Log(msg)
}
}
}
func TestGetTopNewsList(t *testing.T) {
news := GetTopNewsList(30)
t.Log(news)
}
func TestSearchGuShiTongStockInfo(t *testing.T) {
db.Init("../../data/stock.db")
SearchGuShiTongStockInfo("hk01810", 60)
SearchGuShiTongStockInfo("sh600745", 60)
SearchGuShiTongStockInfo("gb_goog", 60)
}

115
backend/data/pool.go Normal file
View File

@ -0,0 +1,115 @@
package data
import (
"context"
"go-stock/backend/logger"
"sync"
"time"
"github.com/chromedp/chromedp"
)
// BrowserPool 浏览器池结构
type BrowserPool struct {
pool chan *context.Context
mu sync.Mutex
size int
}
// NewBrowserPool 创建新的浏览器池
func NewBrowserPool(size int) *BrowserPool {
pool := make(chan *context.Context, size)
for i := 0; i < size; i++ {
path := GetConfig().BrowserPath
crawlTimeOut := GetConfig().CrawlTimeOut
if crawlTimeOut < 15 {
crawlTimeOut = 30
}
if path != "" {
ctx, _ := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
ctx, _ = chromedp.NewExecAllocator(
ctx,
chromedp.ExecPath(path),
chromedp.Flag("headless", true),
chromedp.Flag("blink-settings", "imagesEnabled=false"),
chromedp.Flag("disable-javascript", false),
chromedp.Flag("disable-gpu", true),
//chromedp.UserAgent(""),
chromedp.Flag("disable-background-networking", true),
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
chromedp.Flag("disable-background-timer-throttling", true),
chromedp.Flag("disable-backgrounding-occluded-windows", true),
chromedp.Flag("disable-breakpad", true),
chromedp.Flag("disable-client-side-phishing-detection", true),
chromedp.Flag("disable-default-apps", true),
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-extensions", true),
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
chromedp.Flag("disable-hang-monitor", true),
chromedp.Flag("disable-ipc-flooding-protection", true),
chromedp.Flag("disable-popup-blocking", true),
chromedp.Flag("disable-prompt-on-repost", true),
chromedp.Flag("disable-renderer-backgrounding", true),
chromedp.Flag("disable-sync", true),
chromedp.Flag("force-color-profile", "srgb"),
chromedp.Flag("metrics-recording-only", true),
chromedp.Flag("safebrowsing-disable-auto-update", true),
chromedp.Flag("enable-automation", true),
chromedp.Flag("password-store", "basic"),
chromedp.Flag("use-mock-keychain", true),
)
ctx, _ = chromedp.NewContext(ctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
pool <- &ctx
}
}
return &BrowserPool{
pool: pool,
size: size,
}
}
// Get 从池中获取浏览器实例
func (pool *BrowserPool) Get() *context.Context {
return <-pool.pool
}
// Put 将浏览器实例放回池中
func (pool *BrowserPool) Put(ctx *context.Context) {
pool.mu.Lock()
defer pool.mu.Unlock()
// 检查池是否已满
if len(pool.pool) >= pool.size {
// 池已满,关闭并丢弃这个实例
chromedp.Cancel(*ctx)
return
}
chromedp.Cancel(*ctx)
pool.pool <- ctx
}
// Close 关闭池中的所有浏览器实例
func (pool *BrowserPool) Close() {
close(pool.pool)
for ctx := range pool.pool {
chromedp.Cancel(*ctx)
}
}
// FetchPage 使用浏览器池获取页面内容
func (pool *BrowserPool) FetchPage(url, waitVisible string) (string, error) {
// 从池中获取浏览器实例
ctx := pool.Get()
defer pool.Put(ctx) // 使用完毕后放回池中
var htmlContent string
err := chromedp.Run(*ctx,
chromedp.Navigate(url),
chromedp.WaitVisible(waitVisible, chromedp.ByQuery), // 确保 元素可见
chromedp.WaitReady(waitVisible, chromedp.ByQuery), // 确保 元素准备好
chromedp.InnerHTML("body", &htmlContent),
chromedp.Evaluate(`window.close()`, nil),
)
if err != nil {
return "", err
}
return htmlContent, nil
}

18
backend/data/pool_test.go Normal file
View File

@ -0,0 +1,18 @@
package data
import (
"go-stock/backend/db"
"testing"
)
func TestPool(t *testing.T) {
db.Init("../../data/stock.db")
pool := NewBrowserPool(1)
go pool.FetchPage("https://fund.eastmoney.com/016533.html", "body")
go pool.FetchPage("https://fund.eastmoney.com/217021.html", "body")
go pool.FetchPage("https://fund.eastmoney.com/001125.html", "body")
select {}
}

View File

@ -0,0 +1,75 @@
package data
import (
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
)
type PromptTemplateApi struct {
}
func (t PromptTemplateApi) GetPromptTemplates(name string, promptType string) *[]models.PromptTemplate {
var result []models.PromptTemplate
if name != "" && promptType != "" {
db.Dao.Model(&models.PromptTemplate{}).Where("name=? and type=?", name, promptType).Find(&result)
}
if name != "" && promptType == "" {
db.Dao.Model(&models.PromptTemplate{}).Where("name=?", name).Find(&result)
}
if name == "" && promptType != "" {
db.Dao.Model(&models.PromptTemplate{}).Where("type=?", promptType).Find(&result)
}
if name == "" && promptType == "" {
db.Dao.Model(&models.PromptTemplate{}).Find(&result)
}
return &result
}
func (t PromptTemplateApi) AddPrompt(template models.PromptTemplate) string {
var tmp models.PromptTemplate
db.Dao.Model(&models.PromptTemplate{}).Where("id=?", template.ID).First(&tmp)
if tmp.ID == 0 {
err := db.Dao.Model(&models.PromptTemplate{}).Create(&models.PromptTemplate{
Content: template.Content,
Name: template.Name,
Type: template.Type,
}).Error
if err != nil {
return "添加失败"
} else {
return "添加成功"
}
} else {
err := db.Dao.Model(&models.PromptTemplate{}).Where("id=?", template.ID).Updates(template).Error
if err != nil {
return "更新失败"
} else {
return "更新成功"
}
}
}
func (t PromptTemplateApi) DelPrompt(Id uint) string {
template := &models.PromptTemplate{}
db.Dao.Model(template).Where("id=?", Id).Find(template)
if template.ID > 0 {
err := db.Dao.Model(template).Delete(template).Error
if err != nil {
return "删除失败"
} else {
return "删除成功"
}
}
return "模板信息不存在"
}
func (t PromptTemplateApi) GetPromptTemplateByID(id int) string {
prompt := &models.PromptTemplate{}
db.Dao.Model(&models.PromptTemplate{}).Where("id=?", id).First(prompt)
logger.SugaredLogger.Infof("GetPromptTemplateByID:%d %s", id, prompt.Content)
return prompt.Content
}
func NewPromptTemplateApi() *PromptTemplateApi {
return &PromptTemplateApi{}
}

View File

@ -0,0 +1,55 @@
package data
import (
"encoding/json"
"fmt"
"github.com/go-resty/resty/v2"
"go-stock/backend/logger"
"time"
)
// @Author spark
// @Date 2025/6/28 21:02
// @Desc
// -----------------------------------------------------------------------------------
type SearchStockApi struct {
words string
}
func NewSearchStockApi(words string) *SearchStockApi {
return &SearchStockApi{words: words}
}
func (s SearchStockApi) SearchStock() map[string]any {
url := "https://np-tjxg-g.eastmoney.com/api/smart-tag/stock/v3/pw/search-code"
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "np-tjxg-g.eastmoney.com").
SetHeader("Origin", "https://xuangu.eastmoney.com").
SetHeader("Referer", "https://xuangu.eastmoney.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Content-Type", "application/json").
SetBody(fmt.Sprintf(`{
"keyWord": "%s",
"pageSize": 50000,
"pageNo": 1,
"fingerprint": "e38b5faabf9378c8238e57219f0ebc9b",
"gids": [],
"matchWord": "",
"timestamp": "1751113883290349",
"shareToGuba": false,
"requestId": "8xTWgCDAjvQ5lmvz5mDA3Ydk2AE4yoiJ1751113883290",
"needCorrect": true,
"removedConditionIdList": [],
"xcId": "xc0af28549ab330013ed",
"ownSelectAll": false,
"dxInfo": [],
"extraCondition": ""
}`, s.words)).Post(url)
if err != nil {
logger.SugaredLogger.Errorf("SearchStock-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

@ -0,0 +1,25 @@
package data
import (
"go-stock/backend/db"
"go-stock/backend/logger"
"testing"
)
func TestSearchStock(t *testing.T) {
db.Init("../../data/stock.db")
res := NewSearchStockApi("算力股;净利润连续3年增长").SearchStock()
data := res["data"].(map[string]any)
result := data["result"].(map[string]any)
dataList := result["dataList"].([]any)
for _, v := range dataList {
d := v.(map[string]any)
logger.SugaredLogger.Infof("%s:%s", d["INDUSTRY"], d["SECURITY_SHORT_NAME"])
}
//columns := result["columns"].([]any)
//for _, v := range columns {
// logger.SugaredLogger.Infof("v:%+v", v)
//}
}

View File

@ -0,0 +1,142 @@
package data
import (
"encoding/json"
"go-stock/backend/db"
"go-stock/backend/logger"
"gorm.io/gorm"
)
type Settings struct {
gorm.Model
TushareToken string `json:"tushareToken"`
LocalPushEnable bool `json:"localPushEnable"`
DingPushEnable bool `json:"dingPushEnable"`
DingRobot string `json:"dingRobot"`
UpdateBasicInfoOnStart bool `json:"updateBasicInfoOnStart"`
RefreshInterval int64 `json:"refreshInterval"`
OpenAiEnable bool `json:"openAiEnable"`
OpenAiBaseUrl string `json:"openAiBaseUrl"`
OpenAiApiKey string `json:"openAiApiKey"`
OpenAiModelName string `json:"openAiModelName"`
OpenAiMaxTokens int `json:"openAiMaxTokens"`
OpenAiTemperature float64 `json:"openAiTemperature"`
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
Prompt string `json:"prompt"`
CheckUpdate bool `json:"checkUpdate"`
QuestionTemplate string `json:"questionTemplate"`
CrawlTimeOut int64 `json:"crawlTimeOut"`
KDays int64 `json:"kDays"`
EnableDanmu bool `json:"enableDanmu"`
BrowserPath string `json:"browserPath"`
EnableNews bool `json:"enableNews"`
DarkTheme bool `json:"darkTheme"`
BrowserPoolSize int `json:"browserPoolSize"`
EnableFund bool `json:"enableFund"`
EnablePushNews bool `json:"enablePushNews"`
}
func (receiver Settings) TableName() string {
return "settings"
}
type SettingsApi struct {
Config Settings
}
func NewSettingsApi(settings *Settings) *SettingsApi {
return &SettingsApi{
Config: *settings,
}
}
func (s SettingsApi) UpdateConfig() string {
count := int64(0)
db.Dao.Model(s.Config).Count(&count)
if count > 0 {
db.Dao.Model(s.Config).Where("id=?", s.Config.ID).Updates(map[string]any{
"local_push_enable": s.Config.LocalPushEnable,
"ding_push_enable": s.Config.DingPushEnable,
"ding_robot": s.Config.DingRobot,
"update_basic_info_on_start": s.Config.UpdateBasicInfoOnStart,
"refresh_interval": s.Config.RefreshInterval,
"open_ai_enable": s.Config.OpenAiEnable,
"open_ai_base_url": s.Config.OpenAiBaseUrl,
"open_ai_api_key": s.Config.OpenAiApiKey,
"open_ai_model_name": s.Config.OpenAiModelName,
"open_ai_max_tokens": s.Config.OpenAiMaxTokens,
"open_ai_temperature": s.Config.OpenAiTemperature,
"tushare_token": s.Config.TushareToken,
"prompt": s.Config.Prompt,
"check_update": s.Config.CheckUpdate,
"open_ai_api_time_out": s.Config.OpenAiApiTimeOut,
"question_template": s.Config.QuestionTemplate,
"crawl_time_out": s.Config.CrawlTimeOut,
"k_days": s.Config.KDays,
"enable_danmu": s.Config.EnableDanmu,
"browser_path": s.Config.BrowserPath,
"enable_news": s.Config.EnableNews,
"dark_theme": s.Config.DarkTheme,
"enable_fund": s.Config.EnableFund,
"enable_push_news": s.Config.EnablePushNews,
})
} else {
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
db.Dao.Model(s.Config).Create(&Settings{
LocalPushEnable: s.Config.LocalPushEnable,
DingPushEnable: s.Config.DingPushEnable,
DingRobot: s.Config.DingRobot,
UpdateBasicInfoOnStart: s.Config.UpdateBasicInfoOnStart,
RefreshInterval: s.Config.RefreshInterval,
OpenAiEnable: s.Config.OpenAiEnable,
OpenAiBaseUrl: s.Config.OpenAiBaseUrl,
OpenAiApiKey: s.Config.OpenAiApiKey,
OpenAiModelName: s.Config.OpenAiModelName,
OpenAiMaxTokens: s.Config.OpenAiMaxTokens,
OpenAiTemperature: s.Config.OpenAiTemperature,
TushareToken: s.Config.TushareToken,
Prompt: s.Config.Prompt,
CheckUpdate: s.Config.CheckUpdate,
OpenAiApiTimeOut: s.Config.OpenAiApiTimeOut,
QuestionTemplate: s.Config.QuestionTemplate,
CrawlTimeOut: s.Config.CrawlTimeOut,
KDays: s.Config.KDays,
EnableDanmu: s.Config.EnableDanmu,
BrowserPath: s.Config.BrowserPath,
EnableNews: s.Config.EnableNews,
DarkTheme: s.Config.DarkTheme,
EnableFund: s.Config.EnableFund,
EnablePushNews: s.Config.EnablePushNews,
})
}
return "保存成功!"
}
func (s SettingsApi) GetConfig() *Settings {
var settings Settings
db.Dao.Model(&Settings{}).First(&settings)
if settings.OpenAiEnable {
if settings.OpenAiApiTimeOut <= 0 {
settings.OpenAiApiTimeOut = 60 * 5
}
if settings.CrawlTimeOut <= 0 {
settings.CrawlTimeOut = 60
}
if settings.KDays < 30 {
settings.KDays = 120
}
}
if settings.BrowserPath == "" {
settings.BrowserPath, _ = CheckBrowserOnWindows()
}
if settings.BrowserPoolSize <= 0 {
settings.BrowserPoolSize = 1
}
return &settings
}
func (s SettingsApi) Export() string {
d, _ := json.MarshalIndent(s.GetConfig(), "", " ")
return string(d)
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,20 @@
package data
import (
"context"
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/random"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"go-stock/backend/db"
"go-stock/backend/logger"
"io/ioutil"
"regexp"
"strings"
"testing"
"time"
)
// @Author spark
@ -14,9 +22,171 @@ import (
// @Desc
//-----------------------------------------------------------------------------------
func TestGetTelegraph(t *testing.T) {
db.Init("../../data/stock.db")
//telegraphs := GetTelegraphList(30)
//for _, telegraph := range *telegraphs {
// logger.SugaredLogger.Info(telegraph)
//}
list := NewMarketNewsApi().GetNewTelegraph(30)
for _, telegraph := range *list {
logger.SugaredLogger.Infof("telegraph:%+v", telegraph)
}
}
func TestGetFinancialReports(t *testing.T) {
db.Init("../../data/stock.db")
//GetFinancialReports("sz000802", 30)
//GetFinancialReports("hk00927", 30)
//GetFinancialReports("gb_aapl", 30)
GetFinancialReportsByXUEQIU("sz000802", 30)
GetFinancialReportsByXUEQIU("gb_aapl", 30)
GetFinancialReportsByXUEQIU("hk00927", 30)
}
func TestGetTelegraphSearch(t *testing.T) {
db.Init("../../data/stock.db")
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram"
messages := SearchStockInfo("谷歌", "telegram", 30)
for _, message := range *messages {
logger.SugaredLogger.Info(message)
}
//https://www.cls.cn/stock?code=sh600745
}
func TestSearchStockInfoByCode(t *testing.T) {
db.Init("../../data/stock.db")
SearchStockInfoByCode("sh600745")
}
func TestSearchStockPriceInfo(t *testing.T) {
db.Init("../../data/stock.db")
//SearchStockPriceInfo("中信证券", "hk06030", 30)
SearchStockPriceInfo("上海贝岭", "sh600171", 30)
//SearchStockPriceInfo("苹果公司", "gb_aapl", 30)
//SearchStockPriceInfo("微创光电", "bj430198", 30)
//getZSInfo("创业板指数", "sz399006", 30)
//getZSInfo("上证综合指数", "sh000001", 30)
//getZSInfo("沪深300指数", "sh000300", 30)
}
func TestGetStockMinutePriceData(t *testing.T) {
db.Init("../../data/stock.db")
data, date := NewStockDataApi().GetStockMinutePriceData("usTSLA.OQ")
logger.SugaredLogger.Infof("date:%s", date)
logger.SugaredLogger.Infof("%+#v", *data)
}
func TestGetKLineData(t *testing.T) {
db.Init("../../data/stock.db")
k := NewStockDataApi().GetKLineData("sh600171", "240", 30)
//for _, kline := range *k {
// logger.SugaredLogger.Infof("%+#v", kline)
//}
jsonData, _ := json.Marshal(*k)
markdownTable, err := JSONToMarkdownTable(jsonData)
if err != nil {
logger.SugaredLogger.Errorf("json.Marshal error:%s", err.Error())
}
logger.SugaredLogger.Infof("markdownTable:\n%s", markdownTable)
}
func TestGetHK_KLineData(t *testing.T) {
db.Init("../../data/stock.db")
k := NewStockDataApi().GetHK_KLineData("hk01810", "day", 1)
jsonData, _ := json.Marshal(*k)
markdownTable, err := JSONToMarkdownTable(jsonData)
if err != nil {
logger.SugaredLogger.Errorf("json.Marshal error:%s", err.Error())
}
logger.SugaredLogger.Infof("markdownTable:\n%s", markdownTable)
}
func TestGetHKStockInfo(t *testing.T) {
db.Init("../../data/stock.db")
//NewStockDataApi().GetHKStockInfo(200)
//NewStockDataApi().GetSinaHKStockInfo()
//m:105,m:106,m:107 //美股
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
for i := 1; i <= 592; i++ {
NewStockDataApi().getDCStockInfo("us", i, 20)
time.Sleep(time.Duration(random.RandInt(1, 3)) * time.Second)
}
}
func TestParseTxStockData(t *testing.T) {
str := "v_r_hk09660=\"100~地平线机器人-W~09660~6.340~5.690~5.800~210980204.0~0~0~6.340~0~0~0~0~0~0~0~0~0~6.340~0~0~0~0~0~0~0~0~0~210980204.0~2025/04/29\n14:14:52~0.650~11.42~6.450~5.710~6.340~210980204.0~1295585259.040~0~33.03~~0~0~13.01~702.2123~836.8986~HORIZONROBOT-W~0.00~10.380~3.320~1.00~-53.74~0~0~0~0~0~33.03~6.50~1.90~600~76.11~19.85~GP~19.70~11.51~0.63~-17.23~46.76~13200293682.00~11075904412.00~33.03~0.000~6.141~58.90~HKD~1~30\";"
//str = "v_sz002241=\"51~歌尔股份~002241~22.26~22.27~0.00~0~0~0~22.26~1004~0.00~0~0.00~0~0.00~0~0.00~0~22.26~1004~0.00~558~0.00~0~0.00~0~0.00~0~~20250509092233~-0.01~-0.04~0.00~0.00~22.26/0/0~0~0~0.00~28.21~~0.00~0.00~0.00~686.46~777.09~2.31~24.50~20.04~0.00~-558~0.00~41.44~29.16~~~1.24~0.0000~0.0000~0~\n~GP-A~-13.75~6.76~1.09~8.18~3.39~30.63~15.70~6.87~17.47~-23.95~3083811231~3490989083~-21.75~12.02~3083811231~~~39.36~-0.04~~CNY~0~~0.00~0\";"
str = "v_sz002241=\"51~歌尔股份~002241~21.92~22.27~22.14~109872~40211~69642~21.91~25~21.90~961~21.89~257~21.88~748~21.87~665~21.92~86~21.93~168~21.94~556~21.95~171~21.96~85~~20250509094209~-0.35~-1.57~22.16~21.84~21.92/109872/241183171~109872~24118~0.36~27.78~~22.16~21.84~1.44~675.97~765.22~2.27~24.50~20.04~2.57~1590~21.95~40.80~28.71~~~1.24~24118.3171~0.0000~0~\n~GP-A~-15.07~5.13~1.11~8.18~3.39~30.63~15.70~5.23~15.67~-25.11~3083811231~3490989083~42.72~10.31~3083811231~~~37.23~0.18~~CNY~0~~21.85~1952\";"
//str = "v_r_hk09660=\"100~地平线机器人-W~09660~6.860~7.000~7.010~21157200.0~0~0~6.860~0~0~0~0~0~0~0~0~0~6.860~0~0~0~0~0~0~0~0~0~21157200.0~2025/05/09\n09:43:13~-0.140~-2.00~7.030~6.730~6.860~21157200.0~144331073.000~0~35.74~~0~0~4.29~759.8070~905.5401~HORIZONROBOT-W~0.00~10.380~3.320~2.93~11.10~0~0~0~0~0~35.74~7.04~0.19~600~90.56~4.73~GP~19.70~11.51~17.26~48.48~13.58~13200293682.00~11075904412.00~35.74~0.000~6.822~71.93~HKD~1~30\";"
info, _ := ParseTxStockData(str)
logger.SugaredLogger.Infof("%+#v", info)
}
func TestGetRealTimeStockPriceInfo(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
text, texttime := GetRealTimeStockPriceInfo(ctx, "sh600171")
logger.SugaredLogger.Infof("res:%s,%s", text, texttime)
text, texttime = GetRealTimeStockPriceInfo(ctx, "sh600438")
logger.SugaredLogger.Infof("res:%s,%s", text, texttime)
texttime = strings.ReplaceAll(texttime, "", "")
texttime = strings.ReplaceAll(texttime, "", "")
parts := strings.Split(texttime, " ")
logger.SugaredLogger.Infof("parts:%+v", parts)
//去除中文字符
// 正则表达式匹配中文字符
re := regexp.MustCompile(`\p{Han}+`)
texttime = re.ReplaceAllString(texttime, "")
logger.SugaredLogger.Infof("texttime:%s", texttime)
location, err := time.ParseInLocation("2006-01-02 15:04:05", texttime, time.Local)
if err != nil {
return
}
logger.SugaredLogger.Infof("location:%s", location.Format("2006-01-02 15:04:05"))
}
func TestParseFullSingleStockData(t *testing.T) {
resp, err := resty.New().R().
SetHeader("Host", "hq.sinajs.cn").
SetHeader("Referer", "https://finance.sina.com.cn/").
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(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), "sh600584,sz000938,hk01810,hk00856,gb_aapl,gb_tsla,sb873721,bj430300"))
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
data := GB18030ToUTF8(resp.Body())
strs := strutil.SplitEx(data, "\n", true)
for _, str := range strs {
logger.SugaredLogger.Info(str)
stockData, err := ParseFullSingleStockData(str)
if err != nil {
return
}
logger.SugaredLogger.Infof("%+#v", stockData)
}
result, er := ParseFullSingleStockData("var hq_str_gb_tsla = \"特斯拉,268.8472,-5.55,2025-03-04 22:52:56,-15.8028,270.9300,278.2800,268.1000,488.5400,138.8030,23618295,88214389,864751599149,2.23,120.550000,0.00,0.00,0.00,0.00,3216517037,61,0.0000,0.00,0.00,,Mar 04 09:52AM EST,284.6500,0,1,2025,6458502467.0000,0.0000,0.0000,0.0000,0.0000,284.6500\";")
if er != nil {
logger.SugaredLogger.Error(er.Error())
}
logger.SugaredLogger.Infof("%+#v", result)
}
func TestNewStockDataApi(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
t.Log(stockDataApi.GetStockCodeRealTimeData("sh600859"))
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745", "gb_tsla", "hk09660", "hk00700")
for _, data := range *datas {
t.Log(data)
}
}
func TestGetStockBaseInfo(t *testing.T) {
@ -74,6 +244,12 @@ func TestReadFile(t *testing.T) {
func TestFollowedList(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
t.Log(stockDataApi.GetFollowList())
stockDataApi.GetFollowList(1)
}
func TestStockDataApi_GetIndexBasic(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
stockDataApi.GetIndexBasic()
}

View File

@ -0,0 +1,80 @@
package data
import (
"go-stock/backend/db"
"gorm.io/gorm"
)
// @Author spark
// @Date 2025/4/3 11:18
// @Desc
// -----------------------------------------------------------------------------------
type Group struct {
gorm.Model
Name string `json:"name" gorm:"index"`
Sort int `json:"sort"`
}
func (Group) TableName() string {
return "stock_groups"
}
type GroupStock struct {
gorm.Model
StockCode string `json:"stockCode" gorm:"index"`
GroupId int `json:"groupId" gorm:"index"`
GroupInfo Group `json:"groupInfo" gorm:"foreignKey:GroupId;references:ID"`
}
func (GroupStock) TableName() string {
return "group_stock_info"
}
type StockGroupApi struct {
dao *gorm.DB
}
func NewStockGroupApi(dao *gorm.DB) *StockGroupApi {
return &StockGroupApi{dao: db.Dao}
}
func (receiver StockGroupApi) AddGroup(group Group) bool {
err := receiver.dao.Where("name = ?", group.Name).FirstOrCreate(&group).Updates(&Group{
Name: group.Name,
Sort: group.Sort,
}).Error
return err == nil
}
func (receiver StockGroupApi) GetGroupList() []Group {
var groups []Group
receiver.dao.Find(&groups)
return groups
}
func (receiver StockGroupApi) GetGroupStockByGroupId(groupId int) []GroupStock {
var stockGroup []GroupStock
receiver.dao.Preload("GroupInfo").Where("group_id = ?", groupId).Find(&stockGroup)
return stockGroup
}
func (receiver StockGroupApi) AddStockGroup(groupId int, stockCode string) bool {
err := receiver.dao.Where("group_id = ? and stock_code = ?", groupId, stockCode).FirstOrCreate(&GroupStock{
GroupId: groupId,
StockCode: stockCode,
}).Updates(&GroupStock{
GroupId: groupId,
StockCode: stockCode,
}).Error
return err == nil
}
func (receiver StockGroupApi) RemoveStockGroup(code string, name string, id int) bool {
err := receiver.dao.Where("group_id = ? and stock_code = ?", id, code).Delete(&GroupStock{}).Error
return err == nil
}
func (receiver StockGroupApi) RemoveGroup(id int) bool {
err := receiver.dao.Where("id = ?", id).Delete(&Group{}).Error
err = receiver.dao.Where("group_id = ?", id).Delete(&GroupStock{}).Error
return err == nil
}

View File

@ -0,0 +1,290 @@
package data
import (
"bufio"
"fmt"
"github.com/go-ego/gse"
"go-stock/backend/logger"
"os"
"strings"
)
// 金融情感词典,包含股票市场相关的专业词汇
var (
seg gse.Segmenter
// 正面金融词汇及其权重
positiveFinanceWords = map[string]float64{
"上涨": 2.0, "涨停": 3.0, "牛市": 3.0, "反弹": 2.0, "新高": 2.5,
"利好": 2.5, "增持": 2.0, "买入": 2.0, "推荐": 1.5, "看多": 2.0,
"盈利": 2.0, "增长": 2.0, "超预期": 2.5, "强劲": 1.5, "回升": 1.5,
"复苏": 2.0, "突破": 2.0, "创新高": 3.0, "回暖": 1.5, "上扬": 1.5,
"利好消息": 3.0, "收益增长": 2.5, "利润增长": 2.5, "业绩优异": 2.5,
"潜力股": 2.0, "绩优股": 2.0, "强势": 1.5, "走高": 1.5, "攀升": 1.5,
"大涨": 2.5, "飙升": 3.0, "井喷": 3.0, "爆发": 2.5, "暴涨": 3.0,
}
// 负面金融词汇及其权重
negativeFinanceWords = map[string]float64{
"下跌": 2.0, "跌停": 3.0, "熊市": 3.0, "回调": 1.5, "新低": 2.5,
"利空": 2.5, "减持": 2.0, "卖出": 2.0, "看空": 2.0, "亏损": 2.5,
"下滑": 2.0, "萎缩": 2.0, "不及预期": 2.5, "疲软": 1.5, "恶化": 2.0,
"衰退": 2.0, "跌破": 2.0, "创新低": 3.0, "走弱": 1.5, "下挫": 1.5,
"利空消息": 3.0, "收益下降": 2.5, "利润下滑": 2.5, "业绩不佳": 2.5,
"垃圾股": 2.0, "风险股": 2.0, "弱势": 1.5, "走低": 1.5, "缩量": 2.5,
"大跌": 2.5, "暴跌": 3.0, "崩盘": 3.0, "跳水": 3.0, "重挫": 3.0,
}
// 否定词,用于反转情感极性
negationWords = map[string]struct{}{
"不": {}, "没": {}, "无": {}, "非": {}, "未": {}, "别": {}, "勿": {},
}
// 程度副词,用于调整情感强度
degreeWords = map[string]float64{
"非常": 1.8, "极其": 2.2, "太": 1.8, "很": 1.5,
"比较": 0.8, "稍微": 0.6, "有点": 0.7, "显著": 1.5,
"大幅": 1.8, "急剧": 2.0, "轻微": 0.6, "小幅": 0.7,
}
// 转折词,用于识别情感转折
transitionWords = map[string]struct{}{
"但是": {}, "然而": {}, "不过": {}, "却": {}, "可是": {},
}
)
func init() {
// 加载默认词典
err := seg.LoadDict()
if err != nil {
logger.SugaredLogger.Error(err.Error())
}
}
// SentimentResult 情感分析结果类型
type SentimentResult struct {
Score float64 // 情感得分
Category SentimentType // 情感类别
PositiveCount int // 正面词数量
NegativeCount int // 负面词数量
Description string // 情感描述
}
// SentimentType 情感类型枚举
type SentimentType int
const (
Positive SentimentType = iota
Negative
Neutral
)
// AnalyzeSentiment 判断文本的情感
func AnalyzeSentiment(text string) SentimentResult {
// 初始化得分
score := 0.0
positiveCount := 0
negativeCount := 0
// 分词(简单按单个字符分割)
words := splitWords(text)
// 检查文本是否包含转折词,并分割成两部分
var transitionIndex int
var hasTransition bool
for i, word := range words {
if _, ok := transitionWords[word]; ok {
transitionIndex = i
hasTransition = true
break
}
}
// 处理有转折的文本
if hasTransition {
// 转折前的部分
preTransitionWords := words[:transitionIndex]
preScore, prePos, preNeg := calculateScore(preTransitionWords)
// 转折后的部分,权重加倍
postTransitionWords := words[transitionIndex+1:]
postScore, postPos, postNeg := calculateScore(postTransitionWords)
postScore *= 1.5 // 转折后的情感更重要
score = preScore + postScore
positiveCount = prePos + postPos
negativeCount = preNeg + postNeg
} else {
// 没有转折的文本
score, positiveCount, negativeCount = calculateScore(words)
}
// 确定情感类别
var category SentimentType
switch {
case score > 1.0:
category = Positive
case score < -1.0:
category = Negative
default:
category = Neutral
}
return SentimentResult{
Score: score,
Category: category,
PositiveCount: positiveCount,
NegativeCount: negativeCount,
Description: GetSentimentDescription(category),
}
}
// 计算情感得分
func calculateScore(words []string) (float64, int, int) {
score := 0.0
positiveCount := 0
negativeCount := 0
// 遍历每个词,计算情感得分
for i, word := range words {
// 首先检查是否为程度副词
degree, isDegree := degreeWords[word]
// 检查是否为否定词
_, isNegation := negationWords[word]
// 检查是否为金融正面词
if posScore, isPositive := positiveFinanceWords[word]; isPositive {
// 检查前一个词是否为否定词或程度副词
if i > 0 {
prevWord := words[i-1]
if _, isNeg := negationWords[prevWord]; isNeg {
score -= posScore
negativeCount++
continue
}
if deg, isDeg := degreeWords[prevWord]; isDeg {
score += posScore * deg
positiveCount++
continue
}
}
score += posScore
positiveCount++
continue
}
// 检查是否为金融负面词
if negScore, isNegative := negativeFinanceWords[word]; isNegative {
// 检查前一个词是否为否定词或程度副词
if i > 0 {
prevWord := words[i-1]
if _, isNeg := negationWords[prevWord]; isNeg {
score += negScore
positiveCount++
continue
}
if deg, isDeg := degreeWords[prevWord]; isDeg {
score -= negScore * deg
negativeCount++
continue
}
}
score -= negScore
negativeCount++
continue
}
// 处理程度副词(如果后面跟着情感词)
if isDegree && i+1 < len(words) {
nextWord := words[i+1]
if posScore, isPositive := positiveFinanceWords[nextWord]; isPositive {
score += posScore * degree
positiveCount++
continue
}
if negScore, isNegative := negativeFinanceWords[nextWord]; isNegative {
score -= negScore * degree
negativeCount++
continue
}
}
// 处理否定词(如果后面跟着情感词)
if isNegation && i+1 < len(words) {
nextWord := words[i+1]
if posScore, isPositive := positiveFinanceWords[nextWord]; isPositive {
score -= posScore
negativeCount++
continue
}
if negScore, isNegative := negativeFinanceWords[nextWord]; isNegative {
score += negScore
positiveCount++
continue
}
}
}
return score, positiveCount, negativeCount
}
// 简单的分词函数,考虑了中文和英文
func splitWords(text string) []string {
return seg.Cut(text, true)
}
// GetSentimentDescription 获取情感类别的文本描述
func GetSentimentDescription(category SentimentType) string {
switch category {
case Positive:
return "看涨"
case Negative:
return "看跌"
case Neutral:
return "中性"
default:
return "未知"
}
}
func main() {
// 从命令行读取输入
reader := bufio.NewReader(os.Stdin)
fmt.Println("请输入要分析的股市相关文本输入exit退出")
for {
fmt.Print("> ")
text, err := reader.ReadString('\n')
if err != nil {
fmt.Println("读取输入时出错:", err)
continue
}
// 去除换行符
text = strings.TrimSpace(text)
// 检查是否退出
if text == "exit" {
break
}
// 分析情感
result := AnalyzeSentiment(text)
// 输出结果
fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n",
GetSentimentDescription(result.Category),
result.Score,
result.PositiveCount,
result.NegativeCount)
}
}

View File

@ -0,0 +1,36 @@
package data
import (
"fmt"
"strings"
"testing"
)
// @Author spark
// @Date 2025/6/19 13:05
// @Desc
//-----------------------------------------------------------------------------------
func TestAnalyzeSentiment(t *testing.T) {
// 分析情感
text := " 【调查韩国近两成中小学生过度使用智能手机或互联网】财联社6月19日电韩国女性家族部18日公布的一项年度调查结果显示接受调查的韩国中小学生中共计约17.3%、即超过21万人使用智能手机或互联网的程度达到了“危险水平”这意味着他们因过度依赖智能手机或互联网而需要关注或干预这一比例引人担忧。 (新华社)\n"
text = "消息人士称联合利华Unilever正在为Graze零食品牌寻找买家。\n"
text = "【韩国未来5年将投入51万亿韩元发展文化产业】 据韩联社韩国文化体育观光部文体部今后5年将投入51万亿韩元约合人民币2667亿元预算落实总统李在明在竞选时期提出的“将韩国打造成全球五大文化强国之一”的承诺。\n"
//text = "【油气股持续拉升 国际实业午后涨停】财联社6月19日电油气股午后持续拉升国际实业、宝莫股份午后涨停准油股份、山东墨龙。茂化实华此前涨停通源石油、海默科技、贝肯能源、中曼石油、科力股份等多股涨超5%。\n"
//text = " 【三大指数均跌逾1% 下跌个股近4800只】财联社6月19日电指数持续走弱沪指下挫跌逾1.00%深成指跌1.25%创业板指跌1.39%。核聚变、风电、军工、食品消费等板块指数跌幅居前沪深京三市下跌个股近4800只。\n"
text = "【银行理财首单网下打新落地】财联社6月20日电记者从多渠道获悉光大理财以申报价格17元参与信通电子网下打新并成功入围有效报价成为行业内首家参与网下打新的银行理财公司。光大理财工作人员向证券时报记者表示本次光大理财是以其管理的混合类产品“阳光橙增盈绝对收益策略”参与了此次网下打新该产品为光大理财“固收+”银行理财产品。资料显示信通电子成立于1996年核心产品包括输电线路智能巡检系统、变电站智能辅控系统、移动智能终端及其他产品。根据其招股说明书信通电子2023、2024年营业收入分别较上年增长19.08%和7.97%净利润分别较上年增长5.6%和15.11%。 (证券时报)"
text = " 【以军称拦截数枚伊朗导弹】财联社6月20日电据央视新闻报道以军在贝尔谢巴及周边区域拦截了数枚伊朗导弹但仍有导弹或拦截残骸落地。以色列国防军发文表示搜救队伍正在一处“空中物体落地”的所在区域开展工作公众目前可以离开避难场所。伊朗方面对上述说法暂无回应。"
words := splitWords(text)
fmt.Println(strings.Join(words, " "))
result := AnalyzeSentiment(text)
// 输出结果
fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n",
result.Description,
result.Score,
result.PositiveCount,
result.NegativeCount)
}

View File

@ -0,0 +1,93 @@
package data
import (
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/slice"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"go-stock/backend/logger"
"strings"
"time"
)
// @Author spark
// @Date 2025/2/17 12:33
// @Desc
//-----------------------------------------------------------------------------------
type TushareApi struct {
client *resty.Client
config *Settings
}
func NewTushareApi(config *Settings) *TushareApi {
return &TushareApi{
client: resty.New(),
config: config,
}
}
// GetDaily tushare A股日线行情
func (receiver TushareApi) GetDaily(tsCode, startDate, endDate string, crawlTimeOut int64) string {
//logger.SugaredLogger.Debugf("tushare daily request: ts_code=%s, start_date=%s, end_date=%s", tsCode, startDate, endDate)
fields := "ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount"
resp := &TushareStockBasicResponse{}
stockType := getStockType(tsCode)
tsCodeNEW := getTsCode(tsCode)
//logger.SugaredLogger.Debugf("tushare daily request: %s,tsCode:%s,tsCodeNEW:%s", stockType, tsCode, tsCodeNEW)
_, err := receiver.client.SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
SetHeader("content-type", "application/json").
SetBody(&TushareRequest{
ApiName: stockType,
Token: receiver.config.TushareToken,
Params: map[string]any{
"ts_code": tsCodeNEW,
"start_date": startDate,
"end_date": endDate,
},
Fields: fields}).
SetResult(resp).
Post(tushareApiUrl)
if err != nil {
logger.SugaredLogger.Error(err)
return ""
}
res := ""
if resp.Data.Items != nil && len(resp.Data.Items) > 0 {
fieldsStr := slice.JoinFunc(resp.Data.Fields, ",", func(s string) string {
return "\"" + convertor.ToString(s) + "\""
})
res += fieldsStr + "\n"
for _, item := range resp.Data.Items {
//logger.SugaredLogger.Debugf("%s", slice.Join(item, ","))
t := slice.JoinFunc(item, ",", func(s any) any {
return "\"" + convertor.ToString(s) + "\""
})
res += t + "\n"
}
}
//logger.SugaredLogger.Debugf("tushare response: %s", res)
return res
}
func getTsCode(code string) any {
if strutil.HasPrefixAny(code, []string{"US", "us", "gb_"}) {
code = strings.Replace(code, "gb_", "", 1)
code = strings.Replace(code, "us", "", 1)
return code
}
return code
}
func getStockType(code string) string {
if strutil.HasSuffixAny(code, []string{"SZ", "SH", "sh", "sz"}) {
return "daily"
}
if strutil.HasSuffixAny(code, []string{"HK", "hk"}) {
return "hk_daily"
}
if strutil.HasPrefixAny(code, []string{"US", "us", "gb_"}) {
return "us_daily"
}
return ""
}

View File

@ -0,0 +1,29 @@
package data
import (
"go-stock/backend/db"
"testing"
)
// @Author spark
// @Date 2025/2/17 12:44
// @Desc
// -----------------------------------------------------------------------------------
func TestGetDaily(t *testing.T) {
db.Init("../../data/stock.db")
tushareApi := NewTushareApi(GetConfig())
res := tushareApi.GetDaily("00927.HK", "20250101", "20250217", 30)
t.Log(res)
}
func TestGetUSDaily(t *testing.T) {
db.Init("../../data/stock.db")
tushareApi := NewTushareApi(GetConfig())
res := tushareApi.GetDaily("gb_AAPL", "20250101", "20250217", 30)
t.Log(res)
//
}

111
backend/data/utils.go Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,47 @@
package data
import (
"github.com/duke-git/lancet/v2/slice"
"go-stock/backend/logger"
"os"
"testing"
)
// TestRemoveNonPrintable tests the RemoveAllBlankChar function.
func TestRemoveNonPrintable(t *testing.T) {
//tests := []struct {
// input string
// expected string
//}{
// {"新 希 望", "新希望"},
// {"", ""},
// {"Hello, World!", "Hello, World!"},
// {"\x00\x01\x02", ""},
// {"Hello\x00World", "HelloWorld"},
// {"\x1F\x20\x7E\x7F", " \x7E"},
//}
//for _, test := range tests {
// actual := RemoveAllBlankChar(test.input)
// if actual != test.expected {
// t.Errorf("RemoveAllBlankChar(%q) = %q; expected %q", test.input, actual, test.expected)
// }
//}
txt := "新 希 望"
txt2 := RemoveAllBlankChar(txt)
logger.SugaredLogger.Infof("RemoveAllBlankChar(%s)", txt2)
logger.SugaredLogger.Infof("RemoveAllBlankChar(%s)", txt)
}
func TestConvertStockCodeToTushareCode(t *testing.T) {
logger.SugaredLogger.Infof("ConvertStockCodeToTushareCode(%s)", ConvertStockCodeToTushareCode("sz000802"))
logger.SugaredLogger.Infof("ConvertTushareCodeToStockCode(%s)", ConvertTushareCodeToStockCode("000802.SZ"))
}
func TestReplaceSensitiveWords(t *testing.T) {
txt := "新 希 望习近平"
txt2 := ReplaceSensitiveWords(txt)
logger.SugaredLogger.Infof("ReplaceSensitiveWords(%s)", txt2)
os.WriteFile("words.txt", []byte(slice.Join(SensitiveWords, "\n")), 0644)
}

364
backend/models/models.go Normal file
View File

@ -0,0 +1,364 @@
package models
import (
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
"time"
)
// @Author spark
// @Date 2025/2/6 15:25
// @Desc
//-----------------------------------------------------------------------------------
type GitHubReleaseVersion struct {
Url string `json:"url"`
AssetsUrl string `json:"assets_url"`
UploadUrl string `json:"upload_url"`
HtmlUrl string `json:"html_url"`
Id int `json:"id"`
Author struct {
Login string `json:"login"`
Id int `json:"id"`
NodeId string `json:"node_id"`
AvatarUrl string `json:"avatar_url"`
GravatarId string `json:"gravatar_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
FollowersUrl string `json:"followers_url"`
FollowingUrl string `json:"following_url"`
GistsUrl string `json:"gists_url"`
StarredUrl string `json:"starred_url"`
SubscriptionsUrl string `json:"subscriptions_url"`
OrganizationsUrl string `json:"organizations_url"`
ReposUrl string `json:"repos_url"`
EventsUrl string `json:"events_url"`
ReceivedEventsUrl string `json:"received_events_url"`
Type string `json:"type"`
UserViewType string `json:"user_view_type"`
SiteAdmin bool `json:"site_admin"`
} `json:"author"`
NodeId string `json:"node_id"`
TagName string `json:"tag_name"`
TargetCommitish string `json:"target_commitish"`
Name string `json:"name"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
CreatedAt time.Time `json:"created_at"`
PublishedAt time.Time `json:"published_at"`
Assets []struct {
Url string `json:"url"`
Id int `json:"id"`
NodeId string `json:"node_id"`
Name string `json:"name"`
Label string `json:"label"`
Uploader struct {
Login string `json:"login"`
Id int `json:"id"`
NodeId string `json:"node_id"`
AvatarUrl string `json:"avatar_url"`
GravatarId string `json:"gravatar_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
FollowersUrl string `json:"followers_url"`
FollowingUrl string `json:"following_url"`
GistsUrl string `json:"gists_url"`
StarredUrl string `json:"starred_url"`
SubscriptionsUrl string `json:"subscriptions_url"`
OrganizationsUrl string `json:"organizations_url"`
ReposUrl string `json:"repos_url"`
EventsUrl string `json:"events_url"`
ReceivedEventsUrl string `json:"received_events_url"`
Type string `json:"type"`
UserViewType string `json:"user_view_type"`
SiteAdmin bool `json:"site_admin"`
} `json:"uploader"`
ContentType string `json:"content_type"`
State string `json:"state"`
Size int `json:"size"`
DownloadCount int `json:"download_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
BrowserDownloadUrl string `json:"browser_download_url"`
} `json:"assets"`
TarballUrl string `json:"tarball_url"`
ZipballUrl string `json:"zipball_url"`
Body string `json:"body"`
Tag Tag `json:"tag"`
Commit Commit `json:"commit"`
}
type Tag struct {
Ref string `json:"ref"`
NodeId string `json:"node_id"`
Url string `json:"url"`
Object struct {
Sha string `json:"sha"`
Type string `json:"type"`
Url string `json:"url"`
} `json:"object"`
}
type Commit struct {
Sha string `json:"sha"`
NodeId string `json:"node_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
Author struct {
Name string `json:"name"`
Email string `json:"email"`
Date time.Time `json:"date"`
} `json:"author"`
Committer struct {
Name string `json:"name"`
Email string `json:"email"`
Date time.Time `json:"date"`
} `json:"committer"`
Tree struct {
Sha string `json:"sha"`
Url string `json:"url"`
} `json:"tree"`
Message string `json:"message"`
Parents []struct {
Sha string `json:"sha"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
} `json:"parents"`
Verification struct {
Verified bool `json:"verified"`
Reason string `json:"reason"`
Signature interface{} `json:"signature"`
Payload interface{} `json:"payload"`
VerifiedAt interface{} `json:"verified_at"`
} `json:"verification"`
}
type AIResponseResult struct {
gorm.Model
ChatId string `json:"chatId"`
ModelName string `json:"modelName"`
StockCode string `json:"stockCode"`
StockName string `json:"stockName"`
Question string `json:"question"`
Content string `json:"content"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver AIResponseResult) TableName() string {
return "ai_response_result"
}
type VersionInfo struct {
gorm.Model
Version string `json:"version"`
Content string `json:"content"`
Icon string `json:"icon"`
Alipay string `json:"alipay"`
Wxpay string `json:"wxpay"`
BuildTimeStamp int64 `json:"buildTimeStamp"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver VersionInfo) TableName() string {
return "version_info"
}
type StockInfoHK struct {
gorm.Model
Code string `json:"code"`
Name string `json:"name"`
FullName string `json:"fullName"`
EName string `json:"eName"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver StockInfoHK) TableName() string {
return "stock_base_info_hk"
}
type StockInfoUS struct {
gorm.Model
Code string `json:"code"`
Name string `json:"name"`
FullName string `json:"fullName"`
EName string `json:"eName"`
Exchange string `json:"exchange"`
Type string `json:"type"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
func (receiver StockInfoUS) TableName() string {
return "stock_base_info_us"
}
type Resp struct {
Code int `json:"code"`
Message string `json:"message"`
}
type PromptTemplate struct {
ID int `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `json:"name"`
Content string `json:"content"`
Type string `json:"type"`
}
func (p PromptTemplate) TableName() string {
return "prompt_templates"
}
type Prompt struct {
ID int `json:"ID"`
Name string `json:"name"`
Content string `json:"content"`
Type string `json:"type"`
}
type Telegraph struct {
gorm.Model
Time string `json:"time"`
Content string `json:"content"`
SubjectTags []string `json:"subjects" gorm:"-:all"`
StocksTags []string `json:"stocks" gorm:"-:all"`
IsRed bool `json:"isRed"`
Url string `json:"url"`
Source string `json:"source"`
TelegraphTags []TelegraphTags `json:"tags" gorm:"-:migration;foreignKey:TelegraphId"`
SentimentResult string `json:"sentimentResult" gorm:"-:all"`
}
type TelegraphTags struct {
gorm.Model
TagId uint `json:"tagId"`
TelegraphId uint `json:"telegraphId"`
}
func (t TelegraphTags) TableName() string {
return "telegraph_tags"
}
type Tags struct {
gorm.Model
Name string `json:"name"`
Type string `json:"type"`
}
func (p Tags) TableName() string {
return "tags"
}
func (p Telegraph) TableName() string {
return "telegraph_list"
}
type SinaStockInfo struct {
Symbol string `json:"symbol"`
Name string `json:"name"`
Engname string `json:"engname"`
Tradetype string `json:"tradetype"`
Lasttrade string `json:"lasttrade"`
Prevclose string `json:"prevclose"`
Open string `json:"open"`
High string `json:"high"`
Low string `json:"low"`
Volume string `json:"volume"`
Currentvolume string `json:"currentvolume"`
Amount string `json:"amount"`
Ticktime string `json:"ticktime"`
Buy string `json:"buy"`
Sell string `json:"sell"`
High52Week string `json:"high_52week"`
Low52Week string `json:"low_52week"`
Eps string `json:"eps"`
Dividend string `json:"dividend"`
StocksSum string `json:"stocks_sum"`
Pricechange string `json:"pricechange"`
Changepercent string `json:"changepercent"`
MarketValue string `json:"market_value"`
PeRatio string `json:"pe_ratio"`
}
type LongTigerRankData struct {
ACCUMAMOUNT float64 `json:"ACCUM_AMOUNT"`
BILLBOARDBUYAMT float64 `json:"BILLBOARD_BUY_AMT"`
BILLBOARDDEALAMT float64 `json:"BILLBOARD_DEAL_AMT"`
BILLBOARDNETAMT float64 `json:"BILLBOARD_NET_AMT"`
BILLBOARDSELLAMT float64 `json:"BILLBOARD_SELL_AMT"`
CHANGERATE float64 `json:"CHANGE_RATE"`
CLOSEPRICE float64 `json:"CLOSE_PRICE"`
DEALAMOUNTRATIO float64 `json:"DEAL_AMOUNT_RATIO"`
DEALNETRATIO float64 `json:"DEAL_NET_RATIO"`
EXPLAIN string `json:"EXPLAIN"`
EXPLANATION string `json:"EXPLANATION"`
FREEMARKETCAP float64 `json:"FREE_MARKET_CAP"`
SECUCODE string `json:"SECUCODE" gorm:"index"`
SECURITYCODE string `json:"SECURITY_CODE"`
SECURITYNAMEABBR string `json:"SECURITY_NAME_ABBR"`
SECURITYTYPECODE string `json:"SECURITY_TYPE_CODE"`
TRADEDATE string `json:"TRADE_DATE" gorm:"index"`
TURNOVERRATE float64 `json:"TURNOVERRATE"`
}
func (l LongTigerRankData) TableName() string {
return "long_tiger_rank"
}
type TVNews struct {
Id string `json:"id"`
Title string `json:"title"`
Published int `json:"published"`
Urgency int `json:"urgency"`
Permission string `json:"permission"`
StoryPath string `json:"storyPath"`
Provider struct {
Id string `json:"id"`
Name string `json:"name"`
LogoId string `json:"logo_id"`
} `json:"provider"`
}
type XUEQIUHot struct {
Data struct {
Items []HotItem `json:"items"`
ItemsSize int `json:"items_size"`
} `json:"data"`
ErrorCode int `json:"error_code"`
ErrorDescription string `json:"error_description"`
}
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 HotEvent struct {
PicSize interface{} `json:"pic_size"`
Tag string `json:"tag"`
Id int `json:"id"`
Pic string `json:"pic"`
Hot int `json:"hot"`
StatusCount int `json:"status_count"`
Content string `json:"content"`
}

View File

@ -0,0 +1,49 @@
package models
import (
"encoding/json"
"github.com/duke-git/lancet/v2/strutil"
"go-stock/backend/db"
"go-stock/backend/logger"
"os"
"testing"
)
// @Author spark
// @Date 2025/2/22 16:09
// @Desc
// -----------------------------------------------------------------------------------
type StockInfoHKResp struct {
Code int `json:"code"`
Status string `json:"status"`
StockInfos *[]StockInfoData `json:"data"`
}
type StockInfoData struct {
C string `json:"c"`
N string `json:"n"`
T string `json:"t"`
E string `json:"e"`
}
func TestStockInfoHK(t *testing.T) {
db.Init("../../data/stock.db")
db.Dao.AutoMigrate(&StockInfoHK{})
bs, _ := os.ReadFile("../../build/hk.json")
v := &StockInfoHKResp{}
err := json.Unmarshal(bs, v)
if err != nil {
return
}
hks := &[]StockInfoHK{}
for i, data := range *v.StockInfos {
logger.SugaredLogger.Infof("第%d条数据: %+v", i, data)
hk := &StockInfoHK{
Code: strutil.PadStart(data.C, 5, "0") + ".HK",
EName: data.N,
}
*hks = append(*hks, *hk)
}
db.Dao.Create(&hks)
}

BIN
build/app.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

15192
build/hk.json Normal file

File diff suppressed because it is too large Load Diff

BIN
build/screenshot/alipay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
build/screenshot/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

BIN
build/screenshot/img13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

BIN
build/screenshot/img_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
build/screenshot/img_10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
build/screenshot/img_11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
build/screenshot/img_12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
build/screenshot/img_13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

BIN
build/screenshot/img_14.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

BIN
build/screenshot/img_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
build/screenshot/img_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
build/screenshot/img_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
build/screenshot/img_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
build/screenshot/img_6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
build/screenshot/img_7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
build/screenshot/img_8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
build/screenshot/img_9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
build/screenshot/wxpay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

48622
build/stock_base_info_hk.json Normal file

File diff suppressed because it is too large Load Diff

189906
build/stock_base_info_us.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -5,19 +5,19 @@
!include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "{{.Name}}"
!define INFO_PROJECTNAME "go-stock"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
!define INFO_COMPANYNAME "sparkmemory"
!endif
!ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
!define INFO_PRODUCTNAME "go-stock"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
!define INFO_PRODUCTVERSION "1.0.0"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
!define INFO_COPYRIGHT "Copyright#sparkmemory@163.com"
!endif
!ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
@ -203,20 +203,12 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!macro wails.associateFiles
; Create file associations
{{range .Info.FileAssociations}}
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
File "..\{{.IconName}}.ico"
{{end}}
!macroend
!macro wails.unassociateFiles
; Delete app associations
{{range .Info.FileAssociations}}
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
Delete "$INSTDIR\{{.IconName}}.ico"
{{end}}
!macroend
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
@ -235,15 +227,10 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!macro wails.associateCustomProtocols
; Create custom protocols associations
{{range .Info.Protocols}}
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
{{end}}
!macroend
!macro wails.unassociateCustomProtocols
; Delete app custom protocol associations
{{range .Info.Protocols}}
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
{{end}}
!macroend

BIN
build/wx.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>wails-naive-demo</title>
<title>go-stock:AI赋能股票分析</title>
<link href="./src/style.css" rel="stylesheet">
</head>
<body>

File diff suppressed because it is too large Load Diff

View File

@ -9,14 +9,37 @@
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.2.25"
"@types/file-saver": "^2.0.7",
"@vavt/cm-extension": "^1.8.0",
"@vavt/v3-extension": "^3.0.0",
"date-fns": "^4.1.0",
"echarts": "^5.6.0",
"file-saver": "^2.0.5",
"html2canvas": "^1.4.1",
"lodash": "^4.17.21",
"md-editor-v3": "^5.2.3",
"vue": "^3.2.25",
"vue-router": "^4.5.0",
"vue3-danmaku": "^1.6.1"
},
"devDependencies": {
"@vicons/antd": "^0.13.0",
"@vicons/carbon": "^0.13.0",
"@vicons/fa": "^0.13.0",
"@vicons/fluent": "^0.13.0",
"@vicons/ionicons4": "^0.13.0",
"@vicons/ionicons5": "^0.13.0",
"@vicons/material": "^0.13.0",
"@vicons/tabler": "^0.13.0",
"@vitejs/plugin-vue": "^5.2.1",
"naive-ui": "^2.40.3",
"html-docx-js-typescript": "^0.1.5",
"naive-ui": "^2.41.0",
"vfonts": "^0.0.3",
"vite": "5.4.6"
"vite": "^6.3.5"
},
"keywords": [],
"keywords": [
"AI赋能股票分析",
"go-stock"
],
"author": "spark"
}

View File

@ -1 +1 @@
9ce62efac1fed08499bbf20c8a5fd1b2
2d63c3a999d797889c01d6c96451b197

View File

@ -1,32 +1,678 @@
<script setup>
import stockInfo from './components/stock.vue'
import {ref} from "vue";
import {
EventsEmit,
EventsOff,
EventsOn,
Quit,
WindowFullscreen,
WindowHide,
WindowUnfullscreen
} from '../wailsjs/runtime'
import {h, onBeforeMount, onBeforeUnmount, onMounted, ref} from "vue";
import {RouterLink, useRouter} from 'vue-router'
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,dateZhCN,zhCN} from 'naive-ui'
import {
AlarmOutline,
AnalyticsOutline,
BarChartSharp, Bonfire, BonfireOutline, EaselSharp,
ExpandOutline, Flag,
Flame, FlameSharp, InformationOutline,
LogoGithub,
NewspaperOutline,
NewspaperSharp, Notifications,
PowerOutline, Pulse,
ReorderTwoOutline,
SettingsOutline, Skull, SkullOutline, SkullSharp,
SparklesOutline,
StarOutline,
Wallet, WarningOutline,
} from '@vicons/ionicons5'
import {AnalyzeSentiment, GetConfig, GetGroupList} from "../wailsjs/go/main/App";
import {Dragon, Fire, Gripfire} from "@vicons/fa";
import {ReportSearch} from "@vicons/tabler";
import {LocalFireDepartmentRound} from "@vicons/material";
import {BoxSearch20Regular, CommentNote20Filled} from "@vicons/fluent";
import {FireFilled, FireOutlined, NotificationFilled, StockOutlined} from "@vicons/antd";
const content = ref('数据来源于网络,仅供参考\n投资有风险,入市需谨慎')
const router = useRouter()
const loading = ref(true)
const loadingMsg = ref("加载数据中...")
const enableNews = ref(false)
const contentStyle = ref("")
const enableFund = ref(false)
const enableDarkTheme = ref(null)
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎\n\n未经授权,禁止商业目的!')
const isFullscreen = ref(false)
const activeKey = ref('')
const containerRef = ref({})
const realtimeProfit = ref(0)
const telegraph = ref([])
const groupList = ref([])
const menuOptions = ref([
{
label: () =>
h(
RouterLink,
{
to: {
name: 'stock',
query: {
groupName: '全部',
groupId: 0,
},
params: {},
}
},
{default: () => '股票自选',}
),
key: 'stock',
icon: renderIcon(StarOutline),
children: [
{
label: () =>
h(
'a',
{
href: '#',
type: 'info',
onClick: () => {
//console.log("push",item)
router.push({
name: 'stock',
query: {
groupName: '全部',
groupId: 0,
},
})
EventsEmit("changeTab", {ID: 0, name: '全部'})
},
to: {
name: 'stock',
query: {
groupName: '全部',
groupId: 0,
},
}
},
{default: () => '全部',}
),
key: 0,
}
],
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
params: {}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
},
},
{default: () => '市场行情'}
),
key: 'market',
icon: renderIcon(NewspaperOutline),
children: [
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "市场快讯",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
},
},
{default: () => '市场快讯',}
),
key: 'market1',
icon: renderIcon(NewspaperSharp),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "全球股指",
},
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '全球股指'})
},
},
{default: () => '全球股指',}
),
key: 'market2',
icon: renderIcon(BarChartSharp),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "指标行情",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '指标行情'})
},
},
{default: () => '指标行情',}
),
key: 'market3',
icon: renderIcon(AnalyticsOutline),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "行业排名",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '行业排名'})
},
},
{default: () => '行业排名',}
),
key: 'market4',
icon: renderIcon(Flag),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "个股资金流向",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '个股资金流向'})
},
},
{default: () => '个股资金流向',}
),
key: 'market5',
icon: renderIcon(Pulse),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "龙虎榜",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '龙虎榜'})
},
},
{default: () => '龙虎榜',}
),
key: 'market6',
icon: renderIcon(Dragon),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "个股研报",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '个股研报'})
},
},
{default: () => '个股研报',}
),
key: 'market7',
icon: renderIcon(StockOutlined),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "公司公告",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '公司公告'})
},
},
{default: () => '公司公告',}
),
key: 'market8',
icon: renderIcon(NotificationFilled),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "行业研究",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '行业研究'})
},
},
{default: () => '行业研究',}
),
key: 'market9',
icon: renderIcon(ReportSearch),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "当前热门",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '当前热门'})
},
},
{default: () => '当前热门',}
),
key: 'market10',
icon: renderIcon(Gripfire),
},
{
label: () =>
h(
RouterLink,
{
href: '#',
to: {
name: 'market',
query: {
name: "指标选股",
}
},
onClick: () => {
EventsEmit("changeMarketTab", {ID: 0, name: '指标选股'})
},
},
{default: () => '指标选股',}
),
key: 'market11',
icon: renderIcon(BoxSearch20Regular),
},
]
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'fund',
params: {},
}
},
{default: () => '基金自选',}
),
show: enableFund.value,
key: 'fund',
icon: renderIcon(SparklesOutline),
children: [
{
label: () => h(NText, {type: realtimeProfit.value > 0 ? 'error' : 'success'}, {default: () => '功能完善中!'}),
key: 'realtimeProfit',
show: realtimeProfit.value,
icon: renderIcon(AlarmOutline),
},
]
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'settings',
params: {}
}
},
{default: () => '设置'}
),
key: 'settings',
icon: renderIcon(SettingsOutline),
},
{
label: () =>
h(
RouterLink,
{
to: {
name: 'about',
params: {}
}
},
{default: () => '关于'}
),
key: 'about',
icon: renderIcon(LogoGithub),
},
{
label: () => h("a", {
href: '#',
onClick: toggleFullscreen,
title: '全屏 Ctrl+F 退出全屏 Esc',
}, {default: () => isFullscreen.value ? '取消全屏' : '全屏'}),
key: 'full',
icon: renderIcon(ExpandOutline),
},
{
label: () => h("a", {
href: '#',
onClick: WindowHide,
title: '隐藏到托盘区 Ctrl+H',
}, {default: () => '隐藏到托盘区'}),
key: 'hide',
icon: renderIcon(ReorderTwoOutline),
},
// {
// label: ()=> h("a", {
// href: 'javascript:void(0)',
// style: 'cursor: move;',
// onClick: toggleStartMoveWindow,
// }, { default: () => '' }),
// key: 'move',
// icon: renderIcon(MoveOutline),
// },
{
label: () => h("a", {
href: '#',
onClick: Quit,
}, {default: () => '退出程序'}),
key: 'exit',
icon: renderIcon(PowerOutline),
},
])
function renderIcon(icon) {
return () => h(NIcon, null, {default: () => h(icon)})
}
function toggleFullscreen(e) {
//console.log(e)
if (isFullscreen.value) {
WindowUnfullscreen()
//e.target.innerHTML = ''
} else {
WindowFullscreen()
// e.target.innerHTML = ''
}
isFullscreen.value = !isFullscreen.value
}
// const drag = ref(false)
// const lastPos= ref({x:0,y:0})
// function toggleStartMoveWindow(e) {
// drag.value=!drag.value
// lastPos.value={x:e.clientX,y:e.clientY}
// }
// function dragstart(e) {
// if (drag.value) {
// let x=e.clientX-lastPos.value.x
// let y=e.clientY-lastPos.value.y
// WindowGetPosition().then((pos) => {
// WindowSetPosition(pos.x+x,pos.y+y)
// })
// }
// }
// window.addEventListener('mousemove', dragstart)
EventsOn("realtime_profit", (data) => {
realtimeProfit.value = data
})
EventsOn("telegraph", (data) => {
telegraph.value = data
})
EventsOn("loadingMsg", (data) => {
if(data==="done"){
loadingMsg.value = "加载完成..."
EventsEmit("loadingDone", "app")
loading.value = false
}else{
loading.value = true
loadingMsg.value = data
}
})
onBeforeUnmount(() => {
EventsOff("realtime_profit")
EventsOff("loadingMsg")
EventsOff("telegraph")
EventsOff("newsPush")
})
window.onerror = function (msg, source, lineno, colno, error) {
//
EventsEmit("frontendError", {
page: "App.vue",
message: msg,
source: source,
lineno: lineno,
colno: colno,
error: error ? error.stack : null,
});
return true;
};
onBeforeMount(() => {
GetGroupList().then(result => {
groupList.value = result
menuOptions.value.map((item) => {
//console.log(item)
if (item.key === 'stock') {
item.children.push(...groupList.value.map(item => {
return {
label: () =>
h(
'a',
{
href: '#',
type: 'info',
onClick: () => {
//console.log("push",item)
router.push({
name: 'stock',
query: {
groupName: item.name,
groupId: item.ID,
},
})
setTimeout(() => {
EventsEmit("changeTab", item)
}, 100)
},
to: {
name: 'stock',
query: {
groupName: item.name,
groupId: item.ID,
},
}
},
{default: () => item.name,}
),
key: item.ID,
}
}))
}
})
})
GetConfig().then((res) => {
//console.log(res)
enableFund.value = res.enableFund
menuOptions.value.filter((item) => {
if (item.key === 'fund') {
item.show = res.enableFund
}
})
if (res.darkTheme) {
enableDarkTheme.value = darkTheme
} else {
enableDarkTheme.value = null
}
})
})
onMounted(() => {
contentStyle.value = "max-height: calc(92vh);overflow: hidden"
GetConfig().then((res) => {
if (res.enableNews) {
enableNews.value = true
}
enableFund.value = res.enableFund
const {notification } =createDiscreteApi(["notification"], {
configProviderProps: {
theme: enableDarkTheme.value ? darkTheme : lightTheme ,
max: 3,
},
})
EventsOn("newsPush", (data) => {
//console.log(data)
if(data.isRed){
notification.create({
//type:"error",
// avatar: () => h(NIcon,{component:Notifications,color:"red"}),
title: data.time,
content: () => h(NText,{type:"error"}, { default: () => data.content }),
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
duration:1000*40,
})
}else{
notification.create({
//type:"info",
//avatar: () => h(NIcon,{component:Notifications}),
title: data.time,
content: () => h(NText,{type:"info"}, { default: () => data.content }),
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
duration:1000*30 ,
})
}
})
})
})
</script>
<template>
<n-watermark
:content="content"
cross
selectable
:font-size="12"
:line-height="12"
:height="400"
:width="200"
:x-offset="50"
:y-offset="50"
:rotate="-15"
style="height: 100%"
>
<n-flex justify="center">
<n-message-provider >
<n-modal-provider>
<stockInfo/>
</n-modal-provider>
<n-config-provider ref="containerRef" :theme="enableDarkTheme" :locale="zhCN" :date-locale="dateZhCN">
<n-message-provider>
<n-notification-provider>
<n-modal-provider>
<n-dialog-provider>
<n-watermark
:content="content"
cross
selectable
:font-size="16"
:line-height="16"
:width="500"
:height="400"
:x-offset="50"
:y-offset="150"
:rotate="-15"
>
<n-flex>
<n-grid x-gap="12" :cols="1">
<n-gi>
<n-spin :show="loading">
<template #description>
{{ loadingMsg }}
</template>
<n-marquee :speed="100" style="position: relative;top:0;z-index: 19;width: 100%"
v-if="(telegraph.length>0)&&(enableNews)">
<n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
{{ item }}
</n-tag>
</n-marquee>
<n-scrollbar :style="contentStyle">
<n-skeleton v-if="loading" height="calc(100vh)" />
<RouterView/>
</n-scrollbar>
</n-spin>
</n-gi>
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%;">
<n-card size="small" style="--wails-draggable:drag">
<n-menu style="font-size: 18px;"
v-model:value="activeKey"
mode="horizontal"
:options="menuOptions"
responsive
/>
</n-card>
</n-gi>
</n-grid>
</n-flex>
</n-watermark>
</n-dialog-provider>
</n-modal-provider>
</n-notification-provider>
</n-message-provider>
</n-flex>
</n-watermark>
</n-config-provider>
</template>
<style>

View File

@ -0,0 +1,102 @@
<script setup lang="ts">
import {nextTick, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
import {ClsCalendar} from "../../wailsjs/go/main/App";
import { addMonths, format ,parse} from 'date-fns';
import { zhCN } from 'date-fns/locale';
import {useMessage} from 'naive-ui'
import {Star48Filled} from "@vicons/fluent";
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0'); // 0+1
const day = String(today.getDate()).padStart(2, '0');
// YYYY-MM-DD
const formattedDate = `${year}-${month}-${day}`;
const formattedYM = `${year}-${month}`;
const list = ref([])
const message=useMessage()
function goBackToday() {
setTimeout(() => {
nextTick(
() => {
const elementById = document.getElementById(formattedDate);
if (elementById) {
elementById.scrollIntoView({
behavior: 'auto',
block: 'start'
})
}
}
)
}, 500)
}
onBeforeMount(() => {
ClsCalendar().then(res => {
list.value = res
goBackToday();
})
})
function getweekday(date){
let day=parse(date, 'yyyy-MM-dd', new Date())
return format(day, 'EEEE', {locale: zhCN})
}
</script>
<template>
<!-- <n-timeline size="large" style="text-align: left">-->
<!-- <n-timeline-item v-for="item in list" :key="item.date" :title="item.date" type="info" >-->
<!-- <n-list>-->
<!-- <n-list-item v-for="l in item.list" :key="l.article_id ">-->
<!-- <n-text>{{l.title}}</n-text>-->
<!-- </n-list-item>-->
<!-- </n-list>-->
<!-- </n-timeline-item>-->
<!-- </n-timeline>-->
<n-list bordered style="max-height: calc(100vh - 230px);text-align: left;">
<n-scrollbar style="max-height: calc(100vh - 230px);" >
<n-list-item v-for="(item, index) in list" :id="item.calendar_day" :key="item.calendar_day">
<n-thing :title="item.calendar_day +' '+item.week">
<n-list :bordered="false" hoverable>
<n-list-item v-for="(l,i ) in item.items" :key="l.id ">
<n-flex justify="space-between">
<n-text :type="item.calendar_day===formattedDate?'warning':'info'">{{i+1}}# {{l.title}}
<n-tag v-if="l.event" size="small" round type="success">事件</n-tag>
<n-tag v-if="l.economic" size="small" round type="error">数据</n-tag>
</n-text>
<n-rate v-if="l.event&&(l.event.star>0)" readonly :default-value="l.event.star">
<n-icon :component="Star48Filled"/>
</n-rate>
<n-rate v-if="l.economic&&(l.economic.star>0)" readonly :default-value="l.economic.star" >
<n-icon :component="Star48Filled"/>
</n-rate>
</n-flex>
<n-flex v-if="l.economic">
<n-tag type="warning" :bordered="false" :size="'small'">公布{{l.economic.actual }}</n-tag>
<n-tag type="warning" :bordered="false" :size="'small'">预测{{l.economic.consensus}}</n-tag>
<n-tag type="warning" :bordered="false" :size="'small'">前值{{l.economic.front}}</n-tag>
</n-flex>
</n-list-item>
</n-list>
</n-thing>
</n-list-item>
<n-list-item v-if="list.length==0">
<n-text type="info">没有数据</n-text>
</n-list-item>
<n-list-item v-else style="text-align: center;">
<n-button-group>
<n-button strong secondary type="warning" @click="goBackToday">回到今天</n-button>
</n-button-group>
</n-list-item>
</n-scrollbar>
</n-list>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,37 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotEvent} from "../../wailsjs/go/main/App";
const list = ref([])
const task =ref()
onBeforeMount(async () => {
list.value = await HotEvent(50)
task.value=setInterval(async ()=>{
list.value = await HotEvent(50)
}, 1000*10)
})
onUnmounted(async ()=>{
clearInterval(task.value)
})
</script>
<template>
<n-list bordered>
<template #header>
雪球热门
</template>
<n-list-item v-for="(item, index) in list" :key="index">
<n-thing :title="item.tag" :description="item.content" >
<template v-if="item.pic" #avatar>
<n-avatar :src="item.pic" :size="60">
</n-avatar>
</template>
</n-thing>
</n-list-item>
</n-list>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,88 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotStock} from "../../wailsjs/go/main/App";
import KLineChart from "./KLineChart.vue";
import {ArrowBack, ArrowDown, ArrowUp} from "@vicons/ionicons5";
const {marketType}=defineProps(
{
marketType: {
type: String,
default: '10'
}
}
)
const task =ref()
const list = ref([])
onBeforeMount(async () => {
list.value = await HotStock(marketType)
task.value = setInterval(async () => {
list.value = await HotStock(marketType)
}, 5000)
})
onUnmounted(()=>{
clearInterval(task.value)
})
function getMarketCode(item) {
if (item.exchange === 'SZ') {
return item.code.toLowerCase()
}
if (item.exchange === 'SH') {
return item.code.toLowerCase()
}
if (item.exchange === 'HK') {
return (item.exchange + item.code).toLowerCase()
}
return ("gb_"+item.code).toLowerCase()
}
</script>
<template>
<n-table striped size="small">
<n-thead>
<n-tr>
<n-th>股票名称</n-th>
<n-th>涨跌幅</n-th>
<n-th>当前价格</n-th>
<n-th>热度</n-th>
<n-th>热度变化</n-th>
<n-th>排名变化</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in list" :key="item.code">
<n-td><n-text type="info">
<n-popover trigger="hover" placement="right">
<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>
</n-popover>
</n-text></n-td>
<n-td><n-text :type="item.percent>0?'error':'success'">{{item.percent}}%</n-text></n-td>
<n-td><n-text type="info">{{item.current}}</n-text></n-td>
<n-td><n-text type="info">{{item.value}}</n-text></n-td>
<n-td><n-text :type="item.increment>0?'error':'success'">
{{item.increment}}
<n-icon v-if="item.increment>0" :component="ArrowUp"/>
<n-icon v-else :component="ArrowDown"/>
</n-text></n-td>
<n-td>
<n-text :type="item.rank_change>0?'error':'success'">
{{item.rank_change}}
<n-icon v-if="item.rank_change>0" :component="ArrowUp"/>
<n-text v-else-if="item.rank_change==0" ></n-text>
<n-icon v-else :component="ArrowDown"/>
</n-text>
</n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,73 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotTopic} from "../../wailsjs/go/main/App";
const list = ref([])
const task =ref()
onBeforeMount(async () => {
list.value = await HotTopic(10)
setInterval(async ()=>{
list.value = await HotTopic(10)
}, 1000*10)
})
onUnmounted(()=>{
clearInterval(task.value)
})
function openCenteredWindow(url, width, height) {
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
return window.open(
url,
'centeredWindow',
`width=${width},height=${height},left=${left},top=${top}`
);
}
function showPage(htid) {
openCenteredWindow(`https://gubatopic.eastmoney.com/topic_v3.html?htid=${htid}`, 1000, 600)
}
</script>
<template>
<n-list bordered hoverable clickable>
<!-- <template #header>-->
<!-- 股吧热门-->
<!-- </template>-->
<n-list-item v-for="(item, index) in list" :key="index">
<n-thing :title="item.nickname" :description="item.desc" :description-style="'font-size: 14px;'" @click="showPage(item.htid)">
<template v-if="item.squareImg" #avatar>
<n-avatar :src="item.squareImg" :size="60">
</n-avatar>
</template>
<template v-if="item.stock_list" #footer>
<n-flex>
<n-tag type="info" v-for="(v, i) in item.stock_list" :bordered="false" size="small">
{{v.name}}
</n-tag>
</n-flex>
</template>
<template v-if="item.clickNumber" #header-extra>
<n-flex>
<n-button secondary type="warning" size="tiny">讨论数<n-number-animation
show-separator
:from="0"
:to="item.postNumber"
/>
</n-button >
<n-tag :bordered="false" type="warning" size="small">浏览量<n-number-animation
show-separator
:from="0"
:to="item.clickNumber"
/>
</n-tag>
</n-flex>
</template>
</n-thing>
</n-list-item>
</n-list>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,115 @@
<script setup>
import {onBeforeMount, ref} from 'vue'
import {GetStockList, IndustryResearchReport,EMDictCode} from "../../wailsjs/go/main/App";
import {ArrowDownOutline, CaretDown, CaretUp, PulseOutline, Refresh, RefreshCircleSharp,} from "@vicons/ionicons5";
import {useMessage} from "naive-ui";
import {BrowserOpenURL} from "../../wailsjs/runtime";
const message=useMessage()
const list = ref([])
const options = ref([])
function getIndustryResearchReport(value) {
message.loading("正在刷新数据...")
IndustryResearchReport(value).then(result => {
console.log(result)
list.value = result
})
}
onBeforeMount(()=>{
getIndustryResearchReport('');
})
function ratingChangeName(ratingChange){
if(ratingChange===0){
return '调高'
}else if(ratingChange===1){
return '调低'
}else if(ratingChange===2){
return '首次'
}else if(ratingChange===3){
return '维持'
}else if (ratingChange===4){
return '无变化'
}else{
return ''
}
}
function openWin(code) {
BrowserOpenURL("https://pdf.dfcfw.com/pdf/H3_"+code+"_1.pdf?1749744888000.pdf")
}
function EMDictCodeList(keyVal){
if (keyVal){
EMDictCode('016').then(result => {
console.log(result)
options.value=result.filter((value,index,array) => value.bkName.includes(keyVal)||value.firstLetter.includes(keyVal)||value.bkCode.includes(keyVal)).map(item => {
return {
label: item.bkName+" - "+item.bkCode,
value: item.bkCode
}
})
})
}else{
getIndustryResearchReport('')
}
}
function handleSearch(value) {
getIndustryResearchReport(value)
}
</script>
<template>
<n-card>
<n-auto-complete :options="options" placeholder="请输入行业名称关键词搜索" clearable filterable :on-select="handleSearch" :on-update:value="EMDictCodeList" />
</n-card>
<n-table striped size="small">
<n-thead>
<n-tr>
<!-- <n-th>代码</n-th>-->
<!-- <n-th>名称</n-th>-->
<n-th>行业</n-th>
<n-th>标题</n-th>
<n-th>东财评级</n-th>
<n-th>评级变动</n-th>
<n-th>机构评级</n-th>
<n-th>分析师</n-th>
<n-th>机构</n-th>
<n-th> <n-flex justify="space-between">日期<n-icon @click="getIndustryResearchReport" color="#409EFF" :size="20" :component="RefreshCircleSharp"/></n-flex></n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in list" :key="item.infoCode">
<!-- <n-td>{{item.stockCode}}</n-td>-->
<!-- <n-td :title="item.stockCode">-->
<!-- <n-popover trigger="hover" placement="right">-->
<!-- <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>-->
<!-- </n-popover>-->
<!-- </n-td>-->
<n-td><n-tag type="info" :bordered="false">{{item.industryName}}</n-tag></n-td>
<n-td>
<n-a type="info" @click="openWin(item.infoCode)"><n-text type="success">{{item.title}}</n-text></n-a>
</n-td>
<n-td><n-text :type="item.emRatingName==='增持'?'error':'info'">
{{item.emRatingName}}
</n-text></n-td>
<n-td><n-text :type="item.ratingChange===0?'error':'info'">{{ratingChangeName(item.ratingChange)}}</n-text></n-td>
<n-td>{{item.sRatingName }}</n-td>
<n-td><n-ellipsis style="max-width: 120px">{{item.researcher}}</n-ellipsis></n-td>
<n-td>{{item.orgSName}}</n-td>
<n-td>{{item.publishDate.substring(0,10)}}</n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,108 @@
<script setup lang="ts">
import {nextTick, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
import {InvestCalendarTimeLine} from "../../wailsjs/go/main/App";
import { addMonths, format ,parse} from 'date-fns';
import { zhCN } from 'date-fns/locale';
import {useMessage} from 'naive-ui'
import {Star48Filled} from "@vicons/fluent";
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0'); // 0+1
const day = String(today.getDate()).padStart(2, '0');
// YYYY-MM-DD
const formattedDate = `${year}-${month}-${day}`;
const formattedYM = `${year}-${month}`;
const list = ref([])
const message=useMessage()
function goBackToday() {
setTimeout(() => {
nextTick(
() => {
const elementById = document.getElementById(formattedDate);
if (elementById) {
elementById.scrollIntoView({
behavior: 'auto',
block: 'start'
})
}
}
)
}, 500)
}
onBeforeMount(() => {
InvestCalendarTimeLine(formattedYM).then(res => {
list.value = res
goBackToday();
})
})
onMounted(()=>{
})
function loadMore(){
if (list.value.length>0){
let day=parse(list.value[list.value.length-1].date, 'yyyy-MM-dd', new Date())
let nextMonth=addMonths(day,1)
let ym = format(nextMonth, 'yyyy-MM');
console.log(ym)
InvestCalendarTimeLine(ym).then(res => {
if (res.length==0){
message.warning("没有更多数据了")
return
}
list.value.push( ...res)
})
}
}
function getweekday(date){
let day=parse(date, 'yyyy-MM-dd', new Date())
return format(day, 'EEEE', {locale: zhCN})
}
</script>
<template>
<!-- <n-timeline size="large" style="text-align: left">-->
<!-- <n-timeline-item v-for="item in list" :key="item.date" :title="item.date" type="info" >-->
<!-- <n-list>-->
<!-- <n-list-item v-for="l in item.list" :key="l.article_id ">-->
<!-- <n-text>{{l.title}}</n-text>-->
<!-- </n-list-item>-->
<!-- </n-list>-->
<!-- </n-timeline-item>-->
<!-- </n-timeline>-->
<n-list bordered style="max-height: calc(100vh - 230px);text-align: left;">
<n-scrollbar style="max-height: calc(100vh - 230px);" >
<n-list-item v-for="(item, index) in list" :id="item.date" :key="item.date">
<n-thing :title="item.date+' '+getweekday(item.date)">
<n-list :bordered="false" hoverable>
<n-list-item v-for="(l,i ) in item.list" :key="l.article_id ">
<n-flex justify="space-between">
<n-text :type="item.date===formattedDate?'warning':'info'">{{i+1}}# {{l.title}}</n-text>
<n-rate v-if="l.like_count>0" readonly :default-value="l.like_count" :count="l.like_count" >
<n-icon :component="Star48Filled"/>
</n-rate>
</n-flex>
</n-list-item>
</n-list>
</n-thing>
</n-list-item>
<n-list-item v-if="list.length==0">
<n-text type="info">没有数据</n-text>
</n-list-item>
<n-list-item v-else style="text-align: center;">
<n-button-group>
<n-button strong secondary type="info" @click="loadMore">加载更多</n-button>
<n-button strong secondary type="warning" @click="goBackToday">回到今天</n-button>
</n-button-group>
</n-list-item>
</n-scrollbar>
</n-list>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,386 @@
<script setup>
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({
code: {
type: String,
default: ''
},
name: {
type: String,
default: ''
},
kDays: {
type: Number,
default: 14
},
chartHeight: {
type: Number,
default: 500
},
darkTheme: {
type: Boolean,
default: false
}
})
const upColor = '#ec0000';
const upBorderColor = '';
const downColor = '#00da3c';
const downBorderColor = '';
const kLineChartRef = ref(null);
onMounted(() => {
handleKLine(code,name)
})
function handleKLine(code,name){
GetStockKLine(code,name,365).then(result => {
//console.log("GetStockKLine",result)
const chart = echarts.init(kLineChartRef.value);
const categoryData = [];
const values = [];
const volumns=[];
for (let i = 0; i < result.length; i++) {
let resultElement=result[i]
//console.log("resultElement:{}",resultElement)
categoryData.push(resultElement.day)
let flag=resultElement.close>resultElement.open?1:-1
values.push([
resultElement.open,
resultElement.close,
resultElement.low,
resultElement.high
])
volumns.push([i,resultElement.volume/10000,flag])
}
////console.log("categoryData",categoryData)
////console.log("values",values)
let option = {
title: {
text: name+" "+code,
left: '20px',
textStyle: {
color: darkTheme?'#ccc':'#456'
}
},
darkMode: darkTheme,
//backgroundColor: '#1c1c1c',
// color:['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'],
animation: false,
legend: {
right: 20,
top: 0,
data: ['日K', 'MA5', 'MA10', 'MA20', 'MA30'],
textStyle: {
color: darkTheme?'#ccc':'#456'
},
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
lineStyle: {
color: '#376df4',
width: 2,
opacity: 1
}
},
borderWidth: 2,
borderColor: darkTheme?'#456':'#ccc',
backgroundColor: darkTheme?'#456':'#fff',
padding: 10,
textStyle: {
color: darkTheme?'#ccc':'#456'
},
formatter: function (params) {//
//console.log("params",params)
let currentItemData = _.filter(params, (param) => param.seriesIndex === 0)[0].data;
let ma5=_.filter(params, (param) => param.seriesIndex === 1)[0].data;//ma5
let ma10=_.filter(params, (param) => param.seriesIndex === 2)[0].data;//ma10
let ma20=_.filter(params, (param) => param.seriesIndex === 3)[0].data;//ma20
let ma30=_.filter(params, (param) => param.seriesIndex === 4)[0].data;//ma30
let volum=_.filter(params, (param) => param.seriesIndex === 5)[0].data;
return _.filter(params, (param) => param.seriesIndex === 0)[0].name + '<br>' +
'开盘:' + currentItemData[1] + '<br>' +
'收盘:' + currentItemData[2] + '<br>' +
'最低:' + currentItemData[3] + '<br>' +
'最高:' + currentItemData[4] + '<br>' +
'成交量(万手):' + volum[1] + '<br>' +
'MA5日均线:' + ma5 + '<br>' +
'MA10日均线:' + ma10 + '<br>' +
'MA20日均线:' + ma20 + '<br>' +
'MA30日均线:' + ma30
}
},
axisPointer: {
link: [
{
xAxisIndex: 'all'
}
],
label: {
backgroundColor: '#888'
}
},
visualMap: {
show: false,
seriesIndex: 5,
dimension: 2,
pieces: [
{
value: -1,
color: downColor
},
{
value: 1,
color: upColor
}
]
},
grid: [
{
left: '8%',
right: '8%',
height: '50%',
},
{
left: '8%',
right: '8%',
top: '66%',
height: '15%'
}
],
xAxis: [
{
type: 'category',
data: categoryData,
boundaryGap: false,
axisLine: { onZero: false },
splitLine: { show: false },
min: 'dataMin',
max: 'dataMax',
axisPointer: {
z: 100
}
},
{
type: 'category',
gridIndex: 1,
data: categoryData,
boundaryGap: false,
axisLine: { onZero: false },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false },
min: 'dataMin',
max: 'dataMax'
}
],
yAxis: [
{
scale: true,
splitArea: {
show: true
}
},
{
scale: true,
gridIndex: 1,
splitNumber: 2,
axisLabel: { show: false },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false }
}
],
dataZoom: [
{
type: 'inside',
xAxisIndex: [0, 1],
start: 100-kDays,
end: 100
},
{
show: true,
xAxisIndex: [0, 1],
type: 'slider',
top: '85%',
start: 100-kDays,
end: 100
}
],
series: [
{
name: '日K',
type: 'candlestick',
data: values,
itemStyle: {
color: upColor,
color0: downColor,
// borderColor: upBorderColor,
// borderColor0: downBorderColor
},
markPoint: {
//symbol: 'none',
label: {
formatter: function (param) {
return param != null ? param.value + '' : '';
}
},
data: [
{
name: '最高',
type: 'max',
valueDim: 'highest'
},
{
name: '最低',
type: 'min',
valueDim: 'lowest'
},
{
name: '平均收盘价',
type: 'average',
valueDim: 'close'
}
],
tooltip: {
formatter: function (param) {
return param.name + '<br>' + (param.data.coord || '');
}
}
},
markLine: {
symbol: ['none', 'none'],
data: [
[
{
name: 'from lowest to highest',
type: 'min',
valueDim: 'lowest',
symbol: 'circle',
symbolSize: 10,
label: {
show: false
},
emphasis: {
label: {
show: false
}
}
},
{
type: 'max',
valueDim: 'highest',
symbol: 'circle',
symbolSize: 10,
label: {
show: false
},
emphasis: {
label: {
show: false
}
}
}
],
{
name: 'min line on close',
type: 'min',
valueDim: 'close'
},
{
name: 'max line on close',
type: 'max',
valueDim: 'close'
}
]
}
},
{
name: 'MA5',
type: 'line',
data: calculateMA(5,values),
smooth: true,
showSymbol: false,
lineStyle: {
opacity: 0.6
}
},
{
name: 'MA10',
type: 'line',
data: calculateMA(10,values),
smooth: true,
showSymbol: false,
lineStyle: {
opacity: 0.6
}
},
{
name: 'MA20',
type: 'line',
data: calculateMA(20,values),
smooth: true,
showSymbol: false,
lineStyle: {
opacity: 0.6
}
},
{
name: 'MA30',
type: 'line',
data: calculateMA(30,values),
smooth: true,
showSymbol: false,
lineStyle: {
opacity: 0.6
}
},
{
name: '成交量(手)',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
itemStyle: {
color: '#7fbe9e'
},
data: volumns
}
]
};
chart.setOption(option);
chart.on('click',{seriesName:'日K'}, function(params) {
//console.log("click:",params);
});
})
}
function calculateMA(dayCount,values) {
var result = [];
for (var i = 0, len = values.length; i < len; i++) {
if (i < dayCount) {
result.push('-');
continue;
}
var sum = 0;
for (var j = 0; j < dayCount; j++) {
sum += +values[i - j][1];
}
result.push((sum / dayCount).toFixed(2));
}
return result;
}
</script>
<template>
<div ref="kLineChartRef" style="width: 100%;height: auto;" :style="{height:chartHeight+'px'}"></div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,229 @@
<script setup lang="ts">
import {onBeforeMount, ref} from 'vue'
import {LongTigerRank} from "../../wailsjs/go/main/App";
import {BrowserOpenURL} from "../../wailsjs/runtime";
import {ArrowDownOutline} from "@vicons/ionicons5";
import _ from "lodash";
import KLineChart from "./KLineChart.vue";
import MoneyTrend from "./moneyTrend.vue";
import {NButton, NText, useMessage} from "naive-ui";
const message = useMessage()
const lhbList= ref([])
const EXPLANATIONs = ref([])
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0'); // 0+1
const day = String(today.getDate()).padStart(2, '0');
// YYYY-MM-DD
const formattedDate = `${year}-${month}-${day}`;
const SearchForm= ref({
dateValue: formattedDate,
EXPLANATION:null,
})
onBeforeMount(() => {
longTiger(formattedDate);
})
function longTiger_old(date) {
if(date) {
SearchForm.value.dateValue = date
}
let loading1=message.loading("正在获取龙虎榜数据...",{
duration: 0,
})
LongTigerRank(date).then(res => {
lhbList.value = res
loading1.destroy()
if (res.length === 0) {
message.info("暂无数据,请切换日期")
}
EXPLANATIONs.value=_.uniqBy(_.map(lhbList.value,function (item){
return {
label: item['EXPLANATION'],
value: item['EXPLANATION'],
}
}),'label');
})
}
function longTiger(date) {
if (date) {
SearchForm.value.dateValue = date;
}
let loading1 = message.loading("正在获取龙虎榜数据...", {
duration: 0,
});
const fetchDate = (currentDate, retryCount = 0) => {
if (retryCount > 7) { // 7
lhbList.value = [];
EXPLANATIONs.value = [];
loading1.destroy();
message.info("暂无历史数据");
return;
}
LongTigerRank(currentDate).then(res => {
if (res.length === 0) {
const previousDate = new Date(currentDate);
previousDate.setDate(previousDate.getDate() - 1);
const year = previousDate.getFullYear();
const month = String(previousDate.getMonth() + 1).padStart(2, '0');
const day = String(previousDate.getDate()).padStart(2, '0');
const prevFormattedDate = `${year}-${month}-${day}`;
message.info(`当前日期 ${currentDate} 暂无数据,尝试查询前一日:${prevFormattedDate}`);
SearchForm.value.dateValue = prevFormattedDate;
fetchDate(prevFormattedDate, retryCount + 1); //
} else {
lhbList.value = res;
loading1.destroy();
EXPLANATIONs.value = _.uniqBy(_.map(lhbList.value, function (item) {
return {
label: item['EXPLANATION'],
value: item['EXPLANATION'],
};
}), 'label');
}
}).catch(err => {
loading1.destroy();
message.error("获取数据失败,请重试");
console.error(err);
});
};
fetchDate(date || formattedDate);
}
function handleEXPLANATION(value, option){
SearchForm.value.EXPLANATION = value
if(value){
LongTigerRank(SearchForm.value.dateValue).then(res => {
lhbList.value=_.filter(res, function(o) { return o['EXPLANATION']===value; });
if (res.length === 0) {
message.info("暂无数据,请切换日期")
}
})
}else{
longTiger(SearchForm.value.dateValue)
}
}
</script>
<template>
<n-form :model="SearchForm" >
<n-grid :cols="24" :x-gap="24">
<n-form-item-gi :span="4" label="日期" path="dateValue" label-placement="left">
<n-date-picker v-model:formatted-value="SearchForm.dateValue"
value-format="yyyy-MM-dd" type="date" :on-update:value="(v,v2)=>longTiger(v2)"/>
</n-form-item-gi>
<n-form-item-gi :span="8" label="上榜原因" path="EXPLANATION" label-placement="left">
<n-select clearable placeholder="上榜原因过滤" v-model:value="SearchForm.EXPLANATION" :options="EXPLANATIONs" :on-update:value="handleEXPLANATION"/>
</n-form-item-gi>
<n-form-item-gi :span="10" label="" label-placement="left">
<n-text type="error">*当天的龙虎榜数据通常在收盘结束后一小时左右更新</n-text>
</n-form-item-gi>
</n-grid>
</n-form>
<n-table :single-line="false" striped>
<n-thead>
<n-tr>
<n-th>代码</n-th>
<!-- <n-th width="90px">日期</n-th>-->
<n-th width="60px">名称</n-th>
<n-th>收盘价</n-th>
<n-th width="60px">涨跌幅</n-th>
<n-th>龙虎榜净买额()</n-th>
<n-th>龙虎榜买入额()</n-th>
<n-th>龙虎榜卖出额()</n-th>
<n-th>龙虎榜成交额()</n-th>
<!-- <n-th>市场总成交额()</n-th>-->
<!-- <n-th>净买额占总成交比</n-th>-->
<!-- <n-th>成交额占总成交比</n-th>-->
<n-th width="60px" data-field="TURNOVERRATE">换手率<n-icon :component="ArrowDownOutline" /></n-th>
<n-th>流通市值(亿)</n-th>
<n-th>上榜原因</n-th>
<!-- <n-th>解读</n-th>-->
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="(item, index) in lhbList" :key="index">
<n-td>
<n-tag :bordered=false type="info">{{ item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0] }}</n-tag>
</n-td>
<!-- <n-td>
{{item.TRADE_DATE.substring(0,10)}}
</n-td>-->
<n-td>
<!-- <n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ item.SECURITY_NAME_ABBR }}</n-text>-->
<n-popover trigger="hover" placement="right">
<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>
</n-popover>
</n-td>
<n-td>
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ item.CLOSE_PRICE }}</n-text>
</n-td>
<n-td>
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ (item.CHANGE_RATE).toFixed(2) }}%</n-text>
</n-td>
<n-td>
<!-- <n-text :type="item.BILLBOARD_NET_AMT>0?'error':'success'">{{ (item.BILLBOARD_NET_AMT/10000).toFixed(2) }}</n-text>-->
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-button tag="a" text :type="item.BILLBOARD_NET_AMT>0?'error':'success'" :bordered=false >{{ (item.BILLBOARD_NET_AMT/10000).toFixed(2) }}</n-button>
</template>
<money-trend :code="item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0]" :name="item.SECURITY_NAME_ABBR" :days="360" :dark-theme="true" :chart-height="500" style="width: 800px"></money-trend>
</n-popover>
</n-td>
<n-td>
<n-text :type="'error'">{{ (item.BILLBOARD_BUY_AMT/10000).toFixed(2) }}</n-text>
</n-td>
<n-td>
<n-text :type="'success'">{{ (item.BILLBOARD_SELL_AMT/10000).toFixed(2) }}</n-text>
</n-td>
<n-td>
<n-text :type="'info'">{{ (item.BILLBOARD_DEAL_AMT/10000).toFixed(2) }}</n-text>
</n-td>
<!-- <n-td>-->
<!-- <n-text :type="'info'">{{ (item.ACCUM_AMOUNT/10000).toFixed(2) }}</n-text>-->
<!-- </n-td>-->
<!-- <n-td>-->
<!-- <n-text :type="item.DEAL_NET_RATIO>0?'error':'success'">{{ (item.DEAL_NET_RATIO).toFixed(2) }}%</n-text>-->
<!-- </n-td>-->
<!-- <n-td>-->
<!-- <n-text :type="'info'">{{ (item.DEAL_AMOUNT_RATIO).toFixed(2) }}%</n-text>-->
<!-- </n-td>-->
<n-td>
<n-text :type="'info'">{{ (item.TURNOVERRATE).toFixed(2) }}%</n-text>
</n-td>
<n-td>
<n-text :type="'info'">{{ (item.FREE_MARKET_CAP/100000000).toFixed(2) }}</n-text>
</n-td>
<n-td>
<n-text :type="'info'">{{ item.EXPLANATION }}</n-text>
</n-td>
<!-- <n-td>
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ item.EXPLAIN }}</n-text>
</n-td>-->
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,126 @@
<script setup lang="ts">
import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
import {SearchStock} from "../../wailsjs/go/main/App";
import {useMessage, NText, NTag} from 'naive-ui'
const message = useMessage()
const search = ref('科技股;换手率连续3日大于2')
const columns = ref([])
const dataList = ref([])
function Search() {
const loading = message.loading("正在获取选股数据...", {duration: 0});
SearchStock(search.value).then(res => {
loading.destroy()
//console.log(res)
if(res.code==100){
message.success(res.msg)
columns.value=res.data.result.columns.filter(item=>!item.hiddenNeed&&(item.title!="市场码"&&item.title!="市场简称")).map(item=>{
if(item.children){
return {
title:item.title+(item.unit?'['+item.unit+']':''),
key:item.key,
resizable: true,
minWidth:200,
ellipsis: {
tooltip: true
},
children:item.children.filter(item=>!item.hiddenNeed).map(item=>{
return {
title:item.dateMsg,
key:item.key,
minWidth:100,
resizable: true,
ellipsis: {
tooltip: true
}
}
})
}
}else{
return {
title:item.title+(item.unit?'['+item.unit+']':''),
key:item.key,
resizable: true,
minWidth:100,
ellipsis: {
tooltip: true
}
}
}
})
dataList.value=res.data.result.dataList
}else {
message.error(res.msg)
}
}).catch(err => {
message.error(err)
})
}
function isNumeric(value) {
return !isNaN(parseFloat(value)) && isFinite(value);
}
onBeforeMount(() => {
Search()
})
</script>
<template>
<n-flex>
<n-input-group>
<n-input v-model:value="search" placeholder="请输入选股指标或者要求" />
<n-button type="success" @click="Search">搜索A股</n-button>
</n-input-group>
</n-flex>
<!-- <n-table striped size="small">-->
<!-- <n-thead>-->
<!-- <n-tr>-->
<!-- <n-th v-for="item in columns">{{item.title}}</n-th>-->
<!-- </n-tr>-->
<!-- </n-thead>-->
<!-- <n-tbody>-->
<!-- <n-tr v-for="(item,index) in dataList">-->
<!-- <n-td v-for="d in columns">{{item[d.key]}}</n-td>-->
<!-- </n-tr>-->
<!-- </n-tbody>-->
<!-- </n-table>-->
<n-data-table
:max-height="'calc(100vh - 285px)'"
size="small"
:columns="columns"
:data="dataList"
:pagination="false"
:scroll-x="1800"
:render-cell="(value, rowData, column) => {
if(column.key=='SECURITY_CODE'||column.key=='SERIAL'){
return h(NText, { type: 'info',border: false }, { default: () => `${value}` })
}
if (isNumeric(value)) {
let type='info';
if (Number(value)<0){
type='success';
}
if(Number(value)>=0&&Number(value)<=5){
type='warning';
}
if (Number(value)>5){
type='error';
}
return h(NText, { type: type }, { default: () => `${value}` })
}else{
if(column.key=='SECURITY_SHORT_NAME'){
return h(NTag, { type: 'info',bordered: false }, { default: () => `${value}` })
}else{
return h(NText, { type: 'info' }, { default: () => `${value}` })
}
}
}"
/>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,148 @@
<script setup lang="ts">
import {onBeforeMount, ref} from 'vue'
import {GetStockList, StockNotice} from "../../wailsjs/go/main/App";
import {BrowserOpenURL} from "../../wailsjs/runtime";
import {RefreshCircleSharp} from "@vicons/ionicons5";
import _ from "lodash";
import KLineChart from "./KLineChart.vue";
import MoneyTrend from "./moneyTrend.vue";
import {useMessage} from "naive-ui";
const {stockCode}=defineProps(
{
stockCode: {
type: String,
default: ''
}
}
)
const list = ref([])
const options = ref([])
const message=useMessage()
function getNotice(stockCodes) {
StockNotice(stockCodes).then(result => {
console.log(result)
list.value = result
})
}
onBeforeMount (()=>{
//message.info(""+stockCode)
getNotice(stockCode);
})
function findStockList(query){
if (query){
GetStockList(query).then(result => {
options.value=result.map(item => {
return {
label: item.name+" - "+item.ts_code,
value: item.ts_code
}
})
})
}else{
getNotice("")
}
}
function handleSearch(value) {
getNotice(value)
}
function openWin(code) {
BrowserOpenURL("https://pdf.dfcfw.com/pdf/H2_"+code+"_1.pdf?1750092081000.pdf")
}
function getTypeColor(name){
if(name.includes("质押")||name.includes("冻结")||name.includes("解冻")||name.includes("解押")||name.includes("解禁")){
return "error"
}
if(name.includes("异常")||name.includes("减持")||name.includes("增发")||name.includes("重大")){
return "error"
}
if(name.includes("季度报告")||name.includes("年度报告")||name.includes("澄清公告")||name.includes("风险")){
return "error"
}
if(name.includes("终止")||name.includes("复牌")||name.includes("停牌")||name.includes("退市")){
return "error"
}
if(name.includes("破产")||name.includes("清算")){
return "error"
}
if(name.includes("回购")||name.includes("重组")||name.includes("诉讼")||name.includes("仲裁")||name.includes("转让")||name.includes("收购")){
return "warning"
}
if(name.includes("调研")||name.includes("募集")){
return "warning"
}
return "info"
}
function getmMarketCode(market,code) {
if(market==="0"){
return "sz"+code
}else if(market==="1"){
return "sh"+code
}else if(market==="2"){
return "bj"+code
}else if(market==="3"){
return "hk"+code
}else{
return code
}
}
</script>
<template>
<n-card>
<n-auto-complete :options="options" placeholder="请输入A股名称或者代码" clearable filterable :on-select="handleSearch" :on-update:value="findStockList" />
</n-card>
<n-table striped size="small">
<n-thead>
<n-tr>
<n-th>股票代码</n-th>
<n-th>股票名称</n-th>
<n-th>公告标题</n-th>
<n-th>公告类型</n-th>
<n-th>公告日期</n-th>
<n-th><n-flex>数据更新时间<n-icon @click="getNotice('')" color="#409EFF" :size="20" :component="RefreshCircleSharp"/></n-flex></n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in list" :key="item.art_code">
<n-td>
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-tag type="info" :bordered="false">{{item.codes[0].stock_code }}</n-tag>
</template>
<money-trend style="width: 800px" :code="getmMarketCode(item.codes[0].market_code,item.codes[0].stock_code)" :name="item.codes[0].short_name" :days="360" :dark-theme="true" :chart-height="500"></money-trend>
</n-popover>
</n-td>
<n-td>
<n-popover trigger="hover" placement="right">
<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>
</n-popover>
</n-td>
<n-td>
<n-a type="info" @click="openWin(item.art_code)"><n-text :type="getTypeColor(item.columns[0].column_name)"> {{item.title}}</n-text></n-a>
</n-td>
<n-td>
<n-text :type="getTypeColor(item.columns[0].column_name)">{{item.columns[0].column_name }}</n-text>
</n-td>
<n-td>
<n-tag type="info">{{item.notice_date.substring(0,10) }}</n-tag>
</n-td>
<n-td>
<n-tag type="info">{{item.display_time.substring(0,19)}}</n-tag>
</n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,136 @@
<script setup>
import {onBeforeMount, ref} from 'vue'
import {GetStockList, StockResearchReport} from "../../wailsjs/go/main/App";
import {ArrowDownOutline, CaretDown, CaretUp, PulseOutline, Refresh, RefreshCircleSharp,} from "@vicons/ionicons5";
import KLineChart from "./KLineChart.vue";
import MoneyTrend from "./moneyTrend.vue";
import {useMessage} from "naive-ui";
import {BrowserOpenURL} from "../../wailsjs/runtime";
const {stockCode}=defineProps(
{
stockCode: {
type: String,
default: ''
}
}
)
const message=useMessage()
const list = ref([])
const options = ref([])
function getStockResearchReport(value) {
StockResearchReport(value).then(result => {
//console.log(result)
list.value = result
})
}
onBeforeMount(()=>{
getStockResearchReport(stockCode);
})
function ratingChangeName(ratingChange){
if(ratingChange===0){
return '调高'
}else if(ratingChange===1){
return '调低'
}else if(ratingChange===2){
return '首次'
}else if(ratingChange===3){
return '维持'
}else if (ratingChange===4){
return '无变化'
}else{
return ''
}
}
function getmMarketCode(market,code) {
if(market==="SHENZHEN"){
return "sz"+code
}else if(market==="SHANGHAI"){
return "sh"+code
}else if(market==="BEIJING"){
return "bj"+code
}else if(market==="HONGKONG"){
return "hk"+code
}else{
return code
}
}
function openWin(code) {
BrowserOpenURL("https://pdf.dfcfw.com/pdf/H3_"+code+"_1.pdf?1749744888000.pdf")
}
function findStockList(query){
if (query){
GetStockList(query).then(result => {
options.value=result.map(item => {
return {
label: item.name+" - "+item.ts_code,
value: item.ts_code
}
})
})
}else{
getStockResearchReport('')
}
}
function handleSearch(value) {
getStockResearchReport(value)
}
</script>
<template>
<n-card>
<n-auto-complete :options="options" placeholder="请输入A股名称或者代码" clearable filterable :on-select="handleSearch" :on-update:value="findStockList" />
</n-card>
<n-table striped size="small">
<n-thead>
<n-tr>
<!-- <n-th>代码</n-th>-->
<n-th>名称</n-th>
<n-th>行业</n-th>
<n-th>标题</n-th>
<n-th>东财评级</n-th>
<n-th>评级变动</n-th>
<n-th>机构评级</n-th>
<n-th>分析师</n-th>
<n-th>机构</n-th>
<n-th> <n-flex justify="space-between">日期<n-icon @click="getStockResearchReport" color="#409EFF" :size="20" :component="RefreshCircleSharp"/></n-flex></n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in list" :key="item.infoCode">
<!-- <n-td>{{item.stockCode}}</n-td>-->
<n-td :title="item.stockCode">
<n-popover trigger="hover" placement="right">
<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>
</n-popover>
</n-td>
<n-td><n-tag type="info" :bordered="false">{{item.indvInduName}}</n-tag></n-td>
<n-td>
<n-a type="info" @click="openWin(item.infoCode)">{{item.title}}</n-a>
</n-td>
<n-td><n-text :type="item.emRatingName==='增持'?'error':'info'">
{{item.emRatingName}}
</n-text></n-td>
<n-td><n-text :type="item.ratingChange===0?'error':'info'">{{ratingChangeName(item.ratingChange)}}</n-text></n-td>
<n-td>{{item.sRatingName}}</n-td>
<n-td>{{item.researcher}}</n-td>
<n-td>{{item.orgSName}}</n-td>
<n-td>{{item.publishDate.substring(0,10)}}</n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,220 @@
<script setup>
// import { MdPreview } from 'md-editor-v3';
// preview.cssstyle.css
import 'md-editor-v3/lib/preview.css';
import {h, onBeforeUnmount, onMounted, ref} from 'vue';
import {CheckUpdate, GetVersionInfo} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
import {NAvatar, NButton, useNotification} from "naive-ui";
const updateLog = ref('');
const versionInfo = ref('');
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
const alipay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/alipay.jpg')
const wxpay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/wxpay.jpg')
const notify = useNotification()
onMounted(() => {
document.title = '关于软件';
GetVersionInfo().then((res) => {
updateLog.value = res.content;
versionInfo.value = res.version;
icon.value = res.icon;
alipay.value=res.alipay;
wxpay.value=res.wxpay;
});
})
onBeforeUnmount(() => {
notify.destroyAll()
EventsOff("updateVersion")
})
EventsOn("updateVersion",async (msg) => {
const githubTimeStr = msg.published_at;
// Date
const utcDate = new Date(githubTimeStr);
//
const date = new Date(utcDate.getTime());
const year = date.getFullYear();
// getMonth 0 - 11 1
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');
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
//console.log("GitHub UTC :", utcDate);
//console.log(":", formattedDate);
notify.info({
avatar: () =>
h(NAvatar, {
size: 'small',
round: false,
src: icon.value
}),
title: '发现新版本: ' + msg.tag_name,
content: () => {
//return h(MdPreview, {theme:'dark',modelValue:msg.commit?.message}, null)
return h('div', {
style: {
'text-align': 'left',
'font-size': '14px',
}
}, { default: () => msg.commit?.message })
},
duration: 5000,
meta: "发布时间:"+formattedDate,
action: () => {
return h(NButton, {
type: 'primary',
size: 'small',
onClick: () => {
window.open(msg.html_url)
}
}, { default: () => '查看' })
}
})
})
</script>
<template>
<n-space vertical size="large">
<!-- 软件描述 -->
<n-card size="large">
<n-divider title-placement="center">关于软件</n-divider>
<n-space vertical >
<n-image width="100" :src="icon" />
<h1>
<n-badge :value="versionInfo" :offset="[50,10]" type="success">
<n-gradient-text type="info" :size="50" >go-stock</n-gradient-text>
</n-badge>
</h1>
<n-button size="tiny" @click="CheckUpdate" type="info" tertiary >检查更新</n-button>
<div style="justify-self: center;text-align: left" >
<p>自选股行情实时监控基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
<p>目前已支持A股港股美股未来计划加入基金ETF等支持</p>
<p>支持DeepSeekOpenAI OllamaLMStudioAnythingLLM<a href="https://cloud.siliconflow.cn/i/foufCerk" target="_blank">硅基流动</a><a href="https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ" target="_blank">火山方舟</a>阿里云百炼等平台或模型</p>
<p>
<i style="color: crimson">本软件仅供学习研究目的AI分析结果仅供参考本软件不提供任何投资建议或决策风险自担</i>
</p>
<p>
欢迎点赞GitHub<a href="https://github.com/ArvinLovegood/go-stock" target="_blank">go-stock</a><n-divider vertical />
<a href="https://github.com/ArvinLovegood/go-stock" target="_blank">GitHub</a><n-divider vertical />
<a href="https://github.com/ArvinLovegood/go-stock/issues" target="_blank">Issues</a><n-divider vertical />
<a href="https://github.com/ArvinLovegood/go-stock/releases" target="_blank">Releases</a><n-divider vertical />
</p>
<p v-if="updateLog">更新说明{{updateLog}}</p>
<p>项目社区<a href="https://go-stock.sparkmemory.top/" target="_blank">https://go-stock.sparkmemory.top/</a></p>
<p>QQ交流群<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333" target="_blank">491605333</a></p>
</div>
</n-space>
<n-divider title-placement="center">关于作者</n-divider>
<n-space vertical>
<!-- <h1>关于作者</h1>-->
<n-avatar width="100" src="https://avatars.githubusercontent.com/u/7401917?v=4" />
<h2><a href="https://github.com/ArvinLovegood" target="_blank">@ArvinLovegood</a></h2>
<p>一个热爱编程的小白欢迎关注我的Github</p>
<n-image width="300" src="https://go-stock.sparkmemory.top/assets/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88-DEJtWc_y.png" />
<p>开源不易如果觉得好用可以请作者喝杯咖啡</p>
<n-flex justify="center">
<n-image width="200" :src="alipay" />
<n-image width="200" :src="wxpay" />
</n-flex>
</n-space>
<n-divider title-placement="center">鸣谢</n-divider>
<div style="justify-self: center;text-align: left" >
<p>
感谢以下捐赠者
<n-gradient-text size="small" type="warning">*</n-gradient-text><n-divider vertical />
</p>
<p>
感谢以下开发者
<a href="https://github.com/CodeNoobLH" target="_blank">浓睡不消残酒</a><n-divider vertical />
<a href="https://github.com/gnim2600" target="_blank">@gnim2600</a><n-divider vertical />
<a href="https://github.com/XXXiaohuayanGGG" target="_blank">@XXXiaohuayanGGG</a><n-divider vertical />
<a href="https://github.com/2lovecode" target="_blank">@2lovecode</a><n-divider vertical />
<a href="https://github.com/JerryLookupU" target="_blank">@JerryLookupU</a><n-divider vertical />
</p>
<p>
感谢以下开源项目
<a href="https://github.com/wailsapp/wails" target="_blank">Wails</a><n-divider vertical />
<a href="https://github.com/vuejs" target="_blank">Vue</a><n-divider vertical />
<a href="https://github.com/tusen-ai/naive-ui" target="_blank">NaiveUI</a><n-divider vertical />
</p>
</div>
<n-divider title-placement="center">关于版权和技术支持申明</n-divider>
<div style="justify-self: center;text-align: left" >
<p style="color: #FAA04A">如有问题请先查看项目文档如果问题依然存在请优先加群491605333咨询</p>
<p>
如需软件商业授权或定制开发请联系作者微信(备注 商业咨询)ArvinLovegood
</p>
<n-divider/>
<p>
本软件基于开源技术构建使用WailsNaiveUIVue等开源项目技术上如有问题可以先向对应的开源社区请求帮助
</p>
<p>
开源不易本人精力和时间有限如确实需要一对一技术支持<i style="color: crimson">请先赞助</i>联系微信(备注 技术支持)ArvinLovegood
</p>
<p style="color: #FAA04A">*加微信或者QQ时请先备注或留言需求(<a href="#support">技术支持</a>功能建议商业咨询等否则会被忽略)</p>
<n-table id="support">
<n-thead>
<n-tr>
<n-th>技术支持方式</n-th><n-th>赞助()</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr>
<n-td>
QQ506808970微信ArvinLovegood
</n-td>
<n-td>
100/
</n-td>
</n-tr>
<n-tr>
<n-td>
长期技术支持不限次数新功能优先体验等
</n-td>
<n-td>
5000
</n-td>
</n-tr>
</n-tbody>
</n-table>
</div>
</n-card>
</n-space>
</template>
<style scoped>
/* 可以在这里添加一些样式 */
h1, h2 {
margin: 0;
padding: 6px 0;
}
p {
margin: 2px 0;
}
ul {
list-style-type: disc;
padding-left: 20px;
}
a {
color: #18a058;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>

View File

@ -0,0 +1,278 @@
<script setup>
import {h, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from "vue";
import {Add, ChatboxOutline} from "@vicons/ionicons5";
import {NButton, NEllipsis, NText, useMessage} from "naive-ui";
import {
FollowFund,
GetConfig,
GetFollowedFund,
GetfundList,
GetVersionInfo,
UnFollowFund
} from "../../wailsjs/go/main/App";
import vueDanmaku from 'vue3-danmaku'
const danmus = ref([])
const ws = ref(null)
const icon = ref(null)
const message = useMessage()
const modalShow = ref(false)
const data = reactive({
modelName:"",
chatId: "",
question:"",
name: "",
code: "",
fenshiURL:"",
kURL:"",
fullscreen: false,
airesult: "",
openAiEnable: false,
loading: true,
enableDanmu: false,
})
const followList=ref([])
const options=ref([])
const ticker=ref({})
onBeforeMount(()=>{
GetConfig().then(result => {
if (result.openAiEnable) {
data.openAiEnable = true
}
if (result.enableDanmu) {
data.enableDanmu = true
}
})
GetFollowedFund().then(result => {
followList.value = result
//console.log("followList",followList.value)
})
})
onMounted(() => {
GetVersionInfo().then((res) => {
icon.value = res.icon;
});
// WebSocket
ws.value = new WebSocket('ws://8.134.249.145:16688/ws'); // WebSocket
//ws.value = new WebSocket('ws://localhost:16688/ws'); // WebSocket
ws.value.onopen = () => {
//console.log('WebSocket ');
};
ws.value.onmessage = (event) => {
if(data.enableDanmu){
danmus.value.push(event.data);
}
};
ws.value.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
ws.value.onclose = () => {
//console.log('WebSocket ');
};
ticker.value=setInterval(() => {
GetFollowedFund().then(result => {
followList.value = result
//console.log("followList",followList.value)
})
}, 1000*60)
})
onBeforeUnmount(() => {
clearInterval(ticker.value)
ws.value.close()
message.destroyAll()
})
function SendDanmu(){
ws.value.send(data.name)
}
function AddFund(){
FollowFund(data.code).then(result=>{
if(result){
message.success("关注成功")
GetFollowedFund().then(result => {
followList.value = result
//console.log("followList",followList.value)
})
}
})
}
function unFollow(code){
UnFollowFund(code).then(result=>{
if(result){
message.success("取消关注成功")
GetFollowedFund().then(result => {
followList.value = result
//console.log("followList",followList.value)
})
}
})
}
function getFundList(value){
GetfundList(value).then(result=>{
options.value=[]
result.forEach(item=>{
options.value.push({
label: item.name+" ["+item.code+"]",
value: item.code,
})
})
})
}
function onSelectFund(value){
data.code=value
blinkBorder(value)
}
function formatterTitle(title){
return () => h(NEllipsis,{
style: {
'font-size': '16px',
'max-width': '180px',
},
},{default: () => title,}
)
}
function search(code,name){
setTimeout(() => {
window.open("https://fund.eastmoney.com/"+code+".html","_blank","noreferrer,width=1000,top=100,left=100,status=no,toolbar=no,location=no,scrollbars=no")
//window.open("https://finance.sina.com.cn/fund/quotes/"+code+"/bc.shtml","_blank","width=1000,height=800,top=100,left=100,toolbar=no,location=no")
}, 500)
}
function newchart(code,name){
modalShow.value=true
data.name=name
data.code=code
data.fenshiURL='https://image.sinajs.cn/newchart/v5/fund/nav/ss/'+code+'.gif'+"?t="+Date.now()
}
function blinkBorder(findId){
//
const element = document.getElementById(findId);
if (element) {
//
element.scrollIntoView({ behavior: 'smooth' });
const pelement = document.getElementById(findId +'_gi');
if(pelement){
//
pelement.classList.add('blink-border');
// 3
setTimeout(() => {
pelement.classList.remove('blink-border');
}, 1000*5);
}else{
console.error(`Element with ID ${findId}_gi not found`);
}
}
}
</script>
<template>
<vue-danmaku v-model:danmus="danmus" useSlot style="height:100px; width:100%;z-index: 9;position:absolute; top: 400px; pointer-events: none;" >
<template v-slot:dm="{ index, danmu }">
<n-gradient-text type="info">
<n-icon :component="ChatboxOutline"/>{{ danmu }}
</n-gradient-text>
</template>
</vue-danmaku>
<n-flex justify="start" >
<n-grid :x-gap="8" :cols="3" :y-gap="8" >
<n-gi :id="info.code+'_gi'" v-for="info in followList" style="margin-left: 2px" >
<n-card :id="info.code" :title="formatterTitle(info.name)">
<template #header-extra>
<n-tag size="small" :bordered="false" type="info">{{info.code}}</n-tag>&nbsp;
<n-tag size="small" :bordered="false" type="success" @click="unFollow(info.code)"> 取消关注</n-tag>
</template>
<n-flex>
<n-text size="small" :type="info.netEstimatedRate>0?'error':'success'" :bordered="false" v-if="info.netEstimatedUnit">
估算净值{{info.netEstimatedUnit}}&nbsp;
{{info.netEstimatedRate}} %&nbsp;&nbsp;&nbsp;
({{info.netEstimatedUnitTime}})</n-text>
<br>
<n-text size="small" :type="info.netEstimatedRate>0?'error':'success'" :bordered="false" v-if="info.netUnitValue">
单位净值{{info.netUnitValue}}&nbsp;&nbsp;&nbsp; ({{info.netUnitValueDate}})</n-text>
</n-flex>
<n-flex justify="start" style="margin-top: 10px">
<n-tag size="small" :type="info.fundBasic.netGrowth1>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth1">近一月{{info.fundBasic.netGrowth1}}%</n-tag>
<n-tag size="small" :type="info.fundBasic.netGrowth3>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth3">近三月{{info.fundBasic.netGrowth3}}%</n-tag>
<n-tag size="small" :type="info.fundBasic.netGrowth6>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth6">近六月{{info.fundBasic.netGrowth6}}%</n-tag>
<n-tag size="small" :type="info.fundBasic.netGrowth12>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth12">近一年{{info.fundBasic.netGrowth12}}%</n-tag>
<n-tag size="small" :type="info.fundBasic.netGrowth36>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth36">近三年{{info.fundBasic.netGrowth36}}%</n-tag>
<n-tag size="small" :type="info.fundBasic.netGrowth60>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowth60">近五年{{info.fundBasic.netGrowth60}}%</n-tag>
<n-tag size="small" :type="info.fundBasic.netGrowthYTD>0?'error':'success'" :bordered="false" v-if="info.fundBasic.netGrowthYTD" >今年来{{info.fundBasic.netGrowthYTD}}%</n-tag>
<n-tag size="small" :type="info.fundBasic.netGrowthAll>0?'error':'success'" :bordered="false" >成立来{{info.fundBasic.netGrowthAll}}%</n-tag>
</n-flex>
<template #footer>
<n-flex justify="space-between">
<n-tag size="small" :bordered="false" type="warning"> {{info.fundBasic.type}}</n-tag>
<n-tag size="small" :bordered="false" type="info"> {{info.fundBasic.company}}{{info.fundBasic.manager}}</n-tag>
</n-flex>
</template>
<template #action>
<n-flex justify="end">
<n-button size="tiny" type="error" @click="newchart(info.code,info.name)"> 走势 </n-button>
<n-button size="tiny" type="warning" @click="search(info.code,info.name)"> 详情 </n-button>
</n-flex>
</template>
</n-card>
</n-gi>
</n-grid>
</n-flex>
<n-modal v-model:show="modalShow" :title="data.name" style="width: 400px" :preset="'card'">
<n-image :src="data.fenshiURL" />
</n-modal>
<div style="position: fixed;bottom: 18px;right:5px;z-index: 10;width: 400px">
<n-input-group >
<n-auto-complete v-model:value="data.name"
:input-props="{
autocomplete: 'disabled',
}"
:options="options"
placeholder="基金名称/代码/弹幕"
clearable @update-value="getFundList" :on-select="onSelectFund"/>
<n-button type="primary" @click="AddFund" >
<n-icon :component="Add"/>
关注
</n-button>
<n-button type="info" @click="SendDanmu" v-if="data.enableDanmu" >
<n-icon :component="ChatboxOutline"/>
发送弹幕
</n-button>
</n-input-group>
</div>
</template>
<style scoped>
/* 添加闪烁效果的CSS类 */
.blink-border {
animation: blink-border 1s linear infinite;
border: 4px solid transparent;
}
@keyframes blink-border {
0% {
border-color: red;
}
50% {
border-color: transparent;
}
100% {
border-color: red;
}
}
</style>

View File

@ -0,0 +1,94 @@
<script setup>
import {CaretDown, CaretUp, RefreshCircleOutline} from "@vicons/ionicons5";
import {NText,useMessage} from "naive-ui";
import {onBeforeUnmount, onMounted, onUnmounted, ref} from "vue";
import {GetIndustryMoneyRankSina} from "../../wailsjs/go/main/App";
import KLineChart from "./KLineChart.vue";
const props = defineProps({
headerTitle: {
type: String,
default: '行业资金排名(净流入)'
},
fenlei: {
type: String,
default: '0'
},
sort: {
type: String,
default: 'netamount'
},
})
const message = useMessage()
const dataList= ref([])
const sort = ref(props.sort)
const fenlei= ref(props.fenlei)
const interval = ref(null)
onMounted(()=>{
sort.value=props.sort
fenlei.value=props.fenlei
GetRankData()
interval.value=setInterval(()=>{
GetRankData()
},1000*60)
})
onBeforeUnmount(()=>{
clearInterval(interval.value)
})
function GetRankData(){
message.loading("正在刷新数据...")
GetIndustryMoneyRankSina(fenlei.value,sort.value).then(result => {
if(result.length>0){
dataList.value = result
//console.log(result)
}
})
}
</script>
<template>
<n-table striped size="small">
<n-thead>
<n-tr>
<n-th>板块名称</n-th>
<n-th>涨跌幅</n-th>
<n-th>流入资金/</n-th>
<n-th>流出资金/</n-th>
<n-th>净流入/<n-icon v-if="sort==='0'" :component="CaretDown"/><n-icon v-if="sort==='1'" :component="CaretUp"/></n-th>
<n-th>净流入率</n-th>
<n-th>领涨股</n-th>
<n-th>涨跌幅</n-th>
<n-th>最新价</n-th>
<n-th>净流入率</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in dataList" :key="item.category">
<n-td><n-tag :bordered=false type="info">{{item.name}}</n-tag></n-td>
<n-td> <n-text :type="item.avg_changeratio>0?'error':'success'">{{(item.avg_changeratio*100).toFixed(2)}}%</n-text></n-td>
<n-td><n-text type="info">{{(item.inamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text type="info">{{(item.outamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text :type="item.netamount>0?'error':'success'">{{(item.netamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text :type="item.ratioamount>0?'error':'success'">{{(item.ratioamount*100).toFixed(2)}}%</n-text></n-td>
<n-td>
<!-- <n-text type="info">{{item.ts_name}}</n-text>-->
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-button tag="a" text :type="item.ts_changeratio>0?'error':'success'" :bordered=false >{{ item.ts_name }}</n-button>
</template>
<k-line-chart style="width: 800px" :code="item.ts_symbol" :chart-height="500" :name="item.ts_name" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-td>
<n-td><n-text :type="item.ts_changeratio>0?'error':'success'">{{(item.ts_changeratio*100).toFixed(2)}}%</n-text></n-td>
<n-td><n-text type="info">{{item.ts_trade}}</n-text></n-td>
<n-td><n-text :type="item.ts_ratioamount>0?'error':'success'">{{(item.ts_ratioamount*100).toFixed(2)}}%</n-text></n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,655 @@
<script setup>
import {computed, h, onBeforeMount, onBeforeUnmount, ref} from 'vue'
import {
GetAIResponseResult,
GetConfig,
GetIndustryRank,
GetPromptTemplates,
GetTelegraphList,
GlobalStockIndexes,
ReFleshTelegraphList,
SaveAIResponseResult,
SaveAsMarkdown,
ShareAnalysis,
SummaryStockNews
} from "../../wailsjs/go/main/App";
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
import NewsList from "./newsList.vue";
import KLineChart from "./KLineChart.vue";
import { CaretDown, CaretUp, PulseOutline,} from "@vicons/ionicons5";
import {NAvatar, NButton, NFlex, NText, useMessage, useNotification} from "naive-ui";
import {MdPreview} from "md-editor-v3";
import {useRoute} from 'vue-router'
import RankTable from "./rankTable.vue";
import IndustryMoneyRank from "./industryMoneyRank.vue";
import StockResearchReportList from "./StockResearchReportList.vue";
import StockNoticeList from "./StockNoticeList.vue";
import LongTigerRankList from "./LongTigerRankList.vue";
import IndustryResearchReportList from "./IndustryResearchReportList.vue";
import HotStockList from "./HotStockList.vue";
import HotEvents from "./HotEvents.vue";
import HotTopics from "./HotTopics.vue";
import InvestCalendarTimeLine from "./InvestCalendarTimeLine.vue";
import ClsCalendarTimeLine from "./ClsCalendarTimeLine.vue";
import SelectStock from "./SelectStock.vue";
const route = useRoute()
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
const message = useMessage()
const notify = useNotification()
const panelHeight = ref(window.innerHeight - 240)
const telegraphList = ref([])
const sinaNewsList = ref([])
const common = ref([])
const america = ref([])
const europe = ref([])
const asia = ref([])
const other = ref([])
const globalStockIndexes = ref(null)
const summaryModal = ref(false)
const summaryBTN = ref(true)
const darkTheme = ref(false)
const theme = computed(() => {
return darkTheme ? 'dark' : 'light'
})
const aiSummary = ref(``)
const aiSummaryTime = ref("")
const modelName = ref("")
const chatId = ref("")
const question = ref(``)
const sysPromptId = ref(0)
const loading = ref(true)
const sysPromptOptions = ref([])
const userPromptOptions = ref([])
const promptTemplates = ref([])
const industryRanks = ref([])
const sort = ref("0")
const nowTab = ref("市场快讯")
const indexInterval = ref(null)
const indexIndustryRank = ref(null)
const stockCode= ref('')
function getIndex() {
GlobalStockIndexes().then((res) => {
globalStockIndexes.value = res
common.value = res["common"]
america.value = res["america"]
europe.value = res["europe"]
asia.value = res["asia"]
other.value = res["other"]
})
}
onBeforeMount(() => {
nowTab.value = route.query.name
stockCode.value = route.query.stockCode
GetConfig().then(result => {
summaryBTN.value = result.openAiEnable
darkTheme.value = result.darkTheme
})
GetPromptTemplates("", "").then(res => {
promptTemplates.value = res
sysPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型系统Prompt')
userPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型用户Prompt')
})
GetTelegraphList("财联社电报").then((res) => {
telegraphList.value = res
})
GetTelegraphList("新浪财经").then((res) => {
sinaNewsList.value = res
})
getIndex();
industryRank();
indexInterval.value = setInterval(() => {
getIndex()
}, 3000)
indexIndustryRank.value = setInterval(() => {
industryRank()
}, 1000 * 10)
})
onBeforeUnmount(() => {
EventsOff("changeMarketTab")
EventsOff("newTelegraph")
EventsOff("newSinaNews")
EventsOff("summaryStockNews")
clearInterval(indexInterval.value)
clearInterval(indexIndustryRank.value)
})
EventsOn("changeMarketTab", async (msg) => {
//message.info(msg.name)
updateTab(msg.name)
})
EventsOn("newTelegraph", (data) => {
for (let i = 0; i < data.length; i++) {
telegraphList.value.pop()
}
telegraphList.value.unshift(...data)
})
EventsOn("newSinaNews", (data) => {
for (let i = 0; i < data.length; i++) {
sinaNewsList.value.pop()
}
sinaNewsList.value.unshift(...data)
})
//
window.onresize = () => {
panelHeight.value = window.innerHeight - 240
}
function getAreaName(code) {
switch (code) {
case "america":
return "美洲"
case "europe":
return "欧洲"
case "asia":
return "亚洲"
case "common":
return "常用"
case "other":
return "其他"
}
}
function changeIndustryRankSort() {
if (sort.value === "0") {
sort.value = "1"
} else {
sort.value = "0"
}
industryRank()
}
function industryRank() {
GetIndustryRank(sort.value, 150).then(result => {
if (result.length > 0) {
//console.log(result)
industryRanks.value = result
} else {
message.info("暂无数据")
}
})
}
function reAiSummary() {
aiSummary.value = ""
summaryModal.value = true
loading.value = true
SummaryStockNews(question.value, sysPromptId.value)
}
function getAiSummary() {
summaryModal.value = true
loading.value = true
GetAIResponseResult("市场资讯").then(result => {
if (result.content) {
aiSummary.value = result.content
question.value = result.question
loading.value = false
const date = new Date(result.CreatedAt);
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');
aiSummaryTime.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
modelName.value = result.modelName
} else {
aiSummaryTime.value = ""
aiSummary.value = ""
modelName.value = ""
SummaryStockNews(question.value, sysPromptId.value)
}
})
}
function updateTab(name) {
summaryBTN.value = (name === "市场快讯");
nowTab.value = name
}
EventsOn("summaryStockNews", async (msg) => {
loading.value = false
////console.log(msg)
if (msg === "DONE") {
SaveAIResponseResult("市场资讯", "市场资讯", aiSummary.value, chatId.value, question.value)
message.info("AI分析完成")
message.destroyAll()
} else {
if (msg.chatId) {
chatId.value = msg.chatId
}
if (msg.question) {
question.value = msg.question
}
if (msg.content) {
aiSummary.value = aiSummary.value + msg.content
}
if (msg.extraContent) {
aiSummary.value = aiSummary.value + msg.extraContent
}
if (msg.model) {
modelName.value = msg.model
}
if (msg.time) {
aiSummaryTime.value = msg.time
}
}
})
async function copyToClipboard() {
try {
await navigator.clipboard.writeText(aiSummary.value);
message.success('分析结果已复制到剪切板');
} catch (err) {
message.error('复制失败: ' + err);
}
}
function saveAsMarkdown() {
SaveAsMarkdown('市场资讯', '市场资讯').then(result => {
message.success(result)
})
}
function share() {
ShareAnalysis('市场资讯', '市场资讯').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 ReFlesh(source) {
//console.log("ReFlesh:", source)
ReFleshTelegraphList(source).then(res => {
if (source === "财联社电报") {
telegraphList.value = res
}
if (source === "新浪财经") {
sinaNewsList.value = res
}
})
}
</script>
<template>
<n-card>
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab">
<n-tab-pane name="市场快讯" tab="市场快讯">
<n-grid :cols="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-grid>
</n-tab-pane>
<n-tab-pane name="全球股指" tab="全球股指">
<n-tabs type="segment" animated>
<n-tab-pane name="全球指数" tab="全球指数">
<n-grid :cols="5" :y-gap="0">
<n-gi v-for="(val, key) in globalStockIndexes" :key="key">
<n-list bordered>
<template #header>
{{ getAreaName(key) }}
</template>
<n-list-item v-for="item in val" :key="item.code">
<n-grid :cols="3" :y-gap="0">
<n-gi>
<n-text :type="item.zdf>0?'error':'success'">
<n-image :src="item.img" width="20"/> &nbsp;{{ item.name }}
</n-text>
</n-gi>
<n-gi>
<n-text :type="item.zdf>0?'error':'success'">{{ item.zxj }}</n-text>&nbsp;
<n-text :type="item.zdf>0?'error':'success'">
<n-number-animation :precision="2" :from="0" :to="item.zdf"/>
%
</n-text>
</n-gi>
<n-gi>
<n-text :type="item.state === 'open' ? 'success' : 'warning'">{{
item.state === 'open' ? '开市' : '休市'
}}
</n-text>
</n-gi>
</n-grid>
</n-list-item>
</n-list>
</n-gi>
</n-grid>
</n-tab-pane>
<n-tab-pane name="上证指数" tab="上证指数">
<k-line-chart code="sh000001" :chart-height="panelHeight" name="上证指数" :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"
: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"
: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"
: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"
: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"
: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"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="指标行情" tab="指标行情">
<n-tabs type="segment" animated>
<n-tab-pane name="科创50" tab="科创50">
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :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"
: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"
: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"
: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"
: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"
: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"
: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"
:dark-theme="true"></k-line-chart>
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="行业排名" tab="行业排名">
<n-tabs type="card" animated>
<n-tab-pane name="行业涨幅排名" tab="行业涨幅排名">
<n-table striped>
<n-thead>
<n-tr>
<n-th>行业名称</n-th>
<n-th @click="changeIndustryRankSort">行业涨幅
<n-icon v-if="sort==='0'" :component="CaretDown"/>
<n-icon v-if="sort==='1'" :component="CaretUp"/>
</n-th>
<n-th>行业5日涨幅</n-th>
<n-th>行业20日涨幅</n-th>
<n-th>领涨股</n-th>
<n-th>涨幅</n-th>
<n-th>最新价</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in industryRanks" :key="item.bd_code">
<n-td>
<n-tag :bordered=false type="info">{{ item.bd_name }}</n-tag>
</n-td>
<n-td>
<n-text :type="item.bd_zdf>0?'error':'success'">{{ item.bd_zdf }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.bd_zdf5>0?'error':'success'">{{ item.bd_zdf5 }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.bd_zdf20>0?'error':'success'">{{ item.bd_zdf20 }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_name }}
<n-text type="info">{{ item.nzg_code }}</n-text>
</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_zdf }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'">{{ item.nzg_zxj }}</n-text>
</n-td>
</n-tr>
</n-tbody>
</n-table>
<n-table striped>
<n-thead>
<n-tr>
<n-th>行业名称</n-th>
<n-th @click="changeIndustryRankSort">行业涨幅
<n-icon v-if="sort==='0'" :component="CaretDown"/>
<n-icon v-if="sort==='1'" :component="CaretUp"/>
</n-th>
<n-th>行业5日涨幅</n-th>
<n-th>行业20日涨幅</n-th>
<n-th>领涨股</n-th>
<n-th>涨幅</n-th>
<n-th>最新价</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in industryRanks" :key="item.bd_code">
<n-td>
<n-tag :bordered=false type="info">{{ item.bd_name }}</n-tag>
</n-td>
<n-td>
<n-text :type="item.bd_zdf>0?'error':'success'">{{ item.bd_zdf }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.bd_zdf5>0?'error':'success'">{{ item.bd_zdf5 }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.bd_zdf20>0?'error':'success'">{{ item.bd_zdf20 }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_name }}
<n-text type="info">{{ item.nzg_code }}</n-text>
</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_zdf }}%</n-text>
</n-td>
<n-td>
<n-text :type="item.nzg_zdf>0?'error':'success'">{{ item.nzg_zxj }}</n-text>
</n-td>
</n-tr>
</n-tbody>
</n-table>
</n-tab-pane>
<n-tab-pane name="行业资金排名(净流入)" tab="行业资金排名">
<industryMoneyRank :fenlei="'0'" :header-title="'行业资金排名(净流入)'" :sort="'netamount'"/>
</n-tab-pane>
<n-tab-pane name="证监会行业资金排名(净流入)" tab="证监会行业资金排名">
<industryMoneyRank :fenlei="'2'" :header-title="'证监会行业资金排名(净流入)'" :sort="'netamount'"/>
</n-tab-pane>
<n-tab-pane name="概念板块资金排名(净流入)" tab="概念板块资金排名">
<industryMoneyRank :fenlei="'1'" :header-title="'概念板块资金排名(净流入)'" :sort="'netamount'"/>
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="个股资金流向" tab="个股资金流向">
<n-tabs type="card" animated>
<n-tab-pane name="netamount" tab="净流入额排名">
<RankTable :header-title="'净流入额排名'" :sort="'netamount'"/>
</n-tab-pane>
<n-tab-pane name="outamount" tab="流出资金排名">
<RankTable :header-title="'流出资金排名'" :sort="'outamount'"/>
</n-tab-pane>
<n-tab-pane name="ratioamount" tab="净流入率排名">
<RankTable :header-title="'净流入率排名'" :sort="'ratioamount'"/>
</n-tab-pane>
<n-tab-pane name="r0_net" tab="主力净流入额排名">
<RankTable :header-title="'主力净流入额排名'" :sort="'r0_net'"/>
</n-tab-pane>
<n-tab-pane name="r0_out" tab="主力流出排名">
<RankTable :header-title="'主力流出排名'" :sort="'r0_out'"/>
</n-tab-pane>
<n-tab-pane name="r0_ratio" tab="主力净流入率排名">
<RankTable :header-title="'主力净流入率排名'" :sort="'r0_ratio'"/>
</n-tab-pane>
<n-tab-pane name="r3_net" tab="散户净流入额排名">
<RankTable :header-title="'散户净流入额排名'" :sort="'r3_net'"/>
</n-tab-pane>
<n-tab-pane name="r3_out" tab="散户流出排名">
<RankTable :header-title="'散户流出排名'" :sort="'r3_out'"/>
</n-tab-pane>
<n-tab-pane name="r3_ratio" tab="散户净流入率排名">
<RankTable :header-title="'散户净流入率排名'" :sort="'r3_ratio'"/>
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="龙虎榜" tab="龙虎榜">
<LongTigerRankList />
</n-tab-pane>
<n-tab-pane name="个股研报" tab="个股研报">
<StockResearchReportList :stock-code="stockCode"/>
</n-tab-pane>
<n-tab-pane name="公司公告" tab="公司公告 ">
<StockNoticeList :stock-code="stockCode" />
</n-tab-pane>
<n-tab-pane name="行业研究" tab="行业研究 ">
<IndustryResearchReportList/>
</n-tab-pane>
<n-tab-pane name="当前热门" tab="当前热门">
<n-tabs type="card" animated>
<n-tab-pane name="全球" tab="全球">
<HotStockList :market-type="'10'"/>
</n-tab-pane>
<n-tab-pane name="沪深" tab="沪深">
<HotStockList :market-type="'12'"/>
</n-tab-pane>
<n-tab-pane name="港股" tab="港股">
<HotStockList :market-type="'13'"/>
</n-tab-pane>
<n-tab-pane name="美股" tab="美股">
<HotStockList :market-type="'11'"/>
</n-tab-pane>
<n-tab-pane name="热门话题" tab="热门话题">
<n-grid :cols="1" :y-gap="10">
<n-grid-item>
<HotTopics/>
</n-grid-item>
<!-- <n-grid-item>-->
<!-- <HotEvents/>-->
<!-- </n-grid-item>-->
</n-grid>
</n-tab-pane>
<n-tab-pane name="重大事件时间轴" tab="重大事件时间轴">
<InvestCalendarTimeLine />
</n-tab-pane>
<n-tab-pane name="财经日历" tab="财经日历">
<ClsCalendarTimeLine />
</n-tab-pane>
</n-tabs>
</n-tab-pane>
<n-tab-pane name="指标选股" tab="指标选股">
<select-stock />
</n-tab-pane>
</n-tabs>
</n-card>
<n-modal transform-origin="center" v-model:show="summaryModal" preset="card" style="width: 800px;"
:title="'AI市场资讯总结'">
<n-spin size="small" :show="loading">
<MdPreview style="height: 440px;text-align: left" :modelValue="aiSummary" :theme="theme"/>
</n-spin>
<template #footer>
<n-flex justify="space-between" ref="tipsRef">
<n-text type="info" v-if="aiSummaryTime">
<n-tag v-if="modelName" type="warning" round :title="chatId" :bordered="false">{{ modelName }}</n-tag>
{{ aiSummaryTime }}
</n-text>
<n-text type="error">*AI分析结果仅供参考请以实际行情为准投资需谨慎风险自担</n-text>
</n-flex>
</template>
<template #action>
<n-flex justify="space-between" style="margin-bottom: 10px">
<n-select style="width: 49%" v-model:value="sysPromptId" label-field="name" value-field="ID"
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
<n-select style="width: 49%" v-model:value="question" label-field="name" value-field="content"
:options="userPromptOptions" placeholder="请选择用户提示词"/>
</n-flex>
<n-flex justify="right">
<n-input v-model:value="question" style="text-align: left" clearable
type="textarea"
:show-count="true"
placeholder="请输入您的问题:例如 总结和分析股票市场新闻中的投资机会"
:autosize="{
minRows: 2,
maxRows: 5
}"
/>
<n-button size="tiny" type="warning" @click="reAiSummary">再次总结</n-button>
<n-button size="tiny" type="success" @click="copyToClipboard">复制到剪切板</n-button>
<n-button size="tiny" type="primary" @click="saveAsMarkdown">保存为Markdown文件</n-button>
<n-button size="tiny" type="error" @click="share">分享到项目社区</n-button>
</n-flex>
</template>
</n-modal>
<div style="position: fixed;bottom: 18px;right:25px;z-index: 10;" v-if="summaryBTN">
<n-input-group>
<n-button type="primary" @click="getAiSummary">
<n-icon :component="PulseOutline"/> &nbsp;AI总结
</n-button>
</n-input-group>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,374 @@
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {GetStockMoneyTrendByDay} from "../../wailsjs/go/main/App";
import * as echarts from "echarts";
const {code, name, darkTheme, days, chartHeight} = defineProps({
code: {
type: String,
default: ''
},
name: {
type: String,
default: ''
},
days: {
type: Number,
default: 14
},
chartHeight: {
type: Number,
default: 500
},
darkTheme: {
type: Boolean,
default: false
}
})
const LineChartRef = ref(null);
onMounted(
() => {
handleLine(code, days)
}
)
const handleLine = (code, days) => {
GetStockMoneyTrendByDay(code, days).then(result => {
//console.log("GetStockMoneyTrendByDay", result)
const chart = echarts.init(LineChartRef.value);
const categoryData = [];
const netamount_values = [];
const r0_net_values = [];
const trades_values = [];
let volume = []
let min = 0
let max = 0
for (let i = 0; i < result.length; i++) {
let resultElement = result[i]
categoryData.push(resultElement.opendate)
let netamount = (resultElement.netamount / 10000).toFixed(2);
netamount_values.push(netamount)
let price = Number(resultElement.trade);
trades_values.push(price)
r0_net_values.push((resultElement.r0_net / 10000).toFixed(2))
if (min === 0 || min > price) {
min = price
}
if (max < price) {
max = price
}
if (i > 0) {
let b = Number(Number(result[i].netamount) + Number(result[i - 1].netamount)) / 10000
volume.push(b.toFixed(2))
} else {
volume.push((Number(result[i].netamount) / 10000).toFixed(2))
}
}
//console.log("volume", volume)
const upColor = '#ec0000';
const downColor = '#00da3c';
let option = {
title: {
text: name,
left: '20px',
textStyle: {
color: darkTheme?'#ccc':'#456'
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
lineStyle: {
color: '#376df4',
width: 1,
opacity: 1
}
},
borderWidth: 2,
borderColor: darkTheme?'#456':'#ccc',
backgroundColor: darkTheme?'#456':'#fff',
padding: 10,
textStyle: {
color: darkTheme?'#ccc':'#456'
},
},
axisPointer: {
link: [
{
xAxisIndex: 'all'
}
],
label: {
backgroundColor: '#888'
}
},
legend: {
show: true,
data: ['当日净流入', '主力当日净流入','累计净流入', '股价'],
selected: {
'当日净流入': true,
'主力当日净流入': true,
'累计净流入': true,
'股价': true,
},
//orient: 'vertical',
textStyle: {
color: darkTheme ? 'rgb(253,252,252)' : '#456'
},
right: 150,
},
dataZoom: [
{
type: 'inside',
xAxisIndex: [0, 1],
start: 86,
end: 100
},
{
show: true,
xAxisIndex: [0, 1],
type: 'slider',
top: '90%',
start: 86,
end: 100
}
],
grid: [
{
left: '8%',
right: '8%',
height: '50%',
},
{
left: '8%',
right: '8%',
top: '74%',
height: '15%'
},
],
xAxis: [
{
type: 'category',
data: categoryData,
axisPointer: {
z: 100
},
boundaryGap: false,
axisLine: { onZero: false },
splitLine: { show: false },
min: 'dataMin',
max: 'dataMax',
},
{
gridIndex: 1,
type: 'category',
data: categoryData,
axisLabel: {
show: false
},
}
],
yAxis: [
{
name: '当日净流入/万',
type: 'value',
axisLine: {
show: true
},
splitLine: {
show: false
},
},
{
name: '股价',
type: 'value',
min: min - 1,
max: max + 1,
minInterval: 0.01,
axisLine: {
show: true
},
splitLine: {
show: false
},
},
{
gridIndex: 1,
name: '累计净流入/万',
type: 'value',
axisLine: {
show: true
},
splitLine: {
show: false
},
},
],
series: [
{
yAxisIndex: 0,
name: '当日净流入',
data: netamount_values,
smooth: false,
showSymbol: false,
lineStyle: {
width: 2
},
markPoint: {
symbol: 'arrow',
symbolRotate: 90,
symbolSize: [10, 20],
symbolOffset: [10, 0],
itemStyle: {
color: '#0d7dfc'
},
label: {
position: 'right',
},
data: [
{type: 'max', name: 'Max'},
{type: 'min', name: 'Min'}
]
},
markLine: {
data: [
{
type: 'average',
name: 'Average',
lineStyle: {
color: '#0077ff',
width: 0.5
},
},
]
},
type: 'line'
},
{
yAxisIndex: 0,
name: '主力当日净流入',
data: r0_net_values,
smooth: false,
showSymbol: false,
lineStyle: {
width: 2
},
// markPoint: {
// symbol: 'arrow',
// symbolRotate: 90,
// symbolSize: [10, 20],
// symbolOffset: [10, 0],
// itemStyle: {
// color: '#0d7dfc'
// },
// label: {
// position: 'right',
// },
// data: [
// {type: 'max', name: 'Max'},
// {type: 'min', name: 'Min'}
// ]
// },
// markLine: {
// data: [
// {
// type: 'average',
// name: 'Average',
// lineStyle: {
// color: '#0077ff',
// width: 0.5
// },
// },
// ]
// },
type: 'bar'
},
{
yAxisIndex: 1,
name: '股价',
type: 'line',
data: trades_values,
smooth: true,
showSymbol: false,
lineStyle: {
width: 3
},
markPoint: {
symbol: 'arrow',
symbolRotate: 90,
symbolSize: [10, 20],
symbolOffset: [10, 0],
itemStyle: {
color: '#f39509'
},
label: {
position: 'right',
},
data: [
{type: 'max', name: 'Max'},
{type: 'min', name: 'Min'}
]
},
markLine: {
data: [
{
type: 'average',
name: 'Average',
lineStyle: {
color: '#f39509',
width: 0.5
},
},
]
},
},
{
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 2,
name: '累计净流入',
data: volume,
smooth: true,
showSymbol: false,
lineStyle: {
width: 2
},
markPoint: {
symbol: 'arrow',
symbolRotate: 90,
symbolSize: [10, 20],
symbolOffset: [10, 0],
// itemStyle: {
// color: '#f39509'
// },
label: {
position: 'right',
},
data: [
{type: 'max', name: 'Max'},
{type: 'min', name: 'Min'}
]
},
},
]
};
chart.setOption(option);
})
}
</script>
<template>
<div ref="LineChartRef" style="width: 100%;height: auto;" :style="{height:chartHeight+'px'}"></div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,58 @@
<script setup>
import {ReFleshTelegraphList} from "../../wailsjs/go/main/App";
import {RefreshCircle, RefreshCircleSharp, RefreshOutline} from "@vicons/ionicons5";
const { headerTitle,newsList } = defineProps({
headerTitle: {
type: String,
default: '市场资讯'
},
newsList: {
type: Array,
default: () => []
},
})
const emits = defineEmits(['update:message'])
const updateMessage = () => {
emits('update:message', headerTitle)
}
</script>
<template>
<n-list bordered>
<template #header>
<n-flex justify="space-between">
<n-tag :bordered="false" size="large" type="success" >{{ headerTitle }}</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-space justify="start">
<n-text justify="start" :bordered="false" :type="item.isRed?'error':'info'">
<n-tag size="small" :type="item.isRed?'error':'warning'" :bordered="false"> {{ item.time }}</n-tag>
{{ item.content }}
</n-text>
</n-space>
<n-space v-if="item.subjects" style="margin-top: 2px">
<n-tag :bordered="false" type="success" size="small" v-for="sub in item.subjects">
{{ sub }}
</n-tag>
<n-space v-if="item.stocks">
<n-tag :bordered="false" type="warning" size="small" v-for="sub in item.stocks">
{{ sub }}
</n-tag>
</n-space>
<n-tag v-if="item.url" :bordered="false" type="warning" size="small">
<a :href="item.url" target="_blank">
<n-text type="warning">查看原文</n-text>
</a>
</n-tag>
</n-space>
</n-list-item>
</n-list>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,101 @@
<script setup>
import {CaretDown, CaretUp, RefreshCircleOutline} from "@vicons/ionicons5";
import {NText,useMessage} from "naive-ui";
import {onBeforeUnmount, onMounted, onUnmounted, ref} from "vue";
import {GetMoneyRankSina} from "../../wailsjs/go/main/App";
import KLineChart from "./KLineChart.vue";
const props = defineProps({
headerTitle: {
type: String,
default: '净流入额排名'
},
sort: {
type: String,
default: 'netamount'
},
})
const message = useMessage()
const dataList= ref([])
const sort = ref(props.sort)
const interval = ref(null)
onMounted(()=>{
sort.value=props.sort
GetMoneyRankSinaData()
interval.value=setInterval(()=>{
GetMoneyRankSinaData()
},1000*60)
})
onBeforeUnmount(()=>{
clearInterval(interval.value)
})
function GetMoneyRankSinaData(){
message.loading("正在刷新数据...")
GetMoneyRankSina(sort.value).then(result => {
if(result.length>0){
dataList.value = result
}
})
}
</script>
<template>
<n-table striped size="small">
<n-thead>
<n-tr>
<n-th>代码</n-th>
<n-th>名称</n-th>
<n-th>最新价</n-th>
<n-th>涨跌幅</n-th>
<n-th>换手率</n-th>
<n-th>成交额/</n-th>
<n-th>流出资金/</n-th>
<n-th>流入资金/</n-th>
<n-th>净流入/</n-th>
<n-th>净流入率</n-th>
<n-th v-if="sort === 'r0_net'||sort==='r0_out'">主力流出/</n-th>
<n-th v-if="sort === 'r0_net'">主力流入/</n-th>
<n-th v-if="sort === 'r0_net'">主力净流入/</n-th>
<n-th >主力净流入率</n-th>
<n-th v-if="sort === 'r3_net'||sort==='r3_out'">散户流出/</n-th>
<n-th v-if="sort === 'r3_net'">散户流入/</n-th>
<n-th v-if="sort === 'r3_net'">散户净流入/</n-th>
<n-th >散户净流入率</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in dataList" :key="item.symbol">
<n-td><n-tag :bordered=false type="info">{{ item.symbol }}</n-tag></n-td>
<n-td>
<n-popover trigger="hover" placement="right">
<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>
</n-popover>
</n-td>
<n-td><n-text :type="item.changeratio>0?'error':'success'">{{item.trade}}</n-text></n-td>
<n-td><n-text :type="item.changeratio>0?'error':'success'">{{(item.changeratio*100).toFixed(2)}}%</n-text></n-td>
<n-td><n-text :type="item.turnover>500?'error':'info'">{{(item.turnover/100).toFixed(2)}}%</n-text></n-td>
<n-td><n-text type="info">{{(item.amount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text type="info"> {{(item.outamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text type="info"> {{(item.inamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text type="info"> {{(item.netamount/10000).toFixed(2)}}</n-text></n-td>
<n-td><n-text :type="item.ratioamount>0?'error':'success'"> {{(item.ratioamount*100).toFixed(2)}}%</n-text></n-td>
<n-td v-if="sort === 'r0_net'||sort==='r0_out'"><n-text type="success"> {{(item.r0_out/10000).toFixed(2)}}</n-text></n-td>
<n-td v-if="sort === 'r0_net'"><n-text type="error"> {{(item.r0_in/10000).toFixed(2)}}</n-text></n-td>
<n-td v-if="sort === 'r0_net'"><n-text :type="item.r0_net>0?'error':'success'"> {{(item.r0_net/10000).toFixed(2)}}</n-text></n-td>
<n-td ><n-text :type="item.r0_ratio>0?'error':'success'"> {{(item.r0_ratio*100).toFixed(2)}}%</n-text></n-td>
<n-td v-if="sort === 'r3_net'||sort==='r3_out'"><n-text type="success"> {{(item.r3_out/10000).toFixed(2)}}</n-text></n-td>
<n-td v-if="sort === 'r3_net'"><n-text type="error"> {{(item.r3_in/10000).toFixed(2)}}</n-text></n-td>
<n-td v-if="sort === 'r3_net'"><n-text :type="item.r3_net>0?'error':'success'"> {{(item.r3_net/10000).toFixed(2)}}</n-text></n-td>
<n-td ><n-text :type="item.r3_ratio>0?'error':'success'"> {{(item.r3_ratio*100).toFixed(2)}}%</n-text></n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,446 @@
<script setup>
import {computed, onBeforeUnmount, onMounted, ref} from "vue";
import {
AddPrompt, DelPrompt,
ExportConfig,
GetConfig,
GetPromptTemplates,
SendDingDingMessageByType,
UpdateConfig
} from "../../wailsjs/go/main/App";
import {useMessage} from "naive-ui";
import {data, models} from "../../wailsjs/go/models";
import {EventsEmit} from "../../wailsjs/runtime";
const message = useMessage()
const formRef = ref(null)
const formValue = ref({
ID:1,
tushareToken:'',
dingPush:{
enable:false,
dingRobot: ''
},
localPush:{
enable:true,
},
updateBasicInfoOnStart:false,
refreshInterval:1,
openAI:{
enable:false,
baseUrl: 'https://api.deepseek.com',
apiKey: '',
model: 'deepseek-chat',
temperature: 0.1,
maxTokens: 1024,
prompt:"",
timeout: 5,
questionTemplate: "{{stockName}}分析和总结",
crawlTimeOut:30,
kDays:30,
},
enableDanmu:false,
browserPath: '',
enableNews:false,
darkTheme:true,
enableFund:false,
enablePushNews:false,
})
const promptTemplates=ref([])
onMounted(()=>{
GetConfig().then(res=>{
formValue.value.ID = res.ID
formValue.value.tushareToken = res.tushareToken
formValue.value.dingPush = {
enable:res.dingPushEnable,
dingRobot:res.dingRobot
}
formValue.value.localPush = {
enable:res.localPushEnable,
}
formValue.value.updateBasicInfoOnStart = res.updateBasicInfoOnStart
formValue.value.refreshInterval = res.refreshInterval
formValue.value.openAI = {
enable:res.openAiEnable,
baseUrl: res.openAiBaseUrl,
apiKey:res.openAiApiKey,
model:res.openAiModelName,
temperature:res.openAiTemperature,
maxTokens:res.openAiMaxTokens,
prompt:res.prompt,
timeout:res.openAiApiTimeOut,
questionTemplate:res.questionTemplate?res.questionTemplate:'{{stockName}}分析和总结',
crawlTimeOut:res.crawlTimeOut,
kDays:res.kDays,
}
formValue.value.enableDanmu = res.enableDanmu
formValue.value.browserPath = res.browserPath
formValue.value.enableNews = res.enableNews
formValue.value.darkTheme = res.darkTheme
formValue.value.enableFund = res.enableFund
formValue.value.enablePushNews = res.enablePushNews
//console.log(res)
})
//message.info("")
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
})
})
onBeforeUnmount(() => {
message.destroyAll()
})
function saveConfig(){
let config= new data.Settings({
ID:formValue.value.ID,
dingPushEnable:formValue.value.dingPush.enable,
dingRobot:formValue.value.dingPush.dingRobot,
localPushEnable:formValue.value.localPush.enable,
updateBasicInfoOnStart:formValue.value.updateBasicInfoOnStart,
refreshInterval:formValue.value.refreshInterval,
openAiEnable:formValue.value.openAI.enable,
openAiBaseUrl:formValue.value.openAI.baseUrl,
openAiApiKey:formValue.value.openAI.apiKey,
openAiModelName:formValue.value.openAI.model,
openAiMaxTokens:formValue.value.openAI.maxTokens,
openAiTemperature:formValue.value.openAI.temperature,
tushareToken:formValue.value.tushareToken,
prompt:formValue.value.openAI.prompt,
openAiApiTimeOut:formValue.value.openAI.timeout,
questionTemplate:formValue.value.openAI.questionTemplate,
crawlTimeOut:formValue.value.openAI.crawlTimeOut,
kDays:formValue.value.openAI.kDays,
enableDanmu:formValue.value.enableDanmu,
browserPath:formValue.value.browserPath,
enableNews:formValue.value.enableNews,
darkTheme:formValue.value.darkTheme,
enableFund:formValue.value.enableFund,
enablePushNews:formValue.value.enablePushNews
})
//console.log("Settings",config)
UpdateConfig(config).then(res=>{
message.success(res)
EventsEmit("updateSettings", config);
})
}
function getHeight() {
return document.documentElement.clientHeight
}
function sendTestNotice(){
let markdown="### go-stock test\n"+new Date()
let msg='{' +
' "msgtype": "markdown",' +
' "markdown": {' +
' "title":"go-stock'+new Date()+'",' +
' "text": "'+markdown+'"' +
' },' +
' "at": {' +
' "isAtAll": true' +
' }' +
' }'
SendDingDingMessageByType(msg, "test-"+new Date().getTime(),1).then(res=>{
message.info(res)
})
}
function exportConfig(){
ExportConfig().then(res=>{
message.info(res)
})
}
function importConfig(){
let input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
let file = e.target.files[0];
let reader = new FileReader();
reader.onload = (e) => {
let config = JSON.parse(e.target.result);
//console.log(config)
formValue.value.ID = config.ID
formValue.value.tushareToken = config.tushareToken
formValue.value.dingPush = {
enable:config.dingPushEnable,
dingRobot:config.dingRobot
}
formValue.value.localPush = {
enable:config.localPushEnable,
}
formValue.value.updateBasicInfoOnStart = config.updateBasicInfoOnStart
formValue.value.refreshInterval = config.refreshInterval
formValue.value.openAI = {
enable:config.openAiEnable,
baseUrl: config.openAiBaseUrl,
apiKey:config.openAiApiKey,
model:config.openAiModelName,
temperature:config.openAiTemperature,
maxTokens:config.openAiMaxTokens,
prompt:config.prompt,
timeout:config.openAiApiTimeOut,
questionTemplate:config.questionTemplate,
crawlTimeOut:config.crawlTimeOut,
kDays:config.kDays
}
formValue.value.enableDanmu = config.enableDanmu
formValue.value.browserPath = config.browserPath
formValue.value.enableNews = config.enableNews
formValue.value.darkTheme = config.darkTheme
formValue.value.enableFund = config.enableFund
formValue.value.enablePushNews = config.enablePushNews
// formRef.value.resetFields()
};
reader.readAsText(file);
};
input.click();
}
window.onerror = function (event, source, lineno, colno, error) {
//console.log(event, source, lineno, colno, error)
//
EventsEmit("frontendError", {
page: "settings.vue",
message: event,
source: source,
lineno: lineno,
colno: colno,
error: error ? error.stack : null
});
//message.error(":"+event)
return true;
};
const showManagePromptsModal=ref(false)
const promptTypeOptions=[
{label:"模型系统Prompt",value:'模型系统Prompt'},
{label:"模型用户Prompt",value:'模型用户Prompt'},]
const formPromptRef=ref(null)
const formPrompt=ref({
ID:0,
Name:'',
Content:'',
Type:'',
})
function managePrompts(){
formPrompt.value.ID=0
showManagePromptsModal.value=true
}
function savePrompt(){
AddPrompt(formPrompt.value).then(res=>{
message.success(res)
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
})
showManagePromptsModal.value=false
})
}
function editPrompt(prompt){
//console.log(prompt)
formPrompt.value.ID=prompt.ID
formPrompt.value.Name=prompt.name
formPrompt.value.Content=prompt.content
formPrompt.value.Type=prompt.type
showManagePromptsModal.value=true
}
function deletePrompt(ID){
DelPrompt(ID).then(res=>{
message.success(res)
GetPromptTemplates("","").then(res=>{
//console.log(res)
promptTemplates.value=res
})
})
}
</script>
<template>
<n-flex justify="left" style="margin-top: 12px;padding-left: 12px;">
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'" >
<n-grid :cols="24" :x-gap="24" style="text-align: left" :layout-shift-disabled="true">
<n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">基础设置</n-text>
</n-gi>
<n-form-item-gi :span="10" label="Tushare &nbsp;&nbsp;Token" path="tushareToken" >
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
</n-form-item-gi>
<n-form-item-gi :span="4" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
<n-switch v-model:value="formValue.updateBasicInfoOnStart" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="数据刷新间隔:" path="refreshInterval" >
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
<template #suffix>
</template>
</n-input-number>
</n-form-item-gi>
<n-form-item-gi :span="6" label="暗黑主题:" path="darkTheme" >
<n-switch v-model:value="formValue.darkTheme" />
</n-form-item-gi>
<n-form-item-gi :span="10" label="浏览器安装路径:" path="browserPath" >
<n-input type="text" placeholder="浏览器安装路径" v-model:value="formValue.browserPath" clearable />
</n-form-item-gi>
<n-form-item-gi :span="6" label="指数基金:" path="enableFund" >
<n-switch v-model:value="formValue.enableFund" />
</n-form-item-gi>
</n-grid>
<n-grid :cols="24" :x-gap="24" style="text-align: left">
<n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">通知设置</n-text>
</n-gi>
<n-form-item-gi :span="4" label="钉钉推送:" path="dingPush.enable" >
<n-switch v-model:value="formValue.dingPush.enable" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="本地推送:" path="localPush.enable" >
<n-switch v-model:value="formValue.localPush.enable" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="弹幕功能:" path="enableDanmu" >
<n-switch v-model:value="formValue.enableDanmu" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="显示滚动快讯:" path="enableNews" >
<n-switch v-model:value="formValue.enableNews" />
</n-form-item-gi>
<n-form-item-gi :span="4" label="市场资讯提醒:" path="enablePushNews" >
<n-switch v-model:value="formValue.enablePushNews" />
</n-form-item-gi>
<n-form-item-gi :span="22" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:" path="dingPush.dingRobot" >
<n-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
</n-form-item-gi>
</n-grid>
<n-grid :cols="24" :x-gap="24" style="text-align: left;">
<n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
</n-gi>
<n-form-item-gi :span="3" label="AI诊股" path="openAI.enable" >
<n-switch v-model:value="formValue.openAI.enable" />
</n-form-item-gi>
<n-form-item-gi :span="9" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl" >
<n-input type="text" placeholder="AI接口地址" v-model:value="formValue.openAI.baseUrl" clearable />
</n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="AI Timeout(秒)" title="AI请求超时时间(秒)" path="openAI.timeout" >
<n-input-number min="60" step="1" placeholder="AI请求超时时间(秒)" v-model:value="formValue.openAI.timeout" />
</n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="Crawler Timeout(秒)" title="资讯采集超时时间(秒)" path="openAI.crawlTimeOut" >
<n-input-number min="30" step="1" placeholder="资讯采集超时时间(秒)" v-model:value="formValue.openAI.crawlTimeOut" />
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI 令牌(apiKey)" path="openAI.apiKey" >
<n-input type="text" placeholder="apiKey" v-model:value="formValue.openAI.apiKey" clearable />
</n-form-item-gi>
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="AI模型名称" path="openAI.model" >
<n-input type="text" placeholder="AI模型名称" v-model:value="formValue.openAI.model" clearable />
</n-form-item-gi>
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI temperature" path="openAI.temperature" >
<n-input-number placeholder="temperature" v-model:value="formValue.openAI.temperature"/>
</n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="openAI maxTokens" path="openAI.maxTokens" >
<n-input-number placeholder="maxTokens" v-model:value="formValue.openAI.maxTokens"/>
</n-form-item-gi>
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多" label="日K线数据(天)" path="openAI.maxTokens" >
<n-input-number min="30" step="1" max="365" placeholder="日K线数据(天)" title="天数越多消耗tokens越多" v-model:value="formValue.openAI.kDays"/>
</n-form-item-gi>
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="模型系统 Prompt" path="openAI.prompt" >
<n-input v-model:value="formValue.openAI.prompt"
type="textarea"
:show-count="true"
placeholder="请输入系统prompt"
:autosize="{
minRows: 5,
maxRows: 8
}"
/>
</n-form-item-gi>
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="模型用户 Prompt" path="openAI.questionTemplate" >
<n-input v-model:value="formValue.openAI.questionTemplate"
type="textarea"
:show-count="true"
placeholder="请输入用户prompt:例如{{stockName}}[{{stockCode}}]分析和总结"
:autosize="{
minRows: 5,
maxRows: 8
}"
/>
</n-form-item-gi>
</n-grid>
<n-gi :span="24">
<n-space justify="center">
<n-button type="warning" @click="managePrompts">
添加提示词模板
</n-button>
<n-button type="primary" @click="saveConfig">
保存
</n-button>
<n-button type="info" @click="exportConfig">
导出
</n-button>
<n-button type="error" @click="importConfig">
导入
</n-button>
</n-space>
</n-gi>
</n-form>
<n-gi :span="24" v-if="promptTemplates.length>0" v-for="prompt in promptTemplates" >
<n-flex justify="start">
<n-tag closable @close="deletePrompt(prompt.ID)" @click="editPrompt(prompt)" :title="prompt.content" :type="prompt.type==='模型系统Prompt'?'success':'info'" :bordered="false"> {{prompt.name}} </n-tag>
</n-flex>
</n-gi>
</n-flex>
<n-modal v-model:show="showManagePromptsModal" closable :mask-closable="false">
<n-card
style="width: 800px;height: 600px;text-align: left"
:bordered="false"
:title="(formPrompt.ID>0?'修改':'添加')+'提示词'"
size="huge"
role="dialog"
aria-modal="true"
>
<n-form ref="formPromptRef" :label-placement="'left'" :label-align="'left'" >
<n-form-item label="名称">
<n-input v-model:value="formPrompt.Name" placeholder="请输入提示词名称" />
</n-form-item>
<n-form-item label="类型">
<n-select v-model:value="formPrompt.Type" :options="promptTypeOptions" placeholder="请选择提示词类型" />
</n-form-item>
<n-form-item label="内容">
<n-input v-model:value="formPrompt.Content"
type="textarea"
:show-count="true"
placeholder="请输入prompt"
:autosize="{
minRows: 12,
maxRows: 12,
}"
/>
</n-form-item>
</n-form>
<template #footer>
<n-flex justify="end">
<n-button type="primary" @click="savePrompt">
保存
</n-button>
<n-button type="warning" @click="showManagePromptsModal=false">
取消
</n-button>
</n-flex>
</template>
</n-card>
</n-modal>
</template>
<style scoped>
</style>

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More