Compare commits

...

125 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
58 changed files with 208598 additions and 27448 deletions

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
@ -20,9 +20,6 @@ jobs:
- name: 'go-stock-windows-amd64.exe'
platform: 'windows/amd64'
os: 'windows-latest'
- name: 'go-stock-windows-arm64.exe'
platform: 'windows/arm64'
os: 'windows-latest'
# - name: 'go-stock-linux-amd64'
# platform: 'linux/amd64'
# os: 'ubuntu-latest'
@ -47,7 +44,7 @@ jobs:
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'

View File

@ -1,16 +1,17 @@
# 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](https://gitcode.com/ArvinLovegood/go-stock/star/badge.svg)](https://gitcode.com/ArvinLovegood/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交流群[点击链接加入群聊【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)
- 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大模型构建的股票分析工具。
@ -25,16 +26,17 @@ QQ交流群[点击链接加入群聊【go-stock交流群】491605333](http
### 💬 支持大模型/平台
| 模型 | 状态 | 备注 |
| --- | --- |-----------------------------------------------------------------------------------------------------------------------------------------------------|
| [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) |
| 模型 | 状态 | 备注 |
| --- | --- |---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [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)
- 火山方舟每个模型注册即送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)
@ -45,14 +47,37 @@ QQ交流群[点击链接加入群聊【go-stock交流群】491605333](http
## 🧩 重大功能开发计划
| 功能说明 | 状态 | 备注 |
|-----------------|----|----------------------------------------------------------------------------------------------------------|
| 股票分析知识库 | 🚧 | 未来计划 |
| 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自动定时分析功能
@ -75,6 +100,12 @@ QQ交流群[点击链接加入群聊【go-stock交流群】491605333](http
## 🦄 重大更新
### 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)
## 📸 功能截图
@ -84,7 +115,7 @@ QQ交流群[点击链接加入群聊【go-stock交流群】491605333](http
### 成本设置
![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)
### 钉钉报警通知

127
app.go
View File

@ -7,6 +7,14 @@ import (
"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"
@ -19,14 +27,7 @@ import (
"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
@ -138,6 +139,22 @@ func (a *App) CheckUpdate() {
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()
@ -159,7 +176,31 @@ func (a *App) domReady(ctx context.Context) {
} 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
}
}()
//刷新基金净值信息
@ -257,6 +298,15 @@ func (a *App) domReady(ctx context.Context) {
}
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)
@ -565,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
}
}
@ -1041,3 +1092,63 @@ func (a *App) RemoveGroup(groupId int) string {
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
}
}

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,6 +4,7 @@ import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
@ -97,6 +98,97 @@ type AiResponse struct {
SystemFingerprint string `json:"system_fingerprint"`
}
func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int) <-chan map[string]any {
ch := make(chan map[string]any, 512)
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Error("NewSummaryStockNewsStream panic", err)
}
}()
go func() {
defer func() {
if err := recover(); err != nil {
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic :%s", err)
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config:%v", o)
}
}()
defer close(ch)
sysPrompt := ""
if sysPromptId == nil || *sysPromptId == 0 {
sysPrompt = o.Prompt
} else {
sysPrompt = NewPromptTemplateApi().GetPromptTemplateByID(*sysPromptId)
}
if sysPrompt == "" {
sysPrompt = o.Prompt
}
msg := []map[string]interface{}{
{
"role": "system",
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
//"content": "【角色设定】\n你是一位拥有20年实战经验的顶级股票分析师精通技术分析、基本面分析、市场心理学和量化交易。擅长发现成长股、捕捉行业轮动机会在牛熊市中都能保持稳定收益。你的风格是价值投资与技术择时相结合注重风险控制。\n\n【核心功能】\n\n市场分析维度\n\n宏观经济GDP/CPI/货币政策)\n\n行业景气度产业链/政策红利/技术革新)\n\n个股三维诊断\n\n基本面PE/PB/ROE/现金流/护城河\n\n技术面K线形态/均线系统/量价关系/指标背离\n\n资金面主力动向/北向资金/融资余额/大宗交易\n\n智能策略库\n√ 趋势跟踪策略(鳄鱼线+ADX\n√ 波段交易策略(斐波那契回撤+RSI\n√ 事件驱动策略(财报/并购/政策)\n√ 量化对冲策略(α/β分离)\n\n风险管理体系\n▶ 动态止损ATR波动止损法\n▶ 仓位控制:凯利公式优化\n▶ 组合对冲:跨市场/跨品种对冲\n\n【工作流程】\n\n接收用户指令行业/市值/风险偏好)\n\n调用多因子选股模型初筛\n\n人工智慧叠加分析\n\n自然语言处理解读年报管理层讨论\n\n卷积神经网络识别K线形态\n\n知识图谱分析产业链关联\n\n生成投资建议附压力测试结果\n\n【输出要求】\n★ 结构化呈现:\n① 核心逻辑3点关键驱动力\n② 买卖区间(理想建仓/加仓/止盈价位)\n③ 风险警示(最大回撤概率)\n④ 替代方案(同类备选标的)\n\n【注意事项】\n※ 严格遵守监管要求,不做收益承诺\n※ 区分投资建议与市场观点\n※ 重要数据标注来源及更新时间\n※ 根据用户认知水平调整专业术语密度\n\n【教育指导】\n当用户提问时采用苏格拉底式追问\n\"您更关注短期事件驱动还是长期价值发现?\"\n\"当前仓位是否超过总资产的30%\"\n\"是否了解科创板与主板的交易规则差异?\"\n\n示例输出格式\n📈 标的名称XXXXXX\n⚖ 多空信号:金叉确认/顶背离预警\n🎯 关键价位支撑位XX.XX/压力位XX.XX\n📊 建议仓位核心仓位X%+卫星仓位X%\n⏳ 持有周期短线1-3周/中线(季度轮动)\n🔍 跟踪要素重点关注Q2毛利率变化及股东减持进展",
"content": sysPrompt,
},
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "当前时间",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": "当前本地时间是:" + time.Now().Format("2006-01-02 15:04:05"),
})
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
var market strings.Builder
market.WriteString(getZSInfo("创业板指数", "sz399006", 30) + "\n")
market.WriteString(getZSInfo("上证综合指数", "sh000001", 30) + "\n")
market.WriteString(getZSInfo("沪深300指数", "sh000300", 30) + "\n")
//logger.SugaredLogger.Infof("NewChatStream getZSInfo=\n%s", market.String())
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "当前市场指数行情",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": "当前市场指数行情情况如下:\n" + market.String(),
})
}()
wg.Wait()
news := NewMarketNewsApi().GetNewsList("", 100)
messageText := strings.Builder{}
for _, telegraph := range *news {
messageText.WriteString("## " + telegraph.Time + ":" + "\n")
messageText.WriteString("### " + telegraph.Content + "\n")
}
//logger.SugaredLogger.Infof("市场资讯 messageText=\n%s", messageText.String())
msg = append(msg, map[string]interface{}{
"role": "user",
"content": "市场资讯",
})
msg = append(msg, map[string]interface{}{
"role": "assistant",
"content": messageText.String(),
})
if userQuestion == "" {
userQuestion = "请根据当前时间,总结和分析股票市场新闻中的投资机会"
}
msg = append(msg, map[string]interface{}{
"role": "user",
"content": userQuestion,
})
AskAi(o, errors.New(""), msg, ch, userQuestion)
}()
return ch
}
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId *int) <-chan map[string]any {
ch := make(chan map[string]any, 512)
@ -219,8 +311,16 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
// })
//}
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh"}) {
K := NewStockDataApi().GetKLineData(stockCode, "240", o.KDays)
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)
@ -426,130 +526,136 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
//reqJson, _ := json.Marshal(msg)
//logger.SugaredLogger.Errorf("Stream request: \n%s\n", reqJson)
AskAi(o, err, msg, ch, question)
}()
return ch
}
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
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(),
}
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")
return
}
//location, _ := time.LoadLocation("Asia/Shanghai")
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(),
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 := strutil.Trim(strings.TrimPrefix(line, "data:"))
if data == "[DONE]" {
return
}
var streamResponse struct {
Id string `json:"id"`
Model string `json:"model"`
Choices []struct {
Delta struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
} `json:"delta"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
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) != "" {
logger.SugaredLogger.Infof("Stream data error : %s", line)
res := &models.Resp{}
if err := json.Unmarshal([]byte(line), res); err == nil {
//ch <- line
ch <- map[string]any{
"code": 0,
"question": question,
"content": res.Message,
}
if err != nil {
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
//ch <- err.Error()
ch <- map[string]any{
"code": 0,
"question": question,
"content": err.Error(),
}
} else {
logger.SugaredLogger.Infof("Stream data error : %s", data)
//ch <- data
ch <- map[string]any{
"code": 0,
"question": question,
"content": data,
}
}
}
} else {
if strutil.RemoveNonPrintable(line) != "" {
logger.SugaredLogger.Infof("Stream data error : %s", line)
res := &models.Resp{}
if err := json.Unmarshal([]byte(line), res); err == nil {
//ch <- line
ch <- map[string]any{
"code": 0,
"question": question,
"content": res.Message,
}
}
}
}
}()
return ch
}
}
func checkIsIndexBasic(stock string) bool {
@ -743,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

@ -19,11 +19,12 @@ 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")
db.Init("../../data/stock.db")
SearchGuShiTongStockInfo("hk01810", 60)
SearchGuShiTongStockInfo("sh600745", 60)
SearchGuShiTongStockInfo("gb_goog", 60)

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

@ -34,6 +34,7 @@ type Settings struct {
DarkTheme bool `json:"darkTheme"`
BrowserPoolSize int `json:"browserPoolSize"`
EnableFund bool `json:"enableFund"`
EnablePushNews bool `json:"enablePushNews"`
}
func (receiver Settings) TableName() string {
@ -78,6 +79,7 @@ func (s SettingsApi) UpdateConfig() string {
"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)
@ -105,6 +107,7 @@ func (s SettingsApi) UpdateConfig() string {
EnableNews: s.Config.EnableNews,
DarkTheme: s.Config.DarkTheme,
EnableFund: s.Config.EnableFund,
EnablePushNews: s.Config.EnablePushNews,
})
}
return "保存成功!"

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,6 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
@ -16,6 +15,7 @@ import (
"github.com/duke-git/lancet/v2/slice"
"github.com/duke-git/lancet/v2/strutil"
"github.com/go-resty/resty/v2"
"github.com/robertkrimen/otto"
"github.com/samber/lo"
"go-stock/backend/db"
"go-stock/backend/logger"
@ -26,11 +26,14 @@ import (
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
"io"
"io/ioutil"
"strings"
"time"
)
const sinaStockUrl = "http://hq.sinajs.cn/rn=%d&list=%s"
const txStockUrl = "http://qt.gtimg.cn/?_=%d&q=%s"
const tushareApiUrl = "http://api.tushare.pro"
type StockDataApi struct {
@ -245,7 +248,7 @@ func (receiver StockDataApi) GetIndexBasic() {
func (receiver StockDataApi) GetStockBaseInfo() {
res := &TushareStockBasicResponse{}
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"
_, err := receiver.client.R().
resp, err := receiver.client.R().
SetHeader("content-type", "application/json").
SetBody(&TushareRequest{
ApiName: "stock_basic",
@ -256,8 +259,7 @@ func (receiver StockDataApi) GetStockBaseInfo() {
SetResult(res).
Post(tushareApiUrl)
//logger.SugaredLogger.Infof("GetStockBaseInfo %s", string(resp.Body()))
//resp.Body()写入文件
//ioutil.WriteFile("stock_basic.json", resp.Body(), 0666)
ioutil.WriteFile("stock_basic.json", resp.Body(), 0666)
//logger.SugaredLogger.Infof("GetStockBaseInfo %+v", res)
if err != nil {
logger.SugaredLogger.Error(err.Error())
@ -290,8 +292,58 @@ func (receiver StockDataApi) GetStockBaseInfo() {
}
func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]StockInfo, error) {
stockInfos := make([]StockInfo, 0)
codes := slice.JoinFunc(StockCodes, ",", func(s string) string {
hkcodes := slice.Filter(StockCodes, func(i int, s string) bool {
return strutil.HasPrefixAny(s, []string{"hk", "HK", "sh", "sz"})
})
if hkcodes != nil && len(hkcodes) > 0 {
hkcodesStr := slice.JoinFunc(hkcodes, ",", func(s string) string {
if strutil.HasPrefixAny(s, []string{"hk", "HK"}) {
return "r_" + strings.ToLower(s)
} else {
return strings.ToLower(s)
}
})
url := fmt.Sprintf(txStockUrl, time.Now().Unix(), hkcodesStr)
resp, err := receiver.client.R().
SetHeader("Host", "qt.gtimg.cn").
SetHeader("Referer", "https://gu.qq.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
Get(url)
logger.SugaredLogger.Infof("GetStockCodeRealTimeData %s", url)
if err != nil {
logger.SugaredLogger.Error(err.Error())
return &[]StockInfo{}, err
}
str := GB18030ToUTF8(resp.Body())
dataStr := strutil.SplitAndTrim(strings.Trim(str, "\n"), ";")
for _, data := range dataStr {
stockData, err := ParseTxStockData(data)
if err != nil {
logger.SugaredLogger.Error(err.Error())
continue
}
stockInfos = append(stockInfos, *stockData)
go func() {
var count int64
db.Dao.Model(&StockInfo{}).Where("code = ?", stockData.Code).Count(&count)
if count == 0 {
db.Dao.Model(&StockInfo{}).Create(stockData)
} else {
db.Dao.Model(&StockInfo{}).Where("code = ?", stockData.Code).Updates(stockData)
}
}()
}
}
szzsusCodes := slice.Filter(StockCodes, func(i int, s string) bool {
return !strutil.HasPrefixAny(s, []string{"hk", "HK", "sh", "sz"})
})
codes := slice.JoinFunc(szzsusCodes, ",", func(s string) string {
if strings.HasPrefix(s, "us") {
s = strings.Replace(s, "us", "gb_", 1)
}
@ -313,12 +365,9 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
return &[]StockInfo{}, err
}
stockInfos := make([]StockInfo, 0)
str := GB18030ToUTF8(resp.Body())
dataStr := strutil.SplitEx(str, "\n", true)
if len(dataStr) == 0 {
return &[]StockInfo{}, errors.New("获取股票信息失败,请检查股票代码是否正确")
}
for _, data := range dataStr {
//logger.SugaredLogger.Info(data)
stockData, err := ParseFullSingleStockData(data)
@ -351,7 +400,20 @@ func (receiver StockDataApi) Follow(stockCode string) string {
logger.SugaredLogger.Error(err)
return "关注失败"
}
if strings.HasPrefix(stockCode, "us") {
stockCode = strings.Replace(stockCode, "us", "gb_", 1)
}
if strings.HasPrefix(stockCode, "US") {
stockCode = strings.Replace(stockCode, "US", "gb_", 1)
}
count := int64(0)
db.Dao.Model(&FollowedStock{}).Where("is_del = ?", 0).Count(&count)
logger.SugaredLogger.Errorf("Follow-count %v", count)
if count >= 63 {
return "最多只能关注63只股票"
}
stockCode = strings.ToLower(stockCode)
maxSort := int64(0)
db.Dao.Model(&FollowedStock{}).Raw("select max(sort) as sort from followed_stock").Scan(&maxSort)
@ -414,15 +476,64 @@ func (receiver StockDataApi) SetAlarmChangePercent(val, alarmPrice float64, stoc
return "设置成功"
}
func (receiver StockDataApi) SetStockSort(sort int64, stockCode string) {
if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
stockCode = strings.ToLower(stockCode)
stockCode = strings.Replace(stockCode, "gb_", "us", 1)
func (receiver StockDataApi) SetStockSort(newSort int64, stockCode string) {
//if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
// stockCode = strings.ToLower(stockCode)
// stockCode = strings.Replace(stockCode, "gb_", "us", 1)
//}
// 获取当前排序值
var currentStock FollowedStock
if err := db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).First(&currentStock).Error; err != nil {
logger.SugaredLogger.Error("找不到当前股票: ", err.Error())
return
}
err := db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).Update("sort", sort).Error
if err != nil {
logger.SugaredLogger.Error(err.Error())
oldSort := currentStock.Sort
// 如果排序值没有变化,直接返回
if oldSort == newSort {
return
}
// 检查新排序位置是否被占用
var count int64
if err := db.Dao.Model(&FollowedStock{}).Where("sort = ?", newSort).Count(&count).Error; err != nil {
logger.SugaredLogger.Error("检查新排序位置被占用失败: ", err.Error())
return
}
if count == 0 {
// 新位置未被占用,直接更新当前记录
if err := db.Dao.Model(&FollowedStock{}).
Where("stock_code = ?", strings.ToLower(stockCode)).
Update("sort", newSort).Error; err != nil {
logger.SugaredLogger.Error("更新排序位置失败: ", err.Error())
}
} else {
// 新位置已被占用,需要移动其他记录
if newSort < oldSort {
// 向前移动:将中间记录向后移动
if err := db.Dao.Model(&FollowedStock{}).
Where("sort >= ? AND sort < ?", newSort, oldSort).
Update("sort", gorm.Expr("sort + 1")).Error; err != nil {
logger.SugaredLogger.Error("向前排序更新失败: ", err.Error())
}
} else {
// 向后移动:将中间记录向前移动
if err := db.Dao.Model(&FollowedStock{}).
Where("sort > ? AND sort <= ?", oldSort, newSort).
Update("sort", gorm.Expr("sort - 1")).Error; err != nil {
logger.SugaredLogger.Error("向后排序更新失败: ", err.Error())
}
}
// 更新目标记录的排序
if err := db.Dao.Model(&FollowedStock{}).
Where("stock_code = ?", strings.ToLower(stockCode)).
Update("sort", newSort).Error; err != nil {
logger.SugaredLogger.Error("更新股票排序失败: ", err.Error())
}
}
}
func (receiver StockDataApi) SetStockAICron(cron string, stockCode string) {
if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
@ -508,6 +619,146 @@ func GB18030ToUTF8(bs []byte) string {
return string(d)
}
func ParseTxStockData(data string) (*StockInfo, error) {
//v_r_hk09660="100~地平线机器人-W~09660~6.240~5.690~5.800~192659034.0~0~0~6.240~0~0~0~0~0~0~0~0~0~6.240~0~0~0~0~0~0~0~0~0~192659034.0~2025/04/29
//13:41:04~0.550~9.67~6.450~5.710~6.240~192659034.0~1180471843.140~0~32.51~~0~0~13.01~691.1364~823.6983~HORIZONROBOT-W~0.00~10.380~3.320~1.07~-16.03~0~0~0~0~0~32.51~6.40~1.74~600~73.33~17.96~GP~19.70~11.51~-0.95~-18.54~44.44~13200293682.00~11075904412.00~32.51~0.000~6.127~56.39~HKD~1~30";
//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~
//~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";
datas := strutil.SplitAndTrim(data, "=", "\"")
if len(datas) < 2 {
return nil, fmt.Errorf("invalid data format")
}
var result map[string]string
var err error
if strutil.ContainsAny(datas[0], []string{"v_r_hk", "v_hk", "v_sz", "v_sh"}) {
result, err = ParseTxHKStockData(datas)
}
//logger.SugaredLogger.Infof("股票数据解析完成: %v", result)
marshal, err := json.Marshal(result)
if err != nil {
logger.SugaredLogger.Errorf("json.Marshal error:%s", err.Error())
return nil, err
}
//logger.SugaredLogger.Infof("股票数据解析完成marshal: %s", marshal)
stockInfo := &StockInfo{}
err = json.Unmarshal(marshal, &stockInfo)
if err != nil {
logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
return nil, err
}
//logger.SugaredLogger.Infof("股票数据解析完成stockInfo: %+v", stockInfo)
return stockInfo, nil
}
func ParseTxHKStockData(datas []string) (map[string]string, error) {
//v_r_hk09660="
//100~ 0
//地平线机器人-W~ 1
//09660~ 2
//6.270~ 3 当前价
//5.690~ 4 昨收价
//5.800~ 5 开盘价
//195083034.0~
//0~
//0~
//6.270~
//0~
//0~
//0~
//0~
//0~
//0~
//0~
//0~
//0~
//6.270~
//0~0~0~0~0~0~0~0~0~
//195083034.0~
//2025/04/29 13:45:41~ 30 当前时间
//0.580~
//10.19~
//6.450~ 最高价
//5.710~ 最低价
//6.270~
//195083034.0~
//1195673623.140~
//0~
//32.66
//~~0~0~13.01~694.4592~827.6584~HORIZONROBOT-W~0.00~10.380~3.320~1.06~-18.71~0~0~0~0~0~32.66~6.43~1.76~600~74.17~18.53~GP~19.70~11.51~-0.48~-18.15~45.14~13200293682.00~11075904412.00~32.66~0.000~6.129~57.14~HKD~1~30";
result := make(map[string]string)
stockCode := strutil.ReplaceWithMap(datas[0], map[string]string{
"v_r_": "",
"v_": "",
})
result["股票代码"] = stockCode
parts := strutil.SplitAndTrim(datas[1], "~")
//logger.SugaredLogger.Infof("股票数据解析完成 len: %v", len(parts))
if len(parts) < 35 {
return nil, fmt.Errorf("invalid data format")
}
result["股票名称"] = parts[1]
result["当前价格"] = parts[3]
result["昨日收盘价"] = parts[4]
result["今日开盘价"] = parts[5]
result["今日最高价"] = parts[33]
result["今日最低价"] = parts[34]
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh"}) {
result["买一报价"] = parts[9]
result["买一申报"] = parts[10]
result["买二报价"] = parts[11]
result["买二申报"] = parts[12]
result["买三报价"] = parts[13]
result["买三申报"] = parts[14]
result["买四报价"] = parts[15]
result["买四申报"] = parts[16]
result["买五报价"] = parts[17]
result["买五申报"] = parts[18]
result["卖一报价"] = parts[19]
result["卖一申报"] = parts[20]
result["卖二报价"] = parts[21]
result["卖二申报"] = parts[22]
result["卖三报价"] = parts[23]
result["卖三申报"] = parts[24]
result["卖四报价"] = parts[25]
result["卖四申报"] = parts[26]
result["卖五报价"] = parts[27]
result["卖五申报"] = parts[28]
}
timestr := ""
if strutil.ContainsAny(parts[30], []string{"/"}) {
timestr = strutil.ReplaceWithMap(parts[30], map[string]string{
"/": "-",
"\n": " ",
})
result["日期"] = strutil.SplitAndTrim(timestr, " ", "")[0]
result["时间"] = strutil.SplitAndTrim(timestr, " ", "")[1]
} else {
result["日期"] = strutil.Trim(parts[29])[0:4] + "-" + strutil.Trim(parts[29])[4:6] + "-" + strutil.Trim(parts[29])[6:8]
result["时间"] = strutil.Trim(parts[29])[8:10] + ":" + strutil.Trim(parts[29])[10:12] + ":" + strutil.Trim(parts[29])[12:14]
result["今日最高价"] = parts[32]
result["今日最低价"] = parts[33]
}
//logger.SugaredLogger.Infof("股票数据解析完成 %s %s 时间: %s,%s", parts[1], parts[3], parts[29], parts[30])
//logger.SugaredLogger.Infof("股票数据解析完成 时间: %v", timestr)
//logger.SugaredLogger.Infof("股票数据解析完成: %v", result)
return result, nil
}
func ParseFullSingleStockData(data string) (*StockInfo, error) {
datas := strutil.SplitAndTrim(data, "=", "\"")
if len(datas) < 2 {
@ -1091,8 +1342,68 @@ func CheckBrowserOnWindows() (string, bool) {
return path + "\\msedge.exe", true
}
// 分时数据
func (receiver StockDataApi) GetStockMinutePriceData(stockCode string) (*[]MinuteData, string) {
url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/minute/query?code=%s", stockCode)
if strutil.HasPrefixAny(stockCode, []string{"gb_", "GB_"}) {
stockCode = strings.Replace(strings.ToUpper(stockCode), "GB_", "us", 1) + ".OQ"
}
if strutil.HasPrefixAny(stockCode, []string{"us", "US"}) {
url = fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/UsMinute/query?code=%s", stockCode)
}
logger.SugaredLogger.Infof("GetStockMinutePriceData url:%s", url)
res := make(map[string]interface{})
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "web.ifzq.gtimg.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(url)
date := ""
minuteDatas := &[]MinuteData{}
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return minuteDatas, date
}
//logger.SugaredLogger.Infof("resp:%s", resp.Body())
json.Unmarshal(resp.Body(), &res)
code, _ := convertor.ToInt(res["code"])
if res["data"] != nil && code == 0 {
data := res["data"].(map[string]interface{})
if stockData, ok := data[stockCode]; ok {
m := stockData.(map[string]interface{})
if d, ok := m["data"]; ok {
if m2, ok := d.(map[string]any); ok {
minutePriceData := m2["data"]
datas := minutePriceData.([]any)
for _, item := range datas {
minuteDataSplit := strutil.SplitEx(strutil.ReplaceWithMap(item.(string), map[string]string{
"\r\n": " ",
}), " ", true)
price, _ := convertor.ToFloat(minuteDataSplit[1])
volume, _ := convertor.ToFloat(minuteDataSplit[2])
amount := float64(0)
if len(minuteDataSplit) >= 4 {
amount, _ = convertor.ToFloat(minuteDataSplit[3])
}
minuteData := &MinuteData{
Time: minuteDataSplit[0][0:2] + ":" + minuteDataSplit[0][2:4],
Price: price,
Volume: volume,
Amount: amount,
}
*minuteDatas = append(*minuteDatas, *minuteData)
}
date = m2["date"].(string)
}
}
}
}
return minuteDatas, date
}
func (receiver StockDataApi) GetKLineData(stockCode string, kLineType string, days int64) *[]KLineData {
url := fmt.Sprintf("http://quotes.sina.cn/cn/api/json_v2.php/CN_MarketDataService.getKLineData?symbol=%s&scale=%s&ma=yes&days=%d", stockCode, kLineType, days)
url := fmt.Sprintf("http://quotes.sina.cn/cn/api/json_v2.php/CN_MarketDataService.getKLineData?symbol=%s&scale=%s&ma=yes&datalen=%d", stockCode, kLineType, days)
K := &[]KLineData{}
_, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "quotes.sina.cn").
@ -1105,6 +1416,294 @@ func (receiver StockDataApi) GetKLineData(stockCode string, kLineType string, da
}
return K
}
func (receiver StockDataApi) GetHK_KLineData(stockCode string, kLineType string, days int64) *[]KLineData {
logger.SugaredLogger.Infof("GetHK_KLineData stockCode:%s,kLineType:%s,days:%d", stockCode, kLineType, days)
if strutil.HasPrefixAny(stockCode, []string{"gb_", "GB_"}) {
stockCode = strings.Replace(stockCode, "gb_", "us", 1) + ".OQ"
}
url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=%s,%s,,,%d,qfq", stockCode, kLineType, days)
//logger.SugaredLogger.Infof("url:%s", url)
K := &[]KLineData{}
res := make(map[string]interface{})
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "web.ifzq.gtimg.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(url)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return K
}
//logger.SugaredLogger.Infof("resp:%s", resp.Body())
json.Unmarshal(resp.Body(), &res)
code, _ := convertor.ToInt(res["code"])
if code != 0 {
return K
}
if res["data"] != nil && code == 0 {
data := res["data"].(map[string]interface{})[stockCode].(map[string]interface{})
if data != nil {
var day []any
if data["qfqday"] != nil {
day = data["qfqday"].([]any)
}
if data["day"] != nil {
day = data["day"].([]any)
}
for _, v := range day {
if v != nil {
vv := v.([]any)
KLine := &KLineData{
Day: convertor.ToString(vv[0]),
Open: convertor.ToString(vv[1]),
Close: convertor.ToString(vv[2]),
High: convertor.ToString(vv[3]),
Low: convertor.ToString(vv[4]),
Volume: convertor.ToString(vv[5]),
}
*K = append(*K, *KLine)
}
}
}
}
return K
}
func (receiver StockDataApi) GetSinaHKStockInfo() {
pageSize := 500
for i := 1; i <= 3060/pageSize; i++ {
infos := getSinaStockInfo(receiver, i, pageSize)
for i, info := range *infos {
logger.SugaredLogger.Infof("infos:%d,%s:%s", i, info.Symbol, info.Name)
}
}
}
func getSinaStockInfo(receiver StockDataApi, page, pageSize int) *[]models.SinaStockInfo {
infos := &[]models.SinaStockInfo{}
url := "https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/Market_Center.getHKStockData?page=%d&num=%d&sort=symbol&asc=1&node=qbgg_hk&_s_r_a=init"
_, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).SetProxy("http://localhost:10809").R().
SetHeader("Host", "vip.stock.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").
SetResult(infos).
Get(fmt.Sprintf(url, page, pageSize))
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
}
return infos
}
func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
//m:105,m:106,m:107 //美股
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
fs := ""
switch market {
case "hk":
fs = "m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2"
case "us":
fs = "m:105,m:106,m:107"
}
url := "https://push2.eastmoney.com/api/qt/clist/get?cb=jQuery371047843066631541353_1745889398012&fs=%s&fields=f12,f13,f14,f19,f1,f2,f4,f3,f152,f17,f18,f15,f16,f5,f6&fid=f3&pn=%d&pz=%d&po=1&dect=1"
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "push2.eastmoney.com").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
Get(fmt.Sprintf(url, fs, page, pageSize))
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return
}
body := string(resp.Body())
body = strutil.ReplaceWithMap(body, map[string]string{
"jQuery371047843066631541353_1745889398012(": "",
");": "",
})
js := "var d=" + body
vm := otto.New()
_, err = vm.Run(js)
_, err = vm.Run("var data = JSON.stringify(d);")
value, err := vm.Get("data")
data := make(map[string]any)
err = json.Unmarshal([]byte(value.String()), &data)
if err != nil {
logger.SugaredLogger.Errorf("json:%s", value.String())
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
}
logger.SugaredLogger.Infof("resp:%s", data)
if data["data"] != nil {
datas := data["data"].(map[string]any)
total := datas["total"].(float64)
diff := datas["diff"].(map[string]any)
logger.SugaredLogger.Infof("total:%d", int(total))
for k, item := range diff {
stock := item.(map[string]any)
logger.SugaredLogger.Infof("k:%s,%s:%s", k, stock["f14"], stock["f12"])
if market == "hk" {
stockInfo := &models.StockInfoHK{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
Name: stock["f14"].(string),
}
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stockInfo.Code).First(stockInfo)
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&models.StockInfoHK{}).Create(stockInfo)
}
}
if market == "us" {
stockInfo := &models.StockInfoUS{
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
Name: stock["f14"].(string),
}
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stockInfo.Code).First(stockInfo)
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
if stockInfo.ID == 0 {
db.Dao.Model(&models.StockInfoUS{}).Create(stockInfo)
}
}
}
}
}
func (receiver StockDataApi) GetHKStockInfo(pageSize int) {
url := "https://stock.gtimg.cn/data/hk_rank.php?board=main_all&metric=price&pageSize=%d&reqPage=1&order=desc&var_name=list_data"
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "stock.gtimg.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(url, pageSize))
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return
}
js := "var " + string(resp.Body())
vm := otto.New()
_, err = vm.Run(js)
_, err = vm.Run("var data = JSON.stringify(list_data);")
if err != nil {
return
}
value, err := vm.Get("data")
data := make(map[string]any)
err = json.Unmarshal([]byte(value.String()), &data)
if err != nil {
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
}
logger.SugaredLogger.Infof("resp:%s", data)
if data["code"] != nil && data["code"].(float64) == 0 {
d := data["data"].(map[string]any)
saveHKStockInfo(d)
page_count := int64(d["page_count"].(float64))
logger.SugaredLogger.Infof("page_count:%d", page_count)
page := int64(1)
for page > page_count {
urlx := fmt.Sprintf("https://stock.gtimg.cn/data/hk_rank.php?board=main_all&metric=price&pageSize=%d&reqPage=%d&order=desc&var_name=list_data", pageSize, page)
logger.SugaredLogger.Infof("url:%s", urlx)
resp, err = receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "stock.gtimg.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(urlx)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
break
}
js = "var " + string(resp.Body())
_, err = vm.Run(js)
_, err = vm.Run("var data = JSON.stringify(list_data);")
if err != nil {
return
}
value, err = vm.Get("data")
data = make(map[string]any)
err = json.Unmarshal([]byte(value.String()), &data)
if err != nil {
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
}
logger.SugaredLogger.Infof("resp:%s", data)
if data != nil && data["code"] != nil && data["code"].(float64) == 0 {
if data["data"] != nil {
d = data["data"].(map[string]any)
saveHKStockInfo(d)
}
}
page++
}
//
}
}
func saveHKStockInfo(d map[string]any) {
for _, v := range d["page_data"].([]any) {
vv := v.(string)
splits := strings.Split(vv, "~")
stock := &models.StockInfoHK{
Code: strutil.PadStart(splits[0], 5, "0") + ".HK",
Name: splits[1],
}
logger.SugaredLogger.Infof("vv:%s", vv)
db.Dao.Model(stock).Where("code = ?", stock.Code).First(stock)
if stock.ID == 0 {
logger.SugaredLogger.Infof("stock:%+v", stock)
db.Dao.Model(&models.StockInfoHK{}).Create(stock)
}
}
}
func (receiver StockDataApi) GetCommonKLineData(stockCode string, kLineType string, days int64) *[]KLineData {
url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=%s,%s,,,%d,qfq", stockCode, kLineType, days)
logger.SugaredLogger.Infof("url:%s", url)
K := &[]KLineData{}
res := make(map[string]interface{})
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
SetHeader("Host", "web.ifzq.gtimg.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(url)
if err != nil {
logger.SugaredLogger.Errorf("err:%s", err.Error())
return K
}
logger.SugaredLogger.Infof("resp:%s", resp.Body())
json.Unmarshal(resp.Body(), &res)
code, _ := convertor.ToInt(res["code"])
if code != 0 {
return K
}
if res["data"] != nil && code == 0 {
data := res["data"].(map[string]interface{})[stockCode].(map[string]interface{})
if data != nil {
var day []any
if data["qfqday"] != nil {
day = data["qfqday"].([]any)
}
if data["day"] != nil {
day = data["day"].([]any)
}
for _, v := range day {
if v != nil {
vv := v.([]any)
KLine := &KLineData{
Day: convertor.ToString(vv[0]),
Open: convertor.ToString(vv[1]),
Close: convertor.ToString(vv[2]),
High: convertor.ToString(vv[3]),
Low: convertor.ToString(vv[4]),
Volume: convertor.ToString(vv[5]),
}
*K = append(*K, *KLine)
}
}
}
}
return K
}
// JSONToMarkdownTable 将JSON数据转换为Markdown表格
func JSONToMarkdownTable(jsonData []byte) (string, error) {
@ -1160,3 +1759,10 @@ type KLineData struct {
Close string `json:"close"`
Volume string `json:"volume"`
}
type MinuteData struct {
Time string `json:"time"`
Price float64 `json:"price"`
Volume float64 `json:"volume"`
Amount float64 `json:"amount"`
}

View File

@ -5,6 +5,7 @@ import (
"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"
@ -22,7 +23,16 @@ 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) {
@ -37,6 +47,7 @@ func TestGetFinancialReports(t *testing.T) {
}
func TestGetTelegraphSearch(t *testing.T) {
db.Init("../../data/stock.db")
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram"
messages := SearchStockInfo("谷歌", "telegram", 30)
for _, message := range *messages {
@ -46,21 +57,27 @@ 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) {
db.Init("../../data/stock.db")
//SearchStockPriceInfo("中信证券", "hk06030", 30)
//SearchStockPriceInfo("上海贝岭", "sh600171", 30)
SearchStockPriceInfo("苹果公司", "gb_aapl", 30)
SearchStockPriceInfo("上海贝岭", "sh600171", 30)
//SearchStockPriceInfo("苹果公司", "gb_aapl", 30)
//SearchStockPriceInfo("微创光电", "bj430198", 30)
getZSInfo("创业板指数", "sz399006", 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)
@ -75,6 +92,38 @@ func TestGetKLineData(t *testing.T) {
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)
@ -134,7 +183,7 @@ func TestParseFullSingleStockData(t *testing.T) {
func TestNewStockDataApi(t *testing.T) {
db.Init("../../data/stock.db")
stockDataApi := NewStockDataApi()
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745", "gb_tsla")
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745", "gb_tsla", "hk09660", "hk00700")
for _, data := range *datas {
t.Log(data)
}

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

@ -215,3 +215,150 @@ type Prompt struct {
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"`
}

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

File diff suppressed because it is too large Load Diff

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

@ -12,7 +12,8 @@
"@types/file-saver": "^2.0.7",
"@vavt/cm-extension": "^1.8.0",
"@vavt/v3-extension": "^3.0.0",
"@vicons/ionicons5": "^0.13.0",
"date-fns": "^4.1.0",
"echarts": "^5.6.0",
"file-saver": "^2.0.5",
"html2canvas": "^1.4.1",
"lodash": "^4.17.21",
@ -22,11 +23,19 @@
"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 @@
cf858d682535e094e087a036e009d7f8
2d63c3a999d797889c01d6c96451b197

View File

@ -1,33 +1,56 @@
<script setup>
import {
EventsEmit,
EventsOff,
EventsOn,
Quit,
WindowFullscreen, WindowGetPosition,
WindowFullscreen,
WindowHide,
WindowSetPosition,
WindowUnfullscreen
} from '../wailsjs/runtime'
import {h, onBeforeMount, onMounted, ref} from "vue";
import { RouterLink } from 'vue-router'
import {darkTheme, NGradientText, 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,
PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline, AlarmOutline, SparklesOutline,
SettingsOutline, Skull, SkullOutline, SkullSharp,
SparklesOutline,
StarOutline,
Wallet, WarningOutline,
} from '@vicons/ionicons5'
import {GetConfig} from "../wailsjs/go/main/App";
const enableNews= ref(false)
const contentStyle = ref("")
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 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('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
const enableDarkTheme = ref(null)
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎\n\n未经授权,禁止商业目的!')
const isFullscreen = ref(false)
const activeKey = ref('')
const containerRef= ref({})
const realtimeProfit= ref(0)
const telegraph= ref([])
const containerRef = ref({})
const realtimeProfit = ref(0)
const telegraph = ref([])
const groupList = ref([])
const menuOptions = ref([
{
label: () =>
@ -36,20 +59,299 @@ const menuOptions = ref([
{
to: {
name: 'stock',
params: {
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+"¥"}),
key: 'realtimeProfit',
show: realtimeProfit.value,
icon: renderIcon(WalletOutline),
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),
},
]
},
@ -60,18 +362,17 @@ const menuOptions = ref([
{
to: {
name: 'fund',
params: {
},
params: {},
}
},
{ default: () => '基金自选',}
{default: () => '基金自选',}
),
show: enableFund.value,
key: 'fund',
icon: renderIcon(SparklesOutline),
children:[
children: [
{
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '功能完善中!'}),
label: () => h(NText, {type: realtimeProfit.value > 0 ? 'error' : 'success'}, {default: () => '功能完善中!'}),
key: 'realtimeProfit',
show: realtimeProfit.value,
icon: renderIcon(AlarmOutline),
@ -85,11 +386,10 @@ const menuOptions = ref([
{
to: {
name: 'settings',
params: {
}
params: {}
}
},
{ default: () => '设置' }
{default: () => '设置'}
),
key: 'settings',
icon: renderIcon(SettingsOutline),
@ -101,30 +401,29 @@ const menuOptions = ref([
{
to: {
name: 'about',
params: {
}
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),
},
@ -138,28 +437,31 @@ const menuOptions = ref([
// 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
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) {
@ -177,11 +479,29 @@ function toggleFullscreen(e) {
// }
// window.addEventListener('mousemove', dragstart)
EventsOn("realtime_profit",(data)=>{
realtimeProfit.value=data
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) {
@ -197,91 +517,159 @@ window.onerror = function (msg, source, lineno, colno, error) {
return true;
};
onBeforeMount(()=>{
GetConfig().then((res)=>{
console.log(res)
enableFund.value=res.enableFund
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,
}
}))
}
})
})
menuOptions.value.filter((item)=>{
if(item.key==='fund'){
item.show=res.enableFund
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
if (res.darkTheme) {
enableDarkTheme.value = darkTheme
} else {
enableDarkTheme.value = null
}
})
})
onMounted(()=>{
contentStyle.value="max-height: calc(90vh);overflow: hidden"
GetConfig().then((res)=>{
if(res.enableNews){
enableNews.value=true
onMounted(() => {
contentStyle.value = "max-height: calc(92vh);overflow: hidden"
GetConfig().then((res) => {
if (res.enableNews) {
enableNews.value = true
}
enableFund.value=res.enableFund
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 ref="containerRef" :theme="enableDarkTheme" >
<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-gi>
-->
<n-gi>
<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-text type="warning"> {{telegraph[0]}}</n-text>-->
</n-marquee>
<n-scrollbar :style="contentStyle">
<RouterView />
</n-scrollbar>
</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-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

@ -4,7 +4,7 @@
import 'md-editor-v3/lib/preview.css';
import {h, onBeforeUnmount, onMounted, ref} from 'vue';
import {CheckUpdate, GetVersionInfo} from "../../wailsjs/go/main/App";
import {EventsOn} from "../../wailsjs/runtime";
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
import {NAvatar, NButton, useNotification} from "naive-ui";
const updateLog = ref('');
const versionInfo = ref('');
@ -25,6 +25,7 @@ onMounted(() => {
})
onBeforeUnmount(() => {
notify.destroyAll()
EventsOff("updateVersion")
})
EventsOn("updateVersion",async (msg) => {
@ -43,8 +44,8 @@ EventsOn("updateVersion",async (msg) => {
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
console.log("GitHub UTC 时间:", utcDate);
console.log("转换后的本地时间:", formattedDate);
//console.log("GitHub UTC :", utcDate);
//console.log(":", formattedDate);
notify.info({
avatar: () =>
h(NAvatar, {
@ -134,6 +135,7 @@ EventsOn("updateVersion",async (msg) => {
</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 />

View File

@ -47,7 +47,7 @@ onBeforeMount(()=>{
})
GetFollowedFund().then(result => {
followList.value = result
console.log("followList",followList.value)
//console.log("followList",followList.value)
})
})
@ -60,7 +60,7 @@ onMounted(() => {
//ws.value = new WebSocket('ws://localhost:16688/ws'); // WebSocket
ws.value.onopen = () => {
console.log('WebSocket 连接已打开');
//console.log('WebSocket ');
};
ws.value.onmessage = (event) => {
@ -74,13 +74,13 @@ onMounted(() => {
};
ws.value.onclose = () => {
console.log('WebSocket 连接已关闭');
//console.log('WebSocket ');
};
ticker.value=setInterval(() => {
GetFollowedFund().then(result => {
followList.value = result
console.log("followList",followList.value)
//console.log("followList",followList.value)
})
}, 1000*60)
@ -103,7 +103,7 @@ function AddFund(){
message.success("关注成功")
GetFollowedFund().then(result => {
followList.value = result
console.log("followList",followList.value)
//console.log("followList",followList.value)
})
}
})
@ -114,7 +114,7 @@ function unFollow(code){
message.success("取消关注成功")
GetFollowedFund().then(result => {
followList.value = result
console.log("followList",followList.value)
//console.log("followList",followList.value)
})
}
})

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

@ -45,6 +45,7 @@ const formValue = ref({
enableNews:false,
darkTheme:true,
enableFund:false,
enablePushNews:false,
})
const promptTemplates=ref([])
onMounted(()=>{
@ -78,13 +79,14 @@ onMounted(()=>{
formValue.value.enableNews = res.enableNews
formValue.value.darkTheme = res.darkTheme
formValue.value.enableFund = res.enableFund
formValue.value.enablePushNews = res.enablePushNews
console.log(res)
//console.log(res)
})
//message.info("")
GetPromptTemplates("","").then(res=>{
console.log(res)
//console.log(res)
promptTemplates.value=res
})
})
@ -118,6 +120,7 @@ function saveConfig(){
enableNews:formValue.value.enableNews,
darkTheme:formValue.value.darkTheme,
enableFund:formValue.value.enableFund,
enablePushNews:formValue.value.enablePushNews
})
@ -165,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 = {
@ -195,6 +198,7 @@ function importConfig(){
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);
@ -204,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",
@ -237,14 +241,14 @@ function savePrompt(){
AddPrompt(formPrompt.value).then(res=>{
message.success(res)
GetPromptTemplates("","").then(res=>{
console.log(res)
//console.log(res)
promptTemplates.value=res
})
showManagePromptsModal.value=false
})
}
function editPrompt(prompt){
console.log(prompt)
//console.log(prompt)
formPrompt.value.ID=prompt.ID
formPrompt.value.Name=prompt.name
formPrompt.value.Content=prompt.content
@ -255,7 +259,7 @@ function deletePrompt(ID){
DelPrompt(ID).then(res=>{
message.success(res)
GetPromptTemplates("","").then(res=>{
console.log(res)
//console.log(res)
promptTemplates.value=res
})
})
@ -288,7 +292,7 @@ function deletePrompt(ID){
<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-form-item-gi :span="6" label="指数基金:" path="enableFund" >
<n-switch v-model:value="formValue.enableFund" />
</n-form-item-gi>
</n-grid>
@ -297,18 +301,21 @@ function deletePrompt(ID){
<n-gi :span="24">
<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="5" label="弹幕功能:" path="enableDanmu" >
<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="5" label="是否显示滚动快讯(重启生效)" path="enableNews" >
<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>
@ -319,7 +326,7 @@ function deletePrompt(ID){
<n-gi :span="24">
<n-text type="success" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
</n-gi>
<n-form-item-gi :span="3" label="是否启用AI诊股" path="openAI.enable" >
<n-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" >

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ import naive from 'naive-ui'
import App from './App.vue'
import router from './router/router'
const app = createApp(App)
app.use(router)
app.use(naive)

View File

@ -1,19 +1,22 @@
import {createMemoryHistory, createRouter, createWebHashHistory} 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: '/', 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: createWebHashHistory(),
history: createWebHistory(),
routes,
})

View File

@ -11,10 +11,16 @@ 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>;
@ -33,18 +39,52 @@ 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 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>;
@ -53,6 +93,8 @@ export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:st
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>;
@ -67,6 +109,12 @@ 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>;

View File

@ -18,14 +18,26 @@ 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']();
}
@ -62,14 +74,46 @@ 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']();
}
@ -78,14 +122,50 @@ 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 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);
}
@ -102,6 +182,10 @@ 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);
}
@ -130,6 +214,18 @@ 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);
}

View File

@ -290,6 +290,26 @@ export namespace data {
export class SentimentResult {
Score: number;
Category: number;
PositiveCount: number;
NegativeCount: number;
Description: string;
static createFrom(source: any = {}) {
return new SentimentResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.Score = source["Score"];
this.Category = source["Category"];
this.PositiveCount = source["PositiveCount"];
this.NegativeCount = source["NegativeCount"];
this.Description = source["Description"];
}
}
export class Settings {
ID: number;
// Go type: time
@ -322,6 +342,7 @@ export namespace data {
darkTheme: boolean;
browserPoolSize: number;
enableFund: boolean;
enablePushNews: boolean;
static createFrom(source: any = {}) {
return new Settings(source);
@ -357,6 +378,7 @@ export namespace data {
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 {

19
go.mod
View File

@ -1,8 +1,6 @@
module go-stock
go 1.23
toolchain go1.23.0
go 1.23.0
require (
github.com/PuerkitoBio/goquery v1.10.1
@ -11,15 +9,18 @@ require (
github.com/duke-git/lancet/v2 v2.3.4
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/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.30.0
golang.org/x/text v0.22.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
@ -61,15 +62,19 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/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.33.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.35.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

32
go.sum
View File

@ -26,6 +26,8 @@ github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9g
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-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=
@ -107,6 +109,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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=
@ -115,12 +119,22 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
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.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
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=
@ -140,8 +154,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
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/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
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=
@ -159,8 +173,8 @@ 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/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
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=
@ -185,8 +199,8 @@ 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/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.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=
@ -206,8 +220,8 @@ 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/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
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=
@ -222,6 +236,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X
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/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

193
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"
@ -20,6 +21,7 @@ import (
"os"
goruntime "runtime"
"runtime/debug"
"strings"
"time"
)
@ -55,40 +57,13 @@ 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{})
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{})
go AutoMigrate()
//db.Dao.Model(&data.Group{}).Where("id = ?", 0).FirstOrCreate(&data.Group{
// Name: "默认分组",
// Sort: 0,
//})
if stocksBin != nil && len(stocksBin) > 0 {
go initStockData()
}
log.SugaredLogger.Infof("init stocksBinHK %d", len(stocksBinHK))
if stocksBinHK != nil && len(stocksBinHK) > 0 {
go initStockDataHK()
}
log.SugaredLogger.Infof("init stocksBinUS %d", len(stocksBinUS))
if stocksBinUS != nil && len(stocksBinUS) > 0 {
go initStockDataUS()
}
updateBasicInfo()
// Create an instance of the app structure
app := NewApp()
AppMenu := menu.NewMenu()
@ -130,12 +105,12 @@ func main() {
//var width, height int
//var err error
//
//width, height, err = getScreenResolution()
//if err != nil {
// log.SugaredLogger.Error("get screen resolution error")
// width = 1456
// height = 768
//}
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}
@ -144,12 +119,12 @@ func main() {
}
// Create application with options
err := wails.Run(&options.App{
Title: "go-stock",
//Width: width * 4 / 5,
//Height: height * 4 / 5,
err = wails.Run(&options.App{
Title: "go-stock",
Width: width * 4 / 5,
Height: 900,
MinWidth: 1456,
MinHeight: 900,
MinHeight: 768,
//MaxWidth: width,
//MaxHeight: height,
DisableResize: false,
@ -211,7 +186,30 @@ func main() {
}
func initStockDataUS() {
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 {
@ -219,18 +217,25 @@ func initStockDataUS() {
return
}
log.SugaredLogger.Infof("init stock data us %d", 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
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)
}
db.Dao.Model(&models.StockInfoUS{}).Create(&item)
}
}
func initStockDataHK() {
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 {
@ -238,15 +243,20 @@ func initStockDataHK() {
return
}
log.SugaredLogger.Infof("init stock data hk %d", 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
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)
}
db.Dao.Model(&models.StockInfoHK{}).Create(&item)
}
}
func updateBasicInfo() {
@ -258,7 +268,11 @@ func updateBasicInfo() {
}
}
func initStockData() {
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)
@ -266,26 +280,24 @@ func initStockData() {
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])
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 {
@ -293,7 +305,38 @@ func initStockData() {
} 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) {