mirror of
https://github.com/ArvinLovegood/go-stock.git
synced 2025-07-19 00:00:09 +08:00
461 lines
13 KiB
Go
461 lines
13 KiB
Go
//go:build darwin
|
|
// +build darwin
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"go-stock/backend/data"
|
|
"go-stock/backend/db"
|
|
"go-stock/backend/logger"
|
|
"go-stock/backend/models"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
"github.com/coocood/freecache"
|
|
"github.com/duke-git/lancet/v2/convertor"
|
|
"github.com/duke-git/lancet/v2/mathutil"
|
|
"github.com/duke-git/lancet/v2/slice"
|
|
"github.com/go-resty/resty/v2"
|
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
)
|
|
|
|
// App struct
|
|
type App struct {
|
|
ctx context.Context
|
|
cache *freecache.Cache
|
|
}
|
|
|
|
// NewApp creates a new App application struct
|
|
func NewApp() *App {
|
|
cacheSize := 512 * 1024
|
|
cache := freecache.NewCache(cacheSize)
|
|
return &App{
|
|
cache: cache,
|
|
}
|
|
}
|
|
|
|
// startup is called at application startup
|
|
func (a *App) startup(ctx context.Context) {
|
|
logger.SugaredLogger.Infof("Version:%s", Version)
|
|
// Perform your setup here
|
|
a.ctx = ctx
|
|
|
|
// TODO 创建系统托盘
|
|
|
|
}
|
|
|
|
func checkUpdate(a *App) {
|
|
releaseVersion := &models.GitHubReleaseVersion{}
|
|
_, err := resty.New().R().
|
|
SetResult(releaseVersion).
|
|
Get("https://api.github.com/repos/ArvinLovegood/go-stock/releases/latest")
|
|
if err != nil {
|
|
logger.SugaredLogger.Errorf("get github release version error:%s", err.Error())
|
|
return
|
|
}
|
|
logger.SugaredLogger.Infof("releaseVersion:%+v", releaseVersion.TagName)
|
|
if releaseVersion.TagName != Version {
|
|
go runtime.EventsEmit(a.ctx, "updateVersion", releaseVersion)
|
|
}
|
|
}
|
|
|
|
// domReady is called after front-end resources have been loaded
|
|
func (a *App) domReady(ctx context.Context) {
|
|
// Add your action here
|
|
//定时更新数据
|
|
go func() {
|
|
config := data.NewSettingsApi(&data.Settings{}).GetConfig()
|
|
interval := config.RefreshInterval
|
|
if interval <= 0 {
|
|
interval = 1
|
|
}
|
|
ticker := time.NewTicker(time.Second * time.Duration(interval))
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
if isTradingTime(time.Now()) {
|
|
MonitorStockPrices(a)
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
ticker := time.NewTicker(time.Second * time.Duration(60))
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
telegraph := refreshTelegraphList()
|
|
if telegraph != nil {
|
|
go runtime.EventsEmit(a.ctx, "telegraph", telegraph)
|
|
}
|
|
}
|
|
|
|
}()
|
|
go runtime.EventsEmit(a.ctx, "telegraph", refreshTelegraphList())
|
|
go MonitorStockPrices(a)
|
|
|
|
//检查新版本
|
|
go func() {
|
|
checkUpdate(a)
|
|
}()
|
|
}
|
|
|
|
func refreshTelegraphList() *[]string {
|
|
url := "https://www.cls.cn/telegraph"
|
|
response, err := resty.New().R().
|
|
SetHeader("Referer", "https://www.cls.cn/").
|
|
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60").
|
|
Get(fmt.Sprintf(url))
|
|
if err != nil {
|
|
return &[]string{}
|
|
}
|
|
//logger.SugaredLogger.Info(string(response.Body()))
|
|
document, err := goquery.NewDocumentFromReader(strings.NewReader(string(response.Body())))
|
|
if err != nil {
|
|
return &[]string{}
|
|
}
|
|
var telegraph []string
|
|
document.Find("div.telegraph-content-box").Each(func(i int, selection *goquery.Selection) {
|
|
//logger.SugaredLogger.Info(selection.Text())
|
|
telegraph = append(telegraph, selection.Text())
|
|
})
|
|
return &telegraph
|
|
}
|
|
|
|
// isTradingDay 判断是否是交易日
|
|
func isTradingDay(date time.Time) bool {
|
|
weekday := date.Weekday()
|
|
// 判断是否是周末
|
|
if weekday == time.Saturday || weekday == time.Sunday {
|
|
return false
|
|
}
|
|
// 这里可以添加具体的节假日判断逻辑
|
|
// 例如:判断是否是春节、国庆节等
|
|
return true
|
|
}
|
|
|
|
// isTradingTime 判断是否是交易时间
|
|
func isTradingTime(date time.Time) bool {
|
|
if !isTradingDay(date) {
|
|
return false
|
|
}
|
|
|
|
hour, minute, _ := date.Clock()
|
|
|
|
// 判断是否在9:15到11:30之间
|
|
if (hour == 9 && minute >= 15) || (hour == 10) || (hour == 11 && minute <= 30) {
|
|
return true
|
|
}
|
|
|
|
// 判断是否在13:00到15:00之间
|
|
if (hour == 13) || (hour == 14) || (hour == 15 && minute <= 0) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func MonitorStockPrices(a *App) {
|
|
dest := &[]data.FollowedStock{}
|
|
db.Dao.Model(&data.FollowedStock{}).Find(dest)
|
|
total := float64(0)
|
|
//for _, follow := range *dest {
|
|
// stockData := getStockInfo(follow)
|
|
// total += stockData.ProfitAmountToday
|
|
// price, _ := convertor.ToFloat(stockData.Price)
|
|
// if stockData.PrePrice != price {
|
|
// go runtime.EventsEmit(a.ctx, "stock_price", stockData)
|
|
// }
|
|
//}
|
|
|
|
stockInfos := GetStockInfos(*dest...)
|
|
for _, stockInfo := range *stockInfos {
|
|
total += stockInfo.ProfitAmountToday
|
|
price, _ := convertor.ToFloat(stockInfo.Price)
|
|
if stockInfo.PrePrice != price {
|
|
go runtime.EventsEmit(a.ctx, "stock_price", stockInfo)
|
|
}
|
|
}
|
|
if total != 0 {
|
|
// title := "go-stock " + time.Now().Format(time.DateTime) + fmt.Sprintf(" %.2f¥", total)
|
|
// systray.SetTooltip(title)
|
|
}
|
|
|
|
go runtime.EventsEmit(a.ctx, "realtime_profit", fmt.Sprintf(" %.2f", total))
|
|
//runtime.WindowSetTitle(a.ctx, title)
|
|
|
|
}
|
|
func GetStockInfos(follows ...data.FollowedStock) *[]data.StockInfo {
|
|
stockCodes := make([]string, 0)
|
|
for _, follow := range follows {
|
|
stockCodes = append(stockCodes, follow.StockCode)
|
|
}
|
|
stockData, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCodes...)
|
|
if err != nil {
|
|
logger.SugaredLogger.Errorf("get stock code real time data error:%s", err.Error())
|
|
return nil
|
|
}
|
|
stockInfos := make([]data.StockInfo, 0)
|
|
for _, info := range *stockData {
|
|
v, ok := slice.FindBy(follows, func(idx int, follow data.FollowedStock) bool {
|
|
return follow.StockCode == info.Code
|
|
})
|
|
if ok {
|
|
addStockFollowData(v, &info)
|
|
stockInfos = append(stockInfos, info)
|
|
}
|
|
}
|
|
return &stockInfos
|
|
}
|
|
func getStockInfo(follow data.FollowedStock) *data.StockInfo {
|
|
stockCode := follow.StockCode
|
|
stockDatas, err := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
|
|
if err != nil || len(*stockDatas) == 0 {
|
|
return &data.StockInfo{}
|
|
}
|
|
stockData := (*stockDatas)[0]
|
|
addStockFollowData(follow, &stockData)
|
|
return &stockData
|
|
}
|
|
|
|
func addStockFollowData(follow data.FollowedStock, stockData *data.StockInfo) {
|
|
stockData.PrePrice = follow.Price //上次当前价格
|
|
stockData.Sort = follow.Sort
|
|
stockData.CostPrice = follow.CostPrice //成本价
|
|
stockData.CostVolume = follow.Volume //成本量
|
|
stockData.AlarmChangePercent = follow.AlarmChangePercent
|
|
stockData.AlarmPrice = follow.AlarmPrice
|
|
|
|
//当前价格
|
|
price, _ := convertor.ToFloat(stockData.Price)
|
|
//当前价格为0 时 使用卖一价格作为当前价格
|
|
if price == 0 {
|
|
price, _ = convertor.ToFloat(stockData.A1P)
|
|
}
|
|
//当前价格依然为0 时 使用买一报价作为当前价格
|
|
if price == 0 {
|
|
price, _ = convertor.ToFloat(stockData.B1P)
|
|
}
|
|
|
|
//昨日收盘价
|
|
preClosePrice, _ := convertor.ToFloat(stockData.PreClose)
|
|
|
|
//当前价格依然为0 时 使用昨日收盘价为当前价格
|
|
if price == 0 {
|
|
price = preClosePrice
|
|
}
|
|
|
|
//今日最高价
|
|
highPrice, _ := convertor.ToFloat(stockData.High)
|
|
if highPrice == 0 {
|
|
highPrice, _ = convertor.ToFloat(stockData.Open)
|
|
}
|
|
|
|
//今日最低价
|
|
lowPrice, _ := convertor.ToFloat(stockData.Low)
|
|
if lowPrice == 0 {
|
|
lowPrice, _ = convertor.ToFloat(stockData.Open)
|
|
}
|
|
//开盘价
|
|
//openPrice, _ := convertor.ToFloat(stockData.Open)
|
|
|
|
if price > 0 {
|
|
stockData.ChangePrice = mathutil.RoundToFloat(price-preClosePrice, 2)
|
|
stockData.ChangePercent = mathutil.RoundToFloat(mathutil.Div(price-preClosePrice, preClosePrice)*100, 3)
|
|
}
|
|
if highPrice > 0 {
|
|
stockData.HighRate = mathutil.RoundToFloat(mathutil.Div(highPrice-preClosePrice, preClosePrice)*100, 3)
|
|
}
|
|
if lowPrice > 0 {
|
|
stockData.LowRate = mathutil.RoundToFloat(mathutil.Div(lowPrice-preClosePrice, preClosePrice)*100, 3)
|
|
}
|
|
if follow.CostPrice > 0 && follow.Volume > 0 {
|
|
if price > 0 {
|
|
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(price-follow.CostPrice, follow.CostPrice)*100, 3)
|
|
stockData.ProfitAmount = mathutil.RoundToFloat((price-follow.CostPrice)*float64(follow.Volume), 2)
|
|
stockData.ProfitAmountToday = mathutil.RoundToFloat((price-preClosePrice)*float64(follow.Volume), 2)
|
|
} else {
|
|
//未开盘时当前价格为昨日收盘价
|
|
stockData.Profit = mathutil.RoundToFloat(mathutil.Div(preClosePrice-follow.CostPrice, follow.CostPrice)*100, 3)
|
|
stockData.ProfitAmount = mathutil.RoundToFloat((preClosePrice-follow.CostPrice)*float64(follow.Volume), 2)
|
|
// 未开盘时,今日盈亏为 0
|
|
stockData.ProfitAmountToday = 0
|
|
}
|
|
|
|
}
|
|
|
|
//logger.SugaredLogger.Debugf("stockData:%+v", stockData)
|
|
if follow.Price != price && price > 0 {
|
|
go db.Dao.Model(follow).Where("stock_code = ?", follow.StockCode).Updates(map[string]interface{}{
|
|
"price": price,
|
|
})
|
|
}
|
|
}
|
|
|
|
// beforeClose is called when the application is about to quit,
|
|
// either by clicking the window close button or calling runtime.Quit.
|
|
// Returning true will cause the application to continue, false will continue shutdown as normal.
|
|
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
|
|
|
dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
|
|
Type: runtime.QuestionDialog,
|
|
Title: "go-stock",
|
|
Message: "确定关闭吗?",
|
|
Buttons: []string{"确定"},
|
|
Icon: icon,
|
|
CancelButton: "取消",
|
|
})
|
|
|
|
if err != nil {
|
|
logger.SugaredLogger.Errorf("dialog error:%s", err.Error())
|
|
return false
|
|
}
|
|
logger.SugaredLogger.Debugf("dialog:%s", dialog)
|
|
if dialog == "No" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// shutdown is called at application termination
|
|
func (a *App) shutdown(ctx context.Context) {
|
|
// Perform your teardown here
|
|
// systray.Quit()
|
|
}
|
|
|
|
// Greet returns a greeting for the given name
|
|
func (a *App) Greet(stockCode string) *data.StockInfo {
|
|
//stockInfo, _ := data.NewStockDataApi().GetStockCodeRealTimeData(stockCode)
|
|
|
|
follow := &data.FollowedStock{
|
|
StockCode: stockCode,
|
|
}
|
|
db.Dao.Model(follow).Where("stock_code = ?", stockCode).First(follow)
|
|
stockInfo := getStockInfo(*follow)
|
|
return stockInfo
|
|
}
|
|
|
|
func (a *App) Follow(stockCode string) string {
|
|
return data.NewStockDataApi().Follow(stockCode)
|
|
}
|
|
|
|
func (a *App) UnFollow(stockCode string) string {
|
|
return data.NewStockDataApi().UnFollow(stockCode)
|
|
}
|
|
|
|
func (a *App) GetFollowList() []data.FollowedStock {
|
|
return data.NewStockDataApi().GetFollowList()
|
|
}
|
|
|
|
func (a *App) GetStockList(key string) []data.StockBasic {
|
|
return data.NewStockDataApi().GetStockList(key)
|
|
}
|
|
|
|
func (a *App) SetCostPriceAndVolume(stockCode string, price float64, volume int64) string {
|
|
return data.NewStockDataApi().SetCostPriceAndVolume(price, volume, stockCode)
|
|
}
|
|
|
|
func (a *App) SetAlarmChangePercent(val, alarmPrice float64, stockCode string) string {
|
|
return data.NewStockDataApi().SetAlarmChangePercent(val, alarmPrice, stockCode)
|
|
}
|
|
func (a *App) SetStockSort(sort int64, stockCode string) {
|
|
data.NewStockDataApi().SetStockSort(sort, stockCode)
|
|
}
|
|
func (a *App) SendDingDingMessage(message string, stockCode string) string {
|
|
ttl, _ := a.cache.TTL([]byte(stockCode))
|
|
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
|
|
if ttl > 0 {
|
|
return ""
|
|
}
|
|
err := a.cache.Set([]byte(stockCode), []byte("1"), 60*5)
|
|
if err != nil {
|
|
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
|
|
return ""
|
|
}
|
|
return data.NewDingDingAPI().SendDingDingMessage(message)
|
|
}
|
|
|
|
// SendDingDingMessageByType msgType 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
|
|
func (a *App) SendDingDingMessageByType(message string, stockCode string, msgType int) string {
|
|
ttl, _ := a.cache.TTL([]byte(stockCode))
|
|
logger.SugaredLogger.Infof("stockCode %s ttl:%d", stockCode, ttl)
|
|
if ttl > 0 {
|
|
return ""
|
|
}
|
|
err := a.cache.Set([]byte(stockCode), []byte("1"), getMsgTypeTTL(msgType))
|
|
if err != nil {
|
|
logger.SugaredLogger.Errorf("set cache error:%s", err.Error())
|
|
return ""
|
|
}
|
|
stockInfo := &data.StockInfo{}
|
|
db.Dao.Model(stockInfo).Where("code = ?", stockCode).First(stockInfo)
|
|
go data.NewAlertWindowsApi("go-stock消息通知", getMsgTypeName(msgType), GenNotificationMsg(stockInfo), "").SendNotification()
|
|
return data.NewDingDingAPI().SendDingDingMessage(message)
|
|
}
|
|
|
|
func (a *App) NewChat(stock string) string {
|
|
return data.NewDeepSeekOpenAi().NewChat(stock)
|
|
}
|
|
|
|
func (a *App) NewChatStream(stock, stockCode string) {
|
|
msgs := data.NewDeepSeekOpenAi().NewChatStream(stock, stockCode)
|
|
for msg := range msgs {
|
|
runtime.EventsEmit(a.ctx, "newChatStream", msg)
|
|
}
|
|
runtime.EventsEmit(a.ctx, "newChatStream", "DONE")
|
|
}
|
|
|
|
func GenNotificationMsg(stockInfo *data.StockInfo) string {
|
|
Price, err := convertor.ToFloat(stockInfo.Price)
|
|
if err != nil {
|
|
Price = 0
|
|
}
|
|
PreClose, err := convertor.ToFloat(stockInfo.PreClose)
|
|
if err != nil {
|
|
PreClose = 0
|
|
}
|
|
var RF float64
|
|
if PreClose > 0 {
|
|
RF = mathutil.RoundToFloat(((Price-PreClose)/PreClose)*100, 2)
|
|
}
|
|
|
|
return "[" + stockInfo.Name + "] " + stockInfo.Price + " " + convertor.ToString(RF) + "% " + stockInfo.Date + " " + stockInfo.Time
|
|
}
|
|
|
|
// msgType : 1 涨跌报警(5分钟);2 股价报警(30分钟) 3 成本价报警(30分钟)
|
|
func getMsgTypeTTL(msgType int) int {
|
|
switch msgType {
|
|
case 1:
|
|
return 60 * 5
|
|
case 2:
|
|
return 60 * 30
|
|
case 3:
|
|
return 60 * 30
|
|
default:
|
|
return 60 * 5
|
|
}
|
|
}
|
|
|
|
func getMsgTypeName(msgType int) string {
|
|
switch msgType {
|
|
case 1:
|
|
return "涨跌报警"
|
|
case 2:
|
|
return "股价报警"
|
|
case 3:
|
|
return "成本价报警"
|
|
default:
|
|
return "未知类型"
|
|
}
|
|
}
|
|
|
|
func (a *App) UpdateConfig(settings *data.Settings) string {
|
|
logger.SugaredLogger.Infof("UpdateConfig:%+v", settings)
|
|
return data.NewSettingsApi(settings).UpdateConfig()
|
|
}
|
|
|
|
func (a *App) GetConfig() *data.Settings {
|
|
return data.NewSettingsApi(&data.Settings{}).GetConfig()
|
|
}
|