mirror of
https://github.com/ArvinLovegood/go-stock.git
synced 2025-07-19 00:00:09 +08:00
Compare commits
169 Commits
v2025.3.20
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
6116e9287a | ||
|
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 | ||
|
3535ba57ab | ||
|
cedff896bb | ||
|
ffc212abc3 | ||
|
7bacbe0d89 | ||
|
6be23d6abc | ||
|
3a74e0ed98 | ||
|
4b0b3c0491 | ||
|
2bd63cf2f4 | ||
|
71d8822d15 | ||
|
db3594af77 | ||
|
c7d728e613 | ||
|
3ca2eed575 | ||
|
8cd55034c3 | ||
|
344c43cbf1 | ||
|
8c49b00057 | ||
|
51cc21107a | ||
|
ece40d1fc0 | ||
|
1a3c8b4fae | ||
|
09d3a16841 | ||
|
65bc8cde47 | ||
|
b45d5dc762 | ||
|
512f9a0757 | ||
|
9e5650617b | ||
|
bac10a2a04 | ||
|
65060a91ce | ||
|
2ae3893325 | ||
|
fdaa80777d | ||
|
5de74f220f | ||
|
c5065b0504 | ||
|
9ebb246e5c | ||
|
5096bfac68 | ||
|
63e898bef8 | ||
|
7af3fe72d5 | ||
|
3402f0d296 | ||
|
51aae0539c | ||
|
7b625e2e80 | ||
|
f1e40e7d3b | ||
|
5f8556cc3d | ||
|
34e2de07fb | ||
|
b186a17a81 | ||
|
95c3909dc9 | ||
|
54b0c7ccb3 | ||
|
e44bc55301 | ||
|
fd3046b2c3 | ||
|
2b41dc11c1 | ||
|
076dc4f9ef | ||
|
1a728672c8 | ||
|
c8178a6c5f | ||
|
9d546fd214 | ||
|
d467adbdec | ||
|
c08776d028 | ||
|
c3c770b2ed | ||
|
63a05954f8 | ||
|
98c81107fc | ||
|
fb862564e1 | ||
|
c0bad34e36 | ||
|
f7a2681157 | ||
|
ee5c47f2dc |
6
.github/workflows/main.yml
vendored
6
.github/workflows/main.yml
vendored
@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
tags:
|
||||
# Match any new tag
|
||||
- '*'
|
||||
- '*-release'
|
||||
|
||||
env:
|
||||
# Necessary for most environments as build failure can occur due to OOM issues
|
||||
@ -44,7 +44,7 @@ jobs:
|
||||
build-name: ${{ matrix.build.name }}
|
||||
build-platform: ${{ matrix.build.platform }}
|
||||
package: true
|
||||
go-version: '1.23'
|
||||
go-version: '1.24'
|
||||
build-tags: ${{ github.ref_name }}
|
||||
build-commit-message: ${{ steps.get_commit_message.outputs.commit_message }}
|
||||
node-version: '18.x'
|
||||
node-version: '20.x'
|
||||
|
83
README.md
83
README.md
@ -1,21 +1,17 @@
|
||||
# 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))
|
||||
|
||||
### 🌟公众号
|
||||

|
||||
|
||||
### 🎁 推广链接
|
||||
开源不易,有需要的朋友可以看看,小破服务器挣个续费钱。😘
|
||||
- [AI大模型设置说明](https://mp.weixin.qq.com/s/O8-nRQC2JoUdqQ5jaMQHjA)
|
||||
- [野草云](https://my.yecaoyun.com/aff.php?aff=5743): 高速、稳定的香港/美国VPS服务器,仅22元/月起!
|
||||
|
||||
### 📈 交流群
|
||||
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大模型构建的股票分析工具。
|
||||
@ -30,34 +26,63 @@ QQ交流群:[点击链接加入群聊【go-stock交流群】:491605333](http
|
||||
|
||||
|
||||
### 💬 支持大模型/平台
|
||||
| 模型 | 状态 | 备注 |
|
||||
| --- | --- |-----------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
|
||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
|
||||
| [AnythingLLM](https://anythingllm.com/) | ✅ | 本地知识库 |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | deepseek-reasoner模型测试有问题,可通过本地模型或聚合模型平台使用 |
|
||||
| [大模型聚合平台](https://cloud.siliconflow.cn/i/foufCerk) | ✅ | 如:[硅基流动](https://cloud.siliconflow.cn/i/foufCerk),[火山方舟](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ) |
|
||||
| 模型 | 状态 | 备注 |
|
||||
| --- | --- |---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
|
||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
|
||||
| [AnythingLLM](https://anythingllm.com/) | ✅ | 本地知识库 |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | deepseek-reasoner,deepseek-chat |
|
||||
| [大模型聚合平台](https://cloud.siliconflow.cn/i/foufCerk) | ✅ | 如:[硅基流动](https://cloud.siliconflow.cn/i/foufCerk),[火山方舟](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ) ,[优云智算](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock) |
|
||||
|
||||
### <span style="color: #568DF4;">各位亲爱的朋友们,如果您对这个项目感兴趣,请先给我一个<i style="color: #EA2626;">star</i>吧,谢谢!</span>💕
|
||||
- 优云智算(by UCloud):万卡规模4090免费用10小时,新人注册另增50万tokens,海量热门源项目镜像一键部署,[注册链接](https://www.compshare.cn/image-community?ytag=GPU_YY-gh_gostock)
|
||||
- 经测试目前硅基流动(siliconflow)提供的deepSeek api 服务比较稳定,注册即送2000万Tokens,[注册链接](https://cloud.siliconflow.cn/i/foufCerk)
|
||||
- 火山方舟:每个模型注册即送50万tokens,[注册链接](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=IJSE43PZ)
|
||||
- Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究,[注册链接](https://tushare.pro/register?reg=701944)
|
||||
- Tushare大数据开放社区,免费提供各类金融数据,助力行业和量化研究(注意:Tushare只需要120积分即可,注册完成个人资料补充即可得120积分!!!),[注册链接](https://tushare.pro/register?reg=701944)
|
||||
- 软件快速迭代开发中,请大家优先测试和使用最新发布的版本。
|
||||
- 欢迎大家提出宝贵的建议,欢迎提issue,PR。当然更欢迎[赞助我](#都划到这了如果我的项目对您有帮助请赞助我吧)。💕
|
||||
|
||||
|
||||
## 🧩 重大功能开发计划
|
||||
| 功能说明 | 状态 | 备注 |
|
||||
|-----------------|----|--------------------------------------------------------------------------------------------------------|
|
||||
| ETF支持 | 🚧 | ETF数据支持 |
|
||||
| 美股支持 | ✅ | 美股数据支持 |
|
||||
| 港股支持 | ✅ | 港股数据支持 (目前有延迟) |
|
||||
| 多轮对话 | ✅ | 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浏览器抓取新闻资讯 |
|
||||
| 不再强制依赖Chrome浏览器 | ✅ | 默认使用edge浏览器抓取新闻资讯 |
|
||||
|
||||
## 👀 更新日志
|
||||
### 2025.06.30 添加指标选股功能
|
||||
### 2025.06.27 添加财经日历和重大事件时间轴功能
|
||||
### 2025.06.25 添加热门股票、事件和话题功能
|
||||
### 2025.06.18 更新内置股票基础数据,软件内实时市场资讯信息提醒,添加行业研究功能
|
||||
### 2025.06.15 添加公司公告信息搜索/查看功能
|
||||
### 2025.06.15 添加个股研报到弹出菜单
|
||||
### 2025.06.13 添加个股研报功能
|
||||
### 2025.06.12 添加龙虎榜功能,新增行业排名分类
|
||||
### 2025.05.30 优化股票分时图显示
|
||||
### 2025.05.20 修复财联社电报获取问题
|
||||
### 2025.05.16 优化资金趋势图表组件
|
||||
### 2025.05.15 重构应用加载和数据初始化逻辑,添加股票资金趋势功能,资金趋势图表增加主力当日净流入数据并优化展示效果
|
||||
### 2025.05.14 添加个股资金流向功能,排行榜增加股票行情K线图弹窗
|
||||
### 2025.05.13 添加行业排名功能
|
||||
### 2025.05.09 添加A股盘口数据解析和展示功能
|
||||
### 2025.05.07 优化分时图的展示
|
||||
### 2025.04.29 补全港股/美股基础数据,优化港股股价延迟问题,优化初始化逻辑
|
||||
### 2025.04.25 市场资讯支持AI分析和总结:让AI帮你读市场!
|
||||
### 2025.04.24 新增市场行情模块:即时掌握全球市场行情资讯/动态,从此再也不用偷摸去各大财经网站啦。go-stock一键帮你搞定!
|
||||
### 2025.04.22 优化K线图展示,支持拉伸放大,看得更舒服啦!
|
||||
### 2025.04.21 港股,美股K线数据获取优化
|
||||
### 2025.04.01 优化部分设置选项,避免重启软件
|
||||
### 2025.03.31 优化数据爬取
|
||||
### 2025.03.30 AI自动定时分析功能
|
||||
### 2025.03.29 多提示词模板管理,AI分析时支持选择不同提示词模板
|
||||
### 2025.03.28 AI分析结果保存为markdown文件时,支持保存位置目录选择
|
||||
### 2025.03.15 自定义爬虫使用的浏览器路径配置
|
||||
### 2025.03.14 优化编译构建,大幅减少编译后的程序文件大小
|
||||
### 2025.03.09 基金估值和净值监控查看
|
||||
@ -75,6 +100,12 @@ QQ交流群:[点击链接加入群聊【go-stock交流群】:491605333](http
|
||||
|
||||
## 🦄 重大更新
|
||||
### BIG NEWS !!! 重大更新!!!
|
||||
- 2025.04.25 市场资讯支持AI分析和总结:让AI帮你读市场!
|
||||

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

|
||||

|
||||
- 
|
||||
- 2025.01.17 新增AI大模型分析股票功能
|
||||

|
||||
## 📸 功能截图
|
||||
@ -84,7 +115,7 @@ QQ交流群:[点击链接加入群聊【go-stock交流群】:491605333](http
|
||||
### 成本设置
|
||||

|
||||
### 日K
|
||||

|
||||

|
||||
### 分时
|
||||

|
||||
### 钉钉报警通知
|
||||
|
532
app.go
532
app.go
@ -5,39 +5,49 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/mathutil"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/getlantern/systray"
|
||||
"github.com/energye/systray"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/go-toast/toast"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
cache *freecache.Cache
|
||||
ctx context.Context
|
||||
cache *freecache.Cache
|
||||
cron *cron.Cron
|
||||
cronEntrys map[string]cron.EntryID
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
cacheSize := 512 * 1024
|
||||
cache := freecache.NewCache(cacheSize)
|
||||
c := cron.New(cron.WithSeconds())
|
||||
c.Start()
|
||||
return &App{
|
||||
cache: cache,
|
||||
cache: cache,
|
||||
cron: c,
|
||||
cronEntrys: make(map[string]cron.EntryID),
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,12 +62,47 @@ func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
|
||||
// 创建系统托盘
|
||||
systray.Run(func() {
|
||||
go onReady(a)
|
||||
//systray.RunWithExternalLoop(func() {
|
||||
// onReady(a)
|
||||
//}, func() {
|
||||
// onExit(a)
|
||||
//})
|
||||
runtime.EventsOn(ctx, "updateSettings", func(optionalData ...interface{}) {
|
||||
logger.SugaredLogger.Infof("updateSettings : %v\n", optionalData)
|
||||
config := &data.Settings{}
|
||||
setMap := optionalData[0].(map[string]interface{})
|
||||
|
||||
// 将 map 转换为 JSON 字节切片
|
||||
jsonData, err := json.Marshal(setMap)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
// 将 JSON 字节切片解析到结构体中
|
||||
err = json.Unmarshal(jsonData, config)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("updateSettings config:%+v", config)
|
||||
if config.DarkTheme {
|
||||
runtime.WindowSetBackgroundColour(ctx, 27, 38, 54, 1)
|
||||
runtime.WindowSetDarkTheme(ctx)
|
||||
} else {
|
||||
runtime.WindowSetBackgroundColour(ctx, 255, 255, 255, 1)
|
||||
runtime.WindowSetLightTheme(ctx)
|
||||
}
|
||||
runtime.WindowReloadApp(ctx)
|
||||
|
||||
})
|
||||
go systray.Run(func() {
|
||||
onReady(a)
|
||||
}, func() {
|
||||
go onExit(a)
|
||||
onExit(a)
|
||||
})
|
||||
|
||||
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
|
||||
}
|
||||
|
||||
func (a *App) CheckUpdate() {
|
||||
@ -94,48 +139,130 @@ func (a *App) CheckUpdate() {
|
||||
func (a *App) domReady(ctx context.Context) {
|
||||
defer PanicHandler()
|
||||
|
||||
if stocksBin != nil && len(stocksBin) > 0 {
|
||||
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查A股基础信息...")
|
||||
go initStockData(a.ctx)
|
||||
}
|
||||
|
||||
if stocksBinHK != nil && len(stocksBinHK) > 0 {
|
||||
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查港股基础信息...")
|
||||
go initStockDataHK(a.ctx)
|
||||
}
|
||||
|
||||
if stocksBinUS != nil && len(stocksBinUS) > 0 {
|
||||
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查美股基础信息...")
|
||||
go initStockDataUS(a.ctx)
|
||||
}
|
||||
updateBasicInfo()
|
||||
|
||||
// Add your action here
|
||||
//定时更新数据
|
||||
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
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 {
|
||||
//ticker := time.NewTicker(time.Second * time.Duration(interval))
|
||||
//defer ticker.Stop()
|
||||
//for range ticker.C {
|
||||
// MonitorStockPrices(a)
|
||||
//}
|
||||
id, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", interval), func() {
|
||||
MonitorStockPrices(a)
|
||||
})
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
|
||||
} else {
|
||||
a.cronEntrys["MonitorStockPrices"] = id
|
||||
}
|
||||
entryID, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", interval+10), func() {
|
||||
news := data.NewMarketNewsApi().GetNewTelegraph(30)
|
||||
if config.EnablePushNews {
|
||||
go a.NewsPush(news)
|
||||
}
|
||||
go runtime.EventsEmit(a.ctx, "newTelegraph", news)
|
||||
})
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
|
||||
} else {
|
||||
a.cronEntrys["GetNewTelegraph"] = entryID
|
||||
}
|
||||
|
||||
entryIDSina, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", interval+10), func() {
|
||||
news := data.NewMarketNewsApi().GetSinaNews(30)
|
||||
if config.EnablePushNews {
|
||||
go a.NewsPush(news)
|
||||
}
|
||||
go runtime.EventsEmit(a.ctx, "newSinaNews", news)
|
||||
})
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
|
||||
} else {
|
||||
a.cronEntrys["newSinaNews"] = entryIDSina
|
||||
}
|
||||
}()
|
||||
|
||||
//刷新基金净值信息
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * time.Duration(60))
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
MonitorFundPrices(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)
|
||||
//ticker := time.NewTicker(time.Second * time.Duration(60))
|
||||
//defer ticker.Stop()
|
||||
//for range ticker.C {
|
||||
// MonitorFundPrices(a)
|
||||
//}
|
||||
if config.EnableFund {
|
||||
id, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", 60), func() {
|
||||
MonitorFundPrices(a)
|
||||
})
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
|
||||
} else {
|
||||
a.cronEntrys["MonitorFundPrices"] = id
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
|
||||
|
||||
if config.EnableNews {
|
||||
//go func() {
|
||||
// ticker := time.NewTicker(time.Second * time.Duration(60))
|
||||
// defer ticker.Stop()
|
||||
// for range ticker.C {
|
||||
// telegraph := refreshTelegraphList()
|
||||
// if telegraph != nil {
|
||||
// go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}()
|
||||
|
||||
id, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", 60), func() {
|
||||
telegraph := refreshTelegraphList()
|
||||
if telegraph != nil {
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
|
||||
} else {
|
||||
a.cronEntrys["refreshTelegraphList"] = id
|
||||
}
|
||||
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
|
||||
}
|
||||
go MonitorStockPrices(a)
|
||||
go MonitorFundPrices(a)
|
||||
go data.NewFundApi().AllFund()
|
||||
if config.EnableFund {
|
||||
go MonitorFundPrices(a)
|
||||
go data.NewFundApi().AllFund()
|
||||
}
|
||||
//检查新版本
|
||||
go func() {
|
||||
a.CheckUpdate()
|
||||
a.cron.AddFunc("30 05 8,12,20 * * *", func() {
|
||||
logger.SugaredLogger.Errorf("Checking for updates...")
|
||||
a.CheckUpdate()
|
||||
})
|
||||
|
||||
}()
|
||||
|
||||
//检查谷歌浏览器
|
||||
@ -155,6 +282,58 @@ func (a *App) domReady(ctx context.Context) {
|
||||
// logger.SugaredLogger.Infof("Edge浏览器已安装,路径为: %s", path)
|
||||
// }
|
||||
//}()
|
||||
followList := data.NewStockDataApi().GetFollowList(0)
|
||||
for _, follow := range *followList {
|
||||
if follow.Cron == nil || *follow.Cron == "" {
|
||||
continue
|
||||
}
|
||||
entryID, err := a.cron.AddFunc(*follow.Cron, a.AddCronTask(follow))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("添加自动分析任务失败:%s cron=%s entryID:%v", follow.Name, *follow.Cron, entryID)
|
||||
continue
|
||||
}
|
||||
a.cronEntrys[follow.StockCode] = entryID
|
||||
}
|
||||
logger.SugaredLogger.Infof("domReady-cronEntrys:%+v", a.cronEntrys)
|
||||
|
||||
}
|
||||
|
||||
func (a *App) NewsPush(news *[]models.Telegraph) {
|
||||
for _, telegraph := range *news {
|
||||
//if telegraph.IsRed {
|
||||
go runtime.EventsEmit(a.ctx, "newsPush", telegraph)
|
||||
go data.NewAlertWindowsApi("go-stock", telegraph.Source+" "+telegraph.Time, telegraph.Content, string(icon)).SendNotification()
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) AddCronTask(follow data.FollowedStock) func() {
|
||||
return func() {
|
||||
go runtime.EventsEmit(a.ctx, "warnMsg", "开始自动分析"+follow.Name+"_"+follow.StockCode)
|
||||
ai := data.NewDeepSeekOpenAi(a.ctx)
|
||||
msgs := ai.NewChatStream(follow.Name, follow.StockCode, "", nil)
|
||||
var res strings.Builder
|
||||
|
||||
chatId := ""
|
||||
question := ""
|
||||
for msg := range msgs {
|
||||
if msg["extraContent"] != nil {
|
||||
res.WriteString(msg["extraContent"].(string) + "\n")
|
||||
}
|
||||
if msg["content"] != nil {
|
||||
res.WriteString(msg["content"].(string))
|
||||
}
|
||||
if msg["chatId"] != nil {
|
||||
chatId = msg["chatId"].(string)
|
||||
}
|
||||
if msg["question"] != nil {
|
||||
question = msg["question"].(string)
|
||||
}
|
||||
}
|
||||
data.NewDeepSeekOpenAi(a.ctx).SaveAIResponseResult(follow.StockCode, follow.Name, res.String(), chatId, question)
|
||||
go runtime.EventsEmit(a.ctx, "warnMsg", "AI分析完成:"+follow.Name+"_"+follow.StockCode)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func refreshTelegraphList() *[]string {
|
||||
@ -316,11 +495,13 @@ func MonitorStockPrices(a *App) {
|
||||
}
|
||||
|
||||
total += stockInfo.ProfitAmountToday
|
||||
//price, _ := convertor.ToFloat(stockInfo.Price)
|
||||
//if stockInfo.PrePrice != price {
|
||||
//logger.SugaredLogger.Infof("-----------------------股票代码: %s, 股票名称: %s, 股票价格: %s,盘前盘后:%s", stockInfo.Code, stockInfo.Name, stockInfo.Price, stockInfo.BA)
|
||||
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
|
||||
//}
|
||||
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)
|
||||
@ -380,6 +561,7 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
|
||||
stockData.CostVolume = follow.Volume //成本量
|
||||
stockData.AlarmChangePercent = follow.AlarmChangePercent
|
||||
stockData.AlarmPrice = follow.AlarmPrice
|
||||
stockData.Groups = follow.Groups
|
||||
|
||||
//当前价格
|
||||
price, _ := convertor.ToFloat(stockData.Price)
|
||||
@ -433,7 +615,8 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
|
||||
//未开盘时当前价格为昨日收盘价
|
||||
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
|
||||
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
|
||||
stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
|
||||
// 未开盘时,今日盈亏为 0
|
||||
stockData.ProfitAmountToday = 0
|
||||
}
|
||||
|
||||
}
|
||||
@ -468,15 +651,19 @@ func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||
logger.SugaredLogger.Debugf("dialog:%s", dialog)
|
||||
if dialog == "No" {
|
||||
return true
|
||||
} else {
|
||||
systray.Quit()
|
||||
a.cron.Stop()
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// shutdown is called at application termination
|
||||
func (a *App) shutdown(ctx context.Context) {
|
||||
defer PanicHandler()
|
||||
// Perform your teardown here
|
||||
systray.Quit()
|
||||
//os.Exit(0)
|
||||
logger.SugaredLogger.Infof("application shutdown Version:%s", Version)
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
@ -486,7 +673,7 @@ func (a *App) Greet(stockCode string) *data.StockInfo {
|
||||
follow := &data.FollowedStock{
|
||||
StockCode: stockCode,
|
||||
}
|
||||
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
|
||||
db.Dao.Model(follow).Where("stock_code = ?", stockCode).Preload("Groups").Preload("Groups.GroupInfo").First(follow)
|
||||
stockInfo := getStockInfo(*follow)
|
||||
return stockInfo
|
||||
}
|
||||
@ -499,8 +686,8 @@ func (a *App) UnFollow(stockCode string) string {
|
||||
return data.NewStockDataApi().UnFollow(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) GetFollowList() *[]data.FollowedStock {
|
||||
return data.NewStockDataApi().GetFollowList()
|
||||
func (a *App) GetFollowList(groupId int) *[]data.FollowedStock {
|
||||
return data.NewStockDataApi().GetFollowList(groupId)
|
||||
}
|
||||
|
||||
func (a *App) GetStockList(key string) []data.StockBasic {
|
||||
@ -560,8 +747,8 @@ func (a *App) SendDingDingMessageByType(message string, stockCode string, msgTyp
|
||||
return data.NewDingDingAPI().SendDingDingMessage(message)
|
||||
}
|
||||
|
||||
func (a *App) NewChatStream(stock, stockCode, question string) {
|
||||
msgs := data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question)
|
||||
func (a *App) NewChatStream(stock, stockCode, question string, sysPromptId *int) {
|
||||
msgs := data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId)
|
||||
for msg := range msgs {
|
||||
runtime.EventsEmit(a.ctx, "newChatStream", msg)
|
||||
}
|
||||
@ -670,46 +857,62 @@ func getMsgTypeName(msgType int) string {
|
||||
|
||||
func onExit(a *App) {
|
||||
// 清理操作
|
||||
logger.SugaredLogger.Infof("onExit")
|
||||
runtime.Quit(a.ctx)
|
||||
logger.SugaredLogger.Infof("systray onExit")
|
||||
//systray.Quit()
|
||||
//runtime.Quit(a.ctx)
|
||||
}
|
||||
|
||||
func onReady(a *App) {
|
||||
|
||||
// 初始化操作
|
||||
logger.SugaredLogger.Infof("onReady")
|
||||
logger.SugaredLogger.Infof("systray onReady")
|
||||
systray.SetIcon(icon2)
|
||||
systray.SetTitle("go-stock")
|
||||
systray.SetTooltip("go-stock 股票行情实时获取")
|
||||
// 创建菜单项
|
||||
show := systray.AddMenuItem("显示", "显示应用程序")
|
||||
show.Click(func() {
|
||||
//logger.SugaredLogger.Infof("显示应用程序")
|
||||
runtime.WindowShow(a.ctx)
|
||||
})
|
||||
hide := systray.AddMenuItem("隐藏", "隐藏应用程序")
|
||||
hide.Click(func() {
|
||||
//logger.SugaredLogger.Infof("隐藏应用程序")
|
||||
runtime.WindowHide(a.ctx)
|
||||
})
|
||||
systray.AddSeparator()
|
||||
mQuitOrig := systray.AddMenuItem("退出", "退出应用程序")
|
||||
|
||||
// 监听菜单项点击事件
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-mQuitOrig.ClickedCh:
|
||||
logger.SugaredLogger.Infof("退出应用程序")
|
||||
runtime.Quit(a.ctx)
|
||||
//systray.Quit()
|
||||
case <-show.ClickedCh:
|
||||
logger.SugaredLogger.Infof("显示应用程序")
|
||||
runtime.WindowShow(a.ctx)
|
||||
//runtime.WindowShow(a.ctx)
|
||||
case <-hide.ClickedCh:
|
||||
logger.SugaredLogger.Infof("隐藏应用程序")
|
||||
runtime.WindowHide(a.ctx)
|
||||
|
||||
}
|
||||
}
|
||||
}()
|
||||
mQuitOrig.Click(func() {
|
||||
//logger.SugaredLogger.Infof("退出应用程序")
|
||||
runtime.Quit(a.ctx)
|
||||
})
|
||||
systray.SetOnRClick(func(menu systray.IMenu) {
|
||||
menu.ShowMenu()
|
||||
//logger.SugaredLogger.Infof("SetOnRClick")
|
||||
})
|
||||
systray.SetOnClick(func(menu systray.IMenu) {
|
||||
//logger.SugaredLogger.Infof("SetOnClick")
|
||||
menu.ShowMenu()
|
||||
})
|
||||
systray.SetOnDClick(func(menu systray.IMenu) {
|
||||
menu.ShowMenu()
|
||||
//logger.SugaredLogger.Infof("SetOnDClick")
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) UpdateConfig(settings *data.Settings) string {
|
||||
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
|
||||
//logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
|
||||
if settings.RefreshInterval > 0 {
|
||||
if entryID, exists := a.cronEntrys["MonitorStockPrices"]; exists {
|
||||
a.cron.Remove(entryID)
|
||||
}
|
||||
id, _ := a.cron.AddFunc(fmt.Sprintf("@every %ds", settings.RefreshInterval), func() {
|
||||
//logger.SugaredLogger.Infof("MonitorStockPrices:%s", time.Now())
|
||||
MonitorStockPrices(a)
|
||||
})
|
||||
a.cronEntrys["MonitorStockPrices"] = id
|
||||
}
|
||||
|
||||
return data.NewSettingsApi(settings).UpdateConfig()
|
||||
}
|
||||
|
||||
@ -736,13 +939,13 @@ func (a *App) ExportConfig() string {
|
||||
return "导出成功:" + file
|
||||
}
|
||||
func getScreenResolution() (int, int, error) {
|
||||
user32 := syscall.NewLazyDLL("user32.dll")
|
||||
getSystemMetrics := user32.NewProc("GetSystemMetrics")
|
||||
//user32 := syscall.NewLazyDLL("user32.dll")
|
||||
//getSystemMetrics := user32.NewProc("GetSystemMetrics")
|
||||
//
|
||||
//width, _, _ := getSystemMetrics.Call(0)
|
||||
//height, _, _ := getSystemMetrics.Call(1)
|
||||
|
||||
width, _, _ := getSystemMetrics.Call(0)
|
||||
height, _, _ := getSystemMetrics.Call(1)
|
||||
|
||||
return int(width), int(height), nil
|
||||
return int(1366), int(768), nil
|
||||
}
|
||||
|
||||
func (a *App) ShareAnalysis(stockCode, stockName string) string {
|
||||
@ -778,3 +981,174 @@ func (a *App) FollowFund(fundCode string) string {
|
||||
func (a *App) UnFollowFund(fundCode string) string {
|
||||
return data.NewFundApi().UnFollowFund(fundCode)
|
||||
}
|
||||
func (a *App) SaveAsMarkdown(stockCode, stockName string) string {
|
||||
res := data.NewDeepSeekOpenAi(a.ctx).GetAIResponseResult(stockCode)
|
||||
if res != nil && len(res.Content) > 100 {
|
||||
analysisTime := res.CreatedAt.Format("2006-01-02_15_04_05")
|
||||
file, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
Title: "保存为Markdown",
|
||||
DefaultFilename: fmt.Sprintf("%s[%s]AI分析结果_%s.md", stockName, stockCode, analysisTime),
|
||||
Filters: []runtime.FileFilter{
|
||||
{
|
||||
DisplayName: "Markdown",
|
||||
Pattern: "*.md;*.markdown",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
err = os.WriteFile(file, []byte(res.Content), 0644)
|
||||
return "已保存至:" + file
|
||||
}
|
||||
return "分析结果异常,无法保存。"
|
||||
}
|
||||
|
||||
func (a *App) GetPromptTemplates(name, promptType string) *[]models.PromptTemplate {
|
||||
return data.NewPromptTemplateApi().GetPromptTemplates(name, promptType)
|
||||
}
|
||||
func (a *App) AddPrompt(prompt models.Prompt) string {
|
||||
promptTemplate := models.PromptTemplate{
|
||||
ID: prompt.ID,
|
||||
Content: prompt.Content,
|
||||
Name: prompt.Name,
|
||||
Type: prompt.Type,
|
||||
}
|
||||
return data.NewPromptTemplateApi().AddPrompt(promptTemplate)
|
||||
}
|
||||
func (a *App) DelPrompt(id uint) string {
|
||||
return data.NewPromptTemplateApi().DelPrompt(id)
|
||||
}
|
||||
func (a *App) SetStockAICron(cronText, stockCode string) {
|
||||
data.NewStockDataApi().SetStockAICron(cronText, stockCode)
|
||||
if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
|
||||
stockCode = strings.ToUpper(stockCode)
|
||||
stockCode = strings.Replace(stockCode, "gb_", "us", 1)
|
||||
stockCode = strings.Replace(stockCode, "GB_", "us", 1)
|
||||
}
|
||||
if entryID, exists := a.cronEntrys[stockCode]; exists {
|
||||
a.cron.Remove(entryID)
|
||||
}
|
||||
follow := data.NewStockDataApi().GetFollowedStockByStockCode(stockCode)
|
||||
id, _ := a.cron.AddFunc(cronText, a.AddCronTask(follow))
|
||||
a.cronEntrys[stockCode] = id
|
||||
|
||||
}
|
||||
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
|
||||
notification := toast.Notification{
|
||||
AppID: "go-stock",
|
||||
Title: "go-stock",
|
||||
Message: "程序已经在运行了",
|
||||
Icon: "",
|
||||
Duration: "short",
|
||||
Audio: toast.Default,
|
||||
}
|
||||
err := notification.Push()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
}
|
||||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
|
||||
func (a *App) AddGroup(group data.Group) string {
|
||||
ok := data.NewStockGroupApi(db.Dao).AddGroup(group)
|
||||
if ok {
|
||||
return "添加成功"
|
||||
} else {
|
||||
return "添加失败"
|
||||
}
|
||||
}
|
||||
func (a *App) GetGroupList() []data.Group {
|
||||
return data.NewStockGroupApi(db.Dao).GetGroupList()
|
||||
}
|
||||
|
||||
func (a *App) GetGroupStockList(groupId int) []data.GroupStock {
|
||||
return data.NewStockGroupApi(db.Dao).GetGroupStockByGroupId(groupId)
|
||||
}
|
||||
|
||||
func (a *App) AddStockGroup(groupId int, stockCode string) string {
|
||||
ok := data.NewStockGroupApi(db.Dao).AddStockGroup(groupId, stockCode)
|
||||
if ok {
|
||||
return "添加成功"
|
||||
} else {
|
||||
return "添加失败"
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) RemoveStockGroup(code, name string, groupId int) string {
|
||||
ok := data.NewStockGroupApi(db.Dao).RemoveStockGroup(code, name, groupId)
|
||||
if ok {
|
||||
return "移除成功"
|
||||
} else {
|
||||
return "移除失败"
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) RemoveGroup(groupId int) string {
|
||||
ok := data.NewStockGroupApi(db.Dao).RemoveGroup(groupId)
|
||||
if ok {
|
||||
return "移除成功"
|
||||
} else {
|
||||
return "移除失败"
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) GetStockKLine(stockCode, stockName string, days int64) *[]data.KLineData {
|
||||
return data.NewStockDataApi().GetHK_KLineData(stockCode, "day", days)
|
||||
}
|
||||
|
||||
func (a *App) GetStockMinutePriceLineData(stockCode, stockName string) map[string]any {
|
||||
res := make(map[string]any, 4)
|
||||
priceData, date := data.NewStockDataApi().GetStockMinutePriceData(stockCode)
|
||||
res["priceData"] = priceData
|
||||
res["date"] = date
|
||||
res["stockName"] = stockName
|
||||
res["stockCode"] = stockCode
|
||||
return res
|
||||
}
|
||||
|
||||
func (a *App) GetStockCommonKLine(stockCode, stockName string, days int64) *[]data.KLineData {
|
||||
return data.NewStockDataApi().GetCommonKLineData(stockCode, "day", days)
|
||||
}
|
||||
|
||||
func (a *App) GetTelegraphList(source string) *[]*models.Telegraph {
|
||||
telegraphs := data.NewMarketNewsApi().GetTelegraphList(source)
|
||||
return telegraphs
|
||||
}
|
||||
|
||||
func (a *App) ReFleshTelegraphList(source string) *[]*models.Telegraph {
|
||||
data.NewMarketNewsApi().GetNewTelegraph(30)
|
||||
data.NewMarketNewsApi().GetSinaNews(30)
|
||||
telegraphs := data.NewMarketNewsApi().GetTelegraphList(source)
|
||||
return telegraphs
|
||||
}
|
||||
|
||||
func (a *App) GlobalStockIndexes() map[string]any {
|
||||
return data.NewMarketNewsApi().GlobalStockIndexes(30)
|
||||
}
|
||||
|
||||
func (a *App) SummaryStockNews(question string, sysPromptId *int) {
|
||||
msgs := data.NewDeepSeekOpenAi(a.ctx).NewSummaryStockNewsStream(question, sysPromptId)
|
||||
for msg := range msgs {
|
||||
runtime.EventsEmit(a.ctx, "summaryStockNews", msg)
|
||||
}
|
||||
runtime.EventsEmit(a.ctx, "summaryStockNews", "DONE")
|
||||
}
|
||||
func (a *App) GetIndustryRank(sort string, cnt int) []any {
|
||||
res := data.NewMarketNewsApi().GetIndustryRank(sort, cnt)
|
||||
return res["data"].([]any)
|
||||
}
|
||||
func (a *App) GetIndustryMoneyRankSina(fenlei, sort string) []map[string]any {
|
||||
res := data.NewMarketNewsApi().GetIndustryMoneyRankSina(fenlei, sort)
|
||||
return res
|
||||
}
|
||||
func (a *App) GetMoneyRankSina(sort string) []map[string]any {
|
||||
res := data.NewMarketNewsApi().GetMoneyRankSina(sort)
|
||||
return res
|
||||
}
|
||||
|
||||
func (a *App) GetStockMoneyTrendByDay(stockCode string, days int) []map[string]any {
|
||||
res := data.NewMarketNewsApi().GetStockMoneyTrendByDay(stockCode, days)
|
||||
slice.Reverse(res)
|
||||
return res
|
||||
}
|
||||
|
61
app_common.go
Normal file
61
app_common.go
Normal file
@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/models"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/6/8 20:45
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func (a *App) LongTigerRank(date string) *[]models.LongTigerRankData {
|
||||
return data.NewMarketNewsApi().LongTiger(date)
|
||||
}
|
||||
|
||||
func (a *App) StockResearchReport(stockCode string) []any {
|
||||
return data.NewMarketNewsApi().StockResearchReport(stockCode, 7)
|
||||
}
|
||||
func (a *App) StockNotice(stockCode string) []any {
|
||||
return data.NewMarketNewsApi().StockNotice(stockCode)
|
||||
}
|
||||
|
||||
func (a *App) IndustryResearchReport(industryCode string) []any {
|
||||
return data.NewMarketNewsApi().IndustryResearchReport(industryCode, 7)
|
||||
}
|
||||
func (a App) EMDictCode(code string) []any {
|
||||
return data.NewMarketNewsApi().EMDictCode(code, a.cache)
|
||||
}
|
||||
|
||||
func (a App) AnalyzeSentiment(text string) data.SentimentResult {
|
||||
return data.AnalyzeSentiment(text)
|
||||
}
|
||||
|
||||
func (a App) HotStock(marketType string) *[]models.HotItem {
|
||||
return data.NewMarketNewsApi().XUEQIUHotStock(100, marketType)
|
||||
}
|
||||
|
||||
func (a App) HotEvent(size int) *[]models.HotEvent {
|
||||
if size <= 0 {
|
||||
size = 10
|
||||
}
|
||||
return data.NewMarketNewsApi().HotEvent(size)
|
||||
}
|
||||
func (a App) HotTopic(size int) []any {
|
||||
if size <= 0 {
|
||||
size = 10
|
||||
}
|
||||
return data.NewMarketNewsApi().HotTopic(size)
|
||||
}
|
||||
|
||||
func (a App) InvestCalendarTimeLine(yearMonth string) []any {
|
||||
return data.NewMarketNewsApi().InvestCalendar(yearMonth)
|
||||
}
|
||||
func (a App) ClsCalendar() []any {
|
||||
return data.NewMarketNewsApi().ClsCalendar()
|
||||
}
|
||||
|
||||
func (a App) SearchStock(words string) map[string]any {
|
||||
return data.NewSearchStockApi(words).SearchStock()
|
||||
}
|
@ -279,7 +279,8 @@ func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
|
||||
//未开盘时当前价格为昨日收盘价
|
||||
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
|
||||
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
|
||||
stockData.ProfitAmountToday = mathutil.RoundToFloat((preClosePrice-preClosePrice)*float64(follow.Volume), 2)
|
||||
// 未开盘时,今日盈亏为 0
|
||||
stockData.ProfitAmountToday = 0
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string)
|
||||
}
|
||||
|
||||
func (a AlertWindowsApi) SendNotification() bool {
|
||||
if getConfig().LocalPushEnable == false {
|
||||
if GetConfig().LocalPushEnable == false {
|
||||
logger.SugaredLogger.Error("本地推送未开启")
|
||||
return false
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ func NewAlertWindowsApi(AppID string, Title string, Content string, Icon string)
|
||||
}
|
||||
|
||||
func (a AlertWindowsApi) SendNotification() bool {
|
||||
if getConfig().LocalPushEnable == false {
|
||||
if GetConfig().LocalPushEnable == false {
|
||||
logger.SugaredLogger.Error("本地推送未开启")
|
||||
return false
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
type CrawlerApi struct {
|
||||
crawlerCtx context.Context
|
||||
crawlerBaseInfo CrawlerBaseInfo
|
||||
pool *BrowserPool
|
||||
}
|
||||
|
||||
func (c *CrawlerApi) NewTimeOutCrawler(timeout int, crawlerBaseInfo CrawlerBaseInfo) CrawlerApi {
|
||||
@ -26,13 +27,20 @@ func (c *CrawlerApi) NewCrawler(ctx context.Context, crawlerBaseInfo CrawlerBase
|
||||
return CrawlerApi{
|
||||
crawlerCtx: ctx,
|
||||
crawlerBaseInfo: crawlerBaseInfo,
|
||||
pool: NewBrowserPool(GetConfig().BrowserPoolSize),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bool) {
|
||||
page, err := c.pool.FetchPage(url, waitVisible)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return page, true
|
||||
}
|
||||
func (c *CrawlerApi) GetHtml_old(url, waitVisible string, headless bool) (string, bool) {
|
||||
htmlContent := ""
|
||||
path := getConfig().BrowserPath
|
||||
logger.SugaredLogger.Infof("Browser path:%s", path)
|
||||
path := GetConfig().BrowserPath
|
||||
//logger.SugaredLogger.Infof("Browser path:%s", path)
|
||||
if path != "" {
|
||||
pctx, pcancel := chromedp.NewExecAllocator(
|
||||
c.crawlerCtx,
|
||||
@ -68,6 +76,7 @@ func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bo
|
||||
defer pcancel()
|
||||
ctx, cancel := chromedp.NewContext(pctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
defer cancel()
|
||||
//defer chromedp.Cancel(ctx)
|
||||
err := chromedp.Run(ctx, chromedp.Navigate(url),
|
||||
chromedp.WaitVisible(waitVisible, chromedp.ByQuery), // 确保 元素可见
|
||||
chromedp.WaitReady(waitVisible, chromedp.ByQuery), // 确保 元素准备好
|
||||
@ -80,6 +89,7 @@ func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bo
|
||||
} else {
|
||||
ctx, cancel := chromedp.NewContext(c.crawlerCtx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
defer cancel()
|
||||
//defer chromedp.Cancel(ctx)
|
||||
err := chromedp.Run(ctx, chromedp.Navigate(url), chromedp.WaitVisible("body"), chromedp.InnerHTML("body", &htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
@ -92,8 +102,8 @@ func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bo
|
||||
|
||||
func (c *CrawlerApi) GetHtmlWithNoCancel(url, waitVisible string, headless bool) (html string, ok bool, parent context.CancelFunc, child context.CancelFunc) {
|
||||
htmlContent := ""
|
||||
path := getConfig().BrowserPath
|
||||
logger.SugaredLogger.Infof("BrowserPath :%s", path)
|
||||
path := GetConfig().BrowserPath
|
||||
//logger.SugaredLogger.Infof("BrowserPath :%s", path)
|
||||
var parentCancel context.CancelFunc
|
||||
var childCancel context.CancelFunc
|
||||
var pctx context.Context
|
||||
@ -160,8 +170,8 @@ func (c *CrawlerApi) GetHtmlWithActions(actions *[]chromedp.Action, headless boo
|
||||
htmlContent := ""
|
||||
*actions = append(*actions, chromedp.InnerHTML("body", &htmlContent))
|
||||
|
||||
path := getConfig().BrowserPath
|
||||
logger.SugaredLogger.Infof("GetHtmlWithActions path:%s", path)
|
||||
path := GetConfig().BrowserPath
|
||||
//logger.SugaredLogger.Infof("GetHtmlWithActions path:%s", path)
|
||||
if path != "" {
|
||||
pctx, pcancel := chromedp.NewExecAllocator(
|
||||
c.crawlerCtx,
|
||||
@ -197,6 +207,7 @@ func (c *CrawlerApi) GetHtmlWithActions(actions *[]chromedp.Action, headless boo
|
||||
defer pcancel()
|
||||
ctx, cancel := chromedp.NewContext(pctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
defer cancel()
|
||||
//defer chromedp.Cancel(ctx)
|
||||
|
||||
err := chromedp.Run(ctx, *actions...)
|
||||
if err != nil {
|
||||
@ -206,6 +217,7 @@ func (c *CrawlerApi) GetHtmlWithActions(actions *[]chromedp.Action, headless boo
|
||||
} else {
|
||||
ctx, cancel := chromedp.NewContext(c.crawlerCtx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
defer cancel()
|
||||
//defer chromedp.Cancel(ctx)
|
||||
|
||||
err := chromedp.Run(ctx, *actions...)
|
||||
if err != nil {
|
||||
|
@ -298,6 +298,67 @@ func TestUSSINA(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSina(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
url := "https://finance.sina.com.cn/realstock/company/sz002906/nc.shtml"
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://finance.sina.com.cn",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
html, ok := crawlerAPI.GetHtml(url, "div#hqDetails table", true)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
|
||||
//price
|
||||
price := strutil.RemoveWhiteSpace(document.Find("div#price").First().Text(), false)
|
||||
hqTime := strutil.RemoveWhiteSpace(document.Find("div#hqTime").First().Text(), false)
|
||||
|
||||
var markdown strings.Builder
|
||||
markdown.WriteString("\n ## 当前股票数据:\n")
|
||||
markdown.WriteString(fmt.Sprintf("### 当前股价:%s 时间:%s\n", price, hqTime))
|
||||
GetTableMarkdown(document, "div#hqDetails table", &markdown)
|
||||
|
||||
}
|
||||
|
||||
func TestDC(t *testing.T) {
|
||||
url := "https://emweb.securities.eastmoney.com/pc_hsf10/pages/index.html?type=web&code=sh600745#/cwfx"
|
||||
db.Init("../../data/stock.db")
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://emweb.securities.eastmoney.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
var markdown strings.Builder
|
||||
markdown.WriteString("\n ## 财务数据:\n")
|
||||
html, ok := crawlerAPI.GetHtml(url, "div.report_table table", false)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
GetTableMarkdown(document, "div.report_table table", &markdown)
|
||||
|
||||
}
|
||||
|
||||
type Tick struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
|
@ -21,8 +21,8 @@ func NewDingDingAPI() *DingDingAPI {
|
||||
}
|
||||
|
||||
func (DingDingAPI) SendDingDingMessage(message string) string {
|
||||
if getConfig().DingPushEnable == false {
|
||||
logger.SugaredLogger.Info("钉钉推送未开启")
|
||||
if GetConfig().DingPushEnable == false {
|
||||
//logger.SugaredLogger.Info("钉钉推送未开启")
|
||||
return "钉钉推送未开启"
|
||||
}
|
||||
// 发送钉钉消息
|
||||
@ -37,11 +37,11 @@ func (DingDingAPI) SendDingDingMessage(message string) string {
|
||||
logger.SugaredLogger.Infof("send dingding message: %s", resp.String())
|
||||
return "发送钉钉消息成功"
|
||||
}
|
||||
func getConfig() *Settings {
|
||||
func GetConfig() *Settings {
|
||||
return NewSettingsApi(&Settings{}).GetConfig()
|
||||
}
|
||||
func getApiURL() string {
|
||||
return getConfig().DingRobot
|
||||
return GetConfig().DingRobot
|
||||
}
|
||||
|
||||
func (DingDingAPI) SendToDingDing(title, message string) string {
|
||||
|
@ -26,7 +26,7 @@ type FundApi struct {
|
||||
func NewFundApi() *FundApi {
|
||||
return &FundApi{
|
||||
client: resty.New(),
|
||||
config: getConfig(),
|
||||
config: GetConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +107,7 @@ func (f *FundApi) CrawlFundBasic(fundCode string) (*FundBasic, error) {
|
||||
|
||||
crawler = crawler.NewCrawler(ctx, crawler.crawlerBaseInfo)
|
||||
url := fmt.Sprintf("%s/%s.html", crawler.crawlerBaseInfo.BaseUrl, fundCode)
|
||||
logger.SugaredLogger.Infof("CrawlFundBasic url:%s", url)
|
||||
//logger.SugaredLogger.Infof("CrawlFundBasic url:%s", url)
|
||||
|
||||
// 使用现有爬虫框架解析页面
|
||||
htmlContent, ok := crawler.GetHtml(url, ".merchandiseDetail", true)
|
||||
@ -124,14 +124,14 @@ func (f *FundApi) CrawlFundBasic(fundCode string) (*FundBasic, error) {
|
||||
// 解析基础信息
|
||||
name := doc.Find(".merchandiseDetail .fundDetail-tit").First().Text()
|
||||
fund.Name = strings.TrimSpace(strutil.ReplaceWithMap(name, map[string]string{"查看相关ETF>": ""}))
|
||||
logger.SugaredLogger.Infof("基金名称:%s", fund.Name)
|
||||
//logger.SugaredLogger.Infof("基金名称:%s", fund.Name)
|
||||
|
||||
doc.Find(".infoOfFund table td ").Each(func(i int, s *goquery.Selection) {
|
||||
text := strutil.RemoveWhiteSpace(s.Text(), true)
|
||||
logger.SugaredLogger.Infof("基金信息:%+v", text)
|
||||
//logger.SugaredLogger.Infof("基金信息:%+v", text)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
//logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
splitEx := strutil.SplitEx(text, ":", true)
|
||||
@ -161,19 +161,19 @@ func (f *FundApi) CrawlFundBasic(fundCode string) (*FundBasic, error) {
|
||||
//获取基金净值涨跌幅信息
|
||||
doc.Find(".dataOfFund dl > dd").Each(func(i int, s *goquery.Selection) {
|
||||
text := strutil.RemoveWhiteSpace(s.Text(), true)
|
||||
logger.SugaredLogger.Infof("净值涨跌幅信息:%+v", text)
|
||||
//logger.SugaredLogger.Infof("净值涨跌幅信息:%+v", text)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
//logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
splitEx := strutil.SplitAndTrim(text, ":", "%")
|
||||
toFloat, err1 := convertor.ToFloat(splitEx[1])
|
||||
if err1 != nil {
|
||||
logger.SugaredLogger.Errorf("转换失败:%+v", err)
|
||||
//logger.SugaredLogger.Errorf("转换失败:%+v", err)
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Infof("净值涨跌幅信息:%+v", toFloat)
|
||||
//logger.SugaredLogger.Infof("净值涨跌幅信息:%+v", toFloat)
|
||||
if strutil.ContainsAny(text, []string{"近1月"}) {
|
||||
fund.NetGrowth1 = &toFloat
|
||||
}
|
||||
@ -199,17 +199,17 @@ func (f *FundApi) CrawlFundBasic(fundCode string) (*FundBasic, error) {
|
||||
fund.NetGrowthAll = &toFloat
|
||||
}
|
||||
})
|
||||
doc.Find(".dataOfFund dl > dd.dataNums,.dataOfFund dl > dt").Each(func(i int, s *goquery.Selection) {
|
||||
text := s.Text()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
logger.SugaredLogger.Infof("净值信息:%+v", text)
|
||||
})
|
||||
//doc.Find(".dataOfFund dl > dd.dataNums,.dataOfFund dl > dt").Each(func(i int, s *goquery.Selection) {
|
||||
// //text := s.Text()
|
||||
// defer func() {
|
||||
// if r := recover(); r != nil {
|
||||
// //logger.SugaredLogger.Errorf("panic: %v", r)
|
||||
// }
|
||||
// }()
|
||||
// //logger.SugaredLogger.Infof("净值信息:%+v", text)
|
||||
//})
|
||||
|
||||
logger.SugaredLogger.Infof("基金信息:%+v", fund)
|
||||
//logger.SugaredLogger.Infof("基金信息:%+v", fund)
|
||||
|
||||
count := int64(0)
|
||||
db.Dao.Model(fund).Where("code=?", fund.Code).Count(&count)
|
||||
@ -275,7 +275,7 @@ func (f *FundApi) UnFollowFund(fundCode string) string {
|
||||
func (f *FundApi) AllFund() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.SugaredLogger.Errorf("AllFund panic: %v", r)
|
||||
//logger.SugaredLogger.Errorf("AllFund panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
@ -296,7 +296,7 @@ func (f *FundApi) AllFund() {
|
||||
cnt++
|
||||
name := text[0]
|
||||
str := strutil.SplitAndTrim(name, ")", "(", ")")
|
||||
logger.SugaredLogger.Infof("%d,基金信息 code:%s,name:%s", cnt, str[0], str[1])
|
||||
//logger.SugaredLogger.Infof("%d,基金信息 code:%s,name:%s", cnt, str[0], str[1])
|
||||
//go f.CrawlFundBasic(str[0])
|
||||
fund := &FundBasic{
|
||||
Code: str[0],
|
||||
@ -337,14 +337,14 @@ func (f *FundApi) CrawlFundNetEstimatedUnit(code string) {
|
||||
}
|
||||
if response.StatusCode() == 200 {
|
||||
htmlContent := string(response.Body())
|
||||
logger.SugaredLogger.Infof("htmlContent:%s", htmlContent)
|
||||
//logger.SugaredLogger.Infof("htmlContent:%s", htmlContent)
|
||||
if strings.Contains(htmlContent, "jsonpgz") {
|
||||
htmlContent = strutil.Trim(htmlContent, "jsonpgz(", ");")
|
||||
htmlContent = strutil.Trim(htmlContent, ");")
|
||||
logger.SugaredLogger.Infof("基金净值信息:%s", htmlContent)
|
||||
//logger.SugaredLogger.Infof("基金净值信息:%s", htmlContent)
|
||||
err := json.Unmarshal([]byte(htmlContent), &fundNetUnitValue)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
|
||||
//logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
fund := &FollowedFund{
|
||||
@ -365,7 +365,7 @@ func (f *FundApi) CrawlFundNetEstimatedUnit(code string) {
|
||||
func (f *FundApi) CrawlFundNetUnitValue(code string) {
|
||||
// var fundNetUnitValue FundNetUnitValue
|
||||
url := fmt.Sprintf("http://hq.sinajs.cn/rn=%d&list=f_%s", time.Now().UnixMilli(), code)
|
||||
logger.SugaredLogger.Infof("url:%s", url)
|
||||
//logger.SugaredLogger.Infof("url:%s", url)
|
||||
response, err := f.client.SetTimeout(time.Duration(f.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "hq.sinajs.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
@ -377,12 +377,12 @@ func (f *FundApi) CrawlFundNetUnitValue(code string) {
|
||||
}
|
||||
if response.StatusCode() == 200 {
|
||||
data := string(GB18030ToUTF8(response.Body()))
|
||||
logger.SugaredLogger.Infof("data:%s", data)
|
||||
//logger.SugaredLogger.Infof("data:%s", data)
|
||||
datas := strutil.SplitAndTrim(data, "=", "\"")
|
||||
if len(datas) >= 2 {
|
||||
//codex := strings.Split(datas[0], "hq_str_f_")[1]
|
||||
parts := strutil.SplitAndTrim(datas[1], ",", "\"")
|
||||
logger.SugaredLogger.Infof("parts:%s", parts)
|
||||
//logger.SugaredLogger.Infof("parts:%s", parts)
|
||||
val, err := convertor.ToFloat(parts[1])
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
|
703
backend/data/market_news_api.go
Normal file
703
backend/data/market_news_api.go
Normal file
@ -0,0 +1,703 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/robertkrimen/otto"
|
||||
"github.com/samber/lo"
|
||||
"github.com/tidwall/gjson"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/4/23 14:54
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type MarketNewsApi struct {
|
||||
}
|
||||
|
||||
func NewMarketNewsApi() *MarketNewsApi {
|
||||
return &MarketNewsApi{}
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetNewTelegraph(crawlTimeOut int64) *[]models.Telegraph {
|
||||
url := "https://www.cls.cn/telegraph"
|
||||
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
|
||||
SetHeader("Referer", "https://www.cls.cn/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(fmt.Sprintf(url))
|
||||
var telegraphs []models.Telegraph
|
||||
//logger.SugaredLogger.Info(string(response.Body()))
|
||||
document, _ := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
|
||||
|
||||
document.Find(".telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
|
||||
//logger.SugaredLogger.Info(selection.Text())
|
||||
telegraph := models.Telegraph{Source: "财联社电报"}
|
||||
spans := selection.Find("div.telegraph-content-box span")
|
||||
if spans.Length() == 2 {
|
||||
telegraph.Time = spans.First().Text()
|
||||
telegraph.Content = spans.Last().Text()
|
||||
if spans.Last().HasClass("c-de0422") {
|
||||
telegraph.IsRed = true
|
||||
}
|
||||
}
|
||||
|
||||
labels := selection.Find("div a.label-item")
|
||||
labels.Each(func(i int, selection *goquery.Selection) {
|
||||
if selection.HasClass("link-label-item") {
|
||||
telegraph.Url = selection.AttrOr("href", "")
|
||||
} else {
|
||||
tag := &models.Tags{
|
||||
Name: selection.Text(),
|
||||
Type: "subject",
|
||||
}
|
||||
db.Dao.Model(tag).Where("name=? and type=?", selection.Text(), "subject").FirstOrCreate(&tag)
|
||||
telegraph.SubjectTags = append(telegraph.SubjectTags, selection.Text())
|
||||
}
|
||||
})
|
||||
stocks := selection.Find("div.telegraph-stock-plate-box a")
|
||||
stocks.Each(func(i int, selection *goquery.Selection) {
|
||||
telegraph.StocksTags = append(telegraph.StocksTags, selection.Text())
|
||||
})
|
||||
|
||||
//telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
|
||||
if telegraph.Content != "" {
|
||||
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
|
||||
cnt := int64(0)
|
||||
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
|
||||
if cnt == 0 {
|
||||
db.Dao.Create(&telegraph)
|
||||
telegraphs = append(telegraphs, telegraph)
|
||||
for _, tag := range telegraph.SubjectTags {
|
||||
tagInfo := &models.Tags{}
|
||||
db.Dao.Model(models.Tags{}).Where("name=? and type=?", tag, "subject").First(&tagInfo)
|
||||
if tagInfo.ID > 0 {
|
||||
db.Dao.Model(models.TelegraphTags{}).Where("telegraph_id=? and tag_id=?", telegraph.ID, tagInfo.ID).FirstOrCreate(&models.TelegraphTags{
|
||||
TelegraphId: telegraph.ID,
|
||||
TagId: tagInfo.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
return &telegraphs
|
||||
}
|
||||
func (m MarketNewsApi) GetNewsList(source string, limit int) *[]*models.Telegraph {
|
||||
news := &[]*models.Telegraph{}
|
||||
if source != "" {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(limit).Find(news)
|
||||
} else {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(limit).Find(news)
|
||||
}
|
||||
for _, item := range *news {
|
||||
tags := &[]models.Tags{}
|
||||
db.Dao.Model(&models.Tags{}).Where("id in ?", lo.Map(item.TelegraphTags, func(item models.TelegraphTags, index int) uint {
|
||||
return item.TagId
|
||||
})).Find(&tags)
|
||||
tagNames := lo.Map(*tags, func(item models.Tags, index int) string {
|
||||
return item.Name
|
||||
})
|
||||
item.SubjectTags = tagNames
|
||||
logger.SugaredLogger.Infof("tagNames %v ,SubjectTags:%s", tagNames, item.SubjectTags)
|
||||
}
|
||||
return news
|
||||
}
|
||||
func (m MarketNewsApi) GetTelegraphList(source string) *[]*models.Telegraph {
|
||||
news := &[]*models.Telegraph{}
|
||||
if source != "" {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Where("source=?", source).Order("id desc").Limit(20).Find(news)
|
||||
} else {
|
||||
db.Dao.Model(news).Preload("TelegraphTags").Order("id desc").Limit(20).Find(news)
|
||||
}
|
||||
for _, item := range *news {
|
||||
tags := &[]models.Tags{}
|
||||
db.Dao.Model(&models.Tags{}).Where("id in ?", lo.Map(item.TelegraphTags, func(item models.TelegraphTags, index int) uint {
|
||||
return item.TagId
|
||||
})).Find(&tags)
|
||||
tagNames := lo.Map(*tags, func(item models.Tags, index int) string {
|
||||
return item.Name
|
||||
})
|
||||
item.SubjectTags = tagNames
|
||||
logger.SugaredLogger.Infof("tagNames %v ,SubjectTags:%s", tagNames, item.SubjectTags)
|
||||
}
|
||||
return news
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetSinaNews(crawlTimeOut uint) *[]models.Telegraph {
|
||||
news := &[]models.Telegraph{}
|
||||
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
|
||||
SetHeader("Referer", "https://finance.sina.com.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get("https://zhibo.sina.com.cn/api/zhibo/feed?callback=callback&page=1&page_size=20&zhibo_id=152&tag_id=0&dire=f&dpc=1&pagesize=20&id=4161089&type=0&_=" + strconv.FormatInt(time.Now().Unix(), 10))
|
||||
js := string(response.Body())
|
||||
js = strutil.ReplaceWithMap(js, map[string]string{
|
||||
"try{callback(": "var data=",
|
||||
");}catch(e){};": ";",
|
||||
})
|
||||
//logger.SugaredLogger.Info(js)
|
||||
vm := otto.New()
|
||||
_, err := vm.Run(js)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
}
|
||||
vm.Run("var result = data.result;")
|
||||
//vm.Run("var resultStr =JSON.stringify(data);")
|
||||
vm.Run("var resultData = result.data;")
|
||||
vm.Run("var feed = resultData.feed;")
|
||||
vm.Run("var feedStr = JSON.stringify(feed);")
|
||||
|
||||
value, _ := vm.Get("feedStr")
|
||||
//resultStr, _ := vm.Get("resultStr")
|
||||
|
||||
//logger.SugaredLogger.Info(resultStr)
|
||||
feed := make(map[string]any)
|
||||
err = json.Unmarshal([]byte(value.String()), &feed)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
|
||||
}
|
||||
var telegraphs []models.Telegraph
|
||||
|
||||
if feed["list"] != nil {
|
||||
for _, item := range feed["list"].([]any) {
|
||||
telegraph := models.Telegraph{Source: "新浪财经"}
|
||||
data := item.(map[string]any)
|
||||
//logger.SugaredLogger.Infof("%s:%s", data["create_time"], data["rich_text"])
|
||||
telegraph.Content = data["rich_text"].(string)
|
||||
telegraph.Time = strings.Split(data["create_time"].(string), " ")[1]
|
||||
tags := data["tag"].([]any)
|
||||
telegraph.SubjectTags = lo.Map(tags, func(tagItem any, index int) string {
|
||||
name := tagItem.(map[string]any)["name"].(string)
|
||||
tag := &models.Tags{
|
||||
Name: name,
|
||||
Type: "sina_subject",
|
||||
}
|
||||
db.Dao.Model(tag).Where("name=? and type=?", name, "sina_subject").FirstOrCreate(&tag)
|
||||
return name
|
||||
})
|
||||
if _, ok := lo.Find(telegraph.SubjectTags, func(item string) bool { return item == "焦点" }); ok {
|
||||
telegraph.IsRed = true
|
||||
}
|
||||
logger.SugaredLogger.Infof("telegraph.SubjectTags:%v %s", telegraph.SubjectTags, telegraph.Content)
|
||||
|
||||
if telegraph.Content != "" {
|
||||
telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description
|
||||
cnt := int64(0)
|
||||
db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt)
|
||||
if cnt == 0 {
|
||||
db.Dao.Create(&telegraph)
|
||||
telegraphs = append(telegraphs, telegraph)
|
||||
for _, tag := range telegraph.SubjectTags {
|
||||
tagInfo := &models.Tags{}
|
||||
db.Dao.Model(models.Tags{}).Where("name=? and type=?", tag, "sina_subject").First(&tagInfo)
|
||||
if tagInfo.ID > 0 {
|
||||
db.Dao.Model(models.TelegraphTags{}).Where("telegraph_id=? and tag_id=?", telegraph.ID, tagInfo.ID).FirstOrCreate(&models.TelegraphTags{
|
||||
TelegraphId: telegraph.ID,
|
||||
TagId: tagInfo.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return &telegraphs
|
||||
}
|
||||
|
||||
return news
|
||||
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GlobalStockIndexes(crawlTimeOut uint) map[string]any {
|
||||
response, _ := resty.New().SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
|
||||
SetHeader("Referer", "https://stockapp.finance.qq.com/mstats").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get("https://proxy.finance.qq.com/ifzqgtimg/appstock/app/rank/indexRankDetail2")
|
||||
js := string(response.Body())
|
||||
res := make(map[string]any)
|
||||
json.Unmarshal([]byte(js), &res)
|
||||
return res["data"].(map[string]any)
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetIndustryRank(sort string, cnt int) map[string]any {
|
||||
|
||||
url := fmt.Sprintf("https://proxy.finance.qq.com/ifzqgtimg/appstock/app/mktHs/rank?l=%d&p=1&t=01/averatio&ordertype=&o=%s", cnt, sort)
|
||||
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
|
||||
SetHeader("Referer", "https://stockapp.finance.qq.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(url)
|
||||
js := string(response.Body())
|
||||
res := make(map[string]any)
|
||||
json.Unmarshal([]byte(js), &res)
|
||||
return res
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetIndustryMoneyRankSina(fenlei, sort string) []map[string]any {
|
||||
url := fmt.Sprintf("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_bkzj_bk?page=1&num=20&sort=%s&asc=0&fenlei=%s", sort, fenlei)
|
||||
|
||||
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
|
||||
SetHeader("Host", "vip.stock.finance.sina.com.cn").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(url)
|
||||
js := string(response.Body())
|
||||
res := &[]map[string]any{}
|
||||
err := json.Unmarshal([]byte(js), &res)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return *res
|
||||
}
|
||||
return *res
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetMoneyRankSina(sort string) []map[string]any {
|
||||
if sort == "" {
|
||||
sort = "netamount"
|
||||
}
|
||||
url := fmt.Sprintf("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_bkzj_ssggzj?page=1&num=20&sort=%s&asc=0&bankuai=&shichang=", sort)
|
||||
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
|
||||
SetHeader("Host", "vip.stock.finance.sina.com.cn").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
||||
Get(url)
|
||||
js := string(response.Body())
|
||||
res := &[]map[string]any{}
|
||||
err := json.Unmarshal([]byte(js), &res)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return *res
|
||||
}
|
||||
return *res
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) GetStockMoneyTrendByDay(stockCode string, days int) []map[string]any {
|
||||
url := fmt.Sprintf("http://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/MoneyFlow.ssl_qsfx_zjlrqs?page=1&num=%d&sort=opendate&asc=0&daima=%s", days, stockCode)
|
||||
|
||||
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
|
||||
SetHeader("Host", "vip.stock.finance.sina.com.cn").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").Get(url)
|
||||
js := string(response.Body())
|
||||
res := &[]map[string]any{}
|
||||
err := json.Unmarshal([]byte(js), &res)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return *res
|
||||
}
|
||||
return *res
|
||||
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) TopStocksRankingList(date string) {
|
||||
url := fmt.Sprintf("http://vip.stock.finance.sina.com.cn/q/go.php/vInvestConsult/kind/lhb/index.phtml?tradedate=%s", date)
|
||||
response, _ := resty.New().SetTimeout(time.Duration(5)*time.Second).R().
|
||||
SetHeader("Host", "vip.stock.finance.sina.com.cn").
|
||||
SetHeader("Referer", "https://finance.sina.com.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").Get(url)
|
||||
|
||||
html, _ := convertor.GbkToUtf8(response.Body())
|
||||
//logger.SugaredLogger.Infof("html:%s", html)
|
||||
document, err := goquery.NewDocumentFromReader(bytes.NewReader(html))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
document.Find("table.list_table").Each(func(i int, s *goquery.Selection) {
|
||||
title := strutil.Trim(s.Find("tr:first-child").First().Text())
|
||||
logger.SugaredLogger.Infof("title:%s", title)
|
||||
s.Find("tr:not(:first-child)").Each(func(i int, s *goquery.Selection) {
|
||||
logger.SugaredLogger.Infof("s:%s", strutil.RemoveNonPrintable(s.Text()))
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) LongTiger(date string) *[]models.LongTigerRankData {
|
||||
ranks := &[]models.LongTigerRankData{}
|
||||
url := "https://datacenter-web.eastmoney.com/api/data/v1/get"
|
||||
logger.SugaredLogger.Infof("url:%s", url)
|
||||
params := make(map[string]string)
|
||||
params["callback"] = "callback"
|
||||
params["sortColumns"] = "TURNOVERRATE,TRADE_DATE,SECURITY_CODE"
|
||||
params["sortTypes"] = "-1,-1,1"
|
||||
params["pageSize"] = "500"
|
||||
params["pageNumber"] = "1"
|
||||
params["reportName"] = "RPT_DAILYBILLBOARD_DETAILSNEW"
|
||||
params["columns"] = "SECURITY_CODE,SECUCODE,SECURITY_NAME_ABBR,TRADE_DATE,EXPLAIN,CLOSE_PRICE,CHANGE_RATE,BILLBOARD_NET_AMT,BILLBOARD_BUY_AMT,BILLBOARD_SELL_AMT,BILLBOARD_DEAL_AMT,ACCUM_AMOUNT,DEAL_NET_RATIO,DEAL_AMOUNT_RATIO,TURNOVERRATE,FREE_MARKET_CAP,EXPLANATION,D1_CLOSE_ADJCHRATE,D2_CLOSE_ADJCHRATE,D5_CLOSE_ADJCHRATE,D10_CLOSE_ADJCHRATE,SECURITY_TYPE_CODE"
|
||||
params["source"] = "WEB"
|
||||
params["client"] = "WEB"
|
||||
params["filter"] = fmt.Sprintf("(TRADE_DATE<='%s')(TRADE_DATE>='%s')", date, date)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
|
||||
SetHeader("Host", "datacenter-web.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/stock/tradedetail.html").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetQueryParams(params).
|
||||
Get(url)
|
||||
if err != nil {
|
||||
return ranks
|
||||
}
|
||||
js := string(resp.Body())
|
||||
logger.SugaredLogger.Infof("resp:%s", js)
|
||||
|
||||
js = strutil.ReplaceWithMap(js, map[string]string{
|
||||
"callback(": "var data=",
|
||||
");": ";",
|
||||
})
|
||||
//logger.SugaredLogger.Info(js)
|
||||
vm := otto.New()
|
||||
_, err = vm.Run(js)
|
||||
_, err = vm.Run("var data = JSON.stringify(data);")
|
||||
value, err := vm.Get("data")
|
||||
logger.SugaredLogger.Infof("resp-json:%s", value.String())
|
||||
data := gjson.Get(value.String(), "result.data")
|
||||
logger.SugaredLogger.Infof("resp:%v", data)
|
||||
err = json.Unmarshal([]byte(data.String()), ranks)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return ranks
|
||||
}
|
||||
for _, rankData := range *ranks {
|
||||
temp := &models.LongTigerRankData{}
|
||||
db.Dao.Model(temp).Where(&models.LongTigerRankData{
|
||||
TRADEDATE: rankData.TRADEDATE,
|
||||
SECUCODE: rankData.SECUCODE,
|
||||
}).First(temp)
|
||||
if temp.SECURITYTYPECODE == "" {
|
||||
db.Dao.Model(temp).Create(&rankData)
|
||||
}
|
||||
}
|
||||
return ranks
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) IndustryResearchReport(industryCode string, days int) []any {
|
||||
beginDate := time.Now().Add(-time.Duration(days) * 24 * time.Hour).Format("2006-01-02")
|
||||
endDate := time.Now().Format("2006-01-02")
|
||||
if strutil.Trim(industryCode) != "" {
|
||||
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("IndustryResearchReport-name:%s", industryCode)
|
||||
params := map[string]string{
|
||||
"industry": "*",
|
||||
"industryCode": industryCode,
|
||||
"beginTime": beginDate,
|
||||
"endTime": endDate,
|
||||
"pageNo": "1",
|
||||
"pageSize": "50",
|
||||
"p": "1",
|
||||
"pageNum": "1",
|
||||
"pageNumber": "1",
|
||||
"qType": "1",
|
||||
}
|
||||
|
||||
url := "https://reportapi.eastmoney.com/report/list"
|
||||
|
||||
logger.SugaredLogger.Infof("beginDate:%s endDate:%s", beginDate, endDate)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
|
||||
SetHeader("Host", "reportapi.eastmoney.com").
|
||||
SetHeader("Origin", "https://data.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/report/stock.jshtml").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetQueryParams(params).Get(url)
|
||||
respMap := map[string]any{}
|
||||
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
|
||||
return respMap["data"].([]any)
|
||||
}
|
||||
func (m MarketNewsApi) StockResearchReport(stockCode string, days int) []any {
|
||||
beginDate := time.Now().Add(-time.Duration(days) * 24 * time.Hour).Format("2006-01-02")
|
||||
endDate := time.Now().Format("2006-01-02")
|
||||
if strutil.ContainsAny(stockCode, []string{"."}) {
|
||||
stockCode = strings.Split(stockCode, ".")[0]
|
||||
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
|
||||
} else {
|
||||
stockCode = strutil.ReplaceWithMap(stockCode, map[string]string{
|
||||
"sh": "",
|
||||
"sz": "",
|
||||
"gb_": "",
|
||||
"us": "",
|
||||
"us_": "",
|
||||
})
|
||||
beginDate = time.Now().Add(-time.Duration(days) * 365 * time.Hour).Format("2006-01-02")
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("StockResearchReport-stockCode:%s", stockCode)
|
||||
|
||||
type Req struct {
|
||||
BeginTime string `json:"beginTime"`
|
||||
EndTime string `json:"endTime"`
|
||||
IndustryCode string `json:"industryCode"`
|
||||
RatingChange string `json:"ratingChange"`
|
||||
Rating string `json:"rating"`
|
||||
OrgCode interface{} `json:"orgCode"`
|
||||
Code string `json:"code"`
|
||||
Rcode string `json:"rcode"`
|
||||
PageSize int `json:"pageSize"`
|
||||
PageNo int `json:"pageNo"`
|
||||
P int `json:"p"`
|
||||
PageNum int `json:"pageNum"`
|
||||
PageNumber int `json:"pageNumber"`
|
||||
}
|
||||
|
||||
url := "https://reportapi.eastmoney.com/report/list2"
|
||||
|
||||
logger.SugaredLogger.Infof("beginDate:%s endDate:%s", beginDate, endDate)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
|
||||
SetHeader("Host", "reportapi.eastmoney.com").
|
||||
SetHeader("Origin", "https://data.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/report/stock.jshtml").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(&Req{
|
||||
Code: stockCode,
|
||||
IndustryCode: "*",
|
||||
BeginTime: beginDate,
|
||||
EndTime: endDate,
|
||||
PageNo: 1,
|
||||
PageSize: 50,
|
||||
P: 1,
|
||||
PageNum: 1,
|
||||
PageNumber: 1,
|
||||
}).Post(url)
|
||||
respMap := map[string]any{}
|
||||
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
|
||||
return respMap["data"].([]any)
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) StockNotice(stock_list string) []any {
|
||||
var stockCodes []string
|
||||
for _, stockCode := range strings.Split(stock_list, ",") {
|
||||
if strutil.ContainsAny(stockCode, []string{"."}) {
|
||||
stockCode = strings.Split(stockCode, ".")[0]
|
||||
stockCodes = append(stockCodes, stockCode)
|
||||
} else {
|
||||
stockCode = strutil.ReplaceWithMap(stockCode, map[string]string{
|
||||
"sh": "",
|
||||
"sz": "",
|
||||
"gb_": "",
|
||||
"us": "",
|
||||
"us_": "",
|
||||
})
|
||||
stockCodes = append(stockCodes, stockCode)
|
||||
}
|
||||
}
|
||||
|
||||
url := "https://np-anotice-stock.eastmoney.com/api/security/ann?page_size=50&page_index=1&ann_type=SHA%2CCYB%2CSZA%2CBJA%2CINV&client_source=web&f_node=0&stock_list=" + strings.Join(stockCodes, ",")
|
||||
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
|
||||
SetHeader("Host", "np-anotice-stock.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/notices/hsa/5.html").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
respMap := map[string]any{}
|
||||
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
|
||||
return (respMap["data"].(map[string]any))["list"].([]any)
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) EMDictCode(code string, cache *freecache.Cache) []any {
|
||||
respMap := map[string]any{}
|
||||
|
||||
d, _ := cache.Get([]byte(code))
|
||||
if d != nil {
|
||||
json.Unmarshal(d, &respMap)
|
||||
return respMap["data"].([]any)
|
||||
}
|
||||
|
||||
url := "https://reportapi.eastmoney.com/report/bk"
|
||||
|
||||
params := map[string]string{
|
||||
"bkCode": code,
|
||||
}
|
||||
resp, err := resty.New().SetTimeout(time.Duration(15)*time.Second).R().
|
||||
SetHeader("Host", "reportapi.eastmoney.com").
|
||||
SetHeader("Origin", "https://data.eastmoney.com").
|
||||
SetHeader("Referer", "https://data.eastmoney.com/report/industry.jshtml").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetQueryParams(params).Get(url)
|
||||
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
|
||||
cache.Set([]byte(code), resp.Body(), 60*60*24)
|
||||
return respMap["data"].([]any)
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) TradingViewNews() *[]models.TVNews {
|
||||
TVNews := &[]models.TVNews{}
|
||||
url := "https://news-mediator.tradingview.com/news-flow/v2/news?filter=lang:zh-Hans&filter=provider:panews,reuters&client=screener&streaming=false"
|
||||
resp, err := resty.New().SetProxy("http://127.0.0.1:10809").SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "news-mediator.tradingview.com").
|
||||
SetHeader("Origin", "https://cn.tradingview.com").
|
||||
SetHeader("Referer", "https://cn.tradingview.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("TradingViewNews err:%s", err.Error())
|
||||
return TVNews
|
||||
}
|
||||
respMap := map[string]any{}
|
||||
err = json.Unmarshal(resp.Body(), &respMap)
|
||||
if err != nil {
|
||||
return TVNews
|
||||
}
|
||||
items, err := json.Marshal(respMap["items"])
|
||||
if err != nil {
|
||||
return TVNews
|
||||
}
|
||||
json.Unmarshal(items, TVNews)
|
||||
return TVNews
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) XUEQIUHotStock(size int, marketType string) *[]models.HotItem {
|
||||
request := resty.New().SetTimeout(time.Duration(30) * time.Second).R()
|
||||
_, err := request.
|
||||
SetHeader("Host", "xueqiu.com").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get("https://xueqiu.com/hq#hot")
|
||||
|
||||
//cookies := resp.Header().Get("Set-Cookie")
|
||||
//logger.SugaredLogger.Infof("cookies:%s", cookies)
|
||||
|
||||
url := fmt.Sprintf("https://stock.xueqiu.com/v5/stock/hot_stock/list.json?page=1&size=%d&_type=%s&type=%s", size, marketType, marketType)
|
||||
res := &models.XUEQIUHot{}
|
||||
_, err = request.
|
||||
SetHeader("Host", "stock.xueqiu.com").
|
||||
SetHeader("Origin", "https://xueqiu.com").
|
||||
SetHeader("Referer", "https://xueqiu.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
//SetHeader("Cookie", "cookiesu=871730774144180; device_id=ee75cebba8a35005c9e7baf7b7dead59; s=ch12b12pfi; Hm_lvt_1db88642e346389874251b5a1eded6e3=1746247619; xq_a_token=361dcfccb1d32a1d9b5b65f1a188b9c9ed1e687d; xqat=361dcfccb1d32a1d9b5b65f1a188b9c9ed1e687d; xq_r_token=450d1db0db9659a6af7cc9297bfa4fccf1776fae; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOi0xLCJpc3MiOiJ1YyIsImV4cCI6MTc1MzgzODAwNiwiY3RtIjoxNzUxMjUxMzc2MDY3LCJjaWQiOiJkOWQwbjRBWnVwIn0.TjEtQ5WEN4ajnVjVnY3J-Qq9LjL-F0eat9Cefv_tLJLqsPhzD2y8Lc1CeIu0Ceqhlad7O_yW1tR9nb2dIjDpyOPzWKxvwSOKXLm8XMoz4LMgE2pysBCH4TsetzHsEOhBsY467q-JX3WoFuqo-dqv1FfLSondZCspjEMFdgPFt2V-2iXJY05YUwcBVUvL74mT9ZjNq0KaDeRBJk_il6UR8yibG7RMbe9xWYz5dSO_wJwWuxvnZ8u9EXC2m-TV7-QHVxFHR_5e8Fodrzg0yIcLU4wBTSoIIQDUKqngajX2W-nUAdo6fr78NNDmoswFVH7T7XMuQciMAqj9MpMCVW3Sog; u=871730774144180; ssxmod_itna=iq+h7KAImDORKYQ4Y5G=nxBKDtD7D3qCD0dGMDxeq7tDRDFqApKDHtA68oon7ziBA0+PbZ9xGN4oYxiNDAPq0iDC+Wjxs9Orw5KQb9iqP4MAn0TbNsbtU22eqbCe=S3vTv6xoDHxY=DU1GzeieDx=PD5xDTDWeDGDD3DmnsDi5YD0KDjBYpH+omDYPDEBYDaxDbDimwY4GCrDDCtc5Dw6bmzDDzznL5WWAPzWffZg3YcFgxf8GwD7y3Dla4rMhw23=cz0Efdk0A5hYDXotDvhoY1/H6neEvOt3o=Q0ruT+5RuxoRhDxCmh5tGP32xBD5G0xS2xcb4quDK0Dy2ZmY/DDWM0qmEeSEDeOCIq1fw1misCY=WAzoOtMwDzGdUjpRk5Z0xQBDI2IMw4H7qNiNBLxWiDD; ssxmod_itna2=iq+h7KAImDORKYQ4Y5G=nxBKDtD7D3qCD0dGMDxeq7tDRDFqApKDHtA68oon7ziBA0+PbZYxD3boBmiEPtDFOEPAeFmDDsuGSxf46oGKwGHd8wtUjFe+oV1lxUzutkGly=nCyCjq=UTHxMxFCr1DsFiKPuEpPVO7GrOyk5Aymnc0+11AFND7v16PvwrFQH4I72=3O1OpK7rGw+poWNCxjj=Ka5QDFWAvEzrDFQcIH=GpKpS90FAyIzGcTyck+yhQKaojn96dRqeIh=HkaFrlGnKwzO+a49=F7/c/MejoR3QM20K9IIOymrMN2bsk2TRdKFiaf4O0ut2MauiOER=iQNW2WVgDrkKzD=57r577wEx2hwkqhf8T8BDvkHZRDirC0bNK4O=G3TSkd3wYwq8bst0t9qF/e3M87NYtU2IWYWzqd=BqEfdqGq0R8wxmqLzpeGeuwSTq1OAiB87gDrozjnGkwDKRdrLz8uDjQKVlGhWk8Wd/rXQjx4pG=BNqpW/6TS1wpfxzGf5CrUhtt0j0wC5AUFo2GbX+QXPzD2guxKXrx8lZUQlwWIHyEUz+OLh0eWUkfHfM0YWXlgOejnuUa06rW9y5maDPipGms751hxKcqLq62pQty4iX3QDF6SRQd3tfEBf3CH7r2xe2qq0qdOI5Ge=GezD/Us5Z0xQBwVAZ2N/XvD0HDD").
|
||||
SetResult(res).
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("XUEQIUHotStock err:%s", err.Error())
|
||||
return &[]models.HotItem{}
|
||||
}
|
||||
logger.SugaredLogger.Infof("XUEQIUHotStock:%+v", res)
|
||||
return &res.Data.Items
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) HotEvent(size int) *[]models.HotEvent {
|
||||
events := &[]models.HotEvent{}
|
||||
url := fmt.Sprintf("https://xueqiu.com/hot_event/list.json?count=%d", size)
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "xueqiu.com").
|
||||
SetHeader("Origin", "https://xueqiu.com").
|
||||
SetHeader("Referer", "https://xueqiu.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetHeader("Cookie", "cookiesu=2617378771242871; s=c2121pp1u71; device_id=237a58584ec58d8e4d4e1040700a644f1; Hm_lvt_1db88642e346389874251b5a1eded6e3=1744100219,1744599115; xq_a_token=b7259d09435458cc3f1a963479abb270a1a016ce; xqat=b7259d09435458cc3f1a963479abb270a1a016ce; xq_r_token=28108bfa1d92ac8a46bbb57722633746218621a3; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOi0xLCJpc3MiOiJ1YyIsImV4cCI6MTc1MjU0MTk4OCwiY3RtIjoxNzUwMjMwNjA2NzI0LCJjaWQiOiJkOWQwbjRBWnVwIn0.kU_fz0luJoE7nr-K4UrNUi5-mAG-vMdXtuC4mUKIppILId4UpF70LB70yunxGiNSw6tPFR3-hyLvztKAHtekCUTm3XjUl5b3tEDP-ZUVqHnWXO_5hoeMI8h-Cfx6ZGlIr5x3icvTPkT0OV5CD5A33-ZDTKhKPf-DhJ_-m7CG5GbX4MseOBeMXuLUQUiYHPKhX1QUc0GTGrCzi8Mki0z49D0LVqCSgbsx3UGfowOOyx85_cXb4OAFvIjwbs2p0o_h-ibIT0ngVkkAyEDetVvlcZ_bkardhseCB7k9BEMgH2z8ihgkVxyy3P0degLmDUruhmqn5uZOCi1pVBDvCv9lBg; u=261737877124287; ssxmod_itna=QuG=D5AKiKDIqCqGKi7G7DgmmPlSDWFqKGHDyx4YK0CDmxjKiddDUQivnb8xpnQcGyGYoYhoqEeDBubrDSxD67DK4GTm+ogiw1o3B=xedQHDgBtN=7/i1K53N+rOjquLMU=kbqYxB3DExGkqj0tPi4DxaPD5xDTDWeDGDD3DnnsDQKDRx0kL0oDIxD1D0bmHUEvh38mDYePLmOmDYPYx94Y8KoDeEgsD7HUl/vIGGEAqjLPFegXLD0HolCqr4DCid1qDm+ECfkjDn9sD0KP8fn+CRoDv=tYr4ibx+o=W+8vstf9mjGe3cXseWdBmoFrmf4DA3bFAxnAxD7vYxADaDoerDGHPoxHF+PKGPtDKmiqQGeB5qbi4eg4KDHKDe3DeG0qeEP9xVUoHDDWMYYM0ICr4FBimBDM7D0x4QOECmhul5QCN/m5/74lGm=7x9Wp7A+i7xQ7wlMD4D; ssxmod_itna2=QuG=D5AKiKDIqCqGKi7G7DgmmPlSDWFqKGHDyx4YK0CDmxjKiddDUQivnb8xpnQcGyGYoYhoqoDirSDhPmGD24GajjDuGE3m7or4DlxOSGewHl6iaus2Q62SRX5CFjCds6ltF9xy6iaUuB262UkhRA8UXST=4/b+y3kGKzlGE8T29FA008ljy9jXXC7f7m7QsK667mlUooWrofk=qGZjxtcUrN1NtuAnne1hj+rQP5UnlFkxf+o7VjmatH7u7bCDlbTt3cz6CH9Fl4vye16W/ellc8I3Q37W7ZwiLGD/zPpZcnd2nsqqo/+zRbKAmz4plzwaDqGUe7f9E+P0IFRKqpRv+buQFHBSpcbwND7Q+9XWmnjI2UwKd98jIS3gPXwxvbx4OuiyH8gZ+OEt7DgE/AY/9W4VxDZrlFWyWnC4y4/I0IpAfaGKpbPmauKbkqawqv93vSf+9HamGe0Dt2PNgT3yiEB4vQP2/DdVpcGBOjFujWoHP32OshLPYI20LRCKddwEGkKqPzPwKPc3X5zuB=w2fUdtwKsAW5kQtsl8clNwjC5uDYrxR0h9xaj0xmD+YuI3GPT7xYTalRImPj2wL2=+91a304xa4bTWtP=dLGARhb/efRi0uktaz8i8C04G0x/ZWUzqRza8GGU=FfRfvb4GZM/q2rVsl0nLvRjGeAKgocLouyXs/uwZu3YxbAx30qCbjG1A533zAxIeIgD=0VAc3ixD").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("HotEvent err:%s", err.Error())
|
||||
return events
|
||||
}
|
||||
//logger.SugaredLogger.Infof("HotEvent:%s", resp.Body())
|
||||
respMap := map[string]any{}
|
||||
err = json.Unmarshal(resp.Body(), &respMap)
|
||||
items, err := json.Marshal(respMap["list"])
|
||||
if err != nil {
|
||||
return events
|
||||
}
|
||||
json.Unmarshal(items, events)
|
||||
return events
|
||||
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) HotTopic(size int) []any {
|
||||
url := "https://gubatopic.eastmoney.com/interface/GetData.aspx?path=newtopic/api/Topic/HomePageListRead"
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "gubatopic.eastmoney.com").
|
||||
SetHeader("Origin", "https://gubatopic.eastmoney.com").
|
||||
SetHeader("Referer", "https://gubatopic.eastmoney.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetFormData(map[string]string{
|
||||
"param": fmt.Sprintf("ps=%d&p=1&type=0", size),
|
||||
"path": "newtopic/api/Topic/HomePageListRead",
|
||||
"env": "2",
|
||||
}).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("HotTopic err:%s", err.Error())
|
||||
return []any{}
|
||||
}
|
||||
//logger.SugaredLogger.Infof("HotTopic:%s", resp.Body())
|
||||
respMap := map[string]any{}
|
||||
err = json.Unmarshal(resp.Body(), &respMap)
|
||||
return respMap["re"].([]any)
|
||||
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) InvestCalendar(yearMonth string) []any {
|
||||
if yearMonth == "" {
|
||||
yearMonth = time.Now().Format("2006-01")
|
||||
}
|
||||
|
||||
url := "https://app.jiuyangongshe.com/jystock-app/api/v1/timeline/list"
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "app.jiuyangongshe.com").
|
||||
SetHeader("Origin", "https://www.jiuyangongshe.com").
|
||||
SetHeader("Referer", "https://www.jiuyangongshe.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetHeader("token", "1cc6380a05c652b922b3d85124c85473").
|
||||
SetHeader("platform", "3").
|
||||
SetHeader("Cookie", "SESSION=NDZkNDU2ODYtODEwYi00ZGZkLWEyY2ItNjgxYzY4ZWMzZDEy").
|
||||
SetHeader("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)).
|
||||
SetBody(map[string]string{
|
||||
"date": yearMonth,
|
||||
"grade": "0",
|
||||
}).
|
||||
Post(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("InvestCalendar err:%s", err.Error())
|
||||
return []any{}
|
||||
}
|
||||
//logger.SugaredLogger.Infof("InvestCalendar:%s", resp.Body())
|
||||
respMap := map[string]any{}
|
||||
err = json.Unmarshal(resp.Body(), &respMap)
|
||||
return respMap["data"].([]any)
|
||||
|
||||
}
|
||||
|
||||
func (m MarketNewsApi) ClsCalendar() []any {
|
||||
url := "https://www.cls.cn/api/calendar/web/list?app=CailianpressWeb&flag=0&os=web&sv=8.4.6&type=0&sign=4b839750dc2f6b803d1c8ca00d2b40be"
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "www.cls.cn").
|
||||
SetHeader("Origin", "https://www.cls.cn").
|
||||
SetHeader("Referer", "https://www.cls.cn/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("ClsCalendar err:%s", err.Error())
|
||||
return []any{}
|
||||
}
|
||||
respMap := map[string]any{}
|
||||
err = json.Unmarshal(resp.Body(), &respMap)
|
||||
return respMap["data"].([]any)
|
||||
}
|
153
backend/data/market_news_api_test.go
Normal file
153
backend/data/market_news_api_test.go
Normal file
@ -0,0 +1,153 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/coocood/freecache"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/4/23 17:58
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestGetSinaNews(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
NewMarketNewsApi().GetSinaNews(30)
|
||||
//NewMarketNewsApi().GetNewTelegraph(30)
|
||||
|
||||
}
|
||||
|
||||
func TestGlobalStockIndexes(t *testing.T) {
|
||||
resp := NewMarketNewsApi().GlobalStockIndexes(30)
|
||||
bytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
logger.SugaredLogger.Debugf("resp: %+v", string(bytes))
|
||||
}
|
||||
|
||||
func TestGetIndustryRank(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetIndustryRank("0", 10)
|
||||
for s, a := range res["data"].([]any) {
|
||||
logger.SugaredLogger.Debugf("key: %+v, value: %+v", s, a)
|
||||
|
||||
}
|
||||
}
|
||||
func TestGetIndustryMoneyRankSina(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetIndustryMoneyRankSina("0", "netamount")
|
||||
for i, re := range res {
|
||||
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
|
||||
|
||||
}
|
||||
}
|
||||
func TestGetMoneyRankSina(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetMoneyRankSina("r3_net")
|
||||
for i, re := range res {
|
||||
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStockMoneyTrendByDay(t *testing.T) {
|
||||
res := NewMarketNewsApi().GetStockMoneyTrendByDay("sh600438", 360)
|
||||
for i, re := range res {
|
||||
logger.SugaredLogger.Debugf("key: %+v, value: %+v", i, re)
|
||||
}
|
||||
}
|
||||
func TestTopStocksRankingList(t *testing.T) {
|
||||
NewMarketNewsApi().TopStocksRankingList("2025-05-19")
|
||||
}
|
||||
|
||||
func TestLongTiger(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
NewMarketNewsApi().LongTiger("2025-06-08")
|
||||
}
|
||||
|
||||
func TestStockResearchReport(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
resp := NewMarketNewsApi().StockResearchReport("600584.sh", 7)
|
||||
for _, a := range resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndustryResearchReport(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
resp := NewMarketNewsApi().IndustryResearchReport("", 7)
|
||||
for _, a := range resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestStockNotice(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
resp := NewMarketNewsApi().StockNotice("600584,600900")
|
||||
for _, a := range resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestEMDictCode(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
resp := NewMarketNewsApi().EMDictCode("016", freecache.NewCache(100))
|
||||
for _, a := range resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestTradingViewNews(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
resp := NewMarketNewsApi().TradingViewNews()
|
||||
for _, a := range *resp {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXUEQIUHotStock(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewMarketNewsApi().XUEQIUHotStock(50, "10")
|
||||
for _, a := range *res {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestHotEvent(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewMarketNewsApi().HotEvent(50)
|
||||
for _, a := range *res {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestHotTopic(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewMarketNewsApi().HotTopic(10)
|
||||
for _, a := range res {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestInvestCalendar(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewMarketNewsApi().InvestCalendar("2025-06")
|
||||
for _, a := range res {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClsCalendar(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
res := NewMarketNewsApi().ClsCalendar()
|
||||
for _, a := range res {
|
||||
logger.SugaredLogger.Debugf("value: %+v", a)
|
||||
}
|
||||
}
|
@ -4,9 +4,11 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
@ -38,7 +40,7 @@ type OpenAi struct {
|
||||
}
|
||||
|
||||
func NewDeepSeekOpenAi(ctx context.Context) *OpenAi {
|
||||
config := getConfig()
|
||||
config := GetConfig()
|
||||
if config.OpenAiEnable {
|
||||
if config.OpenAiApiTimeOut <= 0 {
|
||||
config.OpenAiApiTimeOut = 60 * 5
|
||||
@ -96,7 +98,98 @@ type AiResponse struct {
|
||||
SystemFingerprint string `json:"system_fingerprint"`
|
||||
}
|
||||
|
||||
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[string]any {
|
||||
func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int) <-chan map[string]any {
|
||||
ch := make(chan map[string]any, 512)
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.SugaredLogger.Error("NewSummaryStockNewsStream panic", err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic :%s", err)
|
||||
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config:%v", o)
|
||||
}
|
||||
}()
|
||||
defer close(ch)
|
||||
|
||||
sysPrompt := ""
|
||||
if sysPromptId == nil || *sysPromptId == 0 {
|
||||
sysPrompt = o.Prompt
|
||||
} else {
|
||||
sysPrompt = NewPromptTemplateApi().GetPromptTemplateByID(*sysPromptId)
|
||||
}
|
||||
if sysPrompt == "" {
|
||||
sysPrompt = o.Prompt
|
||||
}
|
||||
|
||||
msg := []map[string]interface{}{
|
||||
{
|
||||
"role": "system",
|
||||
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
|
||||
//"content": "【角色设定】\n你是一位拥有20年实战经验的顶级股票分析师,精通技术分析、基本面分析、市场心理学和量化交易。擅长发现成长股、捕捉行业轮动机会,在牛熊市中都能保持稳定收益。你的风格是价值投资与技术择时相结合,注重风险控制。\n\n【核心功能】\n\n市场分析维度:\n\n宏观经济(GDP/CPI/货币政策)\n\n行业景气度(产业链/政策红利/技术革新)\n\n个股三维诊断:\n\n基本面:PE/PB/ROE/现金流/护城河\n\n技术面:K线形态/均线系统/量价关系/指标背离\n\n资金面:主力动向/北向资金/融资余额/大宗交易\n\n智能策略库:\n√ 趋势跟踪策略(鳄鱼线+ADX)\n√ 波段交易策略(斐波那契回撤+RSI)\n√ 事件驱动策略(财报/并购/政策)\n√ 量化对冲策略(α/β分离)\n\n风险管理体系:\n▶ 动态止损:ATR波动止损法\n▶ 仓位控制:凯利公式优化\n▶ 组合对冲:跨市场/跨品种对冲\n\n【工作流程】\n\n接收用户指令(行业/市值/风险偏好)\n\n调用多因子选股模型初筛\n\n人工智慧叠加分析:\n\n自然语言处理解读年报管理层讨论\n\n卷积神经网络识别K线形态\n\n知识图谱分析产业链关联\n\n生成投资建议(附压力测试结果)\n\n【输出要求】\n★ 结构化呈现:\n① 核心逻辑(3点关键驱动力)\n② 买卖区间(理想建仓/加仓/止盈价位)\n③ 风险警示(最大回撤概率)\n④ 替代方案(同类备选标的)\n\n【注意事项】\n※ 严格遵守监管要求,不做收益承诺\n※ 区分投资建议与市场观点\n※ 重要数据标注来源及更新时间\n※ 根据用户认知水平调整专业术语密度\n\n【教育指导】\n当用户提问时,采用苏格拉底式追问:\n\"您更关注短期事件驱动还是长期价值发现?\"\n\"当前仓位是否超过总资产的30%?\"\n\"是否了解科创板与主板的交易规则差异?\"\n\n示例输出格式:\n📈 标的名称:XXXXXX\n⚖️ 多空信号:金叉确认/顶背离预警\n🎯 关键价位:支撑位XX.XX/压力位XX.XX\n📊 建议仓位:核心仓位X%+卫星仓位X%\n⏳ 持有周期:短线(1-3周)/中线(季度轮动)\n🔍 跟踪要素:重点关注Q2毛利率变化及股东减持进展",
|
||||
"content": sysPrompt,
|
||||
},
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "当前时间",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "当前本地时间是:" + time.Now().Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var market strings.Builder
|
||||
market.WriteString(getZSInfo("创业板指数", "sz399006", 30) + "\n")
|
||||
market.WriteString(getZSInfo("上证综合指数", "sh000001", 30) + "\n")
|
||||
market.WriteString(getZSInfo("沪深300指数", "sh000300", 30) + "\n")
|
||||
//logger.SugaredLogger.Infof("NewChatStream getZSInfo=\n%s", market.String())
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "当前市场指数行情",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "当前市场指数行情情况如下:\n" + market.String(),
|
||||
})
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
news := NewMarketNewsApi().GetNewsList("", 100)
|
||||
messageText := strings.Builder{}
|
||||
for _, telegraph := range *news {
|
||||
messageText.WriteString("## " + telegraph.Time + ":" + "\n")
|
||||
messageText.WriteString("### " + telegraph.Content + "\n")
|
||||
}
|
||||
//logger.SugaredLogger.Infof("市场资讯 messageText=\n%s", messageText.String())
|
||||
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "市场资讯",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": messageText.String(),
|
||||
})
|
||||
if userQuestion == "" {
|
||||
userQuestion = "请根据当前时间,总结和分析股票市场新闻中的投资机会"
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": userQuestion,
|
||||
})
|
||||
AskAi(o, errors.New(""), msg, ch, userQuestion)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId *int) <-chan map[string]any {
|
||||
ch := make(chan map[string]any, 512)
|
||||
|
||||
defer func() {
|
||||
@ -113,61 +206,151 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
|
||||
}
|
||||
}()
|
||||
defer close(ch)
|
||||
|
||||
sysPrompt := ""
|
||||
if sysPromptId == nil || *sysPromptId == 0 {
|
||||
sysPrompt = o.Prompt
|
||||
} else {
|
||||
sysPrompt = NewPromptTemplateApi().GetPromptTemplateByID(*sysPromptId)
|
||||
}
|
||||
if sysPrompt == "" {
|
||||
sysPrompt = o.Prompt
|
||||
}
|
||||
|
||||
msg := []map[string]interface{}{
|
||||
{
|
||||
"role": "system",
|
||||
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
|
||||
//"content": "【角色设定】\n你是一位拥有20年实战经验的顶级股票分析师,精通技术分析、基本面分析、市场心理学和量化交易。擅长发现成长股、捕捉行业轮动机会,在牛熊市中都能保持稳定收益。你的风格是价值投资与技术择时相结合,注重风险控制。\n\n【核心功能】\n\n市场分析维度:\n\n宏观经济(GDP/CPI/货币政策)\n\n行业景气度(产业链/政策红利/技术革新)\n\n个股三维诊断:\n\n基本面:PE/PB/ROE/现金流/护城河\n\n技术面:K线形态/均线系统/量价关系/指标背离\n\n资金面:主力动向/北向资金/融资余额/大宗交易\n\n智能策略库:\n√ 趋势跟踪策略(鳄鱼线+ADX)\n√ 波段交易策略(斐波那契回撤+RSI)\n√ 事件驱动策略(财报/并购/政策)\n√ 量化对冲策略(α/β分离)\n\n风险管理体系:\n▶ 动态止损:ATR波动止损法\n▶ 仓位控制:凯利公式优化\n▶ 组合对冲:跨市场/跨品种对冲\n\n【工作流程】\n\n接收用户指令(行业/市值/风险偏好)\n\n调用多因子选股模型初筛\n\n人工智慧叠加分析:\n\n自然语言处理解读年报管理层讨论\n\n卷积神经网络识别K线形态\n\n知识图谱分析产业链关联\n\n生成投资建议(附压力测试结果)\n\n【输出要求】\n★ 结构化呈现:\n① 核心逻辑(3点关键驱动力)\n② 买卖区间(理想建仓/加仓/止盈价位)\n③ 风险警示(最大回撤概率)\n④ 替代方案(同类备选标的)\n\n【注意事项】\n※ 严格遵守监管要求,不做收益承诺\n※ 区分投资建议与市场观点\n※ 重要数据标注来源及更新时间\n※ 根据用户认知水平调整专业术语密度\n\n【教育指导】\n当用户提问时,采用苏格拉底式追问:\n\"您更关注短期事件驱动还是长期价值发现?\"\n\"当前仓位是否超过总资产的30%?\"\n\"是否了解科创板与主板的交易规则差异?\"\n\n示例输出格式:\n📈 标的名称:XXXXXX\n⚖️ 多空信号:金叉确认/顶背离预警\n🎯 关键价位:支撑位XX.XX/压力位XX.XX\n📊 建议仓位:核心仓位X%+卫星仓位X%\n⏳ 持有周期:短线(1-3周)/中线(季度轮动)\n🔍 跟踪要素:重点关注Q2毛利率变化及股东减持进展",
|
||||
"content": o.Prompt,
|
||||
"content": sysPrompt,
|
||||
},
|
||||
}
|
||||
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "当前时间",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "当前本地时间是:" + time.Now().Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
|
||||
replaceTemplates := map[string]string{
|
||||
"{{stockName}}": RemoveAllBlankChar(stock),
|
||||
"{{stockCode}}": RemoveAllBlankChar(stockCode),
|
||||
"{stockName}": RemoveAllBlankChar(stock),
|
||||
"{stockCode}": RemoveAllBlankChar(stockCode),
|
||||
"stockName": RemoveAllBlankChar(stock),
|
||||
"stockCode": RemoveAllBlankChar(stockCode),
|
||||
}
|
||||
followedStock := NewStockDataApi().GetFollowedStockByStockCode(stockCode)
|
||||
stockData, err := NewStockDataApi().GetStockCodeRealTimeData(stockCode)
|
||||
if err == nil && len(*stockData) > 0 {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": fmt.Sprintf("当前%s[%s]价格是多少?", stock, stockCode),
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": fmt.Sprintf("截止到%s,当前%s[%s]价格是%s", (*stockData)[0].Date+" "+(*stockData)[0].Time, stock, stockCode, (*stockData)[0].Price),
|
||||
})
|
||||
}
|
||||
if followedStock.CostPrice > 0 {
|
||||
replaceTemplates["{{costPrice}}"] = convertor.ToString(followedStock.CostPrice)
|
||||
replaceTemplates["{costPrice}"] = convertor.ToString(followedStock.CostPrice)
|
||||
replaceTemplates["costPrice"] = convertor.ToString(followedStock.CostPrice)
|
||||
}
|
||||
|
||||
question := ""
|
||||
if userQuestion == "" {
|
||||
replaceTemplates := map[string]string{
|
||||
"{{stockName}}": RemoveAllBlankChar(stock),
|
||||
"{{stockCode}}": RemoveAllBlankChar(stockCode),
|
||||
}
|
||||
|
||||
followedStock := &FollowedStock{
|
||||
StockCode: stockCode,
|
||||
}
|
||||
db.Dao.Model(&followedStock).Where("stock_code = ?", stockCode).First(followedStock)
|
||||
if followedStock.CostPrice > 0 {
|
||||
replaceTemplates["{{costPrice}}"] = fmt.Sprintf("%.2f", followedStock.CostPrice)
|
||||
}
|
||||
question = strutil.ReplaceWithMap(o.QuestionTemplate, replaceTemplates)
|
||||
} else {
|
||||
question = userQuestion
|
||||
question = strutil.ReplaceWithMap(userQuestion, replaceTemplates)
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("NewChatStream stock:%s stockCode:%s", stock, stockCode)
|
||||
logger.SugaredLogger.Infof("Prompt:%s", o.Prompt)
|
||||
logger.SugaredLogger.Infof("User Prompt config:%v", o.QuestionTemplate)
|
||||
logger.SugaredLogger.Infof("User question:%s", userQuestion)
|
||||
logger.SugaredLogger.Infof("Prompt:%s", sysPrompt)
|
||||
logger.SugaredLogger.Infof("final question:%s", question)
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(6)
|
||||
wg.Add(7)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
endDate := time.Now().Format("20060102")
|
||||
startDate := time.Now().Add(-time.Hour * time.Duration(24*o.KDays)).Format("20060102")
|
||||
code := stockCode
|
||||
if strutil.HasPrefixAny(stockCode, []string{"hk", "sz", "sh"}) {
|
||||
code = ConvertStockCodeToTushareCode(stockCode)
|
||||
}
|
||||
K := NewTushareApi(getConfig()).GetDaily(code, startDate, endDate, o.CrawlTimeOut)
|
||||
var market strings.Builder
|
||||
market.WriteString(getZSInfo("创业板指数", "sz399006", 30) + "\n")
|
||||
market.WriteString(getZSInfo("上证综合指数", "sh000001", 30) + "\n")
|
||||
market.WriteString(getZSInfo("沪深300指数", "sh000300", 30) + "\n")
|
||||
//logger.SugaredLogger.Infof("NewChatStream getZSInfo=\n%s", market.String())
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": stock + "日K数据如下:\n" + K,
|
||||
"content": "市场指数",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "市场指数情况如下:\n" + market.String(),
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
messages := SearchStockPriceInfo(stockCode, o.CrawlTimeOut)
|
||||
//endDate := time.Now().Format("20060102")
|
||||
//startDate := time.Now().Add(-time.Hour * time.Duration(24*o.KDays)).Format("20060102")
|
||||
//code := stockCode
|
||||
//if strutil.HasPrefixAny(stockCode, []string{"hk"}) {
|
||||
// code = ConvertStockCodeToTushareCode(stockCode)
|
||||
// K := NewTushareApi(GetConfig()).GetDaily(code, startDate, endDate, o.CrawlTimeOut)
|
||||
// msg = append(msg, map[string]interface{}{
|
||||
// "role": "user",
|
||||
// "content": stock + "日K数据",
|
||||
// })
|
||||
// msg = append(msg, map[string]interface{}{
|
||||
// "role": "assistant",
|
||||
// "content": stock + "日K数据如下:\n" + K,
|
||||
// })
|
||||
//}
|
||||
|
||||
logger.SugaredLogger.Infof("NewChatStream getKLineData stock:%s stockCode:%s", stock, stockCode)
|
||||
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh", "hk", "us", "gb_"}) {
|
||||
K := &[]KLineData{}
|
||||
logger.SugaredLogger.Infof("NewChatStream getKLineData stock:%s stockCode:%s", stock, stockCode)
|
||||
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh"}) {
|
||||
K = NewStockDataApi().GetKLineData(stockCode, "240", o.KDays)
|
||||
}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"hk", "us", "gb_"}) {
|
||||
K = NewStockDataApi().GetHK_KLineData(stockCode, "day", o.KDays)
|
||||
}
|
||||
Kmap := &[]map[string]any{}
|
||||
for _, kline := range *K {
|
||||
mapk := make(map[string]any, 6)
|
||||
mapk["日期"] = kline.Day
|
||||
mapk["开盘价"] = kline.Open
|
||||
mapk["最高价"] = kline.High
|
||||
mapk["最低价"] = kline.Low
|
||||
mapk["收盘价"] = kline.Close
|
||||
Volume, _ := convertor.ToFloat(kline.Volume)
|
||||
mapk["成交量(万手)"] = Volume / 10000.00 / 100.00
|
||||
*Kmap = append(*Kmap, mapk)
|
||||
}
|
||||
jsonData, _ := json.Marshal(Kmap)
|
||||
markdownTable, _ := JSONToMarkdownTable(jsonData)
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": stock + "日K数据",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "## " + stock + "日K数据如下:\n" + markdownTable,
|
||||
})
|
||||
logger.SugaredLogger.Infof("getKLineData=\n%s", markdownTable)
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
messages := SearchStockPriceInfo(stock, stockCode, o.CrawlTimeOut)
|
||||
if messages == nil || len(*messages) == 0 {
|
||||
logger.SugaredLogger.Error("获取股票价格失败")
|
||||
//ch <- "***❗获取股票价格失败,分析结果可能不准确***<hr>"
|
||||
@ -185,8 +368,14 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": stock + time.Now().Format(time.DateOnly) + "价格:" + price,
|
||||
"content": stock + "股价数据",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "\n## " + stock + "股价数据:\n" + price,
|
||||
})
|
||||
logger.SugaredLogger.Infof("SearchStockPriceInfo stock:%s stockCode:%s", stock, stockCode)
|
||||
logger.SugaredLogger.Infof("SearchStockPriceInfo assistant:%s", "\n## "+stock+"股价数据:\n"+price)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
@ -195,8 +384,7 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
|
||||
if checkIsIndexBasic(stock) {
|
||||
return
|
||||
}
|
||||
|
||||
messages := GetFinancialReports(stockCode, o.CrawlTimeOut)
|
||||
messages := GetFinancialReportsByXUEQIU(stockCode, o.CrawlTimeOut)
|
||||
if messages == nil || len(*messages) == 0 {
|
||||
logger.SugaredLogger.Error("获取股票财报失败")
|
||||
// "***❗获取股票财报失败,分析结果可能不准确***<hr>"
|
||||
@ -208,9 +396,13 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
|
||||
go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票财报失败,分析结果可能不准确")
|
||||
return
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": stock + "财报数据",
|
||||
})
|
||||
for _, message := range *messages {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"role": "assistant",
|
||||
"content": stock + message,
|
||||
})
|
||||
}
|
||||
@ -225,12 +417,19 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
|
||||
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取市场资讯失败,分析结果可能不准确")
|
||||
return
|
||||
}
|
||||
var messageText strings.Builder
|
||||
for _, message := range *messages {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": message,
|
||||
})
|
||||
messageText.WriteString(message + "\n")
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "市场资讯",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": messageText.String(),
|
||||
})
|
||||
|
||||
messages = GetTopNewsList(o.CrawlTimeOut)
|
||||
if messages == nil || len(*messages) == 0 {
|
||||
logger.SugaredLogger.Error("获取新闻资讯失败")
|
||||
@ -238,12 +437,18 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
|
||||
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取新闻资讯失败,分析结果可能不准确")
|
||||
return
|
||||
}
|
||||
var newsText strings.Builder
|
||||
for _, message := range *messages {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": message,
|
||||
})
|
||||
newsText.WriteString(message + "\n")
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "新闻资讯",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": newsText.String(),
|
||||
})
|
||||
}()
|
||||
|
||||
//go func() {
|
||||
@ -271,12 +476,18 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
|
||||
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股票电报资讯失败,分析结果可能不准确")
|
||||
return
|
||||
}
|
||||
var newsText strings.Builder
|
||||
for _, message := range *messages {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": message,
|
||||
})
|
||||
newsText.WriteString(message + "\n")
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": stock + "相关新闻资讯",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": newsText.String(),
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
@ -293,12 +504,18 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
|
||||
//go runtime.EventsEmit(o.ctx, "warnMsg", "❗获取股势通资讯失败,分析结果可能不准确")
|
||||
return
|
||||
}
|
||||
var newsText strings.Builder
|
||||
for _, message := range *messages {
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": message,
|
||||
})
|
||||
newsText.WriteString(message + "\n")
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": stock + "相关新闻资讯",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": newsText.String(),
|
||||
})
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
@ -306,129 +523,139 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
|
||||
"role": "user",
|
||||
"content": question,
|
||||
})
|
||||
client := resty.New()
|
||||
client.SetBaseURL(strutil.Trim(o.BaseUrl))
|
||||
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
//client.SetRetryCount(3)
|
||||
if o.TimeOut <= 0 {
|
||||
o.TimeOut = 300
|
||||
}
|
||||
client.SetTimeout(time.Duration(o.TimeOut) * time.Second)
|
||||
resp, err := client.R().
|
||||
SetDoNotParseResponse(true).
|
||||
SetBody(map[string]interface{}{
|
||||
"model": o.Model,
|
||||
"max_tokens": o.MaxTokens,
|
||||
"temperature": o.Temperature,
|
||||
"stream": true,
|
||||
"messages": msg,
|
||||
}).
|
||||
Post("/chat/completions")
|
||||
|
||||
body := resp.RawBody()
|
||||
defer body.Close()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
|
||||
//ch <- err.Error()
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": err.Error(),
|
||||
//reqJson, _ := json.Marshal(msg)
|
||||
//logger.SugaredLogger.Errorf("Stream request: \n%s\n", reqJson)
|
||||
AskAi(o, err, msg, ch, question)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func AskAi(o OpenAi, err error, messages []map[string]interface{}, ch chan map[string]any, question string) {
|
||||
client := resty.New()
|
||||
client.SetBaseURL(strutil.Trim(o.BaseUrl))
|
||||
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
//client.SetRetryCount(3)
|
||||
if o.TimeOut <= 0 {
|
||||
o.TimeOut = 300
|
||||
}
|
||||
client.SetTimeout(time.Duration(o.TimeOut) * time.Second)
|
||||
resp, err := client.R().
|
||||
SetDoNotParseResponse(true).
|
||||
SetBody(map[string]interface{}{
|
||||
"model": o.Model,
|
||||
"max_tokens": o.MaxTokens,
|
||||
"temperature": o.Temperature,
|
||||
"stream": true,
|
||||
"messages": messages,
|
||||
}).
|
||||
Post("/chat/completions")
|
||||
|
||||
body := resp.RawBody()
|
||||
defer body.Close()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
|
||||
//ch <- err.Error()
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": err.Error(),
|
||||
}
|
||||
return
|
||||
}
|
||||
//location, _ := time.LoadLocation("Asia/Shanghai")
|
||||
|
||||
scanner := bufio.NewScanner(body)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
//logger.SugaredLogger.Infof("Received data: %s", line)
|
||||
if strings.HasPrefix(line, "data:") {
|
||||
data := strutil.Trim(strings.TrimPrefix(line, "data:"))
|
||||
if data == "[DONE]" {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(body)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
logger.SugaredLogger.Infof("Received data: %s", line)
|
||||
if strings.HasPrefix(line, "data:") {
|
||||
data := strutil.Trim(strings.TrimPrefix(line, "data:"))
|
||||
if data == "[DONE]" {
|
||||
return
|
||||
}
|
||||
var streamResponse struct {
|
||||
Id string `json:"id"`
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content"`
|
||||
} `json:"delta"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
var streamResponse struct {
|
||||
Id string `json:"id"`
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content"`
|
||||
} `json:"delta"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &streamResponse); err == nil {
|
||||
for _, choice := range streamResponse.Choices {
|
||||
if content := choice.Delta.Content; content != "" {
|
||||
//ch <- content
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": content,
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("Content data: %s", content)
|
||||
if err := json.Unmarshal([]byte(data), &streamResponse); err == nil {
|
||||
for _, choice := range streamResponse.Choices {
|
||||
if content := choice.Delta.Content; content != "" {
|
||||
//ch <- content
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": content,
|
||||
"time": time.Now().Format(time.DateTime),
|
||||
}
|
||||
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
|
||||
//ch <- reasoningContent
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": reasoningContent,
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
|
||||
}
|
||||
if choice.FinishReason == "stop" {
|
||||
return
|
||||
}
|
||||
//logger.SugaredLogger.Infof("Content data: %s", content)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
|
||||
//ch <- err.Error()
|
||||
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
|
||||
//ch <- reasoningContent
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"content": err.Error(),
|
||||
}
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", data)
|
||||
//ch <- data
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": data,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": reasoningContent,
|
||||
"time": time.Now().Format(time.DateTime),
|
||||
}
|
||||
|
||||
//logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
|
||||
}
|
||||
if choice.FinishReason == "stop" {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if strutil.RemoveNonPrintable(line) != "" {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", line)
|
||||
res := &models.Resp{}
|
||||
if err := json.Unmarshal([]byte(line), res); err == nil {
|
||||
//ch <- line
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": res.Message,
|
||||
}
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
|
||||
//ch <- err.Error()
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": err.Error(),
|
||||
}
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", data)
|
||||
//ch <- data
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": data,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if strutil.RemoveNonPrintable(line) != "" {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", line)
|
||||
res := &models.Resp{}
|
||||
if err := json.Unmarshal([]byte(line), res); err == nil {
|
||||
//ch <- line
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": res.Message,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func checkIsIndexBasic(stock string) bool {
|
||||
@ -459,7 +686,7 @@ func SearchGuShiTongStockInfo(stock string, crawlTimeOut int64) *[]string {
|
||||
url = "https://gushitong.baidu.com/stock/us-" + strings.Replace(stock, "gb_", "", 1)
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("SearchGuShiTongStockInfo搜索股票-%s: %s", stock, url)
|
||||
//logger.SugaredLogger.Infof("SearchGuShiTongStockInfo搜索股票-%s: %s", stock, url)
|
||||
actions := []chromedp.Action{
|
||||
chromedp.Navigate(url),
|
||||
chromedp.WaitVisible("div.cos-tab"),
|
||||
@ -480,14 +707,13 @@ func SearchGuShiTongStockInfo(stock string, crawlTimeOut int64) *[]string {
|
||||
document.Find("div.finance-hover,div.list-date").Each(func(i int, selection *goquery.Selection) {
|
||||
text := strutil.RemoveWhiteSpace(selection.Text(), false)
|
||||
messages = append(messages, ReplaceSensitiveWords(text))
|
||||
logger.SugaredLogger.Infof("SearchGuShiTongStockInfo搜索到消息-%s: %s", "", text)
|
||||
//logger.SugaredLogger.Infof("SearchGuShiTongStockInfo搜索到消息-%s: %s", "", text)
|
||||
})
|
||||
logger.SugaredLogger.Infof("messages:%d", len(messages))
|
||||
//logger.SugaredLogger.Infof("messages:%d", len(messages))
|
||||
}
|
||||
return &messages
|
||||
}
|
||||
|
||||
func GetFinancialReports(stockCode string, crawlTimeOut int64) *[]string {
|
||||
func GetFinancialReportsByXUEQIU(stockCode string, crawlTimeOut int64) *[]string {
|
||||
if strutil.HasPrefixAny(stockCode, []string{"HK", "hk"}) {
|
||||
stockCode = strings.ReplaceAll(stockCode, "hk", "")
|
||||
stockCode = strings.ReplaceAll(stockCode, "HK", "")
|
||||
@ -496,91 +722,74 @@ func GetFinancialReports(stockCode string, crawlTimeOut int64) *[]string {
|
||||
stockCode = strings.ReplaceAll(stockCode, "us", "")
|
||||
stockCode = strings.ReplaceAll(stockCode, "gb_", "")
|
||||
}
|
||||
|
||||
// 创建一个 chromedp 上下文
|
||||
timeoutCtx, timeoutCtxCancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
|
||||
defer timeoutCtxCancel()
|
||||
var ctx context.Context
|
||||
var cancel context.CancelFunc
|
||||
path := getConfig().BrowserPath
|
||||
logger.SugaredLogger.Infof("GetFinancialReports path:%s", path)
|
||||
|
||||
if path != "" {
|
||||
pctx, pcancel := chromedp.NewExecAllocator(
|
||||
timeoutCtx,
|
||||
chromedp.ExecPath(path),
|
||||
chromedp.Flag("headless", true),
|
||||
chromedp.Flag("disable-javascript", false),
|
||||
chromedp.Flag("disable-gpu", true),
|
||||
chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"),
|
||||
chromedp.Flag("disable-background-networking", true),
|
||||
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
|
||||
chromedp.Flag("disable-background-timer-throttling", true),
|
||||
chromedp.Flag("disable-backgrounding-occluded-windows", true),
|
||||
chromedp.Flag("disable-breakpad", true),
|
||||
chromedp.Flag("disable-client-side-phishing-detection", true),
|
||||
chromedp.Flag("disable-default-apps", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
chromedp.Flag("disable-extensions", true),
|
||||
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
|
||||
chromedp.Flag("disable-hang-monitor", true),
|
||||
chromedp.Flag("disable-ipc-flooding-protection", true),
|
||||
chromedp.Flag("disable-popup-blocking", true),
|
||||
chromedp.Flag("disable-prompt-on-repost", true),
|
||||
chromedp.Flag("disable-renderer-backgrounding", true),
|
||||
chromedp.Flag("disable-sync", true),
|
||||
chromedp.Flag("force-color-profile", "srgb"),
|
||||
chromedp.Flag("metrics-recording-only", true),
|
||||
chromedp.Flag("safebrowsing-disable-auto-update", true),
|
||||
chromedp.Flag("enable-automation", true),
|
||||
chromedp.Flag("password-store", "basic"),
|
||||
chromedp.Flag("use-mock-keychain", true),
|
||||
)
|
||||
defer pcancel()
|
||||
ctx, cancel = chromedp.NewContext(
|
||||
pctx,
|
||||
chromedp.WithLogf(logger.SugaredLogger.Infof),
|
||||
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
|
||||
)
|
||||
} else {
|
||||
ctx, cancel = chromedp.NewContext(
|
||||
timeoutCtx,
|
||||
chromedp.WithLogf(logger.SugaredLogger.Infof),
|
||||
chromedp.WithErrorf(logger.SugaredLogger.Errorf),
|
||||
)
|
||||
}
|
||||
defer cancel()
|
||||
var htmlContent string
|
||||
url := fmt.Sprintf("https://xueqiu.com/snowman/S/%s/detail#/ZYCWZB", stockCode)
|
||||
err := chromedp.Run(ctx,
|
||||
chromedp.Navigate(url),
|
||||
// 等待页面加载完成,可以根据需要调整等待时间
|
||||
chromedp.WaitVisible("table.table", chromedp.ByQuery),
|
||||
chromedp.OuterHTML("html", &htmlContent, chromedp.ByQuery),
|
||||
)
|
||||
waitVisible := "div.tab-table-responsive table"
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://xueqiu.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
var markdown strings.Builder
|
||||
markdown.WriteString("\n## 财务数据:\n")
|
||||
html, ok := crawlerAPI.GetHtml(url, waitVisible, true)
|
||||
if !ok {
|
||||
return &[]string{""}
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
GetTableMarkdown(document, waitVisible, &markdown)
|
||||
return &[]string{markdown.String()}
|
||||
}
|
||||
func GetFinancialReports(stockCode string, crawlTimeOut int64) *[]string {
|
||||
url := "https://emweb.securities.eastmoney.com/pc_hsf10/pages/index.html?type=web&code=" + stockCode + "#/cwfx"
|
||||
waitVisible := "div.report_table table"
|
||||
if strutil.HasPrefixAny(stockCode, []string{"HK", "hk"}) {
|
||||
stockCode = strings.ReplaceAll(stockCode, "hk", "")
|
||||
stockCode = strings.ReplaceAll(stockCode, "HK", "")
|
||||
url = "https://emweb.securities.eastmoney.com/PC_HKF10/pages/home/index.html?code=" + stockCode + "&type=web&color=w#/NewFinancialAnalysis"
|
||||
waitVisible = "div table.commonTable"
|
||||
}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"us", "gb_"}) {
|
||||
stockCode = strings.ReplaceAll(stockCode, "us", "")
|
||||
stockCode = strings.ReplaceAll(stockCode, "gb_", "")
|
||||
url = "https://emweb.securities.eastmoney.com/pc_usf10/pages/index.html?type=web&code=" + stockCode + "#/cwfx"
|
||||
waitVisible = "div.zyzb_table_detail table"
|
||||
|
||||
}
|
||||
|
||||
//logger.SugaredLogger.Infof("GetFinancialReports搜索股票-%s: %s", stockCode, url)
|
||||
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://emweb.securities.eastmoney.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
var markdown strings.Builder
|
||||
markdown.WriteString("\n## 财务数据:\n")
|
||||
html, ok := crawlerAPI.GetHtml(url, waitVisible, true)
|
||||
if !ok {
|
||||
return &[]string{""}
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return &[]string{}
|
||||
}
|
||||
var messages []string
|
||||
document.Find("table tr").Each(func(i int, selection *goquery.Selection) {
|
||||
tr := ""
|
||||
selection.Find("th,td").Each(func(i int, selection *goquery.Selection) {
|
||||
ret := selection.Find("p").First().Text()
|
||||
if ret == "" {
|
||||
ret = selection.Text()
|
||||
}
|
||||
text := strutil.RemoveNonPrintable(ret)
|
||||
tr += text + " "
|
||||
})
|
||||
logger.SugaredLogger.Infof("%s", tr+" \n")
|
||||
messages = append(messages, tr+" \n")
|
||||
})
|
||||
return &messages
|
||||
GetTableMarkdown(document, waitVisible, &markdown)
|
||||
return &[]string{markdown.String()}
|
||||
}
|
||||
|
||||
func GetTelegraphList(crawlTimeOut int64) *[]string {
|
||||
@ -599,7 +808,7 @@ func GetTelegraphList(crawlTimeOut int64) *[]string {
|
||||
}
|
||||
var telegraph []string
|
||||
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
|
||||
logger.SugaredLogger.Info(selection.Text())
|
||||
//logger.SugaredLogger.Info(selection.Text())
|
||||
telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
|
||||
})
|
||||
return &telegraph
|
||||
@ -621,7 +830,7 @@ func GetTopNewsList(crawlTimeOut int64) *[]string {
|
||||
}
|
||||
var telegraph []string
|
||||
document.Find("div.home-article-title a,div.home-article-rec a").Each(func(i int, selection *goquery.Selection) {
|
||||
logger.SugaredLogger.Info(selection.Text())
|
||||
//logger.SugaredLogger.Info(selection.Text())
|
||||
telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text()))
|
||||
})
|
||||
return &telegraph
|
||||
@ -640,6 +849,6 @@ func (o OpenAi) SaveAIResponseResult(stockCode, stockName, result, chatId, quest
|
||||
|
||||
func (o OpenAi) GetAIResponseResult(stock string) *models.AIResponseResult {
|
||||
var result models.AIResponseResult
|
||||
db.Dao.Where("stock_code = ?", stock).Order("id desc").Limit(1).First(&result)
|
||||
db.Dao.Where("stock_code = ?", stock).Order("id desc").Limit(1).Find(&result)
|
||||
return &result
|
||||
}
|
||||
|
@ -2,13 +2,14 @@ package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go-stock/backend/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
|
||||
//db.Init("../../data/stock.db")
|
||||
db.Init("../../data/stock.db")
|
||||
ai := NewDeepSeekOpenAi(context.TODO())
|
||||
res := ai.NewChatStream("上海贝岭", "sh600171", "分析以上股票资金流入信息,找出适合买入的股票,给出具体操作建议")
|
||||
res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
|
||||
for {
|
||||
select {
|
||||
case msg := <-res:
|
||||
@ -18,11 +19,12 @@ func TestNewDeepSeekOpenAiConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetTopNewsList(t *testing.T) {
|
||||
GetTopNewsList(30)
|
||||
news := GetTopNewsList(30)
|
||||
t.Log(news)
|
||||
}
|
||||
|
||||
func TestSearchGuShiTongStockInfo(t *testing.T) {
|
||||
//db.Init("../../data/stock.db")
|
||||
db.Init("../../data/stock.db")
|
||||
SearchGuShiTongStockInfo("hk01810", 60)
|
||||
SearchGuShiTongStockInfo("sh600745", 60)
|
||||
SearchGuShiTongStockInfo("gb_goog", 60)
|
||||
|
115
backend/data/pool.go
Normal file
115
backend/data/pool.go
Normal file
@ -0,0 +1,115 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go-stock/backend/logger"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
// BrowserPool 浏览器池结构
|
||||
type BrowserPool struct {
|
||||
pool chan *context.Context
|
||||
mu sync.Mutex
|
||||
size int
|
||||
}
|
||||
|
||||
// NewBrowserPool 创建新的浏览器池
|
||||
func NewBrowserPool(size int) *BrowserPool {
|
||||
pool := make(chan *context.Context, size)
|
||||
for i := 0; i < size; i++ {
|
||||
path := GetConfig().BrowserPath
|
||||
crawlTimeOut := GetConfig().CrawlTimeOut
|
||||
if crawlTimeOut < 15 {
|
||||
crawlTimeOut = 30
|
||||
}
|
||||
if path != "" {
|
||||
ctx, _ := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
|
||||
ctx, _ = chromedp.NewExecAllocator(
|
||||
ctx,
|
||||
chromedp.ExecPath(path),
|
||||
chromedp.Flag("headless", true),
|
||||
chromedp.Flag("blink-settings", "imagesEnabled=false"),
|
||||
chromedp.Flag("disable-javascript", false),
|
||||
chromedp.Flag("disable-gpu", true),
|
||||
//chromedp.UserAgent(""),
|
||||
chromedp.Flag("disable-background-networking", true),
|
||||
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
|
||||
chromedp.Flag("disable-background-timer-throttling", true),
|
||||
chromedp.Flag("disable-backgrounding-occluded-windows", true),
|
||||
chromedp.Flag("disable-breakpad", true),
|
||||
chromedp.Flag("disable-client-side-phishing-detection", true),
|
||||
chromedp.Flag("disable-default-apps", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
chromedp.Flag("disable-extensions", true),
|
||||
chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"),
|
||||
chromedp.Flag("disable-hang-monitor", true),
|
||||
chromedp.Flag("disable-ipc-flooding-protection", true),
|
||||
chromedp.Flag("disable-popup-blocking", true),
|
||||
chromedp.Flag("disable-prompt-on-repost", true),
|
||||
chromedp.Flag("disable-renderer-backgrounding", true),
|
||||
chromedp.Flag("disable-sync", true),
|
||||
chromedp.Flag("force-color-profile", "srgb"),
|
||||
chromedp.Flag("metrics-recording-only", true),
|
||||
chromedp.Flag("safebrowsing-disable-auto-update", true),
|
||||
chromedp.Flag("enable-automation", true),
|
||||
chromedp.Flag("password-store", "basic"),
|
||||
chromedp.Flag("use-mock-keychain", true),
|
||||
)
|
||||
ctx, _ = chromedp.NewContext(ctx, chromedp.WithLogf(logger.SugaredLogger.Infof))
|
||||
pool <- &ctx
|
||||
}
|
||||
}
|
||||
return &BrowserPool{
|
||||
pool: pool,
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
// Get 从池中获取浏览器实例
|
||||
func (pool *BrowserPool) Get() *context.Context {
|
||||
return <-pool.pool
|
||||
}
|
||||
|
||||
// Put 将浏览器实例放回池中
|
||||
func (pool *BrowserPool) Put(ctx *context.Context) {
|
||||
pool.mu.Lock()
|
||||
defer pool.mu.Unlock()
|
||||
// 检查池是否已满
|
||||
if len(pool.pool) >= pool.size {
|
||||
// 池已满,关闭并丢弃这个实例
|
||||
chromedp.Cancel(*ctx)
|
||||
return
|
||||
}
|
||||
chromedp.Cancel(*ctx)
|
||||
pool.pool <- ctx
|
||||
}
|
||||
|
||||
// Close 关闭池中的所有浏览器实例
|
||||
func (pool *BrowserPool) Close() {
|
||||
close(pool.pool)
|
||||
for ctx := range pool.pool {
|
||||
chromedp.Cancel(*ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// FetchPage 使用浏览器池获取页面内容
|
||||
func (pool *BrowserPool) FetchPage(url, waitVisible string) (string, error) {
|
||||
// 从池中获取浏览器实例
|
||||
ctx := pool.Get()
|
||||
defer pool.Put(ctx) // 使用完毕后放回池中
|
||||
var htmlContent string
|
||||
err := chromedp.Run(*ctx,
|
||||
chromedp.Navigate(url),
|
||||
chromedp.WaitVisible(waitVisible, chromedp.ByQuery), // 确保 元素可见
|
||||
chromedp.WaitReady(waitVisible, chromedp.ByQuery), // 确保 元素准备好
|
||||
chromedp.InnerHTML("body", &htmlContent),
|
||||
chromedp.Evaluate(`window.close()`, nil),
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return htmlContent, nil
|
||||
}
|
18
backend/data/pool_test.go
Normal file
18
backend/data/pool_test.go
Normal file
@ -0,0 +1,18 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPool(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
pool := NewBrowserPool(1)
|
||||
go pool.FetchPage("https://fund.eastmoney.com/016533.html", "body")
|
||||
go pool.FetchPage("https://fund.eastmoney.com/217021.html", "body")
|
||||
go pool.FetchPage("https://fund.eastmoney.com/001125.html", "body")
|
||||
|
||||
select {}
|
||||
|
||||
}
|
75
backend/data/prompt_template_api.go
Normal file
75
backend/data/prompt_template_api.go
Normal file
@ -0,0 +1,75 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
)
|
||||
|
||||
type PromptTemplateApi struct {
|
||||
}
|
||||
|
||||
func (t PromptTemplateApi) GetPromptTemplates(name string, promptType string) *[]models.PromptTemplate {
|
||||
var result []models.PromptTemplate
|
||||
if name != "" && promptType != "" {
|
||||
db.Dao.Model(&models.PromptTemplate{}).Where("name=? and type=?", name, promptType).Find(&result)
|
||||
}
|
||||
if name != "" && promptType == "" {
|
||||
db.Dao.Model(&models.PromptTemplate{}).Where("name=?", name).Find(&result)
|
||||
}
|
||||
if name == "" && promptType != "" {
|
||||
db.Dao.Model(&models.PromptTemplate{}).Where("type=?", promptType).Find(&result)
|
||||
}
|
||||
if name == "" && promptType == "" {
|
||||
db.Dao.Model(&models.PromptTemplate{}).Find(&result)
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
func (t PromptTemplateApi) AddPrompt(template models.PromptTemplate) string {
|
||||
var tmp models.PromptTemplate
|
||||
db.Dao.Model(&models.PromptTemplate{}).Where("id=?", template.ID).First(&tmp)
|
||||
if tmp.ID == 0 {
|
||||
err := db.Dao.Model(&models.PromptTemplate{}).Create(&models.PromptTemplate{
|
||||
Content: template.Content,
|
||||
Name: template.Name,
|
||||
Type: template.Type,
|
||||
}).Error
|
||||
if err != nil {
|
||||
return "添加失败"
|
||||
} else {
|
||||
return "添加成功"
|
||||
}
|
||||
} else {
|
||||
err := db.Dao.Model(&models.PromptTemplate{}).Where("id=?", template.ID).Updates(template).Error
|
||||
if err != nil {
|
||||
return "更新失败"
|
||||
} else {
|
||||
return "更新成功"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t PromptTemplateApi) DelPrompt(Id uint) string {
|
||||
template := &models.PromptTemplate{}
|
||||
db.Dao.Model(template).Where("id=?", Id).Find(template)
|
||||
if template.ID > 0 {
|
||||
err := db.Dao.Model(template).Delete(template).Error
|
||||
if err != nil {
|
||||
return "删除失败"
|
||||
} else {
|
||||
return "删除成功"
|
||||
}
|
||||
}
|
||||
return "模板信息不存在"
|
||||
}
|
||||
|
||||
func (t PromptTemplateApi) GetPromptTemplateByID(id int) string {
|
||||
prompt := &models.PromptTemplate{}
|
||||
db.Dao.Model(&models.PromptTemplate{}).Where("id=?", id).First(prompt)
|
||||
logger.SugaredLogger.Infof("GetPromptTemplateByID:%d %s", id, prompt.Content)
|
||||
return prompt.Content
|
||||
}
|
||||
func NewPromptTemplateApi() *PromptTemplateApi {
|
||||
return &PromptTemplateApi{}
|
||||
}
|
55
backend/data/search_stock_api.go
Normal file
55
backend/data/search_stock_api.go
Normal file
@ -0,0 +1,55 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/logger"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/6/28 21:02
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type SearchStockApi struct {
|
||||
words string
|
||||
}
|
||||
|
||||
func NewSearchStockApi(words string) *SearchStockApi {
|
||||
return &SearchStockApi{words: words}
|
||||
}
|
||||
func (s SearchStockApi) SearchStock() map[string]any {
|
||||
url := "https://np-tjxg-g.eastmoney.com/api/smart-tag/stock/v3/pw/search-code"
|
||||
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
|
||||
SetHeader("Host", "np-tjxg-g.eastmoney.com").
|
||||
SetHeader("Origin", "https://xuangu.eastmoney.com").
|
||||
SetHeader("Referer", "https://xuangu.eastmoney.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(fmt.Sprintf(`{
|
||||
"keyWord": "%s",
|
||||
"pageSize": 50000,
|
||||
"pageNo": 1,
|
||||
"fingerprint": "e38b5faabf9378c8238e57219f0ebc9b",
|
||||
"gids": [],
|
||||
"matchWord": "",
|
||||
"timestamp": "1751113883290349",
|
||||
"shareToGuba": false,
|
||||
"requestId": "8xTWgCDAjvQ5lmvz5mDA3Ydk2AE4yoiJ1751113883290",
|
||||
"needCorrect": true,
|
||||
"removedConditionIdList": [],
|
||||
"xcId": "xc0af28549ab330013ed",
|
||||
"ownSelectAll": false,
|
||||
"dxInfo": [],
|
||||
"extraCondition": ""
|
||||
}`, s.words)).Post(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("SearchStock-err:%+v", err)
|
||||
return map[string]any{}
|
||||
}
|
||||
respMap := map[string]any{}
|
||||
json.Unmarshal(resp.Body(), &respMap)
|
||||
//logger.SugaredLogger.Infof("resp:%+v", respMap["data"])
|
||||
return respMap
|
||||
}
|
25
backend/data/search_stock_api_test.go
Normal file
25
backend/data/search_stock_api_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSearchStock(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
res := NewSearchStockApi("算力股;净利润连续3年增长").SearchStock()
|
||||
data := res["data"].(map[string]any)
|
||||
result := data["result"].(map[string]any)
|
||||
dataList := result["dataList"].([]any)
|
||||
for _, v := range dataList {
|
||||
d := v.(map[string]any)
|
||||
logger.SugaredLogger.Infof("%s:%s", d["INDUSTRY"], d["SECURITY_SHORT_NAME"])
|
||||
}
|
||||
//columns := result["columns"].([]any)
|
||||
//for _, v := range columns {
|
||||
// logger.SugaredLogger.Infof("v:%+v", v)
|
||||
//}
|
||||
|
||||
}
|
@ -30,6 +30,11 @@ type Settings struct {
|
||||
KDays int64 `json:"kDays"`
|
||||
EnableDanmu bool `json:"enableDanmu"`
|
||||
BrowserPath string `json:"browserPath"`
|
||||
EnableNews bool `json:"enableNews"`
|
||||
DarkTheme bool `json:"darkTheme"`
|
||||
BrowserPoolSize int `json:"browserPoolSize"`
|
||||
EnableFund bool `json:"enableFund"`
|
||||
EnablePushNews bool `json:"enablePushNews"`
|
||||
}
|
||||
|
||||
func (receiver Settings) TableName() string {
|
||||
@ -71,6 +76,10 @@ func (s SettingsApi) UpdateConfig() string {
|
||||
"k_days": s.Config.KDays,
|
||||
"enable_danmu": s.Config.EnableDanmu,
|
||||
"browser_path": s.Config.BrowserPath,
|
||||
"enable_news": s.Config.EnableNews,
|
||||
"dark_theme": s.Config.DarkTheme,
|
||||
"enable_fund": s.Config.EnableFund,
|
||||
"enable_push_news": s.Config.EnablePushNews,
|
||||
})
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("未找到配置,创建默认配置:%+v", s.Config)
|
||||
@ -95,6 +104,10 @@ func (s SettingsApi) UpdateConfig() string {
|
||||
KDays: s.Config.KDays,
|
||||
EnableDanmu: s.Config.EnableDanmu,
|
||||
BrowserPath: s.Config.BrowserPath,
|
||||
EnableNews: s.Config.EnableNews,
|
||||
DarkTheme: s.Config.DarkTheme,
|
||||
EnableFund: s.Config.EnableFund,
|
||||
EnablePushNews: s.Config.EnablePushNews,
|
||||
})
|
||||
}
|
||||
return "保存成功!"
|
||||
@ -117,7 +130,9 @@ func (s SettingsApi) GetConfig() *Settings {
|
||||
if settings.BrowserPath == "" {
|
||||
settings.BrowserPath, _ = CheckBrowserOnWindows()
|
||||
}
|
||||
|
||||
if settings.BrowserPoolSize <= 0 {
|
||||
settings.BrowserPoolSize = 1
|
||||
}
|
||||
return &settings
|
||||
}
|
||||
|
||||
|
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
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/random"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go-stock/backend/db"
|
||||
@ -22,17 +23,31 @@ import (
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestGetTelegraph(t *testing.T) {
|
||||
GetTelegraphList(30)
|
||||
db.Init("../../data/stock.db")
|
||||
|
||||
//telegraphs := GetTelegraphList(30)
|
||||
//for _, telegraph := range *telegraphs {
|
||||
// logger.SugaredLogger.Info(telegraph)
|
||||
//}
|
||||
list := NewMarketNewsApi().GetNewTelegraph(30)
|
||||
for _, telegraph := range *list {
|
||||
logger.SugaredLogger.Infof("telegraph:%+v", telegraph)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFinancialReports(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
//GetFinancialReports("sz000802", 30)
|
||||
//GetFinancialReports("hk00927", 30)
|
||||
GetFinancialReports("gb_aapl", 30)
|
||||
//GetFinancialReports("gb_aapl", 30)
|
||||
GetFinancialReportsByXUEQIU("sz000802", 30)
|
||||
GetFinancialReportsByXUEQIU("gb_aapl", 30)
|
||||
GetFinancialReportsByXUEQIU("hk00927", 30)
|
||||
|
||||
}
|
||||
|
||||
func TestGetTelegraphSearch(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
//url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram"
|
||||
messages := SearchStockInfo("谷歌", "telegram", 30)
|
||||
for _, message := range *messages {
|
||||
@ -42,17 +57,73 @@ func TestGetTelegraphSearch(t *testing.T) {
|
||||
//https://www.cls.cn/stock?code=sh600745
|
||||
}
|
||||
func TestSearchStockInfoByCode(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
SearchStockInfoByCode("sh600745")
|
||||
}
|
||||
|
||||
func TestSearchStockPriceInfo(t *testing.T) {
|
||||
//db.Init("../../data/stock.db")
|
||||
//SearchStockPriceInfo("hk06030", 30)
|
||||
//SearchStockPriceInfo("sh600171", 30)
|
||||
//SearchStockPriceInfo("gb_aapl", 30)
|
||||
SearchStockPriceInfo("bj430198", 30)
|
||||
db.Init("../../data/stock.db")
|
||||
//SearchStockPriceInfo("中信证券", "hk06030", 30)
|
||||
SearchStockPriceInfo("上海贝岭", "sh600171", 30)
|
||||
//SearchStockPriceInfo("苹果公司", "gb_aapl", 30)
|
||||
//SearchStockPriceInfo("微创光电", "bj430198", 30)
|
||||
//getZSInfo("创业板指数", "sz399006", 30)
|
||||
//getZSInfo("上证综合指数", "sh000001", 30)
|
||||
//getZSInfo("沪深300指数", "sh000300", 30)
|
||||
|
||||
}
|
||||
func TestGetStockMinutePriceData(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
data, date := NewStockDataApi().GetStockMinutePriceData("usTSLA.OQ")
|
||||
logger.SugaredLogger.Infof("date:%s", date)
|
||||
logger.SugaredLogger.Infof("%+#v", *data)
|
||||
}
|
||||
func TestGetKLineData(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
k := NewStockDataApi().GetKLineData("sh600171", "240", 30)
|
||||
//for _, kline := range *k {
|
||||
// logger.SugaredLogger.Infof("%+#v", kline)
|
||||
//}
|
||||
jsonData, _ := json.Marshal(*k)
|
||||
markdownTable, err := JSONToMarkdownTable(jsonData)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Marshal error:%s", err.Error())
|
||||
}
|
||||
logger.SugaredLogger.Infof("markdownTable:\n%s", markdownTable)
|
||||
|
||||
}
|
||||
func TestGetHK_KLineData(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
k := NewStockDataApi().GetHK_KLineData("hk01810", "day", 1)
|
||||
jsonData, _ := json.Marshal(*k)
|
||||
markdownTable, err := JSONToMarkdownTable(jsonData)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Marshal error:%s", err.Error())
|
||||
}
|
||||
logger.SugaredLogger.Infof("markdownTable:\n%s", markdownTable)
|
||||
|
||||
}
|
||||
|
||||
func TestGetHKStockInfo(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
//NewStockDataApi().GetHKStockInfo(200)
|
||||
//NewStockDataApi().GetSinaHKStockInfo()
|
||||
//m:105,m:106,m:107 //美股
|
||||
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
|
||||
for i := 1; i <= 592; i++ {
|
||||
NewStockDataApi().getDCStockInfo("us", i, 20)
|
||||
time.Sleep(time.Duration(random.RandInt(1, 3)) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTxStockData(t *testing.T) {
|
||||
str := "v_r_hk09660=\"100~地平线机器人-W~09660~6.340~5.690~5.800~210980204.0~0~0~6.340~0~0~0~0~0~0~0~0~0~6.340~0~0~0~0~0~0~0~0~0~210980204.0~2025/04/29\n14:14:52~0.650~11.42~6.450~5.710~6.340~210980204.0~1295585259.040~0~33.03~~0~0~13.01~702.2123~836.8986~HORIZONROBOT-W~0.00~10.380~3.320~1.00~-53.74~0~0~0~0~0~33.03~6.50~1.90~600~76.11~19.85~GP~19.70~11.51~0.63~-17.23~46.76~13200293682.00~11075904412.00~33.03~0.000~6.141~58.90~HKD~1~30\";"
|
||||
//str = "v_sz002241=\"51~歌尔股份~002241~22.26~22.27~0.00~0~0~0~22.26~1004~0.00~0~0.00~0~0.00~0~0.00~0~22.26~1004~0.00~558~0.00~0~0.00~0~0.00~0~~20250509092233~-0.01~-0.04~0.00~0.00~22.26/0/0~0~0~0.00~28.21~~0.00~0.00~0.00~686.46~777.09~2.31~24.50~20.04~0.00~-558~0.00~41.44~29.16~~~1.24~0.0000~0.0000~0~\n~GP-A~-13.75~6.76~1.09~8.18~3.39~30.63~15.70~6.87~17.47~-23.95~3083811231~3490989083~-21.75~12.02~3083811231~~~39.36~-0.04~~CNY~0~~0.00~0\";"
|
||||
str = "v_sz002241=\"51~歌尔股份~002241~21.92~22.27~22.14~109872~40211~69642~21.91~25~21.90~961~21.89~257~21.88~748~21.87~665~21.92~86~21.93~168~21.94~556~21.95~171~21.96~85~~20250509094209~-0.35~-1.57~22.16~21.84~21.92/109872/241183171~109872~24118~0.36~27.78~~22.16~21.84~1.44~675.97~765.22~2.27~24.50~20.04~2.57~1590~21.95~40.80~28.71~~~1.24~24118.3171~0.0000~0~\n~GP-A~-15.07~5.13~1.11~8.18~3.39~30.63~15.70~5.23~15.67~-25.11~3083811231~3490989083~42.72~10.31~3083811231~~~37.23~0.18~~CNY~0~~21.85~1952\";"
|
||||
//str = "v_r_hk09660=\"100~地平线机器人-W~09660~6.860~7.000~7.010~21157200.0~0~0~6.860~0~0~0~0~0~0~0~0~0~6.860~0~0~0~0~0~0~0~0~0~21157200.0~2025/05/09\n09:43:13~-0.140~-2.00~7.030~6.730~6.860~21157200.0~144331073.000~0~35.74~~0~0~4.29~759.8070~905.5401~HORIZONROBOT-W~0.00~10.380~3.320~2.93~11.10~0~0~0~0~0~35.74~7.04~0.19~600~90.56~4.73~GP~19.70~11.51~17.26~48.48~13.58~13200293682.00~11075904412.00~35.74~0.000~6.822~71.93~HKD~1~30\";"
|
||||
info, _ := ParseTxStockData(str)
|
||||
logger.SugaredLogger.Infof("%+#v", info)
|
||||
}
|
||||
|
||||
func TestGetRealTimeStockPriceInfo(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
@ -112,7 +183,7 @@ func TestParseFullSingleStockData(t *testing.T) {
|
||||
func TestNewStockDataApi(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
stockDataApi := NewStockDataApi()
|
||||
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745", "gb_tsla")
|
||||
datas, _ := stockDataApi.GetStockCodeRealTimeData("sh600859", "sh600745", "gb_tsla", "hk09660", "hk00700")
|
||||
for _, data := range *datas {
|
||||
t.Log(data)
|
||||
}
|
||||
@ -173,7 +244,7 @@ func TestReadFile(t *testing.T) {
|
||||
func TestFollowedList(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
stockDataApi := NewStockDataApi()
|
||||
stockDataApi.GetFollowList()
|
||||
stockDataApi.GetFollowList(1)
|
||||
|
||||
}
|
||||
|
||||
|
80
backend/data/stock_group_api.go
Normal file
80
backend/data/stock_group_api.go
Normal file
@ -0,0 +1,80 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"go-stock/backend/db"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/4/3 11:18
|
||||
// @Desc
|
||||
// -----------------------------------------------------------------------------------
|
||||
type Group struct {
|
||||
gorm.Model
|
||||
Name string `json:"name" gorm:"index"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
func (Group) TableName() string {
|
||||
return "stock_groups"
|
||||
}
|
||||
|
||||
type GroupStock struct {
|
||||
gorm.Model
|
||||
StockCode string `json:"stockCode" gorm:"index"`
|
||||
GroupId int `json:"groupId" gorm:"index"`
|
||||
GroupInfo Group `json:"groupInfo" gorm:"foreignKey:GroupId;references:ID"`
|
||||
}
|
||||
|
||||
func (GroupStock) TableName() string {
|
||||
return "group_stock_info"
|
||||
}
|
||||
|
||||
type StockGroupApi struct {
|
||||
dao *gorm.DB
|
||||
}
|
||||
|
||||
func NewStockGroupApi(dao *gorm.DB) *StockGroupApi {
|
||||
return &StockGroupApi{dao: db.Dao}
|
||||
}
|
||||
|
||||
func (receiver StockGroupApi) AddGroup(group Group) bool {
|
||||
err := receiver.dao.Where("name = ?", group.Name).FirstOrCreate(&group).Updates(&Group{
|
||||
Name: group.Name,
|
||||
Sort: group.Sort,
|
||||
}).Error
|
||||
return err == nil
|
||||
}
|
||||
func (receiver StockGroupApi) GetGroupList() []Group {
|
||||
var groups []Group
|
||||
receiver.dao.Find(&groups)
|
||||
return groups
|
||||
}
|
||||
func (receiver StockGroupApi) GetGroupStockByGroupId(groupId int) []GroupStock {
|
||||
var stockGroup []GroupStock
|
||||
receiver.dao.Preload("GroupInfo").Where("group_id = ?", groupId).Find(&stockGroup)
|
||||
return stockGroup
|
||||
}
|
||||
|
||||
func (receiver StockGroupApi) AddStockGroup(groupId int, stockCode string) bool {
|
||||
err := receiver.dao.Where("group_id = ? and stock_code = ?", groupId, stockCode).FirstOrCreate(&GroupStock{
|
||||
GroupId: groupId,
|
||||
StockCode: stockCode,
|
||||
}).Updates(&GroupStock{
|
||||
GroupId: groupId,
|
||||
StockCode: stockCode,
|
||||
}).Error
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (receiver StockGroupApi) RemoveStockGroup(code string, name string, id int) bool {
|
||||
err := receiver.dao.Where("group_id = ? and stock_code = ?", id, code).Delete(&GroupStock{}).Error
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (receiver StockGroupApi) RemoveGroup(id int) bool {
|
||||
err := receiver.dao.Where("id = ?", id).Delete(&Group{}).Error
|
||||
err = receiver.dao.Where("group_id = ?", id).Delete(&GroupStock{}).Error
|
||||
return err == nil
|
||||
|
||||
}
|
290
backend/data/stock_sentiment_analysis.go
Normal file
290
backend/data/stock_sentiment_analysis.go
Normal file
@ -0,0 +1,290 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/go-ego/gse"
|
||||
"go-stock/backend/logger"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 金融情感词典,包含股票市场相关的专业词汇
|
||||
var (
|
||||
seg gse.Segmenter
|
||||
|
||||
// 正面金融词汇及其权重
|
||||
positiveFinanceWords = map[string]float64{
|
||||
"上涨": 2.0, "涨停": 3.0, "牛市": 3.0, "反弹": 2.0, "新高": 2.5,
|
||||
"利好": 2.5, "增持": 2.0, "买入": 2.0, "推荐": 1.5, "看多": 2.0,
|
||||
"盈利": 2.0, "增长": 2.0, "超预期": 2.5, "强劲": 1.5, "回升": 1.5,
|
||||
"复苏": 2.0, "突破": 2.0, "创新高": 3.0, "回暖": 1.5, "上扬": 1.5,
|
||||
"利好消息": 3.0, "收益增长": 2.5, "利润增长": 2.5, "业绩优异": 2.5,
|
||||
"潜力股": 2.0, "绩优股": 2.0, "强势": 1.5, "走高": 1.5, "攀升": 1.5,
|
||||
"大涨": 2.5, "飙升": 3.0, "井喷": 3.0, "爆发": 2.5, "暴涨": 3.0,
|
||||
}
|
||||
|
||||
// 负面金融词汇及其权重
|
||||
negativeFinanceWords = map[string]float64{
|
||||
"下跌": 2.0, "跌停": 3.0, "熊市": 3.0, "回调": 1.5, "新低": 2.5,
|
||||
"利空": 2.5, "减持": 2.0, "卖出": 2.0, "看空": 2.0, "亏损": 2.5,
|
||||
"下滑": 2.0, "萎缩": 2.0, "不及预期": 2.5, "疲软": 1.5, "恶化": 2.0,
|
||||
"衰退": 2.0, "跌破": 2.0, "创新低": 3.0, "走弱": 1.5, "下挫": 1.5,
|
||||
"利空消息": 3.0, "收益下降": 2.5, "利润下滑": 2.5, "业绩不佳": 2.5,
|
||||
"垃圾股": 2.0, "风险股": 2.0, "弱势": 1.5, "走低": 1.5, "缩量": 2.5,
|
||||
"大跌": 2.5, "暴跌": 3.0, "崩盘": 3.0, "跳水": 3.0, "重挫": 3.0,
|
||||
}
|
||||
|
||||
// 否定词,用于反转情感极性
|
||||
negationWords = map[string]struct{}{
|
||||
"不": {}, "没": {}, "无": {}, "非": {}, "未": {}, "别": {}, "勿": {},
|
||||
}
|
||||
|
||||
// 程度副词,用于调整情感强度
|
||||
degreeWords = map[string]float64{
|
||||
"非常": 1.8, "极其": 2.2, "太": 1.8, "很": 1.5,
|
||||
"比较": 0.8, "稍微": 0.6, "有点": 0.7, "显著": 1.5,
|
||||
"大幅": 1.8, "急剧": 2.0, "轻微": 0.6, "小幅": 0.7,
|
||||
}
|
||||
|
||||
// 转折词,用于识别情感转折
|
||||
transitionWords = map[string]struct{}{
|
||||
"但是": {}, "然而": {}, "不过": {}, "却": {}, "可是": {},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// 加载默认词典
|
||||
err := seg.LoadDict()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// SentimentResult 情感分析结果类型
|
||||
type SentimentResult struct {
|
||||
Score float64 // 情感得分
|
||||
Category SentimentType // 情感类别
|
||||
PositiveCount int // 正面词数量
|
||||
NegativeCount int // 负面词数量
|
||||
Description string // 情感描述
|
||||
}
|
||||
|
||||
// SentimentType 情感类型枚举
|
||||
type SentimentType int
|
||||
|
||||
const (
|
||||
Positive SentimentType = iota
|
||||
Negative
|
||||
Neutral
|
||||
)
|
||||
|
||||
// AnalyzeSentiment 判断文本的情感
|
||||
func AnalyzeSentiment(text string) SentimentResult {
|
||||
// 初始化得分
|
||||
score := 0.0
|
||||
positiveCount := 0
|
||||
negativeCount := 0
|
||||
|
||||
// 分词(简单按单个字符分割)
|
||||
words := splitWords(text)
|
||||
|
||||
// 检查文本是否包含转折词,并分割成两部分
|
||||
var transitionIndex int
|
||||
var hasTransition bool
|
||||
for i, word := range words {
|
||||
if _, ok := transitionWords[word]; ok {
|
||||
transitionIndex = i
|
||||
hasTransition = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 处理有转折的文本
|
||||
if hasTransition {
|
||||
// 转折前的部分
|
||||
preTransitionWords := words[:transitionIndex]
|
||||
preScore, prePos, preNeg := calculateScore(preTransitionWords)
|
||||
|
||||
// 转折后的部分,权重加倍
|
||||
postTransitionWords := words[transitionIndex+1:]
|
||||
postScore, postPos, postNeg := calculateScore(postTransitionWords)
|
||||
postScore *= 1.5 // 转折后的情感更重要
|
||||
|
||||
score = preScore + postScore
|
||||
positiveCount = prePos + postPos
|
||||
negativeCount = preNeg + postNeg
|
||||
} else {
|
||||
// 没有转折的文本
|
||||
score, positiveCount, negativeCount = calculateScore(words)
|
||||
}
|
||||
|
||||
// 确定情感类别
|
||||
var category SentimentType
|
||||
switch {
|
||||
case score > 1.0:
|
||||
category = Positive
|
||||
case score < -1.0:
|
||||
category = Negative
|
||||
default:
|
||||
category = Neutral
|
||||
}
|
||||
|
||||
return SentimentResult{
|
||||
Score: score,
|
||||
Category: category,
|
||||
PositiveCount: positiveCount,
|
||||
NegativeCount: negativeCount,
|
||||
Description: GetSentimentDescription(category),
|
||||
}
|
||||
}
|
||||
|
||||
// 计算情感得分
|
||||
func calculateScore(words []string) (float64, int, int) {
|
||||
score := 0.0
|
||||
positiveCount := 0
|
||||
negativeCount := 0
|
||||
|
||||
// 遍历每个词,计算情感得分
|
||||
for i, word := range words {
|
||||
// 首先检查是否为程度副词
|
||||
degree, isDegree := degreeWords[word]
|
||||
|
||||
// 检查是否为否定词
|
||||
_, isNegation := negationWords[word]
|
||||
|
||||
// 检查是否为金融正面词
|
||||
if posScore, isPositive := positiveFinanceWords[word]; isPositive {
|
||||
// 检查前一个词是否为否定词或程度副词
|
||||
if i > 0 {
|
||||
prevWord := words[i-1]
|
||||
if _, isNeg := negationWords[prevWord]; isNeg {
|
||||
score -= posScore
|
||||
negativeCount++
|
||||
continue
|
||||
}
|
||||
|
||||
if deg, isDeg := degreeWords[prevWord]; isDeg {
|
||||
score += posScore * deg
|
||||
positiveCount++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
score += posScore
|
||||
positiveCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否为金融负面词
|
||||
if negScore, isNegative := negativeFinanceWords[word]; isNegative {
|
||||
// 检查前一个词是否为否定词或程度副词
|
||||
if i > 0 {
|
||||
prevWord := words[i-1]
|
||||
if _, isNeg := negationWords[prevWord]; isNeg {
|
||||
score += negScore
|
||||
positiveCount++
|
||||
continue
|
||||
}
|
||||
|
||||
if deg, isDeg := degreeWords[prevWord]; isDeg {
|
||||
score -= negScore * deg
|
||||
negativeCount++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
score -= negScore
|
||||
negativeCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理程度副词(如果后面跟着情感词)
|
||||
if isDegree && i+1 < len(words) {
|
||||
nextWord := words[i+1]
|
||||
|
||||
if posScore, isPositive := positiveFinanceWords[nextWord]; isPositive {
|
||||
score += posScore * degree
|
||||
positiveCount++
|
||||
continue
|
||||
}
|
||||
|
||||
if negScore, isNegative := negativeFinanceWords[nextWord]; isNegative {
|
||||
score -= negScore * degree
|
||||
negativeCount++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 处理否定词(如果后面跟着情感词)
|
||||
if isNegation && i+1 < len(words) {
|
||||
nextWord := words[i+1]
|
||||
|
||||
if posScore, isPositive := positiveFinanceWords[nextWord]; isPositive {
|
||||
score -= posScore
|
||||
negativeCount++
|
||||
continue
|
||||
}
|
||||
|
||||
if negScore, isNegative := negativeFinanceWords[nextWord]; isNegative {
|
||||
score += negScore
|
||||
positiveCount++
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return score, positiveCount, negativeCount
|
||||
}
|
||||
|
||||
// 简单的分词函数,考虑了中文和英文
|
||||
func splitWords(text string) []string {
|
||||
return seg.Cut(text, true)
|
||||
}
|
||||
|
||||
// GetSentimentDescription 获取情感类别的文本描述
|
||||
func GetSentimentDescription(category SentimentType) string {
|
||||
switch category {
|
||||
case Positive:
|
||||
return "看涨"
|
||||
case Negative:
|
||||
return "看跌"
|
||||
case Neutral:
|
||||
return "中性"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 从命令行读取输入
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Println("请输入要分析的股市相关文本(输入exit退出):")
|
||||
|
||||
for {
|
||||
fmt.Print("> ")
|
||||
text, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
fmt.Println("读取输入时出错:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 去除换行符
|
||||
text = strings.TrimSpace(text)
|
||||
|
||||
// 检查是否退出
|
||||
if text == "exit" {
|
||||
break
|
||||
}
|
||||
|
||||
// 分析情感
|
||||
result := AnalyzeSentiment(text)
|
||||
|
||||
// 输出结果
|
||||
fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n",
|
||||
GetSentimentDescription(result.Category),
|
||||
result.Score,
|
||||
result.PositiveCount,
|
||||
result.NegativeCount)
|
||||
}
|
||||
}
|
36
backend/data/stock_sentiment_analysis_test.go
Normal file
36
backend/data/stock_sentiment_analysis_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// @Author spark
|
||||
// @Date 2025/6/19 13:05
|
||||
// @Desc
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
func TestAnalyzeSentiment(t *testing.T) {
|
||||
// 分析情感
|
||||
text := " 【调查:韩国近两成中小学生过度使用智能手机或互联网】财联社6月19日电,韩国女性家族部18日公布的一项年度调查结果显示,接受调查的韩国中小学生中,共计约17.3%、即超过21万人使用智能手机或互联网的程度达到了“危险水平”,这意味着他们因过度依赖智能手机或互联网而需要关注或干预,这一比例引人担忧。 (新华社)\n"
|
||||
text = "消息人士称,联合利华(Unilever)正在为Graze零食品牌寻找买家。\n"
|
||||
text = "【韩国未来5年将投入51万亿韩元发展文化产业】 据韩联社,韩国文化体育观光部(文体部)今后5年将投入51万亿韩元(约合人民币2667亿元)预算,落实总统李在明在竞选时期提出的“将韩国打造成全球五大文化强国之一”的承诺。\n"
|
||||
//text = "【油气股持续拉升 国际实业午后涨停】财联社6月19日电,油气股午后持续拉升,国际实业、宝莫股份午后涨停,准油股份、山东墨龙。茂化实华此前涨停,通源石油、海默科技、贝肯能源、中曼石油、科力股份等多股涨超5%。\n"
|
||||
//text = " 【三大指数均跌逾1% 下跌个股近4800只】财联社6月19日电,指数持续走弱,沪指下挫跌逾1.00%,深成指跌1.25%,创业板指跌1.39%。核聚变、风电、军工、食品消费等板块指数跌幅居前,沪深京三市下跌个股近4800只。\n"
|
||||
text = "【银行理财首单网下打新落地】财联社6月20日电,记者从多渠道获悉,光大理财以申报价格17元参与信通电子网下打新,并成功入围有效报价,成为行业内首家参与网下打新的银行理财公司。光大理财工作人员向证券时报记者表示,本次光大理财是以其管理的混合类产品“阳光橙增盈绝对收益策略”参与了此次网下打新,该产品为光大理财“固收+”银行理财产品。资料显示,信通电子成立于1996年,核心产品包括输电线路智能巡检系统、变电站智能辅控系统、移动智能终端及其他产品。根据其招股说明书,信通电子2023、2024年营业收入分别较上年增长19.08%和7.97%,净利润分别较上年增长5.6%和15.11%。 (证券时报)"
|
||||
text = " 【以军称拦截数枚伊朗导弹】财联社6月20日电,据央视新闻报道,以军在贝尔谢巴及周边区域拦截了数枚伊朗导弹,但仍有导弹或拦截残骸落地。以色列国防军发文表示,搜救队伍正在一处“空中物体落地”的所在区域开展工作,公众目前可以离开避难场所。伊朗方面对上述说法暂无回应。"
|
||||
|
||||
words := splitWords(text)
|
||||
fmt.Println(strings.Join(words, " "))
|
||||
|
||||
result := AnalyzeSentiment(text)
|
||||
|
||||
// 输出结果
|
||||
fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n",
|
||||
result.Description,
|
||||
result.Score,
|
||||
result.PositiveCount,
|
||||
result.NegativeCount)
|
||||
|
||||
}
|
@ -29,12 +29,12 @@ func NewTushareApi(config *Settings) *TushareApi {
|
||||
|
||||
// GetDaily tushare A股日线行情
|
||||
func (receiver TushareApi) GetDaily(tsCode, startDate, endDate string, crawlTimeOut int64) string {
|
||||
logger.SugaredLogger.Debugf("tushare daily request: ts_code=%s, start_date=%s, end_date=%s", tsCode, startDate, endDate)
|
||||
//logger.SugaredLogger.Debugf("tushare daily request: ts_code=%s, start_date=%s, end_date=%s", tsCode, startDate, endDate)
|
||||
fields := "ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount"
|
||||
resp := &TushareStockBasicResponse{}
|
||||
stockType := getStockType(tsCode)
|
||||
tsCodeNEW := getTsCode(tsCode)
|
||||
logger.SugaredLogger.Debugf("tushare daily request: %s,tsCode:%s,tsCodeNEW:%s", stockType, tsCode, tsCodeNEW)
|
||||
//logger.SugaredLogger.Debugf("tushare daily request: %s,tsCode:%s,tsCodeNEW:%s", stockType, tsCode, tsCodeNEW)
|
||||
_, err := receiver.client.SetTimeout(time.Duration(crawlTimeOut)*time.Second).R().
|
||||
SetHeader("content-type", "application/json").
|
||||
SetBody(&TushareRequest{
|
||||
@ -66,7 +66,7 @@ func (receiver TushareApi) GetDaily(tsCode, startDate, endDate string, crawlTime
|
||||
res += t + "\n"
|
||||
}
|
||||
}
|
||||
logger.SugaredLogger.Debugf("tushare response: %s", res)
|
||||
//logger.SugaredLogger.Debugf("tushare response: %s", res)
|
||||
return res
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
// -----------------------------------------------------------------------------------
|
||||
func TestGetDaily(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
tushareApi := NewTushareApi(getConfig())
|
||||
tushareApi := NewTushareApi(GetConfig())
|
||||
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(GetConfig())
|
||||
|
||||
res := tushareApi.GetDaily("gb_AAPL", "20250101", "20250217", 30)
|
||||
t.Log(res)
|
||||
|
@ -1,6 +1,8 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"go-stock/backend/logger"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
@ -57,3 +59,53 @@ func ConvertTushareCodeToStockCode(stockCode string) string {
|
||||
stockCode = strings.ToLower(RemoveAllDigitChar(stockCode)) + RemoveAllNonDigitChar(stockCode)
|
||||
return strings.ReplaceAll(stockCode, ".", "")
|
||||
}
|
||||
|
||||
func GetTableMarkdown(document *goquery.Document, waitVisible string, markdown *strings.Builder) {
|
||||
document.Find(waitVisible).First().Find("tr").Each(func(index int, item *goquery.Selection) {
|
||||
row := ""
|
||||
item.Find("th, td").Each(func(i int, cell *goquery.Selection) {
|
||||
text := cell.Children().FilterFunction(func(i int, s *goquery.Selection) bool {
|
||||
return isVisible(s)
|
||||
}).Text()
|
||||
if text == "" {
|
||||
text = cell.Text()
|
||||
}
|
||||
|
||||
row += "|" + text
|
||||
})
|
||||
row += "|"
|
||||
|
||||
if index == 0 {
|
||||
// Header row
|
||||
markdown.WriteString(row + "\n")
|
||||
// Separator row
|
||||
separator := ""
|
||||
item.Find("th, td").Each(func(i int, cell *goquery.Selection) {
|
||||
separator += "|---"
|
||||
})
|
||||
markdown.WriteString(separator + "|\n")
|
||||
} else {
|
||||
// Data row
|
||||
markdown.WriteString(row + "\n")
|
||||
}
|
||||
})
|
||||
logger.SugaredLogger.Infof("\n%s", markdown.String())
|
||||
}
|
||||
|
||||
// isVisible 函数用于判断元素是否可见
|
||||
func isVisible(s *goquery.Selection) bool {
|
||||
// 检查 display 属性
|
||||
display, _ := s.Attr("style")
|
||||
if strings.Contains(strings.ToLower(display), "display: none") {
|
||||
return false
|
||||
}
|
||||
// 检查 visibility 属性
|
||||
if strings.Contains(strings.ToLower(display), "visibility: hidden") {
|
||||
return false
|
||||
}
|
||||
// 检查 opacity 属性
|
||||
if strings.Contains(strings.ToLower(display), "opacity: 0") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -195,3 +195,170 @@ type Resp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type PromptTemplate struct {
|
||||
ID int `gorm:"primarykey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (p PromptTemplate) TableName() string {
|
||||
return "prompt_templates"
|
||||
}
|
||||
|
||||
type Prompt struct {
|
||||
ID int `json:"ID"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type Telegraph struct {
|
||||
gorm.Model
|
||||
Time string `json:"time"`
|
||||
Content string `json:"content"`
|
||||
SubjectTags []string `json:"subjects" gorm:"-:all"`
|
||||
StocksTags []string `json:"stocks" gorm:"-:all"`
|
||||
IsRed bool `json:"isRed"`
|
||||
Url string `json:"url"`
|
||||
Source string `json:"source"`
|
||||
TelegraphTags []TelegraphTags `json:"tags" gorm:"-:migration;foreignKey:TelegraphId"`
|
||||
SentimentResult string `json:"sentimentResult" gorm:"-:all"`
|
||||
}
|
||||
type TelegraphTags struct {
|
||||
gorm.Model
|
||||
TagId uint `json:"tagId"`
|
||||
TelegraphId uint `json:"telegraphId"`
|
||||
}
|
||||
|
||||
func (t TelegraphTags) TableName() string {
|
||||
return "telegraph_tags"
|
||||
}
|
||||
|
||||
type Tags struct {
|
||||
gorm.Model
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (p Tags) TableName() string {
|
||||
return "tags"
|
||||
}
|
||||
|
||||
func (p Telegraph) TableName() string {
|
||||
return "telegraph_list"
|
||||
}
|
||||
|
||||
type SinaStockInfo struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Name string `json:"name"`
|
||||
Engname string `json:"engname"`
|
||||
Tradetype string `json:"tradetype"`
|
||||
Lasttrade string `json:"lasttrade"`
|
||||
Prevclose string `json:"prevclose"`
|
||||
Open string `json:"open"`
|
||||
High string `json:"high"`
|
||||
Low string `json:"low"`
|
||||
Volume string `json:"volume"`
|
||||
Currentvolume string `json:"currentvolume"`
|
||||
Amount string `json:"amount"`
|
||||
Ticktime string `json:"ticktime"`
|
||||
Buy string `json:"buy"`
|
||||
Sell string `json:"sell"`
|
||||
High52Week string `json:"high_52week"`
|
||||
Low52Week string `json:"low_52week"`
|
||||
Eps string `json:"eps"`
|
||||
Dividend string `json:"dividend"`
|
||||
StocksSum string `json:"stocks_sum"`
|
||||
Pricechange string `json:"pricechange"`
|
||||
Changepercent string `json:"changepercent"`
|
||||
MarketValue string `json:"market_value"`
|
||||
PeRatio string `json:"pe_ratio"`
|
||||
}
|
||||
|
||||
type LongTigerRankData struct {
|
||||
ACCUMAMOUNT float64 `json:"ACCUM_AMOUNT"`
|
||||
BILLBOARDBUYAMT float64 `json:"BILLBOARD_BUY_AMT"`
|
||||
BILLBOARDDEALAMT float64 `json:"BILLBOARD_DEAL_AMT"`
|
||||
BILLBOARDNETAMT float64 `json:"BILLBOARD_NET_AMT"`
|
||||
BILLBOARDSELLAMT float64 `json:"BILLBOARD_SELL_AMT"`
|
||||
CHANGERATE float64 `json:"CHANGE_RATE"`
|
||||
CLOSEPRICE float64 `json:"CLOSE_PRICE"`
|
||||
DEALAMOUNTRATIO float64 `json:"DEAL_AMOUNT_RATIO"`
|
||||
DEALNETRATIO float64 `json:"DEAL_NET_RATIO"`
|
||||
EXPLAIN string `json:"EXPLAIN"`
|
||||
EXPLANATION string `json:"EXPLANATION"`
|
||||
FREEMARKETCAP float64 `json:"FREE_MARKET_CAP"`
|
||||
SECUCODE string `json:"SECUCODE" gorm:"index"`
|
||||
SECURITYCODE string `json:"SECURITY_CODE"`
|
||||
SECURITYNAMEABBR string `json:"SECURITY_NAME_ABBR"`
|
||||
SECURITYTYPECODE string `json:"SECURITY_TYPE_CODE"`
|
||||
TRADEDATE string `json:"TRADE_DATE" gorm:"index"`
|
||||
TURNOVERRATE float64 `json:"TURNOVERRATE"`
|
||||
}
|
||||
|
||||
func (l LongTigerRankData) TableName() string {
|
||||
return "long_tiger_rank"
|
||||
}
|
||||
|
||||
type TVNews struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Published int `json:"published"`
|
||||
Urgency int `json:"urgency"`
|
||||
Permission string `json:"permission"`
|
||||
StoryPath string `json:"storyPath"`
|
||||
Provider struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
LogoId string `json:"logo_id"`
|
||||
} `json:"provider"`
|
||||
}
|
||||
|
||||
type XUEQIUHot struct {
|
||||
Data struct {
|
||||
Items []HotItem `json:"items"`
|
||||
ItemsSize int `json:"items_size"`
|
||||
} `json:"data"`
|
||||
ErrorCode int `json:"error_code"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
type HotItem struct {
|
||||
Type int `json:"type"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Value float64 `json:"value"`
|
||||
Increment int `json:"increment"`
|
||||
RankChange int `json:"rank_change"`
|
||||
HasExist interface{} `json:"has_exist"`
|
||||
Symbol string `json:"symbol"`
|
||||
Percent float64 `json:"percent"`
|
||||
Current float64 `json:"current"`
|
||||
Chg float64 `json:"chg"`
|
||||
Exchange string `json:"exchange"`
|
||||
StockType int `json:"stock_type"`
|
||||
SubType string `json:"sub_type"`
|
||||
Ad int `json:"ad"`
|
||||
AdId interface{} `json:"ad_id"`
|
||||
ContentId interface{} `json:"content_id"`
|
||||
Page interface{} `json:"page"`
|
||||
Model interface{} `json:"model"`
|
||||
Location interface{} `json:"location"`
|
||||
TradeSession interface{} `json:"trade_session"`
|
||||
CurrentExt interface{} `json:"current_ext"`
|
||||
PercentExt interface{} `json:"percent_ext"`
|
||||
}
|
||||
|
||||
type HotEvent struct {
|
||||
PicSize interface{} `json:"pic_size"`
|
||||
Tag string `json:"tag"`
|
||||
Id int `json:"id"`
|
||||
Pic string `json:"pic"`
|
||||
Hot int `json:"hot"`
|
||||
StatusCount int `json:"status_count"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
BIN
build/screenshot/img13.png
Normal file
BIN
build/screenshot/img13.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 336 KiB |
BIN
build/screenshot/img_12.png
Normal file
BIN
build/screenshot/img_12.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 170 KiB |
BIN
build/screenshot/img_13.png
Normal file
BIN
build/screenshot/img_13.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 198 KiB |
BIN
build/screenshot/img_14.png
Normal file
BIN
build/screenshot/img_14.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 160 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
713
frontend/package-lock.json
generated
713
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,20 +12,30 @@
|
||||
"@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",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue3-danmaku": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vicons/antd": "^0.13.0",
|
||||
"@vicons/carbon": "^0.13.0",
|
||||
"@vicons/fa": "^0.13.0",
|
||||
"@vicons/fluent": "^0.13.0",
|
||||
"@vicons/ionicons4": "^0.13.0",
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"@vicons/material": "^0.13.0",
|
||||
"@vicons/tabler": "^0.13.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"html-docx-js-typescript": "^0.1.5",
|
||||
"naive-ui": "^2.41.0",
|
||||
"vfonts": "^0.0.3",
|
||||
"vite": "^5.4.12"
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"keywords": [
|
||||
"AI赋能股票分析",
|
||||
|
@ -1 +1 @@
|
||||
2091cce83d29f564a50e85f1667b2f4c
|
||||
2d63c3a999d797889c01d6c96451b197
|
@ -1,30 +1,56 @@
|
||||
<script setup>
|
||||
import {
|
||||
EventsEmit,
|
||||
EventsOff,
|
||||
EventsOn,
|
||||
Quit,
|
||||
WindowFullscreen, WindowGetPosition,
|
||||
WindowFullscreen,
|
||||
WindowHide,
|
||||
WindowSetPosition,
|
||||
WindowUnfullscreen
|
||||
} from '../wailsjs/runtime'
|
||||
import {h, onMounted, ref} from "vue";
|
||||
import { RouterLink } from 'vue-router'
|
||||
import {darkTheme, NGradientText, NIcon, NText,} from 'naive-ui'
|
||||
import {h, onBeforeMount, onBeforeUnmount, onMounted, ref} from "vue";
|
||||
import {RouterLink, useRouter} from 'vue-router'
|
||||
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,dateZhCN,zhCN} from 'naive-ui'
|
||||
import {
|
||||
SettingsOutline,
|
||||
AlarmOutline,
|
||||
AnalyticsOutline,
|
||||
BarChartSharp, Bonfire, BonfireOutline, EaselSharp,
|
||||
ExpandOutline, Flag,
|
||||
Flame, FlameSharp, InformationOutline,
|
||||
LogoGithub,
|
||||
NewspaperOutline,
|
||||
NewspaperSharp, Notifications,
|
||||
PowerOutline, Pulse,
|
||||
ReorderTwoOutline,
|
||||
ExpandOutline,
|
||||
PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline, AlarmOutline, SparklesOutline,
|
||||
SettingsOutline, Skull, SkullOutline, SkullSharp,
|
||||
SparklesOutline,
|
||||
StarOutline,
|
||||
Wallet, WarningOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
import {AnalyzeSentiment, GetConfig, GetGroupList} from "../wailsjs/go/main/App";
|
||||
import {Dragon, Fire, Gripfire} from "@vicons/fa";
|
||||
import {ReportSearch} from "@vicons/tabler";
|
||||
import {LocalFireDepartmentRound} from "@vicons/material";
|
||||
import {BoxSearch20Regular, CommentNote20Filled} from "@vicons/fluent";
|
||||
import {FireFilled, FireOutlined, NotificationFilled, StockOutlined} from "@vicons/antd";
|
||||
|
||||
const contentStyle = ref("")
|
||||
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
|
||||
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(true)
|
||||
const loadingMsg = ref("加载数据中...")
|
||||
const enableNews = ref(false)
|
||||
const contentStyle = ref("")
|
||||
const enableFund = ref(false)
|
||||
const enableDarkTheme = ref(null)
|
||||
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎\n\n未经授权,禁止商业目的!')
|
||||
const isFullscreen = ref(false)
|
||||
const activeKey = ref('stock')
|
||||
const containerRef= ref({})
|
||||
const realtimeProfit= ref(0)
|
||||
const telegraph= ref([])
|
||||
const activeKey = ref('')
|
||||
const containerRef = ref({})
|
||||
const realtimeProfit = ref(0)
|
||||
const telegraph = ref([])
|
||||
const groupList = ref([])
|
||||
const menuOptions = ref([
|
||||
{
|
||||
label: () =>
|
||||
@ -33,21 +59,299 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'stock',
|
||||
params: {
|
||||
id: 'zh-CN'
|
||||
query: {
|
||||
groupName: '全部',
|
||||
groupId: 0,
|
||||
},
|
||||
params: {},
|
||||
}
|
||||
},
|
||||
{ default: () => '股票自选',}
|
||||
{default: () => '股票自选',}
|
||||
),
|
||||
key: 'stock',
|
||||
icon: renderIcon(StarOutline),
|
||||
children:[
|
||||
children: [
|
||||
{
|
||||
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '当日盈亏 '+realtimeProfit.value+"¥"}),
|
||||
key: 'realtimeProfit',
|
||||
show: realtimeProfit.value,
|
||||
icon: renderIcon(WalletOutline),
|
||||
label: () =>
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
href: '#',
|
||||
type: 'info',
|
||||
onClick: () => {
|
||||
//console.log("push",item)
|
||||
router.push({
|
||||
name: 'stock',
|
||||
query: {
|
||||
groupName: '全部',
|
||||
groupId: 0,
|
||||
},
|
||||
})
|
||||
EventsEmit("changeTab", {ID: 0, name: '全部'})
|
||||
},
|
||||
to: {
|
||||
name: 'stock',
|
||||
query: {
|
||||
groupName: '全部',
|
||||
groupId: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
{default: () => '全部',}
|
||||
),
|
||||
key: 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
params: {}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
|
||||
},
|
||||
},
|
||||
{default: () => '市场行情'}
|
||||
),
|
||||
key: 'market',
|
||||
icon: renderIcon(NewspaperOutline),
|
||||
children: [
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "市场快讯",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
|
||||
},
|
||||
},
|
||||
{default: () => '市场快讯',}
|
||||
),
|
||||
key: 'market1',
|
||||
icon: renderIcon(NewspaperSharp),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "全球股指",
|
||||
},
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '全球股指'})
|
||||
},
|
||||
},
|
||||
{default: () => '全球股指',}
|
||||
),
|
||||
key: 'market2',
|
||||
icon: renderIcon(BarChartSharp),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "指标行情",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '指标行情'})
|
||||
},
|
||||
},
|
||||
{default: () => '指标行情',}
|
||||
),
|
||||
key: 'market3',
|
||||
icon: renderIcon(AnalyticsOutline),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "行业排名",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '行业排名'})
|
||||
},
|
||||
},
|
||||
{default: () => '行业排名',}
|
||||
),
|
||||
key: 'market4',
|
||||
icon: renderIcon(Flag),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "个股资金流向",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '个股资金流向'})
|
||||
},
|
||||
},
|
||||
{default: () => '个股资金流向',}
|
||||
),
|
||||
key: 'market5',
|
||||
icon: renderIcon(Pulse),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "龙虎榜",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '龙虎榜'})
|
||||
},
|
||||
},
|
||||
{default: () => '龙虎榜',}
|
||||
),
|
||||
key: 'market6',
|
||||
icon: renderIcon(Dragon),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "个股研报",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '个股研报'})
|
||||
},
|
||||
},
|
||||
{default: () => '个股研报',}
|
||||
),
|
||||
key: 'market7',
|
||||
icon: renderIcon(StockOutlined),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "公司公告",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '公司公告'})
|
||||
},
|
||||
},
|
||||
{default: () => '公司公告',}
|
||||
),
|
||||
key: 'market8',
|
||||
icon: renderIcon(NotificationFilled),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "行业研究",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '行业研究'})
|
||||
},
|
||||
},
|
||||
{default: () => '行业研究',}
|
||||
),
|
||||
key: 'market9',
|
||||
icon: renderIcon(ReportSearch),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "当前热门",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '当前热门'})
|
||||
},
|
||||
},
|
||||
{default: () => '当前热门',}
|
||||
),
|
||||
key: 'market10',
|
||||
icon: renderIcon(Gripfire),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "指标选股",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '指标选股'})
|
||||
},
|
||||
},
|
||||
{default: () => '指标选股',}
|
||||
),
|
||||
key: 'market11',
|
||||
icon: renderIcon(BoxSearch20Regular),
|
||||
},
|
||||
]
|
||||
},
|
||||
@ -58,18 +362,17 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'fund',
|
||||
params: {
|
||||
id: 'zh-CN'
|
||||
},
|
||||
params: {},
|
||||
}
|
||||
},
|
||||
{ default: () => '基金自选',}
|
||||
{default: () => '基金自选',}
|
||||
),
|
||||
show: enableFund.value,
|
||||
key: 'fund',
|
||||
icon: renderIcon(SparklesOutline),
|
||||
children:[
|
||||
children: [
|
||||
{
|
||||
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '功能完善中!'}),
|
||||
label: () => h(NText, {type: realtimeProfit.value > 0 ? 'error' : 'success'}, {default: () => '功能完善中!'}),
|
||||
key: 'realtimeProfit',
|
||||
show: realtimeProfit.value,
|
||||
icon: renderIcon(AlarmOutline),
|
||||
@ -83,12 +386,10 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'settings',
|
||||
params: {
|
||||
id: 'zh-CN'
|
||||
}
|
||||
params: {}
|
||||
}
|
||||
},
|
||||
{ default: () => '设置' }
|
||||
{default: () => '设置'}
|
||||
),
|
||||
key: 'settings',
|
||||
icon: renderIcon(SettingsOutline),
|
||||
@ -100,88 +401,107 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'about',
|
||||
params: {
|
||||
id: 'zh-CN'
|
||||
}
|
||||
params: {}
|
||||
}
|
||||
},
|
||||
{ default: () => '关于' }
|
||||
{default: () => '关于'}
|
||||
),
|
||||
key: 'about',
|
||||
icon: renderIcon(LogoGithub),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
label: () => h("a", {
|
||||
href: '#',
|
||||
onClick: toggleFullscreen,
|
||||
title: '全屏 Ctrl+F 退出全屏 Esc',
|
||||
}, { default: () => isFullscreen.value?'取消全屏':'全屏' }),
|
||||
}, {default: () => isFullscreen.value ? '取消全屏' : '全屏'}),
|
||||
key: 'full',
|
||||
icon: renderIcon(ExpandOutline),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
label: () => h("a", {
|
||||
href: '#',
|
||||
onClick: WindowHide,
|
||||
title: '隐藏到托盘区 Ctrl+H',
|
||||
}, { default: () => '隐藏到托盘区' }),
|
||||
}, {default: () => '隐藏到托盘区'}),
|
||||
key: 'hide',
|
||||
icon: renderIcon(ReorderTwoOutline),
|
||||
},
|
||||
// {
|
||||
// label: ()=> h("a", {
|
||||
// href: 'javascript:void(0)',
|
||||
// style: 'cursor: move;',
|
||||
// onClick: toggleStartMoveWindow,
|
||||
// }, { default: () => '移动' }),
|
||||
// key: 'move',
|
||||
// icon: renderIcon(MoveOutline),
|
||||
// },
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
href: 'javascript:void(0)',
|
||||
style: 'cursor: move;',
|
||||
onClick: toggleStartMoveWindow,
|
||||
}, { default: () => '移动' }),
|
||||
key: 'move',
|
||||
icon: renderIcon(MoveOutline),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
label: () => h("a", {
|
||||
href: '#',
|
||||
onClick: Quit,
|
||||
}, { default: () => '退出程序' }),
|
||||
}, {default: () => '退出程序'}),
|
||||
key: 'exit',
|
||||
icon: renderIcon(PowerOutline),
|
||||
},
|
||||
])
|
||||
|
||||
function renderIcon(icon) {
|
||||
return () => h(NIcon, null, { default: () => h(icon) })
|
||||
return () => h(NIcon, null, {default: () => h(icon)})
|
||||
}
|
||||
|
||||
function toggleFullscreen(e) {
|
||||
//console.log(e)
|
||||
if (isFullscreen.value) {
|
||||
WindowUnfullscreen()
|
||||
//e.target.innerHTML = '全屏'
|
||||
} else {
|
||||
WindowFullscreen()
|
||||
// e.target.innerHTML = '取消全屏'
|
||||
}
|
||||
isFullscreen.value=!isFullscreen.value
|
||||
}
|
||||
const drag = ref(false)
|
||||
const lastPos= ref({x:0,y:0})
|
||||
function toggleStartMoveWindow(e) {
|
||||
drag.value=!drag.value
|
||||
lastPos.value={x:e.clientX,y:e.clientY}
|
||||
}
|
||||
function dragstart(e) {
|
||||
if (drag.value) {
|
||||
let x=e.clientX-lastPos.value.x
|
||||
let y=e.clientY-lastPos.value.y
|
||||
WindowGetPosition().then((pos) => {
|
||||
WindowSetPosition(pos.x+x,pos.y+y)
|
||||
})
|
||||
if (isFullscreen.value) {
|
||||
WindowUnfullscreen()
|
||||
//e.target.innerHTML = '全屏'
|
||||
} else {
|
||||
WindowFullscreen()
|
||||
// e.target.innerHTML = '取消全屏'
|
||||
}
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
}
|
||||
window.addEventListener('mousemove', dragstart)
|
||||
|
||||
EventsOn("realtime_profit",(data)=>{
|
||||
realtimeProfit.value=data
|
||||
// const drag = ref(false)
|
||||
// const lastPos= ref({x:0,y:0})
|
||||
// function toggleStartMoveWindow(e) {
|
||||
// drag.value=!drag.value
|
||||
// lastPos.value={x:e.clientX,y:e.clientY}
|
||||
// }
|
||||
// function dragstart(e) {
|
||||
// if (drag.value) {
|
||||
// let x=e.clientX-lastPos.value.x
|
||||
// let y=e.clientY-lastPos.value.y
|
||||
// WindowGetPosition().then((pos) => {
|
||||
// WindowSetPosition(pos.x+x,pos.y+y)
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// window.addEventListener('mousemove', dragstart)
|
||||
|
||||
EventsOn("realtime_profit", (data) => {
|
||||
realtimeProfit.value = data
|
||||
})
|
||||
EventsOn("telegraph",(data)=>{
|
||||
telegraph.value=data
|
||||
EventsOn("telegraph", (data) => {
|
||||
telegraph.value = data
|
||||
})
|
||||
|
||||
EventsOn("loadingMsg", (data) => {
|
||||
if(data==="done"){
|
||||
loadingMsg.value = "加载完成..."
|
||||
EventsEmit("loadingDone", "app")
|
||||
loading.value = false
|
||||
}else{
|
||||
loading.value = true
|
||||
loadingMsg.value = data
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
EventsOff("realtime_profit")
|
||||
EventsOff("loadingMsg")
|
||||
EventsOff("telegraph")
|
||||
EventsOff("newsPush")
|
||||
})
|
||||
|
||||
window.onerror = function (msg, source, lineno, colno, error) {
|
||||
@ -197,68 +517,159 @@ window.onerror = function (msg, source, lineno, colno, error) {
|
||||
return true;
|
||||
};
|
||||
|
||||
onMounted(()=>{
|
||||
contentStyle.value="max-height: calc(90vh);overflow: hidden"
|
||||
onBeforeMount(() => {
|
||||
GetGroupList().then(result => {
|
||||
groupList.value = result
|
||||
menuOptions.value.map((item) => {
|
||||
//console.log(item)
|
||||
if (item.key === 'stock') {
|
||||
item.children.push(...groupList.value.map(item => {
|
||||
return {
|
||||
label: () =>
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
href: '#',
|
||||
type: 'info',
|
||||
onClick: () => {
|
||||
//console.log("push",item)
|
||||
router.push({
|
||||
name: 'stock',
|
||||
query: {
|
||||
groupName: item.name,
|
||||
groupId: item.ID,
|
||||
},
|
||||
})
|
||||
setTimeout(() => {
|
||||
EventsEmit("changeTab", item)
|
||||
}, 100)
|
||||
},
|
||||
to: {
|
||||
name: 'stock',
|
||||
query: {
|
||||
groupName: item.name,
|
||||
groupId: item.ID,
|
||||
},
|
||||
}
|
||||
},
|
||||
{default: () => item.name,}
|
||||
),
|
||||
key: item.ID,
|
||||
}
|
||||
}))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
GetConfig().then((res) => {
|
||||
//console.log(res)
|
||||
enableFund.value = res.enableFund
|
||||
|
||||
menuOptions.value.filter((item) => {
|
||||
if (item.key === 'fund') {
|
||||
item.show = res.enableFund
|
||||
}
|
||||
})
|
||||
|
||||
if (res.darkTheme) {
|
||||
enableDarkTheme.value = darkTheme
|
||||
} else {
|
||||
enableDarkTheme.value = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
contentStyle.value = "max-height: calc(92vh);overflow: hidden"
|
||||
GetConfig().then((res) => {
|
||||
if (res.enableNews) {
|
||||
enableNews.value = true
|
||||
}
|
||||
enableFund.value = res.enableFund
|
||||
const {notification } =createDiscreteApi(["notification"], {
|
||||
configProviderProps: {
|
||||
theme: enableDarkTheme.value ? darkTheme : lightTheme ,
|
||||
max: 3,
|
||||
},
|
||||
})
|
||||
EventsOn("newsPush", (data) => {
|
||||
//console.log(data)
|
||||
if(data.isRed){
|
||||
notification.create({
|
||||
//type:"error",
|
||||
// avatar: () => h(NIcon,{component:Notifications,color:"red"}),
|
||||
title: data.time,
|
||||
content: () => h(NText,{type:"error"}, { default: () => data.content }),
|
||||
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
|
||||
duration:1000*40,
|
||||
})
|
||||
}else{
|
||||
notification.create({
|
||||
//type:"info",
|
||||
//avatar: () => h(NIcon,{component:Notifications}),
|
||||
title: data.time,
|
||||
content: () => h(NText,{type:"info"}, { default: () => data.content }),
|
||||
meta: () => h(NText,{type:"warning"}, { default: () => data.source}),
|
||||
duration:1000*30 ,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
|
||||
|
||||
<n-config-provider :theme="darkTheme" ref="containerRef" >
|
||||
<n-message-provider >
|
||||
<n-config-provider ref="containerRef" :theme="enableDarkTheme" :locale="zhCN" :date-locale="dateZhCN">
|
||||
<n-message-provider>
|
||||
<n-notification-provider>
|
||||
<n-modal-provider>
|
||||
<n-dialog-provider>
|
||||
|
||||
<n-watermark
|
||||
:content="content"
|
||||
cross
|
||||
selectable
|
||||
:font-size="16"
|
||||
:line-height="16"
|
||||
:width="500"
|
||||
:height="400"
|
||||
:x-offset="50"
|
||||
:y-offset="150"
|
||||
:rotate="-15"
|
||||
>
|
||||
<n-flex justify="center">
|
||||
<n-grid x-gap="12" :cols="1">
|
||||
<!--
|
||||
<n-gi style="position: relative;top:1px;z-index: 19;width: 100%" v-if="telegraph.length>0">
|
||||
|
||||
</n-gi>
|
||||
-->
|
||||
|
||||
<n-gi>
|
||||
<n-marquee :speed="100" style="position: relative;top:0;z-index: 19;width: 100%" v-if="telegraph.length>0">
|
||||
<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">
|
||||
<n-menu style="font-size: 18px;"
|
||||
v-model:value="activeKey"
|
||||
mode="horizontal"
|
||||
:options="menuOptions"
|
||||
responsive
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-flex>
|
||||
</n-watermark>
|
||||
</n-dialog-provider>
|
||||
</n-modal-provider>
|
||||
<n-modal-provider>
|
||||
<n-dialog-provider>
|
||||
<n-watermark
|
||||
:content="content"
|
||||
cross
|
||||
selectable
|
||||
:font-size="16"
|
||||
:line-height="16"
|
||||
:width="500"
|
||||
:height="400"
|
||||
:x-offset="50"
|
||||
:y-offset="150"
|
||||
:rotate="-15"
|
||||
>
|
||||
<n-flex>
|
||||
<n-grid x-gap="12" :cols="1">
|
||||
<n-gi>
|
||||
<n-spin :show="loading">
|
||||
<template #description>
|
||||
{{ loadingMsg }}
|
||||
</template>
|
||||
<n-marquee :speed="100" style="position: relative;top:0;z-index: 19;width: 100%"
|
||||
v-if="(telegraph.length>0)&&(enableNews)">
|
||||
<n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
|
||||
{{ item }}
|
||||
</n-tag>
|
||||
</n-marquee>
|
||||
<n-scrollbar :style="contentStyle">
|
||||
<n-skeleton v-if="loading" height="calc(100vh)" />
|
||||
<RouterView/>
|
||||
</n-scrollbar>
|
||||
</n-spin>
|
||||
</n-gi>
|
||||
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%;">
|
||||
<n-card size="small" style="--wails-draggable:drag">
|
||||
<n-menu style="font-size: 18px;"
|
||||
v-model:value="activeKey"
|
||||
mode="horizontal"
|
||||
:options="menuOptions"
|
||||
responsive
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-flex>
|
||||
</n-watermark>
|
||||
</n-dialog-provider>
|
||||
</n-modal-provider>
|
||||
</n-notification-provider>
|
||||
</n-message-provider>
|
||||
</n-config-provider>
|
||||
|
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>
|
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" :name="item.name" :k-days="20" :dark-theme="true"></k-line-chart>
|
||||
</n-popover>
|
||||
</n-text></n-td>
|
||||
<n-td><n-text :type="item.percent>0?'error':'success'">{{item.percent}}%</n-text></n-td>
|
||||
<n-td><n-text type="info">{{item.current}}</n-text></n-td>
|
||||
<n-td><n-text type="info">{{item.value}}</n-text></n-td>
|
||||
<n-td><n-text :type="item.increment>0?'error':'success'">
|
||||
{{item.increment}}
|
||||
<n-icon v-if="item.increment>0" :component="ArrowUp"/>
|
||||
<n-icon v-else :component="ArrowDown"/>
|
||||
</n-text></n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.rank_change>0?'error':'success'">
|
||||
{{item.rank_change}}
|
||||
<n-icon v-if="item.rank_change>0" :component="ArrowUp"/>
|
||||
<n-text v-else-if="item.rank_change==0" ></n-text>
|
||||
<n-icon v-else :component="ArrowDown"/>
|
||||
</n-text>
|
||||
</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
73
frontend/src/components/HotTopics.vue
Normal file
73
frontend/src/components/HotTopics.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import {onBeforeMount, onUnmounted, ref} from 'vue'
|
||||
import {HotTopic} from "../../wailsjs/go/main/App";
|
||||
const list = ref([])
|
||||
const task =ref()
|
||||
|
||||
onBeforeMount(async () => {
|
||||
list.value = await HotTopic(10)
|
||||
setInterval(async ()=>{
|
||||
list.value = await HotTopic(10)
|
||||
}, 1000*10)
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
clearInterval(task.value)
|
||||
})
|
||||
|
||||
function openCenteredWindow(url, width, height) {
|
||||
const left = (window.screen.width - width) / 2;
|
||||
const top = (window.screen.height - height) / 2;
|
||||
|
||||
return window.open(
|
||||
url,
|
||||
'centeredWindow',
|
||||
`width=${width},height=${height},left=${left},top=${top}`
|
||||
);
|
||||
}
|
||||
function showPage(htid) {
|
||||
openCenteredWindow(`https://gubatopic.eastmoney.com/topic_v3.html?htid=${htid}`, 1000, 600)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-list bordered hoverable clickable>
|
||||
<!-- <template #header>-->
|
||||
<!-- 股吧热门-->
|
||||
<!-- </template>-->
|
||||
<n-list-item v-for="(item, index) in list" :key="index">
|
||||
<n-thing :title="item.nickname" :description="item.desc" :description-style="'font-size: 14px;'" @click="showPage(item.htid)">
|
||||
<template v-if="item.squareImg" #avatar>
|
||||
<n-avatar :src="item.squareImg" :size="60">
|
||||
</n-avatar>
|
||||
</template>
|
||||
<template v-if="item.stock_list" #footer>
|
||||
<n-flex>
|
||||
<n-tag type="info" v-for="(v, i) in item.stock_list" :bordered="false" size="small">
|
||||
{{v.name}}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template v-if="item.clickNumber" #header-extra>
|
||||
<n-flex>
|
||||
<n-button secondary type="warning" size="tiny">讨论数:<n-number-animation
|
||||
show-separator
|
||||
:from="0"
|
||||
:to="item.postNumber"
|
||||
/>
|
||||
</n-button >
|
||||
<n-tag :bordered="false" type="warning" size="small">浏览量:<n-number-animation
|
||||
show-separator
|
||||
:from="0"
|
||||
:to="item.clickNumber"
|
||||
/>
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
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>
|
386
frontend/src/components/KLineChart.vue
Normal file
386
frontend/src/components/KLineChart.vue
Normal file
@ -0,0 +1,386 @@
|
||||
<script setup>
|
||||
|
||||
import {GetStockKLine} from "../../wailsjs/go/main/App";
|
||||
import * as echarts from "echarts";
|
||||
import {onMounted, ref} from "vue";
|
||||
import _ from "lodash";
|
||||
const { code,name,darkTheme,kDays ,chartHeight} = defineProps({
|
||||
code: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
kDays: {
|
||||
type: Number,
|
||||
default: 14
|
||||
},
|
||||
chartHeight: {
|
||||
type: Number,
|
||||
default: 500
|
||||
},
|
||||
darkTheme: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const upColor = '#ec0000';
|
||||
const upBorderColor = '';
|
||||
const downColor = '#00da3c';
|
||||
const downBorderColor = '';
|
||||
const kLineChartRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
handleKLine(code,name)
|
||||
})
|
||||
|
||||
function handleKLine(code,name){
|
||||
GetStockKLine(code,name,365).then(result => {
|
||||
//console.log("GetStockKLine",result)
|
||||
const chart = echarts.init(kLineChartRef.value);
|
||||
const categoryData = [];
|
||||
const values = [];
|
||||
const volumns=[];
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
let resultElement=result[i]
|
||||
//console.log("resultElement:{}",resultElement)
|
||||
categoryData.push(resultElement.day)
|
||||
let flag=resultElement.close>resultElement.open?1:-1
|
||||
values.push([
|
||||
resultElement.open,
|
||||
resultElement.close,
|
||||
resultElement.low,
|
||||
resultElement.high
|
||||
])
|
||||
volumns.push([i,resultElement.volume/10000,flag])
|
||||
}
|
||||
////console.log("categoryData",categoryData)
|
||||
////console.log("values",values)
|
||||
let option = {
|
||||
title: {
|
||||
text: name+" "+code,
|
||||
left: '20px',
|
||||
textStyle: {
|
||||
color: darkTheme?'#ccc':'#456'
|
||||
}
|
||||
},
|
||||
darkMode: darkTheme,
|
||||
//backgroundColor: '#1c1c1c',
|
||||
// color:['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'],
|
||||
animation: false,
|
||||
legend: {
|
||||
right: 20,
|
||||
top: 0,
|
||||
data: ['日K', 'MA5', 'MA10', 'MA20', 'MA30'],
|
||||
textStyle: {
|
||||
color: darkTheme?'#ccc':'#456'
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
lineStyle: {
|
||||
color: '#376df4',
|
||||
width: 2,
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
borderWidth: 2,
|
||||
borderColor: darkTheme?'#456':'#ccc',
|
||||
backgroundColor: darkTheme?'#456':'#fff',
|
||||
padding: 10,
|
||||
textStyle: {
|
||||
color: darkTheme?'#ccc':'#456'
|
||||
},
|
||||
formatter: function (params) {//修改鼠标划过显示为中文
|
||||
//console.log("params",params)
|
||||
let currentItemData = _.filter(params, (param) => param.seriesIndex === 0)[0].data;
|
||||
let ma5=_.filter(params, (param) => param.seriesIndex === 1)[0].data;//ma5的值
|
||||
let ma10=_.filter(params, (param) => param.seriesIndex === 2)[0].data;//ma10的值
|
||||
let ma20=_.filter(params, (param) => param.seriesIndex === 3)[0].data;//ma20的值
|
||||
let ma30=_.filter(params, (param) => param.seriesIndex === 4)[0].data;//ma30的值
|
||||
let volum=_.filter(params, (param) => param.seriesIndex === 5)[0].data;
|
||||
return _.filter(params, (param) => param.seriesIndex === 0)[0].name + '<br>' +
|
||||
'开盘:' + currentItemData[1] + '<br>' +
|
||||
'收盘:' + currentItemData[2] + '<br>' +
|
||||
'最低:' + currentItemData[3] + '<br>' +
|
||||
'最高:' + currentItemData[4] + '<br>' +
|
||||
'成交量(万手):' + volum[1] + '<br>' +
|
||||
'MA5日均线:' + ma5 + '<br>' +
|
||||
'MA10日均线:' + ma10 + '<br>' +
|
||||
'MA20日均线:' + ma20 + '<br>' +
|
||||
'MA30日均线:' + ma30
|
||||
}
|
||||
},
|
||||
axisPointer: {
|
||||
link: [
|
||||
{
|
||||
xAxisIndex: 'all'
|
||||
}
|
||||
],
|
||||
label: {
|
||||
backgroundColor: '#888'
|
||||
}
|
||||
},
|
||||
visualMap: {
|
||||
show: false,
|
||||
seriesIndex: 5,
|
||||
dimension: 2,
|
||||
pieces: [
|
||||
{
|
||||
value: -1,
|
||||
color: downColor
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
color: upColor
|
||||
}
|
||||
]
|
||||
},
|
||||
grid: [
|
||||
{
|
||||
left: '8%',
|
||||
right: '8%',
|
||||
height: '50%',
|
||||
},
|
||||
{
|
||||
left: '8%',
|
||||
right: '8%',
|
||||
top: '66%',
|
||||
height: '15%'
|
||||
}
|
||||
],
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: categoryData,
|
||||
boundaryGap: false,
|
||||
axisLine: { onZero: false },
|
||||
splitLine: { show: false },
|
||||
min: 'dataMin',
|
||||
max: 'dataMax',
|
||||
axisPointer: {
|
||||
z: 100
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
gridIndex: 1,
|
||||
data: categoryData,
|
||||
boundaryGap: false,
|
||||
axisLine: { onZero: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false },
|
||||
min: 'dataMin',
|
||||
max: 'dataMax'
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
scale: true,
|
||||
splitArea: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
{
|
||||
scale: true,
|
||||
gridIndex: 1,
|
||||
splitNumber: 2,
|
||||
axisLabel: { show: false },
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false }
|
||||
}
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0, 1],
|
||||
start: 100-kDays,
|
||||
end: 100
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
xAxisIndex: [0, 1],
|
||||
type: 'slider',
|
||||
top: '85%',
|
||||
start: 100-kDays,
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
|
||||
series: [
|
||||
{
|
||||
name: '日K',
|
||||
type: 'candlestick',
|
||||
data: values,
|
||||
itemStyle: {
|
||||
color: upColor,
|
||||
color0: downColor,
|
||||
// borderColor: upBorderColor,
|
||||
// borderColor0: downBorderColor
|
||||
},
|
||||
markPoint: {
|
||||
//symbol: 'none',
|
||||
label: {
|
||||
formatter: function (param) {
|
||||
return param != null ? param.value + '' : '';
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{
|
||||
name: '最高',
|
||||
type: 'max',
|
||||
valueDim: 'highest'
|
||||
},
|
||||
{
|
||||
name: '最低',
|
||||
type: 'min',
|
||||
valueDim: 'lowest'
|
||||
},
|
||||
{
|
||||
name: '平均收盘价',
|
||||
type: 'average',
|
||||
valueDim: 'close'
|
||||
}
|
||||
],
|
||||
tooltip: {
|
||||
formatter: function (param) {
|
||||
return param.name + '<br>' + (param.data.coord || '');
|
||||
}
|
||||
}
|
||||
},
|
||||
markLine: {
|
||||
symbol: ['none', 'none'],
|
||||
data: [
|
||||
[
|
||||
{
|
||||
name: 'from lowest to highest',
|
||||
type: 'min',
|
||||
valueDim: 'lowest',
|
||||
symbol: 'circle',
|
||||
symbolSize: 10,
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'max',
|
||||
valueDim: 'highest',
|
||||
symbol: 'circle',
|
||||
symbolSize: 10,
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
name: 'min line on close',
|
||||
type: 'min',
|
||||
valueDim: 'close'
|
||||
},
|
||||
{
|
||||
name: 'max line on close',
|
||||
type: 'max',
|
||||
valueDim: 'close'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MA5',
|
||||
type: 'line',
|
||||
data: calculateMA(5,values),
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
opacity: 0.6
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MA10',
|
||||
type: 'line',
|
||||
data: calculateMA(10,values),
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
opacity: 0.6
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MA20',
|
||||
type: 'line',
|
||||
data: calculateMA(20,values),
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
opacity: 0.6
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MA30',
|
||||
type: 'line',
|
||||
data: calculateMA(30,values),
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
opacity: 0.6
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '成交量(手)',
|
||||
type: 'bar',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
itemStyle: {
|
||||
color: '#7fbe9e'
|
||||
},
|
||||
data: volumns
|
||||
}
|
||||
]
|
||||
};
|
||||
chart.setOption(option);
|
||||
|
||||
chart.on('click',{seriesName:'日K'}, function(params) {
|
||||
//console.log("click:",params);
|
||||
});
|
||||
})
|
||||
}
|
||||
function calculateMA(dayCount,values) {
|
||||
var result = [];
|
||||
for (var i = 0, len = values.length; i < len; i++) {
|
||||
if (i < dayCount) {
|
||||
result.push('-');
|
||||
continue;
|
||||
}
|
||||
var sum = 0;
|
||||
for (var j = 0; j < dayCount; j++) {
|
||||
sum += +values[i - j][1];
|
||||
}
|
||||
result.push((sum / dayCount).toFixed(2));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="kLineChartRef" style="width: 100%;height: auto;" :style="{height:chartHeight+'px'}"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
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" :name="item.SECURITY_NAME_ABBR" :k-days="20" :dark-theme="true"></k-line-chart>
|
||||
</n-popover>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ item.CLOSE_PRICE }}</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ (item.CHANGE_RATE).toFixed(2) }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<!-- <n-text :type="item.BILLBOARD_NET_AMT>0?'error':'success'">{{ (item.BILLBOARD_NET_AMT/10000).toFixed(2) }}</n-text>-->
|
||||
|
||||
|
||||
<n-popover trigger="hover" placement="right">
|
||||
<template #trigger>
|
||||
<n-button tag="a" text :type="item.BILLBOARD_NET_AMT>0?'error':'success'" :bordered=false >{{ (item.BILLBOARD_NET_AMT/10000).toFixed(2) }}</n-button>
|
||||
</template>
|
||||
<money-trend :code="item.SECUCODE.split('.')[1].toLowerCase()+item.SECUCODE.split('.')[0]" :name="item.SECURITY_NAME_ABBR" :days="360" :dark-theme="true" :chart-height="500" style="width: 800px"></money-trend>
|
||||
</n-popover>
|
||||
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="'error'">{{ (item.BILLBOARD_BUY_AMT/10000).toFixed(2) }}</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="'success'">{{ (item.BILLBOARD_SELL_AMT/10000).toFixed(2) }}</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="'info'">{{ (item.BILLBOARD_DEAL_AMT/10000).toFixed(2) }}</n-text>
|
||||
</n-td>
|
||||
<!-- <n-td>-->
|
||||
<!-- <n-text :type="'info'">{{ (item.ACCUM_AMOUNT/10000).toFixed(2) }}</n-text>-->
|
||||
<!-- </n-td>-->
|
||||
<!-- <n-td>-->
|
||||
<!-- <n-text :type="item.DEAL_NET_RATIO>0?'error':'success'">{{ (item.DEAL_NET_RATIO).toFixed(2) }}%</n-text>-->
|
||||
<!-- </n-td>-->
|
||||
<!-- <n-td>-->
|
||||
<!-- <n-text :type="'info'">{{ (item.DEAL_AMOUNT_RATIO).toFixed(2) }}%</n-text>-->
|
||||
<!-- </n-td>-->
|
||||
<n-td>
|
||||
<n-text :type="'info'">{{ (item.TURNOVERRATE).toFixed(2) }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="'info'">{{ (item.FREE_MARKET_CAP/100000000).toFixed(2) }}</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="'info'">{{ item.EXPLANATION }}</n-text>
|
||||
</n-td>
|
||||
<!-- <n-td>
|
||||
<n-text :type="item.CHANGE_RATE>0?'error':'success'">{{ item.EXPLAIN }}</n-text>
|
||||
</n-td>-->
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
126
frontend/src/components/SelectStock.vue
Normal file
126
frontend/src/components/SelectStock.vue
Normal file
@ -0,0 +1,126 @@
|
||||
<script setup lang="ts">
|
||||
import {h, onBeforeMount, onMounted, onUnmounted, ref} from 'vue'
|
||||
import {SearchStock} from "../../wailsjs/go/main/App";
|
||||
import {useMessage, NText, NTag} from 'naive-ui'
|
||||
const message = useMessage()
|
||||
const search = ref('科技股;换手率连续3日大于2')
|
||||
const columns = ref([])
|
||||
const dataList = ref([])
|
||||
|
||||
function Search() {
|
||||
const loading = message.loading("正在获取选股数据...", {duration: 0});
|
||||
SearchStock(search.value).then(res => {
|
||||
loading.destroy()
|
||||
//console.log(res)
|
||||
if(res.code==100){
|
||||
message.success(res.msg)
|
||||
columns.value=res.data.result.columns.filter(item=>!item.hiddenNeed&&(item.title!="市场码"&&item.title!="市场简称")).map(item=>{
|
||||
|
||||
if(item.children){
|
||||
return {
|
||||
title:item.title+(item.unit?'['+item.unit+']':''),
|
||||
key:item.key,
|
||||
resizable: true,
|
||||
minWidth:200,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
children:item.children.filter(item=>!item.hiddenNeed).map(item=>{
|
||||
return {
|
||||
title:item.dateMsg,
|
||||
key:item.key,
|
||||
minWidth:100,
|
||||
resizable: true,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}else{
|
||||
return {
|
||||
title:item.title+(item.unit?'['+item.unit+']':''),
|
||||
key:item.key,
|
||||
resizable: true,
|
||||
minWidth:100,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
dataList.value=res.data.result.dataList
|
||||
}else {
|
||||
message.error(res.msg)
|
||||
}
|
||||
}).catch(err => {
|
||||
message.error(err)
|
||||
})
|
||||
}
|
||||
function isNumeric(value) {
|
||||
return !isNaN(parseFloat(value)) && isFinite(value);
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
Search()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="search" placeholder="请输入选股指标或者要求" />
|
||||
<n-button type="success" @click="Search">搜索A股</n-button>
|
||||
</n-input-group>
|
||||
</n-flex>
|
||||
<!-- <n-table striped size="small">-->
|
||||
<!-- <n-thead>-->
|
||||
<!-- <n-tr>-->
|
||||
<!-- <n-th v-for="item in columns">{{item.title}}</n-th>-->
|
||||
<!-- </n-tr>-->
|
||||
<!-- </n-thead>-->
|
||||
<!-- <n-tbody>-->
|
||||
<!-- <n-tr v-for="(item,index) in dataList">-->
|
||||
<!-- <n-td v-for="d in columns">{{item[d.key]}}</n-td>-->
|
||||
<!-- </n-tr>-->
|
||||
<!-- </n-tbody>-->
|
||||
<!-- </n-table>-->
|
||||
<n-data-table
|
||||
:max-height="'calc(100vh - 285px)'"
|
||||
size="small"
|
||||
:columns="columns"
|
||||
:data="dataList"
|
||||
:pagination="false"
|
||||
:scroll-x="1800"
|
||||
:render-cell="(value, rowData, column) => {
|
||||
|
||||
if(column.key=='SECURITY_CODE'||column.key=='SERIAL'){
|
||||
return h(NText, { type: 'info',border: false }, { default: () => `${value}` })
|
||||
}
|
||||
if (isNumeric(value)) {
|
||||
let type='info';
|
||||
if (Number(value)<0){
|
||||
type='success';
|
||||
}
|
||||
if(Number(value)>=0&&Number(value)<=5){
|
||||
type='warning';
|
||||
}
|
||||
if (Number(value)>5){
|
||||
type='error';
|
||||
}
|
||||
return h(NText, { type: type }, { default: () => `${value}` })
|
||||
}else{
|
||||
if(column.key=='SECURITY_SHORT_NAME'){
|
||||
return h(NTag, { type: 'info',bordered: false }, { default: () => `${value}` })
|
||||
}else{
|
||||
return h(NText, { type: 'info' }, { default: () => `${value}` })
|
||||
}
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
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" :name="item.codes[0].short_name" :k-days="20" :dark-theme="true"></k-line-chart>
|
||||
</n-popover>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-a type="info" @click="openWin(item.art_code)"><n-text :type="getTypeColor(item.columns[0].column_name)"> {{item.title}}</n-text></n-a>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="getTypeColor(item.columns[0].column_name)">{{item.columns[0].column_name }}</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-tag type="info">{{item.notice_date.substring(0,10) }}</n-tag>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-tag type="info">{{item.display_time.substring(0,19)}}</n-tag>
|
||||
</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
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" :name="item.stockName" :k-days="20" :dark-theme="true"></k-line-chart>
|
||||
</n-popover>
|
||||
</n-td>
|
||||
<n-td><n-tag type="info" :bordered="false">{{item.indvInduName}}</n-tag></n-td>
|
||||
<n-td>
|
||||
<n-a type="info" @click="openWin(item.infoCode)">{{item.title}}</n-a>
|
||||
</n-td>
|
||||
<n-td><n-text :type="item.emRatingName==='增持'?'error':'info'">
|
||||
{{item.emRatingName}}
|
||||
</n-text></n-td>
|
||||
<n-td><n-text :type="item.ratingChange===0?'error':'info'">{{ratingChangeName(item.ratingChange)}}</n-text></n-td>
|
||||
<n-td>{{item.sRatingName}}</n-td>
|
||||
<n-td>{{item.researcher}}</n-td>
|
||||
<n-td>{{item.orgSName}}</n-td>
|
||||
<n-td>{{item.publishDate.substring(0,10)}}</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -2,9 +2,9 @@
|
||||
// import { MdPreview } from 'md-editor-v3';
|
||||
// preview.css相比style.css少了编辑器那部分样式
|
||||
import 'md-editor-v3/lib/preview.css';
|
||||
import {h, onMounted, ref} from 'vue';
|
||||
import {h, onBeforeUnmount, onMounted, ref} from 'vue';
|
||||
import {CheckUpdate, GetVersionInfo} from "../../wailsjs/go/main/App";
|
||||
import {EventsOn} from "../../wailsjs/runtime";
|
||||
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
|
||||
import {NAvatar, NButton, useNotification} from "naive-ui";
|
||||
const updateLog = ref('');
|
||||
const versionInfo = ref('');
|
||||
@ -23,8 +23,10 @@ onMounted(() => {
|
||||
wxpay.value=res.wxpay;
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
notify.destroyAll()
|
||||
EventsOff("updateVersion")
|
||||
})
|
||||
|
||||
EventsOn("updateVersion",async (msg) => {
|
||||
const githubTimeStr = msg.published_at;
|
||||
@ -42,8 +44,8 @@ EventsOn("updateVersion",async (msg) => {
|
||||
|
||||
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
|
||||
console.log("GitHub UTC 时间:", utcDate);
|
||||
console.log("转换后的本地时间:", formattedDate);
|
||||
//console.log("GitHub UTC 时间:", utcDate);
|
||||
//console.log("转换后的本地时间:", formattedDate);
|
||||
notify.info({
|
||||
avatar: () =>
|
||||
h(NAvatar, {
|
||||
@ -133,6 +135,7 @@ EventsOn("updateVersion",async (msg) => {
|
||||
</p>
|
||||
<p>
|
||||
感谢以下开发者:
|
||||
<a href="https://github.com/CodeNoobLH" target="_blank">浓睡不消残酒</a><n-divider vertical />
|
||||
<a href="https://github.com/gnim2600" target="_blank">@gnim2600</a><n-divider vertical />
|
||||
<a href="https://github.com/XXXiaohuayanGGG" target="_blank">@XXXiaohuayanGGG</a><n-divider vertical />
|
||||
<a href="https://github.com/2lovecode" target="_blank">@2lovecode</a><n-divider vertical />
|
||||
|
@ -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)
|
||||
|
||||
@ -89,6 +89,7 @@ onMounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(ticker.value)
|
||||
ws.value.close()
|
||||
message.destroyAll()
|
||||
})
|
||||
|
||||
|
||||
@ -102,7 +103,7 @@ function AddFund(){
|
||||
message.success("关注成功")
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
console.log("followList",followList.value)
|
||||
//console.log("followList",followList.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
@ -113,7 +114,7 @@ function unFollow(code){
|
||||
message.success("取消关注成功")
|
||||
GetFollowedFund().then(result => {
|
||||
followList.value = result
|
||||
console.log("followList",followList.value)
|
||||
//console.log("followList",followList.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
@ -180,7 +181,13 @@ function blinkBorder(findId){
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vue-danmaku v-model:danmus="danmus" style="height:100px; width:100%;z-index: 9;position:absolute; top: 400px; pointer-events: none;" ></vue-danmaku>
|
||||
<vue-danmaku v-model:danmus="danmus" useSlot style="height:100px; width:100%;z-index: 9;position:absolute; top: 400px; pointer-events: none;" >
|
||||
<template v-slot:dm="{ index, danmu }">
|
||||
<n-gradient-text type="info">
|
||||
<n-icon :component="ChatboxOutline"/>{{ danmu }}
|
||||
</n-gradient-text>
|
||||
</template>
|
||||
</vue-danmaku>
|
||||
<n-flex justify="start" >
|
||||
<n-grid :x-gap="8" :cols="3" :y-gap="8" >
|
||||
<n-gi :id="info.code+'_gi'" v-for="info in followList" style="margin-left: 2px" >
|
||||
@ -229,7 +236,7 @@ function blinkBorder(findId){
|
||||
<n-image :src="data.fenshiURL" />
|
||||
</n-modal>
|
||||
|
||||
<div style="position: fixed;bottom: 18px;right:2px;z-index: 10;width: 400px">
|
||||
<div style="position: fixed;bottom: 18px;right:5px;z-index: 10;width: 400px">
|
||||
<n-input-group >
|
||||
<n-auto-complete v-model:value="data.name"
|
||||
:input-props="{
|
||||
@ -238,12 +245,14 @@ function blinkBorder(findId){
|
||||
:options="options"
|
||||
placeholder="基金名称/代码/弹幕"
|
||||
clearable @update-value="getFundList" :on-select="onSelectFund"/>
|
||||
<n-button type="primary" @click="AddFund">
|
||||
<n-icon :component="Add"/> 关注
|
||||
</n-button>
|
||||
<n-button type="error" @click="SendDanmu" v-if="data.enableDanmu">
|
||||
<n-icon :component="ChatboxOutline"/> 发送弹幕
|
||||
</n-button>
|
||||
<n-button type="primary" @click="AddFund" >
|
||||
<n-icon :component="Add"/>
|
||||
关注
|
||||
</n-button>
|
||||
<n-button type="info" @click="SendDanmu" v-if="data.enableDanmu" >
|
||||
<n-icon :component="ChatboxOutline"/>
|
||||
发送弹幕
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</div>
|
||||
</template>
|
||||
|
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>
|
655
frontend/src/components/market.vue
Normal file
655
frontend/src/components/market.vue
Normal file
@ -0,0 +1,655 @@
|
||||
<script setup>
|
||||
import {computed, h, onBeforeMount, onBeforeUnmount, ref} from 'vue'
|
||||
import {
|
||||
GetAIResponseResult,
|
||||
GetConfig,
|
||||
GetIndustryRank,
|
||||
GetPromptTemplates,
|
||||
GetTelegraphList,
|
||||
GlobalStockIndexes,
|
||||
ReFleshTelegraphList,
|
||||
SaveAIResponseResult,
|
||||
SaveAsMarkdown,
|
||||
ShareAnalysis,
|
||||
SummaryStockNews
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import {EventsOff, EventsOn} from "../../wailsjs/runtime";
|
||||
import NewsList from "./newsList.vue";
|
||||
import KLineChart from "./KLineChart.vue";
|
||||
import { CaretDown, CaretUp, PulseOutline,} from "@vicons/ionicons5";
|
||||
import {NAvatar, NButton, NFlex, NText, useMessage, useNotification} from "naive-ui";
|
||||
import {MdPreview} from "md-editor-v3";
|
||||
import {useRoute} from 'vue-router'
|
||||
import RankTable from "./rankTable.vue";
|
||||
import IndustryMoneyRank from "./industryMoneyRank.vue";
|
||||
import StockResearchReportList from "./StockResearchReportList.vue";
|
||||
import StockNoticeList from "./StockNoticeList.vue";
|
||||
import LongTigerRankList from "./LongTigerRankList.vue";
|
||||
import IndustryResearchReportList from "./IndustryResearchReportList.vue";
|
||||
import HotStockList from "./HotStockList.vue";
|
||||
import HotEvents from "./HotEvents.vue";
|
||||
import HotTopics from "./HotTopics.vue";
|
||||
import InvestCalendarTimeLine from "./InvestCalendarTimeLine.vue";
|
||||
import ClsCalendarTimeLine from "./ClsCalendarTimeLine.vue";
|
||||
import SelectStock from "./SelectStock.vue";
|
||||
|
||||
const route = useRoute()
|
||||
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
|
||||
|
||||
const message = useMessage()
|
||||
const notify = useNotification()
|
||||
const panelHeight = ref(window.innerHeight - 240)
|
||||
|
||||
const telegraphList = ref([])
|
||||
const sinaNewsList = ref([])
|
||||
const common = ref([])
|
||||
const america = ref([])
|
||||
const europe = ref([])
|
||||
const asia = ref([])
|
||||
const other = ref([])
|
||||
const globalStockIndexes = ref(null)
|
||||
const summaryModal = ref(false)
|
||||
const summaryBTN = ref(true)
|
||||
const darkTheme = ref(false)
|
||||
const theme = computed(() => {
|
||||
return darkTheme ? 'dark' : 'light'
|
||||
})
|
||||
const aiSummary = ref(``)
|
||||
const aiSummaryTime = ref("")
|
||||
const modelName = ref("")
|
||||
const chatId = ref("")
|
||||
const question = ref(``)
|
||||
const sysPromptId = ref(0)
|
||||
const loading = ref(true)
|
||||
const sysPromptOptions = ref([])
|
||||
const userPromptOptions = ref([])
|
||||
const promptTemplates = ref([])
|
||||
const industryRanks = ref([])
|
||||
const sort = ref("0")
|
||||
const nowTab = ref("市场快讯")
|
||||
const indexInterval = ref(null)
|
||||
const indexIndustryRank = ref(null)
|
||||
const stockCode= ref('')
|
||||
|
||||
function getIndex() {
|
||||
GlobalStockIndexes().then((res) => {
|
||||
globalStockIndexes.value = res
|
||||
common.value = res["common"]
|
||||
america.value = res["america"]
|
||||
europe.value = res["europe"]
|
||||
asia.value = res["asia"]
|
||||
other.value = res["other"]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
onBeforeMount(() => {
|
||||
nowTab.value = route.query.name
|
||||
stockCode.value = route.query.stockCode
|
||||
GetConfig().then(result => {
|
||||
summaryBTN.value = result.openAiEnable
|
||||
darkTheme.value = result.darkTheme
|
||||
})
|
||||
GetPromptTemplates("", "").then(res => {
|
||||
promptTemplates.value = res
|
||||
sysPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型系统Prompt')
|
||||
userPromptOptions.value = promptTemplates.value.filter(item => item.type === '模型用户Prompt')
|
||||
})
|
||||
|
||||
GetTelegraphList("财联社电报").then((res) => {
|
||||
telegraphList.value = res
|
||||
})
|
||||
GetTelegraphList("新浪财经").then((res) => {
|
||||
sinaNewsList.value = res
|
||||
})
|
||||
getIndex();
|
||||
industryRank();
|
||||
indexInterval.value = setInterval(() => {
|
||||
getIndex()
|
||||
}, 3000)
|
||||
|
||||
indexIndustryRank.value = setInterval(() => {
|
||||
industryRank()
|
||||
}, 1000 * 10)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
EventsOff("changeMarketTab")
|
||||
EventsOff("newTelegraph")
|
||||
EventsOff("newSinaNews")
|
||||
EventsOff("summaryStockNews")
|
||||
clearInterval(indexInterval.value)
|
||||
clearInterval(indexIndustryRank.value)
|
||||
})
|
||||
|
||||
EventsOn("changeMarketTab", async (msg) => {
|
||||
//message.info(msg.name)
|
||||
updateTab(msg.name)
|
||||
})
|
||||
|
||||
EventsOn("newTelegraph", (data) => {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
telegraphList.value.pop()
|
||||
}
|
||||
telegraphList.value.unshift(...data)
|
||||
})
|
||||
EventsOn("newSinaNews", (data) => {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
sinaNewsList.value.pop()
|
||||
}
|
||||
sinaNewsList.value.unshift(...data)
|
||||
})
|
||||
|
||||
//获取页面高度
|
||||
window.onresize = () => {
|
||||
panelHeight.value = window.innerHeight - 240
|
||||
}
|
||||
|
||||
function getAreaName(code) {
|
||||
switch (code) {
|
||||
case "america":
|
||||
return "美洲"
|
||||
case "europe":
|
||||
return "欧洲"
|
||||
case "asia":
|
||||
return "亚洲"
|
||||
case "common":
|
||||
return "常用"
|
||||
case "other":
|
||||
return "其他"
|
||||
}
|
||||
}
|
||||
|
||||
function changeIndustryRankSort() {
|
||||
if (sort.value === "0") {
|
||||
sort.value = "1"
|
||||
} else {
|
||||
sort.value = "0"
|
||||
}
|
||||
industryRank()
|
||||
}
|
||||
|
||||
function industryRank() {
|
||||
|
||||
GetIndustryRank(sort.value, 150).then(result => {
|
||||
if (result.length > 0) {
|
||||
//console.log(result)
|
||||
industryRanks.value = result
|
||||
} else {
|
||||
message.info("暂无数据")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function reAiSummary() {
|
||||
aiSummary.value = ""
|
||||
summaryModal.value = true
|
||||
loading.value = true
|
||||
SummaryStockNews(question.value, sysPromptId.value)
|
||||
}
|
||||
|
||||
function getAiSummary() {
|
||||
summaryModal.value = true
|
||||
loading.value = true
|
||||
GetAIResponseResult("市场资讯").then(result => {
|
||||
if (result.content) {
|
||||
aiSummary.value = result.content
|
||||
question.value = result.question
|
||||
loading.value = false
|
||||
|
||||
const date = new Date(result.CreatedAt);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
aiSummaryTime.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
modelName.value = result.modelName
|
||||
} else {
|
||||
aiSummaryTime.value = ""
|
||||
aiSummary.value = ""
|
||||
modelName.value = ""
|
||||
SummaryStockNews(question.value, sysPromptId.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateTab(name) {
|
||||
summaryBTN.value = (name === "市场快讯");
|
||||
nowTab.value = name
|
||||
}
|
||||
|
||||
EventsOn("summaryStockNews", async (msg) => {
|
||||
loading.value = false
|
||||
////console.log(msg)
|
||||
if (msg === "DONE") {
|
||||
SaveAIResponseResult("市场资讯", "市场资讯", aiSummary.value, chatId.value, question.value)
|
||||
message.info("AI分析完成!")
|
||||
message.destroyAll()
|
||||
|
||||
} else {
|
||||
if (msg.chatId) {
|
||||
chatId.value = msg.chatId
|
||||
}
|
||||
if (msg.question) {
|
||||
question.value = msg.question
|
||||
}
|
||||
if (msg.content) {
|
||||
aiSummary.value = aiSummary.value + msg.content
|
||||
}
|
||||
if (msg.extraContent) {
|
||||
aiSummary.value = aiSummary.value + msg.extraContent
|
||||
}
|
||||
if (msg.model) {
|
||||
modelName.value = msg.model
|
||||
}
|
||||
if (msg.time) {
|
||||
aiSummaryTime.value = msg.time
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function copyToClipboard() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(aiSummary.value);
|
||||
message.success('分析结果已复制到剪切板');
|
||||
} catch (err) {
|
||||
message.error('复制失败: ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
function saveAsMarkdown() {
|
||||
SaveAsMarkdown('市场资讯', '市场资讯').then(result => {
|
||||
message.success(result)
|
||||
})
|
||||
}
|
||||
|
||||
function share() {
|
||||
ShareAnalysis('市场资讯', '市场资讯').then(msg => {
|
||||
//message.info(msg)
|
||||
notify.info({
|
||||
avatar: () =>
|
||||
h(NAvatar, {
|
||||
size: 'small',
|
||||
round: false,
|
||||
src: icon.value
|
||||
}),
|
||||
title: '分享到社区',
|
||||
duration: 1000 * 30,
|
||||
content: () => {
|
||||
return h('div', {
|
||||
style: {
|
||||
'text-align': 'left',
|
||||
'font-size': '14px',
|
||||
}
|
||||
}, {default: () => msg})
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function ReFlesh(source) {
|
||||
//console.log("ReFlesh:", source)
|
||||
ReFleshTelegraphList(source).then(res => {
|
||||
if (source === "财联社电报") {
|
||||
telegraphList.value = res
|
||||
}
|
||||
if (source === "新浪财经") {
|
||||
sinaNewsList.value = res
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card>
|
||||
<n-tabs type="line" animated @update-value="updateTab" :value="nowTab">
|
||||
<n-tab-pane name="市场快讯" tab="市场快讯">
|
||||
<n-grid :cols="2" :y-gap="0">
|
||||
<n-gi>
|
||||
<news-list :newsList="telegraphList" :header-title="'财联社电报'" @update:message="ReFlesh"></news-list>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<news-list :newsList="sinaNewsList" :header-title="'新浪财经'" @update:message="ReFlesh"></news-list>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="全球股指" tab="全球股指">
|
||||
<n-tabs type="segment" animated>
|
||||
<n-tab-pane name="全球指数" tab="全球指数">
|
||||
<n-grid :cols="5" :y-gap="0">
|
||||
<n-gi v-for="(val, key) in globalStockIndexes" :key="key">
|
||||
<n-list bordered>
|
||||
<template #header>
|
||||
{{ getAreaName(key) }}
|
||||
</template>
|
||||
<n-list-item v-for="item in val" :key="item.code">
|
||||
<n-grid :cols="3" :y-gap="0">
|
||||
<n-gi>
|
||||
|
||||
<n-text :type="item.zdf>0?'error':'success'">
|
||||
<n-image :src="item.img" width="20"/> {{ 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-gi>
|
||||
<n-gi>
|
||||
<n-text :type="item.state === 'open' ? 'success' : 'warning'">{{
|
||||
item.state === 'open' ? '开市' : '休市'
|
||||
}}
|
||||
</n-text>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="上证指数" tab="上证指数">
|
||||
<k-line-chart code="sh000001" :chart-height="panelHeight" name="上证指数" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="深证成指" tab="深证成指">
|
||||
<k-line-chart code="sz399001" :chart-height="panelHeight" name="深证成指" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="创业板指" tab="创业板指">
|
||||
<k-line-chart code="sz399006" :chart-height="panelHeight" name="创业板指" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="恒生指数" tab="恒生指数">
|
||||
<k-line-chart code="hkHSI" :chart-height="panelHeight" name="恒生指数" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="纳斯达克" tab="纳斯达克">
|
||||
<k-line-chart code="us.IXIC" :chart-height="panelHeight" name="纳斯达克" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="道琼斯" tab="道琼斯">
|
||||
<k-line-chart code="us.DJI" :chart-height="panelHeight" name="道琼斯" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="标普500" tab="标普500">
|
||||
<k-line-chart code="us.INX" :chart-height="panelHeight" name="标普500" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="指标行情" tab="指标行情">
|
||||
<n-tabs type="segment" animated>
|
||||
<n-tab-pane name="科创50" tab="科创50">
|
||||
<k-line-chart code="sh000688" :chart-height="panelHeight" name="科创50" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="沪深300" tab="沪深300">
|
||||
<k-line-chart code="sh000300" :chart-height="panelHeight" name="沪深300" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="上证50" tab="上证50">
|
||||
<k-line-chart code="sh000016" :chart-height="panelHeight" name="上证50" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="中证A500" tab="中证A500">
|
||||
<k-line-chart code="sh000510" :chart-height="panelHeight" name="中证A500" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="中证1000" tab="中证1000">
|
||||
<k-line-chart code="sh000852" :chart-height="panelHeight" name="中证1000" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="中证白酒" tab="中证白酒">
|
||||
<k-line-chart code="sz399997" :chart-height="panelHeight" name="中证白酒" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="富时中国三倍做多" tab="富时中国三倍做多">
|
||||
<k-line-chart code="usYINN.AM" :chart-height="panelHeight" name="富时中国三倍做多" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="VIX恐慌指数" tab="VIX恐慌指数">
|
||||
<k-line-chart code="usUVXY.AM" :chart-height="panelHeight" name="VIX恐慌指数" :k-days="20"
|
||||
:dark-theme="true"></k-line-chart>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="行业排名" tab="行业排名">
|
||||
<n-tabs type="card" animated>
|
||||
<n-tab-pane name="行业涨幅排名" tab="行业涨幅排名">
|
||||
<n-table striped>
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<n-th>行业名称</n-th>
|
||||
<n-th @click="changeIndustryRankSort">行业涨幅
|
||||
<n-icon v-if="sort==='0'" :component="CaretDown"/>
|
||||
<n-icon v-if="sort==='1'" :component="CaretUp"/>
|
||||
</n-th>
|
||||
<n-th>行业5日涨幅</n-th>
|
||||
<n-th>行业20日涨幅</n-th>
|
||||
<n-th>领涨股</n-th>
|
||||
<n-th>涨幅</n-th>
|
||||
<n-th>最新价</n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr v-for="item in industryRanks" :key="item.bd_code">
|
||||
<n-td>
|
||||
<n-tag :bordered=false type="info">{{ item.bd_name }}</n-tag>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.bd_zdf>0?'error':'success'">{{ item.bd_zdf }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.bd_zdf5>0?'error':'success'">{{ item.bd_zdf5 }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.bd_zdf20>0?'error':'success'">{{ item.bd_zdf20 }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_name }}
|
||||
<n-text type="info">{{ item.nzg_code }}</n-text>
|
||||
</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_zdf }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.nzg_zdf>0?'error':'success'">{{ item.nzg_zxj }}</n-text>
|
||||
</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
<n-table striped>
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<n-th>行业名称</n-th>
|
||||
<n-th @click="changeIndustryRankSort">行业涨幅
|
||||
<n-icon v-if="sort==='0'" :component="CaretDown"/>
|
||||
<n-icon v-if="sort==='1'" :component="CaretUp"/>
|
||||
</n-th>
|
||||
<n-th>行业5日涨幅</n-th>
|
||||
<n-th>行业20日涨幅</n-th>
|
||||
<n-th>领涨股</n-th>
|
||||
<n-th>涨幅</n-th>
|
||||
<n-th>最新价</n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr v-for="item in industryRanks" :key="item.bd_code">
|
||||
<n-td>
|
||||
<n-tag :bordered=false type="info">{{ item.bd_name }}</n-tag>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.bd_zdf>0?'error':'success'">{{ item.bd_zdf }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.bd_zdf5>0?'error':'success'">{{ item.bd_zdf5 }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.bd_zdf20>0?'error':'success'">{{ item.bd_zdf20 }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_name }}
|
||||
<n-text type="info">{{ item.nzg_code }}</n-text>
|
||||
</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.nzg_zdf>0?'error':'success'"> {{ item.nzg_zdf }}%</n-text>
|
||||
</n-td>
|
||||
<n-td>
|
||||
<n-text :type="item.nzg_zdf>0?'error':'success'">{{ item.nzg_zxj }}</n-text>
|
||||
</n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="行业资金排名(净流入)" tab="行业资金排名">
|
||||
<industryMoneyRank :fenlei="'0'" :header-title="'行业资金排名(净流入)'" :sort="'netamount'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="证监会行业资金排名(净流入)" tab="证监会行业资金排名">
|
||||
<industryMoneyRank :fenlei="'2'" :header-title="'证监会行业资金排名(净流入)'" :sort="'netamount'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="概念板块资金排名(净流入)" tab="概念板块资金排名">
|
||||
<industryMoneyRank :fenlei="'1'" :header-title="'概念板块资金排名(净流入)'" :sort="'netamount'"/>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="个股资金流向" tab="个股资金流向">
|
||||
<n-tabs type="card" animated>
|
||||
<n-tab-pane name="netamount" tab="净流入额排名">
|
||||
<RankTable :header-title="'净流入额排名'" :sort="'netamount'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="outamount" tab="流出资金排名">
|
||||
<RankTable :header-title="'流出资金排名'" :sort="'outamount'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="ratioamount" tab="净流入率排名">
|
||||
<RankTable :header-title="'净流入率排名'" :sort="'ratioamount'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="r0_net" tab="主力净流入额排名">
|
||||
<RankTable :header-title="'主力净流入额排名'" :sort="'r0_net'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="r0_out" tab="主力流出排名">
|
||||
<RankTable :header-title="'主力流出排名'" :sort="'r0_out'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="r0_ratio" tab="主力净流入率排名">
|
||||
<RankTable :header-title="'主力净流入率排名'" :sort="'r0_ratio'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="r3_net" tab="散户净流入额排名">
|
||||
<RankTable :header-title="'散户净流入额排名'" :sort="'r3_net'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="r3_out" tab="散户流出排名">
|
||||
<RankTable :header-title="'散户流出排名'" :sort="'r3_out'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="r3_ratio" tab="散户净流入率排名">
|
||||
<RankTable :header-title="'散户净流入率排名'" :sort="'r3_ratio'"/>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="龙虎榜" tab="龙虎榜">
|
||||
<LongTigerRankList />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="个股研报" tab="个股研报">
|
||||
<StockResearchReportList :stock-code="stockCode"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="公司公告" tab="公司公告 ">
|
||||
<StockNoticeList :stock-code="stockCode" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="行业研究" tab="行业研究 ">
|
||||
<IndustryResearchReportList/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="当前热门" tab="当前热门">
|
||||
<n-tabs type="card" animated>
|
||||
<n-tab-pane name="全球" tab="全球">
|
||||
<HotStockList :market-type="'10'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="沪深" tab="沪深">
|
||||
<HotStockList :market-type="'12'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="港股" tab="港股">
|
||||
<HotStockList :market-type="'13'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="美股" tab="美股">
|
||||
<HotStockList :market-type="'11'"/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="热门话题" tab="热门话题">
|
||||
<n-grid :cols="1" :y-gap="10">
|
||||
<n-grid-item>
|
||||
<HotTopics/>
|
||||
</n-grid-item>
|
||||
<!-- <n-grid-item>-->
|
||||
<!-- <HotEvents/>-->
|
||||
<!-- </n-grid-item>-->
|
||||
</n-grid>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="重大事件时间轴" tab="重大事件时间轴">
|
||||
<InvestCalendarTimeLine />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="财经日历" tab="财经日历">
|
||||
<ClsCalendarTimeLine />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="指标选股" tab="指标选股">
|
||||
<select-stock />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
<n-modal transform-origin="center" v-model:show="summaryModal" preset="card" style="width: 800px;"
|
||||
:title="'AI市场资讯总结'">
|
||||
<n-spin size="small" :show="loading">
|
||||
<MdPreview style="height: 440px;text-align: left" :modelValue="aiSummary" :theme="theme"/>
|
||||
</n-spin>
|
||||
<template #footer>
|
||||
<n-flex justify="space-between" ref="tipsRef">
|
||||
<n-text type="info" v-if="aiSummaryTime">
|
||||
<n-tag v-if="modelName" type="warning" round :title="chatId" :bordered="false">{{ modelName }}</n-tag>
|
||||
{{ aiSummaryTime }}
|
||||
</n-text>
|
||||
<n-text type="error">*AI分析结果仅供参考,请以实际行情为准。投资需谨慎,风险自担。</n-text>
|
||||
</n-flex>
|
||||
</template>
|
||||
<template #action>
|
||||
<n-flex justify="space-between" style="margin-bottom: 10px">
|
||||
<n-select style="width: 49%" v-model:value="sysPromptId" label-field="name" value-field="ID"
|
||||
:options="sysPromptOptions" placeholder="请选择系统提示词"/>
|
||||
<n-select style="width: 49%" v-model:value="question" label-field="name" value-field="content"
|
||||
:options="userPromptOptions" placeholder="请选择用户提示词"/>
|
||||
</n-flex>
|
||||
<n-flex justify="right">
|
||||
<n-input v-model:value="question" style="text-align: left" clearable
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
placeholder="请输入您的问题:例如 总结和分析股票市场新闻中的投资机会"
|
||||
:autosize="{
|
||||
minRows: 2,
|
||||
maxRows: 5
|
||||
}"
|
||||
/>
|
||||
<n-button size="tiny" type="warning" @click="reAiSummary">再次总结</n-button>
|
||||
<n-button size="tiny" type="success" @click="copyToClipboard">复制到剪切板</n-button>
|
||||
<n-button size="tiny" type="primary" @click="saveAsMarkdown">保存为Markdown文件</n-button>
|
||||
<n-button size="tiny" type="error" @click="share">分享到项目社区</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<div style="position: fixed;bottom: 18px;right:25px;z-index: 10;" v-if="summaryBTN">
|
||||
<n-input-group>
|
||||
<n-button type="primary" @click="getAiSummary">
|
||||
<n-icon :component="PulseOutline"/> 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>
|
58
frontend/src/components/newsList.vue
Normal file
58
frontend/src/components/newsList.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<script setup>
|
||||
import {ReFleshTelegraphList} from "../../wailsjs/go/main/App";
|
||||
import {RefreshCircle, RefreshCircleSharp, RefreshOutline} from "@vicons/ionicons5";
|
||||
|
||||
const { headerTitle,newsList } = defineProps({
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '市场资讯'
|
||||
},
|
||||
newsList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:message'])
|
||||
|
||||
const updateMessage = () => {
|
||||
emits('update:message', headerTitle)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-list bordered>
|
||||
<template #header>
|
||||
<n-flex justify="space-between">
|
||||
<n-tag :bordered="false" size="large" type="success" >{{ headerTitle }}</n-tag>
|
||||
<n-button :bordered="false" @click="updateMessage"><n-icon color="#409EFF" size="25" :component="RefreshCircleSharp"/></n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
<n-list-item v-for="item in newsList">
|
||||
<n-space justify="start">
|
||||
<n-text justify="start" :bordered="false" :type="item.isRed?'error':'info'">
|
||||
<n-tag size="small" :type="item.isRed?'error':'warning'" :bordered="false"> {{ item.time }}</n-tag>
|
||||
{{ item.content }}
|
||||
</n-text>
|
||||
</n-space>
|
||||
<n-space v-if="item.subjects" style="margin-top: 2px">
|
||||
<n-tag :bordered="false" type="success" size="small" v-for="sub in item.subjects">
|
||||
{{ sub }}
|
||||
</n-tag>
|
||||
<n-space v-if="item.stocks">
|
||||
<n-tag :bordered="false" type="warning" size="small" v-for="sub in item.stocks">
|
||||
{{ sub }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
<n-tag v-if="item.url" :bordered="false" type="warning" size="small">
|
||||
<a :href="item.url" target="_blank">
|
||||
<n-text type="warning">查看原文</n-text>
|
||||
</a>
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
101
frontend/src/components/rankTable.vue
Normal file
101
frontend/src/components/rankTable.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<script setup>
|
||||
|
||||
import {CaretDown, CaretUp, RefreshCircleOutline} from "@vicons/ionicons5";
|
||||
import {NText,useMessage} from "naive-ui";
|
||||
import {onBeforeUnmount, onMounted, onUnmounted, ref} from "vue";
|
||||
import {GetMoneyRankSina} from "../../wailsjs/go/main/App";
|
||||
import KLineChart from "./KLineChart.vue";
|
||||
|
||||
const props = defineProps({
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '净流入额排名'
|
||||
},
|
||||
sort: {
|
||||
type: String,
|
||||
default: 'netamount'
|
||||
},
|
||||
})
|
||||
const message = useMessage()
|
||||
const dataList= ref([])
|
||||
const sort = ref(props.sort)
|
||||
const interval = ref(null)
|
||||
onMounted(()=>{
|
||||
sort.value=props.sort
|
||||
GetMoneyRankSinaData()
|
||||
interval.value=setInterval(()=>{
|
||||
GetMoneyRankSinaData()
|
||||
},1000*60)
|
||||
})
|
||||
onBeforeUnmount(()=>{
|
||||
clearInterval(interval.value)
|
||||
})
|
||||
function GetMoneyRankSinaData(){
|
||||
message.loading("正在刷新数据...")
|
||||
GetMoneyRankSina(sort.value).then(result => {
|
||||
if(result.length>0){
|
||||
dataList.value = result
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-table striped size="small">
|
||||
<n-thead>
|
||||
<n-tr>
|
||||
<n-th>代码</n-th>
|
||||
<n-th>名称</n-th>
|
||||
<n-th>最新价</n-th>
|
||||
<n-th>涨跌幅</n-th>
|
||||
<n-th>换手率</n-th>
|
||||
<n-th>成交额/万</n-th>
|
||||
<n-th>流出资金/万</n-th>
|
||||
<n-th>流入资金/万</n-th>
|
||||
<n-th>净流入/万</n-th>
|
||||
<n-th>净流入率</n-th>
|
||||
<n-th v-if="sort === 'r0_net'||sort==='r0_out'">主力流出/万</n-th>
|
||||
<n-th v-if="sort === 'r0_net'">主力流入/万</n-th>
|
||||
<n-th v-if="sort === 'r0_net'">主力净流入/万</n-th>
|
||||
<n-th >主力净流入率</n-th>
|
||||
<n-th v-if="sort === 'r3_net'||sort==='r3_out'">散户流出/万</n-th>
|
||||
<n-th v-if="sort === 'r3_net'">散户流入/万</n-th>
|
||||
<n-th v-if="sort === 'r3_net'">散户净流入/万</n-th>
|
||||
<n-th >散户净流入率</n-th>
|
||||
</n-tr>
|
||||
</n-thead>
|
||||
<n-tbody>
|
||||
<n-tr v-for="item in dataList" :key="item.symbol">
|
||||
<n-td><n-tag :bordered=false type="info">{{ item.symbol }}</n-tag></n-td>
|
||||
<n-td>
|
||||
<n-popover trigger="hover" placement="right">
|
||||
<template #trigger>
|
||||
<n-button tag="a" text :type="item.changeratio>0?'error':'success'" :bordered=false >{{ item.name }}</n-button>
|
||||
</template>
|
||||
<k-line-chart style="width: 800px" :code="item.symbol" :chart-height="500" :name="item.name" :k-days="20" :dark-theme="true"></k-line-chart>
|
||||
</n-popover>
|
||||
</n-td>
|
||||
<n-td><n-text :type="item.changeratio>0?'error':'success'">{{item.trade}}</n-text></n-td>
|
||||
<n-td><n-text :type="item.changeratio>0?'error':'success'">{{(item.changeratio*100).toFixed(2)}}%</n-text></n-td>
|
||||
<n-td><n-text :type="item.turnover>500?'error':'info'">{{(item.turnover/100).toFixed(2)}}%</n-text></n-td>
|
||||
<n-td><n-text type="info">{{(item.amount/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td><n-text type="info"> {{(item.outamount/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td><n-text type="info"> {{(item.inamount/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td><n-text type="info"> {{(item.netamount/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td><n-text :type="item.ratioamount>0?'error':'success'"> {{(item.ratioamount*100).toFixed(2)}}%</n-text></n-td>
|
||||
<n-td v-if="sort === 'r0_net'||sort==='r0_out'"><n-text type="success"> {{(item.r0_out/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td v-if="sort === 'r0_net'"><n-text type="error"> {{(item.r0_in/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td v-if="sort === 'r0_net'"><n-text :type="item.r0_net>0?'error':'success'"> {{(item.r0_net/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td ><n-text :type="item.r0_ratio>0?'error':'success'"> {{(item.r0_ratio*100).toFixed(2)}}%</n-text></n-td>
|
||||
<n-td v-if="sort === 'r3_net'||sort==='r3_out'"><n-text type="success"> {{(item.r3_out/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td v-if="sort === 'r3_net'"><n-text type="error"> {{(item.r3_in/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td v-if="sort === 'r3_net'"><n-text :type="item.r3_net>0?'error':'success'"> {{(item.r3_net/10000).toFixed(2)}}</n-text></n-td>
|
||||
<n-td ><n-text :type="item.r3_ratio>0?'error':'success'"> {{(item.r3_ratio*100).toFixed(2)}}%</n-text></n-td>
|
||||
</n-tr>
|
||||
</n-tbody>
|
||||
</n-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -1,9 +1,16 @@
|
||||
<script setup>
|
||||
|
||||
import {onMounted, ref} from "vue";
|
||||
import {ExportConfig, GetConfig, SendDingDingMessageByType, UpdateConfig} from "../../wailsjs/go/main/App";
|
||||
import {computed, onBeforeUnmount, onMounted, ref} from "vue";
|
||||
import {
|
||||
AddPrompt, DelPrompt,
|
||||
ExportConfig,
|
||||
GetConfig,
|
||||
GetPromptTemplates,
|
||||
SendDingDingMessageByType,
|
||||
UpdateConfig
|
||||
} from "../../wailsjs/go/main/App";
|
||||
import {useMessage} from "naive-ui";
|
||||
import {data} from "../../wailsjs/go/models";
|
||||
import {data, models} from "../../wailsjs/go/models";
|
||||
import {EventsEmit} from "../../wailsjs/runtime";
|
||||
const message = useMessage()
|
||||
|
||||
@ -35,8 +42,12 @@ const formValue = ref({
|
||||
},
|
||||
enableDanmu:false,
|
||||
browserPath: '',
|
||||
enableNews:false,
|
||||
darkTheme:true,
|
||||
enableFund:false,
|
||||
enablePushNews:false,
|
||||
})
|
||||
|
||||
const promptTemplates=ref([])
|
||||
onMounted(()=>{
|
||||
GetConfig().then(res=>{
|
||||
formValue.value.ID = res.ID
|
||||
@ -65,13 +76,26 @@ onMounted(()=>{
|
||||
}
|
||||
formValue.value.enableDanmu = res.enableDanmu
|
||||
formValue.value.browserPath = res.browserPath
|
||||
console.log(res)
|
||||
formValue.value.enableNews = res.enableNews
|
||||
formValue.value.darkTheme = res.darkTheme
|
||||
formValue.value.enableFund = res.enableFund
|
||||
formValue.value.enablePushNews = res.enablePushNews
|
||||
|
||||
//console.log(res)
|
||||
})
|
||||
//message.info("加载完成")
|
||||
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
//console.log(res)
|
||||
promptTemplates.value=res
|
||||
})
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
message.destroyAll()
|
||||
})
|
||||
|
||||
|
||||
function saveConfig(){
|
||||
|
||||
let config= new data.Settings({
|
||||
ID:formValue.value.ID,
|
||||
dingPushEnable:formValue.value.dingPush.enable,
|
||||
@ -92,12 +116,18 @@ function saveConfig(){
|
||||
crawlTimeOut:formValue.value.openAI.crawlTimeOut,
|
||||
kDays:formValue.value.openAI.kDays,
|
||||
enableDanmu:formValue.value.enableDanmu,
|
||||
browserPath:formValue.value.browserPath
|
||||
browserPath:formValue.value.browserPath,
|
||||
enableNews:formValue.value.enableNews,
|
||||
darkTheme:formValue.value.darkTheme,
|
||||
enableFund:formValue.value.enableFund,
|
||||
enablePushNews:formValue.value.enablePushNews
|
||||
})
|
||||
|
||||
//console.log("Settings",config)
|
||||
|
||||
//console.log("Settings",config)
|
||||
UpdateConfig(config).then(res=>{
|
||||
message.success(res)
|
||||
EventsEmit("updateSettings", config);
|
||||
})
|
||||
}
|
||||
|
||||
@ -138,7 +168,7 @@ function importConfig(){
|
||||
let reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
let config = JSON.parse(e.target.result);
|
||||
console.log(config)
|
||||
//console.log(config)
|
||||
formValue.value.ID = config.ID
|
||||
formValue.value.tushareToken = config.tushareToken
|
||||
formValue.value.dingPush = {
|
||||
@ -165,6 +195,10 @@ function importConfig(){
|
||||
}
|
||||
formValue.value.enableDanmu = config.enableDanmu
|
||||
formValue.value.browserPath = config.browserPath
|
||||
formValue.value.enableNews = config.enableNews
|
||||
formValue.value.darkTheme = config.darkTheme
|
||||
formValue.value.enableFund = config.enableFund
|
||||
formValue.value.enablePushNews = config.enablePushNews
|
||||
// formRef.value.resetFields()
|
||||
};
|
||||
reader.readAsText(file);
|
||||
@ -174,7 +208,7 @@ function importConfig(){
|
||||
|
||||
|
||||
window.onerror = function (event, source, lineno, colno, error) {
|
||||
console.log(event, source, lineno, colno, error)
|
||||
//console.log(event, source, lineno, colno, error)
|
||||
// 将错误信息发送给后端
|
||||
EventsEmit("frontendError", {
|
||||
page: "settings.vue",
|
||||
@ -187,46 +221,101 @@ window.onerror = function (event, source, lineno, colno, error) {
|
||||
//message.error("发生错误:"+event)
|
||||
return true;
|
||||
};
|
||||
|
||||
const showManagePromptsModal=ref(false)
|
||||
const promptTypeOptions=[
|
||||
{label:"模型系统Prompt",value:'模型系统Prompt'},
|
||||
{label:"模型用户Prompt",value:'模型用户Prompt'},]
|
||||
const formPromptRef=ref(null)
|
||||
const formPrompt=ref({
|
||||
ID:0,
|
||||
Name:'',
|
||||
Content:'',
|
||||
Type:'',
|
||||
})
|
||||
function managePrompts(){
|
||||
formPrompt.value.ID=0
|
||||
showManagePromptsModal.value=true
|
||||
}
|
||||
function savePrompt(){
|
||||
AddPrompt(formPrompt.value).then(res=>{
|
||||
message.success(res)
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
//console.log(res)
|
||||
promptTemplates.value=res
|
||||
})
|
||||
showManagePromptsModal.value=false
|
||||
})
|
||||
}
|
||||
function editPrompt(prompt){
|
||||
//console.log(prompt)
|
||||
formPrompt.value.ID=prompt.ID
|
||||
formPrompt.value.Name=prompt.name
|
||||
formPrompt.value.Content=prompt.content
|
||||
formPrompt.value.Type=prompt.type
|
||||
showManagePromptsModal.value=true
|
||||
}
|
||||
function deletePrompt(ID){
|
||||
DelPrompt(ID).then(res=>{
|
||||
message.success(res)
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
//console.log(res)
|
||||
promptTemplates.value=res
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex justify="left" style="margin-top: 12px;padding-left: 12px">
|
||||
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'">
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left">
|
||||
<n-flex justify="left" style="margin-top: 12px;padding-left: 12px;">
|
||||
<n-form ref="formRef" :label-placement="'left'" :label-align="'left'" >
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left" :layout-shift-disabled="true">
|
||||
<n-gi :span="24">
|
||||
<n-text type="default" style="font-size: 25px;font-weight: bold">基础设置</n-text>
|
||||
<n-text type="success" style="font-size: 25px;font-weight: bold">基础设置</n-text>
|
||||
</n-gi>
|
||||
<n-form-item-gi :span="10" label="Tushare api token:" path="tushareToken" >
|
||||
<n-form-item-gi :span="10" label="Tushare Token:" path="tushareToken" >
|
||||
<n-input type="text" placeholder="Tushare api token" v-model:value="formValue.tushareToken" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="启动时更新A股/指数信息:" path="updateBasicInfoOnStart" >
|
||||
<n-switch v-model:value="formValue.updateBasicInfoOnStart" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" label="数据刷新间隔(重启生效):" path="refreshInterval" >
|
||||
<n-form-item-gi :span="4" label="数据刷新间隔:" path="refreshInterval" >
|
||||
<n-input-number v-model:value="formValue.refreshInterval" placeholder="请输入数据刷新间隔(秒)">
|
||||
<template #suffix>
|
||||
秒
|
||||
</template>
|
||||
</n-input-number>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="22" label="浏览器路径:" path="browserPath" >
|
||||
<n-input type="text" placeholder="浏览器路径" v-model:value="formValue.browserPath" clearable />
|
||||
<n-form-item-gi :span="6" label="暗黑主题:" path="darkTheme" >
|
||||
<n-switch v-model:value="formValue.darkTheme" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="10" label="浏览器安装路径:" path="browserPath" >
|
||||
<n-input type="text" placeholder="浏览器安装路径" v-model:value="formValue.browserPath" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="6" label="指数基金:" path="enableFund" >
|
||||
<n-switch v-model:value="formValue.enableFund" />
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left">
|
||||
<n-gi :span="24">
|
||||
<n-text type="default" style="font-size: 25px;font-weight: bold">通知设置</n-text>
|
||||
<n-text type="success" style="font-size: 25px;font-weight: bold">通知设置</n-text>
|
||||
</n-gi>
|
||||
<n-form-item-gi :span="6" label="是否启用钉钉推送:" path="dingPush.enable" >
|
||||
<n-form-item-gi :span="4" label="钉钉推送:" path="dingPush.enable" >
|
||||
<n-switch v-model:value="formValue.dingPush.enable" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="6" label="是否启用本地推送:" path="localPush.enable" >
|
||||
<n-form-item-gi :span="4" label="本地推送:" path="localPush.enable" >
|
||||
<n-switch v-model:value="formValue.localPush.enable" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" label="弹幕功能:" path="enableDanmu" >
|
||||
<n-form-item-gi :span="4" label="弹幕功能:" path="enableDanmu" >
|
||||
<n-switch v-model:value="formValue.enableDanmu" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="显示滚动快讯:" path="enableNews" >
|
||||
<n-switch v-model:value="formValue.enableNews" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="市场资讯提醒:" path="enablePushNews" >
|
||||
<n-switch v-model:value="formValue.enablePushNews" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="22" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:" path="dingPush.dingRobot" >
|
||||
<n-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
|
||||
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
|
||||
@ -235,36 +324,36 @@ window.onerror = function (event, source, lineno, colno, error) {
|
||||
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left;">
|
||||
<n-gi :span="24">
|
||||
<n-text type="default" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
|
||||
<n-text type="success" style="font-size: 25px;font-weight: bold">OpenAI设置</n-text>
|
||||
</n-gi>
|
||||
<n-form-item-gi :span="3" label="是否启用AI诊股:" path="openAI.enable" >
|
||||
<n-form-item-gi :span="3" label="AI诊股:" path="openAI.enable" >
|
||||
<n-switch v-model:value="formValue.openAI.enable" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="9" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl">
|
||||
<n-form-item-gi :span="9" v-if="formValue.openAI.enable" label="openAI 接口地址:" path="openAI.baseUrl" >
|
||||
<n-input type="text" placeholder="AI接口地址" v-model:value="formValue.openAI.baseUrl" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="AI Timeout(秒):" title="AI请求超时时间(秒)" path="openAI.timeout">
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="AI Timeout(秒):" title="AI请求超时时间(秒)" path="openAI.timeout" >
|
||||
<n-input-number min="60" step="1" placeholder="AI请求超时时间(秒)" v-model:value="formValue.openAI.timeout" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="Crawler Timeout(秒):" title="资讯采集超时时间(秒)" path="openAI.crawlTimeOut">
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="Crawler Timeout(秒):" title="资讯采集超时时间(秒)" path="openAI.crawlTimeOut" >
|
||||
<n-input-number min="30" step="1" placeholder="资讯采集超时时间(秒)" v-model:value="formValue.openAI.crawlTimeOut" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI 令牌(apiKey):" path="openAI.apiKey">
|
||||
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI 令牌(apiKey):" path="openAI.apiKey" >
|
||||
<n-input type="text" placeholder="apiKey" v-model:value="formValue.openAI.apiKey" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="AI模型名称:" path="openAI.model">
|
||||
<n-form-item-gi :span="10" v-if="formValue.openAI.enable" label="AI模型名称:" path="openAI.model" >
|
||||
<n-input type="text" placeholder="AI模型名称" v-model:value="formValue.openAI.model" clearable />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" v-if="formValue.openAI.enable" label="openAI temperature:" path="openAI.temperature" >
|
||||
<n-input-number placeholder="temperature" v-model:value="formValue.openAI.temperature"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="openAI maxTokens:" path="openAI.maxTokens">
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" label="openAI maxTokens:" path="openAI.maxTokens" >
|
||||
<n-input-number placeholder="maxTokens" v-model:value="formValue.openAI.maxTokens"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多" label="日K线数据(天):" path="openAI.maxTokens">
|
||||
<n-form-item-gi :span="5" v-if="formValue.openAI.enable" title="天数越多消耗tokens越多" label="日K线数据(天):" path="openAI.maxTokens" >
|
||||
<n-input-number min="30" step="1" max="365" placeholder="日K线数据(天)" title="天数越多消耗tokens越多" v-model:value="formValue.openAI.kDays"/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="22" v-if="formValue.openAI.enable" label="模型系统 Prompt:" path="openAI.prompt">
|
||||
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="模型系统 Prompt:" path="openAI.prompt" >
|
||||
<n-input v-model:value="formValue.openAI.prompt"
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
@ -275,20 +364,23 @@ window.onerror = function (event, source, lineno, colno, error) {
|
||||
}"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="22" v-if="formValue.openAI.enable" label="模型用户 Prompt:" path="openAI.questionTemplate">
|
||||
<n-form-item-gi :span="11" v-if="formValue.openAI.enable" label="模型用户 Prompt:" path="openAI.questionTemplate" >
|
||||
<n-input v-model:value="formValue.openAI.questionTemplate"
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
placeholder="请输入用户prompt:例如{{stockName}}[{{stockCode}}]分析和总结"
|
||||
:autosize="{
|
||||
minRows: 2,
|
||||
maxRows: 5
|
||||
minRows: 5,
|
||||
maxRows: 8
|
||||
}"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
<n-gi :span="24">
|
||||
<n-space justify="center">
|
||||
<n-button type="warning" @click="managePrompts">
|
||||
添加提示词模板
|
||||
</n-button>
|
||||
<n-button type="primary" @click="saveConfig">
|
||||
保存
|
||||
</n-button>
|
||||
@ -301,7 +393,52 @@ window.onerror = function (event, source, lineno, colno, error) {
|
||||
</n-space>
|
||||
</n-gi>
|
||||
</n-form>
|
||||
<n-gi :span="24" v-if="promptTemplates.length>0" v-for="prompt in promptTemplates" >
|
||||
<n-flex justify="start">
|
||||
<n-tag closable @close="deletePrompt(prompt.ID)" @click="editPrompt(prompt)" :title="prompt.content" :type="prompt.type==='模型系统Prompt'?'success':'info'" :bordered="false"> {{prompt.name}} </n-tag>
|
||||
</n-flex>
|
||||
</n-gi>
|
||||
</n-flex>
|
||||
<n-modal v-model:show="showManagePromptsModal" closable :mask-closable="false">
|
||||
<n-card
|
||||
style="width: 800px;height: 600px;text-align: left"
|
||||
:bordered="false"
|
||||
:title="(formPrompt.ID>0?'修改':'添加')+'提示词'"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<n-form ref="formPromptRef" :label-placement="'left'" :label-align="'left'" >
|
||||
<n-form-item label="名称">
|
||||
<n-input v-model:value="formPrompt.Name" placeholder="请输入提示词名称" />
|
||||
</n-form-item>
|
||||
<n-form-item label="类型">
|
||||
<n-select v-model:value="formPrompt.Type" :options="promptTypeOptions" placeholder="请选择提示词类型" />
|
||||
</n-form-item>
|
||||
<n-form-item label="内容">
|
||||
<n-input v-model:value="formPrompt.Content"
|
||||
type="textarea"
|
||||
:show-count="true"
|
||||
placeholder="请输入prompt"
|
||||
:autosize="{
|
||||
minRows: 12,
|
||||
maxRows: 12,
|
||||
}"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-flex justify="end">
|
||||
<n-button type="primary" @click="savePrompt">
|
||||
保存
|
||||
</n-button>
|
||||
<n-button type="warning" @click="showManagePromptsModal=false">
|
||||
取消
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,6 @@ import router from './router/router'
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.use(naive)
|
||||
app.mount('#app')
|
@ -1,19 +1,22 @@
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import {createMemoryHistory, createRouter, createWebHashHistory, createWebHistory} from 'vue-router'
|
||||
|
||||
import stockView from '../components/stock.vue'
|
||||
import settingsView from '../components/settings.vue'
|
||||
import about from "../components/about.vue";
|
||||
import fundView from "../components/fund.vue";
|
||||
import market from "../components/market.vue";
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: stockView,name: 'stock' },
|
||||
{ path: '/', component: stockView,name: 'stock'},
|
||||
{ path: '/fund', component: fundView,name: 'fund' },
|
||||
{ path: '/settings/:id', component: settingsView,name: 'settings' },
|
||||
{ path: '/settings', component: settingsView,name: 'settings' },
|
||||
{ path: '/about', component: about,name: 'about' },
|
||||
{ path: '/market', component: market,name: 'market' },
|
||||
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
html {
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
/*background-color: rgba(27, 38, 54, 1);*/
|
||||
text-align: center;
|
||||
color: white;
|
||||
/*color: white;*/
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: white;
|
||||
/*color: white;*/
|
||||
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
|
78
frontend/wailsjs/go/main/App.d.ts
vendored
78
frontend/wailsjs/go/main/App.d.ts
vendored
@ -1,10 +1,26 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {models} from '../models';
|
||||
import {data} from '../models';
|
||||
import {models} from '../models';
|
||||
|
||||
export function AddCronTask(arg1:data.FollowedStock):Promise<any>;
|
||||
|
||||
export function AddGroup(arg1:data.Group):Promise<string>;
|
||||
|
||||
export function AddPrompt(arg1:models.Prompt):Promise<string>;
|
||||
|
||||
export function AddStockGroup(arg1:number,arg2:string):Promise<string>;
|
||||
|
||||
export function AnalyzeSentiment(arg1:string):Promise<data.SentimentResult>;
|
||||
|
||||
export function CheckUpdate():Promise<void>;
|
||||
|
||||
export function ClsCalendar():Promise<Array<any>>;
|
||||
|
||||
export function DelPrompt(arg1:number):Promise<string>;
|
||||
|
||||
export function EMDictCode(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function ExportConfig():Promise<string>;
|
||||
|
||||
export function Follow(arg1:string):Promise<string>;
|
||||
@ -15,22 +31,70 @@ export function GetAIResponseResult(arg1:string):Promise<models.AIResponseResult
|
||||
|
||||
export function GetConfig():Promise<data.Settings>;
|
||||
|
||||
export function GetFollowList():Promise<any>;
|
||||
export function GetFollowList(arg1:number):Promise<any>;
|
||||
|
||||
export function GetFollowedFund():Promise<Array<data.FollowedFund>>;
|
||||
|
||||
export function GetGroupList():Promise<Array<data.Group>>;
|
||||
|
||||
export function GetGroupStockList(arg1:number):Promise<Array<data.GroupStock>>;
|
||||
|
||||
export function GetIndustryMoneyRankSina(arg1:string,arg2:string):Promise<Array<Record<string, any>>>;
|
||||
|
||||
export function GetIndustryRank(arg1:string,arg2:number):Promise<Array<any>>;
|
||||
|
||||
export function GetMoneyRankSina(arg1:string):Promise<Array<Record<string, any>>>;
|
||||
|
||||
export function GetPromptTemplates(arg1:string,arg2:string):Promise<any>;
|
||||
|
||||
export function GetStockCommonKLine(arg1:string,arg2:string,arg3:number):Promise<any>;
|
||||
|
||||
export function GetStockKLine(arg1:string,arg2:string,arg3:number):Promise<any>;
|
||||
|
||||
export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
|
||||
|
||||
export function GetStockMinutePriceLineData(arg1:string,arg2:string):Promise<Record<string, any>>;
|
||||
|
||||
export function GetStockMoneyTrendByDay(arg1:string,arg2:number):Promise<Array<Record<string, any>>>;
|
||||
|
||||
export function GetTelegraphList(arg1:string):Promise<any>;
|
||||
|
||||
export function GetVersionInfo():Promise<models.VersionInfo>;
|
||||
|
||||
export function GetfundList(arg1:string):Promise<Array<data.FundBasic>>;
|
||||
|
||||
export function GlobalStockIndexes():Promise<Record<string, any>>;
|
||||
|
||||
export function Greet(arg1:string):Promise<data.StockInfo>;
|
||||
|
||||
export function NewChatStream(arg1:string,arg2:string,arg3:string):Promise<void>;
|
||||
export function HotEvent(arg1:number):Promise<any>;
|
||||
|
||||
export function HotStock(arg1:string):Promise<any>;
|
||||
|
||||
export function HotTopic(arg1:number):Promise<Array<any>>;
|
||||
|
||||
export function IndustryResearchReport(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function InvestCalendarTimeLine(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function LongTigerRank(arg1:string):Promise<any>;
|
||||
|
||||
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any):Promise<void>;
|
||||
|
||||
export function NewsPush(arg1:any):Promise<void>;
|
||||
|
||||
export function ReFleshTelegraphList(arg1:string):Promise<any>;
|
||||
|
||||
export function RemoveGroup(arg1:number):Promise<string>;
|
||||
|
||||
export function RemoveStockGroup(arg1:string,arg2:string,arg3:number):Promise<string>;
|
||||
|
||||
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;
|
||||
|
||||
export function SaveAsMarkdown(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function SearchStock(arg1:string):Promise<Record<string, any>>;
|
||||
|
||||
export function SendDingDingMessage(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function SendDingDingMessageByType(arg1:string,arg2:string,arg3:number):Promise<string>;
|
||||
@ -39,10 +103,18 @@ export function SetAlarmChangePercent(arg1:number,arg2:number,arg3:string):Promi
|
||||
|
||||
export function SetCostPriceAndVolume(arg1:string,arg2:number,arg3:number):Promise<string>;
|
||||
|
||||
export function SetStockAICron(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
export function SetStockSort(arg1:number,arg2:string):Promise<void>;
|
||||
|
||||
export function ShareAnalysis(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function StockNotice(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function StockResearchReport(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function SummaryStockNews(arg1:string,arg2:any):Promise<void>;
|
||||
|
||||
export function UnFollow(arg1:string):Promise<string>;
|
||||
|
||||
export function UnFollowFund(arg1:string):Promise<string>;
|
||||
|
@ -2,10 +2,42 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function AddCronTask(arg1) {
|
||||
return window['go']['main']['App']['AddCronTask'](arg1);
|
||||
}
|
||||
|
||||
export function AddGroup(arg1) {
|
||||
return window['go']['main']['App']['AddGroup'](arg1);
|
||||
}
|
||||
|
||||
export function AddPrompt(arg1) {
|
||||
return window['go']['main']['App']['AddPrompt'](arg1);
|
||||
}
|
||||
|
||||
export function AddStockGroup(arg1, arg2) {
|
||||
return window['go']['main']['App']['AddStockGroup'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function AnalyzeSentiment(arg1) {
|
||||
return window['go']['main']['App']['AnalyzeSentiment'](arg1);
|
||||
}
|
||||
|
||||
export function CheckUpdate() {
|
||||
return window['go']['main']['App']['CheckUpdate']();
|
||||
}
|
||||
|
||||
export function ClsCalendar() {
|
||||
return window['go']['main']['App']['ClsCalendar']();
|
||||
}
|
||||
|
||||
export function DelPrompt(arg1) {
|
||||
return window['go']['main']['App']['DelPrompt'](arg1);
|
||||
}
|
||||
|
||||
export function EMDictCode(arg1) {
|
||||
return window['go']['main']['App']['EMDictCode'](arg1);
|
||||
}
|
||||
|
||||
export function ExportConfig() {
|
||||
return window['go']['main']['App']['ExportConfig']();
|
||||
}
|
||||
@ -26,18 +58,62 @@ export function GetConfig() {
|
||||
return window['go']['main']['App']['GetConfig']();
|
||||
}
|
||||
|
||||
export function GetFollowList() {
|
||||
return window['go']['main']['App']['GetFollowList']();
|
||||
export function GetFollowList(arg1) {
|
||||
return window['go']['main']['App']['GetFollowList'](arg1);
|
||||
}
|
||||
|
||||
export function GetFollowedFund() {
|
||||
return window['go']['main']['App']['GetFollowedFund']();
|
||||
}
|
||||
|
||||
export function GetGroupList() {
|
||||
return window['go']['main']['App']['GetGroupList']();
|
||||
}
|
||||
|
||||
export function GetGroupStockList(arg1) {
|
||||
return window['go']['main']['App']['GetGroupStockList'](arg1);
|
||||
}
|
||||
|
||||
export function GetIndustryMoneyRankSina(arg1, arg2) {
|
||||
return window['go']['main']['App']['GetIndustryMoneyRankSina'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetIndustryRank(arg1, arg2) {
|
||||
return window['go']['main']['App']['GetIndustryRank'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetMoneyRankSina(arg1) {
|
||||
return window['go']['main']['App']['GetMoneyRankSina'](arg1);
|
||||
}
|
||||
|
||||
export function GetPromptTemplates(arg1, arg2) {
|
||||
return window['go']['main']['App']['GetPromptTemplates'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetStockCommonKLine(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['GetStockCommonKLine'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function GetStockKLine(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['GetStockKLine'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function GetStockList(arg1) {
|
||||
return window['go']['main']['App']['GetStockList'](arg1);
|
||||
}
|
||||
|
||||
export function GetStockMinutePriceLineData(arg1, arg2) {
|
||||
return window['go']['main']['App']['GetStockMinutePriceLineData'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetStockMoneyTrendByDay(arg1, arg2) {
|
||||
return window['go']['main']['App']['GetStockMoneyTrendByDay'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetTelegraphList(arg1) {
|
||||
return window['go']['main']['App']['GetTelegraphList'](arg1);
|
||||
}
|
||||
|
||||
export function GetVersionInfo() {
|
||||
return window['go']['main']['App']['GetVersionInfo']();
|
||||
}
|
||||
@ -46,18 +122,70 @@ export function GetfundList(arg1) {
|
||||
return window['go']['main']['App']['GetfundList'](arg1);
|
||||
}
|
||||
|
||||
export function GlobalStockIndexes() {
|
||||
return window['go']['main']['App']['GlobalStockIndexes']();
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function NewChatStream(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3);
|
||||
export function HotEvent(arg1) {
|
||||
return window['go']['main']['App']['HotEvent'](arg1);
|
||||
}
|
||||
|
||||
export function HotStock(arg1) {
|
||||
return window['go']['main']['App']['HotStock'](arg1);
|
||||
}
|
||||
|
||||
export function HotTopic(arg1) {
|
||||
return window['go']['main']['App']['HotTopic'](arg1);
|
||||
}
|
||||
|
||||
export function IndustryResearchReport(arg1) {
|
||||
return window['go']['main']['App']['IndustryResearchReport'](arg1);
|
||||
}
|
||||
|
||||
export function InvestCalendarTimeLine(arg1) {
|
||||
return window['go']['main']['App']['InvestCalendarTimeLine'](arg1);
|
||||
}
|
||||
|
||||
export function LongTigerRank(arg1) {
|
||||
return window['go']['main']['App']['LongTigerRank'](arg1);
|
||||
}
|
||||
|
||||
export function NewChatStream(arg1, arg2, arg3, arg4) {
|
||||
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4);
|
||||
}
|
||||
|
||||
export function NewsPush(arg1) {
|
||||
return window['go']['main']['App']['NewsPush'](arg1);
|
||||
}
|
||||
|
||||
export function ReFleshTelegraphList(arg1) {
|
||||
return window['go']['main']['App']['ReFleshTelegraphList'](arg1);
|
||||
}
|
||||
|
||||
export function RemoveGroup(arg1) {
|
||||
return window['go']['main']['App']['RemoveGroup'](arg1);
|
||||
}
|
||||
|
||||
export function RemoveStockGroup(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['RemoveStockGroup'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5) {
|
||||
return window['go']['main']['App']['SaveAIResponseResult'](arg1, arg2, arg3, arg4, arg5);
|
||||
}
|
||||
|
||||
export function SaveAsMarkdown(arg1, arg2) {
|
||||
return window['go']['main']['App']['SaveAsMarkdown'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SearchStock(arg1) {
|
||||
return window['go']['main']['App']['SearchStock'](arg1);
|
||||
}
|
||||
|
||||
export function SendDingDingMessage(arg1, arg2) {
|
||||
return window['go']['main']['App']['SendDingDingMessage'](arg1, arg2);
|
||||
}
|
||||
@ -74,6 +202,10 @@ export function SetCostPriceAndVolume(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SetCostPriceAndVolume'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function SetStockAICron(arg1, arg2) {
|
||||
return window['go']['main']['App']['SetStockAICron'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SetStockSort(arg1, arg2) {
|
||||
return window['go']['main']['App']['SetStockSort'](arg1, arg2);
|
||||
}
|
||||
@ -82,6 +214,18 @@ export function ShareAnalysis(arg1, arg2) {
|
||||
return window['go']['main']['App']['ShareAnalysis'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function StockNotice(arg1) {
|
||||
return window['go']['main']['App']['StockNotice'](arg1);
|
||||
}
|
||||
|
||||
export function StockResearchReport(arg1) {
|
||||
return window['go']['main']['App']['StockResearchReport'](arg1);
|
||||
}
|
||||
|
||||
export function SummaryStockNews(arg1, arg2) {
|
||||
return window['go']['main']['App']['SummaryStockNews'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function UnFollow(arg1) {
|
||||
return window['go']['main']['App']['UnFollow'](arg1);
|
||||
}
|
||||
|
@ -142,7 +142,174 @@ export namespace data {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class Group {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
name: string;
|
||||
sort: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Group(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.ID = source["ID"];
|
||||
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
|
||||
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
|
||||
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
|
||||
this.name = source["name"];
|
||||
this.sort = source["sort"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class GroupStock {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
CreatedAt: any;
|
||||
// Go type: time
|
||||
UpdatedAt: any;
|
||||
// Go type: gorm
|
||||
DeletedAt: any;
|
||||
stockCode: string;
|
||||
groupId: number;
|
||||
groupInfo: Group;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new GroupStock(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.ID = source["ID"];
|
||||
this.CreatedAt = this.convertValues(source["CreatedAt"], null);
|
||||
this.UpdatedAt = this.convertValues(source["UpdatedAt"], null);
|
||||
this.DeletedAt = this.convertValues(source["DeletedAt"], null);
|
||||
this.stockCode = source["stockCode"];
|
||||
this.groupId = source["groupId"];
|
||||
this.groupInfo = this.convertValues(source["groupInfo"], Group);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class FollowedStock {
|
||||
StockCode: string;
|
||||
Name: string;
|
||||
Volume: number;
|
||||
CostPrice: number;
|
||||
Price: number;
|
||||
PriceChange: number;
|
||||
ChangePercent: number;
|
||||
AlarmChangePercent: number;
|
||||
AlarmPrice: number;
|
||||
// Go type: time
|
||||
Time: any;
|
||||
Sort: number;
|
||||
Cron?: string;
|
||||
IsDel: number;
|
||||
Groups: GroupStock[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FollowedStock(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.StockCode = source["StockCode"];
|
||||
this.Name = source["Name"];
|
||||
this.Volume = source["Volume"];
|
||||
this.CostPrice = source["CostPrice"];
|
||||
this.Price = source["Price"];
|
||||
this.PriceChange = source["PriceChange"];
|
||||
this.ChangePercent = source["ChangePercent"];
|
||||
this.AlarmChangePercent = source["AlarmChangePercent"];
|
||||
this.AlarmPrice = source["AlarmPrice"];
|
||||
this.Time = this.convertValues(source["Time"], null);
|
||||
this.Sort = source["Sort"];
|
||||
this.Cron = source["Cron"];
|
||||
this.IsDel = source["IsDel"];
|
||||
this.Groups = this.convertValues(source["Groups"], GroupStock);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class SentimentResult {
|
||||
Score: number;
|
||||
Category: number;
|
||||
PositiveCount: number;
|
||||
NegativeCount: number;
|
||||
Description: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new SentimentResult(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.Score = source["Score"];
|
||||
this.Category = source["Category"];
|
||||
this.PositiveCount = source["PositiveCount"];
|
||||
this.NegativeCount = source["NegativeCount"];
|
||||
this.Description = source["Description"];
|
||||
}
|
||||
}
|
||||
export class Settings {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
@ -171,6 +338,11 @@ export namespace data {
|
||||
kDays: number;
|
||||
enableDanmu: boolean;
|
||||
browserPath: string;
|
||||
enableNews: boolean;
|
||||
darkTheme: boolean;
|
||||
browserPoolSize: number;
|
||||
enableFund: boolean;
|
||||
enablePushNews: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Settings(source);
|
||||
@ -202,6 +374,11 @@ export namespace data {
|
||||
this.kDays = source["kDays"];
|
||||
this.enableDanmu = source["enableDanmu"];
|
||||
this.browserPath = source["browserPath"];
|
||||
this.enableNews = source["enableNews"];
|
||||
this.darkTheme = source["darkTheme"];
|
||||
this.browserPoolSize = source["browserPoolSize"];
|
||||
this.enableFund = source["enableFund"];
|
||||
this.enablePushNews = source["enablePushNews"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
@ -352,6 +529,7 @@ export namespace data {
|
||||
sort: number;
|
||||
alarmChangePercent: number;
|
||||
alarmPrice: number;
|
||||
Groups: GroupStock[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new StockInfo(source);
|
||||
@ -412,6 +590,7 @@ export namespace data {
|
||||
this.sort = source["sort"];
|
||||
this.alarmChangePercent = source["alarmChangePercent"];
|
||||
this.alarmPrice = source["alarmPrice"];
|
||||
this.Groups = this.convertValues(source["Groups"], GroupStock);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
@ -490,6 +669,24 @@ export namespace models {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class Prompt {
|
||||
ID: number;
|
||||
name: string;
|
||||
content: string;
|
||||
type: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Prompt(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.ID = source["ID"];
|
||||
this.name = source["name"];
|
||||
this.content = source["content"];
|
||||
this.type = source["type"];
|
||||
}
|
||||
}
|
||||
export class VersionInfo {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
|
33
go.mod
33
go.mod
@ -1,23 +1,26 @@
|
||||
module go-stock
|
||||
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.0
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.1
|
||||
github.com/chromedp/chromedp v0.11.2
|
||||
github.com/coocood/freecache v1.2.4
|
||||
github.com/duke-git/lancet/v2 v2.3.4
|
||||
github.com/getlantern/systray v1.2.2
|
||||
github.com/energye/systray v1.0.2
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-ego/gse v0.80.3
|
||||
github.com/go-resty/resty/v2 v2.16.2
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/robertkrimen/otto v0.5.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samber/lo v1.49.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tidwall/gjson v1.14.2
|
||||
github.com/wailsapp/wails/v2 v2.10.1
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/text v0.22.0
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/text v0.23.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/gorm v1.25.12
|
||||
gorm.io/plugin/dbresolver v1.5.3
|
||||
@ -32,15 +35,8 @@ require (
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
@ -60,22 +56,25 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/vcaesar/cedar v0.20.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.19 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
|
66
go.sum
66
go.sum
@ -14,39 +14,26 @@ github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipw
|
||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
|
||||
github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvYJls=
|
||||
github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||
github.com/energye/systray v1.0.2 h1:63R4prQkANtpM2CIA4UrDCuwZFt+FiygG77JYCsNmXc=
|
||||
github.com/energye/systray v1.0.2/go.mod h1:sp7Q/q/I4/w5ebvpSuJVep71s9Bg7L9ZVp69gBASehM=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-ego/gse v0.80.3 h1:YNFkjMhlhQnUeuoFcUEd1ivh6SOB764rT8GDsEbDiEg=
|
||||
github.com/go-ego/gse v0.80.3/go.mod h1:Gt3A9Ry1Eso2Kza4MRaiZ7f2DTAvActmETY46Lxg0gU=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
|
||||
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
@ -55,6 +42,7 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@ -90,8 +78,6 @@ github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
@ -111,8 +97,6 @@ github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@ -125,19 +109,32 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
|
||||
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c h1:coVla7zpsycc+kA9NXpcvv2E4I7+ii6L5hZO2S6C3kw=
|
||||
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
|
||||
github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/vcaesar/cedar v0.20.2 h1:TDx7AdZhilKcfE1WvdToTJf5VrC/FXcUOW+KY1upLZ4=
|
||||
github.com/vcaesar/cedar v0.20.2/go.mod h1:lyuGvALuZZDPNXwpzv/9LyxW+8Y6faN7zauFezNsnik=
|
||||
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
|
||||
github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU=
|
||||
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
|
||||
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
@ -157,8 +154,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@ -176,8 +173,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -186,8 +183,8 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -202,8 +199,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@ -223,8 +220,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -234,12 +231,13 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
|
210
main.go
210
main.go
@ -1,10 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/logger"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
@ -17,10 +18,10 @@ import (
|
||||
"go-stock/backend/db"
|
||||
log "go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
|
||||
"os"
|
||||
goruntime "runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -56,31 +57,12 @@ var VersionCommit string
|
||||
func main() {
|
||||
checkDir("data")
|
||||
db.Init("")
|
||||
db.Dao.AutoMigrate(&data.StockInfo{})
|
||||
db.Dao.AutoMigrate(&data.StockBasic{})
|
||||
db.Dao.AutoMigrate(&data.FollowedStock{})
|
||||
db.Dao.AutoMigrate(&data.IndexBasic{})
|
||||
db.Dao.AutoMigrate(&data.Settings{})
|
||||
db.Dao.AutoMigrate(&models.AIResponseResult{})
|
||||
db.Dao.AutoMigrate(&models.StockInfoHK{})
|
||||
db.Dao.AutoMigrate(&models.StockInfoUS{})
|
||||
db.Dao.AutoMigrate(&data.FollowedFund{})
|
||||
db.Dao.AutoMigrate(&data.FundBasic{})
|
||||
go AutoMigrate()
|
||||
|
||||
if stocksBin != nil && len(stocksBin) > 0 {
|
||||
go initStockData()
|
||||
}
|
||||
log.SugaredLogger.Infof("init stocksBinHK %d", len(stocksBinHK))
|
||||
|
||||
if stocksBinHK != nil && len(stocksBinHK) > 0 {
|
||||
go initStockDataHK()
|
||||
}
|
||||
log.SugaredLogger.Infof("init stocksBinUS %d", len(stocksBinUS))
|
||||
|
||||
if stocksBinUS != nil && len(stocksBinUS) > 0 {
|
||||
go initStockDataUS()
|
||||
}
|
||||
updateBasicInfo()
|
||||
//db.Dao.Model(&data.Group{}).Where("id = ?", 0).FirstOrCreate(&data.Group{
|
||||
// Name: "默认分组",
|
||||
// Sort: 0,
|
||||
//})
|
||||
|
||||
// Create an instance of the app structure
|
||||
app := NewApp()
|
||||
@ -120,32 +102,38 @@ func main() {
|
||||
log.SugaredLogger.Info("version: " + Version)
|
||||
log.SugaredLogger.Info("commit: " + VersionCommit)
|
||||
// Create application with options
|
||||
var width, height int
|
||||
var err error
|
||||
|
||||
width, height, err = getScreenResolution()
|
||||
//var width, height int
|
||||
//var err error
|
||||
//
|
||||
width, _, err := getScreenResolution()
|
||||
if err != nil {
|
||||
log.SugaredLogger.Error("get screen resolution error")
|
||||
width = 1366
|
||||
height = 768
|
||||
width = 1456
|
||||
//height = 768
|
||||
}
|
||||
|
||||
darkTheme := data.NewSettingsApi(&data.Settings{}).GetConfig().DarkTheme
|
||||
backgroundColour := &options.RGBA{R: 255, G: 255, B: 255, A: 1}
|
||||
if darkTheme {
|
||||
backgroundColour = &options.RGBA{R: 27, G: 38, B: 54, A: 1}
|
||||
}
|
||||
|
||||
// Create application with options
|
||||
err = wails.Run(&options.App{
|
||||
Title: "go-stock",
|
||||
Width: width * 4 / 5,
|
||||
Height: height * 4 / 5,
|
||||
MinWidth: 1024,
|
||||
MinHeight: 768,
|
||||
MaxWidth: width,
|
||||
MaxHeight: height,
|
||||
Title: "go-stock",
|
||||
Width: width * 4 / 5,
|
||||
Height: 900,
|
||||
MinWidth: 1456,
|
||||
MinHeight: 768,
|
||||
//MaxWidth: width,
|
||||
//MaxHeight: height,
|
||||
DisableResize: false,
|
||||
Fullscreen: false,
|
||||
Frameless: true,
|
||||
StartHidden: false,
|
||||
HideWindowOnClose: false,
|
||||
EnableDefaultContextMenu: true,
|
||||
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||||
BackgroundColour: backgroundColour,
|
||||
Assets: assets,
|
||||
Menu: AppMenu,
|
||||
Logger: nil,
|
||||
@ -156,6 +144,10 @@ func main() {
|
||||
OnBeforeClose: app.beforeClose,
|
||||
OnShutdown: app.shutdown,
|
||||
WindowStartState: options.Normal,
|
||||
SingleInstanceLock: &options.SingleInstanceLock{
|
||||
UniqueId: "go-stock",
|
||||
OnSecondInstanceLaunch: OnSecondInstanceLaunch,
|
||||
},
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
@ -194,7 +186,30 @@ func main() {
|
||||
|
||||
}
|
||||
|
||||
func initStockDataUS() {
|
||||
func AutoMigrate() {
|
||||
db.Dao.AutoMigrate(&data.StockInfo{})
|
||||
db.Dao.AutoMigrate(&data.StockBasic{})
|
||||
db.Dao.AutoMigrate(&data.FollowedStock{})
|
||||
db.Dao.AutoMigrate(&data.IndexBasic{})
|
||||
db.Dao.AutoMigrate(&data.Settings{})
|
||||
db.Dao.AutoMigrate(&models.AIResponseResult{})
|
||||
db.Dao.AutoMigrate(&models.StockInfoHK{})
|
||||
db.Dao.AutoMigrate(&models.StockInfoUS{})
|
||||
db.Dao.AutoMigrate(&data.FollowedFund{})
|
||||
db.Dao.AutoMigrate(&data.FundBasic{})
|
||||
db.Dao.AutoMigrate(&models.PromptTemplate{})
|
||||
db.Dao.AutoMigrate(&data.Group{})
|
||||
db.Dao.AutoMigrate(&data.GroupStock{})
|
||||
db.Dao.AutoMigrate(&models.Tags{})
|
||||
db.Dao.AutoMigrate(&models.Telegraph{})
|
||||
db.Dao.AutoMigrate(&models.TelegraphTags{})
|
||||
db.Dao.AutoMigrate(&models.LongTigerRankData{})
|
||||
}
|
||||
|
||||
func initStockDataUS(ctx context.Context) {
|
||||
defer func() {
|
||||
go runtime.EventsEmit(ctx, "loadingMsg", "done")
|
||||
}()
|
||||
var v []models.StockInfoUS
|
||||
err := json.Unmarshal(stocksBinUS, &v)
|
||||
if err != nil {
|
||||
@ -202,18 +217,25 @@ func initStockDataUS() {
|
||||
return
|
||||
}
|
||||
log.SugaredLogger.Infof("init stock data us %d", len(v))
|
||||
for _, item := range v {
|
||||
var count int64
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", item.Code).Count(&count)
|
||||
if count > 0 {
|
||||
log.SugaredLogger.Infof("stock data us %s exist", item.Code)
|
||||
continue
|
||||
var total int64
|
||||
db.Dao.Model(&models.StockInfoUS{}).Count(&total)
|
||||
if total != int64(len(v)) {
|
||||
for _, item := range v {
|
||||
var count int64
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", item.Code).Count(&count)
|
||||
if count > 0 {
|
||||
//log.SugaredLogger.Infof("stock data us %s exist", item.Code)
|
||||
continue
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoUS{}).Create(&item)
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoUS{}).Create(&item)
|
||||
}
|
||||
}
|
||||
|
||||
func initStockDataHK() {
|
||||
func initStockDataHK(ctx context.Context) {
|
||||
defer func() {
|
||||
go runtime.EventsEmit(ctx, "loadingMsg", "done")
|
||||
}()
|
||||
var v []models.StockInfoHK
|
||||
err := json.Unmarshal(stocksBinHK, &v)
|
||||
if err != nil {
|
||||
@ -221,15 +243,20 @@ func initStockDataHK() {
|
||||
return
|
||||
}
|
||||
log.SugaredLogger.Infof("init stock data hk %d", len(v))
|
||||
for _, item := range v {
|
||||
var count int64
|
||||
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", item.Code).Count(&count)
|
||||
if count > 0 {
|
||||
log.SugaredLogger.Infof("stock data hk %s exist", item.Code)
|
||||
continue
|
||||
var total int64
|
||||
db.Dao.Model(&models.StockInfoHK{}).Count(&total)
|
||||
if total != int64(len(v)) {
|
||||
for _, item := range v {
|
||||
var count int64
|
||||
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", item.Code).Count(&count)
|
||||
if count > 0 {
|
||||
//log.SugaredLogger.Infof("stock data hk %s exist", item.Code)
|
||||
continue
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Create(&item)
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Create(&item)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func updateBasicInfo() {
|
||||
@ -241,7 +268,11 @@ func updateBasicInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
func initStockData() {
|
||||
func initStockData(ctx context.Context) {
|
||||
defer func() {
|
||||
go runtime.EventsEmit(ctx, "loadingMsg", "done")
|
||||
}()
|
||||
fields := "ts_code,symbol,name,area,industry,cnspell,market,list_date,act_name,act_ent_type,fullname,exchange,list_status,curr_type,enname,delist_date,is_hs"
|
||||
log.SugaredLogger.Info("init stock data")
|
||||
res := &data.TushareStockBasicResponse{}
|
||||
err := json.Unmarshal(stocksBin, res)
|
||||
@ -249,26 +280,24 @@ func initStockData() {
|
||||
log.SugaredLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range res.Data.Items {
|
||||
stock := &data.StockBasic{}
|
||||
stock.Exchange = convertor.ToString(item[0])
|
||||
stock.IsHs = convertor.ToString(item[1])
|
||||
stock.Name = convertor.ToString(item[2])
|
||||
stock.Industry = convertor.ToString(item[3])
|
||||
stock.ListStatus = convertor.ToString(item[4])
|
||||
stock.ActName = convertor.ToString(item[5])
|
||||
stock.ID = uint(item[6].(float64))
|
||||
stock.CurrType = convertor.ToString(item[7])
|
||||
stock.Area = convertor.ToString(item[8])
|
||||
stock.ListDate = convertor.ToString(item[9])
|
||||
stock.DelistDate = convertor.ToString(item[10])
|
||||
stock.ActEntType = convertor.ToString(item[11])
|
||||
stock.TsCode = convertor.ToString(item[12])
|
||||
stock.Symbol = convertor.ToString(item[13])
|
||||
stock.Cnspell = convertor.ToString(item[14])
|
||||
stock.Fullname = convertor.ToString(item[20])
|
||||
stock.Ename = convertor.ToString(item[21])
|
||||
|
||||
stockData := map[string]any{}
|
||||
for _, field := range strings.Split(fields, ",") {
|
||||
//logger.SugaredLogger.Infof("field: %s", field)
|
||||
idx := slice.IndexOf(res.Data.Fields, field)
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
stockData[field] = item[idx]
|
||||
}
|
||||
jsonData, _ := json.Marshal(stockData)
|
||||
err := json.Unmarshal(jsonData, stock)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
stock.ID = 0
|
||||
var count int64
|
||||
db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).Count(&count)
|
||||
if count > 0 {
|
||||
@ -276,7 +305,38 @@ func initStockData() {
|
||||
} else {
|
||||
db.Dao.Create(stock)
|
||||
}
|
||||
|
||||
//db.Dao.Model(&data.StockBasic{}).FirstOrCreate(stock, &data.StockBasic{TsCode: stock.TsCode}).Where("ts_code = ?", stock.TsCode).Updates(stock)
|
||||
}
|
||||
|
||||
//for _, item := range res.Data.Items {
|
||||
// stock := &data.StockBasic{}
|
||||
// stock.Exchange = convertor.ToString(item[0])
|
||||
// stock.IsHs = convertor.ToString(item[1])
|
||||
// stock.Name = convertor.ToString(item[2])
|
||||
// stock.Industry = convertor.ToString(item[3])
|
||||
// stock.ListStatus = convertor.ToString(item[4])
|
||||
// stock.ActName = convertor.ToString(item[5])
|
||||
// stock.ID = uint(item[6].(float64))
|
||||
// stock.CurrType = convertor.ToString(item[7])
|
||||
// stock.Area = convertor.ToString(item[8])
|
||||
// stock.ListDate = convertor.ToString(item[9])
|
||||
// stock.DelistDate = convertor.ToString(item[10])
|
||||
// stock.ActEntType = convertor.ToString(item[11])
|
||||
// stock.TsCode = convertor.ToString(item[12])
|
||||
// stock.Symbol = convertor.ToString(item[13])
|
||||
// stock.Cnspell = convertor.ToString(item[14])
|
||||
// stock.Fullname = convertor.ToString(item[20])
|
||||
// stock.Ename = convertor.ToString(item[21])
|
||||
//
|
||||
// var count int64
|
||||
// db.Dao.Model(&data.StockBasic{}).Where("ts_code = ?", stock.TsCode).Count(&count)
|
||||
// if count > 0 {
|
||||
// continue
|
||||
// } else {
|
||||
// db.Dao.Create(stock)
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
func checkDir(dir string) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user