mirror of
https://github.com/ArvinLovegood/go-stock.git
synced 2025-07-19 00:00:09 +08:00
Compare commits
144 Commits
v2025.3.31
...
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 |
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'
|
||||
|
65
README.md
65
README.md
@ -1,16 +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))
|
||||
|
||||
### 🌟公众号
|
||||

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

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

|
||||

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

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

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

|
||||

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

|
||||
### 钉钉报警通知
|
||||
|
333
app.go
333
app.go
@ -5,7 +5,16 @@ 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"
|
||||
@ -18,14 +27,7 @@ import (
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go-stock/backend/data"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// App struct
|
||||
@ -65,12 +67,42 @@ func (a *App) startup(ctx context.Context) {
|
||||
//}, 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{})
|
||||
|
||||
systray.Run(func() {
|
||||
// 将 map 转换为 JSON 字节切片
|
||||
jsonData, err := json.Marshal(setMap)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("Marshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
// 将 JSON 字节切片解析到结构体中
|
||||
err = json.Unmarshal(jsonData, config)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("Unmarshal error:%s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logger.SugaredLogger.Infof("updateSettings config:%+v", config)
|
||||
if config.DarkTheme {
|
||||
runtime.WindowSetBackgroundColour(ctx, 27, 38, 54, 1)
|
||||
runtime.WindowSetDarkTheme(ctx)
|
||||
} else {
|
||||
runtime.WindowSetBackgroundColour(ctx, 255, 255, 255, 1)
|
||||
runtime.WindowSetLightTheme(ctx)
|
||||
}
|
||||
runtime.WindowReloadApp(ctx)
|
||||
|
||||
})
|
||||
go systray.Run(func() {
|
||||
onReady(a)
|
||||
}, func() {
|
||||
onExit(a)
|
||||
})
|
||||
|
||||
logger.SugaredLogger.Infof(" application startup Version:%s", Version)
|
||||
}
|
||||
|
||||
func (a *App) CheckUpdate() {
|
||||
@ -107,6 +139,22 @@ func (a *App) CheckUpdate() {
|
||||
func (a *App) domReady(ctx context.Context) {
|
||||
defer PanicHandler()
|
||||
|
||||
if stocksBin != nil && len(stocksBin) > 0 {
|
||||
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查A股基础信息...")
|
||||
go initStockData(a.ctx)
|
||||
}
|
||||
|
||||
if stocksBinHK != nil && len(stocksBinHK) > 0 {
|
||||
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查港股基础信息...")
|
||||
go initStockDataHK(a.ctx)
|
||||
}
|
||||
|
||||
if stocksBinUS != nil && len(stocksBinUS) > 0 {
|
||||
go runtime.EventsEmit(a.ctx, "loadingMsg", "检查美股基础信息...")
|
||||
go initStockDataUS(a.ctx)
|
||||
}
|
||||
updateBasicInfo()
|
||||
|
||||
// Add your action here
|
||||
//定时更新数据
|
||||
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
|
||||
@ -115,39 +163,98 @@ func (a *App) domReady(ctx context.Context) {
|
||||
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)
|
||||
//ticker := time.NewTicker(time.Second * time.Duration(60))
|
||||
//defer ticker.Stop()
|
||||
//for range ticker.C {
|
||||
// MonitorFundPrices(a)
|
||||
//}
|
||||
if config.EnableFund {
|
||||
id, err := a.cron.AddFunc(fmt.Sprintf("@every %ds", 60), func() {
|
||||
MonitorFundPrices(a)
|
||||
})
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("AddFunc error:%s", err.Error())
|
||||
} else {
|
||||
a.cronEntrys["MonitorFundPrices"] = id
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
if config.EnableNews {
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * time.Duration(60))
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
telegraph := refreshTelegraphList()
|
||||
if telegraph != nil {
|
||||
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
|
||||
}
|
||||
}
|
||||
//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()
|
||||
@ -175,21 +282,31 @@ func (a *App) domReady(ctx context.Context) {
|
||||
// logger.SugaredLogger.Infof("Edge浏览器已安装,路径为: %s", path)
|
||||
// }
|
||||
//}()
|
||||
followList := data.NewStockDataApi().GetFollowList()
|
||||
followList := data.NewStockDataApi().GetFollowList(0)
|
||||
for _, follow := range *followList {
|
||||
if follow.Cron == "" {
|
||||
if follow.Cron == nil || *follow.Cron == "" {
|
||||
continue
|
||||
}
|
||||
entryID, err := a.cron.AddFunc(follow.Cron, a.AddCronTask(follow))
|
||||
logger.SugaredLogger.Errorf("添加自动分析任务:%s cron=%s entryID:%v", follow.Name, follow.Cron, entryID)
|
||||
a.cronEntrys[follow.StockCode] = entryID
|
||||
entryID, err := a.cron.AddFunc(*follow.Cron, a.AddCronTask(follow))
|
||||
if err != nil {
|
||||
return
|
||||
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)
|
||||
@ -379,10 +496,12 @@ 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)
|
||||
//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)
|
||||
@ -442,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)
|
||||
@ -495,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
|
||||
}
|
||||
|
||||
}
|
||||
@ -530,17 +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()
|
||||
a.cron.Stop()
|
||||
os.Exit(0)
|
||||
//os.Exit(0)
|
||||
logger.SugaredLogger.Infof("application shutdown Version:%s", Version)
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
@ -550,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
|
||||
}
|
||||
@ -563,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 {
|
||||
@ -734,15 +857,15 @@ func getMsgTypeName(msgType int) string {
|
||||
|
||||
func onExit(a *App) {
|
||||
// 清理操作
|
||||
logger.SugaredLogger.Infof("onExit")
|
||||
systray.Quit()
|
||||
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 股票行情实时获取")
|
||||
@ -779,6 +902,17 @@ func onReady(a *App) {
|
||||
|
||||
func (a *App) UpdateConfig(settings *data.Settings) string {
|
||||
//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()
|
||||
}
|
||||
|
||||
@ -896,7 +1030,9 @@ func (a *App) SetStockAICron(cronText, stockCode string) {
|
||||
a.cron.Remove(entryID)
|
||||
}
|
||||
follow := data.NewStockDataApi().GetFollowedStockByStockCode(stockCode)
|
||||
a.cron.AddFunc(cronText, a.AddCronTask(follow))
|
||||
id, _ := a.cron.AddFunc(cronText, a.AddCronTask(follow))
|
||||
a.cronEntrys[stockCode] = id
|
||||
|
||||
}
|
||||
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
|
||||
notification := toast.Notification{
|
||||
@ -913,3 +1049,106 @@ func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -344,7 +344,7 @@ func (f *FundApi) CrawlFundNetEstimatedUnit(code string) {
|
||||
//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{
|
||||
|
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"
|
||||
@ -96,6 +98,97 @@ type AiResponse struct {
|
||||
SystemFingerprint string `json:"system_fingerprint"`
|
||||
}
|
||||
|
||||
func (o OpenAi) NewSummaryStockNewsStream(userQuestion string, sysPromptId *int) <-chan map[string]any {
|
||||
ch := make(chan map[string]any, 512)
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.SugaredLogger.Error("NewSummaryStockNewsStream panic", err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic :%s", err)
|
||||
logger.SugaredLogger.Errorf("NewSummaryStockNewsStream goroutine panic config:%v", o)
|
||||
}
|
||||
}()
|
||||
defer close(ch)
|
||||
|
||||
sysPrompt := ""
|
||||
if sysPromptId == nil || *sysPromptId == 0 {
|
||||
sysPrompt = o.Prompt
|
||||
} else {
|
||||
sysPrompt = NewPromptTemplateApi().GetPromptTemplateByID(*sysPromptId)
|
||||
}
|
||||
if sysPrompt == "" {
|
||||
sysPrompt = o.Prompt
|
||||
}
|
||||
|
||||
msg := []map[string]interface{}{
|
||||
{
|
||||
"role": "system",
|
||||
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
|
||||
//"content": "【角色设定】\n你是一位拥有20年实战经验的顶级股票分析师,精通技术分析、基本面分析、市场心理学和量化交易。擅长发现成长股、捕捉行业轮动机会,在牛熊市中都能保持稳定收益。你的风格是价值投资与技术择时相结合,注重风险控制。\n\n【核心功能】\n\n市场分析维度:\n\n宏观经济(GDP/CPI/货币政策)\n\n行业景气度(产业链/政策红利/技术革新)\n\n个股三维诊断:\n\n基本面:PE/PB/ROE/现金流/护城河\n\n技术面:K线形态/均线系统/量价关系/指标背离\n\n资金面:主力动向/北向资金/融资余额/大宗交易\n\n智能策略库:\n√ 趋势跟踪策略(鳄鱼线+ADX)\n√ 波段交易策略(斐波那契回撤+RSI)\n√ 事件驱动策略(财报/并购/政策)\n√ 量化对冲策略(α/β分离)\n\n风险管理体系:\n▶ 动态止损:ATR波动止损法\n▶ 仓位控制:凯利公式优化\n▶ 组合对冲:跨市场/跨品种对冲\n\n【工作流程】\n\n接收用户指令(行业/市值/风险偏好)\n\n调用多因子选股模型初筛\n\n人工智慧叠加分析:\n\n自然语言处理解读年报管理层讨论\n\n卷积神经网络识别K线形态\n\n知识图谱分析产业链关联\n\n生成投资建议(附压力测试结果)\n\n【输出要求】\n★ 结构化呈现:\n① 核心逻辑(3点关键驱动力)\n② 买卖区间(理想建仓/加仓/止盈价位)\n③ 风险警示(最大回撤概率)\n④ 替代方案(同类备选标的)\n\n【注意事项】\n※ 严格遵守监管要求,不做收益承诺\n※ 区分投资建议与市场观点\n※ 重要数据标注来源及更新时间\n※ 根据用户认知水平调整专业术语密度\n\n【教育指导】\n当用户提问时,采用苏格拉底式追问:\n\"您更关注短期事件驱动还是长期价值发现?\"\n\"当前仓位是否超过总资产的30%?\"\n\"是否了解科创板与主板的交易规则差异?\"\n\n示例输出格式:\n📈 标的名称:XXXXXX\n⚖️ 多空信号:金叉确认/顶背离预警\n🎯 关键价位:支撑位XX.XX/压力位XX.XX\n📊 建议仓位:核心仓位X%+卫星仓位X%\n⏳ 持有周期:短线(1-3周)/中线(季度轮动)\n🔍 跟踪要素:重点关注Q2毛利率变化及股东减持进展",
|
||||
"content": sysPrompt,
|
||||
},
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "当前时间",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "当前本地时间是:" + time.Now().Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var market strings.Builder
|
||||
market.WriteString(getZSInfo("创业板指数", "sz399006", 30) + "\n")
|
||||
market.WriteString(getZSInfo("上证综合指数", "sh000001", 30) + "\n")
|
||||
market.WriteString(getZSInfo("沪深300指数", "sh000300", 30) + "\n")
|
||||
//logger.SugaredLogger.Infof("NewChatStream getZSInfo=\n%s", market.String())
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "当前市场指数行情",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "当前市场指数行情情况如下:\n" + market.String(),
|
||||
})
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
news := NewMarketNewsApi().GetNewsList("", 100)
|
||||
messageText := strings.Builder{}
|
||||
for _, telegraph := range *news {
|
||||
messageText.WriteString("## " + telegraph.Time + ":" + "\n")
|
||||
messageText.WriteString("### " + telegraph.Content + "\n")
|
||||
}
|
||||
//logger.SugaredLogger.Infof("市场资讯 messageText=\n%s", messageText.String())
|
||||
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "市场资讯",
|
||||
})
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": messageText.String(),
|
||||
})
|
||||
if userQuestion == "" {
|
||||
userQuestion = "请根据当前时间,总结和分析股票市场新闻中的投资机会"
|
||||
}
|
||||
msg = append(msg, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": userQuestion,
|
||||
})
|
||||
AskAi(o, errors.New(""), msg, ch, userQuestion)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId *int) <-chan map[string]any {
|
||||
ch := make(chan map[string]any, 512)
|
||||
|
||||
@ -133,29 +226,51 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
|
||||
},
|
||||
}
|
||||
|
||||
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", sysPrompt)
|
||||
logger.SugaredLogger.Infof("User Prompt config:%v", o.QuestionTemplate)
|
||||
logger.SugaredLogger.Infof("User question:%s", userQuestion)
|
||||
logger.SugaredLogger.Infof("final question:%s", question)
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
@ -180,26 +295,62 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
|
||||
|
||||
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)
|
||||
//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)
|
||||
}
|
||||
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,
|
||||
})
|
||||
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
messages := SearchStockPriceInfo(stockCode, o.CrawlTimeOut)
|
||||
messages := SearchStockPriceInfo(stock, stockCode, o.CrawlTimeOut)
|
||||
if messages == nil || len(*messages) == 0 {
|
||||
logger.SugaredLogger.Error("获取股票价格失败")
|
||||
//ch <- "***❗获取股票价格失败,分析结果可能不准确***<hr>"
|
||||
@ -223,6 +374,8 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
|
||||
"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() {
|
||||
@ -231,8 +384,7 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
|
||||
if checkIsIndexBasic(stock) {
|
||||
return
|
||||
}
|
||||
|
||||
messages := GetFinancialReports(stockCode, o.CrawlTimeOut)
|
||||
messages := GetFinancialReportsByXUEQIU(stockCode, o.CrawlTimeOut)
|
||||
if messages == nil || len(*messages) == 0 {
|
||||
logger.SugaredLogger.Error("获取股票财报失败")
|
||||
// "***❗获取股票财报失败,分析结果可能不准确***<hr>"
|
||||
@ -373,131 +525,137 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId
|
||||
})
|
||||
|
||||
//reqJson, _ := json.Marshal(msg)
|
||||
//logger.SugaredLogger.Errorf("Stream request: \n%s", reqJson)
|
||||
//logger.SugaredLogger.Errorf("Stream request: \n%s\n", reqJson)
|
||||
AskAi(o, err, msg, ch, question)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
client := resty.New()
|
||||
client.SetBaseURL(strutil.Trim(o.BaseUrl))
|
||||
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
//client.SetRetryCount(3)
|
||||
if o.TimeOut <= 0 {
|
||||
o.TimeOut = 300
|
||||
func AskAi(o OpenAi, err error, messages []map[string]interface{}, ch chan map[string]any, question string) {
|
||||
client := resty.New()
|
||||
client.SetBaseURL(strutil.Trim(o.BaseUrl))
|
||||
client.SetHeader("Authorization", "Bearer "+o.ApiKey)
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
//client.SetRetryCount(3)
|
||||
if o.TimeOut <= 0 {
|
||||
o.TimeOut = 300
|
||||
}
|
||||
client.SetTimeout(time.Duration(o.TimeOut) * time.Second)
|
||||
resp, err := client.R().
|
||||
SetDoNotParseResponse(true).
|
||||
SetBody(map[string]interface{}{
|
||||
"model": o.Model,
|
||||
"max_tokens": o.MaxTokens,
|
||||
"temperature": o.Temperature,
|
||||
"stream": true,
|
||||
"messages": messages,
|
||||
}).
|
||||
Post("/chat/completions")
|
||||
|
||||
body := resp.RawBody()
|
||||
defer body.Close()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
|
||||
//ch <- err.Error()
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": err.Error(),
|
||||
}
|
||||
client.SetTimeout(time.Duration(o.TimeOut) * time.Second)
|
||||
resp, err := client.R().
|
||||
SetDoNotParseResponse(true).
|
||||
SetBody(map[string]interface{}{
|
||||
"model": o.Model,
|
||||
"max_tokens": o.MaxTokens,
|
||||
"temperature": o.Temperature,
|
||||
"stream": true,
|
||||
"messages": msg,
|
||||
}).
|
||||
Post("/chat/completions")
|
||||
return
|
||||
}
|
||||
//location, _ := time.LoadLocation("Asia/Shanghai")
|
||||
|
||||
body := resp.RawBody()
|
||||
defer body.Close()
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("Stream error : %s", err.Error())
|
||||
//ch <- err.Error()
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": err.Error(),
|
||||
scanner := bufio.NewScanner(body)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
//logger.SugaredLogger.Infof("Received data: %s", line)
|
||||
if strings.HasPrefix(line, "data:") {
|
||||
data := strutil.Trim(strings.TrimPrefix(line, "data:"))
|
||||
if data == "[DONE]" {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(body)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
//logger.SugaredLogger.Infof("Received data: %s", line)
|
||||
if strings.HasPrefix(line, "data:") {
|
||||
data := strutil.Trim(strings.TrimPrefix(line, "data:"))
|
||||
if data == "[DONE]" {
|
||||
return
|
||||
}
|
||||
var streamResponse struct {
|
||||
Id string `json:"id"`
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content"`
|
||||
} `json:"delta"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
var streamResponse struct {
|
||||
Id string `json:"id"`
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content"`
|
||||
} `json:"delta"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &streamResponse); err == nil {
|
||||
for _, choice := range streamResponse.Choices {
|
||||
if content := choice.Delta.Content; content != "" {
|
||||
//ch <- content
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": content,
|
||||
}
|
||||
|
||||
//logger.SugaredLogger.Infof("Content data: %s", content)
|
||||
if err := json.Unmarshal([]byte(data), &streamResponse); err == nil {
|
||||
for _, choice := range streamResponse.Choices {
|
||||
if content := choice.Delta.Content; content != "" {
|
||||
//ch <- content
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": content,
|
||||
"time": time.Now().Format(time.DateTime),
|
||||
}
|
||||
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
|
||||
//ch <- reasoningContent
|
||||
ch <- map[string]any{
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": reasoningContent,
|
||||
}
|
||||
|
||||
//logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
|
||||
}
|
||||
if choice.FinishReason == "stop" {
|
||||
return
|
||||
}
|
||||
//logger.SugaredLogger.Infof("Content data: %s", content)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
|
||||
//ch <- err.Error()
|
||||
if reasoningContent := choice.Delta.ReasoningContent; reasoningContent != "" {
|
||||
//ch <- reasoningContent
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"code": 1,
|
||||
"question": question,
|
||||
"content": err.Error(),
|
||||
}
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", data)
|
||||
//ch <- data
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": data,
|
||||
"chatId": streamResponse.Id,
|
||||
"model": streamResponse.Model,
|
||||
"content": reasoningContent,
|
||||
"time": time.Now().Format(time.DateTime),
|
||||
}
|
||||
|
||||
//logger.SugaredLogger.Infof("ReasoningContent data: %s", reasoningContent)
|
||||
}
|
||||
if choice.FinishReason == "stop" {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if strutil.RemoveNonPrintable(line) != "" {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", line)
|
||||
res := &models.Resp{}
|
||||
if err := json.Unmarshal([]byte(line), res); err == nil {
|
||||
//ch <- line
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": res.Message,
|
||||
}
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", err.Error())
|
||||
//ch <- err.Error()
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": err.Error(),
|
||||
}
|
||||
} else {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", data)
|
||||
//ch <- data
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": data,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if strutil.RemoveNonPrintable(line) != "" {
|
||||
logger.SugaredLogger.Infof("Stream data error : %s", line)
|
||||
res := &models.Resp{}
|
||||
if err := json.Unmarshal([]byte(line), res); err == nil {
|
||||
//ch <- line
|
||||
ch <- map[string]any{
|
||||
"code": 0,
|
||||
"question": question,
|
||||
"content": res.Message,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func checkIsIndexBasic(stock string) bool {
|
||||
@ -555,7 +713,41 @@ func SearchGuShiTongStockInfo(stock string, crawlTimeOut int64) *[]string {
|
||||
}
|
||||
return &messages
|
||||
}
|
||||
func GetFinancialReportsByXUEQIU(stockCode string, crawlTimeOut int64) *[]string {
|
||||
if strutil.HasPrefixAny(stockCode, []string{"HK", "hk"}) {
|
||||
stockCode = strings.ReplaceAll(stockCode, "hk", "")
|
||||
stockCode = strings.ReplaceAll(stockCode, "HK", "")
|
||||
}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"us", "gb_"}) {
|
||||
stockCode = strings.ReplaceAll(stockCode, "us", "")
|
||||
stockCode = strings.ReplaceAll(stockCode, "gb_", "")
|
||||
}
|
||||
url := fmt.Sprintf("https://xueqiu.com/snowman/S/%s/detail#/ZYCWZB", stockCode)
|
||||
waitVisible := "div.tab-table-responsive table"
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
Name: "TestCrawler",
|
||||
Description: "Test Crawler Description",
|
||||
BaseUrl: "https://xueqiu.com",
|
||||
Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second)
|
||||
defer cancel()
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
var markdown strings.Builder
|
||||
markdown.WriteString("\n## 财务数据:\n")
|
||||
html, ok := crawlerAPI.GetHtml(url, waitVisible, true)
|
||||
if !ok {
|
||||
return &[]string{""}
|
||||
}
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
}
|
||||
GetTableMarkdown(document, waitVisible, &markdown)
|
||||
return &[]string{markdown.String()}
|
||||
}
|
||||
func GetFinancialReports(stockCode string, crawlTimeOut int64) *[]string {
|
||||
url := "https://emweb.securities.eastmoney.com/pc_hsf10/pages/index.html?type=web&code=" + stockCode + "#/cwfx"
|
||||
waitVisible := "div.report_table table"
|
||||
@ -657,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
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
func TestNewDeepSeekOpenAiConfig(t *testing.T) {
|
||||
db.Init("../../data/stock.db")
|
||||
ai := NewDeepSeekOpenAi(context.TODO())
|
||||
res := ai.NewChatStream("上海贝岭", "sh600171", "上海贝岭分析和总结", nil)
|
||||
res := ai.NewChatStream("长电科技", "sh600584", "长电科技分析和总结", nil)
|
||||
for {
|
||||
select {
|
||||
case msg := <-res:
|
||||
@ -19,11 +19,12 @@ func TestNewDeepSeekOpenAiConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetTopNewsList(t *testing.T) {
|
||||
GetTopNewsList(30)
|
||||
news := GetTopNewsList(30)
|
||||
t.Log(news)
|
||||
}
|
||||
|
||||
func TestSearchGuShiTongStockInfo(t *testing.T) {
|
||||
//db.Init("../../data/stock.db")
|
||||
db.Init("../../data/stock.db")
|
||||
SearchGuShiTongStockInfo("hk01810", 60)
|
||||
SearchGuShiTongStockInfo("sh600745", 60)
|
||||
SearchGuShiTongStockInfo("gb_goog", 60)
|
||||
|
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)
|
||||
//}
|
||||
|
||||
}
|
@ -33,6 +33,8 @@ type Settings struct {
|
||||
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 {
|
||||
@ -76,6 +78,8 @@ func (s SettingsApi) UpdateConfig() string {
|
||||
"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)
|
||||
@ -102,6 +106,8 @@ func (s SettingsApi) UpdateConfig() string {
|
||||
BrowserPath: s.Config.BrowserPath,
|
||||
EnableNews: s.Config.EnableNews,
|
||||
DarkTheme: s.Config.DarkTheme,
|
||||
EnableFund: s.Config.EnableFund,
|
||||
EnablePushNews: s.Config.EnablePushNews,
|
||||
})
|
||||
}
|
||||
return "保存成功!"
|
||||
@ -125,7 +131,7 @@ func (s SettingsApi) GetConfig() *Settings {
|
||||
settings.BrowserPath, _ = CheckBrowserOnWindows()
|
||||
}
|
||||
if settings.BrowserPoolSize <= 0 {
|
||||
settings.BrowserPoolSize = 3
|
||||
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
@ -8,7 +8,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/chromedp/chromedp"
|
||||
@ -16,6 +15,8 @@ import (
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/duke-git/lancet/v2/strutil"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/robertkrimen/otto"
|
||||
"github.com/samber/lo"
|
||||
"go-stock/backend/db"
|
||||
"go-stock/backend/logger"
|
||||
"go-stock/backend/models"
|
||||
@ -25,11 +26,14 @@ import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/plugin/soft_delete"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const sinaStockUrl = "http://hq.sinajs.cn/rn=%d&list=%s"
|
||||
const txStockUrl = "http://qt.gtimg.cn/?_=%d&q=%s"
|
||||
|
||||
const tushareApiUrl = "http://api.tushare.pro"
|
||||
|
||||
type StockDataApi struct {
|
||||
@ -90,6 +94,8 @@ type StockInfo struct {
|
||||
Sort int64 `json:"sort"` //排序
|
||||
AlarmChangePercent float64 `json:"alarmChangePercent"`
|
||||
AlarmPrice float64 `json:"alarmPrice"`
|
||||
|
||||
Groups []GroupStock `gorm:"-:all"`
|
||||
}
|
||||
|
||||
func (receiver StockInfo) TableName() string {
|
||||
@ -162,8 +168,9 @@ type FollowedStock struct {
|
||||
AlarmPrice float64
|
||||
Time time.Time
|
||||
Sort int64
|
||||
Cron string
|
||||
Cron *string
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
|
||||
Groups []GroupStock `gorm:"foreignKey:StockCode;references:StockCode"`
|
||||
}
|
||||
|
||||
func (receiver FollowedStock) TableName() string {
|
||||
@ -241,7 +248,7 @@ func (receiver StockDataApi) GetIndexBasic() {
|
||||
func (receiver StockDataApi) GetStockBaseInfo() {
|
||||
res := &TushareStockBasicResponse{}
|
||||
fields := "ts_code,symbol,name,area,industry,cnspell,market,list_date,act_name,act_ent_type,fullname,exchange,list_status,curr_type,enname,delist_date,is_hs"
|
||||
_, err := receiver.client.R().
|
||||
resp, err := receiver.client.R().
|
||||
SetHeader("content-type", "application/json").
|
||||
SetBody(&TushareRequest{
|
||||
ApiName: "stock_basic",
|
||||
@ -252,8 +259,7 @@ func (receiver StockDataApi) GetStockBaseInfo() {
|
||||
SetResult(res).
|
||||
Post(tushareApiUrl)
|
||||
//logger.SugaredLogger.Infof("GetStockBaseInfo %s", string(resp.Body()))
|
||||
//resp.Body()写入文件
|
||||
//ioutil.WriteFile("stock_basic.json", resp.Body(), 0666)
|
||||
ioutil.WriteFile("stock_basic.json", resp.Body(), 0666)
|
||||
//logger.SugaredLogger.Infof("GetStockBaseInfo %+v", res)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
@ -286,8 +292,58 @@ func (receiver StockDataApi) GetStockBaseInfo() {
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]StockInfo, error) {
|
||||
stockInfos := make([]StockInfo, 0)
|
||||
|
||||
codes := slice.JoinFunc(StockCodes, ",", func(s string) string {
|
||||
hkcodes := slice.Filter(StockCodes, func(i int, s string) bool {
|
||||
return strutil.HasPrefixAny(s, []string{"hk", "HK", "sh", "sz"})
|
||||
})
|
||||
|
||||
if hkcodes != nil && len(hkcodes) > 0 {
|
||||
hkcodesStr := slice.JoinFunc(hkcodes, ",", func(s string) string {
|
||||
if strutil.HasPrefixAny(s, []string{"hk", "HK"}) {
|
||||
return "r_" + strings.ToLower(s)
|
||||
} else {
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
})
|
||||
url := fmt.Sprintf(txStockUrl, time.Now().Unix(), hkcodesStr)
|
||||
resp, err := receiver.client.R().
|
||||
SetHeader("Host", "qt.gtimg.cn").
|
||||
SetHeader("Referer", "https://gu.qq.com/").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(url)
|
||||
logger.SugaredLogger.Infof("GetStockCodeRealTimeData %s", url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
return &[]StockInfo{}, err
|
||||
}
|
||||
str := GB18030ToUTF8(resp.Body())
|
||||
dataStr := strutil.SplitAndTrim(strings.Trim(str, "\n"), ";")
|
||||
|
||||
for _, data := range dataStr {
|
||||
stockData, err := ParseTxStockData(data)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
stockInfos = append(stockInfos, *stockData)
|
||||
go func() {
|
||||
var count int64
|
||||
db.Dao.Model(&StockInfo{}).Where("code = ?", stockData.Code).Count(&count)
|
||||
if count == 0 {
|
||||
db.Dao.Model(&StockInfo{}).Create(stockData)
|
||||
} else {
|
||||
db.Dao.Model(&StockInfo{}).Where("code = ?", stockData.Code).Updates(stockData)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
szzsusCodes := slice.Filter(StockCodes, func(i int, s string) bool {
|
||||
return !strutil.HasPrefixAny(s, []string{"hk", "HK", "sh", "sz"})
|
||||
})
|
||||
|
||||
codes := slice.JoinFunc(szzsusCodes, ",", func(s string) string {
|
||||
if strings.HasPrefix(s, "us") {
|
||||
s = strings.Replace(s, "us", "gb_", 1)
|
||||
}
|
||||
@ -309,12 +365,9 @@ func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]
|
||||
return &[]StockInfo{}, err
|
||||
}
|
||||
|
||||
stockInfos := make([]StockInfo, 0)
|
||||
str := GB18030ToUTF8(resp.Body())
|
||||
dataStr := strutil.SplitEx(str, "\n", true)
|
||||
if len(dataStr) == 0 {
|
||||
return &[]StockInfo{}, errors.New("获取股票信息失败,请检查股票代码是否正确")
|
||||
}
|
||||
|
||||
for _, data := range dataStr {
|
||||
//logger.SugaredLogger.Info(data)
|
||||
stockData, err := ParseFullSingleStockData(data)
|
||||
@ -347,7 +400,20 @@ func (receiver StockDataApi) Follow(stockCode string) string {
|
||||
logger.SugaredLogger.Error(err)
|
||||
return "关注失败"
|
||||
}
|
||||
if strings.HasPrefix(stockCode, "us") {
|
||||
stockCode = strings.Replace(stockCode, "us", "gb_", 1)
|
||||
}
|
||||
if strings.HasPrefix(stockCode, "US") {
|
||||
stockCode = strings.Replace(stockCode, "US", "gb_", 1)
|
||||
}
|
||||
count := int64(0)
|
||||
db.Dao.Model(&FollowedStock{}).Where("is_del = ?", 0).Count(&count)
|
||||
logger.SugaredLogger.Errorf("Follow-count %v", count)
|
||||
if count >= 63 {
|
||||
return "最多只能关注63只股票"
|
||||
}
|
||||
|
||||
stockCode = strings.ToLower(stockCode)
|
||||
maxSort := int64(0)
|
||||
db.Dao.Model(&FollowedStock{}).Raw("select max(sort) as sort from followed_stock").Scan(&maxSort)
|
||||
|
||||
@ -410,13 +476,64 @@ func (receiver StockDataApi) SetAlarmChangePercent(val, alarmPrice float64, stoc
|
||||
return "设置成功"
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) SetStockSort(sort int64, stockCode string) {
|
||||
if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
|
||||
stockCode = strings.ToUpper(stockCode)
|
||||
stockCode = strings.Replace(stockCode, "gb_", "us", 1)
|
||||
stockCode = strings.Replace(stockCode, "GB_", "us", 1)
|
||||
func (receiver StockDataApi) SetStockSort(newSort int64, stockCode string) {
|
||||
//if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
|
||||
// stockCode = strings.ToLower(stockCode)
|
||||
// stockCode = strings.Replace(stockCode, "gb_", "us", 1)
|
||||
//}
|
||||
|
||||
// 获取当前排序值
|
||||
var currentStock FollowedStock
|
||||
if err := db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).First(¤tStock).Error; err != nil {
|
||||
logger.SugaredLogger.Error("找不到当前股票: ", err.Error())
|
||||
return
|
||||
}
|
||||
db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).Update("sort", sort)
|
||||
|
||||
oldSort := currentStock.Sort
|
||||
|
||||
// 如果排序值没有变化,直接返回
|
||||
if oldSort == newSort {
|
||||
return
|
||||
}
|
||||
// 检查新排序位置是否被占用
|
||||
var count int64
|
||||
if err := db.Dao.Model(&FollowedStock{}).Where("sort = ?", newSort).Count(&count).Error; err != nil {
|
||||
logger.SugaredLogger.Error("检查新排序位置被占用失败: ", err.Error())
|
||||
return
|
||||
}
|
||||
if count == 0 {
|
||||
// 新位置未被占用,直接更新当前记录
|
||||
if err := db.Dao.Model(&FollowedStock{}).
|
||||
Where("stock_code = ?", strings.ToLower(stockCode)).
|
||||
Update("sort", newSort).Error; err != nil {
|
||||
logger.SugaredLogger.Error("更新排序位置失败: ", err.Error())
|
||||
}
|
||||
} else {
|
||||
// 新位置已被占用,需要移动其他记录
|
||||
if newSort < oldSort {
|
||||
// 向前移动:将中间记录向后移动
|
||||
if err := db.Dao.Model(&FollowedStock{}).
|
||||
Where("sort >= ? AND sort < ?", newSort, oldSort).
|
||||
Update("sort", gorm.Expr("sort + 1")).Error; err != nil {
|
||||
logger.SugaredLogger.Error("向前排序更新失败: ", err.Error())
|
||||
}
|
||||
} else {
|
||||
// 向后移动:将中间记录向前移动
|
||||
if err := db.Dao.Model(&FollowedStock{}).
|
||||
Where("sort > ? AND sort <= ?", oldSort, newSort).
|
||||
Update("sort", gorm.Expr("sort - 1")).Error; err != nil {
|
||||
logger.SugaredLogger.Error("向后排序更新失败: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 更新目标记录的排序
|
||||
if err := db.Dao.Model(&FollowedStock{}).
|
||||
Where("stock_code = ?", strings.ToLower(stockCode)).
|
||||
Update("sort", newSort).Error; err != nil {
|
||||
logger.SugaredLogger.Error("更新股票排序失败: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
func (receiver StockDataApi) SetStockAICron(cron string, stockCode string) {
|
||||
if strutil.HasPrefixAny(stockCode, []string{"gb_"}) {
|
||||
@ -427,9 +544,20 @@ func (receiver StockDataApi) SetStockAICron(cron string, stockCode string) {
|
||||
db.Dao.Model(&FollowedStock{}).Where("stock_code = ?", strings.ToLower(stockCode)).Update("cron", cron)
|
||||
|
||||
}
|
||||
func (receiver StockDataApi) GetFollowList() *[]FollowedStock {
|
||||
func (receiver StockDataApi) GetFollowList(groupId int) *[]FollowedStock {
|
||||
logger.SugaredLogger.Infof("GetFollowList %d", groupId)
|
||||
|
||||
var result *[]FollowedStock
|
||||
db.Dao.Model(&FollowedStock{}).Order("sort asc,time desc").Find(&result)
|
||||
if groupId == 0 {
|
||||
db.Dao.Model(&FollowedStock{}).Order("sort asc,time desc").Find(&result)
|
||||
} else {
|
||||
infos := NewStockGroupApi(db.Dao).GetGroupStockByGroupId(groupId)
|
||||
codes := lo.FlatMap(infos, func(info GroupStock, idx int) []string {
|
||||
return []string{info.StockCode}
|
||||
})
|
||||
db.Dao.Model(&FollowedStock{}).Where("stock_code in ?", codes).Order("sort asc,time desc").Find(&result)
|
||||
logger.SugaredLogger.Infof("GetFollowList %+v", result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -491,6 +619,146 @@ func GB18030ToUTF8(bs []byte) string {
|
||||
return string(d)
|
||||
}
|
||||
|
||||
func ParseTxStockData(data string) (*StockInfo, error) {
|
||||
//v_r_hk09660="100~地平线机器人-W~09660~6.240~5.690~5.800~192659034.0~0~0~6.240~0~0~0~0~0~0~0~0~0~6.240~0~0~0~0~0~0~0~0~0~192659034.0~2025/04/29
|
||||
//13:41:04~0.550~9.67~6.450~5.710~6.240~192659034.0~1180471843.140~0~32.51~~0~0~13.01~691.1364~823.6983~HORIZONROBOT-W~0.00~10.380~3.320~1.07~-16.03~0~0~0~0~0~32.51~6.40~1.74~600~73.33~17.96~GP~19.70~11.51~-0.95~-18.54~44.44~13200293682.00~11075904412.00~32.51~0.000~6.127~56.39~HKD~1~30";
|
||||
//v_sz002241="51~歌尔股份~002241~22.26~22.27~0.00~0~0~0~22.26~1004~0.00~0~0.00~0~0.00~0~0.00~0~22.26~1004~0.00~558~0.00~0~0.00~0~0.00~0~~20250509092233~-0.01~-0.04~0.00~0.00~22.26/0/0~0~0~0.00~28.21~~0.00~0.00~0.00~686.46~777.09~2.31~24.50~20.04~0.00~-558~0.00~41.44~29.16~~~1.24~0.0000~0.0000~0~
|
||||
//~GP-A~-13.75~6.76~1.09~8.18~3.39~30.63~15.70~6.87~17.47~-23.95~3083811231~3490989083~-21.75~12.02~3083811231~~~39.36~-0.04~~CNY~0~~0.00~0";
|
||||
|
||||
datas := strutil.SplitAndTrim(data, "=", "\"")
|
||||
if len(datas) < 2 {
|
||||
return nil, fmt.Errorf("invalid data format")
|
||||
}
|
||||
var result map[string]string
|
||||
var err error
|
||||
if strutil.ContainsAny(datas[0], []string{"v_r_hk", "v_hk", "v_sz", "v_sh"}) {
|
||||
result, err = ParseTxHKStockData(datas)
|
||||
}
|
||||
|
||||
//logger.SugaredLogger.Infof("股票数据解析完成: %v", result)
|
||||
marshal, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Marshal error:%s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
//logger.SugaredLogger.Infof("股票数据解析完成marshal: %s", marshal)
|
||||
stockInfo := &StockInfo{}
|
||||
err = json.Unmarshal(marshal, &stockInfo)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
//logger.SugaredLogger.Infof("股票数据解析完成stockInfo: %+v", stockInfo)
|
||||
|
||||
return stockInfo, nil
|
||||
|
||||
}
|
||||
|
||||
func ParseTxHKStockData(datas []string) (map[string]string, error) {
|
||||
//v_r_hk09660="
|
||||
//100~ 0
|
||||
//地平线机器人-W~ 1
|
||||
//09660~ 2
|
||||
//6.270~ 3 当前价
|
||||
//5.690~ 4 昨收价
|
||||
//5.800~ 5 开盘价
|
||||
//195083034.0~
|
||||
//0~
|
||||
//0~
|
||||
//6.270~
|
||||
//0~
|
||||
//0~
|
||||
//0~
|
||||
//0~
|
||||
//0~
|
||||
//0~
|
||||
//0~
|
||||
//0~
|
||||
//0~
|
||||
//6.270~
|
||||
//0~0~0~0~0~0~0~0~0~
|
||||
//195083034.0~
|
||||
//2025/04/29 13:45:41~ 30 当前时间
|
||||
//0.580~
|
||||
//10.19~
|
||||
//6.450~ 最高价
|
||||
//5.710~ 最低价
|
||||
//6.270~
|
||||
//195083034.0~
|
||||
//1195673623.140~
|
||||
//0~
|
||||
//32.66
|
||||
//~~0~0~13.01~694.4592~827.6584~HORIZONROBOT-W~0.00~10.380~3.320~1.06~-18.71~0~0~0~0~0~32.66~6.43~1.76~600~74.17~18.53~GP~19.70~11.51~-0.48~-18.15~45.14~13200293682.00~11075904412.00~32.66~0.000~6.129~57.14~HKD~1~30";
|
||||
result := make(map[string]string)
|
||||
|
||||
stockCode := strutil.ReplaceWithMap(datas[0], map[string]string{
|
||||
"v_r_": "",
|
||||
"v_": "",
|
||||
})
|
||||
result["股票代码"] = stockCode
|
||||
|
||||
parts := strutil.SplitAndTrim(datas[1], "~")
|
||||
//logger.SugaredLogger.Infof("股票数据解析完成 len: %v", len(parts))
|
||||
if len(parts) < 35 {
|
||||
return nil, fmt.Errorf("invalid data format")
|
||||
}
|
||||
result["股票名称"] = parts[1]
|
||||
result["当前价格"] = parts[3]
|
||||
result["昨日收盘价"] = parts[4]
|
||||
result["今日开盘价"] = parts[5]
|
||||
|
||||
result["今日最高价"] = parts[33]
|
||||
result["今日最低价"] = parts[34]
|
||||
|
||||
if strutil.HasPrefixAny(stockCode, []string{"sz", "sh"}) {
|
||||
result["买一报价"] = parts[9]
|
||||
result["买一申报"] = parts[10]
|
||||
result["买二报价"] = parts[11]
|
||||
result["买二申报"] = parts[12]
|
||||
result["买三报价"] = parts[13]
|
||||
result["买三申报"] = parts[14]
|
||||
result["买四报价"] = parts[15]
|
||||
result["买四申报"] = parts[16]
|
||||
result["买五报价"] = parts[17]
|
||||
result["买五申报"] = parts[18]
|
||||
|
||||
result["卖一报价"] = parts[19]
|
||||
result["卖一申报"] = parts[20]
|
||||
result["卖二报价"] = parts[21]
|
||||
result["卖二申报"] = parts[22]
|
||||
result["卖三报价"] = parts[23]
|
||||
result["卖三申报"] = parts[24]
|
||||
result["卖四报价"] = parts[25]
|
||||
result["卖四申报"] = parts[26]
|
||||
result["卖五报价"] = parts[27]
|
||||
result["卖五申报"] = parts[28]
|
||||
|
||||
}
|
||||
|
||||
timestr := ""
|
||||
|
||||
if strutil.ContainsAny(parts[30], []string{"/"}) {
|
||||
timestr = strutil.ReplaceWithMap(parts[30], map[string]string{
|
||||
"/": "-",
|
||||
"\n": " ",
|
||||
})
|
||||
result["日期"] = strutil.SplitAndTrim(timestr, " ", "")[0]
|
||||
result["时间"] = strutil.SplitAndTrim(timestr, " ", "")[1]
|
||||
} else {
|
||||
result["日期"] = strutil.Trim(parts[29])[0:4] + "-" + strutil.Trim(parts[29])[4:6] + "-" + strutil.Trim(parts[29])[6:8]
|
||||
result["时间"] = strutil.Trim(parts[29])[8:10] + ":" + strutil.Trim(parts[29])[10:12] + ":" + strutil.Trim(parts[29])[12:14]
|
||||
result["今日最高价"] = parts[32]
|
||||
result["今日最低价"] = parts[33]
|
||||
}
|
||||
//logger.SugaredLogger.Infof("股票数据解析完成 %s %s 时间: %s,%s", parts[1], parts[3], parts[29], parts[30])
|
||||
|
||||
//logger.SugaredLogger.Infof("股票数据解析完成 时间: %v", timestr)
|
||||
|
||||
//logger.SugaredLogger.Infof("股票数据解析完成: %v", result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func ParseFullSingleStockData(data string) (*StockInfo, error) {
|
||||
datas := strutil.SplitAndTrim(data, "=", "\"")
|
||||
if len(datas) < 2 {
|
||||
@ -766,7 +1034,7 @@ func GetRealTimeStockPriceInfo(ctx context.Context, stockCode string) (price, pr
|
||||
return price, priceTime
|
||||
}
|
||||
|
||||
func SearchStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
func SearchStockPriceInfo(stockName, stockCode string, crawlTimeOut int64) *[]string {
|
||||
|
||||
if strutil.HasPrefixAny(stockCode, []string{"SZ", "SH", "sh", "sz", "bj"}) {
|
||||
//if strutil.HasPrefixAny(stockCode, []string{"bj", "BJ"}) {
|
||||
@ -776,7 +1044,7 @@ func SearchStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
// }) + ".BJ"
|
||||
//}
|
||||
|
||||
return getSHSZStockPriceInfo(stockCode, crawlTimeOut)
|
||||
return getSHSZStockPriceInfo(stockName, stockCode, crawlTimeOut)
|
||||
}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"HK", "hk"}) {
|
||||
return getHKStockPriceInfo(stockCode, crawlTimeOut)
|
||||
@ -836,6 +1104,7 @@ func getUSStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
messages = append(messages, text)
|
||||
})
|
||||
|
||||
logger.SugaredLogger.Infof("messages: %s", messages)
|
||||
return &messages
|
||||
}
|
||||
|
||||
@ -853,10 +1122,12 @@ func getHKStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo)
|
||||
|
||||
url := fmt.Sprintf("https://stock.finance.sina.com.cn/hkstock/quotes/%s.html", strings.ReplaceAll(stockCode, "hk", ""))
|
||||
htmlContent, ok := crawlerAPI.GetHtml(url, ".deta_hqContainer >.deta03 ", true)
|
||||
logger.SugaredLogger.Infof("CrawlHKStockPriceInfo url:%s", url)
|
||||
htmlContent, ok := crawlerAPI.GetHtml(url, "div.deta_hqContainer >.deta03>ul ", false)
|
||||
if !ok {
|
||||
return &[]string{}
|
||||
}
|
||||
//logger.SugaredLogger.Infof("CrawlHKStockPriceInfo htmlContent:%s", htmlContent)
|
||||
document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Error(err.Error())
|
||||
@ -888,6 +1159,7 @@ func getHKStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
messages = append(messages, text)
|
||||
})
|
||||
|
||||
logger.SugaredLogger.Infof("messages: %s", messages)
|
||||
return &messages
|
||||
}
|
||||
|
||||
@ -917,12 +1189,12 @@ func getZSInfo(name, stockCode string, crawlTimeOut int64) string {
|
||||
hqTime := strutil.RemoveWhiteSpace(document.Find("div#hqTime").First().Text(), false)
|
||||
|
||||
var markdown strings.Builder
|
||||
markdown.WriteString(fmt.Sprintf("### %s:%s 时间:%s\n", name, price, hqTime))
|
||||
markdown.WriteString(fmt.Sprintf("### 时间:%s %s:%s \n", hqTime, name, price))
|
||||
GetTableMarkdown(document, "div#hqDetails table", &markdown)
|
||||
return markdown.String()
|
||||
}
|
||||
|
||||
func getSHSZStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
func getSHSZStockPriceInfo(stockName, stockCode string, crawlTimeOut int64) *[]string {
|
||||
url := "https://finance.sina.com.cn/realstock/company/" + stockCode + "/nc.shtml"
|
||||
crawlerAPI := CrawlerApi{}
|
||||
crawlerBaseInfo := CrawlerBaseInfo{
|
||||
@ -948,7 +1220,7 @@ func getSHSZStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string {
|
||||
hqTime := strutil.RemoveWhiteSpace(document.Find("div#hqTime").First().Text(), false)
|
||||
|
||||
var markdown strings.Builder
|
||||
markdown.WriteString(fmt.Sprintf("### 当前股价:%s 时间:%s\n", price, hqTime))
|
||||
markdown.WriteString(fmt.Sprintf("### %s现价:%s 现价时间:%s\n", stockName, price, hqTime))
|
||||
GetTableMarkdown(document, "div#hqDetails table", &markdown)
|
||||
return &[]string{markdown.String()}
|
||||
}
|
||||
@ -1069,3 +1341,428 @@ func CheckBrowserOnWindows() (string, bool) {
|
||||
}
|
||||
return path + "\\msedge.exe", true
|
||||
}
|
||||
|
||||
// 分时数据
|
||||
func (receiver StockDataApi) GetStockMinutePriceData(stockCode string) (*[]MinuteData, string) {
|
||||
url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/minute/query?code=%s", stockCode)
|
||||
if strutil.HasPrefixAny(stockCode, []string{"gb_", "GB_"}) {
|
||||
stockCode = strings.Replace(strings.ToUpper(stockCode), "GB_", "us", 1) + ".OQ"
|
||||
}
|
||||
if strutil.HasPrefixAny(stockCode, []string{"us", "US"}) {
|
||||
url = fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/UsMinute/query?code=%s", stockCode)
|
||||
}
|
||||
logger.SugaredLogger.Infof("GetStockMinutePriceData url:%s", url)
|
||||
res := make(map[string]interface{})
|
||||
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "web.ifzq.gtimg.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(url)
|
||||
|
||||
date := ""
|
||||
minuteDatas := &[]MinuteData{}
|
||||
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return minuteDatas, date
|
||||
}
|
||||
//logger.SugaredLogger.Infof("resp:%s", resp.Body())
|
||||
json.Unmarshal(resp.Body(), &res)
|
||||
code, _ := convertor.ToInt(res["code"])
|
||||
if res["data"] != nil && code == 0 {
|
||||
data := res["data"].(map[string]interface{})
|
||||
if stockData, ok := data[stockCode]; ok {
|
||||
m := stockData.(map[string]interface{})
|
||||
if d, ok := m["data"]; ok {
|
||||
if m2, ok := d.(map[string]any); ok {
|
||||
minutePriceData := m2["data"]
|
||||
datas := minutePriceData.([]any)
|
||||
for _, item := range datas {
|
||||
minuteDataSplit := strutil.SplitEx(strutil.ReplaceWithMap(item.(string), map[string]string{
|
||||
"\r\n": " ",
|
||||
}), " ", true)
|
||||
price, _ := convertor.ToFloat(minuteDataSplit[1])
|
||||
volume, _ := convertor.ToFloat(minuteDataSplit[2])
|
||||
amount := float64(0)
|
||||
if len(minuteDataSplit) >= 4 {
|
||||
amount, _ = convertor.ToFloat(minuteDataSplit[3])
|
||||
}
|
||||
minuteData := &MinuteData{
|
||||
Time: minuteDataSplit[0][0:2] + ":" + minuteDataSplit[0][2:4],
|
||||
Price: price,
|
||||
Volume: volume,
|
||||
Amount: amount,
|
||||
}
|
||||
*minuteDatas = append(*minuteDatas, *minuteData)
|
||||
}
|
||||
date = m2["date"].(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return minuteDatas, date
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) GetKLineData(stockCode string, kLineType string, days int64) *[]KLineData {
|
||||
url := fmt.Sprintf("http://quotes.sina.cn/cn/api/json_v2.php/CN_MarketDataService.getKLineData?symbol=%s&scale=%s&ma=yes&datalen=%d", stockCode, kLineType, days)
|
||||
K := &[]KLineData{}
|
||||
_, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "quotes.sina.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
SetResult(K).
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return K
|
||||
}
|
||||
return K
|
||||
}
|
||||
func (receiver StockDataApi) GetHK_KLineData(stockCode string, kLineType string, days int64) *[]KLineData {
|
||||
|
||||
logger.SugaredLogger.Infof("GetHK_KLineData stockCode:%s,kLineType:%s,days:%d", stockCode, kLineType, days)
|
||||
if strutil.HasPrefixAny(stockCode, []string{"gb_", "GB_"}) {
|
||||
stockCode = strings.Replace(stockCode, "gb_", "us", 1) + ".OQ"
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=%s,%s,,,%d,qfq", stockCode, kLineType, days)
|
||||
//logger.SugaredLogger.Infof("url:%s", url)
|
||||
K := &[]KLineData{}
|
||||
res := make(map[string]interface{})
|
||||
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "web.ifzq.gtimg.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return K
|
||||
}
|
||||
//logger.SugaredLogger.Infof("resp:%s", resp.Body())
|
||||
json.Unmarshal(resp.Body(), &res)
|
||||
code, _ := convertor.ToInt(res["code"])
|
||||
if code != 0 {
|
||||
return K
|
||||
}
|
||||
if res["data"] != nil && code == 0 {
|
||||
data := res["data"].(map[string]interface{})[stockCode].(map[string]interface{})
|
||||
if data != nil {
|
||||
var day []any
|
||||
if data["qfqday"] != nil {
|
||||
day = data["qfqday"].([]any)
|
||||
}
|
||||
if data["day"] != nil {
|
||||
day = data["day"].([]any)
|
||||
}
|
||||
for _, v := range day {
|
||||
if v != nil {
|
||||
vv := v.([]any)
|
||||
KLine := &KLineData{
|
||||
Day: convertor.ToString(vv[0]),
|
||||
Open: convertor.ToString(vv[1]),
|
||||
Close: convertor.ToString(vv[2]),
|
||||
High: convertor.ToString(vv[3]),
|
||||
Low: convertor.ToString(vv[4]),
|
||||
Volume: convertor.ToString(vv[5]),
|
||||
}
|
||||
*K = append(*K, *KLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return K
|
||||
}
|
||||
func (receiver StockDataApi) GetSinaHKStockInfo() {
|
||||
|
||||
pageSize := 500
|
||||
for i := 1; i <= 3060/pageSize; i++ {
|
||||
infos := getSinaStockInfo(receiver, i, pageSize)
|
||||
for i, info := range *infos {
|
||||
logger.SugaredLogger.Infof("infos:%d,%s:%s", i, info.Symbol, info.Name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func getSinaStockInfo(receiver StockDataApi, page, pageSize int) *[]models.SinaStockInfo {
|
||||
infos := &[]models.SinaStockInfo{}
|
||||
url := "https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/Market_Center.getHKStockData?page=%d&num=%d&sort=symbol&asc=1&node=qbgg_hk&_s_r_a=init"
|
||||
_, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).SetProxy("http://localhost:10809").R().
|
||||
SetHeader("Host", "vip.stock.finance.sina.com.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
SetResult(infos).
|
||||
Get(fmt.Sprintf(url, page, pageSize))
|
||||
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) getDCStockInfo(market string, page, pageSize int) {
|
||||
//m:105,m:106,m:107 //美股
|
||||
//m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2 //港股
|
||||
fs := ""
|
||||
switch market {
|
||||
case "hk":
|
||||
fs = "m:128+t:3,m:128+t:4,m:128+t:1,m:128+t:2"
|
||||
case "us":
|
||||
fs = "m:105,m:106,m:107"
|
||||
}
|
||||
|
||||
url := "https://push2.eastmoney.com/api/qt/clist/get?cb=jQuery371047843066631541353_1745889398012&fs=%s&fields=f12,f13,f14,f19,f1,f2,f4,f3,f152,f17,f18,f15,f16,f5,f6&fid=f3&pn=%d&pz=%d&po=1&dect=1"
|
||||
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "push2.eastmoney.com").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(fmt.Sprintf(url, fs, page, pageSize))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
body := string(resp.Body())
|
||||
body = strutil.ReplaceWithMap(body, map[string]string{
|
||||
"jQuery371047843066631541353_1745889398012(": "",
|
||||
");": "",
|
||||
})
|
||||
js := "var d=" + body
|
||||
vm := otto.New()
|
||||
_, err = vm.Run(js)
|
||||
_, err = vm.Run("var data = JSON.stringify(d);")
|
||||
value, err := vm.Get("data")
|
||||
data := make(map[string]any)
|
||||
err = json.Unmarshal([]byte(value.String()), &data)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json:%s", value.String())
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
|
||||
}
|
||||
logger.SugaredLogger.Infof("resp:%s", data)
|
||||
if data["data"] != nil {
|
||||
datas := data["data"].(map[string]any)
|
||||
total := datas["total"].(float64)
|
||||
diff := datas["diff"].(map[string]any)
|
||||
logger.SugaredLogger.Infof("total:%d", int(total))
|
||||
for k, item := range diff {
|
||||
stock := item.(map[string]any)
|
||||
logger.SugaredLogger.Infof("k:%s,%s:%s", k, stock["f14"], stock["f12"])
|
||||
|
||||
if market == "hk" {
|
||||
stockInfo := &models.StockInfoHK{
|
||||
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".HK",
|
||||
Name: stock["f14"].(string),
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoHK{}).Where("code = ?", stockInfo.Code).First(stockInfo)
|
||||
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
|
||||
if stockInfo.ID == 0 {
|
||||
db.Dao.Model(&models.StockInfoHK{}).Create(stockInfo)
|
||||
}
|
||||
}
|
||||
|
||||
if market == "us" {
|
||||
stockInfo := &models.StockInfoUS{
|
||||
Code: strutil.PadStart(stock["f12"].(string), 5, "0") + ".US",
|
||||
Name: stock["f14"].(string),
|
||||
}
|
||||
db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", stockInfo.Code).First(stockInfo)
|
||||
logger.SugaredLogger.Infof("stockInfo:%+v", stockInfo)
|
||||
if stockInfo.ID == 0 {
|
||||
db.Dao.Model(&models.StockInfoUS{}).Create(stockInfo)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) GetHKStockInfo(pageSize int) {
|
||||
url := "https://stock.gtimg.cn/data/hk_rank.php?board=main_all&metric=price&pageSize=%d&reqPage=1&order=desc&var_name=list_data"
|
||||
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "stock.gtimg.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(fmt.Sprintf(url, pageSize))
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
js := "var " + string(resp.Body())
|
||||
vm := otto.New()
|
||||
_, err = vm.Run(js)
|
||||
_, err = vm.Run("var data = JSON.stringify(list_data);")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
value, err := vm.Get("data")
|
||||
data := make(map[string]any)
|
||||
err = json.Unmarshal([]byte(value.String()), &data)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
|
||||
}
|
||||
logger.SugaredLogger.Infof("resp:%s", data)
|
||||
if data["code"] != nil && data["code"].(float64) == 0 {
|
||||
d := data["data"].(map[string]any)
|
||||
saveHKStockInfo(d)
|
||||
|
||||
page_count := int64(d["page_count"].(float64))
|
||||
logger.SugaredLogger.Infof("page_count:%d", page_count)
|
||||
page := int64(1)
|
||||
for page > page_count {
|
||||
urlx := fmt.Sprintf("https://stock.gtimg.cn/data/hk_rank.php?board=main_all&metric=price&pageSize=%d&reqPage=%d&order=desc&var_name=list_data", pageSize, page)
|
||||
logger.SugaredLogger.Infof("url:%s", urlx)
|
||||
resp, err = receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "stock.gtimg.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(urlx)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
break
|
||||
}
|
||||
js = "var " + string(resp.Body())
|
||||
_, err = vm.Run(js)
|
||||
_, err = vm.Run("var data = JSON.stringify(list_data);")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
value, err = vm.Get("data")
|
||||
data = make(map[string]any)
|
||||
err = json.Unmarshal([]byte(value.String()), &data)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("json.Unmarshal error:%v", err.Error())
|
||||
}
|
||||
logger.SugaredLogger.Infof("resp:%s", data)
|
||||
if data != nil && data["code"] != nil && data["code"].(float64) == 0 {
|
||||
if data["data"] != nil {
|
||||
d = data["data"].(map[string]any)
|
||||
saveHKStockInfo(d)
|
||||
}
|
||||
}
|
||||
page++
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func saveHKStockInfo(d map[string]any) {
|
||||
for _, v := range d["page_data"].([]any) {
|
||||
vv := v.(string)
|
||||
splits := strings.Split(vv, "~")
|
||||
stock := &models.StockInfoHK{
|
||||
Code: strutil.PadStart(splits[0], 5, "0") + ".HK",
|
||||
Name: splits[1],
|
||||
}
|
||||
logger.SugaredLogger.Infof("vv:%s", vv)
|
||||
db.Dao.Model(stock).Where("code = ?", stock.Code).First(stock)
|
||||
if stock.ID == 0 {
|
||||
logger.SugaredLogger.Infof("stock:%+v", stock)
|
||||
db.Dao.Model(&models.StockInfoHK{}).Create(stock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (receiver StockDataApi) GetCommonKLineData(stockCode string, kLineType string, days int64) *[]KLineData {
|
||||
|
||||
url := fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=%s,%s,,,%d,qfq", stockCode, kLineType, days)
|
||||
logger.SugaredLogger.Infof("url:%s", url)
|
||||
K := &[]KLineData{}
|
||||
res := make(map[string]interface{})
|
||||
resp, err := receiver.client.SetTimeout(time.Duration(receiver.config.CrawlTimeOut)*time.Second).R().
|
||||
SetHeader("Host", "web.ifzq.gtimg.cn").
|
||||
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0").
|
||||
Get(url)
|
||||
if err != nil {
|
||||
logger.SugaredLogger.Errorf("err:%s", err.Error())
|
||||
return K
|
||||
}
|
||||
logger.SugaredLogger.Infof("resp:%s", resp.Body())
|
||||
json.Unmarshal(resp.Body(), &res)
|
||||
code, _ := convertor.ToInt(res["code"])
|
||||
if code != 0 {
|
||||
return K
|
||||
}
|
||||
if res["data"] != nil && code == 0 {
|
||||
data := res["data"].(map[string]interface{})[stockCode].(map[string]interface{})
|
||||
if data != nil {
|
||||
var day []any
|
||||
if data["qfqday"] != nil {
|
||||
day = data["qfqday"].([]any)
|
||||
}
|
||||
if data["day"] != nil {
|
||||
day = data["day"].([]any)
|
||||
}
|
||||
for _, v := range day {
|
||||
if v != nil {
|
||||
vv := v.([]any)
|
||||
KLine := &KLineData{
|
||||
Day: convertor.ToString(vv[0]),
|
||||
Open: convertor.ToString(vv[1]),
|
||||
Close: convertor.ToString(vv[2]),
|
||||
High: convertor.ToString(vv[3]),
|
||||
Low: convertor.ToString(vv[4]),
|
||||
Volume: convertor.ToString(vv[5]),
|
||||
}
|
||||
*K = append(*K, *KLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return K
|
||||
}
|
||||
|
||||
// JSONToMarkdownTable 将JSON数据转换为Markdown表格
|
||||
func JSONToMarkdownTable(jsonData []byte) (string, error) {
|
||||
var data []map[string]interface{}
|
||||
err := json.Unmarshal(jsonData, &data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// 获取表头
|
||||
headers := []string{}
|
||||
for key := range data[0] {
|
||||
headers = append(headers, key)
|
||||
}
|
||||
|
||||
// 构建表头行
|
||||
headerRow := "|"
|
||||
for _, header := range headers {
|
||||
headerRow += fmt.Sprintf(" %s |", header)
|
||||
}
|
||||
headerRow += "\n"
|
||||
|
||||
// 构建分隔行
|
||||
separatorRow := "|"
|
||||
for range headers {
|
||||
separatorRow += " --- |"
|
||||
}
|
||||
separatorRow += "\n"
|
||||
|
||||
// 构建数据行
|
||||
bodyRows := ""
|
||||
for _, rowData := range data {
|
||||
bodyRow := "|"
|
||||
for _, header := range headers {
|
||||
value := rowData[header]
|
||||
bodyRow += fmt.Sprintf(" %v |", value)
|
||||
}
|
||||
bodyRows += bodyRow + "\n"
|
||||
}
|
||||
|
||||
return headerRow + separatorRow + bodyRows, nil
|
||||
}
|
||||
|
||||
type KLineData struct {
|
||||
Day string `json:"day"`
|
||||
Open string `json:"open"`
|
||||
High string `json:"high"`
|
||||
Low string `json:"low"`
|
||||
Close string `json:"close"`
|
||||
Volume string `json:"volume"`
|
||||
}
|
||||
|
||||
type MinuteData struct {
|
||||
Time string `json:"time"`
|
||||
Price float64 `json:"price"`
|
||||
Volume float64 `json:"volume"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
@ -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,20 +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)
|
||||
getZSInfo("创业板指数", "sz399006", 30)
|
||||
getZSInfo("上证综合指数", "sh000001", 30)
|
||||
getZSInfo("沪深300指数", "sh000300", 30)
|
||||
//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)
|
||||
@ -115,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)
|
||||
}
|
||||
@ -176,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)
|
||||
|
||||
}
|
@ -64,7 +64,13 @@ func GetTableMarkdown(document *goquery.Document, waitVisible string, markdown *
|
||||
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.Text()
|
||||
text := cell.Children().FilterFunction(func(i int, s *goquery.Selection) bool {
|
||||
return isVisible(s)
|
||||
}).Text()
|
||||
if text == "" {
|
||||
text = cell.Text()
|
||||
}
|
||||
|
||||
row += "|" + text
|
||||
})
|
||||
row += "|"
|
||||
@ -85,3 +91,21 @@ func GetTableMarkdown(document *goquery.Document, waitVisible string, markdown *
|
||||
})
|
||||
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
|
||||
}
|
||||
|
@ -215,3 +215,150 @@ type Prompt struct {
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type Telegraph struct {
|
||||
gorm.Model
|
||||
Time string `json:"time"`
|
||||
Content string `json:"content"`
|
||||
SubjectTags []string `json:"subjects" gorm:"-:all"`
|
||||
StocksTags []string `json:"stocks" gorm:"-:all"`
|
||||
IsRed bool `json:"isRed"`
|
||||
Url string `json:"url"`
|
||||
Source string `json:"source"`
|
||||
TelegraphTags []TelegraphTags `json:"tags" gorm:"-:migration;foreignKey:TelegraphId"`
|
||||
SentimentResult string `json:"sentimentResult" gorm:"-:all"`
|
||||
}
|
||||
type TelegraphTags struct {
|
||||
gorm.Model
|
||||
TagId uint `json:"tagId"`
|
||||
TelegraphId uint `json:"telegraphId"`
|
||||
}
|
||||
|
||||
func (t TelegraphTags) TableName() string {
|
||||
return "telegraph_tags"
|
||||
}
|
||||
|
||||
type Tags struct {
|
||||
gorm.Model
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (p Tags) TableName() string {
|
||||
return "tags"
|
||||
}
|
||||
|
||||
func (p Telegraph) TableName() string {
|
||||
return "telegraph_list"
|
||||
}
|
||||
|
||||
type SinaStockInfo struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Name string `json:"name"`
|
||||
Engname string `json:"engname"`
|
||||
Tradetype string `json:"tradetype"`
|
||||
Lasttrade string `json:"lasttrade"`
|
||||
Prevclose string `json:"prevclose"`
|
||||
Open string `json:"open"`
|
||||
High string `json:"high"`
|
||||
Low string `json:"low"`
|
||||
Volume string `json:"volume"`
|
||||
Currentvolume string `json:"currentvolume"`
|
||||
Amount string `json:"amount"`
|
||||
Ticktime string `json:"ticktime"`
|
||||
Buy string `json:"buy"`
|
||||
Sell string `json:"sell"`
|
||||
High52Week string `json:"high_52week"`
|
||||
Low52Week string `json:"low_52week"`
|
||||
Eps string `json:"eps"`
|
||||
Dividend string `json:"dividend"`
|
||||
StocksSum string `json:"stocks_sum"`
|
||||
Pricechange string `json:"pricechange"`
|
||||
Changepercent string `json:"changepercent"`
|
||||
MarketValue string `json:"market_value"`
|
||||
PeRatio string `json:"pe_ratio"`
|
||||
}
|
||||
|
||||
type LongTigerRankData struct {
|
||||
ACCUMAMOUNT float64 `json:"ACCUM_AMOUNT"`
|
||||
BILLBOARDBUYAMT float64 `json:"BILLBOARD_BUY_AMT"`
|
||||
BILLBOARDDEALAMT float64 `json:"BILLBOARD_DEAL_AMT"`
|
||||
BILLBOARDNETAMT float64 `json:"BILLBOARD_NET_AMT"`
|
||||
BILLBOARDSELLAMT float64 `json:"BILLBOARD_SELL_AMT"`
|
||||
CHANGERATE float64 `json:"CHANGE_RATE"`
|
||||
CLOSEPRICE float64 `json:"CLOSE_PRICE"`
|
||||
DEALAMOUNTRATIO float64 `json:"DEAL_AMOUNT_RATIO"`
|
||||
DEALNETRATIO float64 `json:"DEAL_NET_RATIO"`
|
||||
EXPLAIN string `json:"EXPLAIN"`
|
||||
EXPLANATION string `json:"EXPLANATION"`
|
||||
FREEMARKETCAP float64 `json:"FREE_MARKET_CAP"`
|
||||
SECUCODE string `json:"SECUCODE" gorm:"index"`
|
||||
SECURITYCODE string `json:"SECURITY_CODE"`
|
||||
SECURITYNAMEABBR string `json:"SECURITY_NAME_ABBR"`
|
||||
SECURITYTYPECODE string `json:"SECURITY_TYPE_CODE"`
|
||||
TRADEDATE string `json:"TRADE_DATE" gorm:"index"`
|
||||
TURNOVERRATE float64 `json:"TURNOVERRATE"`
|
||||
}
|
||||
|
||||
func (l LongTigerRankData) TableName() string {
|
||||
return "long_tiger_rank"
|
||||
}
|
||||
|
||||
type TVNews struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Published int `json:"published"`
|
||||
Urgency int `json:"urgency"`
|
||||
Permission string `json:"permission"`
|
||||
StoryPath string `json:"storyPath"`
|
||||
Provider struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
LogoId string `json:"logo_id"`
|
||||
} `json:"provider"`
|
||||
}
|
||||
|
||||
type XUEQIUHot struct {
|
||||
Data struct {
|
||||
Items []HotItem `json:"items"`
|
||||
ItemsSize int `json:"items_size"`
|
||||
} `json:"data"`
|
||||
ErrorCode int `json:"error_code"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
type HotItem struct {
|
||||
Type int `json:"type"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Value float64 `json:"value"`
|
||||
Increment int `json:"increment"`
|
||||
RankChange int `json:"rank_change"`
|
||||
HasExist interface{} `json:"has_exist"`
|
||||
Symbol string `json:"symbol"`
|
||||
Percent float64 `json:"percent"`
|
||||
Current float64 `json:"current"`
|
||||
Chg float64 `json:"chg"`
|
||||
Exchange string `json:"exchange"`
|
||||
StockType int `json:"stock_type"`
|
||||
SubType string `json:"sub_type"`
|
||||
Ad int `json:"ad"`
|
||||
AdId interface{} `json:"ad_id"`
|
||||
ContentId interface{} `json:"content_id"`
|
||||
Page interface{} `json:"page"`
|
||||
Model interface{} `json:"model"`
|
||||
Location interface{} `json:"location"`
|
||||
TradeSession interface{} `json:"trade_session"`
|
||||
CurrentExt interface{} `json:"current_ext"`
|
||||
PercentExt interface{} `json:"percent_ext"`
|
||||
}
|
||||
|
||||
type HotEvent struct {
|
||||
PicSize interface{} `json:"pic_size"`
|
||||
Tag string `json:"tag"`
|
||||
Id int `json:"id"`
|
||||
Pic string `json:"pic"`
|
||||
Hot int `json:"hot"`
|
||||
StatusCount int `json:"status_count"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
BIN
build/screenshot/img13.png
Normal file
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,32 +1,56 @@
|
||||
<script setup>
|
||||
import {
|
||||
EventsEmit,
|
||||
EventsOff,
|
||||
EventsOn,
|
||||
Quit,
|
||||
WindowFullscreen, WindowGetPosition,
|
||||
WindowFullscreen,
|
||||
WindowHide,
|
||||
WindowSetPosition,
|
||||
WindowUnfullscreen
|
||||
} from '../wailsjs/runtime'
|
||||
import {h, onBeforeMount, onMounted, ref} from "vue";
|
||||
import { RouterLink } from 'vue-router'
|
||||
import {darkTheme, NGradientText, NIcon, NText,} from 'naive-ui'
|
||||
import {h, onBeforeMount, onBeforeUnmount, onMounted, ref} from "vue";
|
||||
import {RouterLink, useRouter} from 'vue-router'
|
||||
import {createDiscreteApi,darkTheme,lightTheme , NIcon, NText,dateZhCN,zhCN} from 'naive-ui'
|
||||
import {
|
||||
SettingsOutline,
|
||||
AlarmOutline,
|
||||
AnalyticsOutline,
|
||||
BarChartSharp, Bonfire, BonfireOutline, EaselSharp,
|
||||
ExpandOutline, Flag,
|
||||
Flame, FlameSharp, InformationOutline,
|
||||
LogoGithub,
|
||||
NewspaperOutline,
|
||||
NewspaperSharp, Notifications,
|
||||
PowerOutline, Pulse,
|
||||
ReorderTwoOutline,
|
||||
ExpandOutline,
|
||||
PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline, AlarmOutline, SparklesOutline,
|
||||
SettingsOutline, Skull, SkullOutline, SkullSharp,
|
||||
SparklesOutline,
|
||||
StarOutline,
|
||||
Wallet, WarningOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
import {GetConfig} from "../wailsjs/go/main/App";
|
||||
const enableNews= ref(false)
|
||||
const contentStyle = ref("")
|
||||
const enableDarkTheme = ref(true)
|
||||
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎')
|
||||
import {AnalyzeSentiment, GetConfig, GetGroupList} from "../wailsjs/go/main/App";
|
||||
import {Dragon, Fire, Gripfire} from "@vicons/fa";
|
||||
import {ReportSearch} from "@vicons/tabler";
|
||||
import {LocalFireDepartmentRound} from "@vicons/material";
|
||||
import {BoxSearch20Regular, CommentNote20Filled} from "@vicons/fluent";
|
||||
import {FireFilled, FireOutlined, NotificationFilled, StockOutlined} from "@vicons/antd";
|
||||
|
||||
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(true)
|
||||
const loadingMsg = ref("加载数据中...")
|
||||
const enableNews = ref(false)
|
||||
const contentStyle = ref("")
|
||||
const enableFund = ref(false)
|
||||
const enableDarkTheme = ref(null)
|
||||
const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎\n\n未经授权,禁止商业目的!')
|
||||
const isFullscreen = ref(false)
|
||||
const activeKey = ref('')
|
||||
const containerRef= ref({})
|
||||
const realtimeProfit= ref(0)
|
||||
const telegraph= ref([])
|
||||
const containerRef = ref({})
|
||||
const realtimeProfit = ref(0)
|
||||
const telegraph = ref([])
|
||||
const groupList = ref([])
|
||||
const menuOptions = ref([
|
||||
{
|
||||
label: () =>
|
||||
@ -35,20 +59,299 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'stock',
|
||||
params: {
|
||||
query: {
|
||||
groupName: '全部',
|
||||
groupId: 0,
|
||||
},
|
||||
params: {},
|
||||
}
|
||||
},
|
||||
{ default: () => '股票自选',}
|
||||
{default: () => '股票自选',}
|
||||
),
|
||||
key: 'stock',
|
||||
icon: renderIcon(StarOutline),
|
||||
children:[
|
||||
children: [
|
||||
{
|
||||
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '当日盈亏 '+realtimeProfit.value+"¥"}),
|
||||
key: 'realtimeProfit',
|
||||
show: realtimeProfit.value,
|
||||
icon: renderIcon(WalletOutline),
|
||||
label: () =>
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
href: '#',
|
||||
type: 'info',
|
||||
onClick: () => {
|
||||
//console.log("push",item)
|
||||
router.push({
|
||||
name: 'stock',
|
||||
query: {
|
||||
groupName: '全部',
|
||||
groupId: 0,
|
||||
},
|
||||
})
|
||||
EventsEmit("changeTab", {ID: 0, name: '全部'})
|
||||
},
|
||||
to: {
|
||||
name: 'stock',
|
||||
query: {
|
||||
groupName: '全部',
|
||||
groupId: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
{default: () => '全部',}
|
||||
),
|
||||
key: 0,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
params: {}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
|
||||
},
|
||||
},
|
||||
{default: () => '市场行情'}
|
||||
),
|
||||
key: 'market',
|
||||
icon: renderIcon(NewspaperOutline),
|
||||
children: [
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "市场快讯",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '市场快讯'})
|
||||
},
|
||||
},
|
||||
{default: () => '市场快讯',}
|
||||
),
|
||||
key: 'market1',
|
||||
icon: renderIcon(NewspaperSharp),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "全球股指",
|
||||
},
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '全球股指'})
|
||||
},
|
||||
},
|
||||
{default: () => '全球股指',}
|
||||
),
|
||||
key: 'market2',
|
||||
icon: renderIcon(BarChartSharp),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "指标行情",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '指标行情'})
|
||||
},
|
||||
},
|
||||
{default: () => '指标行情',}
|
||||
),
|
||||
key: 'market3',
|
||||
icon: renderIcon(AnalyticsOutline),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "行业排名",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '行业排名'})
|
||||
},
|
||||
},
|
||||
{default: () => '行业排名',}
|
||||
),
|
||||
key: 'market4',
|
||||
icon: renderIcon(Flag),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "个股资金流向",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '个股资金流向'})
|
||||
},
|
||||
},
|
||||
{default: () => '个股资金流向',}
|
||||
),
|
||||
key: 'market5',
|
||||
icon: renderIcon(Pulse),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "龙虎榜",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '龙虎榜'})
|
||||
},
|
||||
},
|
||||
{default: () => '龙虎榜',}
|
||||
),
|
||||
key: 'market6',
|
||||
icon: renderIcon(Dragon),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "个股研报",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '个股研报'})
|
||||
},
|
||||
},
|
||||
{default: () => '个股研报',}
|
||||
),
|
||||
key: 'market7',
|
||||
icon: renderIcon(StockOutlined),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "公司公告",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '公司公告'})
|
||||
},
|
||||
},
|
||||
{default: () => '公司公告',}
|
||||
),
|
||||
key: 'market8',
|
||||
icon: renderIcon(NotificationFilled),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "行业研究",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '行业研究'})
|
||||
},
|
||||
},
|
||||
{default: () => '行业研究',}
|
||||
),
|
||||
key: 'market9',
|
||||
icon: renderIcon(ReportSearch),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "当前热门",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '当前热门'})
|
||||
},
|
||||
},
|
||||
{default: () => '当前热门',}
|
||||
),
|
||||
key: 'market10',
|
||||
icon: renderIcon(Gripfire),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
href: '#',
|
||||
to: {
|
||||
name: 'market',
|
||||
query: {
|
||||
name: "指标选股",
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
EventsEmit("changeMarketTab", {ID: 0, name: '指标选股'})
|
||||
},
|
||||
},
|
||||
{default: () => '指标选股',}
|
||||
),
|
||||
key: 'market11',
|
||||
icon: renderIcon(BoxSearch20Regular),
|
||||
},
|
||||
]
|
||||
},
|
||||
@ -59,17 +362,17 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'fund',
|
||||
params: {
|
||||
},
|
||||
params: {},
|
||||
}
|
||||
},
|
||||
{ default: () => '基金自选',}
|
||||
{default: () => '基金自选',}
|
||||
),
|
||||
show: enableFund.value,
|
||||
key: 'fund',
|
||||
icon: renderIcon(SparklesOutline),
|
||||
children:[
|
||||
children: [
|
||||
{
|
||||
label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '功能完善中!'}),
|
||||
label: () => h(NText, {type: realtimeProfit.value > 0 ? 'error' : 'success'}, {default: () => '功能完善中!'}),
|
||||
key: 'realtimeProfit',
|
||||
show: realtimeProfit.value,
|
||||
icon: renderIcon(AlarmOutline),
|
||||
@ -83,11 +386,10 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'settings',
|
||||
params: {
|
||||
}
|
||||
params: {}
|
||||
}
|
||||
},
|
||||
{ default: () => '设置' }
|
||||
{default: () => '设置'}
|
||||
),
|
||||
key: 'settings',
|
||||
icon: renderIcon(SettingsOutline),
|
||||
@ -99,30 +401,29 @@ const menuOptions = ref([
|
||||
{
|
||||
to: {
|
||||
name: 'about',
|
||||
params: {
|
||||
}
|
||||
params: {}
|
||||
}
|
||||
},
|
||||
{ default: () => '关于' }
|
||||
{default: () => '关于'}
|
||||
),
|
||||
key: 'about',
|
||||
icon: renderIcon(LogoGithub),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
label: () => h("a", {
|
||||
href: '#',
|
||||
onClick: toggleFullscreen,
|
||||
title: '全屏 Ctrl+F 退出全屏 Esc',
|
||||
}, { default: () => isFullscreen.value?'取消全屏':'全屏' }),
|
||||
}, {default: () => isFullscreen.value ? '取消全屏' : '全屏'}),
|
||||
key: 'full',
|
||||
icon: renderIcon(ExpandOutline),
|
||||
},
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
label: () => h("a", {
|
||||
href: '#',
|
||||
onClick: WindowHide,
|
||||
title: '隐藏到托盘区 Ctrl+H',
|
||||
}, { default: () => '隐藏到托盘区' }),
|
||||
}, {default: () => '隐藏到托盘区'}),
|
||||
key: 'hide',
|
||||
icon: renderIcon(ReorderTwoOutline),
|
||||
},
|
||||
@ -136,28 +437,31 @@ const menuOptions = ref([
|
||||
// icon: renderIcon(MoveOutline),
|
||||
// },
|
||||
{
|
||||
label: ()=> h("a", {
|
||||
label: () => h("a", {
|
||||
href: '#',
|
||||
onClick: Quit,
|
||||
}, { default: () => '退出程序' }),
|
||||
}, {default: () => '退出程序'}),
|
||||
key: 'exit',
|
||||
icon: renderIcon(PowerOutline),
|
||||
},
|
||||
])
|
||||
|
||||
function renderIcon(icon) {
|
||||
return () => h(NIcon, null, { default: () => h(icon) })
|
||||
return () => h(NIcon, null, {default: () => h(icon)})
|
||||
}
|
||||
|
||||
function toggleFullscreen(e) {
|
||||
//console.log(e)
|
||||
if (isFullscreen.value) {
|
||||
WindowUnfullscreen()
|
||||
//e.target.innerHTML = '全屏'
|
||||
} else {
|
||||
WindowFullscreen()
|
||||
// e.target.innerHTML = '取消全屏'
|
||||
}
|
||||
isFullscreen.value=!isFullscreen.value
|
||||
if (isFullscreen.value) {
|
||||
WindowUnfullscreen()
|
||||
//e.target.innerHTML = '全屏'
|
||||
} else {
|
||||
WindowFullscreen()
|
||||
// e.target.innerHTML = '取消全屏'
|
||||
}
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
}
|
||||
|
||||
// const drag = ref(false)
|
||||
// const lastPos= ref({x:0,y:0})
|
||||
// function toggleStartMoveWindow(e) {
|
||||
@ -175,11 +479,29 @@ function toggleFullscreen(e) {
|
||||
// }
|
||||
// window.addEventListener('mousemove', dragstart)
|
||||
|
||||
EventsOn("realtime_profit",(data)=>{
|
||||
realtimeProfit.value=data
|
||||
EventsOn("realtime_profit", (data) => {
|
||||
realtimeProfit.value = data
|
||||
})
|
||||
EventsOn("telegraph",(data)=>{
|
||||
telegraph.value=data
|
||||
EventsOn("telegraph", (data) => {
|
||||
telegraph.value = data
|
||||
})
|
||||
|
||||
EventsOn("loadingMsg", (data) => {
|
||||
if(data==="done"){
|
||||
loadingMsg.value = "加载完成..."
|
||||
EventsEmit("loadingDone", "app")
|
||||
loading.value = false
|
||||
}else{
|
||||
loading.value = true
|
||||
loadingMsg.value = data
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
EventsOff("realtime_profit")
|
||||
EventsOff("loadingMsg")
|
||||
EventsOff("telegraph")
|
||||
EventsOff("newsPush")
|
||||
})
|
||||
|
||||
window.onerror = function (msg, source, lineno, colno, error) {
|
||||
@ -195,89 +517,159 @@ window.onerror = function (msg, source, lineno, colno, error) {
|
||||
return true;
|
||||
};
|
||||
|
||||
onBeforeMount(()=>{
|
||||
// GetConfig().then((res)=>{
|
||||
// console.log(res)
|
||||
// if(res.darkTheme){
|
||||
// enableDarkTheme.value=darkTheme
|
||||
// }
|
||||
// if(res.enableNews){
|
||||
// enableNews.value=true
|
||||
// }
|
||||
// })
|
||||
})
|
||||
onBeforeMount(() => {
|
||||
GetGroupList().then(result => {
|
||||
groupList.value = result
|
||||
menuOptions.value.map((item) => {
|
||||
//console.log(item)
|
||||
if (item.key === 'stock') {
|
||||
item.children.push(...groupList.value.map(item => {
|
||||
return {
|
||||
label: () =>
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
href: '#',
|
||||
type: 'info',
|
||||
onClick: () => {
|
||||
//console.log("push",item)
|
||||
router.push({
|
||||
name: 'stock',
|
||||
query: {
|
||||
groupName: item.name,
|
||||
groupId: item.ID,
|
||||
},
|
||||
})
|
||||
setTimeout(() => {
|
||||
EventsEmit("changeTab", item)
|
||||
}, 100)
|
||||
},
|
||||
to: {
|
||||
name: 'stock',
|
||||
query: {
|
||||
groupName: item.name,
|
||||
groupId: item.ID,
|
||||
},
|
||||
}
|
||||
},
|
||||
{default: () => item.name,}
|
||||
),
|
||||
key: item.ID,
|
||||
}
|
||||
}))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(()=>{
|
||||
contentStyle.value="max-height: calc(90vh);overflow: hidden"
|
||||
GetConfig().then((res)=>{
|
||||
console.log(res)
|
||||
if(res.darkTheme){
|
||||
enableDarkTheme.value=darkTheme
|
||||
}else{
|
||||
enableDarkTheme.value=null
|
||||
}
|
||||
if(res.enableNews){
|
||||
enableNews.value=true
|
||||
|
||||
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 ref="containerRef" :theme="enableDarkTheme" >
|
||||
<n-message-provider >
|
||||
<n-config-provider ref="containerRef" :theme="enableDarkTheme" :locale="zhCN" :date-locale="dateZhCN">
|
||||
<n-message-provider>
|
||||
<n-notification-provider>
|
||||
<n-modal-provider>
|
||||
<n-dialog-provider>
|
||||
|
||||
<n-watermark
|
||||
:content="content"
|
||||
cross
|
||||
selectable
|
||||
:font-size="16"
|
||||
:line-height="16"
|
||||
:width="500"
|
||||
:height="400"
|
||||
:x-offset="50"
|
||||
:y-offset="150"
|
||||
:rotate="-15"
|
||||
>
|
||||
<n-flex justify="center">
|
||||
<n-grid x-gap="12" :cols="1">
|
||||
<!--
|
||||
<n-gi style="position: relative;top:1px;z-index: 19;width: 100%" v-if="telegraph.length>0">
|
||||
|
||||
</n-gi>
|
||||
-->
|
||||
|
||||
<n-gi>
|
||||
<n-marquee :speed="100" style="position: relative;top:0;z-index: 19;width: 100%" v-if="(telegraph.length>0)&&(enableNews)">
|
||||
<n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
|
||||
{{item}}
|
||||
</n-tag>
|
||||
<!-- <n-text type="warning"> {{telegraph[0]}}</n-text>-->
|
||||
</n-marquee>
|
||||
<n-scrollbar :style="contentStyle">
|
||||
<RouterView />
|
||||
</n-scrollbar>
|
||||
</n-gi>
|
||||
|
||||
|
||||
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%;">
|
||||
<n-card size="small" style="--wails-draggable:drag">
|
||||
<n-menu style="font-size: 18px;"
|
||||
v-model:value="activeKey"
|
||||
mode="horizontal"
|
||||
:options="menuOptions"
|
||||
responsive
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-flex>
|
||||
</n-watermark>
|
||||
</n-dialog-provider>
|
||||
</n-modal-provider>
|
||||
<n-modal-provider>
|
||||
<n-dialog-provider>
|
||||
<n-watermark
|
||||
:content="content"
|
||||
cross
|
||||
selectable
|
||||
:font-size="16"
|
||||
:line-height="16"
|
||||
:width="500"
|
||||
:height="400"
|
||||
:x-offset="50"
|
||||
:y-offset="150"
|
||||
:rotate="-15"
|
||||
>
|
||||
<n-flex>
|
||||
<n-grid x-gap="12" :cols="1">
|
||||
<n-gi>
|
||||
<n-spin :show="loading">
|
||||
<template #description>
|
||||
{{ loadingMsg }}
|
||||
</template>
|
||||
<n-marquee :speed="100" style="position: relative;top:0;z-index: 19;width: 100%"
|
||||
v-if="(telegraph.length>0)&&(enableNews)">
|
||||
<n-tag type="warning" v-for="item in telegraph" style="margin-right: 10px">
|
||||
{{ item }}
|
||||
</n-tag>
|
||||
</n-marquee>
|
||||
<n-scrollbar :style="contentStyle">
|
||||
<n-skeleton v-if="loading" height="calc(100vh)" />
|
||||
<RouterView/>
|
||||
</n-scrollbar>
|
||||
</n-spin>
|
||||
</n-gi>
|
||||
<n-gi style="position: fixed;bottom:0;z-index: 9;width: 100%;">
|
||||
<n-card size="small" style="--wails-draggable:drag">
|
||||
<n-menu style="font-size: 18px;"
|
||||
v-model:value="activeKey"
|
||||
mode="horizontal"
|
||||
:options="menuOptions"
|
||||
responsive
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-flex>
|
||||
</n-watermark>
|
||||
</n-dialog-provider>
|
||||
</n-modal-provider>
|
||||
</n-notification-provider>
|
||||
</n-message-provider>
|
||||
</n-config-provider>
|
||||
|
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)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
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,6 +1,6 @@
|
||||
<script setup>
|
||||
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {computed, onBeforeUnmount, onMounted, ref} from "vue";
|
||||
import {
|
||||
AddPrompt, DelPrompt,
|
||||
ExportConfig,
|
||||
@ -44,6 +44,8 @@ const formValue = ref({
|
||||
browserPath: '',
|
||||
enableNews:false,
|
||||
darkTheme:true,
|
||||
enableFund:false,
|
||||
enablePushNews:false,
|
||||
})
|
||||
const promptTemplates=ref([])
|
||||
onMounted(()=>{
|
||||
@ -76,19 +78,24 @@ onMounted(()=>{
|
||||
formValue.value.browserPath = res.browserPath
|
||||
formValue.value.enableNews = res.enableNews
|
||||
formValue.value.darkTheme = res.darkTheme
|
||||
formValue.value.enableFund = res.enableFund
|
||||
formValue.value.enablePushNews = res.enablePushNews
|
||||
|
||||
console.log(res)
|
||||
//console.log(res)
|
||||
})
|
||||
//message.info("加载完成")
|
||||
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
console.log(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,
|
||||
@ -111,12 +118,16 @@ function saveConfig(){
|
||||
enableDanmu:formValue.value.enableDanmu,
|
||||
browserPath:formValue.value.browserPath,
|
||||
enableNews:formValue.value.enableNews,
|
||||
darkTheme:formValue.value.darkTheme
|
||||
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);
|
||||
})
|
||||
}
|
||||
|
||||
@ -157,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 = {
|
||||
@ -186,6 +197,8 @@ function importConfig(){
|
||||
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);
|
||||
@ -195,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",
|
||||
@ -228,14 +241,14 @@ function savePrompt(){
|
||||
AddPrompt(formPrompt.value).then(res=>{
|
||||
message.success(res)
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
console.log(res)
|
||||
//console.log(res)
|
||||
promptTemplates.value=res
|
||||
})
|
||||
showManagePromptsModal.value=false
|
||||
})
|
||||
}
|
||||
function editPrompt(prompt){
|
||||
console.log(prompt)
|
||||
//console.log(prompt)
|
||||
formPrompt.value.ID=prompt.ID
|
||||
formPrompt.value.Name=prompt.name
|
||||
formPrompt.value.Content=prompt.content
|
||||
@ -246,7 +259,7 @@ function deletePrompt(ID){
|
||||
DelPrompt(ID).then(res=>{
|
||||
message.success(res)
|
||||
GetPromptTemplates("","").then(res=>{
|
||||
console.log(res)
|
||||
//console.log(res)
|
||||
promptTemplates.value=res
|
||||
})
|
||||
})
|
||||
@ -254,49 +267,55 @@ function deletePrompt(ID){
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex justify="left" style="margin-top: 12px;padding-left: 12px">
|
||||
<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-grid :cols="24" :x-gap="24" style="text-align: left" :layout-shift-disabled="true">
|
||||
<n-gi :span="24">
|
||||
<n-text type="primary" 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="5" label="暗黑主题(重启生效):" path="darkTheme" >
|
||||
<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="22" label="浏览器路径:" path="browserPath" >
|
||||
<n-input type="text" placeholder="浏览器路径" v-model:value="formValue.browserPath" clearable />
|
||||
<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="primary" 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="5" label="是否显示滚动快讯(重启生效):" path="enableNews" >
|
||||
<n-form-item-gi :span="4" label="显示滚动快讯:" path="enableNews" >
|
||||
<n-switch v-model:value="formValue.enableNews" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="4" label="市场资讯提醒:" path="enablePushNews" >
|
||||
<n-switch v-model:value="formValue.enablePushNews" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="22" v-if="formValue.dingPush.enable" label="钉钉机器人接口地址:" path="dingPush.dingRobot" >
|
||||
<n-input placeholder="请输入钉钉机器人接口地址" v-model:value="formValue.dingPush.dingRobot"/>
|
||||
<n-button type="primary" @click="sendTestNotice">发送测试通知</n-button>
|
||||
@ -305,9 +324,9 @@ function deletePrompt(ID){
|
||||
|
||||
<n-grid :cols="24" :x-gap="24" style="text-align: left;">
|
||||
<n-gi :span="24">
|
||||
<n-text type="primary" 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" >
|
||||
|
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', component: settingsView,name: 'settings' },
|
||||
{ path: '/about', component: about,name: 'about' },
|
||||
{ path: '/market', component: market,name: 'market' },
|
||||
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
|
62
frontend/wailsjs/go/main/App.d.ts
vendored
62
frontend/wailsjs/go/main/App.d.ts
vendored
@ -5,12 +5,22 @@ 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>;
|
||||
@ -21,26 +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 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>;
|
||||
@ -55,6 +109,12 @@ export function SetStockSort(arg1:number,arg2:string):Promise<void>;
|
||||
|
||||
export function ShareAnalysis(arg1:string,arg2:string):Promise<string>;
|
||||
|
||||
export function StockNotice(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function StockResearchReport(arg1:string):Promise<Array<any>>;
|
||||
|
||||
export function SummaryStockNews(arg1:string,arg2:any):Promise<void>;
|
||||
|
||||
export function UnFollow(arg1:string):Promise<string>;
|
||||
|
||||
export function UnFollowFund(arg1:string):Promise<string>;
|
||||
|
@ -6,18 +6,38 @@ 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']();
|
||||
}
|
||||
@ -38,22 +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']();
|
||||
}
|
||||
@ -62,14 +122,58 @@ export function GetfundList(arg1) {
|
||||
return window['go']['main']['App']['GetfundList'](arg1);
|
||||
}
|
||||
|
||||
export function GlobalStockIndexes() {
|
||||
return window['go']['main']['App']['GlobalStockIndexes']();
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function HotEvent(arg1) {
|
||||
return window['go']['main']['App']['HotEvent'](arg1);
|
||||
}
|
||||
|
||||
export function HotStock(arg1) {
|
||||
return window['go']['main']['App']['HotStock'](arg1);
|
||||
}
|
||||
|
||||
export function HotTopic(arg1) {
|
||||
return window['go']['main']['App']['HotTopic'](arg1);
|
||||
}
|
||||
|
||||
export function IndustryResearchReport(arg1) {
|
||||
return window['go']['main']['App']['IndustryResearchReport'](arg1);
|
||||
}
|
||||
|
||||
export function InvestCalendarTimeLine(arg1) {
|
||||
return window['go']['main']['App']['InvestCalendarTimeLine'](arg1);
|
||||
}
|
||||
|
||||
export function LongTigerRank(arg1) {
|
||||
return window['go']['main']['App']['LongTigerRank'](arg1);
|
||||
}
|
||||
|
||||
export function NewChatStream(arg1, arg2, arg3, arg4) {
|
||||
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4);
|
||||
}
|
||||
|
||||
export function NewsPush(arg1) {
|
||||
return window['go']['main']['App']['NewsPush'](arg1);
|
||||
}
|
||||
|
||||
export function ReFleshTelegraphList(arg1) {
|
||||
return window['go']['main']['App']['ReFleshTelegraphList'](arg1);
|
||||
}
|
||||
|
||||
export function RemoveGroup(arg1) {
|
||||
return window['go']['main']['App']['RemoveGroup'](arg1);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@ -78,6 +182,10 @@ export function SaveAsMarkdown(arg1, arg2) {
|
||||
return window['go']['main']['App']['SaveAsMarkdown'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SearchStock(arg1) {
|
||||
return window['go']['main']['App']['SearchStock'](arg1);
|
||||
}
|
||||
|
||||
export function SendDingDingMessage(arg1, arg2) {
|
||||
return window['go']['main']['App']['SendDingDingMessage'](arg1, arg2);
|
||||
}
|
||||
@ -106,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,6 +142,94 @@ 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;
|
||||
@ -155,8 +243,9 @@ export namespace data {
|
||||
// Go type: time
|
||||
Time: any;
|
||||
Sort: number;
|
||||
Cron: string;
|
||||
Cron?: string;
|
||||
IsDel: number;
|
||||
Groups: GroupStock[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FollowedStock(source);
|
||||
@ -177,6 +266,7 @@ export namespace data {
|
||||
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 {
|
||||
@ -198,6 +288,28 @@ export namespace data {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class SentimentResult {
|
||||
Score: number;
|
||||
Category: number;
|
||||
PositiveCount: number;
|
||||
NegativeCount: number;
|
||||
Description: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new SentimentResult(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.Score = source["Score"];
|
||||
this.Category = source["Category"];
|
||||
this.PositiveCount = source["PositiveCount"];
|
||||
this.NegativeCount = source["NegativeCount"];
|
||||
this.Description = source["Description"];
|
||||
}
|
||||
}
|
||||
export class Settings {
|
||||
ID: number;
|
||||
// Go type: time
|
||||
@ -229,6 +341,8 @@ export namespace data {
|
||||
enableNews: boolean;
|
||||
darkTheme: boolean;
|
||||
browserPoolSize: number;
|
||||
enableFund: boolean;
|
||||
enablePushNews: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Settings(source);
|
||||
@ -263,6 +377,8 @@ export namespace data {
|
||||
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 {
|
||||
@ -413,6 +529,7 @@ export namespace data {
|
||||
sort: number;
|
||||
alarmChangePercent: number;
|
||||
alarmPrice: number;
|
||||
Groups: GroupStock[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new StockInfo(source);
|
||||
@ -473,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 {
|
||||
|
21
go.mod
21
go.mod
@ -1,8 +1,6 @@
|
||||
module go-stock
|
||||
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.0
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.1
|
||||
@ -11,14 +9,18 @@ require (
|
||||
github.com/duke-git/lancet/v2 v2.3.4
|
||||
github.com/energye/systray v1.0.2
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-ego/gse v0.80.3
|
||||
github.com/go-resty/resty/v2 v2.16.2
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/robertkrimen/otto v0.5.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samber/lo v1.49.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tidwall/gjson v1.14.2
|
||||
github.com/wailsapp/wails/v2 v2.10.1
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/text v0.22.0
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/text v0.23.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/gorm v1.25.12
|
||||
gorm.io/plugin/dbresolver v1.5.3
|
||||
@ -59,17 +61,20 @@ require (
|
||||
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
|
||||
|
32
go.sum
32
go.sum
@ -26,6 +26,8 @@ github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9g
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-ego/gse v0.80.3 h1:YNFkjMhlhQnUeuoFcUEd1ivh6SOB764rT8GDsEbDiEg=
|
||||
github.com/go-ego/gse v0.80.3/go.mod h1:Gt3A9Ry1Eso2Kza4MRaiZ7f2DTAvActmETY46Lxg0gU=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
|
||||
@ -107,6 +109,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
|
||||
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
@ -115,12 +119,22 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c h1:coVla7zpsycc+kA9NXpcvv2E4I7+ii6L5hZO2S6C3kw=
|
||||
github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
|
||||
github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/vcaesar/cedar v0.20.2 h1:TDx7AdZhilKcfE1WvdToTJf5VrC/FXcUOW+KY1upLZ4=
|
||||
github.com/vcaesar/cedar v0.20.2/go.mod h1:lyuGvALuZZDPNXwpzv/9LyxW+8Y6faN7zauFezNsnik=
|
||||
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
|
||||
github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU=
|
||||
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
|
||||
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
@ -140,8 +154,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@ -159,8 +173,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -185,8 +199,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@ -206,8 +220,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -222,6 +236,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
|
194
main.go
194
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"
|
||||
@ -20,6 +21,7 @@ import (
|
||||
"os"
|
||||
goruntime "runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -55,32 +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{})
|
||||
db.Dao.AutoMigrate(&models.PromptTemplate{})
|
||||
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()
|
||||
@ -123,12 +105,12 @@ func main() {
|
||||
//var width, height int
|
||||
//var err error
|
||||
//
|
||||
//width, height, err = getScreenResolution()
|
||||
//if err != nil {
|
||||
// log.SugaredLogger.Error("get screen resolution error")
|
||||
// width = 1456
|
||||
// height = 768
|
||||
//}
|
||||
width, _, err := getScreenResolution()
|
||||
if err != nil {
|
||||
log.SugaredLogger.Error("get screen resolution error")
|
||||
width = 1456
|
||||
//height = 768
|
||||
}
|
||||
|
||||
darkTheme := data.NewSettingsApi(&data.Settings{}).GetConfig().DarkTheme
|
||||
backgroundColour := &options.RGBA{R: 255, G: 255, B: 255, A: 1}
|
||||
@ -137,12 +119,12 @@ func main() {
|
||||
}
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "go-stock",
|
||||
//Width: width * 4 / 5,
|
||||
//Height: height * 4 / 5,
|
||||
err = wails.Run(&options.App{
|
||||
Title: "go-stock",
|
||||
Width: width * 4 / 5,
|
||||
Height: 900,
|
||||
MinWidth: 1456,
|
||||
MinHeight: 900,
|
||||
MinHeight: 768,
|
||||
//MaxWidth: width,
|
||||
//MaxHeight: height,
|
||||
DisableResize: false,
|
||||
@ -204,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 {
|
||||
@ -212,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 {
|
||||
@ -231,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() {
|
||||
@ -251,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)
|
||||
@ -259,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 {
|
||||
@ -286,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