Compare commits

...

255 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
84 changed files with 266430 additions and 1522 deletions

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

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
@ -38,13 +38,13 @@ jobs:
echo "::set-output name=commit_message::$commit_message"
- name: Build wails x go-stock
uses: ArvinLovegood/wails-build-action@v2.8
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: '18.x'
node-version: '20.x'

1
.gitignore vendored
View File

@ -110,3 +110,4 @@ dist
/build/*.exe
/build/bin/*
frontend/package.json.md5
/build/us.json

101
README.md
View File

@ -1,49 +1,111 @@
![go-stock](./build/appicon.png)
## go-stock : 基于Wails和NaiveUI构建的AI赋能股票分析工具
# 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)
[//]: # ([![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+,其他平台未测试或功能受限。
### 💬 大模型
| 模型 | 状态 | 备注 |
| --- | --- |-------------------------------------------|
| [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模型测试有问题可通过本地模型或聚合模型平台使用 |
| [大模型聚合平台](https://cloud.siliconflow.cn/i/foufCerk) | ✅ | 如:硅基流动,火山方舟等 |
### 📦 立即体验
- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases)
- 绿色版:[go-stock-windows-amd64.exe](https://github.com/ArvinLovegood/go-stock/releases)
### 💬 支持大模型/平台
| 模型 | 状态 | 备注 |
| --- | --- |---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [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) |
### <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)
- Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究,[注册链接](https://tushare.pro/register?reg=701944)
- 火山方舟每个模型注册即送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。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧)。💕
## 🧩 功能开发计划
## 🧩 重大功能开发计划
| 功能说明 | 状态 | 备注 |
|-----------------|----|----------------------------------------------------------------------------------------------------------|
| 多轮对话 | ✅ | AI分析后可继续对话提问 |
| 股票分析知识库 | 🚧 | 未来计划 |
| 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浏览器抓取新闻资讯 |
## 👀 更新日志
### 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)
## 📦 立即体验
- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases)
- 绿色版:[go-stock-windows-amd64.exe](https://github.com/ArvinLovegood/go-stock/releases)
## 🦄 重大更新
### 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)
## 📸 功能截图
@ -53,7 +115,7 @@
### 成本设置
![img.png](build/screenshot/img_7.png)
### 日K
![img_2.png](build/screenshot/img_8.png)
![img_12.png](build/screenshot/img_12.png)
### 分时
![img_3.png](build/screenshot/img_9.png)
### 钉钉报警通知
@ -98,3 +160,4 @@
## License
[Apache License 2.0](LICENSE)

682
app.go
View File

@ -5,37 +5,49 @@ package main
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"os"
"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/getlantern/systray"
"github.com/duke-git/lancet/v2/strutil"
"github.com/energye/systray"
"github.com/go-resty/resty/v2"
"github.com/go-toast/toast"
"github.com/robfig/cron/v3"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
"golang.org/x/sys/windows/registry"
"os"
"strings"
"time"
)
// App struct
type App struct {
ctx context.Context
cache *freecache.Cache
ctx context.Context
cache *freecache.Cache
cron *cron.Cron
cronEntrys map[string]cron.EntryID
}
// NewApp creates a new App application struct
func NewApp() *App {
cacheSize := 512 * 1024
cache := freecache.NewCache(cacheSize)
c := cron.New(cron.WithSeconds())
c.Start()
return &App{
cache: cache,
cache: cache,
cron: c,
cronEntrys: make(map[string]cron.EntryID),
}
}
@ -50,15 +62,50 @@ func (a *App) startup(ctx context.Context) {
a.ctx = ctx
// 创建系统托盘
systray.Run(func() {
go onReady(a)
//systray.RunWithExternalLoop(func() {
// onReady(a)
//}, func() {
// onExit(a)
//})
runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) {
logger.SugaredLogger.Infof("updateSettings : %v\n", optionalData)
config := &data.Settings{}
setMap := optionalData[0].(map[string]interface{})
// 将 map 转换为 JSON 字节切片
jsonData, err := json.Marshal(setMap)
if err != nil {
logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
return
}
// 将 JSON 字节切片解析到结构体中
err = json.Unmarshal(jsonData, config)
if err != nil {
logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
return
}
logger.SugaredLogger.Infof("updateSettings config:%+v", config)
if config.DarkTheme {
runtime.WindowSetBackgroundColour(ctx, 27, 38, 54, 1)
runtime.WindowSetDarkTheme(ctx)
} else {
runtime.WindowSetBackgroundColour(ctx, 255, 255, 255, 1)
runtime.WindowSetLightTheme(ctx)
}
runtime.WindowReloadApp(ctx)
})
go systray.Run(func() {
onReady(a)
}, func() {
go onExit(a)
onExit(a)
})
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
}
func checkUpdate(a *App) {
func (a *App) CheckUpdate() {
releaseVersion := &models.GitHubReleaseVersion{}
_, err := resty.New().R().
SetResult(releaseVersion).
@ -92,40 +139,130 @@ func checkUpdate(a *App) {
func (a *App) domReady(ctx context.Context) {
defer PanicHandler()
if stocksBin != nil && len(stocksBin) > 0 {
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查A股基础信息...")
go initStockData(a.ctx)
}
if stocksBinHK != nil && len(stocksBinHK) > 0 {
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查港股基础信息...")
go initStockDataHK(a.ctx)
}
if stocksBinUS != nil && len(stocksBinUS) > 0 {
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查美股基础信息...")
go initStockDataUS(a.ctx)
}
updateBasicInfo()
// Add your action here
//定时更新数据
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
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)
//ticker := time.NewTicker(time.Second * time.Duration(interval))
//defer ticker.Stop()
//for range ticker.C {
// MonitorStockPrices(a)
//}
id, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", interval), func() {
MonitorStockPrices(a)
})
if err != nil {
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
} else {
a.cronEntrys["MonitorStockPrices"] = id
}
entryID, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", interval+10), func() {
news := data.NewMarketNewsApi().GetNewTelegraph(30)
if config.EnablePushNews {
go a.NewsPush(news)
}
go runtime.EventsEmit(a.ctx, "newTelegraph", news)
})
if err != nil {
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
} else {
a.cronEntrys["GetNewTelegraph"] = entryID
}
entryIDSina, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", interval+10), func() {
news := data.NewMarketNewsApi().GetSinaNews(30)
if config.EnablePushNews {
go a.NewsPush(news)
}
go runtime.EventsEmit(a.ctx, "newSinaNews", news)
})
if err != nil {
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
} else {
a.cronEntrys["newSinaNews"] = entryIDSina
}
}()
//刷新基金净值信息
go func() {
ticker := time.NewTicker(time.Second * time.Duration(60))
defer ticker.Stop()
for range ticker.C {
//ticker := time.NewTicker(time.Second * time.Duration(60))
//defer ticker.Stop()
//for range ticker.C {
// MonitorFundPrices(a)
//}
if config.EnableFund {
id, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", 60), func() {
MonitorFundPrices(a)
})
if err != nil {
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
} else {
a.cronEntrys["MonitorFundPrices"] = id
}
}
}()
if config.EnableNews {
//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)
// }
// }
//
//}()
id, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", 60), func() {
telegraph := refreshTelegraphList()
if telegraph != nil {
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
}
})
if err != nil {
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
} else {
a.cronEntrys["refreshTelegraphList"] = id
}
}()
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
}
go MonitorStockPrices(a)
if config.EnableFund {
go MonitorFundPrices(a)
go data.NewFundApi().AllFund()
}
//检查新版本
go func() {
checkUpdate(a)
a.CheckUpdate()
a.cron.AddFunc("30 05 8,12,20 * * *", func() {
logger.SugaredLogger.Errorf("Checking for updates...")
a.CheckUpdate()
})
}()
//检查谷歌浏览器
@ -137,14 +274,66 @@ func (a *App) domReady(ctx context.Context) {
//}()
//检查Edge浏览器
go func() {
path, e := checkEdgeOnWindows()
if !e {
go runtime.EventsEmit(a.ctx, "warnMsg", "Edge浏览器未安装,ai分析功能可能无法使用")
} else {
logger.SugaredLogger.Infof("Edge浏览器已安装路径为: %s", path)
//go func() {
// path, e := checkEdgeOnWindows()
// if !e {
// go runtime.EventsEmit(a.ctx, "warnMsg", "Edge浏览器未安装,ai分析功能可能无法使用")
// } else {
// logger.SugaredLogger.Infof("Edge浏览器已安装路径为: %s", path)
// }
//}()
followList := data.NewStockDataApi().GetFollowList(0)
for _, follow := range *followList {
if follow.Cron == nil || *follow.Cron == "" {
continue
}
}()
entryID, err := a.cron.AddFunc(*follow.Cron, a.AddCronTask(follow))
if err != nil {
logger.SugaredLogger.Errorf("添加自动分析任务失败:%s cron=%s entryID:%v", follow.Name, *follow.Cron, entryID)
continue
}
a.cronEntrys[follow.StockCode] = entryID
}
logger.SugaredLogger.Infof("domReady-cronEntrys:%+v", a.cronEntrys)
}
func (a *App) NewsPush(news *[]models.Telegraph) {
for _, telegraph := range *news {
//if telegraph.IsRed {
go runtime.EventsEmit(a.ctx, "newsPush", telegraph)
go data.NewAlertWindowsApi("go-stock", telegraph.Source+" "+telegraph.Time, telegraph.Content, string(icon)).SendNotification()
//}
}
}
func (a *App) AddCronTask(follow data.FollowedStock) func() {
return func() {
go runtime.EventsEmit(a.ctx, "warnMsg", "开始自动分析"+follow.Name+"_"+follow.StockCode)
ai := data.NewDeepSeekOpenAi(a.ctx)
msgs := ai.NewChatStream(follow.Name, follow.StockCode, "", nil)
var res strings.Builder
chatId := ""
question := ""
for msg := range msgs {
if msg["extraContent"] != nil {
res.WriteString(msg["extraContent"].(string) + "\n")
}
if msg["content"] != nil {
res.WriteString(msg["content"].(string))
}
if msg["chatId"] != nil {
chatId = msg["chatId"].(string)
}
if msg["question"] != nil {
question = msg["question"].(string)
}
}
data.NewDeepSeekOpenAi(a.ctx).SaveAIResponseResult(follow.StockCode, follow.Name, res.String(), chatId, question)
go runtime.EventsEmit(a.ctx, "warnMsg", "AI分析完成"+follow.Name+"_"+follow.StockCode)
}
}
func refreshTelegraphList() *[]string {
@ -202,6 +391,84 @@ func isTradingTime(date time.Time) bool {
return false
}
// IsHKTradingTime 判断当前时间是否在港股交易时间内
func IsHKTradingTime(date time.Time) bool {
hour, minute, _ := date.Clock()
// 开市前竞价时段09:00 - 09:30
if (hour == 9 && minute >= 0) || (hour == 9 && minute <= 30) {
return true
}
// 上午持续交易时段09:30 - 12:00
if (hour == 9 && minute > 30) || (hour >= 10 && hour < 12) || (hour == 12 && minute == 0) {
return true
}
// 下午持续交易时段13:00 - 16:00
if (hour == 13 && minute >= 0) || (hour >= 14 && hour < 16) || (hour == 16 && minute == 0) {
return true
}
// 收市竞价交易时段16:00 - 16:10
if (hour == 16 && minute >= 0) || (hour == 16 && minute <= 10) {
return true
}
return false
}
// IsUSTradingTime 判断当前时间是否在美股交易时间内
func IsUSTradingTime(date time.Time) bool {
// 获取美国东部时区
est, err := time.LoadLocation("America/New_York")
var estTime time.Time
if err != nil {
estTime = date.Add(time.Hour * -12)
} else {
// 将当前时间转换为美国东部时间
estTime = date.In(est)
}
// 判断是否是周末
weekday := estTime.Weekday()
if weekday == time.Saturday || weekday == time.Sunday {
return false
}
// 获取小时和分钟
hour, minute, _ := estTime.Clock()
// 判断是否在4:00 AM到9:30 AM之间盘前
if (hour == 4) || (hour == 5) || (hour == 6) || (hour == 7) || (hour == 8) || (hour == 9 && minute < 30) {
return true
}
// 判断是否在9:30 AM到4:00 PM之间盘中
if (hour == 9 && minute >= 30) || (hour >= 10 && hour < 16) || (hour == 16 && minute == 0) {
return true
}
// 判断是否在4:00 PM到8:00 PM之间盘后
if (hour == 16 && minute > 0) || (hour >= 17 && hour < 20) || (hour == 20 && minute == 0) {
return true
}
return false
}
func MonitorFundPrices(a *App) {
dest := &[]data.FollowedFund{}
db.Dao.Model(&data.FollowedFund{}).Find(dest)
for _, follow := range *dest {
_, err := data.NewFundApi().CrawlFundBasic(follow.Code)
if err != nil {
logger.SugaredLogger.Errorf("获取基金基本信息失败,基金代码:%s错误信息%s", follow.Code, err.Error())
continue
}
data.NewFundApi().CrawlFundNetEstimatedUnit(follow.Code)
data.NewFundApi().CrawlFundNetUnitValue(follow.Code)
}
}
func MonitorStockPrices(a *App) {
dest := &[]data.FollowedStock{}
db.Dao.Model(&data.FollowedStock{}).Find(dest)
@ -217,11 +484,24 @@ func MonitorStockPrices(a *App) {
stockInfos := GetStockInfos(*dest...)
for _, stockInfo := range *stockInfos {
if strutil.HasPrefixAny(stockInfo.Code, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) {
continue
}
if strutil.HasPrefixAny(stockInfo.Code, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) {
continue
}
if strutil.HasPrefixAny(stockInfo.Code, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) {
continue
}
total += stockInfo.ProfitAmountToday
price, _ := convertor.ToFloat(stockInfo.Price)
if stockInfo.PrePrice != price {
//logger.SugaredLogger.Infof("-----------sz------------股票代码: %s, 股票名称: %s, 股票价格: %s,盘前盘后:%s", stockInfo.Code, stockInfo.Name, stockInfo.Price, stockInfo.BA)
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
}
}
if total != 0 {
title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
@ -236,11 +516,24 @@ func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
stockInfos := make([]data.StockInfo, 0)
stockCodes := make([]string, 0)
for _, follow := range follows {
if strutil.HasPrefixAny(follow.StockCode, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) {
continue
}
if strutil.HasPrefixAny(follow.StockCode, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) {
continue
}
if strutil.HasPrefixAny(follow.StockCode, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) {
continue
}
stockCodes = append(stockCodes, follow.StockCode)
}
stockData, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
for _, info := range *stockData {
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
if strutil.HasPrefixAny(follow.StockCode, []string{"US", "us"}) {
return strings.ToLower(strings.Replace(follow.StockCode, "us", "gb_", 1)) == info.Code
}
return follow.StockCode == info.Code
})
if ok {
@ -268,6 +561,7 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
stockData.CostVolume = follow.Volume //成本量
stockData.AlarmChangePercent = follow.AlarmChangePercent
stockData.AlarmPrice = follow.AlarmPrice
stockData.Groups = follow.Groups
//当前价格
price, _ := convertor.ToFloat(stockData.Price)
@ -302,14 +596,14 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
//开盘价
//openPrice, _ := convertor.ToFloat(stockData.Open)
if price > 0 {
if price > 0 && preClosePrice > 0 {
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
}
if highPrice > 0 {
if highPrice > 0 && preClosePrice > 0 {
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
}
if lowPrice > 0 {
if lowPrice > 0 && preClosePrice > 0 {
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
}
if follow.CostPrice > 0 && follow.Volume > 0 {
@ -321,7 +615,8 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
//未开盘时当前价格为昨日收盘价
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
// 未开盘时,今日盈亏为 0
stockData.ProfitAmountToday = 0
}
}
@ -356,15 +651,19 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) {
logger.SugaredLogger.Debugf("dialog:%s", dialog)
if dialog == "No" {
return true
} else {
systray.Quit()
a.cron.Stop()
return false
}
return false
}
// shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) {
defer PanicHandler()
// Perform your teardown here
systray.Quit()
//os.Exit(0)
logger.SugaredLogger.Infof("application shutdown Version:%s", Version)
}
// Greet returns a greeting for the given name
@ -374,7 +673,7 @@ func (a *App) Greet(stockCode string) *data.StockInfo {
follow := &data.FollowedStock{
StockCode: stockCode,
}
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
db.Dao.Model(follow).Where("stock_code = ?", stockCode).Preload("Groups").Preload("Groups.GroupInfo").First(follow)
stockInfo := getStockInfo(*follow)
return stockInfo
}
@ -387,8 +686,8 @@ 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) GetFollowList(groupId int) *[]data.FollowedStock {
return data.NewStockDataApi().GetFollowList(groupId)
}
func (a *App) GetStockList(key string) []data.StockBasic {
@ -421,8 +720,19 @@ func (a *App) SendDingDingMessage(message string, stockCode string) string {
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
if strutil.HasPrefixAny(stockCode, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) {
return "非A股交易时间"
}
if strutil.HasPrefixAny(stockCode, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) {
return "非港股交易时间"
}
if strutil.HasPrefixAny(stockCode, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) {
return "非美股交易时间"
}
ttl, _ := a.cache.TTL([]byte(stockCode))
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
//logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
if ttl > 0 {
return ""
}
@ -437,8 +747,8 @@ func (a *App) SendDingDingMessageByType(message string, stockCode string, msgTyp
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) NewChatStream(stock, stockCode, question string) {
msgs := data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question)
func (a *App) NewChatStream(stock, stockCode, question string, sysPromptId *int) {
msgs := data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId)
for msg := range msgs {
runtime.EventsEmit(a.ctx, "newChatStream", msg)
}
@ -547,46 +857,62 @@ func getMsgTypeName(msgType int) string {
func onExit(a *App) {
// 清理操作
logger.SugaredLogger.Infof("onExit")
runtime.Quit(a.ctx)
logger.SugaredLogger.Infof("systray onExit")
//systray.Quit()
//runtime.Quit(a.ctx)
}
func onReady(a *App) {
// 初始化操作
logger.SugaredLogger.Infof("onReady")
logger.SugaredLogger.Infof("systray onReady")
systray.SetIcon(icon2)
systray.SetTitle("go-stock")
systray.SetTooltip("go-stock 股票行情实时获取")
// 创建菜单项
show := systray.AddMenuItem("显示", "显示应用程序")
show.Click(func() {
//logger.SugaredLogger.Infof("显示应用程序")
runtime.WindowShow(a.ctx)
})
hide := systray.AddMenuItem("隐藏", "隐藏应用程序")
hide.Click(func() {
//logger.SugaredLogger.Infof("隐藏应用程序")
runtime.WindowHide(a.ctx)
})
systray.AddSeparator()
mQuitOrig := systray.AddMenuItem("退出", "退出应用程序")
// 监听菜单项点击事件
go func() {
for {
select {
case <-mQuitOrig.ClickedCh:
logger.SugaredLogger.Infof("退出应用程序")
runtime.Quit(a.ctx)
//systray.Quit()
case <-show.ClickedCh:
logger.SugaredLogger.Infof("显示应用程序")
runtime.WindowShow(a.ctx)
//runtime.WindowShow(a.ctx)
case <-hide.ClickedCh:
logger.SugaredLogger.Infof("隐藏应用程序")
runtime.WindowHide(a.ctx)
}
}
}()
mQuitOrig.Click(func() {
//logger.SugaredLogger.Infof("退出应用程序")
runtime.Quit(a.ctx)
})
systray.SetOnRClick(func(menu systray.IMenu) {
menu.ShowMenu()
//logger.SugaredLogger.Infof("SetOnRClick")
})
systray.SetOnClick(func(menu systray.IMenu) {
//logger.SugaredLogger.Infof("SetOnClick")
menu.ShowMenu()
})
systray.SetOnDClick(func(menu systray.IMenu) {
menu.ShowMenu()
//logger.SugaredLogger.Infof("SetOnDClick")
})
}
func (a *App) UpdateConfig(settings *data.Settings) string {
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
//logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
if settings.RefreshInterval > 0 {
if entryID, exists := a.cronEntrys["MonitorStockPrices"]; exists {
a.cron.Remove(entryID)
}
id, _ := a.cron.AddFunc(fmt.Sprintf("@every %ds", settings.RefreshInterval), func() {
//logger.SugaredLogger.Infof("MonitorStockPrices:%s", time.Now())
MonitorStockPrices(a)
})
a.cronEntrys["MonitorStockPrices"] = id
}
return data.NewSettingsApi(settings).UpdateConfig()
}
@ -612,3 +938,217 @@ func (a *App) ExportConfig() string {
}
return "导出成功:" + file
}
func getScreenResolution() (int, int, error) {
//user32 := syscall.NewLazyDLL("user32.dll")
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
//
//width, _, _ := getSystemMetrics.Call(0)
//height, _, _ := getSystemMetrics.Call(1)
return int(1366), int(768), nil
}
func (a *App) ShareAnalysis(stockCode, stockName string) string {
//http://go-stock.sparkmemory.top:16688/upload
res := data.NewDeepSeekOpenAi(a.ctx).GetAIResponseResult(stockCode)
if res != nil && len(res.Content) > 100 {
analysisTime := res.CreatedAt.Format("2006/01/02")
logger.SugaredLogger.Infof("%s analysisTime:%s", res.CreatedAt, analysisTime)
response, err := resty.New().SetHeader("ua-x", "go-stock").R().SetFormData(map[string]string{
"text": res.Content,
"stockCode": stockCode,
"stockName": stockName,
"analysisTime": analysisTime,
}).Post("http://go-stock.sparkmemory.top:16688/upload")
if err != nil {
return err.Error()
}
return response.String()
} else {
return "分析结果异常"
}
}
func (a *App) GetfundList(key string) []data.FundBasic {
return data.NewFundApi().GetFundList(key)
}
func (a *App) GetFollowedFund() []data.FollowedFund {
return data.NewFundApi().GetFollowedFund()
}
func (a *App) FollowFund(fundCode string) string {
return data.NewFundApi().FollowFund(fundCode)
}
func (a *App) UnFollowFund(fundCode string) string {
return data.NewFundApi().UnFollowFund(fundCode)
}
func (a *App) SaveAsMarkdown(stockCode, stockName string) string {
res := data.NewDeepSeekOpenAi(a.ctx).GetAIResponseResult(stockCode)
if res != nil && len(res.Content) > 100 {
analysisTime := res.CreatedAt.Format("2006-01-02_15_04_05")
file, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "保存为Markdown",
DefaultFilename: fmt.Sprintf("%s[%s]AI分析结果_%s.md", stockName, stockCode, analysisTime),
Filters: []runtime.FileFilter{
{
DisplayName: "Markdown",
Pattern: "*.md;*.markdown",
},
},
})
if err != nil {
return err.Error()
}
err = os.WriteFile(file, []byte(res.Content), 0644)
return "已保存至:" + file
}
return "分析结果异常,无法保存。"
}
func (a *App) GetPromptTemplates(name, promptType string) *[]models.PromptTemplate {
return data.NewPromptTemplateApi().GetPromptTemplates(name, promptType)
}
func (a *App) AddPrompt(prompt models.Prompt) string {
promptTemplate := models.PromptTemplate{
ID: prompt.ID,
Content: prompt.Content,
Name: prompt.Name,
Type: prompt.Type,
}
return data.NewPromptTemplateApi().AddPrompt(promptTemplate)
}
func (a *App) DelPrompt(id uint) string {
return data.NewPromptTemplateApi().DelPrompt(id)
}
func (a *App) SetStockAICron(cronText, stockCode string) {
data.NewStockDataApi().SetStockAICron(cronText, stockCode)
if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
stockCode = strings.ToUpper(stockCode)
stockCode = strings.Replace(stockCode, "gb_", "us", 1)
stockCode = strings.Replace(stockCode, "GB_", "us", 1)
}
if entryID, exists := a.cronEntrys[stockCode]; exists {
a.cron.Remove(entryID)
}
follow := data.NewStockDataApi().GetFollowedStockByStockCode(stockCode)
id, _ := a.cron.AddFunc(cronText, a.AddCronTask(follow))
a.cronEntrys[stockCode] = id
}
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
notification := toast.Notification{
AppID: "go-stock",
Title: "go-stock",
Message: "程序已经在运行了",
Icon: "",
Duration: "short",
Audio: toast.Default,
}
err := notification.Push()
if err != nil {
logger.SugaredLogger.Error(err)
}
time.Sleep(time.Second * 3)
}
func (a *App) AddGroup(group data.Group) string {
ok := data.NewStockGroupApi(db.Dao).AddGroup(group)
if ok {
return "添加成功"
} else {
return "添加失败"
}
}
func (a *App) GetGroupList() []data.Group {
return data.NewStockGroupApi(db.Dao).GetGroupList()
}
func (a *App) GetGroupStockList(groupId int) []data.GroupStock {
return data.NewStockGroupApi(db.Dao).GetGroupStockByGroupId(groupId)
}
func (a *App) AddStockGroup(groupId int, stockCode string) string {
ok := data.NewStockGroupApi(db.Dao).AddStockGroup(groupId, stockCode)
if ok {
return "添加成功"
} else {
return "添加失败"
}
}
func (a *App) RemoveStockGroup(code, name string, groupId int) string {
ok := data.NewStockGroupApi(db.Dao).RemoveStockGroup(code, name, groupId)
if ok {
return "移除成功"
} else {
return "移除失败"
}
}
func (a *App) RemoveGroup(groupId int) string {
ok := data.NewStockGroupApi(db.Dao).RemoveGroup(groupId)
if ok {
return "移除成功"
} else {
return "移除失败"
}
}
func (a *App) GetStockKLine(stockCode, stockName string, days int64) *[]data.KLineData {
return data.NewStockDataApi().GetHK_KLineData(stockCode, "day", days)
}
func (a *App) GetStockMinutePriceLineData(stockCode, stockName string) map[string]any {
res := make(map[string]any, 4)
priceData, date := data.NewStockDataApi().GetStockMinutePriceData(stockCode)
res["priceData"] = priceData
res["date"] = date
res["stockName"] = stockName
res["stockCode"] = stockCode
return res
}
func (a *App) GetStockCommonKLine(stockCode, stockName string, days int64) *[]data.KLineData {
return data.NewStockDataApi().GetCommonKLineData(stockCode, "day", days)
}
func (a *App) GetTelegraphList(source string) *[]*models.Telegraph {
telegraphs := data.NewMarketNewsApi().GetTelegraphList(source)
return telegraphs
}
func (a *App) ReFleshTelegraphList(source string) *[]*models.Telegraph {
data.NewMarketNewsApi().GetNewTelegraph(30)
data.NewMarketNewsApi().GetSinaNews(30)
telegraphs := data.NewMarketNewsApi().GetTelegraphList(source)
return telegraphs
}
func (a *App) GlobalStockIndexes() map[string]any {
return data.NewMarketNewsApi().GlobalStockIndexes(30)
}
func (a *App) SummaryStockNews(question string, sysPromptId *int) {
msgs := data.NewDeepSeekOpenAi(a.ctx).NewSummaryStockNewsStream(question, sysPromptId)
for msg := range msgs {
runtime.EventsEmit(a.ctx, "summaryStockNews", msg)
}
runtime.EventsEmit(a.ctx, "summaryStockNews", "DONE")
}
func (a *App) GetIndustryRank(sort string, cnt int) []any {
res := data.NewMarketNewsApi().GetIndustryRank(sort, cnt)
return res["data"].([]any)
}
func (a *App) GetIndustryMoneyRankSina(fenlei, sort string) []map[string]any {
res := data.NewMarketNewsApi().GetIndustryMoneyRankSina(fenlei, sort)
return res
}
func (a *App) GetMoneyRankSina(sort string) []map[string]any {
res := data.NewMarketNewsApi().GetMoneyRankSina(sort)
return res
}
func (a *App) GetStockMoneyTrendByDay(stockCode string, days int) []map[string]any {
res := data.NewMarketNewsApi().GetStockMoneyTrendByDay(stockCode, days)
slice.Reverse(res)
return res
}

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()
}

View File

@ -279,7 +279,8 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
//未开盘时当前价格为昨日收盘价
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
// 未开盘时,今日盈亏为 0
stockData.ProfitAmountToday = 0
}
}

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

@ -34,7 +34,7 @@ func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string)
}
func (a AlertWindowsApi) SendNotification() bool {
if getConfig().LocalPushEnable == false {
if GetConfig().LocalPushEnable == false {
logger.SugaredLogger.Error("本地推送未开启")
return false
}

View File

@ -31,7 +31,7 @@ func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string)
}
func (a AlertWindowsApi) SendNotification() bool {
if getConfig().LocalPushEnable == false {
if GetConfig().LocalPushEnable == false {
logger.SugaredLogger.Error("本地推送未开启")
return false
}

View File

@ -15,6 +15,7 @@ import (
type CrawlerApi struct {
crawlerCtx context.Context
crawlerBaseInfo CrawlerBaseInfo
pool *BrowserPool
}
func (c *CrawlerApi) NewTimeOutCrawler(timeout int, crawlerBaseInfo CrawlerBaseInfo) CrawlerApi {
@ -26,18 +27,26 @@ func (c *CrawlerApi) NewCrawler(ctx context.Context, crawlerBaseInfo CrawlerBase
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, e := checkBrowserOnWindows()
logger.SugaredLogger.Infof("GetHtml path:%s", path)
if e {
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"]),
@ -67,6 +76,7 @@ func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bo
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), // 确保 元素准备好
@ -79,6 +89,7 @@ func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bo
} 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())
@ -88,17 +99,85 @@ func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bo
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, e := checkBrowserOnWindows()
logger.SugaredLogger.Infof("GetHtmlWithActions path:%s", path)
if e {
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"]),
@ -128,6 +207,7 @@ func (c *CrawlerApi) GetHtmlWithActions(actions *[]chromedp.Action, headless boo
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 {
@ -137,6 +217,7 @@ func (c *CrawlerApi) GetHtmlWithActions(actions *[]chromedp.Action, headless boo
} 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 {

View File

@ -2,9 +2,14 @@ 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"
@ -116,3 +121,251 @@ func TestGetHtmlWithActions(t *testing.T) {
}
//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

@ -21,8 +21,8 @@ func NewDingDingAPI() *DingDingAPI {
}
func (DingDingAPI) SendDingDingMessage(message string) string {
if getConfig().DingPushEnable == false {
logger.SugaredLogger.Info("钉钉推送未开启")
if GetConfig().DingPushEnable == false {
//logger.SugaredLogger.Info("钉钉推送未开启")
return "钉钉推送未开启"
}
// 发送钉钉消息
@ -37,11 +37,11 @@ func (DingDingAPI) SendDingDingMessage(message string) string {
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
return "发送钉钉消息成功"
}
func getConfig() *Settings {
func GetConfig() *Settings {
return NewSettingsApi(&Settings{}).GetConfig()
}
func getApiURL() string {
return getConfig().DingRobot
return GetConfig().DingRobot
}
func (DingDingAPI) SendToDingDing(title, message string) 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)
}
}

View File

@ -4,9 +4,11 @@ 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"
@ -33,10 +35,12 @@ type OpenAi struct {
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()
config := GetConfig()
if config.OpenAiEnable {
if config.OpenAiApiTimeOut <= 0 {
config.OpenAiApiTimeOut = 60 * 5
@ -44,6 +48,9 @@ func NewDeepSeekOpenAi(ctx context.Context) *OpenAi {
if config.CrawlTimeOut <= 0 {
config.CrawlTimeOut = 60
}
if config.KDays < 30 {
config.KDays = 120
}
}
return &OpenAi{
ctx: ctx,
@ -56,6 +63,8 @@ func NewDeepSeekOpenAi(ctx context.Context) *OpenAi {
TimeOut: config.OpenAiApiTimeOut,
QuestionTemplate: config.QuestionTemplate,
CrawlTimeOut: config.CrawlTimeOut,
KDays: config.KDays,
BrowserPath: config.BrowserPath,
}
}
@ -89,7 +98,98 @@ type AiResponse struct {
SystemFingerprint string `json:"system_fingerprint"`
}
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[string]any {
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() {
@ -106,57 +206,151 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
}
}()
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": o.Prompt,
"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 == "" {
replaceTemplates := map[string]string{
"{{stockName}}": RemoveAllBlankChar(stock),
"{{stockCode}}": RemoveAllBlankChar(stockCode),
}
followedStock := &FollowedStock{
StockCode: stockCode,
}
db.Dao.Model(&followedStock).Where("stock_code = ?", stockCode).First(followedStock)
if followedStock.CostPrice > 0 {
replaceTemplates["{{costPrice}}"] = fmt.Sprintf("%.2f", followedStock.CostPrice)
}
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", o.Prompt)
logger.SugaredLogger.Infof("User Prompt config:%v", o.QuestionTemplate)
logger.SugaredLogger.Infof("User question:%s", userQuestion)
logger.SugaredLogger.Infof("Prompt%s", sysPrompt)
logger.SugaredLogger.Infof("final question:%s", question)
wg := &sync.WaitGroup{}
wg.Add(6)
wg.Add(7)
go func() {
defer wg.Done()
endDate := time.Now().Format("20060102")
startDate := time.Now().Add(-time.Hour * 24 * 365).Format("20060102")
K := NewTushareApi(getConfig()).GetDaily(ConvertStockCodeToTushareCode(stockCode), startDate, endDate, o.CrawlTimeOut)
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": stock + "日K数据如下\n" + K,
"content": "市场指数情况如下:\n" + market.String(),
})
}()
go func() {
defer wg.Done()
messages := SearchStockPriceInfo(stockCode, o.CrawlTimeOut)
//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>"
@ -173,9 +367,15 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
price += message + ";"
}
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": stock + "当前价格:" + price,
"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() {
@ -184,8 +384,7 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
if checkIsIndexBasic(stock) {
return
}
messages := GetFinancialReports(stockCode, o.CrawlTimeOut)
messages := GetFinancialReportsByXUEQIU(stockCode, o.CrawlTimeOut)
if messages == nil || len(*messages) == 0 {
logger.SugaredLogger.Error("获取股票财报失败")
// "***❗获取股票财报失败,分析结果可能不准确***<hr>"
@ -197,6 +396,10 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
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",
@ -214,12 +417,19 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取市场资讯失败,分析结果可能不准确")
return
}
var messageText strings.Builder
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": message,
})
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("获取新闻资讯失败")
@ -227,12 +437,18 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取新闻资讯失败,分析结果可能不准确")
return
}
var newsText strings.Builder
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": message,
})
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() {
@ -246,7 +462,7 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
// }
// for _, message := range *messages {
// msg = append(msg, map[string]interface{}{
// "role": "assistant",
// "role": "user",
// "content": message,
// })
// }
@ -260,12 +476,18 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票电报资讯失败,分析结果可能不准确")
return
}
var newsText strings.Builder
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": message,
})
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() {
@ -282,12 +504,18 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股势通资讯失败,分析结果可能不准确")
return
}
var newsText strings.Builder
for _, message := range *messages {
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": message,
})
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()
@ -295,126 +523,139 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
"role": "user",
"content": question,
})
client := resty.New()
client.SetBaseURL(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": msg,
}).
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(),
//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
}
return
}
scanner := bufio.NewScanner(body)
for scanner.Scan() {
line := scanner.Text()
logger.SugaredLogger.Infof("Received data: %s", line)
if strings.HasPrefix(line, "data: ") {
data := strings.TrimPrefix(line, "data: ")
if data == "[DONE]" {
return
}
var streamResponse struct {
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"`
}
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,
}
logger.SugaredLogger.Infof("Content data: %s", content)
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),
}
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
//ch <- reasoningContent
ch <- map[string]any{
"code": 1,
"question": question,
"chatId": streamResponse.Id,
"model": streamResponse.Model,
"content": reasoningContent,
}
logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
}
if choice.FinishReason == "stop" {
return
}
//logger.SugaredLogger.Infof("Content data: %s", content)
}
} else {
if err != nil {
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
//ch <- err.Error()
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
//ch <- reasoningContent
ch <- map[string]any{
"code": 0,
"code": 1,
"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,
"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 strutil.RemoveNonPrintable(line) != "" {
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": line,
"content": res.Message,
}
logger.SugaredLogger.Infof("Stream data error : %s", line)
}
}
}
}()
return ch
}
}
func checkIsIndexBasic(stock string) bool {
@ -434,7 +675,18 @@ func SearchGuShiTongStockInfo(stock string, crawlTimeOut int64) *[]string {
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)
logger.SugaredLogger.Infof("SearchGuShiTongStockInfo搜索股票-%s: %s", stock, url)
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"),
@ -455,98 +707,89 @@ func SearchGuShiTongStockInfo(stock string, crawlTimeOut int64) *[]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("SearchGuShiTongStockInfo搜索到消息-%s: %s", "", text)
})
logger.SugaredLogger.Infof("messages:%d", len(messages))
//logger.SugaredLogger.Infof("messages:%d", len(messages))
}
return &messages
}
func GetFinancialReports(stockCode string, crawlTimeOut int64) *[]string {
// 创建一个 chromedp 上下文
timeoutCtx, timeoutCtxCancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
defer timeoutCtxCancel()
var ctx context.Context
var cancel context.CancelFunc
path, e := checkBrowserOnWindows()
logger.SugaredLogger.Infof("GetFinancialReports path:%s", path)
if e {
pctx, pcancel := chromedp.NewExecAllocator(
timeoutCtx,
chromedp.ExecPath(path),
chromedp.Flag("headless", true),
chromedp.Flag("disable-javascript", false),
chromedp.Flag("disable-gpu", true),
chromedp.UserAgent("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"),
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),
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
)
} else {
ctx, cancel = chromedp.NewContext(
timeoutCtx,
chromedp.WithLogf(logger.SugaredLogger.Infof),
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
)
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_", "")
}
defer cancel()
var htmlContent string
url := fmt.Sprintf("https://xueqiu.com/snowman/S/%s/detail#/ZYCWZB", stockCode)
err := chromedp.Run(ctx,
chromedp.Navigate(url),
// 等待页面加载完成,可以根据需要调整等待时间
chromedp.WaitVisible("table.table", chromedp.ByQuery),
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
)
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())
}
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
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())
return &[]string{}
}
var messages []string
document.Find("table tr").Each(func(i int, selection *goquery.Selection) {
tr := ""
selection.Find("th,td").Each(func(i int, selection *goquery.Selection) {
ret := selection.Find("p").First().Text()
if ret == "" {
ret = selection.Text()
}
text := strutil.RemoveNonPrintable(ret)
tr += text + " "
})
logger.SugaredLogger.Infof("%s", tr+" \n")
messages = append(messages, tr+" \n")
})
return &messages
GetTableMarkdown(document, waitVisible, &markdown)
return &[]string{markdown.String()}
}
func GetTelegraphList(crawlTimeOut int64) *[]string {
@ -565,7 +808,7 @@ func GetTelegraphList(crawlTimeOut int64) *[]string {
}
var telegraph []string
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
logger.SugaredLogger.Info(selection.Text())
//logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
})
return &telegraph
@ -587,7 +830,7 @@ func GetTopNewsList(crawlTimeOut int64) *[]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())
//logger.SugaredLogger.Info(selection.Text())
telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
})
return &telegraph
@ -606,6 +849,6 @@ func (o OpenAi) SaveAIResponseResult(stockCode, stockName, result, chatId, quest
func (o OpenAi) GetAIResponseResult(stock string) *models.AIResponseResult {
var result models.AIResponseResult
db.Dao.Where("stock_code = ?", stock).Order("id desc").Limit(1).First(&result)
db.Dao.Where("stock_code = ?", stock).Order("id desc").Limit(1).Find(&result)
return &result
}

View File

@ -9,7 +9,7 @@ import (
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
db.Init("../../data/stock.db")
ai := NewDeepSeekOpenAi(context.TODO())
res := ai.NewChatStream("北京文化", "sz000802", "")
res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
for {
select {
case msg := <-res:
@ -19,5 +19,14 @@ func TestNewDeepSeekOpenAiConfig(t *testing.T) {
}
func TestGetTopNewsList(t *testing.T) {
GetTopNewsList(30)
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

@ -27,6 +27,14 @@ type Settings struct {
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 {
@ -65,6 +73,13 @@ func (s SettingsApi) UpdateConfig() string {
"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)
@ -86,6 +101,13 @@ func (s SettingsApi) UpdateConfig() string {
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 "保存成功!"
@ -93,6 +115,24 @@ func (s SettingsApi) UpdateConfig() string {
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
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,17 @@
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"
@ -20,16 +23,33 @@ import (
//-----------------------------------------------------------------------------------
func TestGetTelegraph(t *testing.T) {
GetTelegraphList(30)
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) {
GetFinancialReports("sz000802", 30)
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)
messages := SearchStockInfo("谷歌", "telegram", 30)
for _, message := range *messages {
logger.SugaredLogger.Info(message)
}
@ -37,11 +57,100 @@ func TestGetTelegraphSearch(t *testing.T) {
//https://www.cls.cn/stock?code=sh600745
}
func TestSearchStockInfoByCode(t *testing.T) {
db.Init("../../data/stock.db")
SearchStockInfoByCode("sh600745")
}
func TestSearchStockPriceInfo(t *testing.T) {
SearchStockPriceInfo("sh600745", 30)
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) {
@ -49,7 +158,7 @@ func TestParseFullSingleStockData(t *testing.T) {
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(), "sh600859,sh600745"))
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())
}
@ -57,13 +166,24 @@ func TestParseFullSingleStockData(t *testing.T) {
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()
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745")
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745", "gb_tsla", "hk09660", "hk00700")
for _, data := range *datas {
t.Log(data)
}
@ -124,7 +244,7 @@ func TestReadFile(t *testing.T) {
func TestFollowedList(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
stockDataApi.GetFollowList()
stockDataApi.GetFollowList(1)
}

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

@ -3,8 +3,10 @@ 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"
)
@ -27,16 +29,19 @@ func NewTushareApi(config *Settings) *TushareApi {
// 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)
//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: "daily",
ApiName: stockType,
Token: receiver.config.TushareToken,
Params: map[string]any{
"ts_code": tsCode,
"ts_code": tsCodeNEW,
"start_date": startDate,
"end_date": endDate,
},
@ -61,6 +66,28 @@ func (receiver TushareApi) GetDaily(tsCode, startDate, endDate string, crawlTime
res += t + "\n"
}
}
logger.SugaredLogger.Debugf("tushare response: %s", res)
//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

@ -11,8 +11,19 @@ import (
// -----------------------------------------------------------------------------------
func TestGetDaily(t *testing.T) {
db.Init("../../data/stock.db")
tushareApi := NewTushareApi(getConfig())
res := tushareApi.GetDaily("000802.SZ", "20250101", "20250217", 30)
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)
//
}

View File

@ -1,6 +1,8 @@
package data
import (
"github.com/PuerkitoBio/goquery"
"go-stock/backend/logger"
"regexp"
"strings"
)
@ -17,7 +19,7 @@ var SensitiveWords = strings.Split("戊边、戍边、戌边、边防、李鹏
func ReplaceSensitiveWords(text string) string {
for _, word := range SensitiveWords {
if strings.Contains(text, word) {
text = strings.ReplaceAll(text, word, "*")
text = strings.ReplaceAll(text, word, "")
}
}
return text
@ -57,3 +59,53 @@ func ConvertTushareCodeToStockCode(stockCode string) string {
stockCode = strings.ToLower(RemoveAllDigitChar(stockCode)) + RemoveAllNonDigitChar(stockCode)
return strings.ReplaceAll(stockCode, ".", "")
}
func GetTableMarkdown(document *goquery.Document, waitVisible string, markdown *strings.Builder) {
document.Find(waitVisible).First().Find("tr").Each(func(index int, item *goquery.Selection) {
row := ""
item.Find("th, td").Each(func(i int, cell *goquery.Selection) {
text := cell.Children().FilterFunction(func(i int, s *goquery.Selection) bool {
return isVisible(s)
}).Text()
if text == "" {
text = cell.Text()
}
row += "|" + text
})
row += "|"
if index == 0 {
// Header row
markdown.WriteString(row + "\n")
// Separator row
separator := ""
item.Find("th, td").Each(func(i int, cell *goquery.Selection) {
separator += "|---"
})
markdown.WriteString(separator + "|\n")
} else {
// Data row
markdown.WriteString(row + "\n")
}
})
logger.SugaredLogger.Infof("\n%s", markdown.String())
}
// isVisible 函数用于判断元素是否可见
func isVisible(s *goquery.Selection) bool {
// 检查 display 属性
display, _ := s.Attr("style")
if strings.Contains(strings.ToLower(display), "display: none") {
return false
}
// 检查 visibility 属性
if strings.Contains(strings.ToLower(display), "visibility: hidden") {
return false
}
// 检查 opacity 属性
if strings.Contains(strings.ToLower(display), "opacity: 0") {
return false
}
return true
}

View File

@ -1,7 +1,9 @@
package data
import (
"github.com/duke-git/lancet/v2/slice"
"go-stock/backend/logger"
"os"
"testing"
)
@ -40,4 +42,6 @@ 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)
}

View File

@ -162,3 +162,203 @@ type VersionInfo struct {
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)
}

15192
build/hk.json Normal file

File diff suppressed because it is too large Load Diff

BIN
build/screenshot/img13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 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

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

File diff suppressed because it is too large Load Diff

View File

@ -9,17 +9,33 @@
"preview": "vite preview"
},
"dependencies": {
"@vicons/ionicons5": "^0.13.0",
"@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",
"md-editor-v3": "^5.2.1",
"lodash": "^4.17.21",
"md-editor-v3": "^5.2.3",
"vue": "^3.2.25",
"vue-router": "^4.5.0"
"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",
"html-docx-js-typescript": "^0.1.5",
"naive-ui": "^2.41.0",
"vfonts": "^0.0.3",
"vite": "^5.4.12"
"vite": "^6.3.5"
},
"keywords": [
"AI赋能股票分析",

View File

@ -1 +1 @@
3234b0467a05bbf094762aef81a1e9b0
2d63c3a999d797889c01d6c96451b197

View File

@ -1,30 +1,56 @@
<script setup>
import stockInfo from './components/stock.vue'
import {
EventsEmit,
EventsOff,
EventsOn,
Quit,
WindowFullscreen, WindowGetPosition,
WindowFullscreen,
WindowHide,
WindowIsFullscreen, WindowSetPosition,
WindowUnfullscreen
} from '../wailsjs/runtime'
import {h, ref} from "vue";
import { RouterLink } from 'vue-router'
import {darkTheme, NIcon, NText,} from 'naive-ui'
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 {
SettingsOutline,
AlarmOutline,
AnalyticsOutline,
BarChartSharp, Bonfire, BonfireOutline, EaselSharp,
ExpandOutline, Flag,
Flame, FlameSharp, InformationOutline,
LogoGithub,
NewspaperOutline,
NewspaperSharp, Notifications,
PowerOutline, Pulse,
ReorderTwoOutline,
ExpandOutline,
RefreshOutline, PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline,
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('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
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('stock')
const containerRef= ref({})
const realtimeProfit= ref(0)
const telegraph= ref([])
const activeKey = ref('')
const containerRef = ref({})
const realtimeProfit = ref(0)
const telegraph = ref([])
const groupList = ref([])
const menuOptions = ref([
{
label: () =>
@ -33,21 +59,323 @@ const menuOptions = ref([
{
to: {
name: 'stock',
params: {
id: 'zh-CN'
query: {
groupName: '全部',
groupId: 0,
},
params: {},
}
},
{ default: () => '我的自选',}
{default: () => '股票自选',}
),
key: 'stock',
icon: renderIcon(StarOutline),
children:[
children: [
{
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '当日盈亏 '+realtimeProfit.value+"¥"}),
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(WalletOutline),
icon: renderIcon(AlarmOutline),
},
]
},
@ -58,12 +386,10 @@ const menuOptions = ref([
{
to: {
name: 'settings',
params: {
id: 'zh-CN'
}
params: {}
}
},
{ default: () => '设置' }
{default: () => '设置'}
),
key: 'settings',
icon: renderIcon(SettingsOutline),
@ -75,88 +401,107 @@ const menuOptions = ref([
{
to: {
name: 'about',
params: {
id: 'zh-CN'
}
params: {}
}
},
{ default: () => '关于' }
{default: () => '关于'}
),
key: 'about',
icon: renderIcon(LogoGithub),
},
{
label: ()=> h("a", {
label: () => h("a", {
href: '#',
onClick: toggleFullscreen,
title: '全屏 Ctrl+F 退出全屏 Esc',
}, { default: () => isFullscreen.value?'取消全屏':'全屏' }),
}, {default: () => isFullscreen.value ? '取消全屏' : '全屏'}),
key: 'full',
icon: renderIcon(ExpandOutline),
},
{
label: ()=> h("a", {
label: () => h("a", {
href: '#',
onClick: WindowHide,
title: '隐藏到托盘区 Ctrl+H',
}, { default: () => '隐藏到托盘区' }),
}, {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: 'javascript:void(0)',
style: 'cursor: move;',
onClick: toggleStartMoveWindow,
}, { default: () => '移动' }),
key: 'move',
icon: renderIcon(MoveOutline),
},
{
label: ()=> h("a", {
label: () => h("a", {
href: '#',
onClick: Quit,
}, { default: () => '退出程序' }),
}, {default: () => '退出程序'}),
key: 'exit',
icon: renderIcon(PowerOutline),
},
])
function renderIcon(icon) {
return () => h(NIcon, null, { default: () => h(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)
})
if (isFullscreen.value) {
WindowUnfullscreen()
//e.target.innerHTML = ''
} else {
WindowFullscreen()
// e.target.innerHTML = ''
}
isFullscreen.value = !isFullscreen.value
}
window.addEventListener('mousemove', dragstart)
EventsOn("realtime_profit",(data)=>{
realtimeProfit.value=data
// 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("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) {
@ -171,57 +516,160 @@ window.onerror = function (msg, source, lineno, colno, error) {
});
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-config-provider :theme="darkTheme" ref="containerRef">
<n-message-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 justify="center">
<n-grid x-gap="12" :cols="1">
<n-gi style="position: relative;top:1px;z-index: 19;width: 100%" v-if="telegraph.length>0">
<n-marquee :speed="120" >
<n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
{{item}}
</n-tag>
<!-- <n-text type="warning"> {{telegraph[0]}}</n-text>-->
</n-marquee>
</n-gi>
<n-gi style="padding-bottom: 70px;padding-top: 5px">
<RouterView />
</n-gi>
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%">
<n-card size="small">
<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-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-config-provider>

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

@ -1,14 +1,18 @@
<script setup>
import { MdPreview } from 'md-editor-v3';
// import { MdPreview } from 'md-editor-v3';
// preview.cssstyle.css
import 'md-editor-v3/lib/preview.css';
import {onMounted, ref} from 'vue';
import {GetVersionInfo} from "../../wailsjs/go/main/App";
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) => {
@ -19,6 +23,60 @@ onMounted(() => {
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>
@ -28,10 +86,21 @@ onMounted(() => {
<n-divider title-placement="center">关于软件</n-divider>
<n-space vertical >
<n-image width="100" :src="icon" />
<h1>go-stock <n-tag size="small" round>{{versionInfo}}</n-tag></h1>
<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 />
@ -40,6 +109,8 @@ onMounted(() => {
<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>
@ -48,12 +119,8 @@ onMounted(() => {
<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>
<p>
邮箱<a href="mailto:sparkmemory@163.com">sparkmemory@163.com</a><n-divider vertical />
QQ 506808970<n-divider vertical />
微信ArvinLovegood</p>
<p style="color: #FAA04A">*加微信或者QQ时请备注您的意图(<a href="#support">技术支持</a>功能建议商业咨询等否则会被忽略)</p>
<n-divider vertical />
<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" />
@ -68,6 +135,7 @@ onMounted(() => {
</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 />
@ -82,19 +150,18 @@ onMounted(() => {
</div>
<n-divider title-placement="center">关于版权和技术支持申明</n-divider>
<div style="justify-self: center;text-align: left" >
<p style="color: #FAA04A">如有问题请先查看项目文档如果问题依然存在请优先加群491605333咨询</p>
<p>
本软件仅供学习研究请勿用于非法和商业用途
</p>
<p>
如需商业用途请联系作者微信(备注 商业咨询)ArvinLovegood
如需软件商业授权或定制开发请联系作者微信(备注 商业咨询)ArvinLovegood
</p>
<n-divider/>
<p>
本软件基于开源技术构建使用WailsNaiveUIVue等开源项目技术上如有问题可以先向对应的开源社区请求帮助
</p>
<p>
开源不易本人精力和时间有限如确实需要一对一技术支持请先赞助联系微信(备注 技术支持)ArvinLovegood
开源不易本人精力和时间有限如确实需要一对一技术支持<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>

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

@ -1,9 +1,16 @@
<script setup>
import {onMounted, ref, watch} from "vue";
import {ExportConfig, GetConfig, SendDingDingMessageByType, UpdateConfig} from "../../wailsjs/go/main/App";
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} from "../../wailsjs/go/models";
import {data, models} from "../../wailsjs/go/models";
import {EventsEmit} from "../../wailsjs/runtime";
const message = useMessage()
@ -31,9 +38,16 @@ const formValue = ref({
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
@ -58,14 +72,30 @@ onMounted(()=>{
timeout:res.openAiApiTimeOut,
questionTemplate:res.questionTemplate?res.questionTemplate:'{{stockName}}分析和总结',
crawlTimeOut:res.crawlTimeOut,
kDays:res.kDays,
}
console.log(res)
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,
@ -84,11 +114,20 @@ function saveConfig(){
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)
//console.log("Settings",config)
UpdateConfig(config).then(res=>{
message.success(res)
EventsEmit("updateSettings", config);
})
}
@ -129,7 +168,7 @@ function importConfig(){
let reader = new FileReader();
reader.onload = (e) => {
let config = JSON.parse(e.target.result);
console.log(config)
//console.log(config)
formValue.value.ID = config.ID
formValue.value.tushareToken = config.tushareToken
formValue.value.dingPush = {
@ -152,7 +191,14 @@ function importConfig(){
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);
@ -162,7 +208,7 @@ function importConfig(){
window.onerror = function (event, source, lineno, colno, error) {
console.log(event, source, lineno, colno, error)
//console.log(event, source, lineno, colno, error)
//
EventsEmit("frontendError", {
page: "settings.vue",
@ -175,40 +221,101 @@ window.onerror = function (event, source, lineno, colno, error) {
//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">
<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="default" style="font-size: 25px;font-weight: bold">基础设置</n-text>
<n-text type="success" style="font-size: 25px;font-weight: bold">基础设置</n-text>
</n-gi>
<n-form-item-gi :span="10" label="Tushare api token" path="tushareToken" >
<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="6" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
<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="6" label="数据刷新间隔(重启生效)" path="refreshInterval" >
<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="default" style="font-size: 25px;font-weight: bold">通知设置</n-text>
<n-text type="success" style="font-size: 25px;font-weight: bold">通知设置</n-text>
</n-gi>
<n-form-item-gi :span="6" label="是否启用钉钉推送:" path="dingPush.enable" >
<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="6" label="是否启用本地推送:" path="localPush.enable" >
<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>
@ -217,33 +324,36 @@ window.onerror = function (event, source, lineno, colno, error) {
<n-grid :cols="24" :x-gap="24" style="text-align: left;">
<n-gi :span="24">
<n-text type="default" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
<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-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-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-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-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-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-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="10" v-if="formValue.openAI.enable" label="openAI maxTokens" path="openAI.maxTokens">
<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="22" v-if="formValue.openAI.enable" label="模型系统 Prompt" path="openAI.prompt">
<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"
@ -254,20 +364,23 @@ window.onerror = function (event, source, lineno, colno, error) {
}"
/>
</n-form-item-gi>
<n-form-item-gi :span="22" v-if="formValue.openAI.enable" label="模型用户 Prompt" path="openAI.questionTemplate">
<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: 2,
maxRows: 5
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>
@ -280,7 +393,52 @@ window.onerror = function (event, source, lineno, colno, error) {
</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>

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,6 @@ import router from './router/router'
const app = createApp(App)
app.use(router)
app.use(naive)
app.mount('#app')

View File

@ -1,17 +1,22 @@
import { createMemoryHistory, createRouter } from 'vue-router'
import {createMemoryHistory, createRouter, createWebHashHistory, createWebHistory} from 'vue-router'
import stockView from '../components/stock.vue'
import settingsView from '../components/settings.vue'
import about from "../components/about.vue";
import fundView from "../components/fund.vue";
import market from "../components/market.vue";
const routes = [
{ path: '/', component: stockView,name: 'stock' },
{ path: '/settings/:id', component: settingsView,name: 'settings' },
{ path: '/', component: stockView,name: 'stock'},
{ path: '/fund', component: fundView,name: 'fund' },
{ path: '/settings', component: settingsView,name: 'settings' },
{ path: '/about', component: about,name: 'about' },
{ path: '/market', component: market,name: 'market' },
]
const router = createRouter({
history: createMemoryHistory(),
history: createWebHistory(),
routes,
})

View File

@ -1,12 +1,12 @@
html {
background-color: rgba(27, 38, 54, 1);
/*background-color: rgba(27, 38, 54, 1);*/
text-align: center;
color: white;
/*color: white;*/
}
body {
margin: 0;
color: white;
/*color: white;*/
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;

View File

@ -1,28 +1,100 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {models} from '../models';
import {data} from '../models';
import {models} from '../models';
export function AddCronTask(arg1:data.FollowedStock):Promise<any>;
export function AddGroup(arg1:data.Group):Promise<string>;
export function AddPrompt(arg1:models.Prompt):Promise<string>;
export function AddStockGroup(arg1:number,arg2:string):Promise<string>;
export function AnalyzeSentiment(arg1:string):Promise<data.SentimentResult>;
export function CheckUpdate():Promise<void>;
export function ClsCalendar():Promise<Array<any>>;
export function DelPrompt(arg1:number):Promise<string>;
export function EMDictCode(arg1:string):Promise<Array<any>>;
export function ExportConfig():Promise<string>;
export function Follow(arg1:string):Promise<string>;
export function FollowFund(arg1:string):Promise<string>;
export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult>;
export function GetConfig():Promise<data.Settings>;
export function GetFollowList():Promise<Array<data.FollowedStock>>;
export function GetFollowList(arg1:number):Promise<any>;
export function GetFollowedFund():Promise<Array<data.FollowedFund>>;
export function GetGroupList():Promise<Array<data.Group>>;
export function GetGroupStockList(arg1:number):Promise<Array<data.GroupStock>>;
export function GetIndustryMoneyRankSina(arg1:string,arg2:string):Promise<Array<Record<string, any>>>;
export function GetIndustryRank(arg1:string,arg2:number):Promise<Array<any>>;
export function GetMoneyRankSina(arg1:string):Promise<Array<Record<string, any>>>;
export function GetPromptTemplates(arg1:string,arg2:string):Promise<any>;
export function GetStockCommonKLine(arg1:string,arg2:string,arg3:number):Promise<any>;
export function GetStockKLine(arg1:string,arg2:string,arg3:number):Promise<any>;
export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
export function GetStockMinutePriceLineData(arg1:string,arg2:string):Promise<Record<string, any>>;
export function GetStockMoneyTrendByDay(arg1:string,arg2:number):Promise<Array<Record<string, any>>>;
export function GetTelegraphList(arg1:string):Promise<any>;
export function GetVersionInfo():Promise<models.VersionInfo>;
export function GetfundList(arg1:string):Promise<Array<data.FundBasic>>;
export function GlobalStockIndexes():Promise<Record<string, any>>;
export function Greet(arg1:string):Promise<data.StockInfo>;
export function NewChatStream(arg1:string,arg2:string,arg3:string):Promise<void>;
export function HotEvent(arg1:number):Promise<any>;
export function HotStock(arg1:string):Promise<any>;
export function HotTopic(arg1:number):Promise<Array<any>>;
export function IndustryResearchReport(arg1:string):Promise<Array<any>>;
export function InvestCalendarTimeLine(arg1:string):Promise<Array<any>>;
export function LongTigerRank(arg1:string):Promise<any>;
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any):Promise<void>;
export function NewsPush(arg1:any):Promise<void>;
export function ReFleshTelegraphList(arg1:string):Promise<any>;
export function RemoveGroup(arg1:number):Promise<string>;
export function RemoveStockGroup(arg1:string,arg2:string,arg3:number):Promise<string>;
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;
export function SaveAsMarkdown(arg1:string,arg2:string):Promise<string>;
export function SearchStock(arg1:string):Promise<Record<string, any>>;
export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>;
export function SendDingDingMessageByType(arg1:string,arg2:string,arg3:number):Promise<string>;
@ -31,8 +103,20 @@ export function SetAlarmChangePercent(arg1:number,arg2:number,arg3:string):Promi
export function SetCostPriceAndVolume(arg1:string,arg2:number,arg3:number):Promise<string>;
export function SetStockAICron(arg1:string,arg2:string):Promise<void>;
export function SetStockSort(arg1:number,arg2:string):Promise<void>;
export function ShareAnalysis(arg1:string,arg2:string):Promise<string>;
export function StockNotice(arg1:string):Promise<Array<any>>;
export function StockResearchReport(arg1:string):Promise<Array<any>>;
export function SummaryStockNews(arg1:string,arg2:any):Promise<void>;
export function UnFollow(arg1:string):Promise<string>;
export function UnFollowFund(arg1:string):Promise<string>;
export function UpdateConfig(arg1:data.Settings):Promise<string>;

View File

@ -2,6 +2,42 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function AddCronTask(arg1) {
return window['go']['main']['App']['AddCronTask'](arg1);
}
export function AddGroup(arg1) {
return window['go']['main']['App']['AddGroup'](arg1);
}
export function AddPrompt(arg1) {
return window['go']['main']['App']['AddPrompt'](arg1);
}
export function AddStockGroup(arg1, arg2) {
return window['go']['main']['App']['AddStockGroup'](arg1, arg2);
}
export function AnalyzeSentiment(arg1) {
return window['go']['main']['App']['AnalyzeSentiment'](arg1);
}
export function CheckUpdate() {
return window['go']['main']['App']['CheckUpdate']();
}
export function ClsCalendar() {
return window['go']['main']['App']['ClsCalendar']();
}
export function DelPrompt(arg1) {
return window['go']['main']['App']['DelPrompt'](arg1);
}
export function EMDictCode(arg1) {
return window['go']['main']['App']['EMDictCode'](arg1);
}
export function ExportConfig() {
return window['go']['main']['App']['ExportConfig']();
}
@ -10,6 +46,10 @@ export function Follow(arg1) {
return window['go']['main']['App']['Follow'](arg1);
}
export function FollowFund(arg1) {
return window['go']['main']['App']['FollowFund'](arg1);
}
export function GetAIResponseResult(arg1) {
return window['go']['main']['App']['GetAIResponseResult'](arg1);
}
@ -18,30 +58,134 @@ export function GetConfig() {
return window['go']['main']['App']['GetConfig']();
}
export function GetFollowList() {
return window['go']['main']['App']['GetFollowList']();
export function GetFollowList(arg1) {
return window['go']['main']['App']['GetFollowList'](arg1);
}
export function GetFollowedFund() {
return window['go']['main']['App']['GetFollowedFund']();
}
export function GetGroupList() {
return window['go']['main']['App']['GetGroupList']();
}
export function GetGroupStockList(arg1) {
return window['go']['main']['App']['GetGroupStockList'](arg1);
}
export function GetIndustryMoneyRankSina(arg1, arg2) {
return window['go']['main']['App']['GetIndustryMoneyRankSina'](arg1, arg2);
}
export function GetIndustryRank(arg1, arg2) {
return window['go']['main']['App']['GetIndustryRank'](arg1, arg2);
}
export function GetMoneyRankSina(arg1) {
return window['go']['main']['App']['GetMoneyRankSina'](arg1);
}
export function GetPromptTemplates(arg1, arg2) {
return window['go']['main']['App']['GetPromptTemplates'](arg1, arg2);
}
export function GetStockCommonKLine(arg1, arg2, arg3) {
return window['go']['main']['App']['GetStockCommonKLine'](arg1, arg2, arg3);
}
export function GetStockKLine(arg1, arg2, arg3) {
return window['go']['main']['App']['GetStockKLine'](arg1, arg2, arg3);
}
export function GetStockList(arg1) {
return window['go']['main']['App']['GetStockList'](arg1);
}
export function GetStockMinutePriceLineData(arg1, arg2) {
return window['go']['main']['App']['GetStockMinutePriceLineData'](arg1, arg2);
}
export function GetStockMoneyTrendByDay(arg1, arg2) {
return window['go']['main']['App']['GetStockMoneyTrendByDay'](arg1, arg2);
}
export function GetTelegraphList(arg1) {
return window['go']['main']['App']['GetTelegraphList'](arg1);
}
export function GetVersionInfo() {
return window['go']['main']['App']['GetVersionInfo']();
}
export function GetfundList(arg1) {
return window['go']['main']['App']['GetfundList'](arg1);
}
export function GlobalStockIndexes() {
return window['go']['main']['App']['GlobalStockIndexes']();
}
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
export function NewChatStream(arg1, arg2, arg3) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3);
export function HotEvent(arg1) {
return window['go']['main']['App']['HotEvent'](arg1);
}
export function HotStock(arg1) {
return window['go']['main']['App']['HotStock'](arg1);
}
export function HotTopic(arg1) {
return window['go']['main']['App']['HotTopic'](arg1);
}
export function IndustryResearchReport(arg1) {
return window['go']['main']['App']['IndustryResearchReport'](arg1);
}
export function InvestCalendarTimeLine(arg1) {
return window['go']['main']['App']['InvestCalendarTimeLine'](arg1);
}
export function LongTigerRank(arg1) {
return window['go']['main']['App']['LongTigerRank'](arg1);
}
export function NewChatStream(arg1, arg2, arg3, arg4) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4);
}
export function NewsPush(arg1) {
return window['go']['main']['App']['NewsPush'](arg1);
}
export function ReFleshTelegraphList(arg1) {
return window['go']['main']['App']['ReFleshTelegraphList'](arg1);
}
export function RemoveGroup(arg1) {
return window['go']['main']['App']['RemoveGroup'](arg1);
}
export function RemoveStockGroup(arg1, arg2, arg3) {
return window['go']['main']['App']['RemoveStockGroup'](arg1, arg2, arg3);
}
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5) {
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5);
}
export function SaveAsMarkdown(arg1, arg2) {
return window['go']['main']['App']['SaveAsMarkdown'](arg1, arg2);
}
export function SearchStock(arg1) {
return window['go']['main']['App']['SearchStock'](arg1);
}
export function SendDingDingMessage(arg1, arg2) {
return window['go']['main']['App']['SendDingDingMessage'](arg1, arg2);
}
@ -58,14 +202,38 @@ export function SetCostPriceAndVolume(arg1, arg2, arg3) {
return window['go']['main']['App']['SetCostPriceAndVolume'](arg1, arg2, arg3);
}
export function SetStockAICron(arg1, arg2) {
return window['go']['main']['App']['SetStockAICron'](arg1, arg2);
}
export function SetStockSort(arg1, arg2) {
return window['go']['main']['App']['SetStockSort'](arg1, arg2);
}
export function ShareAnalysis(arg1, arg2) {
return window['go']['main']['App']['ShareAnalysis'](arg1, arg2);
}
export function StockNotice(arg1) {
return window['go']['main']['App']['StockNotice'](arg1);
}
export function StockResearchReport(arg1) {
return window['go']['main']['App']['StockResearchReport'](arg1);
}
export function SummaryStockNews(arg1, arg2) {
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2);
}
export function UnFollow(arg1) {
return window['go']['main']['App']['UnFollow'](arg1);
}
export function UnFollowFund(arg1) {
return window['go']['main']['App']['UnFollowFund'](arg1);
}
export function UpdateConfig(arg1) {
return window['go']['main']['App']['UpdateConfig'](arg1);
}

View File

@ -1,38 +1,70 @@
export namespace data {
export class FollowedStock {
StockCode: string;
Name: string;
Volume: number;
CostPrice: number;
Price: number;
PriceChange: number;
ChangePercent: number;
AlarmChangePercent: number;
AlarmPrice: number;
export class FundBasic {
ID: number;
// Go type: time
Time: any;
Sort: number;
IsDel: number;
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
code: string;
name: string;
fullName: string;
type: string;
establishment: string;
scale: string;
company: string;
manager: string;
rating: string;
trackingTarget: string;
netUnitValue?: number;
netUnitValueDate: string;
netEstimatedUnit?: number;
netEstimatedUnitTime: string;
netAccumulated?: number;
netGrowth1?: number;
netGrowth3?: number;
netGrowth6?: number;
netGrowth12?: number;
netGrowth36?: number;
netGrowth60?: number;
netGrowthYTD?: number;
netGrowthAll?: number;
static createFrom(source: any = {}) {
return new FollowedStock(source);
return new FundBasic(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.StockCode = source["StockCode"];
this.Name = source["Name"];
this.Volume = source["Volume"];
this.CostPrice = source["CostPrice"];
this.Price = source["Price"];
this.PriceChange = source["PriceChange"];
this.ChangePercent = source["ChangePercent"];
this.AlarmChangePercent = source["AlarmChangePercent"];
this.AlarmPrice = source["AlarmPrice"];
this.Time = this.convertValues(source["Time"], null);
this.Sort = source["Sort"];
this.IsDel = source["IsDel"];
this.ID = source["ID"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.code = source["code"];
this.name = source["name"];
this.fullName = source["fullName"];
this.type = source["type"];
this.establishment = source["establishment"];
this.scale = source["scale"];
this.company = source["company"];
this.manager = source["manager"];
this.rating = source["rating"];
this.trackingTarget = source["trackingTarget"];
this.netUnitValue = source["netUnitValue"];
this.netUnitValueDate = source["netUnitValueDate"];
this.netEstimatedUnit = source["netEstimatedUnit"];
this.netEstimatedUnitTime = source["netEstimatedUnitTime"];
this.netAccumulated = source["netAccumulated"];
this.netGrowth1 = source["netGrowth1"];
this.netGrowth3 = source["netGrowth3"];
this.netGrowth6 = source["netGrowth6"];
this.netGrowth12 = source["netGrowth12"];
this.netGrowth36 = source["netGrowth36"];
this.netGrowth60 = source["netGrowth60"];
this.netGrowthYTD = source["netGrowthYTD"];
this.netGrowthAll = source["netGrowthAll"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@ -53,6 +85,231 @@ export namespace data {
return a;
}
}
export class FollowedFund {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
code: string;
name: string;
netUnitValue?: number;
netUnitValueDate: string;
netEstimatedUnit?: number;
netEstimatedUnitTime: string;
netAccumulated?: number;
netEstimatedRate?: number;
fundBasic: FundBasic;
static createFrom(source: any = {}) {
return new FollowedFund(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.code = source["code"];
this.name = source["name"];
this.netUnitValue = source["netUnitValue"];
this.netUnitValueDate = source["netUnitValueDate"];
this.netEstimatedUnit = source["netEstimatedUnit"];
this.netEstimatedUnitTime = source["netEstimatedUnitTime"];
this.netAccumulated = source["netAccumulated"];
this.netEstimatedRate = source["netEstimatedRate"];
this.fundBasic = this.convertValues(source["fundBasic"], FundBasic);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class Group {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
name: string;
sort: number;
static createFrom(source: any = {}) {
return new Group(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.name = source["name"];
this.sort = source["sort"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class GroupStock {
ID: number;
// Go type: time
CreatedAt: any;
// Go type: time
UpdatedAt: any;
// Go type: gorm
DeletedAt: any;
stockCode: string;
groupId: number;
groupInfo: Group;
static createFrom(source: any = {}) {
return new GroupStock(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
this.stockCode = source["stockCode"];
this.groupId = source["groupId"];
this.groupInfo = this.convertValues(source["groupInfo"], Group);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class FollowedStock {
StockCode: string;
Name: string;
Volume: number;
CostPrice: number;
Price: number;
PriceChange: number;
ChangePercent: number;
AlarmChangePercent: number;
AlarmPrice: number;
// Go type: time
Time: any;
Sort: number;
Cron?: string;
IsDel: number;
Groups: GroupStock[];
static createFrom(source: any = {}) {
return new FollowedStock(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.StockCode = source["StockCode"];
this.Name = source["Name"];
this.Volume = source["Volume"];
this.CostPrice = source["CostPrice"];
this.Price = source["Price"];
this.PriceChange = source["PriceChange"];
this.ChangePercent = source["ChangePercent"];
this.AlarmChangePercent = source["AlarmChangePercent"];
this.AlarmPrice = source["AlarmPrice"];
this.Time = this.convertValues(source["Time"], null);
this.Sort = source["Sort"];
this.Cron = source["Cron"];
this.IsDel = source["IsDel"];
this.Groups = this.convertValues(source["Groups"], GroupStock);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class SentimentResult {
Score: number;
Category: number;
PositiveCount: number;
NegativeCount: number;
Description: string;
static createFrom(source: any = {}) {
return new SentimentResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.Score = source["Score"];
this.Category = source["Category"];
this.PositiveCount = source["PositiveCount"];
this.NegativeCount = source["NegativeCount"];
this.Description = source["Description"];
}
}
export class Settings {
ID: number;
// Go type: time
@ -78,6 +335,14 @@ export namespace data {
checkUpdate: boolean;
questionTemplate: string;
crawlTimeOut: number;
kDays: number;
enableDanmu: boolean;
browserPath: string;
enableNews: boolean;
darkTheme: boolean;
browserPoolSize: number;
enableFund: boolean;
enablePushNews: boolean;
static createFrom(source: any = {}) {
return new Settings(source);
@ -106,6 +371,14 @@ export namespace data {
this.checkUpdate = source["checkUpdate"];
this.questionTemplate = source["questionTemplate"];
this.crawlTimeOut = source["crawlTimeOut"];
this.kDays = source["kDays"];
this.enableDanmu = source["enableDanmu"];
this.browserPath = source["browserPath"];
this.enableNews = source["enableNews"];
this.darkTheme = source["darkTheme"];
this.browserPoolSize = source["browserPoolSize"];
this.enableFund = source["enableFund"];
this.enablePushNews = source["enablePushNews"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@ -241,6 +514,9 @@ export namespace data {
"卖四申报": string;
"卖五报价": string;
"卖五申报": string;
"市场": string;
"盘前盘后": string;
"盘前盘后涨跌幅": string;
changePercent: number;
changePrice: number;
highRate: number;
@ -253,6 +529,7 @@ export namespace data {
sort: number;
alarmChangePercent: number;
alarmPrice: number;
Groups: GroupStock[];
static createFrom(source: any = {}) {
return new StockInfo(source);
@ -298,6 +575,9 @@ export namespace data {
this["卖四申报"] = source["卖四申报"];
this["卖五报价"] = source["卖五报价"];
this["卖五申报"] = source["卖五申报"];
this["市场"] = source["市场"];
this["盘前盘后"] = source["盘前盘后"];
this["盘前盘后涨跌幅"] = source["盘前盘后涨跌幅"];
this.changePercent = source["changePercent"];
this.changePrice = source["changePrice"];
this.highRate = source["highRate"];
@ -310,6 +590,7 @@ export namespace data {
this.sort = source["sort"];
this.alarmChangePercent = source["alarmChangePercent"];
this.alarmPrice = source["alarmPrice"];
this.Groups = this.convertValues(source["Groups"], GroupStock);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@ -388,6 +669,24 @@ export namespace models {
return a;
}
}
export class Prompt {
ID: number;
name: string;
content: string;
type: string;
static createFrom(source: any = {}) {
return new Prompt(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.name = source["name"];
this.content = source["content"];
this.type = source["type"];
}
}
export class VersionInfo {
ID: number;
// Go type: time

View File

@ -134,7 +134,7 @@ export function WindowIsFullscreen(): Promise<boolean>;
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
// Sets the width and height of the window.
export function WindowSetSize(width: number, height: number): Promise<Size>;
export function WindowSetSize(width: number, height: number): void;
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
// Gets the width and height of the window.

61
go.mod
View File

@ -1,23 +1,26 @@
module go-stock
go 1.23
toolchain go1.23.0
go 1.23.0
require (
github.com/PuerkitoBio/goquery v1.10.1
github.com/chromedp/chromedp v0.11.2
github.com/coocood/freecache v1.2.4
github.com/duke-git/lancet/v2 v2.3.4
github.com/getlantern/systray v1.2.2
github.com/energye/systray v1.0.2
github.com/glebarez/sqlite v1.11.0
github.com/go-ego/gse v0.80.3
github.com/go-resty/resty/v2 v2.16.2
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/stretchr/testify v1.8.4
github.com/wailsapp/wails/v2 v2.9.2
github.com/robertkrimen/otto v0.5.1
github.com/robfig/cron/v3 v3.0.1
github.com/samber/lo v1.49.1
github.com/stretchr/testify v1.10.0
github.com/tidwall/gjson v1.14.2
github.com/wailsapp/wails/v2 v2.10.1
go.uber.org/zap v1.27.0
golang.org/x/sys v0.28.0
golang.org/x/text v0.21.0
golang.org/x/sys v0.31.0
golang.org/x/text v0.23.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/gorm v1.25.12
gorm.io/plugin/dbresolver v1.5.3
@ -32,50 +35,46 @@ require (
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/labstack/echo/v4 v4.10.2 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
github.com/leaanthony/gosod v1.0.3 // indirect
github.com/labstack/echo/v4 v4.13.3 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/gosod v1.0.4 // indirect
github.com/leaanthony/slicer v1.6.0 // indirect
github.com/leaanthony/u v1.1.0 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/tkrajina/go-reflector v0.5.6 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wailsapp/go-webview2 v1.0.16 // indirect
github.com/vcaesar/cedar v0.20.2 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/net v0.38.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect

148
go.sum
View File

@ -14,39 +14,26 @@ github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipw
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvYJls=
github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
github.com/energye/systray v1.0.2 h1:63R4prQkANtpM2CIA4UrDCuwZFt+FiygG77JYCsNmXc=
github.com/energye/systray v1.0.2/go.mod h1:sp7Q/q/I4/w5ebvpSuJVep71s9Bg7L9ZVp69gBASehM=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ego/gse v0.80.3 h1:YNFkjMhlhQnUeuoFcUEd1ivh6SOB764rT8GDsEbDiEg=
github.com/go-ego/gse v0.80.3/go.mod h1:Gt3A9Ry1Eso2Kza4MRaiZ7f2DTAvActmETY46Lxg0gU=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@ -55,13 +42,14 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@ -72,47 +60,45 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg=
github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -121,29 +107,40 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c h1:coVla7zpsycc+kA9NXpcvv2E4I7+ii6L5hZO2S6C3kw=
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wailsapp/go-webview2 v1.0.16 h1:wffnvnkkLvhRex/aOrA3R7FP7rkvOqL/bir1br7BekU=
github.com/wailsapp/go-webview2 v1.0.16/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
github.com/vcaesar/cedar v0.20.2 h1:TDx7AdZhilKcfE1WvdToTJf5VrC/FXcUOW+KY1upLZ4=
github.com/vcaesar/cedar v0.20.2/go.mod h1:lyuGvALuZZDPNXwpzv/9LyxW+8Y6faN7zauFezNsnik=
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU=
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF2k=
github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
@ -156,8 +153,9 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@ -174,8 +172,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -184,16 +183,11 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -204,8 +198,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -224,10 +219,11 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@ -235,13 +231,13 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=

BIN
img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

234
main.go
View File

@ -1,10 +1,11 @@
package main
import (
"context"
"embed"
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/slice"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
@ -15,11 +16,12 @@ import (
"github.com/wailsapp/wails/v2/pkg/runtime"
"go-stock/backend/data"
"go-stock/backend/db"
log "go-stock/backend/logger"
"go-stock/backend/models"
"log"
"os"
goruntime "runtime"
"runtime/debug"
"strings"
"time"
)
@ -41,6 +43,12 @@ var wxpay []byte
//go:embed build/stock_basic.json
var stocksBin []byte
//go:embed build/stock_base_info_hk.json
var stocksBinHK []byte
//go:embed build/stock_base_info_us.json
var stocksBinUS []byte
//go:generate cp -R ./data ./build/bin
var Version string
@ -49,17 +57,12 @@ var VersionCommit string
func main() {
checkDir("data")
db.Init("")
db.Dao.AutoMigrate(&data.StockInfo{})
db.Dao.AutoMigrate(&data.StockBasic{})
db.Dao.AutoMigrate(&data.FollowedStock{})
db.Dao.AutoMigrate(&data.IndexBasic{})
db.Dao.AutoMigrate(&data.Settings{})
db.Dao.AutoMigrate(&models.AIResponseResult{})
go AutoMigrate()
if stocksBin != nil && len(stocksBin) > 0 {
go initStockData()
}
updateBasicInfo()
//db.Dao.Model(&data.Group{}).Where("id = ?", 0).FirstOrCreate(&data.Group{
// Name: "默认分组",
// Sort: 0,
//})
// Create an instance of the app structure
app := NewApp()
@ -96,24 +99,41 @@ func main() {
//FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
// runtime.Quit(app.ctx)
//})
logger.NewDefaultLogger().Info("version: " + Version)
logger.NewDefaultLogger().Info("commit: " + VersionCommit)
log.SugaredLogger.Info("version: " + Version)
log.SugaredLogger.Info("commit: " + VersionCommit)
// Create application with options
err := wails.Run(&options.App{
Title: "go-stock",
Width: 1366,
Height: 920,
MinWidth: 1024,
MinHeight: 768,
MaxWidth: 1920,
MaxHeight: 960,
//var width, height int
//var err error
//
width, _, err := getScreenResolution()
if err != nil {
log.SugaredLogger.Error("get screen resolution error")
width = 1456
//height = 768
}
darkTheme := data.NewSettingsApi(&data.Settings{}).GetConfig().DarkTheme
backgroundColour := &options.RGBA{R: 255, G: 255, B: 255, A: 1}
if darkTheme {
backgroundColour = &options.RGBA{R: 27, G: 38, B: 54, A: 1}
}
// Create application with options
err = wails.Run(&options.App{
Title: "go-stock",
Width: width * 4 / 5,
Height: 900,
MinWidth: 1456,
MinHeight: 768,
//MaxWidth: width,
//MaxHeight: height,
DisableResize: false,
Fullscreen: false,
Frameless: true,
StartHidden: false,
HideWindowOnClose: false,
EnableDefaultContextMenu: true,
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
BackgroundColour: backgroundColour,
Assets: assets,
Menu: AppMenu,
Logger: nil,
@ -124,6 +144,10 @@ func main() {
OnBeforeClose: app.beforeClose,
OnShutdown: app.shutdown,
WindowStartState: options.Normal,
SingleInstanceLock: &options.SingleInstanceLock{
UniqueId: "go-stock",
OnSecondInstanceLaunch: OnSecondInstanceLaunch,
},
Bind: []interface{}{
app,
},
@ -157,7 +181,80 @@ func main() {
})
if err != nil {
log.Fatal(err)
log.SugaredLogger.Fatal(err)
}
}
func AutoMigrate() {
db.Dao.AutoMigrate(&data.StockInfo{})
db.Dao.AutoMigrate(&data.StockBasic{})
db.Dao.AutoMigrate(&data.FollowedStock{})
db.Dao.AutoMigrate(&data.IndexBasic{})
db.Dao.AutoMigrate(&data.Settings{})
db.Dao.AutoMigrate(&models.AIResponseResult{})
db.Dao.AutoMigrate(&models.StockInfoHK{})
db.Dao.AutoMigrate(&models.StockInfoUS{})
db.Dao.AutoMigrate(&data.FollowedFund{})
db.Dao.AutoMigrate(&data.FundBasic{})
db.Dao.AutoMigrate(&models.PromptTemplate{})
db.Dao.AutoMigrate(&data.Group{})
db.Dao.AutoMigrate(&data.GroupStock{})
db.Dao.AutoMigrate(&models.Tags{})
db.Dao.AutoMigrate(&models.Telegraph{})
db.Dao.AutoMigrate(&models.TelegraphTags{})
db.Dao.AutoMigrate(&models.LongTigerRankData{})
}
func initStockDataUS(ctx context.Context) {
defer func() {
go runtime.EventsEmit(ctx, "loadingMsg", "done")
}()
var v []models.StockInfoUS
err := json.Unmarshal(stocksBinUS, &v)
if err != nil {
log.SugaredLogger.Error(err.Error())
return
}
log.SugaredLogger.Infof("init stock data us %d", len(v))
var total int64
db.Dao.Model(&models.StockInfoUS{}).Count(&total)
if total != int64(len(v)) {
for _, item := range v {
var count int64
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", item.Code).Count(&count)
if count > 0 {
//log.SugaredLogger.Infof("stock data us %s exist", item.Code)
continue
}
db.Dao.Model(&models.StockInfoUS{}).Create(&item)
}
}
}
func initStockDataHK(ctx context.Context) {
defer func() {
go runtime.EventsEmit(ctx, "loadingMsg", "done")
}()
var v []models.StockInfoHK
err := json.Unmarshal(stocksBinHK, &v)
if err != nil {
log.SugaredLogger.Error(err.Error())
return
}
log.SugaredLogger.Infof("init stock data hk %d", len(v))
var total int64
db.Dao.Model(&models.StockInfoHK{}).Count(&total)
if total != int64(len(v)) {
for _, item := range v {
var count int64
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", item.Code).Count(&count)
if count > 0 {
//log.SugaredLogger.Infof("stock data hk %s exist", item.Code)
continue
}
db.Dao.Model(&models.StockInfoHK{}).Create(&item)
}
}
}
@ -171,47 +268,82 @@ func updateBasicInfo() {
}
}
func initStockData() {
var count int64
db.Dao.Model(&data.StockBasic{}).Count(&count)
if count > 0 {
return
}
logger.NewDefaultLogger().Info("init stock data")
func initStockData(ctx context.Context) {
defer func() {
go runtime.EventsEmit(ctx, "loadingMsg", "done")
}()
fields := "ts_code,symbol,name,area,industry,cnspell,market,list_date,act_name,act_ent_type,fullname,exchange,list_status,curr_type,enname,delist_date,is_hs"
log.SugaredLogger.Info("init stock data")
res := &data.TushareStockBasicResponse{}
err := json.Unmarshal(stocksBin, res)
if err != nil {
logger.NewDefaultLogger().Error(err.Error())
log.SugaredLogger.Error(err.Error())
return
}
for _, item := range res.Data.Items {
stock := &data.StockBasic{}
stock.Exchange = convertor.ToString(item[0])
stock.IsHs = convertor.ToString(item[1])
stock.Name = convertor.ToString(item[2])
stock.Industry = convertor.ToString(item[3])
stock.ListStatus = convertor.ToString(item[4])
stock.ActName = convertor.ToString(item[5])
stock.ID = uint(item[6].(float64))
stock.CurrType = convertor.ToString(item[7])
stock.Area = convertor.ToString(item[8])
stock.ListDate = convertor.ToString(item[9])
stock.DelistDate = convertor.ToString(item[10])
stock.ActEntType = convertor.ToString(item[11])
stock.TsCode = convertor.ToString(item[12])
stock.Symbol = convertor.ToString(item[13])
stock.Cnspell = convertor.ToString(item[14])
stock.Fullname = convertor.ToString(item[20])
stock.Ename = convertor.ToString(item[21])
db.Dao.Model(&data.StockBasic{}).FirstOrCreate(stock, &data.StockBasic{TsCode: stock.TsCode}).Updates(stock)
stockData := map[string]any{}
for _, field := range strings.Split(fields, ",") {
//logger.SugaredLogger.Infof("field: %s", field)
idx := slice.IndexOf(res.Data.Fields, field)
if idx == -1 {
continue
}
stockData[field] = item[idx]
}
jsonData, _ := json.Marshal(stockData)
err := json.Unmarshal(jsonData, stock)
if err != nil {
continue
}
stock.ID = 0
var count int64
db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).Count(&count)
if count > 0 {
continue
} else {
db.Dao.Create(stock)
}
//db.Dao.Model(&data.StockBasic{}).FirstOrCreate(stock, &data.StockBasic{TsCode: stock.TsCode}).Where("ts_code = ?", stock.TsCode).Updates(stock)
}
//for _, item := range res.Data.Items {
// stock := &data.StockBasic{}
// stock.Exchange = convertor.ToString(item[0])
// stock.IsHs = convertor.ToString(item[1])
// stock.Name = convertor.ToString(item[2])
// stock.Industry = convertor.ToString(item[3])
// stock.ListStatus = convertor.ToString(item[4])
// stock.ActName = convertor.ToString(item[5])
// stock.ID = uint(item[6].(float64))
// stock.CurrType = convertor.ToString(item[7])
// stock.Area = convertor.ToString(item[8])
// stock.ListDate = convertor.ToString(item[9])
// stock.DelistDate = convertor.ToString(item[10])
// stock.ActEntType = convertor.ToString(item[11])
// stock.TsCode = convertor.ToString(item[12])
// stock.Symbol = convertor.ToString(item[13])
// stock.Cnspell = convertor.ToString(item[14])
// stock.Fullname = convertor.ToString(item[20])
// stock.Ename = convertor.ToString(item[21])
//
// var count int64
// db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).Count(&count)
// if count > 0 {
// continue
// } else {
// db.Dao.Create(stock)
// }
//}
}
func checkDir(dir string) {
_, err := os.Stat(dir)
if os.IsNotExist(err) {
os.Mkdir(dir, os.ModePerm)
logger.NewDefaultLogger().Info("create dir: " + dir)
log.SugaredLogger.Info("create dir: " + dir)
}
}

View File

@ -14,6 +14,6 @@
"productName": "go-stock",
"productVersion": "1.0.0",
"copyright": "Copyright#sparkmemory@163.com",
"comments": "股票行情实时获取,AI赋能分析股票"
"comments": "股票行情实时获取,AI赋能分析股票,支持DeepSeekOpenAI OllamaLMStudioAnythingLLM硅基流动火山方舟阿里云百炼等平台或模型。软件发行版见GitHubhttps://github.com/ArvinLovegood/go-stock"
}
}