Compare commits
326 Commits
v2025.4.25
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
469c4826a3 | ||
|
|
6c31f5aa76 | ||
|
|
e797b64d41 | ||
|
|
12299e8b47 | ||
|
|
7412d56409 | ||
|
|
f75b457082 | ||
|
|
a43095cdd4 | ||
|
|
6ca0d0df32 | ||
|
|
fea9b06a27 | ||
|
|
1d1e437b47 | ||
|
|
eca2f8adee | ||
|
|
49d2109d60 | ||
|
|
f9294fbffd | ||
|
|
cc12a886c1 | ||
|
|
34ea989d47 | ||
|
|
aadff1c5eb | ||
|
|
6b7fed00a3 | ||
|
|
5af0e28601 | ||
|
|
c4af77954e | ||
|
|
8236adb680 | ||
|
|
0adfdab441 | ||
|
|
b1c618a9de | ||
|
|
709c372bf3 | ||
|
|
beb022c448 | ||
|
|
d1aed3419b | ||
|
|
48511c61df | ||
|
|
163e8800f9 | ||
|
|
70e0d19c58 | ||
|
|
4e04bacf22 | ||
|
|
8c75c8533a | ||
|
|
1ad02d4b0c | ||
|
|
a354ab8925 | ||
|
|
6d50be8541 | ||
|
|
6303d535bc | ||
|
|
b464d8f563 | ||
|
|
eea0856c1c | ||
|
|
1dd77d5c08 | ||
|
|
9c1c0382ca | ||
|
|
46065f448b | ||
|
|
a7cee69e68 | ||
|
|
459441f838 | ||
|
|
b4b3b61e8c | ||
|
|
b6a99940ab | ||
|
|
5b0f34a3bd | ||
|
|
e34ebf9895 | ||
|
|
c3521c6d7f | ||
|
|
93b37ca621 | ||
|
|
7069af869b | ||
|
|
dbb6789c05 | ||
|
|
8aed4d2753 | ||
|
|
6bd1bdae02 | ||
|
|
9a40d343aa | ||
|
|
e4cdad6ffe | ||
|
|
a0005dab96 | ||
|
|
c945ca9322 | ||
|
|
7bbc6831f4 | ||
|
|
ab0ccc4fe0 | ||
|
|
fa658357c9 | ||
|
|
746589a972 | ||
|
|
401dd17fa8 | ||
|
|
c365bd9534 | ||
|
|
104ee51e13 | ||
|
|
00f3e5f0e0 | ||
|
|
483ffa2244 | ||
|
|
63d278b9aa | ||
|
|
5621d40c71 | ||
|
|
26e9753b94 | ||
|
|
b7f6dbd2da | ||
|
|
18dd01b613 | ||
|
|
81bb33a135 | ||
|
|
9926b61fac | ||
|
|
5e975b060c | ||
|
|
e8f063fd9b | ||
|
|
8b0b53fae7 | ||
|
|
b29c380055 | ||
|
|
cf58a707c7 | ||
|
|
1ae1bb0116 | ||
|
|
d8971935ee | ||
|
|
9c68458b81 | ||
|
|
b367d1eb40 | ||
|
|
8fe79adbb1 | ||
|
|
1d81fdba87 | ||
|
|
6aca0e15cc | ||
|
|
173ce6f243 | ||
|
|
e7875e73d3 | ||
|
|
ca4727db80 | ||
|
|
84ffe7c5fd | ||
|
|
da02d1bd1c | ||
|
|
bae2bf9c5c | ||
|
|
6568b5949a | ||
|
|
c4287f9b78 | ||
|
|
87441d8923 | ||
|
|
ebd166e72b | ||
|
|
494a60debe | ||
|
|
b3e2565a02 | ||
|
|
c0a87d5d2e | ||
|
|
d74ad3c03d | ||
|
|
6dff9d95c4 | ||
|
|
06967420f8 | ||
|
|
6f4eb0ac86 | ||
|
|
f59255cc6c | ||
|
|
4f4fa46338 | ||
|
|
05bf35fdf4 | ||
|
|
567c81ae7c | ||
|
|
86f4e54d13 | ||
|
|
71e6ff4233 | ||
|
|
e844e2cff9 | ||
|
|
27af39ff61 | ||
|
|
5537ebb87a | ||
|
|
b906140dd5 | ||
|
|
087b953ed8 | ||
|
|
3c5205738f | ||
|
|
b1b34d950b | ||
|
|
83aa4331ad | ||
|
|
d4d3c44cf4 | ||
|
|
81a9cc5927 | ||
|
|
3fc89a85da | ||
|
|
0605c8442d | ||
|
|
cf8591c208 | ||
|
|
7607c4356f | ||
|
|
4aae2ece00 | ||
|
|
369d14025c | ||
|
|
1e7387f3fa | ||
|
|
cfd218f181 | ||
|
|
b8e1f38a32 | ||
|
|
b1a9a8d4d8 | ||
|
|
b98f829286 | ||
|
|
dda160069a | ||
|
|
f80ea181be | ||
|
|
f5c8f5d0ef | ||
|
|
23d3566f31 | ||
|
|
052104b43a | ||
|
|
93e8fb27b5 | ||
|
|
25623d90d7 | ||
|
|
8db94da233 | ||
|
|
60e7d87918 | ||
|
|
615b4d231a | ||
|
|
490a3c0847 | ||
|
|
38f83674ef | ||
|
|
d26c4bc986 | ||
|
|
7e919376b5 | ||
|
|
1d9ef724e6 | ||
|
|
8e982d4430 | ||
|
|
a67559831a | ||
|
|
9718d3311d | ||
|
|
789e7427ce | ||
|
|
801aa14c7a | ||
|
|
f5c621fbcc | ||
|
|
119f0f8aa7 | ||
|
|
fe814974fd | ||
|
|
dd3c231637 | ||
|
|
e05ff94aba | ||
|
|
bbd4bb5b48 | ||
|
|
58f3009902 | ||
|
|
c6b841fb8f | ||
|
|
2b28390414 | ||
|
|
7887dfed5e | ||
|
|
a4c98933a4 | ||
|
|
ad63ffff7f | ||
|
|
1ccc2f8b1f | ||
|
|
dc5483aa07 | ||
|
|
8c82ba4a38 | ||
|
|
fd905ff278 | ||
|
|
6ec0f5fbe0 | ||
|
|
32706fb4dc | ||
|
|
2cb661734f | ||
|
|
4fab910340 | ||
|
|
84e4ba8474 | ||
|
|
76a44fae32 | ||
|
|
7ea974f1a6 | ||
|
|
7ea160b6b5 | ||
|
|
c2f260c613 | ||
|
|
2d224ccfc4 | ||
|
|
a66f2156f1 | ||
|
|
e90727773f | ||
|
|
89dcb713be | ||
|
|
6f4b21207d | ||
|
|
f51e3d863a | ||
|
|
c180c2a5f8 | ||
|
|
3ba18e8ef2 | ||
|
|
f0314187e5 | ||
|
|
6440885688 | ||
|
|
2dd4f072b2 | ||
|
|
b5843bcdb8 | ||
|
|
c65d0b79f4 | ||
|
|
92bb0097cd | ||
|
|
0c6bd7292e | ||
|
|
eeae1f77f4 | ||
|
|
707e353ea8 | ||
|
|
5ee14b703c | ||
|
|
04a46108f3 | ||
|
|
48a601f776 | ||
|
|
e249933f8b | ||
|
|
a8bb2b5399 | ||
|
|
16c89de792 | ||
|
|
71bfed3744 | ||
|
|
edd1bf94f9 | ||
|
|
cfe1abb07f | ||
|
|
8b94e14ec9 | ||
|
|
44e1093e8e | ||
|
|
5e7f34652a | ||
|
|
5b9a81d770 | ||
|
|
7021a59ee6 | ||
|
|
433dea0772 | ||
|
|
378a5c47ba | ||
|
|
9a60736739 | ||
|
|
efe6365ea5 | ||
|
|
062df80712 | ||
|
|
528482db48 | ||
|
|
746e5ec98a | ||
|
|
6d345ae91d | ||
|
|
888a97e4d3 | ||
|
|
ebeaf104bb | ||
|
|
b945a0e0e1 | ||
|
|
111252f8bd | ||
|
|
2e5ec6ace8 | ||
|
|
3e16574faa | ||
|
|
482472af4e | ||
|
|
bdc3689ac8 | ||
|
|
e8ebb577b2 | ||
|
|
71f8265bc2 | ||
|
|
43063fa7fb | ||
|
|
86f041b4d6 | ||
|
|
0ce7e8e7a7 | ||
|
|
bbab60e2ad | ||
|
|
1fbd564bff | ||
|
|
f0ad50303e | ||
|
|
55839d3329 | ||
|
|
3f4cbca4a7 | ||
|
|
6e3b9ff1f9 | ||
|
|
0e45866421 | ||
|
|
e0225c4158 | ||
|
|
2f6c17fb2a | ||
|
|
22b4fcdffb | ||
|
|
7dd10d443e | ||
|
|
5b5590ebd7 | ||
|
|
be02343d68 | ||
|
|
942d249671 | ||
|
|
9f2719cdbc | ||
|
|
0343a95a21 | ||
|
|
9337084ebf | ||
|
|
18834d9281 | ||
|
|
9e06136983 | ||
|
|
30a3d1d9ef | ||
|
|
5b6de9f9f6 | ||
|
|
65d737c695 | ||
|
|
af73691b22 | ||
|
|
b2c12cffbb | ||
|
|
a936dc6371 | ||
|
|
f6d217e4fd | ||
|
|
378b669827 | ||
|
|
0d3fd47552 | ||
|
|
a2fee361e7 | ||
|
|
1ef950b961 | ||
|
|
934b4608b7 | ||
|
|
68e7b6a68c | ||
|
|
700572567e | ||
|
|
c9ade36844 | ||
|
|
0a2491d725 | ||
|
|
9d8af191c5 | ||
|
|
6382be6b19 | ||
|
|
0cafcb9cd4 | ||
|
|
21c7f5390c | ||
|
|
02db6c2e87 | ||
|
|
2811786bfd | ||
|
|
9aa2c4095a | ||
|
|
ad9bea4c24 | ||
|
|
4f8d84b8a0 | ||
|
|
e238700333 | ||
|
|
6bdff0a0f3 | ||
|
|
c7655d2adf | ||
|
|
8996ddf986 | ||
|
|
329936568f | ||
|
|
0d85e24595 | ||
|
|
b266281bbd | ||
|
|
ace3ff7302 | ||
|
|
60b7cdc761 | ||
|
|
3cc597d361 | ||
|
|
68bcfc679a | ||
|
|
78f7808f1b | ||
|
|
d6c3a6b98b | ||
|
|
3b25aa79bb | ||
|
|
e49545a581 | ||
|
|
1185af5a87 | ||
|
|
152a6335d8 | ||
|
|
338e371190 | ||
|
|
3ffcaa0374 | ||
|
|
ed9d9cde77 | ||
|
|
673d446b05 | ||
|
|
e2e0ef2aad | ||
|
|
a8ecbf9329 | ||
|
|
9eded54d8d | ||
|
|
c1d458e5cf | ||
|
|
7158e405a6 | ||
|
|
d993a5525f | ||
|
|
6af6d989ba | ||
|
|
0b3acd9adc | ||
|
|
013de869f4 | ||
|
|
1b67e20932 | ||
|
|
8b510bce94 | ||
|
|
71676eead4 | ||
|
|
2a274db7ae | ||
|
|
4fd5cbf8e6 | ||
|
|
d7b17b2561 | ||
|
|
ad92c41d08 | ||
|
|
47dbbb8813 | ||
|
|
ae9f4073dc | ||
|
|
c7e37e039e | ||
|
|
99b6586c77 | ||
|
|
7e24424ea0 | ||
|
|
58d93c76f6 | ||
|
|
df989b706b | ||
|
|
cf537ca695 | ||
|
|
11a1a47eca | ||
|
|
338064e536 | ||
|
|
8ba26b6250 | ||
|
|
54138ff61e | ||
|
|
d8d5091709 | ||
|
|
7f204ee80d | ||
|
|
4a367b6027 | ||
|
|
e615fc4108 | ||
|
|
2b982f924e | ||
|
|
24e24f8236 | ||
|
|
e77c23e42a | ||
|
|
ef6228922e | ||
|
|
c4caea5be8 |
22
.github/workflows/main.yml
vendored
22
.github/workflows/main.yml
vendored
@@ -4,11 +4,14 @@ on:
|
||||
push:
|
||||
tags:
|
||||
# Match any new tag
|
||||
- '*'
|
||||
- '*-release'
|
||||
- '*-dev'
|
||||
|
||||
env:
|
||||
# Necessary for most environments as build failure can occur due to OOM issues
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
OFFICIAL_STATEMENT: ${{ vars.OFFICIAL_STATEMENT }}
|
||||
BUILD_KEY: ${{ vars.BUILD_KEY }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -23,6 +26,15 @@ jobs:
|
||||
# - name: 'go-stock-linux-amd64'
|
||||
# platform: 'linux/amd64'
|
||||
# os: 'ubuntu-latest'
|
||||
- name: 'go-stock-darwin-universal'
|
||||
platform: 'darwin/universal'
|
||||
os: 'macos-latest'
|
||||
- name: 'go-stock-darwin-intel'
|
||||
platform: 'darwin'
|
||||
os: 'macos-latest'
|
||||
- name: 'go-stock-darwin-arm64'
|
||||
platform: 'darwin/arm64'
|
||||
os: 'macos-latest'
|
||||
|
||||
runs-on: ${{ matrix.build.os }}
|
||||
steps:
|
||||
@@ -38,13 +50,15 @@ jobs:
|
||||
echo "::set-output name=commit_message::$commit_message"
|
||||
|
||||
- name: Build wails x go-stock
|
||||
uses: ArvinLovegood/wails-build-action@v3.4
|
||||
uses: ArvinLovegood/wails-build-action@v3.6
|
||||
id: build
|
||||
with:
|
||||
build-name: ${{ matrix.build.name }}
|
||||
build-platform: ${{ matrix.build.platform }}
|
||||
package: true
|
||||
go-version: '1.23'
|
||||
go-version: '1.25'
|
||||
build-tags: ${{ github.ref_name }}
|
||||
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
|
||||
node-version: '18.x'
|
||||
build-statement: ${{ env.OFFICIAL_STATEMENT }}
|
||||
build-key: ${{ env.BUILD_KEY }}
|
||||
node-version: '20.x'
|
||||
|
||||
85
README.md
85
README.md
@@ -1,16 +1,18 @@
|
||||
# go-stock : 基于Wails和NaiveUI构建的AI赋能股票分析工具
|
||||
# go-stock : 基于大语言模型的AI赋能股票分析工具
|
||||
## 
|
||||

|
||||
[](https://github.com/ArvinLovegood/go-stock)
|
||||
[](https://gitee.com/arvinlovegood_admin/go-stock)
|
||||
[](https://gitcode.com/ArvinLovegood/go-stock)
|
||||
|
||||
[//]: # ([](https://gitcode.com/ArvinLovegood/go-stock))
|
||||
|
||||
### 🌟公众号
|
||||

|
||||
|
||||
### 📈 交流群
|
||||
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大模型构建的股票分析工具。
|
||||
@@ -20,39 +22,80 @@ QQ交流群:[点击链接加入群聊【go-stock交流群】:491605333](http
|
||||
- 开发环境主要基于Windows10+,其他平台未测试或功能受限。
|
||||
|
||||
### 📦 立即体验
|
||||
- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
[//]: # (- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases))
|
||||
- 绿色版:[go-stock-windows-amd64.exe](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
- MACOS绿色版:[go-stock-darwin-universal](https://github.com/ArvinLovegood/go-stock/releases)
|
||||
|
||||
[//]: # (- MACOS安装版:[go-stock-darwin-universal.pkg](https://github.com/ArvinLovegood/go-stock/releases))
|
||||
|
||||
|
||||
### 💬 支持大模型/平台
|
||||
| 模型 | 状态 | 备注 |
|
||||
| --- | --- |-----------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
|
||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
|
||||
| [AnythingLLM](https://anythingllm.com/) | ✅ | 本地知识库 |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | deepseek-reasoner,deepseek-chat |
|
||||
| 模型 | 状态 | 备注 |
|
||||
| --- | --- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [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) |
|
||||
|
||||
### <span style="color: #568DF4;">各位亲爱的朋友们,如果您对这个项目感兴趣,请先给我一个<i style="color: #EA2626;">star</i>吧,谢谢!</span>💕
|
||||
- 经测试目前硅基流动(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)
|
||||
[//]: # (- 优云智算(by UCloud):万卡规模4090免费用10小时,新人注册另增50万tokens,海量热门源项目镜像一键部署,[注册链接](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock))
|
||||
- 火山方舟:新用户每个模型注册即送50万tokens,[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
|
||||
- 硅基流动(siliconflow),注册即送2000万Tokens,[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
|
||||
- Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究(注意:Tushare只需要120积分即可,注册完成个人资料补充即可得120积分!!!),[注册链接](https://tushare.pro/register?reg=701944)
|
||||
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
|
||||
- 欢迎大家提出宝贵的建议,欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧)。💕
|
||||
|
||||
|
||||
### 支持开源💕计划
|
||||
| 赞助计划 | 赞助等级 | 权益说明 |
|
||||
|:--------------------------------|----------------|:-------------------------------------------------------|
|
||||
| 每月 0 RMB | vip0 | 🌟 全部功能,软件自动更新(从GitHub下载),自行解决github平台网络问题。 |
|
||||
| 每月赞助 18.8 RMB<br>每年赞助 120 RMB | vip1 | 💕 全部功能,软件自动更新(从CDN下载),更新快速便捷。AI配置指导,提示词参考等 |
|
||||
| 每月赞助 28.8 RMB<br>每年赞助 240 RMB | vip2 | 💕 💕 vip1全部功能,启动时自动同步最近24小时市场资讯(包括外媒简讯) |
|
||||
| 每月赞助 X RMB | vipX | 🧩 更多计划,视go-stock开源项目发展情况而定...(承接GitHub项目README广告推广💖) |
|
||||
|
||||
## 🧩 重大功能开发计划
|
||||
| 功能说明 | 状态 | 备注 |
|
||||
|-----------------|----|----------------------------------------------------------------------------------------------------------|
|
||||
| 股票分析知识库 | 🚧 | 未来计划 |
|
||||
| Ai智能选股 | ✅ | Ai智能选股功能(市场行情-》AI总结/AI智能体功能) |
|
||||
| ETF支持 | 🚧 | ETF数据支持 (目前可以查看净值和估值) |
|
||||
| 美股支持 | ✅ | 美股数据支持 |
|
||||
| 港股支持 | ✅ | 港股数据支持 (目前有延迟) |
|
||||
| 美股支持 | ✅ | 美股数据支持 |
|
||||
| 港股支持 | ✅ | 港股数据支持 |
|
||||
| 多轮对话 | ✅ | AI分析后可继续对话提问 |
|
||||
| 自定义AI分析提问模板 | ✅ | 可配置的提问模板 [v2025.2.12.7-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.12.7-alpha) |
|
||||
| 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 |
|
||||
|
||||
## 👀 更新日志
|
||||
### 2025.12.16 新增AI思考模式与热门选股策略功能
|
||||
### 2025.11.21 新增带频率权重的情感分析功能
|
||||
### 2025.10.30 添加AI智能体功能开关(默认关闭,因为使用体验不理想),移除页面水印
|
||||
### 2025.09.27 添加机构/券商的研究报告AI工具函数
|
||||
### 2025.08.09 添加AI智能体聊天功能
|
||||
### 2025.07.08 实现软件自动更新功能
|
||||
### 2025.07.07 卡片添加迷你分时图
|
||||
### 2025.07.05 MacOs支持
|
||||
### 2025.07.01 AI分析集成工具函数,AI分析将更加智能
|
||||
### 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线数据获取优化
|
||||
@@ -78,6 +121,10 @@ QQ交流群:[点击链接加入群聊【go-stock交流群】:491605333](http
|
||||
|
||||
## 🦄 重大更新
|
||||
### BIG NEWS !!! 重大更新!!!
|
||||
- 2025.11.21 新增带频率权重的情感分析功能
|
||||

|
||||
- 2025.04.25 市场资讯支持AI分析和总结:让AI帮你读市场!
|
||||

|
||||
- 2025.04.24 新增市场行情模块:即时掌握全球市场行情资讯/动态,从此再也不用偷摸去各大财经网站啦。go-stock一键帮你搞定!
|
||||

|
||||

|
||||
@@ -122,15 +169,15 @@ QQ交流群:[点击链接加入群聊【go-stock交流群】:491605333](http
|
||||
|
||||
## 🐳 关于技术支持申明
|
||||
- 本软件基于开源技术构建,使用Wails、NaiveUI、Vue、AI大模型等开源项目。 技术上如有问题,可以先向对应的开源社区请求帮助。
|
||||
- 开源不易,本人精力和时间有限,如需一对一技术支持,请先赞助。联系微信(备注 技术支持):ArvinLovegood
|
||||
- 开源不易,本人精力和时间有限,如需一对一技术支持,请先赞助。联系QQ(备注 技术支持):506808970
|
||||
|
||||
<img src="./build/wx.jpg" width="301px" height="402px" alt="ArvinLovegood">
|
||||
[//]: # (<img src="./build/wx.jpg" width="301px" height="402px" alt="ArvinLovegood">)
|
||||
|
||||
|
||||
| 技术支持方式 | 赞助(元) |
|
||||
|:--------------------------------|:-----:|
|
||||
| 加 QQ:506808970,微信:ArvinLovegood | 100/次 |
|
||||
| 长期技术支持(不限次数,新功能优先体验等) | 5000 |
|
||||
| 加 QQ:506808970 | 100/次 |
|
||||
| 长期技术支持(不限次数,新功能优先体验等) | 5000 |
|
||||
|
||||
|
||||
|
||||
|
||||
112
app_common.go
Normal file
112
app_common.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go-stock/backend/agent"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/models"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// @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) models.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(5000)
|
||||
}
|
||||
func (a *App) GetHotStrategy() map[string]any {
|
||||
return data.NewSearchStockApi("").HotStrategy()
|
||||
}
|
||||
|
||||
func (a *App) ChatWithAgent(question string, aiConfigId int, sysPromptId *int) {
|
||||
ch := agent.NewStockAiAgentApi().Chat(question, aiConfigId, sysPromptId)
|
||||
for msg := range ch {
|
||||
runtime.EventsEmit(a.ctx, "agent-message", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) AnalyzeSentimentWithFreqWeight(text string) map[string]any {
|
||||
result, cleanFrequencies := data.NewsAnalyze(text, false)
|
||||
return map[string]any{
|
||||
"result": result,
|
||||
"frequencies": cleanFrequencies,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) GetAIResponseResultList(query models.AIResponseResultQuery) *models.AIResponseResultPageData {
|
||||
page, err := data.NewAIResponseResultService().GetAIResponseResultList(query)
|
||||
if err != nil {
|
||||
return &models.AIResponseResultPageData{}
|
||||
}
|
||||
return page
|
||||
}
|
||||
func (a *App) DeleteAIResponseResult(id string) string {
|
||||
err := data.NewAIResponseResultService().DeleteAIResponseResult(id)
|
||||
if err != nil {
|
||||
return "删除失败"
|
||||
}
|
||||
return "删除成功"
|
||||
}
|
||||
func (a *App) BatchDeleteAIResponseResult(ids []uint) string {
|
||||
err := data.NewAIResponseResultService().BatchDeleteAIResponseResult(ids)
|
||||
if err != nil {
|
||||
return "删除失败"
|
||||
}
|
||||
return "删除成功"
|
||||
}
|
||||
|
||||
func (a *App) GetAiRecommendStocksList(query models.AiRecommendStocksQuery) *models.AiRecommendStocksPageData {
|
||||
page, err := data.NewAiRecommendStocksService().GetAiRecommendStocksList(&query)
|
||||
if err != nil {
|
||||
return &models.AiRecommendStocksPageData{}
|
||||
}
|
||||
return page
|
||||
}
|
||||
516
app_darwin.go
516
app_darwin.go
@@ -6,303 +6,168 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/gen2brain/beeep"
|
||||
"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"
|
||||
"strings"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/mathutil"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
cache *freecache.Cache
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
cacheSize := 512 * 1024
|
||||
cache := freecache.NewCache(cacheSize)
|
||||
return &App{
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// startup is called at application startup
|
||||
// startup 在应用程序启动时调用
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
defer PanicHandler()
|
||||
runtime.EventsOn(ctx, "frontendError", func(optionalData ...interface{}) {
|
||||
logger.SugaredLogger.Errorf("Frontend error: %v\n", optionalData)
|
||||
})
|
||||
logger.SugaredLogger.Infof("Version:%s", Version)
|
||||
// Perform your setup here
|
||||
a.ctx = ctx
|
||||
|
||||
// TODO 创建系统托盘
|
||||
// 监听设置更新事件
|
||||
runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) {
|
||||
config := data.GetSettingConfig()
|
||||
//setMap := optionalData[0].(map[string]interface{})
|
||||
//
|
||||
//// 将 map 转换为 JSON 字节切片
|
||||
//jsonData, err := json.Marshal(setMap)
|
||||
//if err != nil {
|
||||
// logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
|
||||
// return
|
||||
//}
|
||||
//// 将 JSON 字节切片解析到结构体中
|
||||
//err = json.Unmarshal(jsonData, config)
|
||||
//if err != nil {
|
||||
// logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
|
||||
// return
|
||||
//}
|
||||
|
||||
logger.SugaredLogger.Infof("updateSettings config:%+v", config)
|
||||
if config.DarkTheme {
|
||||
runtime.WindowSetBackgroundColour(ctx, 27, 38, 54, 1)
|
||||
runtime.WindowSetDarkTheme(ctx)
|
||||
} else {
|
||||
runtime.WindowSetBackgroundColour(ctx, 255, 255, 255, 1)
|
||||
runtime.WindowSetLightTheme(ctx)
|
||||
}
|
||||
runtime.WindowReloadApp(ctx)
|
||||
})
|
||||
|
||||
// 创建 macOS 托盘
|
||||
go func() {
|
||||
// 使用 Beeep 库替代 Windows 的托盘库
|
||||
err := beeep.Notify("go-stock", "应用程序已启动", "")
|
||||
if err != nil {
|
||||
log.Fatalf("系统通知失败: %v", err)
|
||||
}
|
||||
}()
|
||||
go setUpScreen(a)
|
||||
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
|
||||
}
|
||||
|
||||
func checkUpdate(a *App) {
|
||||
releaseVersion := &models.GitHubReleaseVersion{}
|
||||
_, err := resty.New().R().
|
||||
SetResult(releaseVersion).
|
||||
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
|
||||
func setUpScreen(a *App) {
|
||||
screens, _ := runtime.ScreenGetAll(a.ctx)
|
||||
if len(screens) == 0 {
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
|
||||
if releaseVersion.TagName != Version {
|
||||
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
|
||||
}
|
||||
screen := screens[0]
|
||||
sw, sh := screen.Width, screen.Height
|
||||
|
||||
// macOS 菜单栏 + Dock 留出空间
|
||||
topBarHeight := 22
|
||||
dockHeight := 56
|
||||
verticalMargin := topBarHeight + dockHeight
|
||||
|
||||
// 设置窗口为屏幕 80% 宽 × 可用高度 90%
|
||||
w := int(float64(sw) * 0.8)
|
||||
h := int(float64(sh-verticalMargin) * 0.9)
|
||||
|
||||
runtime.WindowSetSize(a.ctx, w, h)
|
||||
runtime.WindowCenter(a.ctx)
|
||||
}
|
||||
|
||||
// domReady is called after front-end resources have been loaded
|
||||
func (a *App) domReady(ctx context.Context) {
|
||||
// Add your action here
|
||||
//定时更新数据
|
||||
go func() {
|
||||
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
interval := config.RefreshInterval
|
||||
if interval <= 0 {
|
||||
interval = 1
|
||||
}
|
||||
ticker := time.NewTicker(time.Second * time.Duration(interval))
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
if isTradingTime(time.Now()) {
|
||||
MonitorStockPrices(a)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * time.Duration(60))
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
telegraph := refreshTelegraphList()
|
||||
if telegraph != nil {
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
|
||||
go MonitorStockPrices(a)
|
||||
|
||||
//检查新版本
|
||||
go func() {
|
||||
checkUpdate(a)
|
||||
}()
|
||||
}
|
||||
|
||||
func refreshTelegraphList() *[]string {
|
||||
url := "https://www.cls.cn/telegraph"
|
||||
response, err := resty.New().R().
|
||||
SetHeader("Referer", "https://www.cls.cn/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(fmt.Sprintf(url))
|
||||
// OnSecondInstanceLaunch 处理第二实例启动时的通知
|
||||
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
|
||||
err := beeep.Notify("go-stock", "程序已经在运行了", "")
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
logger.SugaredLogger.Error(err)
|
||||
}
|
||||
//logger.SugaredLogger.Info(string(response.Body()))
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
|
||||
if err != nil {
|
||||
return &[]string{}
|
||||
}
|
||||
var telegraph []string
|
||||
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
|
||||
//logger.SugaredLogger.Info(selection.Text())
|
||||
telegraph = append(telegraph, selection.Text())
|
||||
})
|
||||
return &telegraph
|
||||
}
|
||||
|
||||
// isTradingDay 判断是否是交易日
|
||||
func isTradingDay(date time.Time) bool {
|
||||
weekday := date.Weekday()
|
||||
// 判断是否是周末
|
||||
if weekday == time.Saturday || weekday == time.Sunday {
|
||||
return false
|
||||
}
|
||||
// 这里可以添加具体的节假日判断逻辑
|
||||
// 例如:判断是否是春节、国庆节等
|
||||
return true
|
||||
}
|
||||
|
||||
// isTradingTime 判断是否是交易时间
|
||||
func isTradingTime(date time.Time) bool {
|
||||
if !isTradingDay(date) {
|
||||
return false
|
||||
}
|
||||
|
||||
hour, minute, _ := date.Clock()
|
||||
|
||||
// 判断是否在9:15到11:30之间
|
||||
if (hour == 9 && minute >= 15) || (hour == 10) || (hour == 11 && minute <= 30) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 判断是否在13:00到15:00之间
|
||||
if (hour == 13) || (hour == 14) || (hour == 15 && minute <= 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
|
||||
func MonitorStockPrices(a *App) {
|
||||
dest := &[]data.FollowedStock{}
|
||||
db.Dao.Model(&data.FollowedStock{}).Find(dest)
|
||||
total := float64(0)
|
||||
//for _, follow := range *dest {
|
||||
// stockData := getStockInfo(follow)
|
||||
// total += stockData.ProfitAmountToday
|
||||
// price, _ := convertor.ToFloat(stockData.Price)
|
||||
// if stockData.PrePrice != price {
|
||||
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
|
||||
// }
|
||||
//}
|
||||
|
||||
// 股票信息处理逻辑
|
||||
stockInfos := GetStockInfos(*dest...)
|
||||
for _, stockInfo := range *stockInfos {
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
|
||||
total += stockInfo.ProfitAmountToday
|
||||
price, _ := convertor.ToFloat(stockInfo.Price)
|
||||
|
||||
if stockInfo.PrePrice != price {
|
||||
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总收益并更新状态
|
||||
if total != 0 {
|
||||
// title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
|
||||
// systray.SetTooltip(title)
|
||||
// 使用通知替代 systray 更新 Tooltip
|
||||
title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
|
||||
|
||||
// 发送通知显示实时数据
|
||||
err := beeep.Notify("go-stock", title, "")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("发送通知失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 触发实时利润事件
|
||||
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
|
||||
//runtime.WindowSetTitle(a.ctx, title)
|
||||
|
||||
}
|
||||
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
|
||||
stockCodes := make([]string, 0)
|
||||
for _, follow := range follows {
|
||||
stockCodes = append(stockCodes, follow.StockCode)
|
||||
}
|
||||
stockData, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
|
||||
|
||||
// onReady 在应用程序准备好时调用
|
||||
func onReady(a *App) {
|
||||
// 初始化操作
|
||||
logger.SugaredLogger.Infof("onReady")
|
||||
|
||||
// 使用 Beeep 发送通知
|
||||
err := beeep.Notify("go-stock", "应用程序已准备就绪", "")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("get stock code real time data error:%s", err.Error())
|
||||
return nil
|
||||
log.Fatalf("系统通知失败: %v", err)
|
||||
}
|
||||
stockInfos := make([]data.StockInfo, 0)
|
||||
for _, info := range *stockData {
|
||||
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
|
||||
return follow.StockCode == info.Code
|
||||
})
|
||||
if ok {
|
||||
addStockFollowData(v, &info)
|
||||
stockInfos = append(stockInfos, info)
|
||||
}
|
||||
}
|
||||
return &stockInfos
|
||||
}
|
||||
func getStockInfo(follow data.FollowedStock) *data.StockInfo {
|
||||
stockCode := follow.StockCode
|
||||
stockDatas, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
|
||||
if err != nil || len(*stockDatas) == 0 {
|
||||
return &data.StockInfo{}
|
||||
}
|
||||
stockData := (*stockDatas)[0]
|
||||
addStockFollowData(follow, &stockData)
|
||||
return &stockData
|
||||
|
||||
// 显示应用窗口
|
||||
runtime.WindowShow(a.ctx)
|
||||
|
||||
// 在 macOS 上没有系统托盘图标菜单,通常我们通过通知或其他方式提供与用户交互的界面
|
||||
}
|
||||
|
||||
func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
|
||||
stockData.PrePrice = follow.Price //上次当前价格
|
||||
stockData.Sort = follow.Sort
|
||||
stockData.CostPrice = follow.CostPrice //成本价
|
||||
stockData.CostVolume = follow.Volume //成本量
|
||||
stockData.AlarmChangePercent = follow.AlarmChangePercent
|
||||
stockData.AlarmPrice = follow.AlarmPrice
|
||||
|
||||
//当前价格
|
||||
price, _ := convertor.ToFloat(stockData.Price)
|
||||
//当前价格为0 时 使用卖一价格作为当前价格
|
||||
if price == 0 {
|
||||
price, _ = convertor.ToFloat(stockData.A1P)
|
||||
}
|
||||
//当前价格依然为0 时 使用买一报价作为当前价格
|
||||
if price == 0 {
|
||||
price, _ = convertor.ToFloat(stockData.B1P)
|
||||
}
|
||||
|
||||
//昨日收盘价
|
||||
preClosePrice, _ := convertor.ToFloat(stockData.PreClose)
|
||||
|
||||
//当前价格依然为0 时 使用昨日收盘价为当前价格
|
||||
if price == 0 {
|
||||
price = preClosePrice
|
||||
}
|
||||
|
||||
//今日最高价
|
||||
highPrice, _ := convertor.ToFloat(stockData.High)
|
||||
if highPrice == 0 {
|
||||
highPrice, _ = convertor.ToFloat(stockData.Open)
|
||||
}
|
||||
|
||||
//今日最低价
|
||||
lowPrice, _ := convertor.ToFloat(stockData.Low)
|
||||
if lowPrice == 0 {
|
||||
lowPrice, _ = convertor.ToFloat(stockData.Open)
|
||||
}
|
||||
//开盘价
|
||||
//openPrice, _ := convertor.ToFloat(stockData.Open)
|
||||
|
||||
if price > 0 {
|
||||
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
|
||||
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if highPrice > 0 {
|
||||
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if lowPrice > 0 {
|
||||
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
|
||||
}
|
||||
if follow.CostPrice > 0 && follow.Volume > 0 {
|
||||
if price > 0 {
|
||||
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(price-follow.CostPrice, follow.CostPrice)*100, 3)
|
||||
stockData.ProfitAmount = mathutil.RoundToFloat((price-follow.CostPrice)*float64(follow.Volume), 2)
|
||||
stockData.ProfitAmountToday = mathutil.RoundToFloat((price-preClosePrice)*float64(follow.Volume), 2)
|
||||
} else {
|
||||
//未开盘时当前价格为昨日收盘价
|
||||
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
|
||||
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
|
||||
// 未开盘时,今日盈亏为 0
|
||||
stockData.ProfitAmountToday = 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//logger.SugaredLogger.Debugf("stockData:%+v", stockData)
|
||||
if follow.Price != price && price > 0 {
|
||||
go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{
|
||||
"price": price,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// beforeClose is called when the application is about to quit,
|
||||
// either by clicking the window close button or calling runtime.Quit.
|
||||
// Returning true will cause the application to continue, false will continue shutdown as normal.
|
||||
// beforeClose 在应用程序关闭前调用,显示确认对话框
|
||||
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
defer PanicHandler()
|
||||
|
||||
// 在 macOS 上使用 MessageDialog 显示确认窗口
|
||||
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.QuestionDialog,
|
||||
Title: "go-stock",
|
||||
Message: "确定关闭吗?",
|
||||
Buttons: []string{"确定"},
|
||||
Buttons: []string{"确定", "取消"},
|
||||
Icon: icon,
|
||||
CancelButton: "取消",
|
||||
})
|
||||
@@ -311,150 +176,27 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Debugf("dialog:%s", dialog)
|
||||
if dialog == "No" {
|
||||
return true
|
||||
if dialog == "取消" {
|
||||
return true // 如果选择了取消,不关闭应用
|
||||
} else {
|
||||
// 在 macOS 上应用退出时执行清理工作
|
||||
a.cron.Stop() // 停止定时任务
|
||||
return false // 如果选择了确定,继续关闭应用
|
||||
}
|
||||
}
|
||||
|
||||
func getFrameless() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// shutdown is called at application termination
|
||||
func (a *App) shutdown(ctx context.Context) {
|
||||
// Perform your teardown here
|
||||
// systray.Quit()
|
||||
}
|
||||
func getScreenResolution() (int, int, int, int, error) {
|
||||
//user32 := syscall.NewLazyDLL("user32.dll")
|
||||
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
|
||||
//
|
||||
//width, _, _ := getSystemMetrics.Call(0)
|
||||
//height, _, _ := getSystemMetrics.Call(1)
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
func (a *App) Greet(stockCode string) *data.StockInfo {
|
||||
//stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
|
||||
|
||||
follow := &data.FollowedStock{
|
||||
StockCode: stockCode,
|
||||
}
|
||||
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
|
||||
stockInfo := getStockInfo(*follow)
|
||||
return stockInfo
|
||||
}
|
||||
|
||||
func (a *App) Follow(stockCode string) string {
|
||||
return data.NewStockDataApi().Follow(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) UnFollow(stockCode string) string {
|
||||
return data.NewStockDataApi().UnFollow(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) GetFollowList() []data.FollowedStock {
|
||||
return data.NewStockDataApi().GetFollowList()
|
||||
}
|
||||
|
||||
func (a *App) GetStockList(key string) []data.StockBasic {
|
||||
return data.NewStockDataApi().GetStockList(key)
|
||||
}
|
||||
|
||||
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
|
||||
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
|
||||
}
|
||||
|
||||
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
|
||||
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
|
||||
}
|
||||
func (a *App) SetStockSort(sort int64, stockCode string) {
|
||||
data.NewStockDataApi().SetStockSort(sort, stockCode)
|
||||
}
|
||||
func (a *App) SendDingDingMessage(message string, stockCode string) string {
|
||||
ttl, _ := a.cache.TTL([]byte(stockCode))
|
||||
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
|
||||
if ttl > 0 {
|
||||
return ""
|
||||
}
|
||||
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
|
||||
return ""
|
||||
}
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
|
||||
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
|
||||
ttl, _ := a.cache.TTL([]byte(stockCode))
|
||||
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
|
||||
if ttl > 0 {
|
||||
return ""
|
||||
}
|
||||
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
|
||||
return ""
|
||||
}
|
||||
stockInfo := &data.StockInfo{}
|
||||
db.Dao.Model(stockInfo).Where("code = ?", stockCode).First(stockInfo)
|
||||
go data.NewAlertWindowsApi("go-stock消息通知", getMsgTypeName(msgType), GenNotificationMsg(stockInfo), "").SendNotification()
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
func (a *App) NewChat(stock string) string {
|
||||
return data.NewDeepSeekOpenAi().NewChat(stock)
|
||||
}
|
||||
|
||||
func (a *App) NewChatStream(stock, stockCode string) {
|
||||
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
|
||||
for msg := range msgs {
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", msg)
|
||||
}
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
|
||||
}
|
||||
|
||||
func GenNotificationMsg(stockInfo *data.StockInfo) string {
|
||||
Price, err := convertor.ToFloat(stockInfo.Price)
|
||||
if err != nil {
|
||||
Price = 0
|
||||
}
|
||||
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
|
||||
if err != nil {
|
||||
PreClose = 0
|
||||
}
|
||||
var RF float64
|
||||
if PreClose > 0 {
|
||||
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
|
||||
}
|
||||
|
||||
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
|
||||
}
|
||||
|
||||
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
|
||||
func getMsgTypeTTL(msgType int) int {
|
||||
switch msgType {
|
||||
case 1:
|
||||
return 60 * 5
|
||||
case 2:
|
||||
return 60 * 30
|
||||
case 3:
|
||||
return 60 * 30
|
||||
default:
|
||||
return 60 * 5
|
||||
}
|
||||
}
|
||||
|
||||
func getMsgTypeName(msgType int) string {
|
||||
switch msgType {
|
||||
case 1:
|
||||
return "涨跌报警"
|
||||
case 2:
|
||||
return "股价报警"
|
||||
case 3:
|
||||
return "成本价报警"
|
||||
default:
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) UpdateConfig(settings *data.Settings) string {
|
||||
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
|
||||
return data.NewSettingsApi(settings).UpdateConfig()
|
||||
}
|
||||
|
||||
func (a *App) GetConfig() *data.Settings {
|
||||
return data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
return int(1200), int(800), 0, 0, nil
|
||||
}
|
||||
|
||||
@@ -184,10 +184,10 @@ func getMsgTypeName(msgType int) string {
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
func (a *App) UpdateConfig(settings *data.Settings) string {
|
||||
return data.NewSettingsApi(settings).UpdateConfig()
|
||||
func (a *App) UpdateConfig(settingConfig *data.SettingConfig) string {
|
||||
return data.UpdateConfig(settingConfig)
|
||||
}
|
||||
|
||||
func (a *App) GetConfig() *data.Settings {
|
||||
return data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
func (a *App) GetConfig() *data.SettingConfig {
|
||||
return data.GetSettingConfig()
|
||||
}
|
||||
|
||||
49
app_test.go
49
app_test.go
@@ -1,9 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -23,3 +29,46 @@ func TestIsUSTradingTime(t *testing.T) {
|
||||
|
||||
t.Log(IsUSTradingTime(time.Now()))
|
||||
}
|
||||
|
||||
func TestCheckStockBaseInfo(t *testing.T) {
|
||||
db.Init("./data/stock.db")
|
||||
NewApp().CheckStockBaseInfo(context.Background())
|
||||
}
|
||||
|
||||
func TestJson(t *testing.T) {
|
||||
db.Init("./data/stock.db")
|
||||
|
||||
jsonStr := "{\n\t\t\"id\" : 3334,\n\t\t\"created_at\" : \"2025-02-28 16:49:31.8342514+08:00\",\n\t\t\"updated_at\" : \"2025-02-28 16:49:31.8342514+08:00\",\n\t\t\"deleted_at\" : null,\n\t\t\"code\" : \"PUK.US\",\n\t\t\"name\" : \"英国保诚集团\",\n\t\t\"full_name\" : \"\",\n\t\t\"e_name\" : \"\",\n\t\t\"exchange\" : \"NASDAQ\",\n\t\t\"type\" : \"stock\",\n\t\t\"is_del\" : 0,\n\t\t\"bk_name\" : null,\n\t\t\"bk_code\" : null\n\t}"
|
||||
|
||||
v := &models.StockInfoUS{}
|
||||
json.Unmarshal([]byte(jsonStr), v)
|
||||
logger.SugaredLogger.Infof("v:%+v", v)
|
||||
|
||||
db.Dao.Model(v).Updates(v)
|
||||
|
||||
}
|
||||
|
||||
func TestUpdateCheck(t *testing.T) {
|
||||
releaseVersion := &models.GitHubReleaseVersion{}
|
||||
_, err := resty.New().R().
|
||||
SetResult(releaseVersion).
|
||||
SetHeader("Accept", "application/vnd.github+json").
|
||||
SetHeader("X-GitHub-Api-Version", "2022-11-28").
|
||||
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
|
||||
// https://api.github.com/repos/OWNER/REPO/releases/latest
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion)
|
||||
}
|
||||
|
||||
func TestGetScreenResolution(t *testing.T) {
|
||||
x, y, w, h, err := getScreenResolution()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("get screen resolution error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("x:%d,y:%d,w:%d,h:%d", x, y, w, h)
|
||||
|
||||
}
|
||||
|
||||
216
app_windows.go
Normal file
216
app_windows.go
Normal file
@@ -0,0 +1,216 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"time"
|
||||
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/energye/systray"
|
||||
"github.com/go-toast/toast"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// startup is called at application startup
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
defer PanicHandler()
|
||||
runtime.EventsOn(ctx, "frontendError", func(optionalData ...interface{}) {
|
||||
logger.SugaredLogger.Errorf("Frontend error: %v\n", optionalData)
|
||||
})
|
||||
logger.SugaredLogger.Infof("Version:%s", Version)
|
||||
// Perform your setup here
|
||||
a.ctx = ctx
|
||||
|
||||
// 创建系统托盘
|
||||
//systray.RunWithExternalLoop(func() {
|
||||
// onReady(a)
|
||||
//}, func() {
|
||||
// onExit(a)
|
||||
//})
|
||||
runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) {
|
||||
logger.SugaredLogger.Infof("updateSettings : %v\n", optionalData)
|
||||
config := data.GetSettingConfig()
|
||||
//setMap := optionalData[0].(map[string]interface{})
|
||||
//
|
||||
//// 将 map 转换为 JSON 字节切片
|
||||
//jsonData, err := json.Marshal(setMap)
|
||||
//if err != nil {
|
||||
// logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
|
||||
// return
|
||||
//}
|
||||
//// 将 JSON 字节切片解析到结构体中
|
||||
//err = json.Unmarshal(jsonData, config)
|
||||
//if err != nil {
|
||||
// logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
|
||||
// return
|
||||
//}
|
||||
|
||||
logger.SugaredLogger.Infof("updateSettings config:%+v", config)
|
||||
if config.DarkTheme {
|
||||
runtime.WindowSetBackgroundColour(ctx, 27, 38, 54, 1)
|
||||
runtime.WindowSetDarkTheme(ctx)
|
||||
} else {
|
||||
runtime.WindowSetBackgroundColour(ctx, 255, 255, 255, 1)
|
||||
runtime.WindowSetLightTheme(ctx)
|
||||
}
|
||||
runtime.WindowReloadApp(ctx)
|
||||
|
||||
})
|
||||
go systray.Run(func() {
|
||||
onReady(a)
|
||||
}, func() {
|
||||
onExit(a)
|
||||
})
|
||||
|
||||
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
|
||||
}
|
||||
|
||||
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
|
||||
notification := toast.Notification{
|
||||
AppID: "go-stock",
|
||||
Title: "go-stock",
|
||||
Message: "程序已经在运行了",
|
||||
Icon: "",
|
||||
Duration: "short",
|
||||
Audio: toast.Default,
|
||||
}
|
||||
err := notification.Push()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
}
|
||||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
|
||||
func MonitorStockPrices(a *App) {
|
||||
dest := &[]data.FollowedStock{}
|
||||
db.Dao.Model(&data.FollowedStock{}).Find(dest)
|
||||
total := float64(0)
|
||||
//for _, follow := range *dest {
|
||||
// stockData := getStockInfo(follow)
|
||||
// total += stockData.ProfitAmountToday
|
||||
// price, _ := convertor.ToFloat(stockData.Price)
|
||||
// if stockData.PrePrice != price {
|
||||
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
|
||||
// }
|
||||
//}
|
||||
|
||||
stockInfos := GetStockInfos(*dest...)
|
||||
for _, stockInfo := range *stockInfos {
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
if strutil.HasPrefixAny(stockInfo.Code, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) {
|
||||
continue
|
||||
}
|
||||
|
||||
total += stockInfo.ProfitAmountToday
|
||||
price, _ := convertor.ToFloat(stockInfo.Price)
|
||||
|
||||
if stockInfo.PrePrice != price {
|
||||
//logger.SugaredLogger.Infof("-----------sz------------股票代码: %s, 股票名称: %s, 股票价格: %s,盘前盘后:%s", stockInfo.Code, stockInfo.Name, stockInfo.Price, stockInfo.BA)
|
||||
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
|
||||
}
|
||||
|
||||
}
|
||||
if total != 0 {
|
||||
title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
|
||||
systray.SetTooltip(title)
|
||||
}
|
||||
|
||||
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
|
||||
//runtime.WindowSetTitle(a.ctx, title)
|
||||
|
||||
}
|
||||
|
||||
func onReady(a *App) {
|
||||
|
||||
// 初始化操作
|
||||
logger.SugaredLogger.Infof("systray onReady")
|
||||
systray.SetIcon(icon2)
|
||||
systray.SetTitle("go-stock")
|
||||
systray.SetTooltip("go-stock 股票行情实时获取")
|
||||
// 创建菜单项
|
||||
show := systray.AddMenuItem("显示", "显示应用程序")
|
||||
show.Click(func() {
|
||||
//logger.SugaredLogger.Infof("显示应用程序")
|
||||
runtime.WindowShow(a.ctx)
|
||||
})
|
||||
hide := systray.AddMenuItem("隐藏", "隐藏应用程序")
|
||||
hide.Click(func() {
|
||||
//logger.SugaredLogger.Infof("隐藏应用程序")
|
||||
runtime.WindowHide(a.ctx)
|
||||
})
|
||||
systray.AddSeparator()
|
||||
mQuitOrig := systray.AddMenuItem("退出", "退出应用程序")
|
||||
mQuitOrig.Click(func() {
|
||||
//logger.SugaredLogger.Infof("退出应用程序")
|
||||
runtime.Quit(a.ctx)
|
||||
})
|
||||
systray.SetOnRClick(func(menu systray.IMenu) {
|
||||
menu.ShowMenu()
|
||||
//logger.SugaredLogger.Infof("SetOnRClick")
|
||||
})
|
||||
systray.SetOnClick(func(menu systray.IMenu) {
|
||||
//logger.SugaredLogger.Infof("SetOnClick")
|
||||
menu.ShowMenu()
|
||||
})
|
||||
systray.SetOnDClick(func(menu systray.IMenu) {
|
||||
menu.ShowMenu()
|
||||
//logger.SugaredLogger.Infof("SetOnDClick")
|
||||
})
|
||||
}
|
||||
|
||||
// beforeClose is called when the application is about to quit,
|
||||
// either by clicking the window close button or calling runtime.Quit.
|
||||
// Returning true will cause the application to continue, false will continue shutdown as normal.
|
||||
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
defer PanicHandler()
|
||||
|
||||
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.QuestionDialog,
|
||||
Title: "go-stock",
|
||||
Message: "确定关闭吗?",
|
||||
Buttons: []string{"确定"},
|
||||
Icon: icon,
|
||||
CancelButton: "取消",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
|
||||
return false
|
||||
}
|
||||
logger.SugaredLogger.Debugf("dialog:%s", dialog)
|
||||
if dialog == "No" {
|
||||
return true
|
||||
} else {
|
||||
systray.Quit()
|
||||
a.cron.Stop()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func getFrameless() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func getScreenResolution() (int, int, int, int, error) {
|
||||
//user32 := syscall.NewLazyDLL("user32.dll")
|
||||
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
|
||||
//
|
||||
//width, _, _ := getSystemMetrics.Call(0)
|
||||
//height, _, _ := getSystemMetrics.Call(1)
|
||||
//return int(width), int(height), 1456, 768, nil
|
||||
|
||||
return int(1366), int(768), 1456, 768, nil
|
||||
}
|
||||
93
backend/agent/agent.go
Normal file
93
backend/agent/agent.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go-stock/backend/agent/tools"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/logger"
|
||||
"time"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/model/ark"
|
||||
"github.com/cloudwego/eino-ext/components/model/deepseek"
|
||||
"github.com/cloudwego/eino-ext/components/model/openai"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/compose"
|
||||
"github.com/cloudwego/eino/flow/agent/react"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// GetStockAiAgent @Author spark
|
||||
// @Date 2025/8/4 16:17
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
func GetStockAiAgent(ctx *context.Context, aiConfig data.AIConfig) *react.Agent {
|
||||
logger.SugaredLogger.Infof("GetStockAiAgent aiConfig: %v", aiConfig)
|
||||
temperature := float32(aiConfig.Temperature)
|
||||
var toolableChatModel model.ToolCallingChatModel
|
||||
var err error
|
||||
if aiConfig.BaseUrl == "https://ark.cn-beijing.volces.com/api/v3" {
|
||||
toolableChatModel, err = ark.NewChatModel(context.Background(), &ark.ChatModelConfig{
|
||||
BaseURL: aiConfig.BaseUrl,
|
||||
Model: aiConfig.ModelName,
|
||||
APIKey: aiConfig.ApiKey,
|
||||
MaxTokens: &aiConfig.MaxTokens,
|
||||
Temperature: &temperature,
|
||||
})
|
||||
|
||||
} else if aiConfig.BaseUrl == "https://api.deepseek.com" {
|
||||
toolableChatModel, err = deepseek.NewChatModel(*ctx, &deepseek.ChatModelConfig{
|
||||
BaseURL: aiConfig.BaseUrl,
|
||||
Model: aiConfig.ModelName,
|
||||
APIKey: aiConfig.ApiKey,
|
||||
Timeout: time.Duration(aiConfig.TimeOut) * time.Second,
|
||||
MaxTokens: aiConfig.MaxTokens,
|
||||
Temperature: temperature,
|
||||
})
|
||||
|
||||
} else {
|
||||
toolableChatModel, err = openai.NewChatModel(*ctx, &openai.ChatModelConfig{
|
||||
BaseURL: aiConfig.BaseUrl,
|
||||
Model: aiConfig.ModelName,
|
||||
APIKey: aiConfig.ApiKey,
|
||||
Timeout: time.Duration(aiConfig.TimeOut) * time.Second,
|
||||
MaxTokens: &aiConfig.MaxTokens,
|
||||
Temperature: &temperature,
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return nil
|
||||
}
|
||||
// 初始化所需的 tools
|
||||
aiTools := compose.ToolsNodeConfig{
|
||||
Tools: []tool.BaseTool{
|
||||
tools.GetQueryEconomicDataTool(),
|
||||
tools.GetQueryStockPriceInfoTool(),
|
||||
tools.GetQueryStockCodeInfoTool(),
|
||||
tools.GetQueryMarketNewsTool(),
|
||||
tools.GetChoiceStockByIndicatorsTool(),
|
||||
tools.GetStockKLineTool(),
|
||||
tools.GetInteractiveAnswerDataTool(),
|
||||
tools.GetFinancialReportTool(),
|
||||
tools.GetQueryStockNewsTool(),
|
||||
tools.GetIndustryResearchReportTool(),
|
||||
tools.GetQueryBKDictTool(),
|
||||
},
|
||||
}
|
||||
// 创建 agent
|
||||
agent, err := react.NewAgent(*ctx, &react.AgentConfig{
|
||||
ToolCallingModel: toolableChatModel,
|
||||
ToolsConfig: aiTools,
|
||||
MaxStep: len(aiTools.Tools)*1 + 3,
|
||||
MessageModifier: func(ctx context.Context, input []*schema.Message) []*schema.Message {
|
||||
return input
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return nil
|
||||
}
|
||||
return agent
|
||||
}
|
||||
91
backend/agent/agent_api.go
Normal file
91
backend/agent/agent_api.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/cloudwego/eino/compose"
|
||||
"github.com/cloudwego/eino/flow/agent"
|
||||
"github.com/cloudwego/eino/flow/agent/react"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/samber/lo"
|
||||
"go-stock/backend/agent/tool_logger"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/logger"
|
||||
"io"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/7 9:07
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type StockAiAgent struct {
|
||||
*react.Agent
|
||||
}
|
||||
|
||||
func NewStockAiAgentApi() *StockAiAgent {
|
||||
return &StockAiAgent{}
|
||||
}
|
||||
|
||||
func (receiver StockAiAgent) newStockAiAgent(ctx *context.Context, aiConfigId int) *StockAiAgent {
|
||||
settingConfig := data.GetSettingConfig()
|
||||
aiConfig, ok := lo.Find(settingConfig.AiConfigs, func(item *data.AIConfig) bool {
|
||||
return uint(aiConfigId) == item.ID
|
||||
})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &StockAiAgent{
|
||||
Agent: GetStockAiAgent(ctx, *aiConfig),
|
||||
}
|
||||
}
|
||||
|
||||
func (receiver StockAiAgent) Chat(question string, aiConfigId int, sysPromptId *int) chan *schema.Message {
|
||||
ch := make(chan *schema.Message, 512)
|
||||
ctx := context.Background()
|
||||
stockAiAgent := receiver.newStockAiAgent(&ctx, aiConfigId)
|
||||
|
||||
sysPrompt := ""
|
||||
if sysPromptId == nil || *sysPromptId == 0 {
|
||||
sysPrompt = "你现在扮演一位拥有20年实战经验的顶级股票投资大师,精通价值投资、趋势交易、量化分析等多种策略。你擅长结合宏观经济、行业周期和企业基本面进行全方位、精准的多维分析,尤其对A股、港股、美股市场有深刻理解,始终秉持“风险控制第一”的原则,善于用通俗易懂的方式传授投资智慧。"
|
||||
} else {
|
||||
sysPrompt = data.NewPromptTemplateApi().GetPromptTemplateByID(*sysPromptId)
|
||||
}
|
||||
agentOption := []agent.AgentOption{
|
||||
agent.WithComposeOptions(compose.WithCallbacks(&tool_logger.LoggerCallback{MessageChanel: ch})),
|
||||
//react.WithChatModelOptions(ark.WithCache(cacheOption)),
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
sr, err := stockAiAgent.Stream(ctx, []*schema.Message{
|
||||
{
|
||||
Role: schema.System,
|
||||
Content: sysPrompt,
|
||||
},
|
||||
{
|
||||
Role: schema.User,
|
||||
Content: question,
|
||||
},
|
||||
}, agentOption...)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("stream error: %v", err)
|
||||
return
|
||||
}
|
||||
defer sr.Close()
|
||||
for {
|
||||
msg, err := sr.Recv()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
// finish
|
||||
break
|
||||
}
|
||||
// error
|
||||
logger.SugaredLogger.Errorf("failed to recv: %v", err)
|
||||
break
|
||||
}
|
||||
logger.SugaredLogger.Infof("stream: %s", msg.String())
|
||||
ch <- msg
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
88
backend/agent/agent_test.go
Normal file
88
backend/agent/agent_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"go-stock/backend/agent/tool_logger"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudwego/eino/compose"
|
||||
"github.com/cloudwego/eino/flow/agent"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/duke-git/lancet/v2/fileutil"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/4 17:32
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestGetStockAiAgent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db.Init("../../data/stock.db")
|
||||
config := data.GetSettingConfig()
|
||||
aiAgent := GetStockAiAgent(&ctx, *config.AiConfigs[0])
|
||||
|
||||
opt := []agent.AgentOption{
|
||||
agent.WithComposeOptions(compose.WithCallbacks(&tool_logger.LoggerCallback{})),
|
||||
//react.WithChatModelOptions(ark.WithCache(cacheOption)),
|
||||
}
|
||||
|
||||
sr, err := aiAgent.Stream(ctx, []*schema.Message{
|
||||
{
|
||||
Role: schema.System,
|
||||
Content: config.Settings.Prompt + "",
|
||||
},
|
||||
{
|
||||
Role: schema.User,
|
||||
Content: "结合以上提供的宏观经济数据/市场指数行情/国内外市场资讯/电报/会议/事件/投资者关注的问题,\n结合宏观经济,事件驱动,政策支持,投资者关注的问题,分析当前市场情绪和热点 找出有潜力/优质的板块/行业/概念/标的/主题,\n多因子深度分析计算上涨或下跌的逻辑和概率,\n最后按风险和投资周期给出具体推荐标的操作建议",
|
||||
},
|
||||
}, opt...)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("stream error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer sr.Close() // remember to close the stream
|
||||
|
||||
md := strings.Builder{}
|
||||
for {
|
||||
msg, err := sr.Recv()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
// finish
|
||||
break
|
||||
}
|
||||
// error
|
||||
logger.SugaredLogger.Errorf("failed to recv: %v", err)
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("stream recv: %v", msg)
|
||||
if msg.ReasoningContent != "" {
|
||||
md.WriteString(msg.ReasoningContent)
|
||||
}
|
||||
if msg.Content != "" {
|
||||
md.WriteString(msg.Content)
|
||||
}
|
||||
}
|
||||
logger.SugaredLogger.Info(md.String())
|
||||
//logger.SugaredLogger.Infof("stream done:\n%s", md.String())
|
||||
}
|
||||
|
||||
func TestAgent(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
md := strings.Builder{}
|
||||
ch := NewStockAiAgentApi().Chat("分析一下立讯精密", 0, nil)
|
||||
for message := range ch {
|
||||
logger.SugaredLogger.Infof("res:%s", message.String())
|
||||
md.WriteString(message.String())
|
||||
}
|
||||
logger.SugaredLogger.Info(md.String())
|
||||
fileutil.WriteStringToFile("../../data/result.md", md.String(), false)
|
||||
}
|
||||
98
backend/agent/tool_logger/tool_logger.go
Normal file
98
backend/agent/tool_logger/tool_logger.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package tool_logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"go-stock/backend/logger"
|
||||
"io"
|
||||
|
||||
"github.com/cloudwego/eino/callbacks"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/flow/agent/react"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/5 10:21
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
type LoggerCallback struct {
|
||||
MessageChanel chan *schema.Message
|
||||
callbacks.HandlerBuilder // 可以用 callbacks.HandlerBuilder 来辅助实现 callback
|
||||
}
|
||||
|
||||
func (cb *LoggerCallback) OnStart(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
|
||||
logger.SugaredLogger.Infof("==================")
|
||||
inputStr, _ := json.MarshalIndent(input, "", " ") // nolint: byted_s_returned_err_check
|
||||
logger.SugaredLogger.Infof("[OnStart] %s\n", string(inputStr))
|
||||
|
||||
modelCallbackInput := model.ConvCallbackInput(input)
|
||||
if modelCallbackInput != nil {
|
||||
for _, message := range modelCallbackInput.Messages {
|
||||
cb.MessageChanel <- message
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (cb *LoggerCallback) OnEnd(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
|
||||
logger.SugaredLogger.Infof("=========[OnEnd]=========")
|
||||
outputStr, _ := json.MarshalIndent(output, "", " ") // nolint: byted_s_returned_err_check
|
||||
logger.SugaredLogger.Infof(string(outputStr))
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (cb *LoggerCallback) OnError(ctx context.Context, info *callbacks.RunInfo, err error) context.Context {
|
||||
logger.SugaredLogger.Infof("=========[OnError]=========")
|
||||
logger.SugaredLogger.Infof("%s", err.Error())
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (cb *LoggerCallback) OnEndWithStreamOutput(ctx context.Context, info *callbacks.RunInfo,
|
||||
output *schema.StreamReader[callbacks.CallbackOutput]) context.Context {
|
||||
|
||||
var graphInfoName = react.GraphName
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.SugaredLogger.Infof("[OnEndStream] panic err:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
defer output.Close() // remember to close the stream in defer
|
||||
|
||||
logger.SugaredLogger.Infof("=========[OnEndStream]=========")
|
||||
for {
|
||||
frame, err := output.Recv()
|
||||
if errors.Is(err, io.EOF) {
|
||||
// finish
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("internal error: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
s, err := json.Marshal(frame)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("internal error: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if info.Name == graphInfoName { // 仅打印 graph 的输出, 否则每个 stream 节点的输出都会打印一遍
|
||||
logger.SugaredLogger.Infof("%s: %s\n", info.Name, string(s))
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (cb *LoggerCallback) OnStartWithStreamInput(ctx context.Context, info *callbacks.RunInfo,
|
||||
input *schema.StreamReader[callbacks.CallbackInput]) context.Context {
|
||||
defer input.Close()
|
||||
return ctx
|
||||
}
|
||||
34
backend/agent/tools/bk_dict_tool.go
Normal file
34
backend/agent/tools/bk_dict_tool.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"go-stock/backend/data"
|
||||
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/coocood/freecache"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/9/27 14:09
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type ToolQueryBKDict struct{}
|
||||
|
||||
func (t ToolQueryBKDict) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryBKDictInfo",
|
||||
Desc: "获取所有板块/行业名称或者代码(bkCode,bkName)",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t ToolQueryBKDict) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
resp := data.NewMarketNewsApi().EMDictCode("016", freecache.NewCache(100))
|
||||
bytes, err := json.Marshal(resp)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func GetQueryBKDictTool() tool.InvokableTool {
|
||||
return &ToolQueryBKDict{}
|
||||
}
|
||||
140
backend/agent/tools/choice_stock_by_indicators_tool.go
Normal file
140
backend/agent/tools/choice_stock_by_indicators_tool.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go-stock/backend/data"
|
||||
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/5 11:17
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetChoiceStockByIndicatorsTool() tool.InvokableTool {
|
||||
return &ChoiceStockByIndicators{}
|
||||
}
|
||||
|
||||
type ChoiceStockByIndicators struct {
|
||||
}
|
||||
|
||||
func (c ChoiceStockByIndicators) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "ChoiceStockByIndicators",
|
||||
Desc: "根据自然语言筛选股票,返回自然语言选股条件要求的股票所有相关数据。输入股票名称可以获取当前股票最新的股价交易数据和基础财务指标信息,多个股票名称使用,分隔。",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"words": {
|
||||
Type: "string",
|
||||
Desc: "选股自然语言。" +
|
||||
"例:上海贝岭,macd,rsi,kdj,boll,5日均线,14日均线,30日均线,60日均线,成交量,OBV,EMA" +
|
||||
"例1:创新药,半导体;PE<30;净利润增长率>50%。 " +
|
||||
"例2:上证指数,科创50。 " +
|
||||
"例3:长电科技,上海贝岭。" +
|
||||
"例4:长电科技,上海贝岭;KDJ,MACD,RSI,BOLL,主力净流入/流出" +
|
||||
"例5:换手率大于3%小于25%.量比1以上. 10日内有过涨停.股价处于峰值的二分之一以下.流通股本<100亿.当日和连续四日净流入;股价在20日均线以上.分时图股价在均线之上.热门板块下涨幅领先的A股. 当日量能20000手以上.沪深个股.近一年市盈率波动小于150%.MACD金叉;不要ST股及不要退市股,非北交所,每股收益>0。" +
|
||||
"例6:沪深主板.流通市值小于100亿.市值大于10亿.60分钟dif大于dea.60分钟skdj指标k值大于d值.skdj指标k值小于90.换手率大于3%.成交额大于1亿元.量比大于2.涨幅大于2%小于7%.股价大于5小于50.创业板.10日均线大于20日均线;不要ST股及不要退市股;不要北交所;不要科创板;不要创业板。" +
|
||||
"例7:股价在20日线上,一月之内涨停次数>=1,量比大于1,换手率大于3%,流通市值大于 50亿小于200亿。" +
|
||||
"例8:基本条件:前期有爆量,回调到 10 日线,当日是缩量阴线,均线趋势向上。;优选条件:一月之内涨停次数>=1" +
|
||||
"例9:今日涨幅大于等于2%小于等于9%;量比大于等于1.1小于等于5;换手率大于等于5%小于等于20%;市值大于等于30小于等于300亿;5日、10日、30日、60日均线、5周、10周、30周、60周均线多头排列",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c ChoiceStockByIndicators) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
parms := map[string]any{}
|
||||
err := json.Unmarshal([]byte(argumentsInJSON), &parms)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content := "无符合条件的数据"
|
||||
words := parms["words"].(string)
|
||||
res := data.NewSearchStockApi(words).SearchStock(random.RandInt(5, 20))
|
||||
if convertor.ToString(res["code"]) == "100" {
|
||||
resData := res["data"].(map[string]any)
|
||||
result := resData["result"].(map[string]any)
|
||||
dataList := result["dataList"].([]any)
|
||||
columns := result["columns"].([]any)
|
||||
headers := map[string]string{}
|
||||
for _, v := range columns {
|
||||
//logger.SugaredLogger.Infof("v:%+v", v)
|
||||
d := v.(map[string]any)
|
||||
//logger.SugaredLogger.Infof("key:%s title:%s dateMsg:%s unit:%s", d["key"], d["title"], d["dateMsg"], d["unit"])
|
||||
title := convertor.ToString(d["title"])
|
||||
if convertor.ToString(d["dateMsg"]) != "" {
|
||||
title = title + "[" + convertor.ToString(d["dateMsg"]) + "]"
|
||||
}
|
||||
if convertor.ToString(d["unit"]) != "" {
|
||||
title = title + "(" + convertor.ToString(d["unit"]) + ")"
|
||||
}
|
||||
headers[d["key"].(string)] = title
|
||||
}
|
||||
table := &[]map[string]any{}
|
||||
for _, v := range dataList {
|
||||
d := v.(map[string]any)
|
||||
tmp := map[string]any{}
|
||||
for key, title := range headers {
|
||||
tmp[title] = convertor.ToString(d[key])
|
||||
}
|
||||
*table = append(*table, tmp)
|
||||
}
|
||||
jsonData, _ := json.Marshal(*table)
|
||||
markdownTable, _ := JSONToMarkdownTable(jsonData)
|
||||
//logger.SugaredLogger.Infof("markdownTable=\n%s", markdownTable)
|
||||
content = "\r\n### 工具筛选出的股票数据:\r\n" + markdownTable + "\r\n"
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// JSONToMarkdownTable 将JSON数据转换为Markdown表格
|
||||
func JSONToMarkdownTable(jsonData []byte) (string, error) {
|
||||
var data []map[string]interface{}
|
||||
err := json.Unmarshal(jsonData, &data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// 获取表头
|
||||
headers := []string{}
|
||||
for key := range data[0] {
|
||||
headers = append(headers, key)
|
||||
}
|
||||
|
||||
// 构建表头行
|
||||
headerRow := "|"
|
||||
for _, header := range headers {
|
||||
headerRow += fmt.Sprintf(" %s |", header)
|
||||
}
|
||||
headerRow += "\n"
|
||||
|
||||
// 构建分隔行
|
||||
separatorRow := "|"
|
||||
for range headers {
|
||||
separatorRow += " --- |"
|
||||
}
|
||||
separatorRow += "\n"
|
||||
|
||||
// 构建数据行
|
||||
bodyRows := ""
|
||||
for _, rowData := range data {
|
||||
bodyRow := "|"
|
||||
for _, header := range headers {
|
||||
value := rowData[header]
|
||||
bodyRow += fmt.Sprintf(" %v |", value)
|
||||
}
|
||||
bodyRows += bodyRow + "\n"
|
||||
}
|
||||
|
||||
return headerRow + separatorRow + bodyRows, nil
|
||||
}
|
||||
35
backend/agent/tools/common.go
Normal file
35
backend/agent/tools/common.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/5 17:20
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetStockCode(dcCode string) string {
|
||||
if strutil.ContainsAny(dcCode, []string{"."}) {
|
||||
sp := strings.Split(dcCode, ".")
|
||||
return strings.ToLower(sp[1] + sp[0])
|
||||
}
|
||||
|
||||
//北京证券交易所 8(83、87、88 等) 创新型中小企业(专精特新为主)
|
||||
//上海证券交易所 6(60、688 等) 大盘蓝筹、科创板(高新技术)
|
||||
//深圳证券交易所 0、3(000、002、30 等) 中小盘、创业板(成长型创新企业)
|
||||
switch dcCode[0:1] {
|
||||
case "8":
|
||||
return "bj" + dcCode
|
||||
case "9":
|
||||
return "bj" + dcCode
|
||||
case "6":
|
||||
return "sh" + dcCode
|
||||
case "0":
|
||||
return "sz" + dcCode
|
||||
case "3":
|
||||
return "sz" + dcCode
|
||||
}
|
||||
return dcCode
|
||||
}
|
||||
79
backend/agent/tools/economic_data_tool.go
Normal file
79
backend/agent/tools/economic_data_tool.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/util"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/4 16:38
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetQueryEconomicDataTool() tool.InvokableTool {
|
||||
return &ToolQueryEconomicData{}
|
||||
}
|
||||
|
||||
type ToolQueryEconomicData struct {
|
||||
}
|
||||
|
||||
func (t ToolQueryEconomicData) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryEconomicData",
|
||||
Desc: "查询宏观经济数据(GDP,CPI,PPI,PMI)",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"flag": {
|
||||
Type: "string",
|
||||
Desc: "all:宏观经济数据(GDP,CPI,PPI,PMI);GDP:国内生产总值;CPI:居民消费价格指数;PPI:工业品出厂价格指数;PMI:采购经理人指数",
|
||||
Required: false,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t ToolQueryEconomicData) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
parms := map[string]any{}
|
||||
err := json.Unmarshal([]byte(argumentsInJSON), &parms)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var market strings.Builder
|
||||
|
||||
switch parms["flag"].(string) {
|
||||
case "GDP":
|
||||
res := data.NewMarketNewsApi().GetGDP()
|
||||
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
|
||||
market.WriteString(md)
|
||||
case "CPI":
|
||||
res2 := data.NewMarketNewsApi().GetCPI()
|
||||
md2 := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res2.CPIResult.Data)
|
||||
market.WriteString(md2)
|
||||
case "PPI":
|
||||
res3 := data.NewMarketNewsApi().GetPPI()
|
||||
md3 := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res3.PPIResult.Data)
|
||||
market.WriteString(md3)
|
||||
case "PMI":
|
||||
res4 := data.NewMarketNewsApi().GetPMI()
|
||||
md4 := util.MarkdownTableWithTitle("商品价格指数(PMI)", res4.PMIResult.Data)
|
||||
market.WriteString(md4)
|
||||
default:
|
||||
res := data.NewMarketNewsApi().GetGDP()
|
||||
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
|
||||
market.WriteString(md)
|
||||
res2 := data.NewMarketNewsApi().GetCPI()
|
||||
md2 := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res2.CPIResult.Data)
|
||||
market.WriteString(md2)
|
||||
res3 := data.NewMarketNewsApi().GetPPI()
|
||||
md3 := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res3.PPIResult.Data)
|
||||
market.WriteString(md3)
|
||||
res4 := data.NewMarketNewsApi().GetPMI()
|
||||
md4 := util.MarkdownTableWithTitle("采购经理人指数(PMI)", res4.PMIResult.Data)
|
||||
market.WriteString(md4)
|
||||
}
|
||||
return market.String(), nil
|
||||
}
|
||||
50
backend/agent/tools/financial_reports_tool.go
Normal file
50
backend/agent/tools/financial_reports_tool.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/data"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/5 15:49
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetFinancialReportTool() tool.InvokableTool {
|
||||
return &FinancialReportTool{}
|
||||
}
|
||||
|
||||
type FinancialReportTool struct {
|
||||
}
|
||||
|
||||
func (f FinancialReportTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "GetFinancialReport",
|
||||
Desc: "查询股票财务报表数据",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"stockCode": {
|
||||
Type: "string",
|
||||
Desc: "股票代码(A股:sh,sz开头;港股hk开头,美股:us开头)不能批量查询",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f FinancialReportTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
stockCode := gjson.Get(argumentsInJSON, "stockCode").String()
|
||||
messages := data.GetFinancialReportsByXUEQIU(GetStockCode(stockCode), 30)
|
||||
if messages == nil || len(*messages) == 0 {
|
||||
return "", fmt.Errorf("没有找到%s的财务报告", stockCode)
|
||||
}
|
||||
md := strings.Builder{}
|
||||
for _, s := range *messages {
|
||||
md.WriteString(s)
|
||||
}
|
||||
return md.String(), nil
|
||||
}
|
||||
69
backend/agent/tools/industry_research_report_tool.go
Normal file
69
backend/agent/tools/industry_research_report_tool.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go-stock/backend/data"
|
||||
log "go-stock/backend/logger"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/9 18:48
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetIndustryResearchReportTool() tool.InvokableTool {
|
||||
return &IndustryResearchReportTool{api: data.NewMarketNewsApi()}
|
||||
}
|
||||
|
||||
type IndustryResearchReportTool struct {
|
||||
api *data.MarketNewsApi
|
||||
}
|
||||
|
||||
func (i IndustryResearchReportTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "GetIndustryResearchReport",
|
||||
Desc: "获取行业/板块研究报告",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"name": {
|
||||
Type: "string",
|
||||
Desc: "行业/板块行业名称",
|
||||
Required: false,
|
||||
},
|
||||
"code": {
|
||||
Type: "string",
|
||||
Desc: "行业/板块代码",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i IndustryResearchReportTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
code := gjson.Get(argumentsInJSON, "code").String()
|
||||
code = strutil.ReplaceWithMap(code, map[string]string{
|
||||
"-": "",
|
||||
"_": "",
|
||||
"bk": "",
|
||||
"BK": "",
|
||||
"bk0": "",
|
||||
"BK0": "",
|
||||
})
|
||||
|
||||
log.SugaredLogger.Debugf("code:%s", code)
|
||||
codeStr := convertor.ToString(code)
|
||||
resp := i.api.IndustryResearchReport(codeStr, 7)
|
||||
md := strings.Builder{}
|
||||
for _, a := range resp {
|
||||
data := a.(map[string]any)
|
||||
md.WriteString(i.api.GetIndustryReportInfo(data["infoCode"].(string)))
|
||||
}
|
||||
log.SugaredLogger.Debugf("codeNum:%s IndustryResearchReport:\n %s", code, md.String())
|
||||
return md.String(), nil
|
||||
}
|
||||
64
backend/agent/tools/interactive_answer_data_tool.go
Normal file
64
backend/agent/tools/interactive_answer_data_tool.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/util"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/5 12:46
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetInteractiveAnswerDataTool() tool.InvokableTool {
|
||||
return &InteractiveAnswerDataTool{}
|
||||
}
|
||||
|
||||
type InteractiveAnswerDataTool struct {
|
||||
}
|
||||
|
||||
func (i InteractiveAnswerDataTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryInteractiveAnswerData",
|
||||
Desc: "获取投资者与上市公司互动问答的数据,反映当前投资者关注的热点问题。",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"page": {
|
||||
Type: "string",
|
||||
Desc: "分页号",
|
||||
Required: true,
|
||||
},
|
||||
"pageSize": {
|
||||
Type: "string",
|
||||
Desc: "分页大小",
|
||||
Required: true,
|
||||
},
|
||||
"keyWord": {
|
||||
Type: "string",
|
||||
Desc: "搜索关键词,多个关键词空格隔开(可输入股票名称或者当前热门板块/行业/概念/标的/事件等)",
|
||||
Required: false,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i InteractiveAnswerDataTool) InvokableRun(ctx context.Context, funcArguments string, opts ...tool.Option) (string, error) {
|
||||
page := gjson.Get(funcArguments, "page").String()
|
||||
pageSize := gjson.Get(funcArguments, "pageSize").String()
|
||||
keyWord := gjson.Get(funcArguments, "keyWord").String()
|
||||
pageNo, err := convertor.ToInt(page)
|
||||
if err != nil {
|
||||
pageNo = 1
|
||||
}
|
||||
pageSizeNum, err := convertor.ToInt(pageSize)
|
||||
if err != nil {
|
||||
pageSizeNum = 50
|
||||
}
|
||||
datas := data.NewMarketNewsApi().InteractiveAnswer(int(pageNo), int(pageSizeNum), keyWord)
|
||||
content := util.MarkdownTableWithTitle("投资互动数据", datas.Results)
|
||||
return content, nil
|
||||
}
|
||||
80
backend/agent/tools/market_news_tool.go
Normal file
80
backend/agent/tools/market_news_tool.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/logger"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/4 16:38
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetQueryMarketNewsTool() tool.InvokableTool {
|
||||
return &QueryMarketNews{}
|
||||
}
|
||||
|
||||
type QueryMarketNews struct {
|
||||
}
|
||||
|
||||
func (q QueryMarketNews) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryMarketNews",
|
||||
Desc: "国内外市场资讯/电报/会议/事件",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (q QueryMarketNews) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
md := strings.Builder{}
|
||||
res := data.NewMarketNewsApi().ClsCalendar()
|
||||
for _, a := range res {
|
||||
bytes, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
//logger.SugaredLogger.Debugf("value: %+v", string(bytes))
|
||||
date := gjson.Get(string(bytes), "calendar_day")
|
||||
md.WriteString("\n### 事件/会议日期:" + date.String())
|
||||
list := gjson.Get(string(bytes), "items")
|
||||
//logger.SugaredLogger.Debugf("value: %+v,list: %+v", date.String(), list)
|
||||
list.ForEach(func(key, value gjson.Result) bool {
|
||||
logger.SugaredLogger.Debugf("key: %+v,value: %+v", key.String(), gjson.Get(value.String(), "title"))
|
||||
md.WriteString("\n- " + gjson.Get(value.String(), "title").String())
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
news := data.NewMarketNewsApi().GetNewsList("", random.RandInt(100, 500))
|
||||
messageText := strings.Builder{}
|
||||
for _, telegraph := range *news {
|
||||
messageText.WriteString("## " + telegraph.Time + ":" + "\n")
|
||||
messageText.WriteString("### " + telegraph.Content + "\n")
|
||||
}
|
||||
md.WriteString("\n### 市场资讯:\n" + messageText.String())
|
||||
|
||||
resp := data.NewMarketNewsApi().TradingViewNews()
|
||||
var newsText strings.Builder
|
||||
for _, a := range *resp {
|
||||
logger.SugaredLogger.Debugf("TradingViewNews: %s", a.Title)
|
||||
newsText.WriteString(a.Title + "\n")
|
||||
}
|
||||
md.WriteString("\n### 全球新闻资讯:\n" + newsText.String())
|
||||
|
||||
reutersNew := data.NewMarketNewsApi().ReutersNew()
|
||||
reutersNewMessageText := strings.Builder{}
|
||||
for _, article := range reutersNew.Result.Articles {
|
||||
reutersNewMessageText.WriteString("## " + article.Title + "\n")
|
||||
reutersNewMessageText.WriteString("### " + article.Description + "\n")
|
||||
}
|
||||
md.WriteString("\n### 外媒全球新闻资讯:\n" + reutersNewMessageText.String())
|
||||
|
||||
return md.String(), nil
|
||||
}
|
||||
49
backend/agent/tools/stock_code_tool.go
Normal file
49
backend/agent/tools/stock_code_tool.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"go-stock/backend/data"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/4 18:25
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetQueryStockCodeInfoTool() tool.InvokableTool {
|
||||
return &QueryStockCodeInfo{}
|
||||
}
|
||||
|
||||
type QueryStockCodeInfo struct {
|
||||
}
|
||||
|
||||
func (q QueryStockCodeInfo) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryStockCodeInfo",
|
||||
Desc: "查询股票/指数信息(股票/指数名称,股票/指数代码,股票/指数拼音,股票/指数拼音首字母,股票/指数交易所等",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"searchWord": {
|
||||
Type: "string",
|
||||
Desc: "股票搜索关键词",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (q QueryStockCodeInfo) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
parms := map[string]any{}
|
||||
err := json.Unmarshal([]byte(argumentsInJSON), &parms)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
stockList := data.NewStockDataApi().GetStockList(parms["searchWord"].(string))
|
||||
marshal, err := json.Marshal(stockList)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(marshal), nil
|
||||
}
|
||||
80
backend/agent/tools/stock_k_line_data_tool.go
Normal file
80
backend/agent/tools/stock_k_line_data_tool.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/data"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/5 11:31
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetStockKLineTool() tool.InvokableTool {
|
||||
return &QueryStockKLine{}
|
||||
}
|
||||
|
||||
type QueryStockKLine struct {
|
||||
}
|
||||
|
||||
func (q QueryStockKLine) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryStockKLine",
|
||||
Desc: "获取股票K线数据。输入股票名称和K线周期,返回股票K线数据。",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"days": {
|
||||
Type: "string",
|
||||
Desc: "日K数据条数。",
|
||||
Required: true,
|
||||
},
|
||||
"stockCode": {
|
||||
Type: "string",
|
||||
Desc: "股票代码(A股:sh,sz开头;港股hk开头,美股:us开头)",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (q QueryStockKLine) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
stockCode := GetStockCode(gjson.Get(argumentsInJSON, "stockCode").String())
|
||||
days := gjson.Get(argumentsInJSON, "days").String()
|
||||
toIntDay, err := convertor.ToInt(days)
|
||||
if err != nil {
|
||||
toIntDay = 90
|
||||
}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh", "hk", "us", "gb_"}) {
|
||||
K := &[]data.KLineData{}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh"}) {
|
||||
K = data.NewStockDataApi().GetKLineData(stockCode, "240", toIntDay)
|
||||
}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"hk", "us", "gb_"}) {
|
||||
K = data.NewStockDataApi().GetHK_KLineData(stockCode, "day", toIntDay)
|
||||
}
|
||||
Kmap := &[]map[string]any{}
|
||||
for _, kline := range *K {
|
||||
mapk := make(map[string]any, 6)
|
||||
mapk["日期"] = kline.Day
|
||||
mapk["开盘价"] = kline.Open
|
||||
mapk["最高价"] = kline.High
|
||||
mapk["最低价"] = kline.Low
|
||||
mapk["收盘价"] = kline.Close
|
||||
Volume, _ := convertor.ToFloat(kline.Volume)
|
||||
mapk["成交量(万手)"] = Volume / 10000.00 / 100.00
|
||||
*Kmap = append(*Kmap, mapk)
|
||||
}
|
||||
jsonData, _ := json.Marshal(Kmap)
|
||||
markdownTable, _ := JSONToMarkdownTable(jsonData)
|
||||
res := "\r\n ### " + stockCode + " " + convertor.ToString(toIntDay) + "日K线数据:\r\n" + markdownTable + "\r\n"
|
||||
return res, nil
|
||||
} else {
|
||||
return "无数据,可能股票代码错误。(A股:sh,sz开头;港股hk开头,美股:us开头)", fmt.Errorf("不支持的股票代码:%s", stockCode)
|
||||
}
|
||||
}
|
||||
42
backend/agent/tools/stock_news_tool.go
Normal file
42
backend/agent/tools/stock_news_tool.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/util"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/5 16:27
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetQueryStockNewsTool() tool.InvokableTool {
|
||||
return &QueryStockNewsTool{}
|
||||
}
|
||||
|
||||
type QueryStockNewsTool struct {
|
||||
}
|
||||
|
||||
func (q QueryStockNewsTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryStockNewsTool",
|
||||
Desc: "按关键词搜索相关市场资讯/新闻",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"searchWords": {
|
||||
Type: "string",
|
||||
Desc: "搜索关键词(多个关键词使用空格分隔)",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (q QueryStockNewsTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
searchWords := gjson.Get(argumentsInJSON, "searchWords").String()
|
||||
res := data.NewMarketNewsApi().CailianpressWeb(searchWords)
|
||||
return util.MarkdownTableWithTitle(searchWords+"市场资讯/新闻", res.List), nil
|
||||
}
|
||||
57
backend/agent/tools/stock_price_info_tool.go
Normal file
57
backend/agent/tools/stock_price_info_tool.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"go-stock/backend/data"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/8/4 17:58
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func GetQueryStockPriceInfoTool() tool.InvokableTool {
|
||||
return &ToolQueryStockPriceInfo{}
|
||||
}
|
||||
|
||||
type ToolQueryStockPriceInfo struct{}
|
||||
|
||||
func (t ToolQueryStockPriceInfo) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: "QueryStockPriceInfo",
|
||||
Desc: "批量获取实时股价数据",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"stockCodes": {
|
||||
Type: "string",
|
||||
Desc: "股票代码,多个,隔开,股票代码必须转化为sh或者sz或者hk开头的形式,例如:sz399001,sh600859",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t ToolQueryStockPriceInfo) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
parms := map[string]any{}
|
||||
err := json.Unmarshal([]byte(argumentsInJSON), &parms)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
stockCodes := strings.Split(parms["stockCodes"].(string), ",")
|
||||
var codes []string
|
||||
for _, code := range stockCodes {
|
||||
codes = append(codes, GetStockCode(code))
|
||||
}
|
||||
realTimeData, err := data.NewStockDataApi().GetStockCodeRealTimeData(codes...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
marshal, err := json.Marshal(realTimeData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(marshal), nil
|
||||
}
|
||||
138
backend/data/ai_recommend_stocks_api.go
Normal file
138
backend/data/ai_recommend_stocks_api.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Package data ai_recommend_stocks_api.go
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/models"
|
||||
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
)
|
||||
|
||||
type AiRecommendStocksService struct{}
|
||||
|
||||
func NewAiRecommendStocksService() *AiRecommendStocksService {
|
||||
return &AiRecommendStocksService{}
|
||||
}
|
||||
|
||||
// CreateAiRecommendStocks 创建AI推荐股票记录
|
||||
func (s *AiRecommendStocksService) CreateAiRecommendStocks(recommend *models.AiRecommendStocks) error {
|
||||
result := db.Dao.Create(recommend)
|
||||
return result.Error
|
||||
}
|
||||
|
||||
func (s *AiRecommendStocksService) BatchCreateAiRecommendStocks(recommends []*models.AiRecommendStocks) error {
|
||||
result := db.Dao.Create(recommends)
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// GetAiRecommendStocksList 分页查询AI推荐股票记录
|
||||
func (s *AiRecommendStocksService) GetAiRecommendStocksList(query *models.AiRecommendStocksQuery) (*models.AiRecommendStocksPageData, error) {
|
||||
var list []models.AiRecommendStocks
|
||||
var total int64
|
||||
|
||||
q := db.Dao.Model(&models.AiRecommendStocks{})
|
||||
|
||||
// 构建查询条件
|
||||
if query.StockCode != "" {
|
||||
q.Or("stock_code LIKE ?", "%"+query.StockCode+"%")
|
||||
}
|
||||
if query.StockName != "" {
|
||||
q.Or("stock_name LIKE ?", "%"+query.StockName+"%")
|
||||
}
|
||||
if query.BkCode != "" {
|
||||
q.Or("bk_code LIKE ?", "%"+query.BkCode+"%")
|
||||
}
|
||||
if query.BkName != "" {
|
||||
q.Or("bk_name LIKE ?", "%"+query.BkName+"%")
|
||||
}
|
||||
|
||||
if query.StartDate != "" && query.EndDate != "" {
|
||||
query.StartDate = strutil.ReplaceWithMap(query.StartDate, map[string]string{
|
||||
"T": " ",
|
||||
"Z": "",
|
||||
})
|
||||
query.StartDate = strutil.ReplaceWithMap(query.StartDate, map[string]string{
|
||||
"T": " ",
|
||||
"Z": "",
|
||||
})
|
||||
q = q.Where("data_time BETWEEN ? AND ?", query.StartDate, query.EndDate)
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
err := q.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置默认分页参数
|
||||
page := query.Page
|
||||
pageSize := query.PageSize
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 || pageSize > 100 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
// 执行分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
err = q.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&list).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
|
||||
|
||||
stockCodes := slice.Map(list, func(index int, item models.AiRecommendStocks) string {
|
||||
return ConvertTushareCodeToStockCode(item.StockCode)
|
||||
})
|
||||
stockData, _ := NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
|
||||
for _, info := range *stockData {
|
||||
for idx, item := range list {
|
||||
if ConvertTushareCodeToStockCode(item.StockCode) == ConvertTushareCodeToStockCode(info.Code) {
|
||||
list[idx].StockCurrentPrice = info.Price
|
||||
list[idx].StockPrePrice = info.PreClose
|
||||
list[idx].StockCurrentPriceTime = info.Date + " " + info.Time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &models.AiRecommendStocksPageData{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAiRecommendStocksByID 根据ID获取AI推荐股票记录
|
||||
func (s *AiRecommendStocksService) GetAiRecommendStocksByID(id uint) (*models.AiRecommendStocks, error) {
|
||||
var recommend models.AiRecommendStocks
|
||||
err := db.Dao.First(&recommend, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &recommend, nil
|
||||
}
|
||||
|
||||
// UpdateAiRecommendStocks 更新AI推荐股票记录
|
||||
func (s *AiRecommendStocksService) UpdateAiRecommendStocks(id uint, recommend *models.AiRecommendStocks) error {
|
||||
result := db.Dao.Model(&models.AiRecommendStocks{}).Where("id = ?", id).Updates(recommend)
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// DeleteAiRecommendStocks 根据ID删除AI推荐股票记录
|
||||
func (s *AiRecommendStocksService) DeleteAiRecommendStocks(id uint) error {
|
||||
// 使用软删除
|
||||
result := db.Dao.Where("id = ?", id).Delete(&models.AiRecommendStocks{})
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// BatchDeleteAiRecommendStocks 批量删除AI推荐股票记录
|
||||
func (s *AiRecommendStocksService) BatchDeleteAiRecommendStocks(ids []uint) error {
|
||||
// 使用软删除
|
||||
result := db.Dao.Where("id IN ?", ids).Delete(&models.AiRecommendStocks{})
|
||||
return result.Error
|
||||
}
|
||||
97
backend/data/ai_response_result_api.go
Normal file
97
backend/data/ai_response_result_api.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/models"
|
||||
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
)
|
||||
|
||||
type AIResponseResultService struct{}
|
||||
|
||||
func NewAIResponseResultService() *AIResponseResultService {
|
||||
return &AIResponseResultService{}
|
||||
}
|
||||
|
||||
// GetAIResponseResultList 分页查询AI响应结果
|
||||
func (s *AIResponseResultService) GetAIResponseResultList(query models.AIResponseResultQuery) (*models.AIResponseResultPageData, error) {
|
||||
var list []models.AIResponseResult
|
||||
var total int64
|
||||
|
||||
q := db.Dao.Model(&models.AIResponseResult{})
|
||||
|
||||
// 构建查询条件
|
||||
if query.ChatId != "" {
|
||||
q.Where("chat_id LIKE ?", "%"+query.ChatId+"%")
|
||||
}
|
||||
if query.ModelName != "" {
|
||||
q.Or("model_name LIKE ?", "%"+query.ModelName+"%")
|
||||
}
|
||||
if query.StockCode != "" {
|
||||
q.Or("stock_code LIKE ?", "%"+query.StockCode+"%")
|
||||
}
|
||||
if query.Question != "" {
|
||||
q.Or("question LIKE ?", "%"+query.Question+"%")
|
||||
}
|
||||
if query.StartDate != "" && query.EndDate != "" {
|
||||
query.StartDate = strutil.ReplaceWithMap(query.StartDate, map[string]string{
|
||||
"T": " ",
|
||||
"Z": "",
|
||||
})
|
||||
query.StartDate = strutil.ReplaceWithMap(query.StartDate, map[string]string{
|
||||
"T": " ",
|
||||
"Z": "",
|
||||
})
|
||||
q = q.Where("created_at BETWEEN ? AND ?", query.StartDate, query.EndDate)
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
err := q.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置默认分页参数
|
||||
page := query.Page
|
||||
pageSize := query.PageSize
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 || pageSize > 100 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
// 执行分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
err = q.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&list).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
|
||||
|
||||
return &models.AIResponseResultPageData{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteAIResponseResult 根据ID删除AI响应结果
|
||||
func (s *AIResponseResultService) DeleteAIResponseResult(id string) error {
|
||||
|
||||
// 使用软删除
|
||||
result := db.Dao.Where("id = ?", id).Delete(&models.AIResponseResult{})
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// BatchDeleteAIResponseResult 批量删除AI响应结果
|
||||
func (s *AIResponseResultService) BatchDeleteAIResponseResult(ids []uint) error {
|
||||
// 使用软删除
|
||||
result := db.Dao.Where("id IN ?", ids).Delete(&models.AIResponseResult{})
|
||||
|
||||
return result.Error
|
||||
}
|
||||
26
backend/data/ai_response_result_api_test.go
Normal file
26
backend/data/ai_response_result_api_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/models"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2026/1/23 17:39
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestAIResponseResultService_GetAIResponseResultList(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
service := NewAIResponseResultService()
|
||||
list, err := service.GetAIResponseResultList(models.AIResponseResultQuery{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.Log(list)
|
||||
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string)
|
||||
}
|
||||
|
||||
func (a AlertWindowsApi) SendNotification() bool {
|
||||
if GetConfig().LocalPushEnable == false {
|
||||
if GetSettingConfig().LocalPushEnable == false {
|
||||
logger.SugaredLogger.Error("本地推送未开启")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/go-toast/toast"
|
||||
"go-stock/backend/logger"
|
||||
|
||||
"github.com/go-toast/toast"
|
||||
)
|
||||
|
||||
// AlertWindowsApi @Author spark
|
||||
@@ -31,7 +33,7 @@ func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string)
|
||||
}
|
||||
|
||||
func (a AlertWindowsApi) SendNotification() bool {
|
||||
if GetConfig().LocalPushEnable == false {
|
||||
if GetSettingConfig().LocalPushEnable == false {
|
||||
logger.SugaredLogger.Error("本地推送未开启")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/go-toast/toast"
|
||||
"go-stock/backend/logger"
|
||||
"testing"
|
||||
|
||||
"github.com/go-toast/toast"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
|
||||
@@ -27,7 +27,7 @@ func (c *CrawlerApi) NewCrawler(ctx context.Context, crawlerBaseInfo CrawlerBase
|
||||
return CrawlerApi{
|
||||
crawlerCtx: ctx,
|
||||
crawlerBaseInfo: crawlerBaseInfo,
|
||||
pool: NewBrowserPool(GetConfig().BrowserPoolSize),
|
||||
pool: NewBrowserPool(GetSettingConfig().BrowserPoolSize),
|
||||
}
|
||||
}
|
||||
func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bool) {
|
||||
@@ -39,7 +39,7 @@ func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bo
|
||||
}
|
||||
func (c *CrawlerApi) GetHtml_old(url, waitVisible string, headless bool) (string, bool) {
|
||||
htmlContent := ""
|
||||
path := GetConfig().BrowserPath
|
||||
path := GetSettingConfig().BrowserPath
|
||||
//logger.SugaredLogger.Infof("Browser path:%s", path)
|
||||
if path != "" {
|
||||
pctx, pcancel := chromedp.NewExecAllocator(
|
||||
@@ -102,7 +102,7 @@ func (c *CrawlerApi) GetHtml_old(url, waitVisible string, headless bool) (string
|
||||
|
||||
func (c *CrawlerApi) GetHtmlWithNoCancel(url, waitVisible string, headless bool) (html string, ok bool, parent context.CancelFunc, child context.CancelFunc) {
|
||||
htmlContent := ""
|
||||
path := GetConfig().BrowserPath
|
||||
path := GetSettingConfig().BrowserPath
|
||||
//logger.SugaredLogger.Infof("BrowserPath :%s", path)
|
||||
var parentCancel context.CancelFunc
|
||||
var childCancel context.CancelFunc
|
||||
@@ -170,7 +170,7 @@ func (c *CrawlerApi) GetHtmlWithActions(actions *[]chromedp.Action, headless boo
|
||||
htmlContent := ""
|
||||
*actions = append(*actions, chromedp.InnerHTML("body", &htmlContent))
|
||||
|
||||
path := GetConfig().BrowserPath
|
||||
path := GetSettingConfig().BrowserPath
|
||||
//logger.SugaredLogger.Infof("GetHtmlWithActions path:%s", path)
|
||||
if path != "" {
|
||||
pctx, pcancel := chromedp.NewExecAllocator(
|
||||
|
||||
1
backend/data/data/dict/README.md
Normal file
1
backend/data/data/dict/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Some dict/zh data is from [github.com/fxsjy/jieba](https://github.com/fxsjy/jieba)
|
||||
428
backend/data/data/dict/base.txt
Normal file
428
backend/data/data/dict/base.txt
Normal file
@@ -0,0 +1,428 @@
|
||||
# 金融股票全场景分词字典(最终去重优化版)
|
||||
# 格式:单词 权重 词性 | 权重280-350分,核心术语优先匹配,无重复词汇
|
||||
|
||||
# 一、净买卖与资金流向(核心交易表述)
|
||||
净卖出 340 v
|
||||
净买入 340 v
|
||||
净卖出额 330 n
|
||||
净买入额 330 n
|
||||
净卖出量 330 n
|
||||
净买入量 330 n
|
||||
资金净流出 340 n
|
||||
资金净流入 340 n
|
||||
净额 330 n
|
||||
买卖净额 330 n
|
||||
资金净额 330 n
|
||||
北向资金净买入 330 n
|
||||
北向资金净卖出 330 n
|
||||
南向资金净买入 320 n
|
||||
南向资金净卖出 320 n
|
||||
主力资金净买入 330 n
|
||||
主力资金净卖出 330 n
|
||||
散户资金净买入 320 n
|
||||
散户资金净卖出 320 n
|
||||
机构资金净买入 330 n
|
||||
机构资金净卖出 330 n
|
||||
游资净买入 320 n
|
||||
游资净卖出 320 n
|
||||
大单净买入 320 n
|
||||
大单净卖出 320 n
|
||||
中单净买入 320 n
|
||||
中单净卖出 320 n
|
||||
小单净买入 320 n
|
||||
小单净卖出 320 n
|
||||
净买入占比 320 n
|
||||
净卖出占比 320 n
|
||||
净买入率 320 n
|
||||
净卖出率 320 n
|
||||
连续净买入 320 v
|
||||
连续净卖出 320 v
|
||||
单日净买入 320 n
|
||||
单日净卖出 320 n
|
||||
累计净买入 320 n
|
||||
累计净卖出 320 n
|
||||
净买入创纪录 310 adj
|
||||
净卖出创纪录 310 adj
|
||||
净买入放量 310 v
|
||||
净卖出放量 310 v
|
||||
净买入缩量 310 v
|
||||
净卖出缩量 310 v
|
||||
净多 310 n
|
||||
净空 310 n
|
||||
净多头 310 n
|
||||
净空头 310 n
|
||||
净多头头寸 310 n
|
||||
净空头头寸 310 n
|
||||
跌超 310 n
|
||||
跌逾 310 n
|
||||
|
||||
# 二、金融资讯与市场分析
|
||||
金融资讯 350 n
|
||||
市场快讯 340 n
|
||||
财经新闻 340 n
|
||||
政策解读 330 n
|
||||
市场分析 330 n
|
||||
行业研报 320 n
|
||||
宏观经济 330 n
|
||||
微观层面 310 n
|
||||
基本面 320 n
|
||||
技术面 320 n
|
||||
资金面 320 n
|
||||
政策面 320 n
|
||||
市场情绪 320 n
|
||||
风险偏好 310 n
|
||||
流动性 320 n
|
||||
估值修复 310 n
|
||||
价值投资 310 n
|
||||
趋势投资 310 n
|
||||
波段操作 310 n
|
||||
左侧交易 290 n
|
||||
右侧交易 290 n
|
||||
止损止盈 300 n
|
||||
仓位管理 300 n
|
||||
资产配置 310 n
|
||||
分散投资 290 n
|
||||
集中投资 290 n
|
||||
风险控制 310 n
|
||||
系统性风险 300 n
|
||||
非系统性风险 290 n
|
||||
黑天鹅事件 310 n
|
||||
灰犀牛事件 300 n
|
||||
熔断机制 300 n
|
||||
市场监管 310 n
|
||||
信息披露 310 n
|
||||
内幕交易 300 n
|
||||
操纵市场 300 n
|
||||
亏损 100 n
|
||||
加工 100 n
|
||||
|
||||
# 三、全球主要股指(含中英文缩写)
|
||||
# 中国市场
|
||||
A股 350 n
|
||||
港股 350 n
|
||||
上证指数 350 n
|
||||
深证成指 350 n
|
||||
创业板指 340 n
|
||||
科创板指 330 n
|
||||
北证50 330 n
|
||||
沪深300 350 n
|
||||
沪深300指数 350 n
|
||||
中证500 340 n
|
||||
中证500指数 340 n
|
||||
中证1000 330 n
|
||||
中证1000指数 330 n
|
||||
上证50 340 n
|
||||
上证50指数 340 n
|
||||
科创50 330 n
|
||||
科创50指数 330 n
|
||||
上证综指 350 n
|
||||
富时中国A50指数 340 n
|
||||
恒生指数 340 n
|
||||
恒生科技指数 340 n
|
||||
恒生国企指数 330 n
|
||||
H股指数 330 n
|
||||
# 美洲市场
|
||||
道琼斯工业平均指数 350 n
|
||||
标普500指数 350 n
|
||||
纳斯达克综合指数 340 n
|
||||
纳斯达克100指数 340 n
|
||||
罗素2000指数 320 n
|
||||
标普400中型股指数 310 n
|
||||
标普600小型股指数 310 n
|
||||
纽约证交所综合指数 310 n
|
||||
纳斯达克中国金龙指数 310 n
|
||||
# 欧洲市场
|
||||
德国DAX指数 330 n
|
||||
法国CAC40指数 330 n
|
||||
富时100指数 330 n
|
||||
欧元斯托克50指数 320 n
|
||||
英国富时250指数 310 n
|
||||
意大利富时MIB指数 310 n
|
||||
西班牙IBEX 35指数 310 n
|
||||
# 亚太其他市场
|
||||
日经225指数 330 n
|
||||
日经500指数 310 n
|
||||
韩国综合股价指数 320 n
|
||||
韩国kospi指数 320 n
|
||||
KOSPI 310 n
|
||||
澳洲标普200指数 310 n
|
||||
印度孟买敏感指数 310 n
|
||||
Sensex 300 n
|
||||
印度Nifty 50指数 310 n
|
||||
# 全球综合指数
|
||||
MSCI指数 320 n
|
||||
MSCI全球指数 330 n
|
||||
MSCI新兴市场指数 330 n
|
||||
富时罗素全球指数 320 n
|
||||
摩根大通全球债券指数 310 n
|
||||
全球股指 300 n
|
||||
发达市场指数 300 n
|
||||
新兴市场指数 300 n
|
||||
金砖国家指数 300 n
|
||||
G20国家指数 300 n
|
||||
# 股指衍生工具
|
||||
指数期货 320 n
|
||||
股指期货 320 n
|
||||
富时中国A50指数期货 320 n
|
||||
沪深300股指期货 320 n
|
||||
标普500股指期货 320 n
|
||||
纳斯达克100股指期货 310 n
|
||||
指数成分股 320 n
|
||||
指数权重股 320 n
|
||||
指数涨幅 320 n
|
||||
指数跌幅 320 n
|
||||
指数反弹 310 n
|
||||
指数回调 310 n
|
||||
指数创新高 310 v
|
||||
指数创新低 310 v
|
||||
指数估值 310 n
|
||||
指数市盈率 310 n
|
||||
|
||||
# 四、财务与估值核心指标
|
||||
市盈率 350 n
|
||||
PE 350 n
|
||||
动态市盈率 340 n
|
||||
静态市盈率 340 n
|
||||
滚动市盈率 340 n
|
||||
市净率 350 n
|
||||
PB 350 n
|
||||
市销率 330 n
|
||||
PS 330 n
|
||||
市现率 320 n
|
||||
PCF 320 n
|
||||
净资产收益率 350 n
|
||||
ROE 350 n
|
||||
总资产收益率 330 n
|
||||
ROA 330 n
|
||||
毛利率 340 n
|
||||
净利率 340 n
|
||||
销售净利率 330 n
|
||||
资产负债率 340 n
|
||||
营收 340 n
|
||||
营业收入 340 n
|
||||
净利润 350 n
|
||||
归母净利润 340 n
|
||||
扣非净利润 340 n
|
||||
EPS 330 n
|
||||
每股收益 330 n
|
||||
现金流 340 n
|
||||
经营活动现金流 330 n
|
||||
自由现金流 330 n
|
||||
营收增长率 330 n
|
||||
净利润增长率 330 n
|
||||
股息率 320 n
|
||||
分红率 320 n
|
||||
换手率 330 n
|
||||
成交量 340 n
|
||||
成交额 340 n
|
||||
量比 320 n
|
||||
振幅 320 n
|
||||
|
||||
# 五、政策与宏观经济
|
||||
货币政策 330 n
|
||||
财政政策 330 n
|
||||
稳健货币政策 320 n
|
||||
积极财政政策 320 n
|
||||
宽松政策 320 n
|
||||
紧缩政策 320 n
|
||||
利率 330 n
|
||||
基准利率 320 n
|
||||
LPR 330 n
|
||||
贷款市场报价利率 320 n
|
||||
存款准备金率 320 n
|
||||
MLF 320 n
|
||||
中期借贷便利 310 n
|
||||
逆回购 320 n
|
||||
正回购 310 n
|
||||
汇率 330 n
|
||||
人民币汇率 330 n
|
||||
美元汇率 320 n
|
||||
通胀 320 n
|
||||
CPI 330 n
|
||||
PPI 330 n
|
||||
GDP 330 n
|
||||
国内生产总值 320 n
|
||||
PMI 330 n
|
||||
采购经理人指数 320 n
|
||||
行业政策 320 n
|
||||
产业政策 320 n
|
||||
税收政策 310 n
|
||||
补贴政策 310 n
|
||||
关税 310 n
|
||||
贸易政策 310 n
|
||||
地缘政治 310 n
|
||||
大宗商品 320 n
|
||||
原油价格 310 n
|
||||
黄金价格 310 n
|
||||
有色金属价格 300 n
|
||||
|
||||
# 六、金融产品与机构
|
||||
股票 320 n
|
||||
基金 320 n
|
||||
公募基金 310 n
|
||||
私募基金 310 n
|
||||
ETF 320 n
|
||||
指数基金 310 n
|
||||
混合型基金 300 n
|
||||
股票型基金 310 n
|
||||
债券型基金 300 n
|
||||
货币基金 290 n
|
||||
REITs 310 n
|
||||
可转债 310 n
|
||||
可交换债 300 n
|
||||
期货 310 n
|
||||
股指期货 310 n
|
||||
国债期货 300 n
|
||||
商品期货 300 n
|
||||
期权 300 n
|
||||
融资融券 310 n
|
||||
两融余额 300 n
|
||||
北向资金 320 n
|
||||
南向资金 310 n
|
||||
沪股通 310 n
|
||||
深股通 310 n
|
||||
陆股通 310 n
|
||||
证券公司 310 n
|
||||
券商 320 n
|
||||
基金公司 300 n
|
||||
保险公司 300 n
|
||||
银行 310 n
|
||||
监管机构 310 n
|
||||
证监会 320 n
|
||||
交易所 320 n
|
||||
上交所 320 n
|
||||
深交所 320 n
|
||||
北交所 310 n
|
||||
港交所 310 n
|
||||
社保基金 310 n
|
||||
养老金 300 n
|
||||
QFII 300 n
|
||||
RQFII 290 n
|
||||
北向资金机构 300 n
|
||||
|
||||
# 七、热点概念与行业
|
||||
AI 330 n
|
||||
人工智能 350 n
|
||||
算力 330 n
|
||||
大数据 320 n
|
||||
云计算 320 n
|
||||
半导体 350 n
|
||||
芯片 350 n
|
||||
集成电路 340 n
|
||||
新能源 350 n
|
||||
光伏 340 n
|
||||
锂电 320 n
|
||||
储能 340 n
|
||||
充电桩 310 n
|
||||
新能源车 320 n
|
||||
智能汽车 310 n
|
||||
自动驾驶 330 n
|
||||
军工 310 n
|
||||
国防军工 300 n
|
||||
医药 310 n
|
||||
创新药 310 n
|
||||
医疗器械 300 n
|
||||
CXO 300 n
|
||||
白酒 310 n
|
||||
消费 320 n
|
||||
可选消费 300 n
|
||||
必选消费 300 n
|
||||
食品饮料 310 n
|
||||
家电 300 n
|
||||
地产 300 n
|
||||
房地产 300 n
|
||||
基建 300 n
|
||||
新基建 310 n
|
||||
数字经济 350 n
|
||||
数字货币 310 n
|
||||
区块链 300 n
|
||||
元宇宙 300 n
|
||||
低空经济 340 n
|
||||
人形机器人 330 n
|
||||
工业互联网 330 n
|
||||
物联网 300 n
|
||||
5G 300 n
|
||||
6G 340 n
|
||||
|
||||
# 八、交易操作与行情
|
||||
上涨 310 v
|
||||
下跌 310 v
|
||||
涨停 310 v
|
||||
跌停 310 v
|
||||
反弹 300 v
|
||||
反转 300 v
|
||||
回调 300 v
|
||||
横盘 290 v
|
||||
震荡 290 v
|
||||
跳水 300 v
|
||||
拉升 300 v
|
||||
砸盘 300 v
|
||||
护盘 290 v
|
||||
建仓 300 v
|
||||
加仓 300 v
|
||||
减仓 300 v
|
||||
清仓 300 v
|
||||
平仓 300 v
|
||||
抄底 300 v
|
||||
逃顶 300 v
|
||||
追涨 290 v
|
||||
杀跌 290 v
|
||||
套牢 280 v
|
||||
解套 280 v
|
||||
净流入 300 n
|
||||
净流出 300 n
|
||||
主力资金 300 n
|
||||
资金流入 290 v
|
||||
资金流出 290 v
|
||||
放量 290 v
|
||||
缩量 290 v
|
||||
高换手 290 n
|
||||
低换手 280 n
|
||||
高估值 290 n
|
||||
低估值 290 n
|
||||
超预期 300 v
|
||||
不及预期 300 v
|
||||
符合预期 290 v
|
||||
利好 310 n
|
||||
利空 310 n
|
||||
政策利好 310 n
|
||||
业绩利好 310 n
|
||||
风险警示 300 n
|
||||
涨停板 300 n
|
||||
跌停板 300 n
|
||||
一字涨停 290 n
|
||||
一字跌停 290 n
|
||||
打开涨停 320 v
|
||||
打开跌停 320 v
|
||||
集合竞价 290 n
|
||||
连续竞价 280 n
|
||||
开盘价 340 n
|
||||
收盘价 340 n
|
||||
最高价 330 n
|
||||
最低价 330 n
|
||||
均价 330 n
|
||||
昨日收盘价 320 n
|
||||
涨跌额 330 n
|
||||
涨跌幅 340 n
|
||||
涨幅 340 n
|
||||
跌幅 340 n
|
||||
涨停价 330 n
|
||||
跌停价 330 n
|
||||
熔断 330 n
|
||||
临时停牌 320 n
|
||||
复牌 320 v
|
||||
停牌 320 n
|
||||
量价齐升 320 n
|
||||
量价背离 320 n
|
||||
高开 320 n
|
||||
低开 320 n
|
||||
平开 320 n
|
||||
高走 320 v
|
||||
低走 320 v
|
||||
震荡上行 320 v
|
||||
震荡下行 320 v
|
||||
|
||||
# 九、委托交易与规则
|
||||
限价委托 340 n
|
||||
市价委托 340 n
|
||||
止损委托 330 n
|
||||
0
backend/data/data/dict/en/dict.txt
Normal file
0
backend/data/data/dict/en/dict.txt
Normal file
1
backend/data/data/dict/jp/README.md
Normal file
1
backend/data/data/dict/jp/README.md
Normal file
@@ -0,0 +1 @@
|
||||
dict.txt 通过内部工具生成, Copyright 2017 ego authors. 商用和拷贝请注明来源和版权
|
||||
885298
backend/data/data/dict/jp/dict.txt
Normal file
885298
backend/data/data/dict/jp/dict.txt
Normal file
File diff suppressed because it is too large
Load Diff
185
backend/data/data/dict/user.txt
Normal file
185
backend/data/data/dict/user.txt
Normal file
@@ -0,0 +1,185 @@
|
||||
# 补充:热点概念与板块(Jieba/gse兼容格式)
|
||||
# 权重说明:核心热点500-700分,事件类400分,负权重词汇按需求保留
|
||||
|
||||
# 一、负权重低优先级词汇(减少无差别匹配干扰)
|
||||
公司 -0.1 n
|
||||
国家 -0.1 n
|
||||
国际 -0.1 n
|
||||
会议 -0.1 n
|
||||
市场 -0.1 n
|
||||
经济 -0.1 n
|
||||
技术 -0.1 n
|
||||
记者 -0.1 n
|
||||
时间 -0.1 n
|
||||
项目 -0.1 n
|
||||
问题 -0.1 n
|
||||
企业 -0.1 n
|
||||
财联社 -0.1 n
|
||||
上涨 -0.1 v
|
||||
下跌 -0.1 v
|
||||
期货 -0.1 n
|
||||
跌幅 -0.1 n
|
||||
跌超 -0.1 adj
|
||||
股票 -0.1 n
|
||||
基金 -0.1 n
|
||||
电讯 -0.1 n
|
||||
建筑 -0.1 n
|
||||
平开 -0.1 n
|
||||
保险 -0.1 n
|
||||
行业 -0.1 n
|
||||
其他 -0.1 n
|
||||
|
||||
# 二、核心热点概念(700分,最高优先级)
|
||||
比特币 700 n
|
||||
摩尔线程 700 n
|
||||
摩尔线程概念 700 n
|
||||
AI算力 700 n
|
||||
生成式AI 700 n
|
||||
量子计算 700 n
|
||||
脑机接口 700 n
|
||||
6G通信 700 n
|
||||
人形机器人 700 n
|
||||
固态电池 700 n
|
||||
ChatGPT概念 700 n
|
||||
Web3.0 700 n
|
||||
元宇宙 700 n
|
||||
数字孪生 700 n
|
||||
量子通信 700 n
|
||||
|
||||
# 三、重点赛道板块(500分,高优先级)
|
||||
冰雪旅游 500 n
|
||||
特高压 500 n
|
||||
跨境电商 500 n
|
||||
新能源汽车 500 n
|
||||
机器人 500 n
|
||||
具身智能 500 n
|
||||
油气 500 n
|
||||
商业航天 500 n
|
||||
光伏储能 500 n
|
||||
锂电材料 500 n
|
||||
半导体设备 500 n
|
||||
集成电路 500 n
|
||||
创新药 500 n
|
||||
CXO 500 n
|
||||
医疗器械 500 n
|
||||
数字经济 500 n
|
||||
数字货币 500 n
|
||||
区块链 500 n
|
||||
低空经济 500 n
|
||||
工业互联网 500 n
|
||||
物联网 500 n
|
||||
5G应用 500 n
|
||||
充电桩 500 n
|
||||
氢能源 500 n
|
||||
核聚变 500 n
|
||||
工业母机 500 n
|
||||
新材料 500 n
|
||||
生物制造 500 n
|
||||
智能网联汽车 500 n
|
||||
乡村振兴 500 n
|
||||
国企改革 500 n
|
||||
央企重组 500 n
|
||||
跨境金融 500 n
|
||||
自贸港 500 n
|
||||
一带一路 500 n
|
||||
绿色低碳 500 n
|
||||
碳交易 500 n
|
||||
数据要素 500 n
|
||||
数字基建 500 n
|
||||
东数西算 500 n
|
||||
国产替代 500 n
|
||||
信创 500 n
|
||||
网络安全 500 n
|
||||
算力网络 500 n
|
||||
边缘计算 500 n
|
||||
虚拟现实 500 n
|
||||
增强现实 500 n
|
||||
智能穿戴 500 n
|
||||
智能家居 500 n
|
||||
车联网 500 n
|
||||
激光雷达 500 n
|
||||
氮化镓 500 n
|
||||
碳化硅 500 n
|
||||
第三代半导体 500 n
|
||||
EDA工具 500 n
|
||||
光刻胶 500 n
|
||||
芯片设计 500 n
|
||||
封装测试 500 n
|
||||
储能电池 500 n
|
||||
钠离子电池 500 n
|
||||
氢燃料电池 500 n
|
||||
光伏组件 500 n
|
||||
风电设备 500 n
|
||||
特高压设备 500 n
|
||||
电力物联网 500 n
|
||||
智能电网 500 n
|
||||
轨道交通 500 n
|
||||
航空航天 500 n
|
||||
海洋工程 500 n
|
||||
高端装备 500 n
|
||||
军工电子 500 n
|
||||
卫星互联网 500 n
|
||||
北斗导航 500 n
|
||||
国产大飞机 500 n
|
||||
生物医药 500 n
|
||||
基因测序 500 n
|
||||
疫苗 500 n
|
||||
医疗美容 500 n
|
||||
养老产业 500 n
|
||||
教育信息化 500 n
|
||||
体育产业 500 n
|
||||
文化创意 500 n
|
||||
旅游复苏 500 n
|
||||
预制菜 500 n
|
||||
白酒 500 n
|
||||
食品饮料 500 n
|
||||
家电下乡 500 n
|
||||
房地产复苏 500 n
|
||||
基建投资 500 n
|
||||
新型城镇化 500 n
|
||||
冷链物流 500 n
|
||||
快递物流 500 n
|
||||
跨境支付 500 n
|
||||
金融科技 500 n
|
||||
消费电子 500 n
|
||||
元宇宙基建 500 n
|
||||
数字藏品 500 n
|
||||
NFT 500 n
|
||||
绿色电力 500 n
|
||||
节能降碳 500 n
|
||||
抽水蓄能 500 n
|
||||
生物质能 500 n
|
||||
地热能 500 n
|
||||
潮汐能 500 n
|
||||
|
||||
# 四、事件驱动型概念(400分,中优先级)
|
||||
俄乌冲突 400 n
|
||||
中东局势 400 n
|
||||
美联储加息 400 n
|
||||
降息预期 400 n
|
||||
贸易摩擦 400 n
|
||||
供应链重构 400 n
|
||||
能源危机 400 n
|
||||
粮食安全 400 n
|
||||
疫情复苏 400 n
|
||||
政策利好 400 n
|
||||
产业扶持 400 n
|
||||
技术突破 400 n
|
||||
并购重组 400 n
|
||||
IPO提速 400 n
|
||||
解禁潮 400 n
|
||||
北向资金流入 400 n
|
||||
南向资金流入 400 n
|
||||
主力资金异动 400 n
|
||||
行业景气度 400 n
|
||||
业绩预增 400 n
|
||||
商誉减值 400 n
|
||||
退市风险 400 n
|
||||
监管新规 400 n
|
||||
税收优惠 400 n
|
||||
补贴政策 400 n
|
||||
基建刺激 400 n
|
||||
消费刺激 400 n
|
||||
新能源补贴 400 n
|
||||
碳达峰政策 400 n
|
||||
碳中和目标 400 n
|
||||
270132
backend/data/data/dict/zh/idf.txt
Normal file
270132
backend/data/data/dict/zh/idf.txt
Normal file
File diff suppressed because it is too large
Load Diff
352279
backend/data/data/dict/zh/s_1.txt
Normal file
352279
backend/data/data/dict/zh/s_1.txt
Normal file
File diff suppressed because it is too large
Load Diff
1161
backend/data/data/dict/zh/stop_tokens.txt
Normal file
1161
backend/data/data/dict/zh/stop_tokens.txt
Normal file
File diff suppressed because it is too large
Load Diff
88
backend/data/data/dict/zh/stop_word.txt
Normal file
88
backend/data/data/dict/zh/stop_word.txt
Normal file
@@ -0,0 +1,88 @@
|
||||
,
|
||||
.
|
||||
?
|
||||
!
|
||||
"
|
||||
@
|
||||
,
|
||||
。
|
||||
、
|
||||
?
|
||||
!
|
||||
:
|
||||
“
|
||||
”
|
||||
;
|
||||
|
||||
(
|
||||
)
|
||||
《
|
||||
》
|
||||
~
|
||||
*
|
||||
<
|
||||
>
|
||||
/
|
||||
\
|
||||
|
|
||||
-
|
||||
_
|
||||
+
|
||||
=
|
||||
&
|
||||
^
|
||||
%
|
||||
#
|
||||
`
|
||||
;
|
||||
$
|
||||
¥
|
||||
‘
|
||||
’
|
||||
〉
|
||||
〈
|
||||
…
|
||||
>
|
||||
<
|
||||
@
|
||||
#
|
||||
$
|
||||
%
|
||||
︿
|
||||
&
|
||||
*
|
||||
+
|
||||
~
|
||||
|
|
||||
[
|
||||
]
|
||||
{
|
||||
}
|
||||
啊
|
||||
阿
|
||||
哎
|
||||
哎呀
|
||||
哎哟
|
||||
唉
|
||||
俺
|
||||
俺们
|
||||
按
|
||||
按照
|
||||
吧
|
||||
吧哒
|
||||
把
|
||||
罢了
|
||||
被
|
||||
本
|
||||
本着
|
||||
比
|
||||
比方
|
||||
比如
|
||||
鄙人
|
||||
彼
|
||||
彼此
|
||||
边
|
||||
别
|
||||
别的
|
||||
别说
|
||||
并
|
||||
236754
backend/data/data/dict/zh/t_1.txt
Normal file
236754
backend/data/data/dict/zh/t_1.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ func NewDingDingAPI() *DingDingAPI {
|
||||
}
|
||||
|
||||
func (DingDingAPI) SendDingDingMessage(message string) string {
|
||||
if GetConfig().DingPushEnable == false {
|
||||
if GetSettingConfig().DingPushEnable == false {
|
||||
//logger.SugaredLogger.Info("钉钉推送未开启")
|
||||
return "钉钉推送未开启"
|
||||
}
|
||||
@@ -37,11 +37,9 @@ func (DingDingAPI) SendDingDingMessage(message string) string {
|
||||
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
|
||||
return "发送钉钉消息成功"
|
||||
}
|
||||
func GetConfig() *Settings {
|
||||
return NewSettingsApi(&Settings{}).GetConfig()
|
||||
}
|
||||
|
||||
func getApiURL() string {
|
||||
return GetConfig().DingRobot
|
||||
return GetSettingConfig().DingRobot
|
||||
}
|
||||
|
||||
func (DingDingAPI) SendToDingDing(title, message string) string {
|
||||
|
||||
@@ -20,13 +20,13 @@ import (
|
||||
|
||||
type FundApi struct {
|
||||
client *resty.Client
|
||||
config *Settings
|
||||
config *SettingConfig
|
||||
}
|
||||
|
||||
func NewFundApi() *FundApi {
|
||||
return &FundApi{
|
||||
client: resty.New(),
|
||||
config: GetConfig(),
|
||||
config: GetSettingConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,17 @@ import (
|
||||
"encoding/json"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"go-stock/backend/util"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -14,7 +24,12 @@ import (
|
||||
|
||||
func TestGetSinaNews(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
NewMarketNewsApi().GetSinaNews(30)
|
||||
InitAnalyzeSentiment()
|
||||
news := NewMarketNewsApi().GetSinaNews(30)
|
||||
for i, telegraph := range *news {
|
||||
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, telegraph)
|
||||
|
||||
}
|
||||
//NewMarketNewsApi().GetNewTelegraph(30)
|
||||
|
||||
}
|
||||
@@ -27,3 +42,255 @@ func TestGlobalStockIndexes(t *testing.T) {
|
||||
}
|
||||
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("688082", 7)
|
||||
for _, a := range resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
data := a.(map[string]any)
|
||||
logger.SugaredLogger.Debugf("value: %s infoCode:%s", data["title"], data["infoCode"])
|
||||
NewMarketNewsApi().GetIndustryReportInfo(data["infoCode"].(string))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndustryResearchReport(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
resp := NewMarketNewsApi().IndustryResearchReport("", 7)
|
||||
for _, a := range resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
data := a.(map[string]any)
|
||||
logger.SugaredLogger.Debugf("value: %s infoCode:%s", data["title"], data["infoCode"])
|
||||
logger.SugaredLogger.Debugf("url: https://pdf.dfcfw.com/pdf/H3_%s_1.pdf", data["infoCode"])
|
||||
//NewMarketNewsApi().GetIndustryReportInfo(data["infoCode"].(string))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
bytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
dict := &[]models.BKDict{}
|
||||
json.Unmarshal(bytes, dict)
|
||||
logger.SugaredLogger.Debugf("value: %s", string(bytes))
|
||||
md := util.MarkdownTableWithTitle("行业/板块代码", dict)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
|
||||
}
|
||||
|
||||
func TestTradingViewNews(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
InitAnalyzeSentiment()
|
||||
NewMarketNewsApi().TradingViewNews()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
md := util.MarkdownTableWithTitle("当前热门股票排名", res)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
}
|
||||
|
||||
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 {
|
||||
bytes, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
date := gjson.Get(string(bytes), "date")
|
||||
list := gjson.Get(string(bytes), "list")
|
||||
|
||||
logger.SugaredLogger.Debugf("value: %+v,list: %+v", date.String(), list)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClsCalendar(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewMarketNewsApi().ClsCalendar()
|
||||
md := strings.Builder{}
|
||||
for _, a := range res {
|
||||
bytes, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
//logger.SugaredLogger.Debugf("value: %+v", string(bytes))
|
||||
date := gjson.Get(string(bytes), "calendar_day")
|
||||
md.WriteString("\n### 事件/会议日期:" + date.String())
|
||||
list := gjson.Get(string(bytes), "items")
|
||||
//logger.SugaredLogger.Debugf("value: %+v,list: %+v", date.String(), list)
|
||||
list.ForEach(func(key, value gjson.Result) bool {
|
||||
logger.SugaredLogger.Debugf("key: %+v,value: %+v", key.String(), gjson.Get(value.String(), "title"))
|
||||
md.WriteString("\n- " + gjson.Get(value.String(), "title").String())
|
||||
return true
|
||||
})
|
||||
}
|
||||
logger.SugaredLogger.Debugf("md:\n %s", md.String())
|
||||
}
|
||||
|
||||
func TestGetGDP(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetGDP()
|
||||
md := util.MarkdownTableWithTitle("国内生产总值(GDP)", res.GDPResult.Data)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
}
|
||||
func TestGetCPI(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetCPI()
|
||||
md := util.MarkdownTableWithTitle("居民消费价格指数(CPI)", res.CPIResult.Data)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
}
|
||||
|
||||
// PPI
|
||||
func TestGetPPI(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetPPI()
|
||||
md := util.MarkdownTableWithTitle("工业品出厂价格指数(PPI)", res.PPIResult.Data)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
}
|
||||
|
||||
// PMI
|
||||
func TestGetPMI(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetPMI()
|
||||
md := util.MarkdownTableWithTitle("采购经理人指数(PMI)", res.PMIResult.Data)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
}
|
||||
func TestGetIndustryReportInfo(t *testing.T) {
|
||||
NewMarketNewsApi().GetIndustryReportInfo("AP202507151709216483")
|
||||
}
|
||||
|
||||
func TestReutersNew(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
NewMarketNewsApi().ReutersNew()
|
||||
}
|
||||
|
||||
func TestInteractiveAnswer(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
datas := NewMarketNewsApi().InteractiveAnswer(1, 100, "立讯精密")
|
||||
logger.SugaredLogger.Debugf("PageSize:%d", datas.PageSize)
|
||||
md := util.MarkdownTableWithTitle("投资互动", datas.Results)
|
||||
logger.SugaredLogger.Debugf(md)
|
||||
|
||||
}
|
||||
func TestGetNewsList2(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
news := NewMarketNewsApi().GetNewsList2("财联社电报", random.RandInt(100, 500))
|
||||
messageText := strings.Builder{}
|
||||
for _, telegraph := range *news {
|
||||
messageText.WriteString("## " + telegraph.Time + ":" + "\n")
|
||||
messageText.WriteString("### " + telegraph.Content + "\n")
|
||||
}
|
||||
logger.SugaredLogger.Debugf("value: %s", messageText.String())
|
||||
}
|
||||
|
||||
func TestTelegraphList(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
InitAnalyzeSentiment()
|
||||
NewMarketNewsApi().TelegraphList(30)
|
||||
}
|
||||
|
||||
func TestProxy(t *testing.T) {
|
||||
response, err := resty.New().
|
||||
SetProxy("http://go-stock:778d4ff2-73f3-4d56-b3c3-d9a730a06ae3@stock.sparkmemory.top:8888").
|
||||
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("https://api.ipify.org")
|
||||
Get("https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang%3Azh-Hans&client=screener&streaming=false&user_prostatus=non_pro")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Debugf("value: %s", response.String())
|
||||
|
||||
}
|
||||
|
||||
func TestNtfy(t *testing.T) {
|
||||
|
||||
//attach := "http://go-stock.sparkmemory.top/%E5%88%86%E6%9E%90%E6%8A%A5%E5%91%8A/%E8%B5%84%E9%87%91%E6%B5%81%E5%90%91/2025-12/AI%EF%BC%9A%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90%E6%8A%A5%E5%91%8A-[2025.12.11_12.02.01].html"
|
||||
//post, err := resty.New().SetBaseURL("https://go-stock.sparkmemory.top:16667").R().
|
||||
// SetHeader("Filename", "AI:市场分析报告-[2025.12.11_12.02.01].html").
|
||||
// SetHeader("Icon", "https://go-stock.sparkmemory.top/appicon.png").
|
||||
// SetHeader("Attach", attach).
|
||||
// SetBody("AI:市场分析报告-[2025.12.11_12.02.01]").Post("/go-stock")
|
||||
//if err != nil {
|
||||
// logger.SugaredLogger.Error(err)
|
||||
// return
|
||||
//}
|
||||
//logger.SugaredLogger.Debugf("value: %s", post.String())
|
||||
logger.SugaredLogger.Debugf("value: %s", filepath.Base("https://go-stock.sparkmemory.top/%E5%88%86%E6%9E%90%E6%8A%A5%E5%91%8A/2025/12/11/%E5%B8%82%E5%9C%BA%E8%B5%84%E8%AE%AF[%E5%B8%82%E5%9C%BA%E8%B5%84%E8%AE%AF]-(2025-12-11)AI%E5%88%86%E6%9E%90%E7%BB%93%E6%9E%9C_20251211131509.html"))
|
||||
logger.SugaredLogger.Debugf("value: %s", strutil.After("/data/go-stock-site/docs/分析报告/2025/12/09/市场资讯[市场资讯]-(2025-12-09)AI分析结果.md", "/data/go-stock-site/docs/"))
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,29 +3,68 @@ package data
|
||||
import (
|
||||
"context"
|
||||
"go-stock/backend/db"
|
||||
log "go-stock/backend/logger"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
ai := NewDeepSeekOpenAi(context.TODO())
|
||||
res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
|
||||
InitAnalyzeSentiment()
|
||||
|
||||
var tools []Tool
|
||||
tools = append(tools, Tool{
|
||||
Type: "function",
|
||||
Function: ToolFunction{
|
||||
Name: "SearchStockByIndicators",
|
||||
Description: "根据自然语言筛选股票,返回自然语言选股条件要求的股票所有相关数据",
|
||||
Parameters: &FunctionParameters{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"words": map[string]any{
|
||||
"type": "string",
|
||||
"description": "选股自然语言,并且条件使用;分隔,或者条件使用,分隔。例如:创新药;PE<30;净利润增长率>50%;",
|
||||
},
|
||||
},
|
||||
Required: []string{"words"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ai := NewDeepSeekOpenAi(context.TODO(), 11)
|
||||
//res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
|
||||
res := ai.NewSummaryStockNewsStreamWithTools("总结市场资讯,发掘潜力标的/行业/板块/概念,控制风险。调用工具函数验证", nil, tools, false)
|
||||
|
||||
for {
|
||||
select {
|
||||
case msg := <-res:
|
||||
t.Log(msg)
|
||||
if len(msg) > 0 {
|
||||
t.Log(msg)
|
||||
if msg["content"] == "DONE" {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTopNewsList(t *testing.T) {
|
||||
GetTopNewsList(30)
|
||||
news := GetTopNewsList(30)
|
||||
t.Log(news)
|
||||
}
|
||||
|
||||
func TestSearchGuShiTongStockInfo(t *testing.T) {
|
||||
//db.Init("../../data/stock.db")
|
||||
SearchGuShiTongStockInfo("hk01810", 60)
|
||||
SearchGuShiTongStockInfo("sh600745", 60)
|
||||
SearchGuShiTongStockInfo("gb_goog", 60)
|
||||
db.Init("../../data/stock.db")
|
||||
//SearchGuShiTongStockInfo("hk01810", 60)
|
||||
msgs := SearchGuShiTongStockInfo("sh600745", 60)
|
||||
for _, msg := range *msgs {
|
||||
log.SugaredLogger.Infof("%s", msg)
|
||||
}
|
||||
//SearchGuShiTongStockInfo("gb_goog", 60)
|
||||
|
||||
}
|
||||
|
||||
func TestGetZSInfo(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
GetZSInfo("中证银行", "sz399986", 5)
|
||||
GetZSInfo("上海贝岭", "sh600171", 5)
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ type BrowserPool struct {
|
||||
func NewBrowserPool(size int) *BrowserPool {
|
||||
pool := make(chan *context.Context, size)
|
||||
for i := 0; i < size; i++ {
|
||||
path := GetConfig().BrowserPath
|
||||
crawlTimeOut := GetConfig().CrawlTimeOut
|
||||
path := GetSettingConfig().BrowserPath
|
||||
crawlTimeOut := GetSettingConfig().CrawlTimeOut
|
||||
if crawlTimeOut < 15 {
|
||||
crawlTimeOut = 30
|
||||
}
|
||||
|
||||
163
backend/data/search_stock_api.go
Normal file
163
backend/data/search_stock_api.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"go-stock/backend/util"
|
||||
"time"
|
||||
|
||||
"github.com/duke-git/lancet/v2/mathutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// @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(pageSize int) map[string]any {
|
||||
qgqpBId := NewSettingsApi().Config.QgqpBId
|
||||
if qgqpBId == "" {
|
||||
return map[string]any{
|
||||
"code": -1,
|
||||
"message": "请先获取东财用户标识(qgqp_b_id):打开浏览器,访问东财网站,按F12打开开发人员工具-》网络面板,随便点开一个请求,复制请求cookie中qgqp_b_id对应的值。保存到设置中的东财唯一标识输入框",
|
||||
}
|
||||
}
|
||||
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:145.0) Gecko/20100101 Firefox/145.0").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(fmt.Sprintf(`{
|
||||
"keyWord": "%s",
|
||||
"pageSize": %d,
|
||||
"pageNo": 1,
|
||||
"fingerprint": "%s",
|
||||
"gids": [],
|
||||
"matchWord": "",
|
||||
"timestamp": "%d",
|
||||
"shareToGuba": false,
|
||||
"requestId": "",
|
||||
"needCorrect": true,
|
||||
"removedConditionIdList": [],
|
||||
"xcId": "",
|
||||
"ownSelectAll": false,
|
||||
"dxInfo": [],
|
||||
"extraCondition": ""
|
||||
}`, s.words, pageSize, qgqpBId, time.Now().Unix())).Post(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("SearchStock-err:%+v", err)
|
||||
return map[string]any{
|
||||
"code": -1,
|
||||
"message": err.Error(),
|
||||
}
|
||||
}
|
||||
respMap := map[string]any{}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
|
||||
return respMap
|
||||
}
|
||||
|
||||
func (s SearchStockApi) SearchBk(pageSize int) map[string]any {
|
||||
url := "https://np-tjxg-b.eastmoney.com/api/smart-tag/bkc/v3/pw/search-code"
|
||||
qgqpBId := NewSettingsApi().Config.QgqpBId
|
||||
if qgqpBId == "" {
|
||||
return map[string]any{
|
||||
"code": -1,
|
||||
"message": "请先获取东财用户标识(qgqp_b_id):打开浏览器,访问东财网站,按F12打开开发人员工具-》网络面板,随便点开一个请求,复制请求cookie中qgqp_b_id对应的值。保存到设置中的东财唯一标识输入框",
|
||||
}
|
||||
}
|
||||
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:145.0) Gecko/20100101 Firefox/145.0").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(fmt.Sprintf(`{
|
||||
"keyWord": "%s",
|
||||
"pageSize": %d,
|
||||
"pageNo": 1,
|
||||
"fingerprint": "%s",
|
||||
"gids": [],
|
||||
"matchWord": "",
|
||||
"timestamp": "%d",
|
||||
"shareToGuba": false,
|
||||
"requestId": "",
|
||||
"needCorrect": true,
|
||||
"removedConditionIdList": [],
|
||||
"xcId": "",
|
||||
"ownSelectAll": false,
|
||||
"dxInfo": [],
|
||||
"extraCondition": ""
|
||||
}`, s.words, pageSize, qgqpBId, time.Now().Unix())).Post(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("SearchStock-err:%+v", err)
|
||||
return map[string]any{
|
||||
"code": -1,
|
||||
"message": err.Error(),
|
||||
}
|
||||
}
|
||||
respMap := map[string]any{}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
|
||||
return respMap
|
||||
}
|
||||
|
||||
func (s SearchStockApi) HotStrategy() map[string]any {
|
||||
url := fmt.Sprintf("https://np-ipick.eastmoney.com/recommend/stock/heat/ranking?count=20&trace=%d&client=web&biz=web_smart_tag", time.Now().Unix())
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "np-ipick.eastmoney.com").
|
||||
SetHeader("Origin", "https://xuangu.eastmoney.com").
|
||||
SetHeader("Referer", "https://xuangu.eastmoney.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("HotStrategy-err:%+v", err)
|
||||
return map[string]any{}
|
||||
}
|
||||
respMap := map[string]any{}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
return respMap
|
||||
}
|
||||
|
||||
func (s SearchStockApi) HotStrategyTable() string {
|
||||
markdownTable := ""
|
||||
res := s.HotStrategy()
|
||||
bytes, _ := json.Marshal(res)
|
||||
strategy := &models.HotStrategy{}
|
||||
json.Unmarshal(bytes, strategy)
|
||||
for _, data := range strategy.Data {
|
||||
data.Chg = mathutil.RoundToFloat(100*data.Chg, 2)
|
||||
}
|
||||
markdownTable = util.MarkdownTableWithTitle("当前热门选股策略", strategy.Data)
|
||||
return markdownTable
|
||||
}
|
||||
|
||||
func (s SearchStockApi) StrategySquare() map[string]any {
|
||||
//https://backtest.10jqka.com.cn/strategysquare/list?order=desc&page=1&pageNum=10&sortType=hot&keyword=
|
||||
url := "https://backtest.10jqka.com.cn/strategysquare/list?order=desc&page=1&pageNum=10&sortType=hot&keyword="
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "backtest.10jqka.com.cn").
|
||||
SetHeader("Origin", "https://backtest.10jqka.com.cn").
|
||||
SetHeader("Referer", "https://backtest.10jqka.com.cn/strategysquare/list").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("StrategySquare-err:%+v", err)
|
||||
return map[string]any{}
|
||||
}
|
||||
respMap := map[string]any{}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
|
||||
return respMap
|
||||
}
|
||||
89
backend/data/search_stock_api_test.go
Normal file
89
backend/data/search_stock_api_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"go-stock/backend/util"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/mathutil"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
)
|
||||
|
||||
func TestSearchStock(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
e := convertor.ToString(math.Floor(float64(9*random.RandFloat(0, 1, 12) + 1)))
|
||||
for i := 0; i < 19; i++ {
|
||||
e += convertor.ToString(math.Floor(float64(9 * random.RandFloat(0, 1, 12))))
|
||||
}
|
||||
logger.SugaredLogger.Infof("e:%s", e)
|
||||
|
||||
//res := NewSearchStockApi("量比大于2,基本面优秀,2025年三季报已披露,主力连续3日净流入,非创业板非科创板非ST").SearchStock(20)
|
||||
res := NewSearchStockApi("今日涨幅前5的概念板块").SearchBk(50)
|
||||
|
||||
logger.SugaredLogger.Infof("res:%+v", res)
|
||||
data := res["data"].(map[string]any)
|
||||
result := data["result"].(map[string]any)
|
||||
dataList := result["dataList"].([]any)
|
||||
columns := result["columns"].([]any)
|
||||
headers := map[string]string{}
|
||||
for _, v := range columns {
|
||||
//logger.SugaredLogger.Infof("v:%+v", v)
|
||||
d := v.(map[string]any)
|
||||
//logger.SugaredLogger.Infof("key:%s title:%s dateMsg:%s unit:%s", d["key"], d["title"], d["dateMsg"], d["unit"])
|
||||
title := convertor.ToString(d["title"])
|
||||
if convertor.ToString(d["dateMsg"]) != "" {
|
||||
title = title + "[" + convertor.ToString(d["dateMsg"]) + "]"
|
||||
}
|
||||
if convertor.ToString(d["unit"]) != "" {
|
||||
title = title + "(" + convertor.ToString(d["unit"]) + ")"
|
||||
}
|
||||
headers[d["key"].(string)] = title
|
||||
}
|
||||
table := &[]map[string]any{}
|
||||
for _, v := range dataList {
|
||||
//logger.SugaredLogger.Infof("v:%+v", v)
|
||||
d := v.(map[string]any)
|
||||
tmp := map[string]any{}
|
||||
for key, title := range headers {
|
||||
//logger.SugaredLogger.Infof("%s:%s", title, convertor.ToString(d[key]))
|
||||
tmp[title] = convertor.ToString(d[key])
|
||||
}
|
||||
*table = append(*table, tmp)
|
||||
//logger.SugaredLogger.Infof("--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------")
|
||||
}
|
||||
jsonData, _ := json.Marshal(*table)
|
||||
markdownTable, _ := JSONToMarkdownTable(jsonData)
|
||||
logger.SugaredLogger.Infof("markdownTable=\n%s", markdownTable)
|
||||
}
|
||||
|
||||
func TestSearchStockApi_HotStrategy(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewSearchStockApi("").HotStrategy()
|
||||
bytes, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
strategy := &models.HotStrategy{}
|
||||
json.Unmarshal(bytes, strategy)
|
||||
for _, data := range strategy.Data {
|
||||
data.Chg = mathutil.RoundToFloat(100*data.Chg, 2)
|
||||
}
|
||||
markdownTable := util.MarkdownTable(strategy.Data)
|
||||
logger.SugaredLogger.Infof("res:%s", markdownTable)
|
||||
//dataList := res["data"].([]any)
|
||||
//for _, v := range dataList {
|
||||
// d := v.(map[string]any)
|
||||
// logger.SugaredLogger.Infof("v:%+v", d)
|
||||
//}
|
||||
}
|
||||
func TestSearchStockApi_HotStrategyTable(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewSearchStockApi("").StrategySquare()
|
||||
logger.SugaredLogger.Infof("res:%+v", res)
|
||||
}
|
||||
@@ -2,8 +2,12 @@ package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -15,125 +19,216 @@ type Settings struct {
|
||||
DingRobot string `json:"dingRobot"`
|
||||
UpdateBasicInfoOnStart bool `json:"updateBasicInfoOnStart"`
|
||||
RefreshInterval int64 `json:"refreshInterval"`
|
||||
|
||||
OpenAiEnable bool `json:"openAiEnable"`
|
||||
OpenAiBaseUrl string `json:"openAiBaseUrl"`
|
||||
OpenAiApiKey string `json:"openAiApiKey"`
|
||||
OpenAiModelName string `json:"openAiModelName"`
|
||||
OpenAiMaxTokens int `json:"openAiMaxTokens"`
|
||||
OpenAiTemperature float64 `json:"openAiTemperature"`
|
||||
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
|
||||
Prompt string `json:"prompt"`
|
||||
CheckUpdate bool `json:"checkUpdate"`
|
||||
QuestionTemplate string `json:"questionTemplate"`
|
||||
CrawlTimeOut int64 `json:"crawlTimeOut"`
|
||||
KDays int64 `json:"kDays"`
|
||||
EnableDanmu bool `json:"enableDanmu"`
|
||||
BrowserPath string `json:"browserPath"`
|
||||
EnableNews bool `json:"enableNews"`
|
||||
DarkTheme bool `json:"darkTheme"`
|
||||
BrowserPoolSize int `json:"browserPoolSize"`
|
||||
EnableFund bool `json:"enableFund"`
|
||||
OpenAiEnable bool `json:"openAiEnable"`
|
||||
Prompt string `json:"prompt"`
|
||||
CheckUpdate bool `json:"checkUpdate"`
|
||||
QuestionTemplate string `json:"questionTemplate"`
|
||||
CrawlTimeOut int64 `json:"crawlTimeOut"`
|
||||
KDays int64 `json:"kDays"`
|
||||
EnableDanmu bool `json:"enableDanmu"`
|
||||
BrowserPath string `json:"browserPath"`
|
||||
EnableNews bool `json:"enableNews"`
|
||||
DarkTheme bool `json:"darkTheme"`
|
||||
BrowserPoolSize int `json:"browserPoolSize"`
|
||||
EnableFund bool `json:"enableFund"`
|
||||
EnablePushNews bool `json:"enablePushNews"`
|
||||
EnableOnlyPushRedNews bool `json:"enableOnlyPushRedNews"`
|
||||
SponsorCode string `json:"sponsorCode"`
|
||||
HttpProxy string `json:"httpProxy"`
|
||||
HttpProxyEnabled bool `json:"httpProxyEnabled"`
|
||||
EnableAgent bool `json:"enableAgent"`
|
||||
QgqpBId string `json:"qgqpBId" gorm:"column:qgqp_b_id"`
|
||||
}
|
||||
|
||||
func (receiver Settings) TableName() string {
|
||||
return "settings"
|
||||
}
|
||||
|
||||
type SettingsApi struct {
|
||||
Config Settings
|
||||
type AIConfig struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string `json:"name"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
ApiKey string `json:"apiKey" `
|
||||
ModelName string `json:"modelName"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
TimeOut int `json:"timeOut"`
|
||||
HttpProxy string `json:"httpProxy"`
|
||||
HttpProxyEnabled bool `json:"httpProxyEnabled"`
|
||||
}
|
||||
|
||||
func NewSettingsApi(settings *Settings) *SettingsApi {
|
||||
func (AIConfig) TableName() string {
|
||||
return "ai_config"
|
||||
}
|
||||
|
||||
type SettingConfig struct {
|
||||
*Settings
|
||||
AiConfigs []*AIConfig `json:"aiConfigs"`
|
||||
}
|
||||
|
||||
type SettingsApi struct {
|
||||
Config *SettingConfig
|
||||
}
|
||||
|
||||
func NewSettingsApi() *SettingsApi {
|
||||
return &SettingsApi{
|
||||
Config: *settings,
|
||||
Config: GetSettingConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s SettingsApi) UpdateConfig() string {
|
||||
func (s *SettingsApi) Export() string {
|
||||
d, _ := json.MarshalIndent(s.Config, "", " ")
|
||||
return string(d)
|
||||
}
|
||||
|
||||
func UpdateConfig(s *SettingConfig) string {
|
||||
count := int64(0)
|
||||
db.Dao.Model(s.Config).Count(&count)
|
||||
db.Dao.Model(&Settings{}).Count(&count)
|
||||
if count > 0 {
|
||||
db.Dao.Model(s.Config).Where("id=?", s.Config.ID).Updates(map[string]any{
|
||||
"local_push_enable": s.Config.LocalPushEnable,
|
||||
"ding_push_enable": s.Config.DingPushEnable,
|
||||
"ding_robot": s.Config.DingRobot,
|
||||
"update_basic_info_on_start": s.Config.UpdateBasicInfoOnStart,
|
||||
"refresh_interval": s.Config.RefreshInterval,
|
||||
"open_ai_enable": s.Config.OpenAiEnable,
|
||||
"open_ai_base_url": s.Config.OpenAiBaseUrl,
|
||||
"open_ai_api_key": s.Config.OpenAiApiKey,
|
||||
"open_ai_model_name": s.Config.OpenAiModelName,
|
||||
"open_ai_max_tokens": s.Config.OpenAiMaxTokens,
|
||||
"open_ai_temperature": s.Config.OpenAiTemperature,
|
||||
"tushare_token": s.Config.TushareToken,
|
||||
"prompt": s.Config.Prompt,
|
||||
"check_update": s.Config.CheckUpdate,
|
||||
"open_ai_api_time_out": s.Config.OpenAiApiTimeOut,
|
||||
"question_template": s.Config.QuestionTemplate,
|
||||
"crawl_time_out": s.Config.CrawlTimeOut,
|
||||
"k_days": s.Config.KDays,
|
||||
"enable_danmu": s.Config.EnableDanmu,
|
||||
"browser_path": s.Config.BrowserPath,
|
||||
"enable_news": s.Config.EnableNews,
|
||||
"dark_theme": s.Config.DarkTheme,
|
||||
"enable_fund": s.Config.EnableFund,
|
||||
db.Dao.Model(&Settings{}).Where("id=?", s.ID).Updates(map[string]any{
|
||||
"local_push_enable": s.LocalPushEnable,
|
||||
"ding_push_enable": s.DingPushEnable,
|
||||
"ding_robot": s.DingRobot,
|
||||
"update_basic_info_on_start": s.UpdateBasicInfoOnStart,
|
||||
"refresh_interval": s.RefreshInterval,
|
||||
"open_ai_enable": s.OpenAiEnable,
|
||||
"tushare_token": s.TushareToken,
|
||||
"prompt": s.Prompt,
|
||||
"check_update": s.CheckUpdate,
|
||||
"question_template": s.QuestionTemplate,
|
||||
"crawl_time_out": s.CrawlTimeOut,
|
||||
"k_days": s.KDays,
|
||||
"enable_danmu": s.EnableDanmu,
|
||||
"browser_path": s.BrowserPath,
|
||||
"enable_news": s.EnableNews,
|
||||
"dark_theme": s.DarkTheme,
|
||||
"enable_fund": s.EnableFund,
|
||||
"enable_push_news": s.EnablePushNews,
|
||||
"enable_only_push_red_news": s.EnableOnlyPushRedNews,
|
||||
"sponsor_code": s.SponsorCode,
|
||||
"http_proxy": s.HttpProxy,
|
||||
"http_proxy_enabled": s.HttpProxyEnabled,
|
||||
"enable_agent": s.EnableAgent,
|
||||
"qgqp_b_id": s.QgqpBId,
|
||||
})
|
||||
|
||||
//更新AiConfig
|
||||
err := updateAiConfigs(s.AiConfigs)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("更新AI模型服务配置失败: %v", err)
|
||||
return "更新AI模型服务配置失败: " + err.Error()
|
||||
}
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
|
||||
db.Dao.Model(s.Config).Create(&Settings{
|
||||
LocalPushEnable: s.Config.LocalPushEnable,
|
||||
DingPushEnable: s.Config.DingPushEnable,
|
||||
DingRobot: s.Config.DingRobot,
|
||||
UpdateBasicInfoOnStart: s.Config.UpdateBasicInfoOnStart,
|
||||
RefreshInterval: s.Config.RefreshInterval,
|
||||
OpenAiEnable: s.Config.OpenAiEnable,
|
||||
OpenAiBaseUrl: s.Config.OpenAiBaseUrl,
|
||||
OpenAiApiKey: s.Config.OpenAiApiKey,
|
||||
OpenAiModelName: s.Config.OpenAiModelName,
|
||||
OpenAiMaxTokens: s.Config.OpenAiMaxTokens,
|
||||
OpenAiTemperature: s.Config.OpenAiTemperature,
|
||||
TushareToken: s.Config.TushareToken,
|
||||
Prompt: s.Config.Prompt,
|
||||
CheckUpdate: s.Config.CheckUpdate,
|
||||
OpenAiApiTimeOut: s.Config.OpenAiApiTimeOut,
|
||||
QuestionTemplate: s.Config.QuestionTemplate,
|
||||
CrawlTimeOut: s.Config.CrawlTimeOut,
|
||||
KDays: s.Config.KDays,
|
||||
EnableDanmu: s.Config.EnableDanmu,
|
||||
BrowserPath: s.Config.BrowserPath,
|
||||
EnableNews: s.Config.EnableNews,
|
||||
DarkTheme: s.Config.DarkTheme,
|
||||
EnableFund: s.Config.EnableFund,
|
||||
})
|
||||
logger.SugaredLogger.Infof("未找到配置,创建默认配置")
|
||||
// 创建主配置
|
||||
result := db.Dao.Model(&Settings{}).Create(&Settings{})
|
||||
if result.Error != nil {
|
||||
logger.SugaredLogger.Error("创建配置失败:", result.Error)
|
||||
return "创建配置失败: " + result.Error.Error()
|
||||
}
|
||||
}
|
||||
return "保存成功!"
|
||||
}
|
||||
func (s SettingsApi) GetConfig() *Settings {
|
||||
var settings Settings
|
||||
db.Dao.Model(&Settings{}).First(&settings)
|
||||
|
||||
func updateAiConfigs(aiConfigs []*AIConfig) error {
|
||||
if len(aiConfigs) == 0 {
|
||||
err := db.Dao.Exec("DELETE FROM ai_config").Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Dao.Exec("DELETE FROM sqlite_sequence WHERE name='ai_config'").Error
|
||||
}
|
||||
var ids []uint
|
||||
lo.ForEach(aiConfigs, func(item *AIConfig, index int) {
|
||||
ids = append(ids, item.ID)
|
||||
})
|
||||
var existAiConfigs []*AIConfig
|
||||
err := db.Dao.Model(&AIConfig{}).Select("id").Where("id in (?) ", ids).Find(&existAiConfigs).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
idMap := make(map[uint]bool)
|
||||
lo.ForEach(existAiConfigs, func(item *AIConfig, index int) {
|
||||
idMap[item.ID] = true
|
||||
})
|
||||
var addAiConfigs []*AIConfig
|
||||
var notDeleteIds []uint
|
||||
var e error
|
||||
lo.ForEach(aiConfigs, func(item *AIConfig, index int) {
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
if !idMap[item.ID] {
|
||||
addAiConfigs = append(addAiConfigs, item)
|
||||
} else {
|
||||
notDeleteIds = append(notDeleteIds, item.ID)
|
||||
e = db.Dao.Model(&AIConfig{}).Where("id=?", item.ID).Updates(map[string]interface{}{
|
||||
"name": item.Name,
|
||||
"base_url": item.BaseUrl,
|
||||
"api_key": item.ApiKey,
|
||||
"model_name": item.ModelName,
|
||||
"max_tokens": item.MaxTokens,
|
||||
"temperature": item.Temperature,
|
||||
"time_out": item.TimeOut,
|
||||
"http_proxy": item.HttpProxy,
|
||||
"http_proxy_enabled": item.HttpProxyEnabled,
|
||||
}).Error
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
//删除旧的配置
|
||||
if len(notDeleteIds) > 0 {
|
||||
err = db.Dao.Exec("DELETE FROM ai_config WHERE id NOT IN ?", notDeleteIds).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
logger.SugaredLogger.Infof("更新aiConfigs +%d", len(addAiConfigs))
|
||||
//批量新增的配置
|
||||
err = db.Dao.CreateInBatches(addAiConfigs, len(addAiConfigs)).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func GetSettingConfig() *SettingConfig {
|
||||
settingConfig := &SettingConfig{}
|
||||
settings := &Settings{}
|
||||
aiConfigs := make([]*AIConfig, 0)
|
||||
// 处理数据库查询可能返回的空结果
|
||||
result := db.Dao.Model(&Settings{}).First(settings)
|
||||
if settings.OpenAiEnable {
|
||||
if settings.OpenAiApiTimeOut <= 0 {
|
||||
settings.OpenAiApiTimeOut = 60 * 5
|
||||
// 处理AI配置查询可能出现的错误
|
||||
result = db.Dao.Model(&AIConfig{}).Find(&aiConfigs)
|
||||
if result.Error != nil {
|
||||
logger.SugaredLogger.Error("查询AI配置失败:", result.Error)
|
||||
} else if len(aiConfigs) > 0 {
|
||||
lo.ForEach(aiConfigs, func(item *AIConfig, index int) {
|
||||
if item.TimeOut <= 0 {
|
||||
item.TimeOut = 60 * 5
|
||||
}
|
||||
})
|
||||
}
|
||||
if settings.CrawlTimeOut <= 0 {
|
||||
settings.CrawlTimeOut = 60
|
||||
}
|
||||
if settings.KDays < 30 {
|
||||
settings.KDays = 120
|
||||
settings.KDays = 60
|
||||
}
|
||||
}
|
||||
if settings.BrowserPath == "" {
|
||||
settings.BrowserPath, _ = CheckBrowserOnWindows()
|
||||
settings.BrowserPath, _ = CheckBrowser()
|
||||
}
|
||||
if settings.BrowserPoolSize <= 0 {
|
||||
settings.BrowserPoolSize = 1
|
||||
}
|
||||
return &settings
|
||||
}
|
||||
settingConfig.Settings = settings
|
||||
settingConfig.AiConfigs = aiConfigs
|
||||
|
||||
func (s SettingsApi) Export() string {
|
||||
d, _ := json.MarshalIndent(s.GetConfig(), "", " ")
|
||||
return string(d)
|
||||
return settingConfig
|
||||
}
|
||||
|
||||
1
backend/data/stock_basic.json
Normal file
1
backend/data/stock_basic.json
Normal file
File diff suppressed because one or more lines are too long
@@ -8,34 +8,38 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
url2 "net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/robertkrimen/otto"
|
||||
"github.com/samber/lo"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/plugin/soft_delete"
|
||||
"io"
|
||||
"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 {
|
||||
client *resty.Client
|
||||
config *Settings
|
||||
config *SettingConfig
|
||||
}
|
||||
type StockInfo struct {
|
||||
gorm.Model
|
||||
@@ -151,6 +155,8 @@ type StockBasic struct {
|
||||
IsHs string `json:"is_hs"`
|
||||
ActName string `json:"act_name"`
|
||||
ActEntType string `json:"act_ent_type"`
|
||||
BKName string `json:"bk_name"`
|
||||
BKCode string `json:"bk_code"`
|
||||
}
|
||||
|
||||
type FollowedStock struct {
|
||||
@@ -168,6 +174,7 @@ type FollowedStock struct {
|
||||
Cron *string
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
Groups []GroupStock `gorm:"foreignKey:StockCode;references:StockCode"`
|
||||
AiConfigId int
|
||||
}
|
||||
|
||||
func (receiver FollowedStock) TableName() string {
|
||||
@@ -192,7 +199,7 @@ func (receiver StockBasic) TableName() string {
|
||||
func NewStockDataApi() *StockDataApi {
|
||||
return &StockDataApi{
|
||||
client: resty.New(),
|
||||
config: GetConfig(),
|
||||
config: GetSettingConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +252,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 +263,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 +296,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 +369,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)
|
||||
@@ -327,6 +380,9 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
if stockData == nil {
|
||||
continue
|
||||
}
|
||||
stockInfos = append(stockInfos, *stockData)
|
||||
|
||||
go func() {
|
||||
@@ -351,6 +407,28 @@ 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)
|
||||
|
||||
// 检查是否已经关注过该股票
|
||||
var existingStock FollowedStock
|
||||
result := db.Dao.Model(&FollowedStock{}).Where("stock_code = ? AND is_del = ?", stockCode, 0).First(&existingStock)
|
||||
if result.Error == nil {
|
||||
// 股票已经关注过
|
||||
return "已经关注了"
|
||||
}
|
||||
|
||||
maxSort := int64(0)
|
||||
db.Dao.Model(&FollowedStock{}).Raw("select max(sort) as sort from followed_stock").Scan(&maxSort)
|
||||
@@ -414,15 +492,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(¤tStock).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 +635,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 {
|
||||
@@ -912,7 +1179,7 @@ func getHKStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
return &messages
|
||||
}
|
||||
|
||||
func getZSInfo(name, stockCode string, crawlTimeOut int64) string {
|
||||
func GetZSInfo(name, stockCode string, crawlTimeOut int64) string {
|
||||
url := "https://finance.sina.com.cn/realstock/company/" + stockCode + "/nc.shtml"
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
@@ -937,6 +1204,10 @@ func getZSInfo(name, stockCode string, crawlTimeOut int64) string {
|
||||
price := strutil.RemoveWhiteSpace(document.Find("div#price").First().Text(), false)
|
||||
hqTime := strutil.RemoveWhiteSpace(document.Find("div#hqTime").First().Text(), false)
|
||||
|
||||
if strutil.ContainsAny(price, []string{"-", "--"}) {
|
||||
return "暂无数据"
|
||||
}
|
||||
|
||||
var markdown strings.Builder
|
||||
markdown.WriteString(fmt.Sprintf("### 时间:%s %s:%s \n", hqTime, name, price))
|
||||
GetTableMarkdown(document, "div#hqDetails table", &markdown)
|
||||
@@ -1047,48 +1318,64 @@ func SearchStockInfoByCode(stock string) *[]string {
|
||||
return &messages
|
||||
}
|
||||
|
||||
// checkChromeOnWindows 在 Windows 系统上检查谷歌浏览器是否安装
|
||||
func checkChromeOnWindows() (string, bool) {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
|
||||
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
defer key.Close()
|
||||
// 分时数据
|
||||
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"
|
||||
}
|
||||
defer key.Close()
|
||||
path, _, err := key.GetStringValue("Path")
|
||||
//logger.SugaredLogger.Infof("Chrome安装路径:%s", path)
|
||||
if err != nil {
|
||||
return "", false
|
||||
if strutil.HasPrefixAny(stockCode, []string{"us", "US"}) {
|
||||
url = fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/UsMinute/query?code=%s", stockCode)
|
||||
}
|
||||
return path + "\\chrome.exe", true
|
||||
}
|
||||
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)
|
||||
|
||||
// CheckBrowserOnWindows 在 Windows 系统上检查Edge浏览器是否安装,并返回安装路径
|
||||
func CheckBrowserOnWindows() (string, bool) {
|
||||
if path, ok := checkChromeOnWindows(); ok {
|
||||
return path, true
|
||||
}
|
||||
date := ""
|
||||
minuteDatas := &[]MinuteData{}
|
||||
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
|
||||
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return "", false
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
defer key.Close()
|
||||
}
|
||||
defer key.Close()
|
||||
path, _, err := key.GetStringValue("Path")
|
||||
//logger.SugaredLogger.Infof("Edge安装路径:%s", path)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return path + "\\msedge.exe", true
|
||||
return minuteDatas, date
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) GetKLineData(stockCode string, kLineType string, days int64) *[]KLineData {
|
||||
@@ -1158,6 +1445,257 @@ func (receiver StockDataApi) GetHK_KLineData(stockCode string, kLineType string,
|
||||
}
|
||||
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 := "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048"
|
||||
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?np=1&fltt=1&invt=2&cb=data&fs=%s&fields=f12,f13,f14,f1,f2,f4,f3,f152,f5,f6,f7,f15,f18,f16,f17,f10,f8,f9,f23,f100,f265&fid=f3&pn=%d&pz=%d&po=1&dect=1&wbp2u=|0|0|0|web&_=%d"
|
||||
sprintfUrl := fmt.Sprintf(url, fs, page, pageSize, time.Now().UnixMilli())
|
||||
logger.SugaredLogger.Infof("page:%d url:%s", page, sprintfUrl)
|
||||
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "push2.eastmoney.com").
|
||||
SetHeader("Referer", "https://quote.eastmoney.com/center/gridlist.html").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0").
|
||||
Get(sprintfUrl)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
body := string(resp.Body())
|
||||
logger.SugaredLogger.Infof("resp:%s", body)
|
||||
vm := otto.New()
|
||||
vm.Run("function data(res){return res};")
|
||||
val, err := vm.Run(body)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("vm.Run error:%v", err.Error())
|
||||
}
|
||||
value, _ := val.Object().Value().Export()
|
||||
marshal, err := json.Marshal(value)
|
||||
data := make(map[string]any)
|
||||
err = json.Unmarshal(marshal, &data)
|
||||
if err != nil {
|
||||
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"].([]any)
|
||||
logger.SugaredLogger.Infof("total:%d", int(total))
|
||||
for k, item := range diff {
|
||||
stock := item.(map[string]any)
|
||||
logger.SugaredLogger.Infof("k:%d,%s:%s:%s %s:%s", k, stock["f14"], stock["f12"], DCToTsCode(stock["f12"].(string)), stock["f100"], stock["f265"])
|
||||
|
||||
if market == "" {
|
||||
stockInfo := &StockBasic{
|
||||
Symbol: stock["f12"].(string),
|
||||
TsCode: DCToTsCode(stock["f12"].(string)),
|
||||
Name: stock["f14"].(string),
|
||||
BKName: stock["f100"].(string),
|
||||
BKCode: stock["f265"].(string),
|
||||
}
|
||||
db.Dao.Model(&StockBasic{}).Where("symbol = ?", stockInfo.Symbol).First(stockInfo)
|
||||
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
|
||||
if stockInfo.ID == 0 {
|
||||
db.Dao.Model(&StockBasic{}).Create(stockInfo)
|
||||
} else {
|
||||
stockInfo = &StockBasic{
|
||||
Symbol: stock["f12"].(string),
|
||||
TsCode: DCToTsCode(stock["f12"].(string)),
|
||||
Name: stock["f14"].(string),
|
||||
BKName: stock["f100"].(string),
|
||||
BKCode: stock["f265"].(string),
|
||||
}
|
||||
db.Dao.Model(&StockBasic{}).Where("symbol = ?", stockInfo.Symbol).Updates(stockInfo)
|
||||
}
|
||||
}
|
||||
|
||||
if market == "hk" {
|
||||
stockInfo := &models.StockInfoHK{
|
||||
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
|
||||
Name: stock["f14"].(string),
|
||||
BKName: stock["f100"].(string),
|
||||
BKCode: stock["f265"].(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)
|
||||
} else {
|
||||
stockInfo = &models.StockInfoHK{
|
||||
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
|
||||
Name: stock["f14"].(string),
|
||||
BKName: stock["f100"].(string),
|
||||
BKCode: stock["f265"].(string),
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stockInfo.Code).Updates(stockInfo)
|
||||
}
|
||||
}
|
||||
|
||||
if market == "us" {
|
||||
stockInfo := &models.StockInfoUS{
|
||||
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
|
||||
Name: stock["f14"].(string),
|
||||
BKName: stock["f100"].(string),
|
||||
BKCode: stock["f265"].(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)
|
||||
} else {
|
||||
stockInfo = &models.StockInfoUS{
|
||||
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
|
||||
Name: stock["f14"].(string),
|
||||
BKName: stock["f100"].(string),
|
||||
BKCode: stock["f265"].(string),
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stockInfo.Code).Updates(stockInfo)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func DCToTsCode(dcCode string) string {
|
||||
//北京证券交易所 8(83、87、88 等) 创新型中小企业(专精特新为主)
|
||||
//上海证券交易所 6(60、688 等) 大盘蓝筹、科创板(高新技术)
|
||||
//深圳证券交易所 0、3(000、002、30 等) 中小盘、创业板(成长型创新企业)
|
||||
switch dcCode[0:1] {
|
||||
case "8":
|
||||
return dcCode + ".BJ"
|
||||
case "9":
|
||||
return dcCode + ".BJ"
|
||||
case "6":
|
||||
return dcCode + ".SH"
|
||||
case "0":
|
||||
return dcCode + ".SZ"
|
||||
case "3":
|
||||
return dcCode + ".SZ"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -1208,6 +1746,70 @@ func (receiver StockDataApi) GetCommonKLineData(stockCode string, kLineType stri
|
||||
return K
|
||||
}
|
||||
|
||||
// GetStockHistoryMoneyData 获取股票历史资金流向数据
|
||||
func (receiver StockDataApi) GetStockHistoryMoneyData() {
|
||||
|
||||
}
|
||||
|
||||
// GetStockMoneyData 获取个股资金流数据
|
||||
func (receiver StockDataApi) GetStockMoneyData() models.StockMoneyDataResp {
|
||||
var resData models.StockMoneyDataResp
|
||||
url := "https://push2.eastmoney.com/api/qt/clist/get?cb=data&fid=f62&po=1&pz=50&pn=1&np=1&fltt=2&invt=2&ut=8dec03ba335b81bf4ebdf7b29ec27d15&fs=m:0+t:6+f:!2,m:0+t:13+f:!2,m:0+t:80+f:!2,m:1+t:2+f:!2,m:1+t:23+f:!2,m:0+t:7+f:!2,m:1+t:3+f:!2&fields=f12,f14,f2,f3,f62,f184,f66,f69,f72,f75,f78,f81,f84,f87,f204,f205,f124,f1,f13,f100,f265"
|
||||
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "push2.eastmoney.com").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
}
|
||||
body := string(resp.Body())
|
||||
logger.SugaredLogger.Infof("resp:%s", body)
|
||||
vm := otto.New()
|
||||
vm.Run("function data(res){return res};")
|
||||
val, err := vm.Run(body)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
}
|
||||
value, err := val.Export()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
}
|
||||
marshal, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return models.StockMoneyDataResp{}
|
||||
}
|
||||
err = json.Unmarshal(marshal, &resData)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return models.StockMoneyDataResp{}
|
||||
}
|
||||
return resData
|
||||
}
|
||||
|
||||
// 获取股票概念题材信息
|
||||
func (receiver StockDataApi) GetStockConceptInfo(stockCode string) models.StockConceptInfoResp {
|
||||
//601138.SH
|
||||
url := "https://datacenter.eastmoney.com/securities/api/data/v1/get?reportName=RPT_F10_CORETHEME_BOARDTYPE&columns=SECUCODE%2CSECURITY_CODE%2CSECURITY_NAME_ABBR%2CNEW_BOARD_CODE%2CBOARD_NAME%2CSELECTED_BOARD_REASON%2CIS_PRECISE%2CBOARD_RANK%2CBOARD_YIELD%2CDERIVE_BOARD_CODE"eColumns=f3~05~NEW_BOARD_CODE~BOARD_YIELD&filter=(SECUCODE%3D%22" + stockCode + "%22)(IS_PRECISE%3D%221%22)&pageNumber=1&pageSize=&sortTypes=1&sortColumns=BOARD_RANK&source=HSF10&client=PC&v=005634233622011753"
|
||||
logger.SugaredLogger.Infof("url:%s", url2.QueryEscape(url))
|
||||
var data models.StockConceptInfoResp
|
||||
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "datacenter.eastmoney.com").
|
||||
SetHeader("Referer", "https://emweb.securities.eastmoney.com/").
|
||||
SetHeader("Origin", "https://emweb.securities.eastmoney.com").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
}
|
||||
err = json.Unmarshal(resp.Body(), &data)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return models.StockConceptInfoResp{}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// JSONToMarkdownTable 将JSON数据转换为Markdown表格
|
||||
func JSONToMarkdownTable(jsonData []byte) (string, error) {
|
||||
var data []map[string]interface{}
|
||||
@@ -1262,3 +1864,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"`
|
||||
}
|
||||
|
||||
37
backend/data/stock_data_api_darwin.go
Normal file
37
backend/data/stock_data_api_darwin.go
Normal file
@@ -0,0 +1,37 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package data
|
||||
|
||||
import "os"
|
||||
|
||||
// CheckChrome 检查 macOS 是否安装了 Chrome 浏览器
|
||||
func CheckChrome() (string, bool) {
|
||||
// 检查 /Applications 目录下是否存在 Chrome
|
||||
locations := []string{
|
||||
// Mac
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
}
|
||||
path := ""
|
||||
for _, location := range locations {
|
||||
_, err := os.Stat(location)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
path = location
|
||||
}
|
||||
if path == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return path, true
|
||||
}
|
||||
|
||||
// CheckBrowser 检查 macOS 是否安装了浏览器,并返回安装路径
|
||||
func CheckBrowser() (string, bool) {
|
||||
if path, ok := CheckChrome(); ok {
|
||||
return path, ok
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
@@ -4,16 +4,19 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/util"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -46,30 +49,46 @@ func TestGetFinancialReports(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetTelegraphSearch(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
searchWords := "半导体 新能源汽车 机器人"
|
||||
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram"
|
||||
messages := SearchStockInfo("谷歌", "telegram", 30)
|
||||
messages := SearchStockInfo(searchWords, "telegram", 30)
|
||||
for _, message := range *messages {
|
||||
logger.SugaredLogger.Info(message)
|
||||
}
|
||||
|
||||
//https://www.cls.cn/stock?code=sh600745
|
||||
}
|
||||
func TestCailianpressWeb(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
searchWords := "半导体 新能源汽车 机器人"
|
||||
res := NewMarketNewsApi().CailianpressWeb(searchWords)
|
||||
md := util.MarkdownTableWithTitle(searchWords+"财联社新闻", res.List)
|
||||
logger.SugaredLogger.Info(md)
|
||||
}
|
||||
|
||||
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("博安生物", "hk06955", 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)
|
||||
@@ -96,6 +115,28 @@ func TestGetHK_KLineData(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
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 //港股
|
||||
//274 224 605
|
||||
for i := 197; i <= 274; i++ {
|
||||
NewStockDataApi().getDCStockInfo("", i, 20)
|
||||
time.Sleep(time.Duration(random.RandInt(2, 5)) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTxStockData(t *testing.T) {
|
||||
str := "v_r_hk09660=\"100~地平线机器人-W~09660~6.340~5.690~5.800~210980204.0~0~0~6.340~0~0~0~0~0~0~0~0~0~6.340~0~0~0~0~0~0~0~0~0~210980204.0~2025/04/29\n14:14:52~0.650~11.42~6.450~5.710~6.340~210980204.0~1295585259.040~0~33.03~~0~0~13.01~702.2123~836.8986~HORIZONROBOT-W~0.00~10.380~3.320~1.00~-53.74~0~0~0~0~0~33.03~6.50~1.90~600~76.11~19.85~GP~19.70~11.51~0.63~-17.23~46.76~13200293682.00~11075904412.00~33.03~0.000~6.141~58.90~HKD~1~30\";"
|
||||
//str = "v_sz002241=\"51~歌尔股份~002241~22.26~22.27~0.00~0~0~0~22.26~1004~0.00~0~0.00~0~0.00~0~0.00~0~22.26~1004~0.00~558~0.00~0~0.00~0~0.00~0~~20250509092233~-0.01~-0.04~0.00~0.00~22.26/0/0~0~0~0.00~28.21~~0.00~0.00~0.00~686.46~777.09~2.31~24.50~20.04~0.00~-558~0.00~41.44~29.16~~~1.24~0.0000~0.0000~0~\n~GP-A~-13.75~6.76~1.09~8.18~3.39~30.63~15.70~6.87~17.47~-23.95~3083811231~3490989083~-21.75~12.02~3083811231~~~39.36~-0.04~~CNY~0~~0.00~0\";"
|
||||
str = "v_sz002241=\"51~歌尔股份~002241~21.92~22.27~22.14~109872~40211~69642~21.91~25~21.90~961~21.89~257~21.88~748~21.87~665~21.92~86~21.93~168~21.94~556~21.95~171~21.96~85~~20250509094209~-0.35~-1.57~22.16~21.84~21.92/109872/241183171~109872~24118~0.36~27.78~~22.16~21.84~1.44~675.97~765.22~2.27~24.50~20.04~2.57~1590~21.95~40.80~28.71~~~1.24~24118.3171~0.0000~0~\n~GP-A~-15.07~5.13~1.11~8.18~3.39~30.63~15.70~5.23~15.67~-25.11~3083811231~3490989083~42.72~10.31~3083811231~~~37.23~0.18~~CNY~0~~21.85~1952\";"
|
||||
//str = "v_r_hk09660=\"100~地平线机器人-W~09660~6.860~7.000~7.010~21157200.0~0~0~6.860~0~0~0~0~0~0~0~0~0~6.860~0~0~0~0~0~0~0~0~0~21157200.0~2025/05/09\n09:43:13~-0.140~-2.00~7.030~6.730~6.860~21157200.0~144331073.000~0~35.74~~0~0~4.29~759.8070~905.5401~HORIZONROBOT-W~0.00~10.380~3.320~2.93~11.10~0~0~0~0~0~35.74~7.04~0.19~600~90.56~4.73~GP~19.70~11.51~17.26~48.48~13.58~13200293682.00~11075904412.00~35.74~0.000~6.822~71.93~HKD~1~30\";"
|
||||
info, _ := ParseTxStockData(str)
|
||||
logger.SugaredLogger.Infof("%+#v", info)
|
||||
}
|
||||
|
||||
func TestGetRealTimeStockPriceInfo(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
@@ -154,7 +195,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("sz002352", "sh600859", "sh600745", "gb_tsla", "hk09660", "hk00700")
|
||||
for _, data := range *datas {
|
||||
t.Log(data)
|
||||
}
|
||||
@@ -224,3 +265,35 @@ func TestStockDataApi_GetIndexBasic(t *testing.T) {
|
||||
stockDataApi := NewStockDataApi()
|
||||
stockDataApi.GetIndexBasic()
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
stockBasics := &[]StockBasic{}
|
||||
resty.New().SetProxy("").R().
|
||||
SetHeader("user", "go-stock").
|
||||
SetResult(stockBasics).
|
||||
Get("http://8.134.249.145:18080/go-stock/stock_basic.json")
|
||||
|
||||
logger.SugaredLogger.Infof("%+v", stockBasics)
|
||||
//db.Dao.Unscoped().Model(&StockBasic{}).Where("1=1").Delete(&StockBasic{})
|
||||
//err := db.Dao.CreateInBatches(stockBasics, 400).Error
|
||||
//if err != nil {
|
||||
// t.Log(err.Error())
|
||||
//}
|
||||
|
||||
}
|
||||
func TestGetStockMoneyData(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
stockDataApi := NewStockDataApi()
|
||||
res := stockDataApi.GetStockMoneyData()
|
||||
logger.SugaredLogger.Infof("%s", util.MarkdownTableWithTitle("今日个股资金流向Top50", res.Data.Diff))
|
||||
}
|
||||
|
||||
func TestGetStockConceptInfo(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
stockDataApi := NewStockDataApi()
|
||||
res := stockDataApi.GetStockConceptInfo("601138.SH")
|
||||
logger.SugaredLogger.Infof("%s", util.MarkdownTableWithTitle("601138.SH所属概念/板块信息", res.Result.Data))
|
||||
|
||||
}
|
||||
|
||||
50
backend/data/stock_data_api_windows.go
Normal file
50
backend/data/stock_data_api_windows.go
Normal file
@@ -0,0 +1,50 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package data
|
||||
|
||||
import "golang.org/x/sys/windows/registry"
|
||||
|
||||
// CheckChrome 在 Windows 系统上检查谷歌浏览器是否安装
|
||||
func CheckChrome() (string, bool) {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
|
||||
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
defer key.Close()
|
||||
}
|
||||
defer key.Close()
|
||||
path, _, err := key.GetStringValue("Path")
|
||||
//logger.SugaredLogger.Infof("Chrome安装路径:%s", path)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return path + "\\chrome.exe", true
|
||||
}
|
||||
|
||||
// CheckBrowser 在 Windows 系统上检查Edge浏览器是否安装,并返回安装路径
|
||||
func CheckBrowser() (string, bool) {
|
||||
if path, ok := CheckChrome(); ok {
|
||||
return path, true
|
||||
}
|
||||
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
// 尝试在 WOW6432Node 中查找(适用于 64 位系统上的 32 位程序)
|
||||
key, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
defer key.Close()
|
||||
}
|
||||
defer key.Close()
|
||||
path, _, err := key.GetStringValue("Path")
|
||||
//logger.SugaredLogger.Infof("Edge安装路径:%s", path)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return path + "\\msedge.exe", true
|
||||
}
|
||||
@@ -39,17 +39,74 @@ func NewStockGroupApi(dao *gorm.DB) *StockGroupApi {
|
||||
}
|
||||
|
||||
func (receiver StockGroupApi) AddGroup(group Group) bool {
|
||||
err := receiver.dao.Where("name = ?", group.Name).FirstOrCreate(&group).Updates(&Group{
|
||||
Name: group.Name,
|
||||
Sort: group.Sort,
|
||||
}).Error
|
||||
// 检查是否已存在相同sort的组
|
||||
var existingGroup Group
|
||||
err := receiver.dao.Where("sort = ?", group.Sort).First(&existingGroup).Error
|
||||
|
||||
// 如果存在相同sort的组,则将该组及之后的所有组向后移动一位
|
||||
if err == nil {
|
||||
// 处理sort冲突:将相同sort值及之后的所有组向后移动一位
|
||||
receiver.dao.Model(&Group{}).Where("sort >= ?", group.Sort).Update("sort", gorm.Expr("sort + ?", 1))
|
||||
}
|
||||
|
||||
// 创建新组
|
||||
err = receiver.dao.Create(&group).Error
|
||||
return err == nil
|
||||
}
|
||||
func (receiver StockGroupApi) GetGroupList() []Group {
|
||||
var groups []Group
|
||||
receiver.dao.Find(&groups)
|
||||
receiver.dao.Order("sort ASC").Find(&groups)
|
||||
return groups
|
||||
}
|
||||
func (receiver StockGroupApi) UpdateGroupSort(id int, newSort int) bool {
|
||||
// First, get the current group to check if it exists
|
||||
var currentGroup Group
|
||||
if err := receiver.dao.First(¤tGroup, id).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the new sort is the same as current, no need to update
|
||||
if currentGroup.Sort == newSort {
|
||||
return true
|
||||
}
|
||||
|
||||
// Get all groups ordered by sort
|
||||
var allGroups []Group
|
||||
receiver.dao.Order("sort ASC").Find(&allGroups)
|
||||
|
||||
// Adjust sort numbers to make space for the new sort value
|
||||
if newSort > currentGroup.Sort {
|
||||
// Moving down: decrease sort of groups between old and new position
|
||||
receiver.dao.Model(&Group{}).Where("sort > ? AND sort <= ? AND id != ?", currentGroup.Sort, newSort, id).Update("sort", gorm.Expr("sort - ?", 1))
|
||||
} else {
|
||||
// Moving up: increase sort of groups between new and old position
|
||||
receiver.dao.Model(&Group{}).Where("sort >= ? AND sort < ? AND id != ?", newSort, currentGroup.Sort, id).Update("sort", gorm.Expr("sort + ?", 1))
|
||||
}
|
||||
|
||||
// Update the target group's sort
|
||||
err := receiver.dao.Model(&Group{}).Where("id = ?", id).Update("sort", newSort).Error
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// InitializeGroupSort initializes sort order for all groups based on created time
|
||||
func (receiver StockGroupApi) InitializeGroupSort() bool {
|
||||
// Get all groups ordered by created time
|
||||
var groups []Group
|
||||
err := receiver.dao.Order("created_at ASC").Find(&groups).Error
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Update each group with new sort value based on their position
|
||||
for i, group := range groups {
|
||||
newSort := i + 1
|
||||
err := receiver.dao.Model(&Group{}).Where("id = ?", group.ID).Update("sort", newSort).Error
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
func (receiver StockGroupApi) GetGroupStockByGroupId(groupId int) []GroupStock {
|
||||
var stockGroup []GroupStock
|
||||
receiver.dao.Preload("GroupInfo").Where("group_id = ?", groupId).Find(&stockGroup)
|
||||
|
||||
578
backend/data/stock_sentiment_analysis.go
Normal file
578
backend/data/stock_sentiment_analysis.go
Normal file
@@ -0,0 +1,578 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/fileutil"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-ego/gse"
|
||||
)
|
||||
|
||||
const basefreq float64 = 100
|
||||
|
||||
// 金融情感词典,包含股票市场相关的专业词汇
|
||||
var (
|
||||
seg gse.Segmenter
|
||||
|
||||
// 正面金融词汇及其权重
|
||||
positiveFinanceWords = map[string]float64{
|
||||
"涨": 1.0, "上涨": 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, "暴涨": 3.0,
|
||||
}
|
||||
|
||||
// 负面金融词汇及其权重
|
||||
negativeFinanceWords = map[string]float64{
|
||||
"跌": 2.0, "下跌": 2.0, "跌停": 3.0, "熊市": 3.0, "回调": 2.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, "走弱": 2.5, "下挫": 2.5,
|
||||
"利空消息": 3.0, "收益下降": 2.5, "利润下滑": 2.5, "业绩不佳": 2.5,
|
||||
"垃圾股": 2.0, "风险股": 2.0, "弱势": 2.5, "走低": 2.5, "缩量": 2.5,
|
||||
"大跌": 2.5, "暴跌": 3.0, "崩盘": 3.0, "跳水": 3.0, "重挫": 3.0, "跌超": 2.5, "跌逾": 2.5, "跌近": 3.0,
|
||||
"被抓": 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, "逾": 1.8, "超": 1.8,
|
||||
}
|
||||
|
||||
// 转折词,用于识别情感转折
|
||||
transitionWords = map[string]struct{}{
|
||||
"但是": {}, "然而": {}, "不过": {}, "却": {}, "可是": {},
|
||||
}
|
||||
)
|
||||
|
||||
//go:embed data/dict/base.txt
|
||||
var baseDict string
|
||||
|
||||
//go:embed data/dict/zh/s_1.txt
|
||||
var zhDict string
|
||||
|
||||
func InitAnalyzeSentiment() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Error(fmt.Sprintf("panic: %v", r))
|
||||
}
|
||||
}()
|
||||
// 加载简体中文词典
|
||||
//err := seg.LoadDict("zh_s")
|
||||
//if err != nil {
|
||||
// logger.SugaredLogger.Error(err.Error())
|
||||
//}
|
||||
|
||||
err := seg.LoadDictEmbed(baseDict)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
} else {
|
||||
logger.SugaredLogger.Info("加载默认词典成功")
|
||||
}
|
||||
seg.CalcToken()
|
||||
|
||||
stocks := &[]StockBasic{}
|
||||
db.Dao.Model(&StockBasic{}).Find(stocks)
|
||||
for _, stock := range *stocks {
|
||||
if strutil.Trim(stock.Name) == "" {
|
||||
continue
|
||||
}
|
||||
err := seg.AddToken(stock.Name, basefreq+100, "n")
|
||||
if strutil.Trim(stock.BKName) != "" {
|
||||
err = seg.AddToken(stock.BKName, basefreq+100, "n")
|
||||
}
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("添加%s失败:%s", stock.Name, err.Error())
|
||||
}
|
||||
}
|
||||
logger.SugaredLogger.Info("加载股票名称词典成功")
|
||||
|
||||
stockhks := &[]models.StockInfoHK{}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Find(stockhks)
|
||||
for _, stock := range *stockhks {
|
||||
if strutil.Trim(stock.Name) == "" {
|
||||
continue
|
||||
}
|
||||
err := seg.AddToken(stock.Name, basefreq+100, "n")
|
||||
if strutil.Trim(stock.BKName) != "" {
|
||||
err = seg.AddToken(stock.BKName, basefreq+100, "n")
|
||||
}
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("添加%s失败:%s", stock.Name, err.Error())
|
||||
}
|
||||
}
|
||||
logger.SugaredLogger.Info("加载港股名称词典成功")
|
||||
//stockus := &[]models.StockInfoUS{}
|
||||
//db.Dao.Model(&models.StockInfoUS{}).Where("trim(name) != ?", "").Find(stockus)
|
||||
//for _, stock := range *stockus {
|
||||
// err := seg.AddToken(stock.Name, 500)
|
||||
// if err != nil {
|
||||
// logger.SugaredLogger.Errorf("添加%s失败:%s", stock.Name, err.Error())
|
||||
// }
|
||||
//}
|
||||
tags := &[]models.Tags{}
|
||||
db.Dao.Model(&models.Tags{}).Where("type = ?", "subject").Find(tags)
|
||||
for _, tag := range *tags {
|
||||
if tag.Name == "" {
|
||||
continue
|
||||
}
|
||||
err := seg.AddToken(tag.Name, basefreq+100, "n")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("添加%s失败:%s", tag.Name, err.Error())
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("添加tags词典[%s]成功", tag.Name)
|
||||
}
|
||||
}
|
||||
logger.SugaredLogger.Info("加载tags词典成功")
|
||||
seg.CalcToken()
|
||||
//加载用户自定义词典 先判断用户词典是否存在
|
||||
if fileutil.IsExist("data/dict/user.txt") {
|
||||
lines, err := fileutil.ReadFileByLine("data/dict/user.txt")
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
for _, line := range lines {
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
k := strutil.SplitAndTrim(line, " ")
|
||||
if len(k) == 0 {
|
||||
continue
|
||||
}
|
||||
_, _, ok := seg.Find(k[0])
|
||||
switch len(k) {
|
||||
case 1:
|
||||
if ok {
|
||||
err = seg.ReAddToken(k[0], basefreq)
|
||||
} else {
|
||||
err = seg.AddToken(k[0], basefreq)
|
||||
}
|
||||
case 2:
|
||||
freq, _ := convertor.ToFloat(k[1])
|
||||
if ok {
|
||||
err = seg.ReAddToken(k[0], freq)
|
||||
} else {
|
||||
err = seg.AddToken(k[0], freq)
|
||||
}
|
||||
case 3:
|
||||
freq, _ := convertor.ToFloat(k[1])
|
||||
if ok {
|
||||
err = seg.ReAddToken(k[0], freq, k[2])
|
||||
} else {
|
||||
err = seg.AddToken(k[0], freq, k[2])
|
||||
}
|
||||
default:
|
||||
logger.SugaredLogger.Errorf("用户词典格式错误:%s", line)
|
||||
}
|
||||
logger.SugaredLogger.Infof("添加用户词典[%s]成功", line)
|
||||
}
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("加载用户词典成功")
|
||||
}
|
||||
} else {
|
||||
logger.SugaredLogger.Info("用户词典不存在")
|
||||
}
|
||||
seg.CalcToken()
|
||||
}
|
||||
|
||||
// getWordWeight 获取词汇权重
|
||||
func getWordWeight(word string) float64 {
|
||||
// 从分词器获取词汇权重
|
||||
|
||||
freq, pos, ok := seg.Dictionary().Find([]byte(word))
|
||||
if ok {
|
||||
logger.SugaredLogger.Infof("获取%s的权重:%f,pos:%s,ok:%v", word, freq, pos, ok)
|
||||
return freq
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// SortByWeightAndFrequency 按权重和频次排序词频结果
|
||||
func SortByWeightAndFrequency(frequencies map[string]models.WordFreqWithWeight) []models.WordFreqWithWeight {
|
||||
// 将map转换为slice以便排序
|
||||
freqSlice := make([]models.WordFreqWithWeight, 0, len(frequencies))
|
||||
for _, freq := range frequencies {
|
||||
freqSlice = append(freqSlice, freq)
|
||||
}
|
||||
|
||||
// 按权重*频次降序排列
|
||||
sort.Slice(freqSlice, func(i, j int) bool {
|
||||
return freqSlice[i].Weight*float64(freqSlice[i].Frequency) > freqSlice[j].Weight*float64(freqSlice[j].Frequency)
|
||||
})
|
||||
logger.SugaredLogger.Infof("排序后的结果:%v", freqSlice)
|
||||
|
||||
return freqSlice
|
||||
}
|
||||
|
||||
// FilterAndSortWords 过滤标点符号并按权重频次排序
|
||||
func FilterAndSortWords(frequencies map[string]models.WordFreqWithWeight) []models.WordFreqWithWeight {
|
||||
// 先过滤标点符号和分隔符
|
||||
cleanFrequencies := FilterPunctuationAndSeparators(frequencies)
|
||||
|
||||
// 再按权重和频次排序
|
||||
sortedFrequencies := SortByWeightAndFrequency(cleanFrequencies)
|
||||
|
||||
return sortedFrequencies
|
||||
}
|
||||
func FilterPunctuationAndSeparators(frequencies map[string]models.WordFreqWithWeight) map[string]models.WordFreqWithWeight {
|
||||
filteredWords := make(map[string]models.WordFreqWithWeight)
|
||||
|
||||
for word, freqInfo := range frequencies {
|
||||
// 过滤纯标点符号和分隔符
|
||||
if !isPunctuationOrSeparator(word) {
|
||||
filteredWords[word] = freqInfo
|
||||
}
|
||||
}
|
||||
return filteredWords
|
||||
}
|
||||
|
||||
// isPunctuationOrSeparator 判断是否为标点符号或分隔符
|
||||
func isPunctuationOrSeparator(word string) bool {
|
||||
// 空字符串
|
||||
if strings.TrimSpace(word) == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否全部由标点符号组成
|
||||
for _, r := range word {
|
||||
if !unicode.IsPunct(r) && !unicode.IsSymbol(r) && !unicode.IsSpace(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// FilterWithRegex 使用正则表达式过滤标点和特殊字符
|
||||
func FilterWithRegex(frequencies map[string]models.WordFreqWithWeight) map[string]models.WordFreqWithWeight {
|
||||
filteredWords := make(map[string]models.WordFreqWithWeight)
|
||||
|
||||
// 匹配标点符号、特殊字符的正则表达式
|
||||
punctuationRegex := regexp.MustCompile(`^[[:punct:][:space:]]+$`)
|
||||
|
||||
for word, freqInfo := range frequencies {
|
||||
// 过滤纯标点符号
|
||||
if !punctuationRegex.MatchString(word) && strings.TrimSpace(word) != "" {
|
||||
filteredWords[word] = freqInfo
|
||||
}
|
||||
}
|
||||
return filteredWords
|
||||
}
|
||||
|
||||
// countWordFrequencyWithWeight 统计词频并包含权重信息
|
||||
func countWordFrequencyWithWeight(text string) map[string]models.WordFreqWithWeight {
|
||||
words := splitWords(text)
|
||||
freqMap := make(map[string]models.WordFreqWithWeight)
|
||||
|
||||
// 统计词频
|
||||
wordCount := make(map[string]int)
|
||||
for _, word := range words {
|
||||
wordCount[word]++
|
||||
}
|
||||
|
||||
// 构建包含权重的结果
|
||||
for word, frequency := range wordCount {
|
||||
weight := getWordWeight(word)
|
||||
if weight >= basefreq {
|
||||
freqMap[word] = models.WordFreqWithWeight{
|
||||
Word: word,
|
||||
Frequency: frequency,
|
||||
Weight: weight,
|
||||
Score: float64(frequency) * weight,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return freqMap
|
||||
}
|
||||
|
||||
// AnalyzeSentimentWithFreqWeight 带权重词频统计的情感分析
|
||||
func AnalyzeSentimentWithFreqWeight(text string) (models.SentimentResult, map[string]models.WordFreqWithWeight) {
|
||||
// 原有情感分析逻辑
|
||||
result := AnalyzeSentiment(text)
|
||||
|
||||
// 带权重的词频统计
|
||||
frequencies := countWordFrequencyWithWeight(text)
|
||||
|
||||
return result, frequencies
|
||||
}
|
||||
|
||||
const (
|
||||
Positive models.SentimentType = iota
|
||||
Negative
|
||||
Neutral
|
||||
)
|
||||
|
||||
// AnalyzeSentiment 判断文本的情感
|
||||
func AnalyzeSentiment(text string) models.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 models.SentimentType
|
||||
switch {
|
||||
case score > 1.0:
|
||||
category = Positive
|
||||
case score < -1.0:
|
||||
category = Negative
|
||||
default:
|
||||
category = Neutral
|
||||
}
|
||||
|
||||
return models.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 models.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)
|
||||
}
|
||||
}
|
||||
|
||||
func SaveAnalyzeSentimentWithFreqWeight(frequencies []models.WordFreqWithWeight) {
|
||||
|
||||
sort.Slice(frequencies, func(i, j int) bool {
|
||||
return frequencies[i].Frequency > frequencies[j].Frequency
|
||||
})
|
||||
wordAnalyzes := make([]models.WordAnalyze, 0)
|
||||
for _, freq := range frequencies[:10] {
|
||||
wordAnalyze := models.WordAnalyze{
|
||||
WordFreqWithWeight: freq,
|
||||
}
|
||||
wordAnalyzes = append(wordAnalyzes, wordAnalyze)
|
||||
}
|
||||
db.Dao.CreateInBatches(wordAnalyzes, 1000)
|
||||
}
|
||||
|
||||
func SaveStockSentimentAnalysis(result models.SentimentResult) {
|
||||
db.Dao.Create(&models.SentimentResultAnalyze{
|
||||
SentimentResult: result,
|
||||
})
|
||||
}
|
||||
|
||||
func NewsAnalyze(text string, save bool) (models.SentimentResult, []models.WordFreqWithWeight) {
|
||||
if text == "" {
|
||||
telegraphs := NewMarketNewsApi().GetNews24HoursList("", 1000*10)
|
||||
messageText := strings.Builder{}
|
||||
for _, telegraph := range *telegraphs {
|
||||
messageText.WriteString(telegraph.Content + "\n")
|
||||
}
|
||||
text = messageText.String()
|
||||
}
|
||||
result, frequencies := AnalyzeSentimentWithFreqWeight(text)
|
||||
// 过滤标点符号和分隔符
|
||||
cleanFrequencies := FilterAndSortWords(frequencies)
|
||||
if save {
|
||||
go SaveAnalyzeSentimentWithFreqWeight(cleanFrequencies)
|
||||
go SaveStockSentimentAnalysis(result)
|
||||
}
|
||||
return result, cleanFrequencies
|
||||
}
|
||||
49
backend/data/stock_sentiment_analysis_test.go
Normal file
49
backend/data/stock_sentiment_analysis_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/6/19 13:05
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestAnalyzeSentiment(t *testing.T) {
|
||||
|
||||
db.Init("../../data/stock.db")
|
||||
InitAnalyzeSentiment()
|
||||
|
||||
messageText := strings.Builder{}
|
||||
news := NewMarketNewsApi().GetNewsList2("", random.RandInt(500, 1000))
|
||||
for _, telegraph := range *news {
|
||||
messageText.WriteString(telegraph.Content + "\n")
|
||||
}
|
||||
|
||||
text := messageText.String()
|
||||
//text = " 【周六你需要知道的隔夜全球要闻:美联储鸽声重振 美股走势回稳】 1、纽约联储行长威廉姆斯表示,随着劳动力市场走软,美联储近期内仍有再次降息的空间。 2、美联储理事斯蒂芬·米兰表示,自上次联邦公开市场委员会(FOMC)会议以来的经济数据应“促使人们偏向鸽派立场”。 3、波士顿联邦储备银行行长柯林斯表示,由于通胀可能在一段时间内保持高位,维持利率不变“目前合适”。 4、据CME“美联储观察”,截至北京时间11月22日6时30分,美联储12月降息25个基点的概率为69.4%,维持利率不变的概率为30.6%。 5、美国劳工统计局表示,11月CPI报告将于12月18日发布,同时取消了10月CPI报告发布,表示无法追溯采集政府停摆期间未能收集的部分数据。 6、俄罗斯总统普京表示,已收到美提出解决俄乌冲突的计划,俄罗斯愿意进行和平谈判。美国总统特朗普表示,他认为27日是乌克兰接受美国支持的和平计划的最后期限。 7、美联储高官鸽派言论提振市场情绪,美股三大指数收盘集体上涨,道琼斯指数涨1.08%,标普500指数涨0.98%,纳斯达克综合指数涨0.88%。甲骨文跌超5%,英伟达跌超1%。纳指本周累计跌2.74%,标普500指数累跌1.95%,道指累跌1.91%。英伟达本周累跌5.9%。 8、热门中概股多数上涨,纳斯达克中国金龙指数收涨1.23%。蔚来涨超3%,哔哩哔哩、理想汽车涨超2%,京东、小鹏汽车涨超1%。 9、国际油价下跌,交易员评估乌克兰与俄罗斯可能达成和平协议的前景。WTI 1月期货下跌1.6%,结算价报每桶58.06美元,为过去五个交易日中第四次下跌。布伦特1月期货下跌1.3%,结算价报每桶62.56美元。 10、美联储延长压力测试改进方案征询期,为银行反馈提供更多时间。 11、由于美国人对个人财务状况的看法恶化,美国消费者信心在11月跌至接近纪录最低水平;密歇根大学数据显示,11月消费者信心指数降至51,10月为53.6。 12、日本央行政策委员会委员Kazuyuki Masu表示,日本央行接近作出加息决定。 13、穆迪将意大利信用评级从BAA3上调至BAA2,展望稳定。\n"
|
||||
//text = "财联社电:英伟达周五冲高回落,股价涨幅收于1%,市场普遍认为其走势疲软"
|
||||
//text = "【本轮巴以冲突已致加沙地带69733人死亡】财联社11月22日电,当地时间22日下午,以军对加沙城西部一辆汽车发动空袭,已造成5人死亡,多人受伤。自2023年10月7日巴以新一轮大规模冲突爆发以来,以色列对加沙地带的袭击已造成69733人死亡、170863人受伤。"
|
||||
//text = "【牛肉加工亏损 美国泰森公司关停缩减相关业务】财联社11月22日电,受牛肉加工业务亏损影响,当地时间21日,美国泰森食品公司发布公告称,将关闭位于内布拉斯加州的一家大型牛肉加工厂,还计划缩小得克萨斯州一家牛肉加工厂的生产规模。根据泰森食品公司的公告,被关闭的这家工厂位于内布拉斯加州列克星敦,日均可宰杀并处理大约5000头牛,约占全美日均牛肉屠宰数量的4.8%。与此同时,公司还计划缩小得克萨斯州一家牛肉加工厂的生产规模,这家工厂每天大约可屠宰6000头牛。据悉,泰森此次业务调整影响两个工厂大约5000个工作岗位。《华尔街日报》报道称,泰森是美国四大肉类加工公司中首家关闭主要牛肉加工厂的公司,其最新财报显示,2025财年牛肉加工是唯一亏损的业务部门,调整后的营业亏损为4.26亿美元。"
|
||||
// 分析情感
|
||||
words := splitWords(text)
|
||||
fmt.Println(strings.Join(words, " "))
|
||||
result, frequencies := AnalyzeSentimentWithFreqWeight(text)
|
||||
// 过滤标点符号和分隔符
|
||||
cleanFrequencies := FilterPunctuationAndSeparators(frequencies)
|
||||
// 输出结果
|
||||
logger.SugaredLogger.Infof("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n 词频统计结果: %v",
|
||||
result.Description,
|
||||
result.Score,
|
||||
result.PositiveCount,
|
||||
result.NegativeCount,
|
||||
cleanFrequencies,
|
||||
)
|
||||
|
||||
}
|
||||
@@ -17,10 +17,10 @@ import (
|
||||
|
||||
type TushareApi struct {
|
||||
client *resty.Client
|
||||
config *Settings
|
||||
config *SettingConfig
|
||||
}
|
||||
|
||||
func NewTushareApi(config *Settings) *TushareApi {
|
||||
func NewTushareApi(config *SettingConfig) *TushareApi {
|
||||
return &TushareApi{
|
||||
client: resty.New(),
|
||||
config: config,
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
// -----------------------------------------------------------------------------------
|
||||
func TestGetDaily(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
tushareApi := NewTushareApi(GetConfig())
|
||||
tushareApi := NewTushareApi(GetSettingConfig())
|
||||
res := tushareApi.GetDaily("00927.HK", "20250101", "20250217", 30)
|
||||
t.Log(res)
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestGetDaily(t *testing.T) {
|
||||
|
||||
func TestGetUSDaily(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
tushareApi := NewTushareApi(GetConfig())
|
||||
tushareApi := NewTushareApi(GetSettingConfig())
|
||||
|
||||
res := tushareApi.GetDaily("gb_AAPL", "20250101", "20250217", 30)
|
||||
t.Log(res)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/plugin/dbresolver"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Dao *gorm.DB
|
||||
@@ -26,7 +27,7 @@ func Init(sqlitePath string) {
|
||||
var openDb *gorm.DB
|
||||
var err error
|
||||
if sqlitePath == "" {
|
||||
sqlitePath = "data/stock.db?cache=shared&mode=rwc&_journal_mode=WAL"
|
||||
sqlitePath = "data/stock.db?cache_size=-524288&journal_mode=WAL"
|
||||
}
|
||||
openDb, err = gorm.Open(sqlite.Open(sqlitePath), &gorm.Config{
|
||||
Logger: dbLogger,
|
||||
@@ -48,8 +49,8 @@ func Init(sqlitePath string) {
|
||||
if err != nil {
|
||||
log.Fatalf("openDb.DB error is %s", err.Error())
|
||||
}
|
||||
dbCon.SetMaxIdleConns(10)
|
||||
dbCon.SetMaxOpenConns(100)
|
||||
dbCon.SetMaxIdleConns(4)
|
||||
dbCon.SetMaxOpenConns(10)
|
||||
dbCon.SetConnMaxLifetime(time.Hour)
|
||||
Dao = openDb
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/plugin/soft_delete"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
@@ -148,15 +149,45 @@ func (receiver AIResponseResult) TableName() string {
|
||||
return "ai_response_result"
|
||||
}
|
||||
|
||||
// AIResponseResultQuery 分页查询参数
|
||||
type AIResponseResultQuery struct {
|
||||
Page int `form:"page" json:"page"` // 页码
|
||||
PageSize int `form:"pageSize" json:"pageSize"` // 每页大小
|
||||
ChatId string `form:"chatId" json:"chatId"` // 聊天ID筛选
|
||||
ModelName string `form:"modelName" json:"modelName"` // 模型名称筛选
|
||||
StockCode string `form:"stockCode" json:"stockCode"` // 股票代码筛选
|
||||
StockName string `form:"stockName" json:"stockName"` // 股票名称筛选
|
||||
Question string `form:"question" json:"question"` // 问题内容模糊搜索
|
||||
StartDate string `form:"startDate" json:"startDate"` // 开始日期
|
||||
EndDate string `form:"endDate" json:"endDate"` // 结束日期
|
||||
}
|
||||
|
||||
// AIResponseResultPageResp 分页查询响应
|
||||
type AIResponseResultPageResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data AIResponseResultPageData `json:"data"`
|
||||
}
|
||||
|
||||
type AIResponseResultPageData struct {
|
||||
List []AIResponseResult `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
TotalPages int `json:"totalPages"`
|
||||
}
|
||||
|
||||
type VersionInfo struct {
|
||||
gorm.Model
|
||||
Version string `json:"version"`
|
||||
Content string `json:"content"`
|
||||
Icon string `json:"icon"`
|
||||
Alipay string `json:"alipay"`
|
||||
Wxpay string `json:"wxpay"`
|
||||
BuildTimeStamp int64 `json:"buildTimeStamp"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
Version string `json:"version"`
|
||||
Content string `json:"content"`
|
||||
Icon string `json:"icon"`
|
||||
Alipay string `json:"alipay"`
|
||||
Wxpay string `json:"wxpay"`
|
||||
Wxgzh string `json:"wxgzh"`
|
||||
BuildTimeStamp int64 `json:"buildTimeStamp"`
|
||||
OfficialStatement string `json:"officialStatement"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
}
|
||||
|
||||
func (receiver VersionInfo) TableName() string {
|
||||
@@ -170,6 +201,8 @@ type StockInfoHK struct {
|
||||
FullName string `json:"fullName"`
|
||||
EName string `json:"eName"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
BKName string `json:"bk_name"`
|
||||
BKCode string `json:"bk_code"`
|
||||
}
|
||||
|
||||
func (receiver StockInfoHK) TableName() string {
|
||||
@@ -185,6 +218,8 @@ type StockInfoUS struct {
|
||||
Exchange string `json:"exchange"`
|
||||
Type string `json:"type"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
BKName string `json:"bk_name"`
|
||||
BKCode string `json:"bk_code"`
|
||||
}
|
||||
|
||||
func (receiver StockInfoUS) TableName() string {
|
||||
@@ -194,6 +229,12 @@ func (receiver StockInfoUS) TableName() string {
|
||||
type Resp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Param string `json:"param"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type PromptTemplate struct {
|
||||
@@ -218,14 +259,17 @@ type Prompt struct {
|
||||
|
||||
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"`
|
||||
Time string `json:"time"`
|
||||
DataTime *time.Time `json:"dataTime" gorm:"index"`
|
||||
Title string `json:"title" gorm:"index"`
|
||||
Content string `json:"content" gorm:"index"`
|
||||
SubjectTags []string `json:"subjects" gorm:"-:all"`
|
||||
StocksTags []string `json:"stocks" gorm:"-:all"`
|
||||
IsRed bool `json:"isRed" gorm:"index"`
|
||||
Url string `json:"url"`
|
||||
Source string `json:"source" gorm:"index"`
|
||||
TelegraphTags []TelegraphTags `json:"tags" gorm:"-:migration;foreignKey:TelegraphId"`
|
||||
SentimentResult string `json:"sentimentResult" gorm:"index"`
|
||||
}
|
||||
type TelegraphTags struct {
|
||||
gorm.Model
|
||||
@@ -250,3 +294,661 @@ func (p Tags) TableName() string {
|
||||
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 TVNewsDetail struct {
|
||||
ShortDescription string `json:"shortDescription"`
|
||||
Tags []struct {
|
||||
Title string `json:"title"`
|
||||
Args []struct {
|
||||
Id string `json:"id"`
|
||||
Value string `json:"value"`
|
||||
} `json:"args"`
|
||||
} `json:"tags"`
|
||||
Copyright string `json:"copyright"`
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Published int `json:"published"`
|
||||
Urgency int `json:"urgency"`
|
||||
StoryPath string `json:"storyPath"`
|
||||
}
|
||||
|
||||
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" md:"-"`
|
||||
Code string `json:"code" md:"股票代码"`
|
||||
Name string `json:"name" md:"股票名称"`
|
||||
Value float64 `json:"value" md:"热度"`
|
||||
Increment int `json:"increment" md:"热度变化"`
|
||||
RankChange int `json:"rank_change" md:"排名变化"`
|
||||
HasExist interface{} `json:"has_exist" md:"-"`
|
||||
Symbol string `json:"symbol" md:"-"`
|
||||
Percent float64 `json:"percent" md:"涨跌幅(%)"`
|
||||
Current float64 `json:"current" md:"股价"`
|
||||
Chg float64 `json:"chg" md:"股价变化"`
|
||||
Exchange string `json:"exchange" md:"交易所代码"`
|
||||
StockType int `json:"stock_type" md:"-"`
|
||||
SubType string `json:"sub_type" md:"-"`
|
||||
Ad int `json:"ad" md:"-"`
|
||||
AdId interface{} `json:"ad_id" md:"-"`
|
||||
ContentId interface{} `json:"content_id" md:"-"`
|
||||
Page interface{} `json:"page" md:"-"`
|
||||
Model interface{} `json:"model" md:"-"`
|
||||
Location interface{} `json:"location" md:"-"`
|
||||
TradeSession interface{} `json:"trade_session" md:"-"`
|
||||
CurrentExt interface{} `json:"current_ext" md:"-"`
|
||||
PercentExt interface{} `json:"percent_ext" md:"-"`
|
||||
}
|
||||
|
||||
type HotEvent struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
type GDP struct {
|
||||
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
|
||||
TIME string `json:"TIME" md:"报告期"`
|
||||
DOMESTICLPRODUCTBASE float64 `json:"DOMESTICL_PRODUCT_BASE" md:"国内生产总值(亿元)"`
|
||||
SUMSAME float64 `json:"SUM_SAME" md:"国内生产总值同比增长(%)"`
|
||||
FIRSTPRODUCTBASE float64 `json:"FIRST_PRODUCT_BASE" md:"第一产业(亿元)"`
|
||||
FIRSTSAME int `json:"FIRST_SAME" md:"第一产业同比增长(%)"`
|
||||
SECONDPRODUCTBASE float64 `json:"SECOND_PRODUCT_BASE" md:"第二产业(亿元)"`
|
||||
SECONDSAME float64 `json:"SECOND_SAME" md:"第二产业同比增长(%)"`
|
||||
THIRDPRODUCTBASE float64 `json:"THIRD_PRODUCT_BASE" md:"第三产业(亿元)"`
|
||||
THIRDSAME float64 `json:"THIRD_SAME" md:"第三产业同比增长(%)"`
|
||||
}
|
||||
type CPI struct {
|
||||
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
|
||||
TIME string `json:"TIME" md:"报告期"`
|
||||
NATIONALBASE float64 `json:"NATIONAL_BASE" md:"全国当月"`
|
||||
NATIONALSAME float64 `json:"NATIONAL_SAME" md:"全国当月同比增长(%)"`
|
||||
NATIONALSEQUENTIAL float64 `json:"NATIONAL_SEQUENTIAL" md:"全国当月环比增长(%)"`
|
||||
NATIONALACCUMULATE float64 `json:"NATIONAL_ACCUMULATE" md:"全国当月累计"`
|
||||
CITYBASE float64 `json:"CITY_BASE" md:"城市当月"`
|
||||
CITYSAME float64 `json:"CITY_SAME" md:"城市当月同比增长(%)"`
|
||||
CITYSEQUENTIAL float64 `json:"CITY_SEQUENTIAL" md:"城市当月环比增长(%)"`
|
||||
CITYACCUMULATE int `json:"CITY_ACCUMULATE" md:"城市当月累计"`
|
||||
RURALBASE float64 `json:"RURAL_BASE" md:"农村当月"`
|
||||
RURALSAME float64 `json:"RURAL_SAME" md:"农村当月同比增长(%)"`
|
||||
RURALSEQUENTIAL int `json:"RURAL_SEQUENTIAL" md:"农村当月环比增长(%)"`
|
||||
RURALACCUMULATE float64 `json:"RURAL_ACCUMULATE" md:"农村当月累计"`
|
||||
}
|
||||
type PPI struct {
|
||||
REPORTDATE string `json:"REPORT_DATE" md:"报告时间"`
|
||||
TIME string `json:"TIME" md:"报告期"`
|
||||
BASE float64 `json:"BASE" md:"当月"`
|
||||
BASESAME float64 `json:"BASE_SAME" md:"当月同比增长(%)"`
|
||||
BASEACCUMULATE float64 `json:"BASE_ACCUMULATE" md:"累计"`
|
||||
}
|
||||
type PMI struct {
|
||||
REPORTDATE string `md:"报告时间" json:"REPORT_DATE"`
|
||||
TIME string `md:"报告期" json:"TIME"`
|
||||
MAKEINDEX float64 `md:"制造业指数" json:"MAKE_INDEX"`
|
||||
MAKESAME float64 `md:"制造业指数同比增长(%)" json:"MAKE_SAME"`
|
||||
NMAKEINDEX float64 `md:"非制造业" json:"NMAKE_INDEX"`
|
||||
NMAKESAME float64 `md:"非制造业同比增长(%)" json:"NMAKE_SAME"`
|
||||
}
|
||||
|
||||
type DCResp struct {
|
||||
Version string `json:"version"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
type GDPResult struct {
|
||||
Pages int `json:"pages"`
|
||||
Data []GDP `json:"data"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
type CPIResult struct {
|
||||
Pages int `json:"pages"`
|
||||
Data []CPI `json:"data"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type PPIResult struct {
|
||||
Pages int `json:"pages"`
|
||||
Data []PPI `json:"data"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
type PMIResult struct {
|
||||
Pages int `json:"pages"`
|
||||
Data []PMI `json:"data"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
type GDPResp struct {
|
||||
DCResp
|
||||
GDPResult GDPResult `json:"result"`
|
||||
}
|
||||
|
||||
type CPIResp struct {
|
||||
DCResp
|
||||
CPIResult CPIResult `json:"result"`
|
||||
}
|
||||
|
||||
type PPIResp struct {
|
||||
DCResp
|
||||
PPIResult PPIResult `json:"result"`
|
||||
}
|
||||
type PMIResp struct {
|
||||
DCResp
|
||||
PMIResult PMIResult `json:"result"`
|
||||
}
|
||||
|
||||
type OldSettings struct {
|
||||
gorm.Model
|
||||
TushareToken string `json:"tushareToken"`
|
||||
LocalPushEnable bool `json:"localPushEnable"`
|
||||
DingPushEnable bool `json:"dingPushEnable"`
|
||||
DingRobot string `json:"dingRobot"`
|
||||
UpdateBasicInfoOnStart bool `json:"updateBasicInfoOnStart"`
|
||||
RefreshInterval int64 `json:"refreshInterval"`
|
||||
|
||||
OpenAiEnable bool `json:"openAiEnable"`
|
||||
OpenAiBaseUrl string `json:"openAiBaseUrl"`
|
||||
OpenAiApiKey string `json:"openAiApiKey"`
|
||||
OpenAiModelName string `json:"openAiModelName"`
|
||||
OpenAiMaxTokens int `json:"openAiMaxTokens"`
|
||||
OpenAiTemperature float64 `json:"openAiTemperature"`
|
||||
OpenAiApiTimeOut int `json:"openAiApiTimeOut"`
|
||||
Prompt string `json:"prompt"`
|
||||
CheckUpdate bool `json:"checkUpdate"`
|
||||
QuestionTemplate string `json:"questionTemplate"`
|
||||
CrawlTimeOut int64 `json:"crawlTimeOut"`
|
||||
KDays int64 `json:"kDays"`
|
||||
EnableDanmu bool `json:"enableDanmu"`
|
||||
BrowserPath string `json:"browserPath"`
|
||||
EnableNews bool `json:"enableNews"`
|
||||
DarkTheme bool `json:"darkTheme"`
|
||||
BrowserPoolSize int `json:"browserPoolSize"`
|
||||
EnableFund bool `json:"enableFund"`
|
||||
EnablePushNews bool `json:"enablePushNews"`
|
||||
SponsorCode string `json:"sponsorCode"`
|
||||
}
|
||||
|
||||
func (receiver OldSettings) TableName() string {
|
||||
return "settings"
|
||||
}
|
||||
|
||||
type ReutersNews struct {
|
||||
StatusCode int `json:"statusCode"`
|
||||
Message string `json:"message"`
|
||||
Result struct {
|
||||
ParentSectionName string `json:"parent_section_name"`
|
||||
Pagination struct {
|
||||
Size int `json:"size"`
|
||||
ExpectedSize int `json:"expected_size"`
|
||||
TotalSize int `json:"total_size"`
|
||||
Orderby string `json:"orderby"`
|
||||
} `json:"pagination"`
|
||||
DateModified time.Time `json:"date_modified"`
|
||||
FetchType string `json:"fetch_type"`
|
||||
Articles []struct {
|
||||
Id string `json:"id"`
|
||||
CanonicalUrl string `json:"canonical_url"`
|
||||
Website string `json:"website"`
|
||||
Web string `json:"web"`
|
||||
Native string `json:"native"`
|
||||
UpdatedTime time.Time `json:"updated_time"`
|
||||
PublishedTime time.Time `json:"published_time"`
|
||||
ArticleType string `json:"article_type"`
|
||||
DisplayMyNews bool `json:"display_my_news"`
|
||||
DisplayNewsletterSignup bool `json:"display_newsletter_signup"`
|
||||
DisplayNotifications bool `json:"display_notifications"`
|
||||
DisplayRelatedMedia bool `json:"display_related_media"`
|
||||
DisplayRelatedOrganizations bool `json:"display_related_organizations"`
|
||||
ContentCode string `json:"content_code"`
|
||||
Source struct {
|
||||
Name string `json:"name"`
|
||||
OriginalName string `json:"original_name"`
|
||||
} `json:"source"`
|
||||
Title string `json:"title"`
|
||||
BasicHeadline string `json:"basic_headline"`
|
||||
Distributor string `json:"distributor"`
|
||||
Description string `json:"description"`
|
||||
PrimaryMediaType string `json:"primary_media_type,omitempty"`
|
||||
PrimaryTag struct {
|
||||
ShortBio string `json:"short_bio"`
|
||||
Description string `json:"description"`
|
||||
Slug string `json:"slug"`
|
||||
Text string `json:"text"`
|
||||
TopicUrl string `json:"topic_url"`
|
||||
CanFollow bool `json:"can_follow,omitempty"`
|
||||
IsTopic bool `json:"is_topic,omitempty"`
|
||||
} `json:"primary_tag"`
|
||||
WordCount int `json:"word_count"`
|
||||
ReadMinutes int `json:"read_minutes"`
|
||||
Kicker struct {
|
||||
Path string `json:"path"`
|
||||
Names []string `json:"names"`
|
||||
Name string `json:"name,omitempty"`
|
||||
} `json:"kicker"`
|
||||
AdTopics []string `json:"ad_topics"`
|
||||
Thumbnail struct {
|
||||
Url string `json:"url"`
|
||||
Caption string `json:"caption,omitempty"`
|
||||
Type string `json:"type"`
|
||||
ResizerUrl string `json:"resizer_url"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Id string `json:"id"`
|
||||
Authors string `json:"authors,omitempty"`
|
||||
AltText string `json:"alt_text"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Company string `json:"company,omitempty"`
|
||||
PurchaseLicensingPath string `json:"purchase_licensing_path,omitempty"`
|
||||
} `json:"thumbnail"`
|
||||
Authors []struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
Company string `json:"company"`
|
||||
Thumbnail struct {
|
||||
Url string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
ResizerUrl string `json:"resizer_url"`
|
||||
} `json:"thumbnail"`
|
||||
SocialLinks []struct {
|
||||
Site string `json:"site"`
|
||||
Url string `json:"url"`
|
||||
} `json:"social_links,omitempty"`
|
||||
Byline string `json:"byline"`
|
||||
Description string `json:"description,omitempty"`
|
||||
TopicUrl string `json:"topic_url,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
} `json:"authors"`
|
||||
DisplayTime time.Time `json:"display_time"`
|
||||
ThumbnailDark struct {
|
||||
Url string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
ResizerUrl string `json:"resizer_url"`
|
||||
Id string `json:"id"`
|
||||
AltText string `json:"alt_text"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
} `json:"thumbnail_dark,omitempty"`
|
||||
} `json:"articles"`
|
||||
Section struct {
|
||||
Id string `json:"id"`
|
||||
AdUnitCode string `json:"ad_unit_code"`
|
||||
Website string `json:"website"`
|
||||
Name string `json:"name"`
|
||||
PageTitle string `json:"page_title"`
|
||||
CanFollow bool `json:"can_follow"`
|
||||
Language string `json:"language"`
|
||||
Type string `json:"type"`
|
||||
Advertising struct {
|
||||
Sponsored string `json:"sponsored"`
|
||||
} `json:"advertising"`
|
||||
VideoPlaylistId string `json:"video_playlistId"`
|
||||
MobileAdUnitPath string `json:"mobile_ad_unit_path"`
|
||||
AdUnitPath string `json:"ad_unit_path"`
|
||||
CollectionAlias string `json:"collection_alias"`
|
||||
SectionAbout string `json:"section_about"`
|
||||
Title string `json:"title"`
|
||||
Personalization struct {
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
ShowTags bool `json:"show_tags"`
|
||||
CanFollow bool `json:"can_follow"`
|
||||
} `json:"personalization"`
|
||||
} `json:"section"`
|
||||
AdUnitPath string `json:"ad_unit_path"`
|
||||
ResponseTime int64 `json:"response_time"`
|
||||
} `json:"result"`
|
||||
Id string `json:"_id"`
|
||||
}
|
||||
|
||||
type InteractiveAnswer struct {
|
||||
PageNo int `json:"pageNo"`
|
||||
PageSize int `json:"pageSize"`
|
||||
TotalRecord int `json:"totalRecord"`
|
||||
TotalPage int `json:"totalPage"`
|
||||
Results []InteractiveAnswerResults `json:"results"`
|
||||
Count bool `json:"count"`
|
||||
}
|
||||
|
||||
type InteractiveAnswerResults struct {
|
||||
EsId string `json:"esId" md:"-"`
|
||||
IndexId string `json:"indexId" md:"-"`
|
||||
ContentType int `json:"contentType" md:"-"`
|
||||
Trade []string `json:"trade" md:"行业名称"`
|
||||
MainContent string `json:"mainContent" md:"投资者提问"`
|
||||
StockCode string `json:"stockCode" md:"股票代码"`
|
||||
Secid string `json:"secid" md:"-"`
|
||||
CompanyShortName string `json:"companyShortName" md:"股票名称"`
|
||||
CompanyLogo string `json:"companyLogo,omitempty" md:"-"`
|
||||
BoardType []string `json:"boardType" md:"-"`
|
||||
PubDate string `json:"pubDate" md:"发布时间"`
|
||||
UpdateDate string `json:"updateDate" md:"-"`
|
||||
Author string `json:"author" md:"-"`
|
||||
AuthorName string `json:"authorName" md:"-"`
|
||||
PubClient string `json:"pubClient" md:"-"`
|
||||
AttachedId string `json:"attachedId" md:"-"`
|
||||
AttachedContent string `json:"attachedContent" md:"上市公司回复"`
|
||||
AttachedAuthor string `json:"attachedAuthor" md:"-"`
|
||||
AttachedPubDate string `json:"attachedPubDate" md:"回复时间"`
|
||||
Score float64 `json:"score" md:"-"`
|
||||
TopStatus int `json:"topStatus" md:"-"`
|
||||
PraiseCount int `json:"praiseCount" md:"-"`
|
||||
PraiseStatus bool `json:"praiseStatus" md:"-"`
|
||||
FavoriteStatus bool `json:"favoriteStatus" md:"-"`
|
||||
AttentionCompany bool `json:"attentionCompany" md:"-"`
|
||||
IsCheck string `json:"isCheck" md:"-"`
|
||||
QaStatus int `json:"qaStatus" md:"-"`
|
||||
PackageDate string `json:"packageDate" md:"-"`
|
||||
RemindStatus bool `json:"remindStatus" md:"-"`
|
||||
InterviewLive bool `json:"interviewLive" md:"-"`
|
||||
}
|
||||
|
||||
type CailianpressWeb struct {
|
||||
Total int `json:"total"`
|
||||
List []struct {
|
||||
Title string `json:"title" md:"资讯标题"`
|
||||
Ctime int `json:"ctime" md:"资讯时间"`
|
||||
Content string `json:"content" md:"资讯内容"`
|
||||
Author string `json:"author" md:"资讯发布者"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
type BKDict struct {
|
||||
gorm.Model `md:"-"`
|
||||
BkCode string `json:"bkCode" md:"行业/板块代码"`
|
||||
BkName string `json:"bkName" md:"行业/板块名称"`
|
||||
FirstLetter string `json:"firstLetter" md:"first_letter"`
|
||||
FubkCode string `json:"fubkCode" md:"fubk_code"`
|
||||
PublishCode string `json:"publishCode" md:"publish_code"`
|
||||
}
|
||||
|
||||
func (b BKDict) TableName() string {
|
||||
return "bk_dict"
|
||||
}
|
||||
|
||||
type WordAnalyze struct {
|
||||
gorm.Model
|
||||
DataTime *time.Time `json:"dataTime" gorm:"index;autoCreateTime"`
|
||||
WordFreqWithWeight
|
||||
}
|
||||
|
||||
// WordFreqWithWeight 词频统计结果,包含权重信息
|
||||
type WordFreqWithWeight struct {
|
||||
Word string
|
||||
Frequency int
|
||||
Weight float64
|
||||
Score float64
|
||||
}
|
||||
|
||||
// SentimentResult 情感分析结果类型
|
||||
type SentimentResult struct {
|
||||
Score float64 // 情感得分
|
||||
Category SentimentType // 情感类别
|
||||
PositiveCount int // 正面词数量
|
||||
NegativeCount int // 负面词数量
|
||||
Description string // 情感描述
|
||||
}
|
||||
|
||||
type SentimentResultAnalyze struct {
|
||||
gorm.Model
|
||||
DataTime *time.Time `json:"dataTime" gorm:"index;autoCreateTime"`
|
||||
SentimentResult
|
||||
}
|
||||
|
||||
// SentimentType 情感类型枚举
|
||||
type SentimentType int
|
||||
|
||||
type HotStrategy struct {
|
||||
ChgEffect bool `json:"chgEffect"`
|
||||
Code int `json:"code"`
|
||||
Data []*HotStrategyData `json:"data"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type HotStrategyData struct {
|
||||
Chg float64 `json:"chg" md:"平均涨幅(%)"`
|
||||
Code string `json:"code" md:"-"`
|
||||
HeatValue int `json:"heatValue" md:"热度值"`
|
||||
Market string `json:"market" md:"-"`
|
||||
Question string `json:"question" md:"选股策略"`
|
||||
Rank int `json:"rank" md:"-"`
|
||||
}
|
||||
|
||||
type NtfyNews struct {
|
||||
Id string `json:"id"`
|
||||
Time int `json:"time"`
|
||||
Expires int `json:"expires"`
|
||||
Event string `json:"event"`
|
||||
Topic string `json:"topic"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Tags []string `json:"tags"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
type THSHotStrategy struct {
|
||||
Result struct {
|
||||
Num int `json:"num"`
|
||||
List []struct {
|
||||
Author struct {
|
||||
Avatar string `json:"avatar"`
|
||||
UserName string `json:"userName"`
|
||||
UserId int `json:"userId"`
|
||||
} `json:"author"`
|
||||
Property struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Query string `json:"query"`
|
||||
Logic string `json:"logic"`
|
||||
BuyPosition interface{} `json:"buyPosition"`
|
||||
Ctime string `json:"ctime"`
|
||||
Tags []string `json:"tags"`
|
||||
WinRate string `json:"winRate"`
|
||||
AnnualYield string `json:"annualYield"`
|
||||
Type int `json:"type"`
|
||||
} `json:"property"`
|
||||
Interaction struct {
|
||||
CommentNum int `json:"commentNum"`
|
||||
CollectNum int `json:"collectNum"`
|
||||
IsCollected bool `json:"isCollected"`
|
||||
IsSubscribe int `json:"isSubscribe"`
|
||||
IsPublish int `json:"isPublish"`
|
||||
Pid int `json:"pid"`
|
||||
} `json:"interaction"`
|
||||
} `json:"list"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
type StockMoneyDataResp struct {
|
||||
Rc int `json:"rc"`
|
||||
Rt int `json:"rt"`
|
||||
Svr int `json:"svr"`
|
||||
Lt int `json:"lt"`
|
||||
Full int `json:"full"`
|
||||
Dlmkts string `json:"dlmkts"`
|
||||
Data StockMoneyData `json:"data"`
|
||||
}
|
||||
|
||||
type StockMoneyData struct {
|
||||
Total int `json:"total"`
|
||||
Diff []StockMoneyDataDiff `json:"diff"`
|
||||
}
|
||||
|
||||
type StockMoneyDataDiff struct {
|
||||
F1 int `json:"f1" md:"-"`
|
||||
F12 string `json:"f12" md:"股票代码"`
|
||||
F13 int `json:"f13" md:"-"`
|
||||
F14 string `json:"f14" md:"股票名称"`
|
||||
F2 float64 `json:"f2" md:"最新价"`
|
||||
F3 float64 `json:"f3" md:"今日涨跌幅(%)"`
|
||||
F62 float64 `json:"f62" md:"今日主力净额(元)"`
|
||||
F184 float64 `json:"f184" md:"今日主力净占比(%)"`
|
||||
F66 float64 `json:"f66" md:"今日超大单净额(元)"`
|
||||
F69 float64 `json:"f69" md:"今日超大单净占比(%)"`
|
||||
F72 float64 `json:"f72" md:"今日大单净额(元)"`
|
||||
F75 float64 `json:"f75" md:"今日大单净占比(%)"`
|
||||
F78 float64 `json:"f78" md:"今日中单净额(元)"`
|
||||
F81 float64 `json:"f81" md:"今日中单净占比(%)"`
|
||||
F84 float64 `json:"f84" md:"今日小单净额(元)"`
|
||||
F87 float64 `json:"f87" md:"今日小单净占比(%)"`
|
||||
F124 int `json:"f124" md:"f124"`
|
||||
F100 string `json:"f100" md:"所属板块"`
|
||||
F265 string `json:"f265" md:"板块代码"`
|
||||
}
|
||||
|
||||
type StockConceptInfoResp struct {
|
||||
Version string `json:"version"`
|
||||
Result StockConceptInfoResult `json:"result"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
type StockConceptInfoResult struct {
|
||||
Pages int `json:"pages"`
|
||||
Data []StockConceptInfo `json:"data"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type StockConceptInfo struct {
|
||||
SECUCODE string `json:"SECUCODE" md:"完整股票代码"`
|
||||
SECURITYCODE string `json:"SECURITY_CODE" md:"股票代码"`
|
||||
SECURITYNAMEABBR string `json:"SECURITY_NAME_ABBR" md:"股票名称"`
|
||||
NEWBOARDCODE string `json:"NEW_BOARD_CODE" md:"板块/概念代码"`
|
||||
BOARDNAME string `json:"BOARD_NAME" md:"板块/概念名称"`
|
||||
SELECTEDBOARDREASON string `json:"SELECTED_BOARD_REASON" md:"板块/概念描述"`
|
||||
ISPRECISE string `json:"IS_PRECISE" md:"-"`
|
||||
BOARDRANK int `json:"BOARD_RANK" md:"-"`
|
||||
BOARDYIELD float64 `json:"BOARD_YIELD" md:"板块/概念涨跌幅(%)"`
|
||||
DERIVEBOARDCODE string `json:"DERIVE_BOARD_CODE" md:"-"`
|
||||
}
|
||||
|
||||
type AiRecommendStocks struct {
|
||||
gorm.Model
|
||||
DataTime *time.Time `json:"dataTime" gorm:"index;autoCreateTime"`
|
||||
ModelName string `json:"modelName" md:"模型名称"`
|
||||
StockCode string `json:"stockCode" md:"股票代码"`
|
||||
StockName string `json:"stockName" md:"股票名称"`
|
||||
BkCode string `json:"bkCode" md:"行业/板块代码"`
|
||||
BkName string `json:"bkName" md:"行业/板块名称"`
|
||||
StockPrice string `json:"stockPrice" md:"推荐时股票价格"`
|
||||
StockCurrentPrice string `json:"stockCurrentPrice" md:"当前价格"`
|
||||
StockCurrentPriceTime string `json:"stockCurrentPriceTime" md:"当前价格时间"`
|
||||
StockClosePrice string `json:"stockClosePrice" md:"推荐时股票收盘价格"`
|
||||
StockPrePrice string `json:"stockPrePrice" md:"前一交易日股票价格"`
|
||||
RecommendReason string `json:"recommendReason" md:"推荐理由/驱动因素/逻辑"`
|
||||
RecommendBuyPrice string `json:"recommendBuyPrice" md:"ai建议买入价"`
|
||||
RecommendStopProfitPrice string `json:"recommendStopProfitPrice" md:"ai建议止盈价"`
|
||||
RecommendStopLossPrice string `json:"recommendStopLossPrice" md:"ai建议止损价"`
|
||||
RiskRemarks string `json:"riskRemarks" md:"风险提示"`
|
||||
Remarks string `json:"remarks" md:"备注"`
|
||||
}
|
||||
|
||||
func (receiver AiRecommendStocks) TableName() string { return "ai_recommend_stocks" }
|
||||
|
||||
type AiRecommendStocksQuery struct {
|
||||
Page int `form:"page" json:"page"` // 页码
|
||||
PageSize int `form:"pageSize" json:"pageSize"` // 每页大小
|
||||
StockCode string `form:"stockCode" json:"stockCode"` // 股票代码筛选
|
||||
StockName string `form:"stockName" json:"stockName"` // 股票名称筛选
|
||||
BkCode string `form:"bkCode" json:"bkCode"` // 板块代码筛选
|
||||
BkName string `form:"bkName" json:"bkName"` // 板块名称筛选
|
||||
StartDate string `form:"startDate" json:"startDate"` // 开始日期
|
||||
EndDate string `form:"endDate" json:"endDate"` // 结束日期
|
||||
}
|
||||
|
||||
type AiRecommendStocksPageResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data AiRecommendStocksPageData `json:"data"`
|
||||
}
|
||||
|
||||
type AiRecommendStocksPageData struct {
|
||||
List []AiRecommendStocks `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
TotalPages int `json:"totalPages"`
|
||||
}
|
||||
|
||||
221
backend/util/html_to_markdown.go
Normal file
221
backend/util/html_to_markdown.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package util
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/7/15 14:08
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"golang.org/x/net/html"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HTMLNode 表示HTML文档中的一个节点
|
||||
type HTMLNode struct {
|
||||
Type html.NodeType
|
||||
Data string
|
||||
Attr []html.Attribute
|
||||
Children []*HTMLNode
|
||||
}
|
||||
|
||||
// HTMLToMarkdown 将HTML转换为Markdown
|
||||
func HTMLToMarkdown(htmlContent string) (string, error) {
|
||||
doc, err := html.Parse(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
root := parseHTMLNode(doc)
|
||||
var buf bytes.Buffer
|
||||
convertNode(&buf, root, 0)
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// parseHTMLNode 递归解析HTML节点
|
||||
func parseHTMLNode(n *html.Node) *HTMLNode {
|
||||
node := &HTMLNode{
|
||||
Type: n.Type,
|
||||
Data: n.Data,
|
||||
Attr: n.Attr,
|
||||
}
|
||||
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
node.Children = append(node.Children, parseHTMLNode(c))
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
// convertNode 递归转换节点为Markdown
|
||||
func convertNode(buf *bytes.Buffer, node *HTMLNode, depth int) {
|
||||
switch node.Type {
|
||||
case html.ElementNode:
|
||||
convertElementNode(buf, node, depth)
|
||||
case html.TextNode:
|
||||
// 处理文本节点,去除多余的空白
|
||||
text := strings.TrimSpace(node.Data)
|
||||
if text != "" {
|
||||
buf.WriteString(text)
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理子节点
|
||||
for _, child := range node.Children {
|
||||
convertNode(buf, child, depth+1)
|
||||
}
|
||||
|
||||
// 处理需要在结束标签后添加内容的元素
|
||||
switch node.Data {
|
||||
case "p", "h1", "h2", "h3", "h4", "h5", "h6", "li":
|
||||
buf.WriteString("\n\n")
|
||||
case "blockquote":
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// convertElementNode 转换元素节点为Markdown
|
||||
func convertElementNode(buf *bytes.Buffer, node *HTMLNode, depth int) {
|
||||
switch node.Data {
|
||||
case "h1":
|
||||
buf.WriteString("# ")
|
||||
case "h2":
|
||||
buf.WriteString("## ")
|
||||
case "h3":
|
||||
buf.WriteString("### ")
|
||||
case "h4":
|
||||
buf.WriteString("#### ")
|
||||
case "h5":
|
||||
buf.WriteString("##### ")
|
||||
case "h6":
|
||||
buf.WriteString("###### ")
|
||||
case "p":
|
||||
// 段落标签不需要特殊标记,直接处理内容
|
||||
case "strong", "b":
|
||||
buf.WriteString("**")
|
||||
case "em", "i":
|
||||
buf.WriteString("*")
|
||||
case "u":
|
||||
buf.WriteString("<u>")
|
||||
case "s", "del":
|
||||
buf.WriteString("~~")
|
||||
case "a":
|
||||
//href := getAttrValue(node.Attr, "href")
|
||||
buf.WriteString("[")
|
||||
case "img":
|
||||
src := getAttrValue(node.Attr, "src")
|
||||
alt := getAttrValue(node.Attr, "alt")
|
||||
buf.WriteString(fmt.Sprintf("", alt, src))
|
||||
case "ul":
|
||||
// 无序列表不需要特殊标记,子项会处理
|
||||
case "ol":
|
||||
// 有序列表不需要特殊标记,子项会处理
|
||||
case "li":
|
||||
if isParentListType(node, "ul") {
|
||||
buf.WriteString("- ")
|
||||
} else {
|
||||
// 计算当前列表项的序号
|
||||
index := 1
|
||||
if parent := findParentList(node); parent != nil {
|
||||
for i, sibling := range parent.Children {
|
||||
if sibling == node {
|
||||
index = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("%d. ", index))
|
||||
}
|
||||
case "blockquote":
|
||||
buf.WriteString("> ")
|
||||
case "code":
|
||||
if isParentPre(node) {
|
||||
// 父节点是pre,使用代码块
|
||||
buf.WriteString("\n```\n")
|
||||
} else {
|
||||
// 行内代码
|
||||
buf.WriteString("`")
|
||||
}
|
||||
case "pre":
|
||||
// 前置代码块由子节点code处理
|
||||
case "br":
|
||||
buf.WriteString("\n")
|
||||
case "hr":
|
||||
buf.WriteString("\n---\n")
|
||||
}
|
||||
|
||||
// 处理闭合标签
|
||||
if needsClosingTag(node.Data) {
|
||||
defer func() {
|
||||
switch node.Data {
|
||||
case "strong", "b":
|
||||
buf.WriteString("**")
|
||||
case "em", "i":
|
||||
buf.WriteString("*")
|
||||
case "u":
|
||||
buf.WriteString("</u>")
|
||||
case "s", "del":
|
||||
buf.WriteString("~~")
|
||||
case "a":
|
||||
href := getAttrValue(node.Attr, "href")
|
||||
buf.WriteString(fmt.Sprintf("](%s)", href))
|
||||
case "code":
|
||||
if isParentPre(node) {
|
||||
buf.WriteString("\n```\n")
|
||||
} else {
|
||||
buf.WriteString("`")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// getAttrValue 获取属性值
|
||||
func getAttrValue(attrs []html.Attribute, key string) string {
|
||||
for _, attr := range attrs {
|
||||
if attr.Key == key {
|
||||
return attr.Val
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// isParentListType 检查父节点是否为指定类型的列表
|
||||
func isParentListType(node *HTMLNode, listType string) bool {
|
||||
parent := findParentList(node)
|
||||
return parent != nil && parent.Data == listType
|
||||
}
|
||||
|
||||
// findParentList 查找父列表节点
|
||||
func findParentList(node *HTMLNode) *HTMLNode {
|
||||
// 简化实现,实际应该递归查找父节点
|
||||
if node.Type == html.ElementNode && (node.Data == "ul" || node.Data == "ol") {
|
||||
return node
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isParentPre 检查父节点是否为pre
|
||||
func isParentPre(node *HTMLNode) bool {
|
||||
if len(node.Children) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, child := range node.Children {
|
||||
if child.Type == html.ElementNode && child.Data == "pre" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// needsClosingTag 判断元素是否需要闭合标签
|
||||
func needsClosingTag(tag string) bool {
|
||||
switch tag {
|
||||
case "img", "br", "hr", "input", "meta", "link":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
6
backend/util/html_to_markdown_test.go
Normal file
6
backend/util/html_to_markdown_test.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package util
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/7/15 14:08
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
286
backend/util/struct_to_markdown.go
Normal file
286
backend/util/struct_to_markdown.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MarkdownTable 生成结构体或结构体切片的Markdown表格表示
|
||||
func MarkdownTable(v interface{}) string {
|
||||
value := reflect.ValueOf(v)
|
||||
if value.Kind() == reflect.Ptr {
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
// 处理单个结构体
|
||||
if value.Kind() == reflect.Struct {
|
||||
return markdownSingleStruct(value)
|
||||
}
|
||||
|
||||
// 处理结构体切片/数组
|
||||
if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
|
||||
if value.Len() == 0 {
|
||||
return "切片/数组为空"
|
||||
}
|
||||
return markdownStructSlice(value)
|
||||
}
|
||||
|
||||
return "输入必须是结构体、结构体指针、结构体切片或数组"
|
||||
}
|
||||
|
||||
func MarkdownTableWithTitle(title string, v interface{}) string {
|
||||
value := reflect.ValueOf(v)
|
||||
if value.Kind() == reflect.Ptr {
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
// 处理单个结构体
|
||||
if value.Kind() == reflect.Struct {
|
||||
return markdownSingleStruct(value)
|
||||
}
|
||||
|
||||
// 处理结构体切片/数组
|
||||
if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
|
||||
if value.Len() == 0 {
|
||||
return "\n## " + title + "\n" + "无数据" + "\n"
|
||||
}
|
||||
return "\n## " + title + "\n" + markdownStructSlice(value) + "\n"
|
||||
}
|
||||
|
||||
return "\n## " + title + "\n" + "无数据" + "\n"
|
||||
}
|
||||
|
||||
// 处理单个结构体
|
||||
func markdownSingleStruct(value reflect.Value) string {
|
||||
t := value.Type()
|
||||
var b strings.Builder
|
||||
|
||||
// 表头
|
||||
b.WriteString("|")
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if shouldSkip(field) {
|
||||
continue
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" %s |", getFieldName(field)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
// 分隔线
|
||||
b.WriteString("|")
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if shouldSkip(field) {
|
||||
continue
|
||||
}
|
||||
b.WriteString(" --- |")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
// 数据行
|
||||
b.WriteString("|")
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if shouldSkip(field) {
|
||||
continue
|
||||
}
|
||||
fieldValue := value.Field(i)
|
||||
b.WriteString(fmt.Sprintf(" %s |", formatValue(fieldValue)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// 处理结构体切片/数组
|
||||
func markdownStructSlice(value reflect.Value) string {
|
||||
if value.Len() == 0 {
|
||||
return "切片/数组为空"
|
||||
}
|
||||
|
||||
firstElem := value.Index(0)
|
||||
if firstElem.Kind() == reflect.Ptr {
|
||||
firstElem = firstElem.Elem()
|
||||
}
|
||||
if firstElem.Kind() != reflect.Struct {
|
||||
return "切片/数组元素必须是结构体或结构体指针"
|
||||
}
|
||||
|
||||
t := firstElem.Type()
|
||||
var b strings.Builder
|
||||
|
||||
// 表头
|
||||
b.WriteString("|")
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if shouldSkip(field) {
|
||||
continue
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" %s |", getFieldName(field)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
// 分隔线
|
||||
b.WriteString("|")
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if shouldSkip(field) {
|
||||
continue
|
||||
}
|
||||
b.WriteString(" --- |")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
// 多行数据
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
elem := value.Index(i)
|
||||
if elem.Kind() == reflect.Ptr {
|
||||
elem = elem.Elem()
|
||||
}
|
||||
|
||||
b.WriteString("|")
|
||||
for j := 0; j < t.NumField(); j++ {
|
||||
field := t.Field(j)
|
||||
if shouldSkip(field) {
|
||||
continue
|
||||
}
|
||||
fieldValue := elem.Field(j)
|
||||
b.WriteString(fmt.Sprintf(" %s |", formatValue(fieldValue)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// 判断是否应该跳过该字段
|
||||
func shouldSkip(field reflect.StructField) bool {
|
||||
return field.Tag.Get("md") == "-"
|
||||
}
|
||||
|
||||
// 获取字段的Markdown表头名称
|
||||
func getFieldName(field reflect.StructField) string {
|
||||
name := field.Tag.Get("md")
|
||||
if name == "" || name == "-" {
|
||||
return field.Name
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// 格式化字段值为字符串
|
||||
func formatValue(value reflect.Value) string {
|
||||
if !value.IsValid() {
|
||||
return "n/a"
|
||||
}
|
||||
|
||||
// 处理指针
|
||||
if value.Kind() == reflect.Ptr {
|
||||
if value.IsNil() {
|
||||
return "nil"
|
||||
}
|
||||
return formatValue(value.Elem())
|
||||
}
|
||||
|
||||
// 处理结构体
|
||||
if value.Kind() == reflect.Struct {
|
||||
var fields []string
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
field := value.Type().Field(i)
|
||||
if shouldSkip(field) {
|
||||
continue
|
||||
}
|
||||
fieldValue := value.Field(i)
|
||||
fields = append(fields, fmt.Sprintf("%s: %s", getFieldName(field), formatValue(fieldValue)))
|
||||
}
|
||||
return "{" + strings.Join(fields, ", ") + "}"
|
||||
}
|
||||
|
||||
// 处理切片/数组
|
||||
if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
|
||||
var items []string
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
items = append(items, formatValue(value.Index(i)))
|
||||
}
|
||||
return "[" + strings.Join(items, ", ") + "]"
|
||||
}
|
||||
|
||||
// 处理映射
|
||||
if value.Kind() == reflect.Map {
|
||||
var items []string
|
||||
for _, key := range value.MapKeys() {
|
||||
keyStr := formatValue(key)
|
||||
valueStr := formatValue(value.MapIndex(key))
|
||||
items = append(items, fmt.Sprintf("%s: %s", keyStr, valueStr))
|
||||
}
|
||||
return "{" + strings.Join(items, ", ") + "}"
|
||||
}
|
||||
|
||||
// 基本类型
|
||||
return fmt.Sprintf("%s", strutil.RemoveNonPrintable(convertor.ToString(value.Interface())))
|
||||
}
|
||||
|
||||
// 示例结构体
|
||||
type Address struct {
|
||||
City string `md:"城市"`
|
||||
Country string `md:"国家"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `md:"姓名"`
|
||||
Age int `md:"年龄"`
|
||||
Email string `md:"邮箱"`
|
||||
Address Address `md:"地址"`
|
||||
Phones []string `md:"电话"`
|
||||
Active bool `md:"活跃状态"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 示例使用:单个结构体
|
||||
user := User{
|
||||
Name: "张三",
|
||||
Age: 30,
|
||||
Email: "zhangsan@example.com",
|
||||
Address: Address{
|
||||
City: "北京",
|
||||
Country: "中国",
|
||||
},
|
||||
Phones: []string{"13800138000", "13900139000"},
|
||||
Active: true,
|
||||
}
|
||||
|
||||
fmt.Println("单个结构体转换:")
|
||||
fmt.Println(MarkdownTable(user))
|
||||
fmt.Println()
|
||||
|
||||
// 示例使用:结构体切片
|
||||
users := []User{
|
||||
{
|
||||
Name: "张三",
|
||||
Age: 30,
|
||||
Email: "zhangsan@example.com",
|
||||
Address: Address{
|
||||
City: "北京",
|
||||
Country: "中国",
|
||||
},
|
||||
Phones: []string{"13800138000"},
|
||||
Active: true,
|
||||
},
|
||||
{
|
||||
Name: "李四",
|
||||
Age: 25,
|
||||
Email: "lisi@example.com",
|
||||
Address: Address{
|
||||
City: "上海",
|
||||
Country: "中国",
|
||||
},
|
||||
Phones: []string{"13900139000", "13700137000"},
|
||||
Active: false,
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Println("结构体切片转换:")
|
||||
fmt.Println(MarkdownTable(users))
|
||||
}
|
||||
54
backend/util/struct_to_markdown_test.go
Normal file
54
backend/util/struct_to_markdown_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMd(t *testing.T) {
|
||||
// 示例使用:单个结构体
|
||||
user := User{
|
||||
Name: "张三",
|
||||
Age: 30,
|
||||
Email: "zhangsan@example.com",
|
||||
Address: Address{
|
||||
City: "北京",
|
||||
Country: "中国",
|
||||
},
|
||||
Phones: []string{"13800138000", "13900139000"},
|
||||
Active: true,
|
||||
}
|
||||
|
||||
fmt.Println("单个结构体转换:")
|
||||
fmt.Println(MarkdownTable(user))
|
||||
fmt.Println()
|
||||
|
||||
// 示例使用:结构体切片
|
||||
users := []User{
|
||||
{
|
||||
Name: "张三",
|
||||
Age: 30,
|
||||
Email: "zhangsan@example.com",
|
||||
Address: Address{
|
||||
City: "北京",
|
||||
Country: "中国",
|
||||
},
|
||||
Phones: []string{"13800138000"},
|
||||
Active: true,
|
||||
},
|
||||
{
|
||||
Name: "李四",
|
||||
Age: 25,
|
||||
Email: "lisi@example.com",
|
||||
Address: Address{
|
||||
City: "上海",
|
||||
Country: "中国",
|
||||
},
|
||||
Phones: []string{"13900139000", "13700137000"},
|
||||
Active: false,
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Println("结构体切片转换:")
|
||||
fmt.Println(MarkdownTable(users))
|
||||
}
|
||||
BIN
build/screenshot/img15.png
Normal file
BIN
build/screenshot/img15.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 205 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
193
data/dict/user.txt
Normal file
193
data/dict/user.txt
Normal file
@@ -0,0 +1,193 @@
|
||||
# 补充:热点概念与板块(Jieba/gse兼容格式)
|
||||
# 权重说明:核心热点500-700分,事件类400分,负权重词汇按需求保留
|
||||
|
||||
# 一、负权重低优先级词汇(减少无差别匹配干扰)
|
||||
公司 -0.1 n
|
||||
国家 -0.1 n
|
||||
国际 -0.1 n
|
||||
会议 -0.1 n
|
||||
市场 -0.1 n
|
||||
经济 -0.1 n
|
||||
技术 -0.1 n
|
||||
记者 -0.1 n
|
||||
时间 -0.1 n
|
||||
项目 -0.1 n
|
||||
问题 -0.1 n
|
||||
企业 -0.1 n
|
||||
财联社 -0.1 n
|
||||
上涨 -0.1 v
|
||||
下跌 -0.1 v
|
||||
期货 -0.1 n
|
||||
跌幅 -0.1 n
|
||||
跌超 -0.1 adj
|
||||
股票 -0.1 n
|
||||
基金 -0.1 n
|
||||
电讯 -0.1 n
|
||||
建筑 -0.1 n
|
||||
平开 -0.1 n
|
||||
保险 -0.1 n
|
||||
行业 -0.1 n
|
||||
其他 -0.1 n
|
||||
a股 -0.1 n
|
||||
港股 -0.1 n
|
||||
etf -0.1 n
|
||||
涨幅 -0.1 n
|
||||
交易所 -0.1 n
|
||||
证券 -0.1 n
|
||||
ai -0.1 n
|
||||
# 二、核心热点概念(700分,最高优先级)
|
||||
端侧AI 700 n
|
||||
AI应用 700 n
|
||||
比特币 700 n
|
||||
摩尔线程 700 n
|
||||
摩尔线程概念 700 n
|
||||
AI算力 700 n
|
||||
生成式AI 700 n
|
||||
量子计算 700 n
|
||||
脑机接口 700 n
|
||||
6G通信 700 n
|
||||
人形机器人 700 n
|
||||
固态电池 700 n
|
||||
ChatGPT概念 700 n
|
||||
Web3.0 700 n
|
||||
元宇宙 700 n
|
||||
数字孪生 700 n
|
||||
量子通信 700 n
|
||||
|
||||
# 三、重点赛道板块(500分,高优先级)
|
||||
冰雪旅游 500 n
|
||||
特高压 500 n
|
||||
跨境电商 500 n
|
||||
新能源汽车 500 n
|
||||
机器人 500 n
|
||||
具身智能 500 n
|
||||
油气 500 n
|
||||
商业航天 500 n
|
||||
光伏储能 500 n
|
||||
锂电材料 500 n
|
||||
半导体设备 500 n
|
||||
集成电路 500 n
|
||||
创新药 500 n
|
||||
CXO 500 n
|
||||
医疗器械 500 n
|
||||
数字经济 500 n
|
||||
数字货币 500 n
|
||||
区块链 500 n
|
||||
低空经济 500 n
|
||||
工业互联网 500 n
|
||||
物联网 500 n
|
||||
5G应用 500 n
|
||||
充电桩 500 n
|
||||
氢能源 500 n
|
||||
核聚变 500 n
|
||||
工业母机 500 n
|
||||
新材料 500 n
|
||||
生物制造 500 n
|
||||
智能网联汽车 500 n
|
||||
乡村振兴 500 n
|
||||
国企改革 500 n
|
||||
央企重组 500 n
|
||||
跨境金融 500 n
|
||||
自贸港 500 n
|
||||
一带一路 500 n
|
||||
绿色低碳 500 n
|
||||
碳交易 500 n
|
||||
数据要素 500 n
|
||||
数字基建 500 n
|
||||
东数西算 500 n
|
||||
国产替代 500 n
|
||||
信创 500 n
|
||||
网络安全 500 n
|
||||
算力网络 500 n
|
||||
边缘计算 500 n
|
||||
虚拟现实 500 n
|
||||
增强现实 500 n
|
||||
智能穿戴 500 n
|
||||
智能家居 500 n
|
||||
车联网 500 n
|
||||
激光雷达 500 n
|
||||
氮化镓 500 n
|
||||
碳化硅 500 n
|
||||
第三代半导体 500 n
|
||||
EDA工具 500 n
|
||||
光刻胶 500 n
|
||||
芯片设计 500 n
|
||||
封装测试 500 n
|
||||
储能电池 500 n
|
||||
钠离子电池 500 n
|
||||
氢燃料电池 500 n
|
||||
光伏组件 500 n
|
||||
风电设备 500 n
|
||||
特高压设备 500 n
|
||||
电力物联网 500 n
|
||||
智能电网 500 n
|
||||
轨道交通 500 n
|
||||
航空航天 500 n
|
||||
海洋工程 500 n
|
||||
高端装备 500 n
|
||||
军工电子 500 n
|
||||
卫星互联网 500 n
|
||||
北斗导航 500 n
|
||||
国产大飞机 500 n
|
||||
生物医药 500 n
|
||||
基因测序 500 n
|
||||
疫苗 500 n
|
||||
医疗美容 500 n
|
||||
养老产业 500 n
|
||||
教育信息化 500 n
|
||||
体育产业 500 n
|
||||
文化创意 500 n
|
||||
旅游复苏 500 n
|
||||
预制菜 500 n
|
||||
白酒 500 n
|
||||
食品饮料 500 n
|
||||
家电下乡 500 n
|
||||
房地产复苏 500 n
|
||||
基建投资 500 n
|
||||
新型城镇化 500 n
|
||||
冷链物流 500 n
|
||||
快递物流 500 n
|
||||
跨境支付 500 n
|
||||
金融科技 500 n
|
||||
消费电子 500 n
|
||||
元宇宙基建 500 n
|
||||
数字藏品 500 n
|
||||
NFT 500 n
|
||||
绿色电力 500 n
|
||||
节能降碳 500 n
|
||||
抽水蓄能 500 n
|
||||
生物质能 500 n
|
||||
地热能 500 n
|
||||
潮汐能 500 n
|
||||
|
||||
# 四、事件驱动型概念(400分,中优先级)
|
||||
俄乌冲突 400 n
|
||||
中东局势 400 n
|
||||
美联储加息 400 n
|
||||
降息预期 400 n
|
||||
贸易摩擦 400 n
|
||||
供应链重构 400 n
|
||||
能源危机 400 n
|
||||
粮食安全 400 n
|
||||
疫情复苏 400 n
|
||||
政策利好 400 n
|
||||
产业扶持 400 n
|
||||
技术突破 400 n
|
||||
并购重组 400 n
|
||||
IPO提速 400 n
|
||||
解禁潮 400 n
|
||||
北向资金流入 400 n
|
||||
南向资金流入 400 n
|
||||
主力资金异动 400 n
|
||||
行业景气度 400 n
|
||||
业绩预增 400 n
|
||||
商誉减值 400 n
|
||||
退市风险 400 n
|
||||
监管新规 400 n
|
||||
税收优惠 400 n
|
||||
补贴政策 400 n
|
||||
基建刺激 400 n
|
||||
消费刺激 400 n
|
||||
新能源补贴 400 n
|
||||
碳达峰政策 400 n
|
||||
碳中和目标 400 n
|
||||
10
frontend/auto-imports.d.ts
vendored
Normal file
10
frontend/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
||||
48
frontend/components.d.ts
vendored
Normal file
48
frontend/components.d.ts
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
About: typeof import('./src/components/about.vue')['default']
|
||||
AgentChat: typeof import('./src/components/agent-chat.vue')['default']
|
||||
AgentChat_bk: typeof import('./src/components/agent-chat_bk.vue')['default']
|
||||
AiRecommendStocksList: typeof import('./src/components/aiRecommendStocksList.vue')['default']
|
||||
AnalyzeMartket: typeof import('./src/components/AnalyzeMartket.vue')['default']
|
||||
ClsCalendarTimeLine: typeof import('./src/components/ClsCalendarTimeLine.vue')['default']
|
||||
EmbeddedUrl: typeof import('./src/components/EmbeddedUrl.vue')['default']
|
||||
Fund: typeof import('./src/components/fund.vue')['default']
|
||||
HotEvents: typeof import('./src/components/HotEvents.vue')['default']
|
||||
HotStockList: typeof import('./src/components/HotStockList.vue')['default']
|
||||
HotTopics: typeof import('./src/components/HotTopics.vue')['default']
|
||||
IndustryMoneyRank: typeof import('./src/components/industryMoneyRank.vue')['default']
|
||||
IndustryResearchReportList: typeof import('./src/components/IndustryResearchReportList.vue')['default']
|
||||
InvestCalendarTimeLine: typeof import('./src/components/InvestCalendarTimeLine.vue')['default']
|
||||
KLineChart: typeof import('./src/components/KLineChart.vue')['default']
|
||||
LongTigerRankList: typeof import('./src/components/LongTigerRankList.vue')['default']
|
||||
Market: typeof import('./src/components/market.vue')['default']
|
||||
MoneyTrend: typeof import('./src/components/moneyTrend.vue')['default']
|
||||
NewsList: typeof import('./src/components/newsList.vue')['default']
|
||||
RankTable: typeof import('./src/components/rankTable.vue')['default']
|
||||
ResearchIndex: typeof import('./src/components/researchIndex.vue')['default']
|
||||
ResearchReport: typeof import('./src/components/researchReport.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SelectStock: typeof import('./src/components/SelectStock.vue')['default']
|
||||
Settings: typeof import('./src/components/settings.vue')['default']
|
||||
Stock: typeof import('./src/components/stock.vue')['default']
|
||||
Stockhotmap: typeof import('./src/components/stockhotmap.vue')['default']
|
||||
StockNoticeList: typeof import('./src/components/StockNoticeList.vue')['default']
|
||||
StockResearchReportList: typeof import('./src/components/StockResearchReportList.vue')['default']
|
||||
StockSparkLine: typeof import('./src/components/stockSparkLine.vue')['default']
|
||||
TChat: typeof import('@tdesign-vue-next/chat')['Chat']
|
||||
TChatAction: typeof import('@tdesign-vue-next/chat')['ChatAction']
|
||||
TChatContent: typeof import('@tdesign-vue-next/chat')['ChatContent']
|
||||
TChatLoading: typeof import('@tdesign-vue-next/chat')['ChatLoading']
|
||||
TChatSender: typeof import('@tdesign-vue-next/chat')['ChatSender']
|
||||
}
|
||||
}
|
||||
1911
frontend/package-lock.json
generated
1911
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,25 +9,39 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tdesign-vue-next/chat": "^0.4.5",
|
||||
"@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",
|
||||
"md-editor-v3": "^5.2.3",
|
||||
"vue": "^3.2.25",
|
||||
"tdesign-icons-vue-next": "^0.3.7",
|
||||
"vue": "^3.5.17",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue3-danmaku": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@tdesign-vue-next/auto-import-resolver": "^0.1.1",
|
||||
"@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": "^6.0.2",
|
||||
"html-docx-js-typescript": "^0.1.5",
|
||||
"naive-ui": "^2.41.0",
|
||||
"less": "^4.4.0",
|
||||
"naive-ui": "^2.43.2",
|
||||
"unplugin-auto-import": "^20.0.0",
|
||||
"unplugin-vue-components": "^29.0.0",
|
||||
"vfonts": "^0.0.3",
|
||||
"vite": "^5.4.12"
|
||||
"vite": "7.2.4"
|
||||
},
|
||||
"keywords": [
|
||||
"AI赋能股票分析",
|
||||
|
||||
@@ -1 +1 @@
|
||||
99aeae4d0e7cbe900b379d3e7d2f44d7
|
||||
f4fb0059ba6044c039be717fcc2e40bc
|
||||
@@ -1,33 +1,59 @@
|
||||
<script setup>
|
||||
import {
|
||||
EventsEmit,
|
||||
EventsOff,
|
||||
EventsOn,
|
||||
Quit,
|
||||
WindowFullscreen, WindowGetPosition,
|
||||
WindowFullscreen,
|
||||
WindowHide,
|
||||
WindowSetPosition,
|
||||
WindowUnfullscreen
|
||||
WindowUnfullscreen,
|
||||
WindowSetTitle
|
||||
} 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,NButton,dateZhCN,zhCN} from 'naive-ui'
|
||||
import {
|
||||
SettingsOutline,
|
||||
AlarmOutline,
|
||||
AnalyticsOutline,
|
||||
BarChartSharp, Bonfire, BonfireOutline, DiamondOutline, EaselSharp,
|
||||
ExpandOutline, Flag,
|
||||
Flame, FlameSharp, FlaskOutline, InformationOutline,
|
||||
LogoGithub,
|
||||
NewspaperOutline,
|
||||
NewspaperSharp, Notifications,
|
||||
PowerOutline, Pulse,
|
||||
ReorderTwoOutline,
|
||||
ExpandOutline,
|
||||
PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline, AlarmOutline, SparklesOutline, NewspaperOutline,
|
||||
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,GetVersionInfo} from "../wailsjs/go/main/App";
|
||||
import {Dragon, Fire, FirefoxBrowser, Gripfire, Robot} from "@vicons/fa";
|
||||
import {ReportAnalytics, ReportMoney, ReportSearch} from "@vicons/tabler";
|
||||
import {LocalFireDepartmentRound} from "@vicons/material";
|
||||
import {BoxSearch20Regular, CommentNote20Filled} from "@vicons/fluent";
|
||||
import {FireFilled, FireOutlined, NotificationFilled, StockOutlined} from "@vicons/antd";
|
||||
|
||||
|
||||
|
||||
|
||||
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 enableAgent = ref(false)
|
||||
const enableDarkTheme = ref(null)
|
||||
const content = ref('未经授权,禁止商业目的!\n\n数据来源于网络,仅供参考;投资有风险,入市需谨慎')
|
||||
const isFullscreen = ref(false)
|
||||
const activeKey = ref('')
|
||||
const containerRef= ref({})
|
||||
const realtimeProfit= ref(0)
|
||||
const telegraph= ref([])
|
||||
const activeKey = ref('stock')
|
||||
const containerRef = ref({})
|
||||
const realtimeProfit = ref(0)
|
||||
const telegraph = ref([])
|
||||
const groupList = ref([])
|
||||
const officialStatement= ref("")
|
||||
const menuOptions = ref([
|
||||
{
|
||||
label: () =>
|
||||
@@ -36,39 +62,340 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'stock',
|
||||
params: {
|
||||
query: {
|
||||
groupName: '全部',
|
||||
groupId: 0,
|
||||
},
|
||||
}
|
||||
params: {},
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'stock'
|
||||
},
|
||||
},
|
||||
{ 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: () => {
|
||||
activeKey.value = 'stock'
|
||||
//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: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
|
||||
},
|
||||
},
|
||||
{default: () => '市场行情'}
|
||||
),
|
||||
key: 'market',
|
||||
icon: renderIcon(NewspaperOutline),
|
||||
children: [
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "市场快讯",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
|
||||
},
|
||||
},
|
||||
{default: () => '市场快讯',}
|
||||
),
|
||||
key: 'market1',
|
||||
icon: renderIcon(NewspaperSharp),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "全球股指",
|
||||
},
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '全球股指'})
|
||||
},
|
||||
},
|
||||
{default: () => '全球股指',}
|
||||
),
|
||||
key: 'market2',
|
||||
icon: renderIcon(BarChartSharp),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "重大指数",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '重大指数'})
|
||||
},
|
||||
},
|
||||
{default: () => '重大指数',}
|
||||
),
|
||||
key: 'market3',
|
||||
icon: renderIcon(AnalyticsOutline),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "行业排名",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '行业排名'})
|
||||
},
|
||||
},
|
||||
{default: () => '行业排名',}
|
||||
),
|
||||
key: 'market4',
|
||||
icon: renderIcon(Flag),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "个股资金流向",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '个股资金流向'})
|
||||
},
|
||||
},
|
||||
{default: () => '个股资金流向',}
|
||||
),
|
||||
key: 'market5',
|
||||
icon: renderIcon(Pulse),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "龙虎榜",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '龙虎榜'})
|
||||
},
|
||||
},
|
||||
{default: () => '龙虎榜',}
|
||||
),
|
||||
key: 'market6',
|
||||
icon: renderIcon(Dragon),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "个股研报",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '个股研报'})
|
||||
},
|
||||
},
|
||||
{default: () => '个股研报',}
|
||||
),
|
||||
key: 'market7',
|
||||
icon: renderIcon(StockOutlined),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "公司公告",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '公司公告'})
|
||||
},
|
||||
},
|
||||
{default: () => '公司公告',}
|
||||
),
|
||||
key: 'market8',
|
||||
icon: renderIcon(NotificationFilled),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "行业研究",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '行业研究'})
|
||||
},
|
||||
},
|
||||
{default: () => '行业研究',}
|
||||
),
|
||||
key: 'market9',
|
||||
icon: renderIcon(ReportSearch),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "当前热门",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '当前热门'})
|
||||
},
|
||||
},
|
||||
{default: () => '当前热门',}
|
||||
),
|
||||
key: 'market10',
|
||||
icon: renderIcon(Gripfire),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "指标选股",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '指标选股'})
|
||||
},
|
||||
},
|
||||
{default: () => '指标选股',}
|
||||
),
|
||||
key: 'market11',
|
||||
icon: renderIcon(BoxSearch20Regular),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "名站优选",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'market'
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '名站优选'})
|
||||
},
|
||||
},
|
||||
{default: () => '名站优选',}
|
||||
),
|
||||
key: 'market12',
|
||||
icon: renderIcon(FirefoxBrowser),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'market',
|
||||
params: {
|
||||
}
|
||||
}
|
||||
},
|
||||
{ default: () => '市场行情' }
|
||||
),
|
||||
key: 'market',
|
||||
icon: renderIcon(NewspaperOutline),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
@@ -76,18 +403,22 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'fund',
|
||||
params: {
|
||||
query: {
|
||||
name: '基金自选',
|
||||
},
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'fund'
|
||||
},
|
||||
},
|
||||
{ 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),
|
||||
@@ -100,12 +431,108 @@ const menuOptions = ref([
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'settings',
|
||||
params: {
|
||||
}
|
||||
name: 'agent',
|
||||
query: {
|
||||
name:"Ai智能体",
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'agent'
|
||||
},
|
||||
}
|
||||
},
|
||||
{ default: () => '设置' }
|
||||
{default: () => 'Ai智能体'}
|
||||
),
|
||||
key: 'agent',
|
||||
show:enableAgent.value,
|
||||
icon: renderIcon(Robot),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'research',
|
||||
query: {
|
||||
name:"研究中心",
|
||||
},
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'research'
|
||||
setTimeout(() => {
|
||||
EventsEmit("changeResearchTab", {ID: 0, name: 'AI分析报告'})
|
||||
}, 100)
|
||||
},
|
||||
},
|
||||
{default: () => '研究中心'}
|
||||
),
|
||||
key: 'research',
|
||||
icon: renderIcon(FlaskOutline),
|
||||
children:[
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'research',
|
||||
query: {
|
||||
name:"AI分析报告",
|
||||
},
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'research'
|
||||
setTimeout(() => {
|
||||
EventsEmit("changeResearchTab", {ID: 0, name: 'AI分析报告'})
|
||||
}, 100)
|
||||
},
|
||||
},
|
||||
{default: () => 'AI分析报告'}
|
||||
),
|
||||
key: 'research1',
|
||||
icon: renderIcon(ReportAnalytics),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'research',
|
||||
query: {
|
||||
name:"股票推荐记录",
|
||||
},
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'research'
|
||||
setTimeout(() => {
|
||||
EventsEmit("changeResearchTab", {ID: 1, name: '股票推荐记录'})
|
||||
}, 100)
|
||||
},
|
||||
},
|
||||
{default: () => '股票推荐记录'}
|
||||
),
|
||||
key: 'research2',
|
||||
icon: renderIcon(DiamondOutline),
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name: 'settings',
|
||||
query: {
|
||||
name:"设置",
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'settings'
|
||||
},
|
||||
}
|
||||
},
|
||||
{default: () => '设置'}
|
||||
),
|
||||
key: 'settings',
|
||||
icon: renderIcon(SettingsOutline),
|
||||
@@ -117,30 +544,35 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'about',
|
||||
params: {
|
||||
query: {
|
||||
name:"关于",
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
activeKey.value = 'about'
|
||||
},
|
||||
},
|
||||
{ default: () => '关于' }
|
||||
{default: () => '关于'}
|
||||
),
|
||||
key: 'about',
|
||||
icon: renderIcon(LogoGithub),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
show:false,
|
||||
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: () => '隐藏到托盘区' }),
|
||||
title: '隐藏到托盘区 Ctrl+Z',
|
||||
}, {default: () => '隐藏到托盘区'}),
|
||||
key: 'hide',
|
||||
icon: renderIcon(ReorderTwoOutline),
|
||||
},
|
||||
@@ -154,28 +586,32 @@ 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) {
|
||||
activeKey.value = 'full'
|
||||
//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) {
|
||||
@@ -193,11 +629,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) {
|
||||
@@ -213,91 +667,180 @@ window.onerror = function (msg, source, lineno, colno, error) {
|
||||
return true;
|
||||
};
|
||||
|
||||
onBeforeMount(()=>{
|
||||
GetConfig().then((res)=>{
|
||||
console.log(res)
|
||||
enableFund.value=res.enableFund
|
||||
onBeforeMount(() => {
|
||||
GetVersionInfo().then(result => {
|
||||
if(result.officialStatement){
|
||||
content.value = result.officialStatement+"\n\n"+content.value
|
||||
officialStatement.value = result.officialStatement
|
||||
}
|
||||
})
|
||||
|
||||
menuOptions.value.filter((item)=>{
|
||||
if(item.key==='fund'){
|
||||
item.show=res.enableFund
|
||||
GetGroupList().then(result => {
|
||||
groupList.value = result
|
||||
menuOptions.value.map((item) => {
|
||||
//console.log(item)
|
||||
if (item.key === 'stock') {
|
||||
item.children.push(...groupList.value.map(item => {
|
||||
return {
|
||||
label: () =>
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
href: '#',
|
||||
type: 'info',
|
||||
onClick: () => {
|
||||
//console.log("push",item)
|
||||
router.push({
|
||||
name: 'stock',
|
||||
query: {
|
||||
groupName: item.name,
|
||||
groupId: item.ID,
|
||||
},
|
||||
})
|
||||
setTimeout(() => {
|
||||
EventsEmit("changeTab", item)
|
||||
}, 100)
|
||||
},
|
||||
to: {
|
||||
name: 'stock',
|
||||
query: {
|
||||
groupName: item.name,
|
||||
groupId: item.ID,
|
||||
},
|
||||
}
|
||||
},
|
||||
{default: () => item.name,}
|
||||
),
|
||||
key: item.ID,
|
||||
}
|
||||
}))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
GetConfig().then((res) => {
|
||||
//console.log(res)
|
||||
enableFund.value = res.enableFund
|
||||
enableAgent.value = res.enableAgent
|
||||
|
||||
menuOptions.value.filter((item) => {
|
||||
if (item.key === 'fund') {
|
||||
item.show = res.enableFund
|
||||
}
|
||||
if (item.key === 'agent') {
|
||||
item.show = res.enableAgent
|
||||
}
|
||||
})
|
||||
|
||||
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(() => {
|
||||
WindowSetTitle("go-stock:AI赋能股票分析✨ "+officialStatement.value+" 未经授权,禁止商业目的! [数据来源于网络,仅供参考;投资有风险,入市需谨慎]")
|
||||
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
|
||||
enableAgent.value = res.enableAgent
|
||||
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('div',{type:"error",style:{
|
||||
"text-align":"left",
|
||||
"font-size":"14px",
|
||||
"color":"#f67979"
|
||||
}}, { 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('div',{type:"info",style:{
|
||||
"text-align":"left",
|
||||
"font-size":"14px",
|
||||
"color": data.source==="go-stock"?"#F98C24":"#549EC8"
|
||||
}}, { 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>
|
||||
<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="''"
|
||||
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:no-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>
|
||||
|
||||
316
frontend/src/components/AnalyzeMartket.vue
Normal file
316
frontend/src/components/AnalyzeMartket.vue
Normal file
@@ -0,0 +1,316 @@
|
||||
<script setup>
|
||||
|
||||
import {AnalyzeSentimentWithFreqWeight,GlobalStockIndexes} from "../../wailsjs/go/main/App";
|
||||
import * as echarts from "echarts";
|
||||
import {onMounted,onUnmounted, ref} from "vue";
|
||||
import _ from "lodash";
|
||||
const { name,darkTheme,kDays ,chartHeight} = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
kDays: {
|
||||
type: Number,
|
||||
default: 14
|
||||
},
|
||||
chartHeight: {
|
||||
type: Number,
|
||||
default: 500
|
||||
},
|
||||
darkTheme: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const common = ref([])
|
||||
const america = ref([])
|
||||
const europe = ref([])
|
||||
const asia = ref([])
|
||||
const mainIndex = ref([])
|
||||
const chinaIndex = ref([])
|
||||
const other = ref([])
|
||||
const globalStockIndexes = ref(null)
|
||||
const chartRef = ref(null);
|
||||
const gaugeChartRef = ref(null);
|
||||
const triggerAreas=ref(["main","extra","arrow"])
|
||||
let handleChartInterval=null
|
||||
let handleIndexInterval=null
|
||||
onMounted(() => {
|
||||
handleChart()
|
||||
getIndex()
|
||||
handleChartInterval=setInterval(function () {
|
||||
handleChart()
|
||||
}, 1000 * 60)
|
||||
|
||||
handleIndexInterval=setInterval(function () {
|
||||
getIndex()
|
||||
}, 1000 * 2)
|
||||
})
|
||||
|
||||
onUnmounted(()=>{
|
||||
clearInterval(handleChartInterval)
|
||||
clearInterval(handleIndexInterval)
|
||||
})
|
||||
|
||||
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"]
|
||||
mainIndex.value=asia.value.filter(function (item) {
|
||||
return ['上海',"深圳","香港","台湾","北京","东京","首尔","纽约","纳斯达克"].includes(item.location)
|
||||
}).concat(america.value.filter(function (item) {
|
||||
return ['上海',"深圳","香港","台湾","北京","东京","首尔","纽约","纳斯达克"].includes(item.location)
|
||||
}))
|
||||
|
||||
chinaIndex.value=asia.value.filter(function (item) {
|
||||
return ['上海',"深圳","香港","台湾","北京"].includes(item.location)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
function handleChart(){
|
||||
const formatUtil = echarts.format;
|
||||
AnalyzeSentimentWithFreqWeight("").then((res) => {
|
||||
const treemapchart = echarts.init(chartRef.value);
|
||||
const gaugeChart=echarts.init(gaugeChartRef.value);
|
||||
let data = res['frequencies'].map(item => ({
|
||||
name: item.Word,
|
||||
// value: item.Frequency,
|
||||
// value: item.Weight,
|
||||
frequency: item.Frequency,
|
||||
weight: item.Weight,
|
||||
value: item.Score,
|
||||
}));
|
||||
|
||||
let data2 = res['frequencies'].map(item => ({
|
||||
name: item.Word,
|
||||
value: item.Frequency,
|
||||
// value: item.Weight,
|
||||
frequency: item.Frequency,
|
||||
weight: item.Weight,
|
||||
//value: item.Score,
|
||||
}));
|
||||
|
||||
let data3 = res['frequencies'].map(item => ({
|
||||
name: item.Word,
|
||||
//value: item.Frequency,
|
||||
value: item.Weight,
|
||||
frequency: item.Frequency,
|
||||
weight: item.Weight,
|
||||
//value: item.Score,
|
||||
}));
|
||||
|
||||
let option = {
|
||||
darkMode: darkTheme,
|
||||
title: {
|
||||
text:name,
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: darkTheme?'#ccc':'#456'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
toolbox: {
|
||||
left: '20px',
|
||||
tooltip:{
|
||||
textStyle: {
|
||||
color: darkTheme?'#ccc':'#456'
|
||||
}
|
||||
},
|
||||
feature: {
|
||||
saveAsImage: {title: '保存图片'},
|
||||
restore: {
|
||||
title: '默认',
|
||||
},
|
||||
myTool2: {
|
||||
show: true,
|
||||
title: '按权重',
|
||||
icon:"path://M393.8816 148.1216a29.3376 29.3376 0 0 1-15.2576 38.0928c-43.776 17.152-81.92 43.8272-114.2784 76.2368A345.7536 345.7536 0 0 0 159.5392 512 352.8704 352.8704 0 0 0 512 864.4608a351.744 351.744 0 0 0 249.5488-102.912 353.536 353.536 0 0 0 76.2368-114.2784c5.6832-15.2576 22.8352-20.992 38.0928-15.2576 15.2576 5.7344 20.992 22.8864 15.2576 38.0928a421.2224 421.2224 0 0 1-89.6 133.376A412.6208 412.6208 0 0 1 512 921.6c-226.7136 0-409.6-182.8864-409.6-409.6 0-108.544 41.9328-211.456 120.0128-289.5872A421.2224 421.2224 0 0 1 355.84 132.864a29.3376 29.3376 0 0 1 38.0928 15.2576zM512 102.4c226.7136 0 409.6 182.8864 409.6 409.6 0 15.2576-13.312 28.5696-28.5696 28.5696H512A29.2864 29.2864 0 0 1 483.4304 512V130.9696c0-15.2576 13.312-28.5696 28.5696-28.5696z m28.5696 59.0336v321.9968h321.9968a350.976 350.976 0 0 0-321.9968-321.9968z",
|
||||
onclick: function (){
|
||||
treemapchart.setOption( {series:{
|
||||
data: data3
|
||||
}})
|
||||
}
|
||||
},
|
||||
myTool1: {
|
||||
show: true,
|
||||
title: '按频次',
|
||||
icon:"path://M895.466667 476.8l-87.424-87.424v-123.626667a49.770667 49.770667 0 0 0-49.770667-49.770666h-123.626667L547.2 128.533333a49.792 49.792 0 0 0-70.4 0l-87.424 87.424h-123.626667a49.770667 49.770667 0 0 0-49.770666 49.770667v123.626667L128.533333 476.8a49.792 49.792 0 0 0 0 70.4l87.424 87.424v123.626667a49.770667 49.770667 0 0 0 49.770667 49.770666h123.626667l87.424 87.424a49.792 49.792 0 0 0 70.4 0l87.424-87.424h123.626666a49.770667 49.770667 0 0 0 49.770667-49.770666v-123.626667l87.424-87.424a49.749333 49.749333 0 0 0 0.042667-70.4z m-137.216 137.194667v144.256h-144.256L512 860.266667l-101.994667-101.994667h-144.256v-144.256L163.733333 512l101.994667-101.994667v-144.256h144.256L512 163.733333l101.994667 101.994667h144.256v144.256L860.266667 512l-102.016 101.994667z M414.378667 514.730667l28.672 10.922666c-18.090667 47.445333-38.229333 92.16-60.757334 133.802667l-30.037333-13.653333a1042.133333 1042.133333 0 0 0 62.122667-131.072zM381.952 367.616L355.669333 384c25.258667 26.282667 45.056 50.176 60.074667 72.021333l25.6-17.749333c-13.994667-20.48-33.792-44.032-59.392-70.656zM537.258667 455.338667c-0.682667 43.690667-6.144 79.189333-16.725334 106.837333-14.336 32.768-44.373333 60.416-89.429333 82.944l21.162667 25.941333c52.224-26.624 85.333333-60.074667 99.328-100.693333 1.706667-5.12 3.413333-10.24 4.778666-15.36 21.504 45.738667 52.906667 83.968 93.866667 115.370667l21.504-24.917334c-51.2-34.474667-86.357333-81.237333-105.813333-140.288 1.706667-15.701333 2.730667-32.085333 2.730666-49.834666h-31.402666z M508.586667 434.858667h115.712c-6.826667 25.258667-15.018667 47.786667-24.917334 66.901333l31.744 8.874667a627.008 627.008 0 0 0 27.989334-85.674667v-21.162667H517.12c3.413333-14.336 6.144-29.354667 8.874667-45.738666l-32.426667-5.12c-7.850667 59.392-25.6 105.813333-52.906667 139.264l26.965334 19.114666c16.725333-19.114667 30.378667-44.373333 40.96-76.458666z",
|
||||
onclick: function (){
|
||||
treemapchart.setOption( {series:{
|
||||
data: data2
|
||||
}})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
formatter: function (info) {
|
||||
var value = info.value.toFixed(2);
|
||||
var frequency = info.data.frequency;
|
||||
var weight = info.data.weight;
|
||||
return [
|
||||
'<div class="tooltip-title">' + info.name+ '</div>',
|
||||
'热度: ' + formatUtil.addCommas(value) + '',
|
||||
'<div class="tooltip-title">频次: ' + formatUtil.addCommas(frequency)+ '</div>',
|
||||
'<div class="tooltip-title">权重: ' + formatUtil.addCommas(weight)+ '</div>',
|
||||
].join('');
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'treemap',
|
||||
breadcrumb:{show: false},
|
||||
left: '0',
|
||||
top: '40',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
tooltip: {
|
||||
show: true
|
||||
},
|
||||
data: data
|
||||
}
|
||||
]
|
||||
};
|
||||
treemapchart.setOption(option);
|
||||
|
||||
|
||||
|
||||
let option2 = {
|
||||
darkMode: darkTheme,
|
||||
series: [
|
||||
{
|
||||
type: 'gauge',
|
||||
startAngle: 180,
|
||||
endAngle: 0,
|
||||
center: ['50%', '75%'],
|
||||
radius: '90%',
|
||||
min: -100,
|
||||
max: 100,
|
||||
splitNumber: 8,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
width: 6,
|
||||
color: [
|
||||
// [0.25, '#FF6E76'],
|
||||
// [0.5, '#FDDD60'],
|
||||
// [0.75, '#58D9F9'],
|
||||
// [1, '#7CFFB2'],
|
||||
|
||||
[0.25, '#03fb6a'],
|
||||
[0.5, '#58e1f9'],
|
||||
[0.75, '#ef5922'],
|
||||
[1, '#f11d29'],
|
||||
|
||||
]
|
||||
}
|
||||
},
|
||||
pointer: {
|
||||
icon: 'path://M12.8,0.7l12,40.1H0.7L12.8,0.7z',
|
||||
length: '12%',
|
||||
width: 20,
|
||||
offsetCenter: [0, '-60%'],
|
||||
itemStyle: {
|
||||
color: 'auto'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
length: 12,
|
||||
lineStyle: {
|
||||
color: 'auto',
|
||||
width: 2
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
length: 20,
|
||||
lineStyle: {
|
||||
color: 'auto',
|
||||
width: 5
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: darkTheme?'#ccc':'#456',
|
||||
fontSize: 20,
|
||||
distance: -45,
|
||||
rotate: 'tangential',
|
||||
formatter: function (value) {
|
||||
if (value ===100) {
|
||||
return '极热';
|
||||
} else if (value === 50) {
|
||||
return '乐观';
|
||||
} else if (value === 0) {
|
||||
return '中性';
|
||||
}else if (value === -50) {
|
||||
return '谨慎';
|
||||
} else if (value === -100) {
|
||||
return '冰点';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
title: {
|
||||
offsetCenter: [0, '-10%'],
|
||||
fontSize: 20
|
||||
},
|
||||
detail: {
|
||||
fontSize: 30,
|
||||
offsetCenter: [0, '-35%'],
|
||||
valueAnimation: true,
|
||||
formatter: function (value) {
|
||||
return value.toFixed(2) + '';
|
||||
},
|
||||
color: 'inherit'
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: res.result.Score*0.2,
|
||||
name: '市场情绪强弱'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
gaugeChart.setOption(option2);
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-collapse :trigger-areas="triggerAreas" :default-expanded-names="['1']" display-directive="show">
|
||||
<n-collapse-item name="1" >
|
||||
<template #header>
|
||||
<n-flex>
|
||||
<n-tag size="small" :bordered="false" v-for="(item, index) in mainIndex" :type="item.zdf>0?'error':'success'">
|
||||
<n-flex>
|
||||
<n-image :width="20" :src="item.img" />
|
||||
<n-text style="font-size: 14px" :type="item.zdf>0?'error':'success'">{{item.name}} {{item.zxj}}</n-text>
|
||||
<n-number-animation :precision="2" :from="0" :to="item.zdf" style="font-size: 14px"/>
|
||||
<n-text style="margin-left: -12px;font-size: 14px" :type="item.zdf>0?'error':'success'">%</n-text>
|
||||
</n-flex>
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
主要股指
|
||||
</template>
|
||||
<n-grid :cols="24" :y-gap="0">
|
||||
<n-gi span="6">
|
||||
<div ref="gaugeChartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
|
||||
</n-gi>
|
||||
<n-gi span="18">
|
||||
<div ref="chartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
102
frontend/src/components/ClsCalendarTimeLine.vue
Normal file
102
frontend/src/components/ClsCalendarTimeLine.vue
Normal 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>
|
||||
133
frontend/src/components/EmbeddedUrl.vue
Normal file
133
frontend/src/components/EmbeddedUrl.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div class="embed-container">
|
||||
<h3 v-if="title">{{ title }}</h3>
|
||||
<div class="iframe-wrapper">
|
||||
<iframe
|
||||
:src="url"
|
||||
:title="iframeTitle"
|
||||
frameborder="0"
|
||||
scrolling="auto"
|
||||
class="embedded-iframe"
|
||||
@load="onLoad"
|
||||
@error="onError"
|
||||
:style="iframeStyle"
|
||||
></iframe>
|
||||
</div>
|
||||
<div v-if="loading" class="loading-indicator">
|
||||
<div class="spinner"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
<p v-if="error" class="error-message">{{ error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
iframeTitle: {
|
||||
type: String,
|
||||
default: '外部内容'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
}
|
||||
})
|
||||
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
|
||||
const onLoad = () => {
|
||||
loading.value = false
|
||||
error.value = null
|
||||
}
|
||||
|
||||
const onError = (event) => {
|
||||
loading.value = false
|
||||
error.value = `加载失败: ${event.message || '无法加载该 URL'}`
|
||||
}
|
||||
|
||||
// 监听 URL 变化,重新加载
|
||||
watch(() => props.url, () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
})
|
||||
|
||||
// 设置 iframe 样式
|
||||
const iframeStyle = {
|
||||
width: props.width,
|
||||
height: props.height
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.embed-container {
|
||||
margin: 1rem 0;
|
||||
border: 0 solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.iframe-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.embedded-iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 3px solid #f3f4f6;
|
||||
border-radius: 50%;
|
||||
border-top-color: #3b82f6;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ef4444;
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
background-color: #fee2e2;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
37
frontend/src/components/HotEvents.vue
Normal file
37
frontend/src/components/HotEvents.vue
Normal 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>
|
||||
88
frontend/src/components/HotStockList.vue
Normal file
88
frontend/src/components/HotStockList.vue
Normal 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" :stockName="item.name" :k-days="20" :dark-theme="true"></k-line-chart>
|
||||
</n-popover>
|
||||
</n-text></n-td>
|
||||
<n-td><n-text :type="item.percent>0?'error':'success'">{{item.percent}}%</n-text></n-td>
|
||||
<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>
|
||||
83
frontend/src/components/HotTopics.vue
Normal file
83
frontend/src/components/HotTopics.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import {onBeforeMount, onUnmounted, ref} from 'vue'
|
||||
import {HotTopic, OpenURL} from "../../wailsjs/go/main/App";
|
||||
import {Environment} from "../../wailsjs/runtime";
|
||||
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;
|
||||
|
||||
Environment().then(env => {
|
||||
switch (env.platform) {
|
||||
case 'windows':
|
||||
window.open(
|
||||
url,
|
||||
'centeredWindow',
|
||||
`width=${width},height=${height},left=${left},top=${top}`
|
||||
)
|
||||
break
|
||||
default:
|
||||
OpenURL(url)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
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>
|
||||
115
frontend/src/components/IndustryResearchReportList.vue
Normal file
115
frontend/src/components/IndustryResearchReportList.vue
Normal 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>
|
||||
108
frontend/src/components/InvestCalendarTimeLine.vue
Normal file
108
frontend/src/components/InvestCalendarTimeLine.vue
Normal 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>
|
||||
@@ -4,12 +4,12 @@ import {GetStockKLine} from "../../wailsjs/go/main/App";
|
||||
import * as echarts from "echarts";
|
||||
import {onMounted, ref} from "vue";
|
||||
import _ from "lodash";
|
||||
const { code,name,darkTheme,kDays ,chartHeight} = defineProps({
|
||||
const { code,stockName,darkTheme,kDays ,chartHeight} = defineProps({
|
||||
code: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
stockName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
@@ -33,13 +33,15 @@ const downBorderColor = '';
|
||||
const kLineChartRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
handleKLine(code,name)
|
||||
handleKLine(code,stockName)
|
||||
})
|
||||
|
||||
function handleKLine(code,name){
|
||||
GetStockKLine(code,name,365).then(result => {
|
||||
function handleKLine(code,stockName){
|
||||
console.log("handleKLine",code,stockName)
|
||||
const chart = echarts.init(kLineChartRef.value);
|
||||
chart.showLoading()
|
||||
GetStockKLine(code,stockName,365).then(result => {
|
||||
//console.log("GetStockKLine",result)
|
||||
const chart = echarts.init(kLineChartRef.value);
|
||||
const categoryData = [];
|
||||
const values = [];
|
||||
const volumns=[];
|
||||
@@ -47,24 +49,28 @@ function handleKLine(code,name){
|
||||
let resultElement=result[i]
|
||||
//console.log("resultElement:{}",resultElement)
|
||||
categoryData.push(resultElement.day)
|
||||
let flag=resultElement.close>resultElement.open?1:-1
|
||||
let flag=Number(resultElement.close)>Number(resultElement.open)?1:-1
|
||||
if(i>0){
|
||||
flag=Number(resultElement.close)>Number(result[i-1].close)?1:-1
|
||||
}
|
||||
values.push([
|
||||
resultElement.open,
|
||||
resultElement.close,
|
||||
resultElement.low,
|
||||
resultElement.high
|
||||
Number(resultElement.open),
|
||||
Number(resultElement.close),
|
||||
Number(resultElement.low),
|
||||
Number(resultElement.high)
|
||||
])
|
||||
volumns.push([i,resultElement.volume/10000,flag])
|
||||
volumns.push([i,Number(resultElement.volume)/10000,flag])
|
||||
}
|
||||
////console.log("categoryData",categoryData)
|
||||
////console.log("values",values)
|
||||
let option = {
|
||||
title: {
|
||||
text: name,
|
||||
left: '20px',
|
||||
text: stockName+" "+categoryData[values.length-1]+" "+values[values.length-1][1]+" "+((values[values.length-1][1]-values[values.length-2][1])/values[values.length-2][1]*100).toFixed(2)+"%",
|
||||
left: '0px',
|
||||
textStyle: {
|
||||
color: darkTheme?'#ccc':'#456'
|
||||
}
|
||||
color: Number(values[values.length-1][1])>Number(values[values.length-2][1])?'red':'green',
|
||||
fontSize: 14
|
||||
},
|
||||
},
|
||||
darkMode: darkTheme,
|
||||
//backgroundColor: '#1c1c1c',
|
||||
@@ -96,7 +102,7 @@ function handleKLine(code,name){
|
||||
color: darkTheme?'#ccc':'#456'
|
||||
},
|
||||
formatter: function (params) {//修改鼠标划过显示为中文
|
||||
console.log("params",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的值
|
||||
@@ -150,7 +156,7 @@ function handleKLine(code,name){
|
||||
left: '8%',
|
||||
right: '8%',
|
||||
top: '66%',
|
||||
height: '15%'
|
||||
height: '18%'
|
||||
}
|
||||
],
|
||||
xAxis: [
|
||||
@@ -184,7 +190,11 @@ function handleKLine(code,name){
|
||||
scale: true,
|
||||
splitArea: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
axisLabel: { show: true },
|
||||
axisLine: { show: true },
|
||||
axisTick: { show: true },
|
||||
splitLine: { show: false }
|
||||
},
|
||||
{
|
||||
scale: true,
|
||||
@@ -354,6 +364,7 @@ function handleKLine(code,name){
|
||||
]
|
||||
};
|
||||
chart.setOption(option);
|
||||
chart.hideLoading()
|
||||
})
|
||||
}
|
||||
function calculateMA(dayCount,values) {
|
||||
@@ -374,7 +385,7 @@ function calculateMA(dayCount,values) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="kLineChartRef" style="width: 100%;height: auto;" :style="{height:chartHeight+'px'}"></div>
|
||||
<div ref="kLineChartRef" style="width: 100%;height: auto;--wails-draggable:no-drag" :style="{height:chartHeight+'px'}" ></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
229
frontend/src/components/LongTigerRankList.vue
Normal file
229
frontend/src/components/LongTigerRankList.vue
Normal 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" :stockName="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>
|
||||
298
frontend/src/components/SelectStock.vue
Normal file
298
frontend/src/components/SelectStock.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<script setup lang="ts">
|
||||
import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
|
||||
import {SearchStock, GetHotStrategy, OpenURL, Follow, GetFollowList} from "../../wailsjs/go/main/App";
|
||||
import {useMessage, NText, NTag, NButton} from 'naive-ui'
|
||||
import {Environment} from "../../wailsjs/runtime"
|
||||
import {RefreshCircleSharp} from "@vicons/ionicons5";
|
||||
import {EventsEmit} from "../../wailsjs/runtime";
|
||||
|
||||
const message = useMessage()
|
||||
const search = ref('')
|
||||
const columns = ref([])
|
||||
const dataList = ref([])
|
||||
const hotStrategy = ref([])
|
||||
const traceInfo = ref('')
|
||||
const tableScrollX = ref(2800) // 默认滚动宽度
|
||||
|
||||
// 计算表格总宽度
|
||||
function calculateTableWidth(cols) {
|
||||
let totalWidth = 0;
|
||||
|
||||
cols.forEach(col => {
|
||||
if (col.children && col.children.length > 0) {
|
||||
// 有子列的情况
|
||||
let childrenWidth = 0;
|
||||
col.children.forEach(child => {
|
||||
childrenWidth += child.width || child.minWidth || 100;
|
||||
});
|
||||
// 取标题列宽度和子列总宽度的较大值
|
||||
totalWidth += Math.max(col.width || col.minWidth || 200, childrenWidth);
|
||||
} else {
|
||||
// 没有子列的情况
|
||||
totalWidth += col.width || col.minWidth || 120;
|
||||
}
|
||||
});
|
||||
|
||||
// 加上操作列的宽度
|
||||
totalWidth += 100;
|
||||
|
||||
return Math.max(totalWidth, 1200); // 最小宽度1200
|
||||
}
|
||||
|
||||
function Search() {
|
||||
if (!search.value) {
|
||||
message.warning('请输入选股指标或者要求')
|
||||
return
|
||||
}
|
||||
|
||||
const loading = message.loading("正在获取选股数据...", {duration: 0});
|
||||
SearchStock(search.value).then(res => {
|
||||
loading.destroy()
|
||||
// console.log(res)
|
||||
if (res.code == 100) {
|
||||
traceInfo.value = res.data.traceInfo.showText
|
||||
// message.success(res.msg)
|
||||
columns.value = res.data.result.columns.filter(item => !item.hiddenNeed && (item.title != "市场码" && item.title != "市场简称")).map(item => {
|
||||
if (item.children) {
|
||||
return {
|
||||
title: item.title + (item.unit ? '[' + item.unit + ']' : ''),
|
||||
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
|
||||
},
|
||||
sorter: (row1, row2) => {
|
||||
if (isNumeric(row1[item.key]) && isNumeric(row2[item.key])) {
|
||||
return row1[item.key] - row2[item.key];
|
||||
} else {
|
||||
return 'default'
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
title: item.title + (item.unit ? '[' + item.unit + ']' : ''),
|
||||
key: item.key,
|
||||
resizable: true,
|
||||
minWidth: 120,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
sorter: (row1, row2) => {
|
||||
if (isNumeric(row1[item.key]) && isNumeric(row2[item.key])) {
|
||||
return row1[item.key] - row2[item.key];
|
||||
} else {
|
||||
return 'default'
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
columns.value.push({
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 80,
|
||||
fixed: 'right', // 固定在右侧
|
||||
render: (row) => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
strong: true,
|
||||
tertiary: true,
|
||||
size: 'small',
|
||||
type: 'warning', // 橙色按钮
|
||||
style: 'font-size: 14px; padding: 0 10px;', // 稍微大一点的按钮
|
||||
onClick: () => handleFollow(row)
|
||||
},
|
||||
{ default: () => '关注' }
|
||||
)
|
||||
}
|
||||
});
|
||||
dataList.value = res.data.result.dataList
|
||||
console.log("sss"+columns.value. length)
|
||||
// 计算并设置表格宽度
|
||||
tableScrollX.value = calculateTableWidth(columns.value);
|
||||
} else {
|
||||
if(res.msg){
|
||||
message.error(res.msg)
|
||||
}
|
||||
if(res.message){
|
||||
message.error(res.message)
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
message.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
// 修改handleFollow方法,使用stock.vue的AddStock逻辑
|
||||
function handleFollow(row) {
|
||||
let code=row.MARKET_SHORT_NAME.toLowerCase()+row.SECURITY_CODE
|
||||
Follow(code).then(result => {
|
||||
if (result === "关注成功") {
|
||||
message.success(result)
|
||||
} else {
|
||||
message.error(result)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isNumeric(value) {
|
||||
return !isNaN(parseFloat(value)) && isFinite(value);
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
GetHotStrategy().then(res => {
|
||||
console.log(res)
|
||||
if (res.code == 1) {
|
||||
hotStrategy.value = res.data
|
||||
search.value = hotStrategy.value[0].question
|
||||
Search()
|
||||
}
|
||||
}).catch(err => {
|
||||
message.error(err)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
function DoSearch(question) {
|
||||
search.value = question
|
||||
Search()
|
||||
}
|
||||
|
||||
function openCenteredWindow(url, width, height) {
|
||||
const left = (window.screen.width - width) / 2;
|
||||
const top = (window.screen.height - height) / 2;
|
||||
|
||||
Environment().then(env => {
|
||||
switch (env.platform) {
|
||||
case 'windows':
|
||||
window.open(
|
||||
url,
|
||||
'centeredWindow',
|
||||
`width=${width},height=${height},left=${left},top=${top},location=no,menubar=no,toolbar=no,display=standalone`
|
||||
)
|
||||
break
|
||||
default:
|
||||
OpenURL(url)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-grid :cols="24" style="max-height: calc(100vh - 165px)">
|
||||
<n-gi :span="4">
|
||||
<n-list bordered style="text-align: left;" hoverable clickable>
|
||||
<n-scrollbar style="max-height: calc(100vh - 170px);">
|
||||
<n-list-item v-for="item in hotStrategy" :key="item.rank" @click="DoSearch(item.question)">
|
||||
<n-ellipsis line-clamp="1" :tooltip="true">
|
||||
<n-tag size="small" :bordered="false" type="info">#{{ item.rank }}</n-tag>
|
||||
<n-text type="warning">{{ item.question }}</n-text>
|
||||
<template #tooltip>
|
||||
<div style="text-align: center;max-width: 180px">
|
||||
<n-text type="warning">{{ item.question }}</n-text>
|
||||
</div>
|
||||
</template>
|
||||
</n-ellipsis>
|
||||
</n-list-item>
|
||||
</n-scrollbar>
|
||||
</n-list>
|
||||
|
||||
<!-- <n-virtual-list :items="hotStrategy" :item-size="hotStrategy.length">-->
|
||||
<!-- <template #default="{ item, index }">-->
|
||||
<!-- <n-card :title="''" size="small">-->
|
||||
<!-- <template #header-extra>-->
|
||||
<!-- {{item.rank}}-->
|
||||
<!-- </template>-->
|
||||
<!-- <n-ellipsis expand-trigger="click" line-clamp="3" :tooltip="false" >-->
|
||||
<!-- <n-text type="warning">{{item.question }}</n-text>-->
|
||||
<!-- </n-ellipsis>-->
|
||||
<!-- </n-card>-->
|
||||
|
||||
<!-- </template>-->
|
||||
<!-- </n-virtual-list>-->
|
||||
</n-gi>
|
||||
<n-gi :span="20">
|
||||
<n-flex style="--wails-draggable:no-drag">
|
||||
<n-input-group style="text-align: left">
|
||||
<n-input :rows="1" clearable v-model:value="search" placeholder="请输入选股指标或者要求"/>
|
||||
<n-button type="primary" @click="Search">搜索A股</n-button>
|
||||
</n-input-group>
|
||||
</n-flex>
|
||||
<n-flex justify="start" v-if="traceInfo" style="margin: 5px 0;--wails-draggable:no-drag">
|
||||
|
||||
<n-ellipsis line-clamp="1" :tooltip="true">
|
||||
<n-text type="info" :bordered="false">选股条件:</n-text>
|
||||
<n-text type="warning" :bordered="true">{{ traceInfo }}</n-text>
|
||||
<template #tooltip>
|
||||
<div style="text-align: center;max-width: 580px">
|
||||
<n-text type="warning">{{ traceInfo }}</n-text>
|
||||
</div>
|
||||
</template>
|
||||
</n-ellipsis>
|
||||
|
||||
<!-- <n-button type="primary" size="small">保存策略</n-button>-->
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
:striped="true"
|
||||
:max-height="'calc(100vh - 150px)'"
|
||||
size="medium"
|
||||
:columns="columns"
|
||||
:data="dataList"
|
||||
:pagination="{pageSize: 10}"
|
||||
:scroll-x="tableScrollX"
|
||||
: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(NButton, { type: 'info',bordered: false ,size:'small',onClick:()=>{
|
||||
//https://quote.eastmoney.com/sz300558.html#fullScreenChart
|
||||
openCenteredWindow(`https://quote.eastmoney.com/${rowData.MARKET_SHORT_NAME}${rowData.SECURITY_CODE}.html#fullScreenChart`,1240,700)
|
||||
}}, { default: () => `${value}` })
|
||||
}else{
|
||||
return h(NText, { type: 'info' }, { default: () => `${value}` })
|
||||
}
|
||||
}
|
||||
}"
|
||||
/>
|
||||
<div style="margin-top: -25px">共找到
|
||||
<n-tag type="info" :bordered="false">{{ dataList.length }}</n-tag>
|
||||
只股
|
||||
</div>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
148
frontend/src/components/StockNoticeList.vue
Normal file
148
frontend/src/components/StockNoticeList.vue
Normal 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" :stockName="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>
|
||||
136
frontend/src/components/StockResearchReportList.vue
Normal file
136
frontend/src/components/StockResearchReportList.vue
Normal 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" :stockName="item.stockName" :k-days="20" :dark-theme="true"></k-line-chart>
|
||||
</n-popover>
|
||||
</n-td>
|
||||
<n-td><n-tag type="info" :bordered="false">{{item.indvInduName}}</n-tag></n-td>
|
||||
<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>
|
||||
@@ -3,15 +3,22 @@
|
||||
// preview.css相比style.css少了编辑器那部分样式
|
||||
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 {NAvatar, NButton, useNotification} from "naive-ui";
|
||||
import {CheckUpdate, GetVersionInfo,GetSponsorInfo,OpenURL} from "../../wailsjs/go/main/App";
|
||||
import {EventsOff, EventsOn,Environment} from "../../wailsjs/runtime";
|
||||
import {NAvatar, NButton, useNotification,NText} from "naive-ui";
|
||||
import { addMonths, format ,parse} from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
const updateLog = ref('');
|
||||
const versionInfo = ref('');
|
||||
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
|
||||
const alipay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/alipay.jpg')
|
||||
const wxpay =ref('https://github.com/ArvinLovegood/go-stock/raw/master/build/screenshot/wxpay.jpg')
|
||||
const wxgzh =ref('https://github.com/ArvinLovegood/go-stock/raw/dev/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')
|
||||
const notify = useNotification()
|
||||
const vipLevel=ref("");
|
||||
const vipStartTime=ref("");
|
||||
const vipEndTime=ref("");
|
||||
const expired=ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
document.title = '关于软件';
|
||||
@@ -21,10 +28,29 @@ onMounted(() => {
|
||||
icon.value = res.icon;
|
||||
alipay.value=res.alipay;
|
||||
wxpay.value=res.wxpay;
|
||||
wxgzh.value=res.wxgzh;
|
||||
|
||||
GetSponsorInfo().then((res) => {
|
||||
vipLevel.value = res.vipLevel;
|
||||
vipStartTime.value = res.vipStartTime;
|
||||
vipEndTime.value = res.vipEndTime;
|
||||
//判断时间是否到期
|
||||
if (res.vipLevel) {
|
||||
if (res.vipEndTime < format(new Date(), 'yyyy-MM-dd HH:mm:ss')) {
|
||||
notify.warning({content: 'VIP已到期'})
|
||||
expired.value = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
notify.destroyAll()
|
||||
EventsOff("updateVersion")
|
||||
})
|
||||
|
||||
EventsOn("updateVersion",async (msg) => {
|
||||
@@ -43,8 +69,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, {
|
||||
@@ -69,7 +95,16 @@ EventsOn("updateVersion",async (msg) => {
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: () => {
|
||||
window.open(msg.html_url)
|
||||
Environment().then(env => {
|
||||
switch (env.platform) {
|
||||
case 'windows':
|
||||
window.open(msg.html_url)
|
||||
break
|
||||
default :
|
||||
OpenURL(msg.html_url)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
}, { default: () => '查看' })
|
||||
}
|
||||
@@ -79,21 +114,22 @@ EventsOn("updateVersion",async (msg) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-space vertical size="large">
|
||||
<n-space vertical size="large" style="--wails-draggable:no-drag">
|
||||
<!-- 软件描述 -->
|
||||
<n-card size="large">
|
||||
<n-divider title-placement="center">关于软件</n-divider>
|
||||
<n-space vertical >
|
||||
<n-image width="100" :src="icon" />
|
||||
<h1>
|
||||
<n-badge :value="versionInfo" :offset="[50,10]" type="success">
|
||||
<n-badge v-if="!vipLevel" :value="versionInfo" :offset="[80,10]" type="success">
|
||||
<n-gradient-text type="info" :size="50" >go-stock</n-gradient-text>
|
||||
</n-badge>
|
||||
<n-badge v-if="vipLevel" :value="versionInfo" :offset="[70,10]" type="success">
|
||||
<n-gradient-text :type="expired?'error':'warning'" :size="50" >go-stock</n-gradient-text><n-tag :bordered="false" size="small" type="warning">VIP{{vipLevel}}</n-tag>
|
||||
</n-badge>
|
||||
</h1>
|
||||
<n-button size="tiny" @click="CheckUpdate" type="info" tertiary >检查更新</n-button>
|
||||
|
||||
|
||||
|
||||
<n-gradient-text :type="expired?'error':'warning'" v-if="vipLevel" >vip到期时间:{{vipEndTime}}</n-gradient-text>
|
||||
<n-button size="tiny" @click="CheckUpdate(1)" type="info" tertiary >检查更新</n-button>
|
||||
<div style="justify-self: center;text-align: left" >
|
||||
<p>自选股行情实时监控,基于Wails和NaiveUI构建的AI赋能股票分析工具</p>
|
||||
<p>目前已支持A股,港股,美股,未来计划加入基金,ETF等支持</p>
|
||||
@@ -112,14 +148,39 @@ EventsOn("updateVersion",async (msg) => {
|
||||
<p>QQ交流群:<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0YQ8qD3exahsD4YLNhzQTWe5ssstWC89&authKey=usOMMRFtIQDC%2FYcatHYapcxQbJ7PwXPHK9OypTXWzNjAq%2FRVvQu9bj2lRgb%2BSZ3p&noverify=0&group_code=491605333" target="_blank">491605333</a></p>
|
||||
</div>
|
||||
</n-space>
|
||||
<n-divider title-placement="center">支持💕开源</n-divider>
|
||||
<n-flex justify="center">
|
||||
<n-table size="small" style="width: 820px">
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<n-th>赞助计划</n-th>
|
||||
<n-th>赞助等级</n-th>
|
||||
<n-th>权益说明</n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr>
|
||||
<n-td>每月 0 RMB</n-td><n-td>vip0</n-td><n-td>🌟 全部功能,软件自动更新(从GitHub下载),自行解决github平台网络问题。</n-td>
|
||||
</n-tr>
|
||||
<n-tr>
|
||||
<n-td>赞助 18.8 RMB/月<br>赞助 120 RMB/年</n-td><n-td>vip1</n-td><n-td>💕 全部功能,软件自动更新(从CDN下载),更新快速便捷。AI配置指导,提示词参考等</n-td>
|
||||
</n-tr>
|
||||
<n-tr>
|
||||
<n-td>赞助 28.8 RMB/月<br>赞助 240 RMB/年</n-td><n-td>vip2</n-td><n-td>💕 vip1全部功能,启动时自动同步最近24小时市场资讯(包括外媒简讯) 💕</n-td>
|
||||
</n-tr>
|
||||
<n-tr>
|
||||
<n-td>每月赞助 X RMB</n-td><n-td>vipX</n-td><n-td>🧩 更多计划,视go-stock开源项目发展情况而定...(承接GitHub项目README广告推广💖)</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</n-flex>
|
||||
<n-divider title-placement="center">关于作者</n-divider>
|
||||
<n-space vertical>
|
||||
<!-- <h1>关于作者</h1>-->
|
||||
<n-avatar width="100" src="https://avatars.githubusercontent.com/u/7401917?v=4" />
|
||||
<h2><a href="https://github.com/ArvinLovegood" target="_blank">@ArvinLovegood</a></h2>
|
||||
<p>一个热爱编程的小白,欢迎关注我的Github</p>
|
||||
<n-image width="300" src="https://go-stock.sparkmemory.top/assets/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88-DEJtWc_y.png" />
|
||||
|
||||
<p>一个热爱编程的小白,欢迎关注我的Github/微信公众号</p>
|
||||
<n-image width="300" :src="wxgzh" />
|
||||
<p>开源不易,如果觉得好用,可以请作者喝杯咖啡。</p>
|
||||
<n-flex justify="center">
|
||||
<n-image width="200" :src="alipay" />
|
||||
@@ -134,6 +195,8 @@ EventsOn("updateVersion",async (msg) => {
|
||||
</p>
|
||||
<p>
|
||||
感谢以下开发者:
|
||||
<a href="https://github.com/GiCo001" target="_blank">@Gico</a><n-divider vertical />
|
||||
<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 />
|
||||
|
||||
365
frontend/src/components/agent-chat.vue
Normal file
365
frontend/src/components/agent-chat.vue
Normal file
@@ -0,0 +1,365 @@
|
||||
<template>
|
||||
<div class="chat-box">
|
||||
<t-chat
|
||||
ref="chatRef"
|
||||
:clear-history="chatList.length > 0 && !isStreamLoad"
|
||||
:data="chatList"
|
||||
:text-loading="loading"
|
||||
:is-stream-load="isStreamLoad"
|
||||
style="height: 100%"
|
||||
@scroll="handleChatScroll"
|
||||
@clear="clearConfirm"
|
||||
>
|
||||
<!-- eslint-disable vue/no-unused-vars -->
|
||||
<template #content="{ item, index }">
|
||||
<t-chat-reasoning v-if="item.role === 'assistant'" expand-icon-placement="right">
|
||||
<t-chat-loading v-if="isStreamLoad" text="思考中..." />
|
||||
<t-chat-content v-if="item.reasoning.length > 0" :content="item.reasoning" />
|
||||
</t-chat-reasoning>
|
||||
<t-chat-content v-if="item.content.length > 0" :content="item.content" />
|
||||
</template>
|
||||
<template #actions="{ item, index }">
|
||||
<t-chat-action
|
||||
:content="item.content"
|
||||
:operation-btn="['copy']"
|
||||
@operation="handleOperation"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<!-- <t-chat-input :stop-disabled="isStreamLoad" @send="inputEnter" @stop="onStop"> </t-chat-input>-->
|
||||
<t-chat-sender
|
||||
ref="chatSenderRef"
|
||||
v-model="inputValue"
|
||||
class="chat-sender"
|
||||
:textarea-props="{
|
||||
placeholder: '请输入消息...',
|
||||
}"
|
||||
:loading="loading"
|
||||
:stop-disabled="isStreamLoad"
|
||||
@send="inputEnter"
|
||||
@stop="onStop"
|
||||
>
|
||||
<template #suffix>
|
||||
<!-- 监听键盘回车发送事件需要在sender组件监听 -->
|
||||
<t-button theme="default" variant="text" size="large" class="btn" @click="inputEnter"> 发送 </t-button>
|
||||
</template>
|
||||
<template #prefix>
|
||||
<NFlex>
|
||||
<NSelect
|
||||
v-model:value="selectValue"
|
||||
:options="selectOptions"
|
||||
label-field="name" value-field="ID"
|
||||
size="tiny"
|
||||
style="width: 200px;"
|
||||
/>
|
||||
</NFlex>
|
||||
</template>
|
||||
</t-chat-sender>
|
||||
|
||||
</template>
|
||||
</t-chat>
|
||||
<t-button v-show="isShowToBottom" variant="text" class="bottomBtn" @click="backBottom">
|
||||
<div class="to-bottom">
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</t-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted, h, onBeforeUnmount, onBeforeMount} from 'vue';
|
||||
import {ArrowDownIcon, CheckCircleIcon, SystemSumIcon} from 'tdesign-icons-vue-next';
|
||||
const fetchCancel = ref(null);
|
||||
const loading = ref(false);
|
||||
|
||||
const inputValue = ref('');
|
||||
// 流式数据加载中
|
||||
const isStreamLoad = ref(false);
|
||||
|
||||
const chatRef = ref(null);
|
||||
const isShowToBottom = ref(false);
|
||||
|
||||
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
|
||||
import {darkTheme, NFlex, NImage,NSelect} from "naive-ui";
|
||||
import {ChatWithAgent, GetAiConfigs, GetConfig, GetSponsorInfo, GetVersionInfo} from "../../wailsjs/go/main/App";
|
||||
import {EventsOff, EventsOn} from '../../wailsjs/runtime'
|
||||
import 'tdesign-vue-next/es/style/index.css';
|
||||
|
||||
|
||||
const allowToolTip = ref(true);
|
||||
const chatSenderRef = ref(null);
|
||||
const selectOptions = ref([]);
|
||||
const selectValue = ref("default");
|
||||
onBeforeUnmount(() => {
|
||||
EventsOff("agent-message")
|
||||
})
|
||||
EventsOn("agent-message", (data) => {
|
||||
console.log(data)
|
||||
if(data['role']==="assistant"){
|
||||
loading.value = false;
|
||||
const lastItem = chatList.value[0];
|
||||
if (data['reasoning_content']){
|
||||
lastItem.reasoning += data['reasoning_content'];
|
||||
}
|
||||
if (data['content']){
|
||||
lastItem.content +=data['content'];
|
||||
}
|
||||
if(data['tool_calls']){
|
||||
for (const tool of data['tool_calls']) {
|
||||
console.log(tool.id, tool.type, tool.function.name, tool.function.arguments);
|
||||
lastItem.reasoning += "\n```"+tool.function.name+"\n" +
|
||||
"参数:"+ (tool.function.arguments?tool.function.arguments:"无")+
|
||||
"\n```\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
if(data['response_meta']&&data['response_meta'].finish_reason==="stop"){
|
||||
isStreamLoad.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
onBeforeMount(() => {
|
||||
GetAiConfigs().then(res=>{
|
||||
console.log(res)
|
||||
selectOptions.value = res
|
||||
selectValue.value = res[0].ID
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
//chatRef.value.scrollToBottom();
|
||||
|
||||
GetConfig().then((res) => {
|
||||
if (res.darkTheme) {
|
||||
document.documentElement.setAttribute("theme-mode", "dark");
|
||||
} else {
|
||||
document.documentElement.removeAttribute("theme-mode"); }
|
||||
})
|
||||
|
||||
|
||||
GetVersionInfo().then((res) => {
|
||||
icon.value = res.icon;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// 滚动到底部
|
||||
const backBottom = () => {
|
||||
chatRef.value.scrollToBottom({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
// 是否显示回到底部按钮
|
||||
const handleChatScroll = function ({ e }) {
|
||||
const scrollTop = e.target.scrollTop;
|
||||
isShowToBottom.value = scrollTop < 0;
|
||||
};
|
||||
// 清空消息
|
||||
const clearConfirm = function () {
|
||||
chatList.value = [];
|
||||
};
|
||||
const handleOperation = function (type, options) {
|
||||
console.log('handleOperation', type, options);
|
||||
};
|
||||
// 倒序渲染
|
||||
const chatList = ref([
|
||||
// {
|
||||
// content: `模型由<span>hunyuan</span>变为<span>GPT4</span>`,
|
||||
// role: 'model-change',
|
||||
// reasoning: '',
|
||||
// },
|
||||
{
|
||||
avatar: h(NImage, { src: icon.value, height: '48px', width: '48px'}),
|
||||
name: 'Go-Stock AI',
|
||||
datetime: '',
|
||||
reasoning: '',
|
||||
content: '我是您的AI赋能股票分析助手,您可以问我任何关于股票投资方面的问题。',
|
||||
role: 'assistant',
|
||||
duration: 10,
|
||||
},
|
||||
{
|
||||
avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
|
||||
name: '宇宙无敌大韭菜',
|
||||
datetime: '',
|
||||
content: '介绍下自己?',
|
||||
role: 'user',
|
||||
reasoning: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const onStop = function () {
|
||||
if (fetchCancel.value) {
|
||||
fetchCancel.value.controller.close();
|
||||
loading.value = false;
|
||||
isStreamLoad.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const inputEnter = function () {
|
||||
if (isStreamLoad.value) {
|
||||
return;
|
||||
}
|
||||
if (!inputValue.value) return;
|
||||
const params = {
|
||||
avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
|
||||
name: '宇宙无敌大韭菜',
|
||||
datetime: new Date().toDateString(),
|
||||
content: inputValue.value,
|
||||
role: 'user',
|
||||
};
|
||||
chatList.value.unshift(params);
|
||||
// 空消息占位
|
||||
const params2 = {
|
||||
avatar: h(NImage, { src: icon.value, height: '48px', width: '48px'}),
|
||||
name: 'Go-Stock AI',
|
||||
datetime: new Date().toDateString(),
|
||||
content: '',
|
||||
reasoning: '',
|
||||
role: 'assistant',
|
||||
};
|
||||
chatList.value.unshift(params2);
|
||||
loading.value = true;
|
||||
isStreamLoad.value = true;
|
||||
ChatWithAgent(inputValue.value,selectValue.value,0)
|
||||
};
|
||||
</script>
|
||||
<style lang="less">
|
||||
/* 应用滚动条样式 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--td-scrollbar-color);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:horizontal:hover {
|
||||
background-color: var(--td-scrollbar-hover-color);
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--td-scroll-track-color);
|
||||
}
|
||||
.chat-box {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
margin: 5px 10px 5px 10px;
|
||||
text-align: left;
|
||||
.bottomBtn {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -20px;
|
||||
bottom: 210px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.08), 0px 16px 24px 2px rgba(0, 0, 0, 0.04),
|
||||
0px 6px 30px 5px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.to-bottom {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid #dcdcdc;
|
||||
box-sizing: border-box;
|
||||
background: var(--td-bg-color-container);
|
||||
border-radius: 50%;
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.t-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.model-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.t-select {
|
||||
width: 112px;
|
||||
height: 32px;
|
||||
margin-right: 8px;
|
||||
.t-input {
|
||||
border-radius: 32px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
.check-box {
|
||||
width: 112px;
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
border: 0;
|
||||
background: #e7e7e7;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 auto;
|
||||
.t-button__text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
span {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.check-box.is-active {
|
||||
border: 1px solid #d9e1ff;
|
||||
background: #f2f3ff;
|
||||
color: var(--td-brand-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.chat-sender {
|
||||
.btn {
|
||||
color: var(--td-text-color-disabled);
|
||||
border: none;
|
||||
&:hover {
|
||||
color: var(--td-brand-color-hover);
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
.btn.t-button {
|
||||
height: var(--td-comp-size-m);
|
||||
padding: 0;
|
||||
}
|
||||
.model-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.t-select {
|
||||
width: 112px;
|
||||
height: var(--td-comp-size-m);
|
||||
margin-right: var(--td-comp-margin-s);
|
||||
.t-input {
|
||||
border-radius: 32px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
.t-input.t-is-focused {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
.check-box {
|
||||
width: 112px;
|
||||
height: var(--td-comp-size-m);
|
||||
border-radius: 32px;
|
||||
border: 0;
|
||||
background: var(--td-bg-color-component);
|
||||
color: var(--td-text-color-primary);
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 auto;
|
||||
.t-button__text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
span {
|
||||
margin-left: var(--td-comp-margin-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
.check-box.is-active {
|
||||
border: 1px solid var(--td-brand-color-focus);
|
||||
background: var(--td-brand-color-light);
|
||||
color: var(--td-text-color-brand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
338
frontend/src/components/agent-chat_bk.vue
Normal file
338
frontend/src/components/agent-chat_bk.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<div class="chat-box">
|
||||
<t-chat
|
||||
ref="chatRef"
|
||||
:clear-history="chatList.length > 0 && !isStreamLoad"
|
||||
:data="chatList"
|
||||
:text-loading="loading"
|
||||
:is-stream-load="isStreamLoad"
|
||||
style="height: 100%"
|
||||
@scroll="handleChatScroll"
|
||||
@clear="clearConfirm"
|
||||
>
|
||||
<!-- eslint-disable vue/no-unused-vars -->
|
||||
<template #content="{ item, index }">
|
||||
<t-chat-reasoning v-if="item.reasoning?.length > 0" expand-icon-placement="right">
|
||||
<template #header>
|
||||
<t-chat-loading v-if="isStreamLoad && item.content.length === 0" text="思考中..." />
|
||||
<div v-else style="display: flex; align-items: center">
|
||||
<CheckCircleIcon style="color: var(--td-success-color-5); font-size: 20px; margin-right: 8px" />
|
||||
<span>已深度思考</span>
|
||||
</div>
|
||||
</template>
|
||||
<t-chat-content v-if="item.reasoning.length > 0" :content="item.reasoning" />
|
||||
</t-chat-reasoning>
|
||||
<t-chat-content v-if="item.content.length > 0" :content="item.content" />
|
||||
</template>
|
||||
<template #actions="{ item, index }">
|
||||
<t-chat-action
|
||||
:content="item.content"
|
||||
:operation-btn="['good', 'bad', 'replay', 'copy']"
|
||||
@operation="handleOperation"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<t-chat-input :stop-disabled="isStreamLoad" @send="inputEnter" @stop="onStop"> </t-chat-input>
|
||||
</template>
|
||||
</t-chat>
|
||||
<t-button v-show="isShowToBottom" variant="text" class="bottomBtn" @click="backBottom">
|
||||
<div class="to-bottom">
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</t-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="jsx">
|
||||
import {ref, onMounted, h, onBeforeUnmount} from 'vue';
|
||||
import { MockSSEResponse } from '../mock-data/index';
|
||||
import { ArrowDownIcon, CheckCircleIcon } from 'tdesign-icons-vue-next';
|
||||
const fetchCancel = ref(null);
|
||||
const loading = ref(false);
|
||||
// 流式数据加载中
|
||||
const isStreamLoad = ref(false);
|
||||
|
||||
const chatRef = ref(null);
|
||||
const isShowToBottom = ref(false);
|
||||
|
||||
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
|
||||
import {darkTheme, NAvatar, NImage} from "naive-ui";
|
||||
import {ChatWithAgent, GetConfig, GetSponsorInfo, GetVersionInfo} from "../../wailsjs/go/main/App";
|
||||
import {EventsOff, EventsOn} from '../../wailsjs/runtime'
|
||||
import 'tdesign-vue-next/es/style/index.css';
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
EventsOff("agent-message")
|
||||
})
|
||||
EventsOn("agent-message", (data) => {
|
||||
console.log(data)
|
||||
if(data['role']==="assistant"){
|
||||
loading.value = false;
|
||||
isStreamLoad.value = true;
|
||||
const lastItem = chatList.value[0];
|
||||
if (data['reasoning_content']){
|
||||
lastItem.reasoning += data['reasoning_content'];
|
||||
}
|
||||
if (data['content']){
|
||||
lastItem.content +=data['content'];
|
||||
}
|
||||
if(data['response_meta'].finish_reason==="stop"){
|
||||
isStreamLoad.value = false;
|
||||
}
|
||||
if(data['tool_calls']){
|
||||
lastItem.tool_calls = data['tool_calls'];
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
//chatRef.value.scrollToBottom();
|
||||
|
||||
GetConfig().then((res) => {
|
||||
if (res.darkTheme) {
|
||||
document.documentElement.setAttribute("theme-mode", "dark");
|
||||
} else {
|
||||
document.documentElement.removeAttribute("theme-mode"); }
|
||||
})
|
||||
|
||||
|
||||
GetVersionInfo().then((res) => {
|
||||
icon.value = res.icon;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// 滚动到底部
|
||||
const backBottom = () => {
|
||||
chatRef.value.scrollToBottom({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
// 是否显示回到底部按钮
|
||||
const handleChatScroll = function ({ e }) {
|
||||
const scrollTop = e.target.scrollTop;
|
||||
isShowToBottom.value = scrollTop < 0;
|
||||
};
|
||||
// 清空消息
|
||||
const clearConfirm = function () {
|
||||
chatList.value = [];
|
||||
};
|
||||
const handleOperation = function (type, options) {
|
||||
console.log('handleOperation', type, options);
|
||||
};
|
||||
// 倒序渲染
|
||||
const chatList = ref([
|
||||
// {
|
||||
// content: `模型由<span>hunyuan</span>变为<span>GPT4</span>`,
|
||||
// role: 'model-change',
|
||||
// reasoning: '',
|
||||
// },
|
||||
{
|
||||
avatar: h(NImage, { src: icon.value, height: '48px', width: '48px'}),
|
||||
name: 'Go-Stock AI',
|
||||
datetime: '',
|
||||
reasoning: '',
|
||||
content: '我是您的AI赋能股票分析助手,您可以问我任何关于股票投资方面的问题。',
|
||||
role: 'assistant',
|
||||
duration: 10,
|
||||
},
|
||||
{
|
||||
avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
|
||||
name: '宇宙无敌大韭菜',
|
||||
datetime: '',
|
||||
content: '介绍下自己?',
|
||||
role: 'user',
|
||||
reasoning: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const onStop = function () {
|
||||
if (fetchCancel.value) {
|
||||
fetchCancel.value.controller.close();
|
||||
loading.value = false;
|
||||
isStreamLoad.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const inputEnter = function (inputValue) {
|
||||
if (isStreamLoad.value) {
|
||||
return;
|
||||
}
|
||||
if (!inputValue) return;
|
||||
const params = {
|
||||
avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
|
||||
name: '宇宙无敌大韭菜',
|
||||
datetime: new Date().toDateString(),
|
||||
content: inputValue,
|
||||
role: 'user',
|
||||
};
|
||||
chatList.value.unshift(params);
|
||||
// 空消息占位
|
||||
const params2 = {
|
||||
avatar: h(NImage, { src: icon.value, height: '48px', width: '48px'}),
|
||||
name: 'Go-Stock AI',
|
||||
datetime: new Date().toDateString(),
|
||||
content: '',
|
||||
reasoning: '',
|
||||
role: 'assistant',
|
||||
};
|
||||
chatList.value.unshift(params2);
|
||||
handleData(inputValue);
|
||||
ChatWithAgent(inputValue,1,0)
|
||||
};
|
||||
|
||||
|
||||
|
||||
const fetchSSE = async (fetchFn, options) => {
|
||||
const response = await fetchFn();
|
||||
const { success, fail, complete } = options;
|
||||
// 如果不 ok 说明有请求错误
|
||||
if (!response.ok) {
|
||||
complete?.(false, response.statusText);
|
||||
fail?.();
|
||||
return;
|
||||
}
|
||||
const reader = response?.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
if (!reader) return;
|
||||
|
||||
reader.read().then(function processText({ done, value }) {
|
||||
if (done) {
|
||||
// 正常的返回
|
||||
complete?.(true);
|
||||
return;
|
||||
}
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
const buffers = chunk.toString().split(/\r?\n/);
|
||||
const jsonData = JSON.parse(buffers);
|
||||
success(jsonData);
|
||||
reader.read().then(processText);
|
||||
});
|
||||
};
|
||||
const handleData = async () => {
|
||||
loading.value = true;
|
||||
isStreamLoad.value = true;
|
||||
const lastItem = chatList.value[0];
|
||||
const mockedData = {
|
||||
reasoning: `嗯,用户问牛顿第一定律是不是适用于所有参考系。首先,我得先回忆一下牛顿第一定律的内容。牛顿第一定律,也就是惯性定律,说物体在没有外力作用时会保持静止或匀速直线运动。也就是说,保持原来的运动状态。
|
||||
|
||||
那问题来了,这个定律是否适用于所有参考系呢?记得以前学过的参考系分惯性系和非惯性系。惯性系里,牛顿定律成立;非惯性系里,可能需要引入惯性力之类的修正。所以牛顿第一定律应该只在惯性参考系中成立,而在非惯性系中不适用,比如加速的电梯或者旋转的参考系,这时候物体会有看似无外力下的加速度,所以必须引入假想的力来解释。`,
|
||||
content: `牛顿第一定律(惯性定律)**并不适用于所有参考系**,它只在**惯性参考系**中成立。以下是关键点:
|
||||
|
||||
---
|
||||
|
||||
### **1. 牛顿第一定律的核心**
|
||||
- **内容**:物体在不受外力(或合力为零)时,将保持静止或匀速直线运动状态。
|
||||
- **本质**:定义了惯性系的存在——即存在一类参考系,在其中惯性定律成立。`,
|
||||
};
|
||||
const mockResponse = new MockSSEResponse(mockedData);
|
||||
fetchCancel.value = mockResponse;
|
||||
await fetchSSE(
|
||||
() => {
|
||||
return mockResponse.getResponse();
|
||||
},
|
||||
{
|
||||
success(result) {
|
||||
console.log('success', result);
|
||||
loading.value = false;
|
||||
lastItem.reasoning += result.delta.reasoning_content;
|
||||
lastItem.content += result.delta.content;
|
||||
},
|
||||
complete(isOk, msg) {
|
||||
if (!isOk) {
|
||||
lastItem.role = 'error';
|
||||
lastItem.content = msg;
|
||||
lastItem.reasoning = msg;
|
||||
}
|
||||
// 显示用时xx秒,业务侧需要自行处理
|
||||
lastItem.duration = 20;
|
||||
// 控制终止按钮
|
||||
isStreamLoad.value = false;
|
||||
loading.value = false;
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
</script>
|
||||
<style lang="less">
|
||||
/* 应用滚动条样式 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--td-scrollbar-color);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:horizontal:hover {
|
||||
background-color: var(--td-scrollbar-hover-color);
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--td-scroll-track-color);
|
||||
}
|
||||
.chat-box {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
margin: 5px 10px 5px 10px;
|
||||
.bottomBtn {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -20px;
|
||||
bottom: 210px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.08), 0px 16px 24px 2px rgba(0, 0, 0, 0.04),
|
||||
0px 6px 30px 5px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.to-bottom {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid #dcdcdc;
|
||||
box-sizing: border-box;
|
||||
background: var(--td-bg-color-container);
|
||||
border-radius: 50%;
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.t-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.model-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.t-select {
|
||||
width: 112px;
|
||||
height: 32px;
|
||||
margin-right: 8px;
|
||||
.t-input {
|
||||
border-radius: 32px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
.check-box {
|
||||
width: 112px;
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
border: 0;
|
||||
background: #e7e7e7;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 auto;
|
||||
.t-button__text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
span {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.check-box.is-active {
|
||||
border: 1px solid #d9e1ff;
|
||||
background: #f2f3ff;
|
||||
color: var(--td-brand-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
405
frontend/src/components/aiRecommendStocksList.vue
Normal file
405
frontend/src/components/aiRecommendStocksList.vue
Normal file
@@ -0,0 +1,405 @@
|
||||
<script setup>
|
||||
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted,onUnmounted, ref,reactive} from 'vue'
|
||||
import {
|
||||
GetAiRecommendStocksList,
|
||||
GetConfig,
|
||||
GetSponsorInfo,
|
||||
SaveAsMarkdown,
|
||||
ShareAnalysis
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import {NAvatar, NButton, NEllipsis, NTag, NText, useMessage, useNotification} from "naive-ui";
|
||||
import KLineChart from "./KLineChart.vue";
|
||||
import {format} from "date-fns";
|
||||
|
||||
const notify = useNotification()
|
||||
const vipLevel=ref("");
|
||||
const vipStartTime=ref("");
|
||||
const vipEndTime=ref("");
|
||||
const expired=ref(false)
|
||||
const isValidVip=ref(false) // 是否是会员
|
||||
|
||||
onBeforeMount(()=> {
|
||||
GetConfig().then(result => {
|
||||
if (result.darkTheme) {
|
||||
editorDataRef.darkTheme = true
|
||||
}
|
||||
})
|
||||
|
||||
GetSponsorInfo().then((res) => {
|
||||
// console.log(res)
|
||||
vipLevel.value = res.vipLevel;
|
||||
vipStartTime.value = res.vipStartTime;
|
||||
vipEndTime.value = res.vipEndTime;
|
||||
//判断时间是否到期
|
||||
if (res.vipLevel) {
|
||||
if (res.vipEndTime < format(new Date(), 'yyyy-MM-dd HH:mm:ss')) {
|
||||
//notify.warning({content: 'VIP已到期'})
|
||||
expired.value = true;
|
||||
}
|
||||
}else{
|
||||
//notify.success({content: '未开通VIP'})
|
||||
}
|
||||
isValidVip.value = !(vipLevel.value === "" || Number(vipLevel.value) <= 0);
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
onMounted(() => {
|
||||
query({
|
||||
page: 1,
|
||||
pageSize: paginationReactive.pageSize,
|
||||
order: "desc",
|
||||
keyword: paginationReactive.keyword,
|
||||
startDate: paginationReactive.range[0],
|
||||
endDate: paginationReactive.range[1]
|
||||
}).then((data) => {
|
||||
console.log( data)
|
||||
dataRef.value = data.data
|
||||
paginationReactive.page = 1
|
||||
paginationReactive.pageCount = data.pageCount
|
||||
paginationReactive.itemCount = data.total
|
||||
loadingRef.value = false
|
||||
})
|
||||
})
|
||||
const message = useMessage()
|
||||
const mdPreviewRef = ref(null)
|
||||
const mdEditorRef = ref(null)
|
||||
const editorDataRef = reactive({
|
||||
show: false,
|
||||
loading: false,
|
||||
darkTheme: false,
|
||||
chatId: "",
|
||||
modelName: "",
|
||||
CreatedAt: "",
|
||||
stockName: "",
|
||||
stockCode: "",
|
||||
question: "",
|
||||
content: "",
|
||||
})
|
||||
const dataRef = ref([])
|
||||
const loadingRef = ref(true)
|
||||
|
||||
// StockClosePrice string `json:"StockClosePrice" md:"推荐时股票收盘价格"`
|
||||
// StockPrePrice string `json:"stockPrePricePrice" md:"前一交易日股票价格"`
|
||||
// RecommendReason string `json:"recommendReason" md:"推荐理由/驱动因素/逻辑"`
|
||||
// RecommendBuyPrice string `json:"recommendBuyPrice" md:"ai建议买入价"`
|
||||
// RecommendStopProfitPrice string `json:"recommendStopProfitPrice" md:"ai建议止盈价"`
|
||||
// RecommendStopLossPrice string `json:"recommendStopLossPrice" md:"ai建议止损价"`
|
||||
// RiskRemarks string `json:"riskRemarks" md:"风险提示"`
|
||||
// Remarks string `json:"remarks" md:"备注"`
|
||||
const columnsRef = ref([
|
||||
{
|
||||
title: '推荐时间',
|
||||
key: 'dataTime',
|
||||
render(row, index) {
|
||||
//2026-01-14T22:13:27.2693252+08:00 格式化为常用时间格式
|
||||
return row.CreatedAt.substring(0, 19).replace('T', ' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '板块概念',
|
||||
key: 'bkName'
|
||||
},
|
||||
{
|
||||
title: '股票名称',
|
||||
key: 'stockName',
|
||||
render(row, index) {
|
||||
return h(NText, { type: "info" }, { default: () => row.stockName })
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '股票代码',
|
||||
key: 'stockCode'
|
||||
},
|
||||
{
|
||||
title: '最新',
|
||||
key: 'stockCurrentPrice',
|
||||
minWidth: 120,
|
||||
render(row, index) {
|
||||
|
||||
let diff = ((Number(row.stockCurrentPrice) - Number(row.stockPrePrice))/ Number(row.stockPrePrice)*100).toFixed(2)
|
||||
|
||||
if(Number(row.stockCurrentPrice)< Number(row.stockPrePrice)) {
|
||||
return [h(NText, { type: "success", bordered: false }, { default: () => row.stockCurrentPrice+` | ${diff}%` })]
|
||||
} else {
|
||||
return [h(NText, { type: "error" , bordered: false}, { default: () => row.stockCurrentPrice+` | ${diff}%` })]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '推荐时',
|
||||
key: 'stockPrice',
|
||||
render(row, index) {
|
||||
|
||||
if(vipLevel.value===""|| Number(vipLevel.value) <=0){
|
||||
return h(NText, { type: "info" }, { default: () => row.stockPrice })
|
||||
}
|
||||
|
||||
let diff = ((Number(row.stockCurrentPrice) - Number(row.stockPrice))/ Number(row.stockPrice)*100).toFixed(2)
|
||||
let flagStr="暂平"
|
||||
let flag="info"
|
||||
if(Number(row.stockCurrentPrice)>Number(row.stockPrice)) {
|
||||
flagStr="暂赢 "+diff+"%"
|
||||
flag="error"
|
||||
}else if(Number(row.stockCurrentPrice)===Number(row.stockPrice)){
|
||||
flagStr="暂平"
|
||||
flag="info"
|
||||
}else{
|
||||
flagStr="暂亏 "+ diff+"%"
|
||||
flag="success"
|
||||
}
|
||||
|
||||
return [h(NText, { type: "info" }, { default: () => row.stockPrice }),h(NTag, { type: flag,size: "tiny", bordered: false }, { default: () => flagStr })]
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '昨收',
|
||||
key: 'stockPrePrice',
|
||||
render(row, index) {
|
||||
return h(NText, { type: "info" }, { default: () => row.stockPrePrice })
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'ai建议买入价',
|
||||
key: 'recommendBuyPrice',
|
||||
render(row, index) {
|
||||
if(vipLevel.value===""|| Number(vipLevel.value) <=0){
|
||||
return h(NText, { type: "info" }, { default: () => row.recommendBuyPrice })
|
||||
}
|
||||
|
||||
|
||||
if(row.recommendBuyPrice.includes("-")){
|
||||
let prices= row.recommendBuyPrice.split("-")
|
||||
if(Number(row.stockCurrentPrice)>=Number(prices[0])&&Number(row.stockCurrentPrice)<=Number(prices[1])){
|
||||
return [h(NText, { type: "success" }, { default: () => row.recommendBuyPrice }),h(NTag, { type: "error", size: "tiny", bordered: false }, { default: () => "Buy" })]
|
||||
}
|
||||
}
|
||||
return h(NText, { type: "info" }, { default: () => row.recommendBuyPrice })
|
||||
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'ai建议止盈价',
|
||||
key: 'recommendStopProfitPrice'
|
||||
},
|
||||
{
|
||||
title: 'ai建议止损价',
|
||||
key: 'recommendStopLossPrice'
|
||||
},
|
||||
{
|
||||
title: '推荐理由',
|
||||
key: 'recommendReason',
|
||||
ellipsis: {
|
||||
tooltip: isValidVip
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '风险提示',
|
||||
key: 'riskRemarks',
|
||||
ellipsis: {
|
||||
tooltip: isValidVip
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
key: 'remarks',
|
||||
ellipsis: {
|
||||
tooltip: isValidVip
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
render(row, index) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
strong: true,
|
||||
tertiary: true,
|
||||
size: 'small',
|
||||
type: 'warning', // 橙色按钮
|
||||
style: 'font-size: 14px; padding: 0 10px;', // 稍微大一点的按钮
|
||||
onClick: () => rowProps(row)
|
||||
},
|
||||
{ default: () => '查看详细' }
|
||||
)
|
||||
}
|
||||
},
|
||||
])
|
||||
const paginationReactive = reactive({
|
||||
page: 1,
|
||||
pageCount: 1,
|
||||
pageSize: 12,
|
||||
itemCount: 0,
|
||||
keyword: "",
|
||||
range: [new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000), new Date(new Date().getTime() + 24 * 60 * 60 * 1000)],
|
||||
prefix({ itemCount }) {
|
||||
return `${itemCount} 条记录`
|
||||
}
|
||||
})
|
||||
|
||||
const modalDataRef = reactive({
|
||||
visible: false,
|
||||
title: "",
|
||||
content: "",
|
||||
riskRemarks: "",
|
||||
stockCode: "",
|
||||
stockName: "",
|
||||
remarks: "",
|
||||
})
|
||||
|
||||
const theme = computed(() => {
|
||||
return editorDataRef.darkTheme ? 'dark' : 'light'
|
||||
})
|
||||
|
||||
|
||||
function query({
|
||||
page,
|
||||
pageSize = 10,
|
||||
order = 'desc',
|
||||
keyword = "",
|
||||
startDate = "",
|
||||
endDate = ""
|
||||
}) {
|
||||
return new Promise((resolve) => {
|
||||
|
||||
GetAiRecommendStocksList({
|
||||
"page": page,
|
||||
"pageSize": pageSize,
|
||||
"modelName":keyword,
|
||||
"stockName":keyword,
|
||||
"stockCode":keyword,
|
||||
"bkName":keyword,
|
||||
"startDate": startDate,
|
||||
"endDate": endDate
|
||||
}).then((res) => {
|
||||
const pagedData =res.list
|
||||
const total = res.total
|
||||
const pageCount =res.totalPages
|
||||
resolve({
|
||||
pageCount,
|
||||
data: pagedData,
|
||||
total
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function handlePageChange(currentPage) {
|
||||
if (!loadingRef.value) {
|
||||
loadingRef.value = true
|
||||
query({
|
||||
page: currentPage,
|
||||
pageSize: paginationReactive.pageSize,
|
||||
order: "desc",
|
||||
keyword: paginationReactive.keyword,
|
||||
startDate: formatDate(paginationReactive.range[0]), // Format date to string
|
||||
endDate: formatDate(paginationReactive.range[1]) // Format date to string
|
||||
}).then((data) => {
|
||||
dataRef.value = data.data
|
||||
paginationReactive.page = currentPage
|
||||
paginationReactive.pageCount = data.pageCount
|
||||
paginationReactive.itemCount = data.total
|
||||
loadingRef.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
function handleSearch() {
|
||||
if (!loadingRef.value) {
|
||||
loadingRef.value = true
|
||||
query({
|
||||
page: 1,
|
||||
pageSize: paginationReactive.pageSize,
|
||||
order: "desc",
|
||||
keyword: paginationReactive.keyword,
|
||||
startDate: formatDate(paginationReactive.range[0]),
|
||||
endDate: formatDate(paginationReactive.range[1])
|
||||
}).then((data) => {
|
||||
dataRef.value = data.data
|
||||
paginationReactive.page = 1
|
||||
paginationReactive.pageCount = data.pageCount
|
||||
paginationReactive.itemCount = data.total
|
||||
loadingRef.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
function getStockCode(stockCode) {
|
||||
if(stockCode.indexOf( ".")>0){
|
||||
stockCode=stockCode.split(".")[1]+stockCode.split(".")[0]
|
||||
}
|
||||
//转化为小写
|
||||
stockCode=stockCode.toLowerCase()
|
||||
return stockCode
|
||||
|
||||
}
|
||||
|
||||
function rowProps(row) {
|
||||
return {
|
||||
style: 'cursor: pointer;',
|
||||
onClick: () => {
|
||||
if(vipLevel.value===""|| Number(vipLevel.value) <=0){
|
||||
notify.warning({content: '未开通VIP或者已经过期'})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
//message.info(row.stockName)
|
||||
modalDataRef.title = row.stockName
|
||||
modalDataRef.content = row.recommendReason
|
||||
modalDataRef.riskRemarks = row.riskRemarks
|
||||
modalDataRef.stockCode = getStockCode(row.stockCode)
|
||||
modalDataRef.stockName = row.stockName
|
||||
modalDataRef.visible = true
|
||||
modalDataRef.remarks = row.remarks
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-input-group>
|
||||
<n-date-picker v-model:value="paginationReactive.range" type="datetimerange" style="width: 50%"/>
|
||||
<n-input clearable placeholder="输入关键词搜索" v-model:value="paginationReactive.keyword"/>
|
||||
<n-button type="primary" ghost @click="handleSearch" @input="handleSearch">
|
||||
搜索
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<n-data-table
|
||||
remote
|
||||
:row-props="rowProps"
|
||||
size="small"
|
||||
:columns="columnsRef"
|
||||
:data="dataRef"
|
||||
:loading="loadingRef"
|
||||
:pagination="paginationReactive"
|
||||
:row-key="(rowData)=>rowData.ID"
|
||||
@update:page="handlePageChange"
|
||||
flex-height
|
||||
style="height: calc(100vh - 210px);margin-top: 10px"
|
||||
/>
|
||||
|
||||
<n-modal v-model:show="modalDataRef.visible" :title="modalDataRef.title" preset="card" style="width: 850px;">
|
||||
<n-gradient-text :size="16" type="warning">{{modalDataRef.remarks}}</n-gradient-text>
|
||||
<n-card size="small">
|
||||
<KLineChart style="width: 800px" :code="getStockCode(modalDataRef.stockCode)" :chart-height="500" :stock-name="modalDataRef.stockName" :k-days="30" :dark-theme="editorDataRef.darkTheme"></KLineChart>
|
||||
</n-card>
|
||||
<n-card size="small">
|
||||
<n-text type="info">{{modalDataRef.content}}</n-text>
|
||||
<n-divider><n-gradient-text type="error">风险提示</n-gradient-text></n-divider>
|
||||
<n-text type="error">{{modalDataRef.riskRemarks}}</n-text>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
GetConfig,
|
||||
GetFollowedFund,
|
||||
GetfundList,
|
||||
GetVersionInfo,
|
||||
GetVersionInfo, OpenURL,
|
||||
UnFollowFund
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import vueDanmaku from 'vue3-danmaku'
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -147,8 +147,19 @@ function formatterTitle(title){
|
||||
|
||||
function search(code,name){
|
||||
setTimeout(() => {
|
||||
window.open("https://fund.eastmoney.com/"+code+".html","_blank","noreferrer,width=1000,top=100,left=100,status=no,toolbar=no,location=no,scrollbars=no")
|
||||
//window.open("https://fund.eastmoney.com/"+code+".html","_blank","noreferrer,width=1000,top=100,left=100,status=no,toolbar=no,location=no,scrollbars=no")
|
||||
//window.open("https://finance.sina.com.cn/fund/quotes/"+code+"/bc.shtml","_blank","width=1000,height=800,top=100,left=100,toolbar=no,location=no")
|
||||
|
||||
Environment().then(env => {
|
||||
switch (env.platform) {
|
||||
case 'windows':
|
||||
window.open("https://fund.eastmoney.com/"+code+".html","_blank","noreferrer,width=1000,top=100,left=100,status=no,toolbar=no,location=no,scrollbars=no")
|
||||
break
|
||||
default :
|
||||
OpenURL("https://fund.eastmoney.com/"+code+".html")
|
||||
}
|
||||
})
|
||||
|
||||
}, 500)
|
||||
}
|
||||
|
||||
|
||||
94
frontend/src/components/industryMoneyRank.vue
Normal file
94
frontend/src/components/industryMoneyRank.vue
Normal 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>
|
||||
@@ -1,20 +1,42 @@
|
||||
<script setup>
|
||||
import {computed, h, onBeforeMount, ref} from 'vue'
|
||||
import * as echarts from "echarts";
|
||||
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted,onUnmounted, ref} from 'vue'
|
||||
import {
|
||||
GetAIResponseResult,
|
||||
GetConfig, GetPromptTemplates,
|
||||
GetConfig,
|
||||
GetIndustryRank,
|
||||
GetPromptTemplates,
|
||||
GetTelegraphList,
|
||||
GlobalStockIndexes,
|
||||
SaveAIResponseResult, SaveAsMarkdown, ShareAnalysis,
|
||||
SummaryStockNews
|
||||
ReFleshTelegraphList,
|
||||
SaveAIResponseResult,
|
||||
SaveAsMarkdown,
|
||||
ShareAnalysis,
|
||||
SummaryStockNews,
|
||||
GetAiConfigs,
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import {EventsOn} from "../../wailsjs/runtime";
|
||||
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
|
||||
import NewsList from "./newsList.vue";
|
||||
import KLineChart from "./KLineChart.vue";
|
||||
import {Add, ChatboxOutline, PulseOutline,} from "@vicons/ionicons5";
|
||||
import { CaretDown, CaretUp, PulseOutline,} from "@vicons/ionicons5";
|
||||
import {NAvatar, NButton, NFlex, NText, useMessage, useNotification} from "naive-ui";
|
||||
import {ExportPDF} from "@vavt/v3-extension";
|
||||
import {MdEditor, MdPreview} from "md-editor-v3";
|
||||
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";
|
||||
import Stockhotmap from "./stockhotmap.vue";
|
||||
|
||||
const route = useRoute()
|
||||
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
|
||||
|
||||
const message = useMessage()
|
||||
@@ -23,29 +45,42 @@ const panelHeight = ref(window.innerHeight - 240)
|
||||
|
||||
const telegraphList = ref([])
|
||||
const sinaNewsList = ref([])
|
||||
|
||||
const foreignNewsList = 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(() => {
|
||||
const summaryModal = ref(false)
|
||||
const summaryBTN = ref(true)
|
||||
const darkTheme = ref(false)
|
||||
const httpProxyEnabled = 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 aiSummary = ref(``)
|
||||
const aiSummaryTime = ref("")
|
||||
const modelName = ref("")
|
||||
const chatId = ref("")
|
||||
const question = ref(``)
|
||||
const aiConfigId = ref(null)
|
||||
const sysPromptId = ref(null)
|
||||
const loading = ref(true)
|
||||
const aiConfigs = ref([])
|
||||
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('')
|
||||
const enableTools= ref(true)
|
||||
const thinkingMode = ref(true)
|
||||
const treemapRef = ref(null);
|
||||
let treemapchart =null;
|
||||
|
||||
function getIndex() {
|
||||
GlobalStockIndexes().then((res) => {
|
||||
@@ -59,42 +94,92 @@ function getIndex() {
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
nowTab.value = route.query.name
|
||||
stockCode.value = route.query.stockCode
|
||||
GetConfig().then(result => {
|
||||
summaryBTN.value= result.openAiEnable
|
||||
darkTheme.value = result.darkTheme
|
||||
summaryBTN.value = result.openAiEnable
|
||||
darkTheme.value = result.darkTheme
|
||||
httpProxyEnabled.value = result.httpProxyEnabled
|
||||
})
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
promptTemplates.value=res
|
||||
sysPromptOptions.value=promptTemplates.value.filter(item => item.type === '模型系统Prompt')
|
||||
userPromptOptions.value=promptTemplates.value.filter(item => item.type === '模型用户Prompt')
|
||||
GetPromptTemplates("", "").then(res => {
|
||||
promptTemplates.value = res
|
||||
sysPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型系统Prompt')
|
||||
userPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型用户Prompt')
|
||||
})
|
||||
|
||||
GetAiConfigs().then(res=>{
|
||||
aiConfigs.value = res
|
||||
aiConfigId.value = res[0].ID
|
||||
})
|
||||
GetTelegraphList("财联社电报").then((res) => {
|
||||
telegraphList.value = res
|
||||
})
|
||||
GetTelegraphList("新浪财经").then((res) => {
|
||||
sinaNewsList.value = res
|
||||
})
|
||||
GetTelegraphList("外媒").then((res) => {
|
||||
foreignNewsList.value = res
|
||||
})
|
||||
getIndex();
|
||||
|
||||
setInterval(() => {
|
||||
industryRank();
|
||||
indexInterval.value = setInterval(() => {
|
||||
getIndex()
|
||||
}, 3000)
|
||||
|
||||
indexIndustryRank.value = setInterval(() => {
|
||||
industryRank()
|
||||
ReFlesh("财联社电报")
|
||||
ReFlesh("新浪财经")
|
||||
ReFlesh("外媒")
|
||||
}, 1000 * 10)
|
||||
|
||||
|
||||
})
|
||||
onMounted(() => {
|
||||
})
|
||||
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
EventsOff("changeMarketTab")
|
||||
EventsOff("newTelegraph")
|
||||
EventsOff("newSinaNews")
|
||||
EventsOff("summaryStockNews")
|
||||
clearInterval(indexInterval.value)
|
||||
clearInterval(indexIndustryRank.value)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
});
|
||||
EventsOn("changeMarketTab", async (msg) => {
|
||||
//message.info(msg.name)
|
||||
console.log(msg.name)
|
||||
updateTab(msg.name)
|
||||
})
|
||||
|
||||
EventsOn("newTelegraph", (data) => {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
telegraphList.value.pop()
|
||||
if (data!=null) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
telegraphList.value.pop()
|
||||
}
|
||||
telegraphList.value.unshift(...data)
|
||||
}
|
||||
telegraphList.value.unshift(...data)
|
||||
})
|
||||
EventsOn("newSinaNews", (data) => {
|
||||
if (data!=null) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
sinaNewsList.value.pop()
|
||||
}
|
||||
sinaNewsList.value.unshift(...data)
|
||||
}
|
||||
})
|
||||
EventsOn("tradingViewNews", (data) => {
|
||||
if (data!=null) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
foreignNewsList.value.pop()
|
||||
}
|
||||
foreignNewsList.value.unshift(...data)
|
||||
}
|
||||
})
|
||||
|
||||
//获取页面高度
|
||||
@@ -102,10 +187,10 @@ window.onresize = () => {
|
||||
panelHeight.value = window.innerHeight - 240
|
||||
}
|
||||
|
||||
function getAreaName(code){
|
||||
function getAreaName(code) {
|
||||
switch (code) {
|
||||
case "america":
|
||||
return "美国"
|
||||
return "美洲"
|
||||
case "europe":
|
||||
return "欧洲"
|
||||
case "asia":
|
||||
@@ -116,19 +201,43 @@ function getAreaName(code){
|
||||
return "其他"
|
||||
}
|
||||
}
|
||||
function reAiSummary(){
|
||||
aiSummary.value=""
|
||||
|
||||
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)
|
||||
SummaryStockNews(question.value,aiConfigId.value, sysPromptId.value,enableTools.value,thinkingMode.value)
|
||||
}
|
||||
function getAiSummary(){
|
||||
|
||||
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
|
||||
if (result.content) {
|
||||
aiSummary.value = result.content
|
||||
question.value = result.question
|
||||
loading.value = false
|
||||
|
||||
const date = new Date(result.CreatedAt);
|
||||
@@ -138,46 +247,47 @@ function getAiSummary(){
|
||||
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)
|
||||
aiSummaryTime.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
modelName.value = result.modelName
|
||||
} else {
|
||||
aiSummaryTime.value = ""
|
||||
aiSummary.value = ""
|
||||
modelName.value = ""
|
||||
//SummaryStockNews(question.value, sysPromptId.value,enableTools.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateTab(name) {
|
||||
summaryBTN.value = (name === "市场快讯");
|
||||
nowTab.value = name
|
||||
}
|
||||
|
||||
EventsOn("summaryStockNews",async (msg) => {
|
||||
EventsOn("summaryStockNews", async (msg) => {
|
||||
loading.value = false
|
||||
////console.log(msg)
|
||||
if (msg === "DONE") {
|
||||
SaveAIResponseResult("市场资讯","市场资讯", aiSummary.value, chatId.value,question.value)
|
||||
await SaveAIResponseResult("市场资讯", "市场资讯", aiSummary.value, chatId.value, question.value,aiConfigId.value)
|
||||
message.info("AI分析完成!")
|
||||
message.destroyAll()
|
||||
|
||||
} else {
|
||||
if(msg.chatId){
|
||||
if (msg.chatId) {
|
||||
chatId.value = msg.chatId
|
||||
}
|
||||
if(msg.question){
|
||||
if (msg.question) {
|
||||
question.value = msg.question
|
||||
}
|
||||
if(msg.content){
|
||||
aiSummary.value =aiSummary.value + msg.content
|
||||
if (msg.content) {
|
||||
aiSummary.value = aiSummary.value + msg.content
|
||||
}
|
||||
if(msg.extraContent){
|
||||
if (msg.extraContent) {
|
||||
aiSummary.value = aiSummary.value + msg.extraContent
|
||||
}
|
||||
if(msg.model){
|
||||
modelName.value=msg.model
|
||||
if (msg.model) {
|
||||
modelName.value = msg.model
|
||||
}
|
||||
if(msg.time){
|
||||
if (msg.time) {
|
||||
aiSummaryTime.value = msg.time
|
||||
}
|
||||
}
|
||||
@@ -191,13 +301,15 @@ async function copyToClipboard() {
|
||||
message.error('复制失败: ' + err);
|
||||
}
|
||||
}
|
||||
function saveAsMarkdown(){
|
||||
SaveAsMarkdown('','市场资讯').then(result => {
|
||||
|
||||
function saveAsMarkdown() {
|
||||
SaveAsMarkdown('市场资讯', '市场资讯').then(result => {
|
||||
message.success(result)
|
||||
})
|
||||
}
|
||||
function share(){
|
||||
ShareAnalysis('市场资讯','市场资讯').then(msg => {
|
||||
|
||||
function share() {
|
||||
ShareAnalysis('市场资讯', '市场资讯').then(msg => {
|
||||
//message.info(msg)
|
||||
notify.info({
|
||||
avatar: () =>
|
||||
@@ -207,32 +319,59 @@ function share(){
|
||||
src: icon.value
|
||||
}),
|
||||
title: '分享到社区',
|
||||
duration:1000*30,
|
||||
duration: 1000 * 30,
|
||||
content: () => {
|
||||
return h('div', {
|
||||
style: {
|
||||
'text-align': 'left',
|
||||
'font-size': '14px',
|
||||
}
|
||||
}, { default: () => msg })
|
||||
}, {default: () => msg})
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function ReFlesh(source) {
|
||||
//console.log("ReFlesh:", source)
|
||||
ReFleshTelegraphList(source).then(res => {
|
||||
if (source === "财联社电报") {
|
||||
telegraphList.value = res
|
||||
}
|
||||
if (source === "新浪财经") {
|
||||
sinaNewsList.value = res
|
||||
}
|
||||
if (source === "外媒") {
|
||||
foreignNewsList.value = res
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card>
|
||||
<n-tabs type="line" animated @update-value="updateTab">
|
||||
<n-tab-pane name="市场快讯" tab="市场快讯" >
|
||||
<n-grid :cols="2" :y-gap="0">
|
||||
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab" style="--wails-draggable:no-drag">
|
||||
<n-tab-pane name="市场快讯" tab="市场快讯">
|
||||
<n-grid :cols="1" :y-gap="0">
|
||||
<n-gi>
|
||||
<news-list :newsList="telegraphList" :header-title="'财联社电报'"></news-list>
|
||||
<AnalyzeMartket :dark-theme="darkTheme" :chart-height="300" :kDays="1" :name="'最近24小时热词'" />
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<news-list :newsList="sinaNewsList" :header-title="'新浪财经'"></news-list>
|
||||
<n-grid :cols="foreignNewsList.length?3:2" :y-gap="0">
|
||||
<n-gi>
|
||||
<news-list :newsList="telegraphList" :header-title="'财联社电报'" @update:message="ReFlesh"></news-list>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<news-list :newsList="sinaNewsList" :header-title="'新浪财经'" @update:message="ReFlesh"></news-list>
|
||||
</n-gi>
|
||||
<n-gi v-if="foreignNewsList.length>0">
|
||||
<news-list :newsList="foreignNewsList" :header-title="'外媒'" @update:message="ReFlesh"></news-list>
|
||||
</n-gi>
|
||||
|
||||
</n-grid>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="全球股指" tab="全球股指">
|
||||
<n-tabs type="segment" animated>
|
||||
@@ -247,11 +386,16 @@ function share(){
|
||||
<n-grid :cols="3" :y-gap="0">
|
||||
<n-gi>
|
||||
|
||||
<n-text :type="item.zdf>0?'error':'success'"><n-image :src="item.img" width="20"/> {{ item.name }}</n-text>
|
||||
<n-text :type="item.zdf>0?'error':'success'">
|
||||
<n-image :src="item.img" width="20"/> {{ item.name }}
|
||||
</n-text>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-text :type="item.zdf>0?'error':'success'">{{ item.zxj }}</n-text>
|
||||
<n-text :type="item.zdf>0?'error':'success'"><n-number-animation :precision="2" :from="0" :to="item.zdf" />%</n-text>
|
||||
<n-text :type="item.zdf>0?'error':'success'">
|
||||
<n-number-animation :precision="2" :from="0" :to="item.zdf"/>
|
||||
%
|
||||
</n-text>
|
||||
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
@@ -267,89 +411,325 @@ function share(){
|
||||
</n-grid>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="上证指数" tab="上证指数">
|
||||
<k-line-chart code="sh000001" :chart-height="panelHeight" name="上证指数" :k-days="20"
|
||||
<k-line-chart code="sh000001" :chart-height="panelHeight" stockName="上证指数" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="深证成指" tab="深证成指">
|
||||
<k-line-chart code="sz399001" :chart-height="panelHeight" name="深证成指" :k-days="20"
|
||||
<k-line-chart code="sz399001" :chart-height="panelHeight" stockName="深证成指" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="创业板指" tab="创业板指">
|
||||
<k-line-chart code="sz399006" :chart-height="panelHeight" name="创业板指" :k-days="20"
|
||||
<k-line-chart code="sz399006" :chart-height="panelHeight" stockName="创业板指" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="恒生指数" tab="恒生指数">
|
||||
<k-line-chart code="hkHSI" :chart-height="panelHeight" name="恒生指数" :k-days="20"
|
||||
<k-line-chart code="hkHSI" :chart-height="panelHeight" stockName="恒生指数" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="纳斯达克" tab="纳斯达克">
|
||||
<k-line-chart code="us.IXIC" :chart-height="panelHeight" name="纳斯达克" :k-days="20"
|
||||
<k-line-chart code="us.IXIC" :chart-height="panelHeight" stockName="纳斯达克" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="道琼斯" tab="道琼斯">
|
||||
<k-line-chart code="us.DJI" :chart-height="panelHeight" name="道琼斯" :k-days="20"
|
||||
<k-line-chart code="us.DJI" :chart-height="panelHeight" stockName="道琼斯" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="标普500" tab="标普500">
|
||||
<k-line-chart code="us.INX" :chart-height="panelHeight" name="标普500" :k-days="20"
|
||||
<k-line-chart code="us.INX" :chart-height="panelHeight" stockName="标普500" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="指标行情" tab="指标行情">
|
||||
<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"
|
||||
<n-tab-pane name="恒生科技指数" tab="恒生科技指数">
|
||||
<k-line-chart code="hkHSTECH" :chart-height="panelHeight" stockName="恒生科技指数" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="科创50" tab="科创50" >
|
||||
<k-line-chart code="sh000688" :chart-height="panelHeight" stockName="科创50" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="科创芯片" tab="科创芯片" >
|
||||
<k-line-chart code="sh000685" :chart-height="panelHeight" stockName="科创芯片" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="证券龙头" tab="证券龙头" >
|
||||
<k-line-chart code="sz399437" :chart-height="panelHeight" stockName="证券龙头" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="高端装备" tab="高端装备" >
|
||||
<k-line-chart code="sz399437" :chart-height="panelHeight" stockName="高端装备" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="中证银行" tab="中证银行">
|
||||
<k-line-chart code="sz399986" :chart-height="panelHeight" stockName="中证银行" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="上证医药" tab="上证医药">
|
||||
<k-line-chart code="sh000037" :chart-height="panelHeight" stockName="上证医药" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="沪深300" tab="沪深300">
|
||||
<k-line-chart code="sh000300" :chart-height="panelHeight" name="沪深300" :k-days="20"
|
||||
<k-line-chart code="sh000300" :chart-height="panelHeight" stockName="沪深300" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="上证50" tab="上证50">
|
||||
<k-line-chart code="sh000016" :chart-height="panelHeight" name="上证50" :k-days="20"
|
||||
<k-line-chart code="sh000016" :chart-height="panelHeight" stockName="上证50" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="中证A500" tab="中证A500">
|
||||
<k-line-chart code="sh000510" :chart-height="panelHeight" name="中证A500" :k-days="20"
|
||||
<k-line-chart code="sh000510" :chart-height="panelHeight" stockName="中证A500" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="中证1000" tab="中证1000">
|
||||
<k-line-chart code="sh000852" :chart-height="panelHeight" name="中证1000" :k-days="20"
|
||||
<k-line-chart code="sh000852" :chart-height="panelHeight" stockName="中证1000" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="中证白酒" tab="中证白酒">
|
||||
<k-line-chart code="sz399997" :chart-height="panelHeight" name="中证白酒" :k-days="20"
|
||||
<k-line-chart code="sz399997" :chart-height="panelHeight" stockName="中证白酒" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="富时中国三倍做多" tab="富时中国三倍做多">
|
||||
<k-line-chart code="usYINN.AM" :chart-height="panelHeight" name="富时中国三倍做多" :k-days="20"
|
||||
<k-line-chart code="usYINN.AM" :chart-height="panelHeight" stockName="富时中国三倍做多" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="VIX恐慌指数" tab="VIX恐慌指数">
|
||||
<k-line-chart code="usUVXY.AM" :chart-height="panelHeight" stockName="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-tab-pane name="名站优选" tab="名站优选">
|
||||
<Stockhotmap />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
<n-modal transform-origin="center" v-model:show="summaryModal" preset="card" style="width: 800px;" :title="'AI市场资讯总结'" >
|
||||
<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"/>
|
||||
<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 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-text type="error">*AI分析结果仅供参考,请以实际行情为准。投资需谨慎,风险自担。</n-text>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template #action>
|
||||
<n-flex justify="left" style="margin-bottom: 10px">
|
||||
<n-switch v-model:value="enableTools" :round="false">
|
||||
<template #checked>
|
||||
启用AI函数工具调用
|
||||
</template>
|
||||
<template #unchecked>
|
||||
不启用AI函数工具调用
|
||||
</template>
|
||||
</n-switch>
|
||||
<n-switch v-model:value="thinkingMode" :round="false">
|
||||
<template #checked>
|
||||
启用思考模式
|
||||
</template>
|
||||
<template #unchecked>
|
||||
不启用思考模式
|
||||
</template>
|
||||
</n-switch>
|
||||
|
||||
|
||||
<n-gradient-text type="error" style="margin-left: 10px">*AI函数工具调用可以增强AI获取数据的能力,但会消耗更多tokens。</n-gradient-text>
|
||||
</n-flex>
|
||||
<n-flex justify="space-between" style="margin-bottom: 10px">
|
||||
<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-select style="width: 32%" v-model:value="aiConfigId" label-field="name" value-field="ID"
|
||||
:options="aiConfigs" placeholder="请选择AI模型服务配置"/>
|
||||
<n-select style="width: 32%" v-model:value="sysPromptId" label-field="name" value-field="ID"
|
||||
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
|
||||
<n-select style="width: 32%" 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
|
||||
<n-input v-model:value="question" style="text-align: left" clearable
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
placeholder="请输入您的问题:例如 总结和分析股票市场新闻中的投资机会"
|
||||
@@ -358,7 +738,7 @@ function share(){
|
||||
maxRows: 5
|
||||
}"
|
||||
/>
|
||||
<n-button size="tiny" type="warning" @click="reAiSummary">再次总结</n-button>
|
||||
<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>
|
||||
@@ -367,14 +747,15 @@ function share(){
|
||||
</n-modal>
|
||||
|
||||
<div style="position: fixed;bottom: 18px;right:25px;z-index: 10;" v-if="summaryBTN">
|
||||
<n-input-group >
|
||||
<n-input-group>
|
||||
<n-button type="primary" @click="getAiSummary">
|
||||
<n-icon :component="PulseOutline"/> AI总结
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
374
frontend/src/components/moneyTrend.vue
Normal file
374
frontend/src/components/moneyTrend.vue
Normal 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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user