diff --git a/app.go b/app.go index d216045..7dee559 100644 --- a/app.go +++ b/app.go @@ -11,6 +11,7 @@ import ( "github.com/duke-git/lancet/v2/convertor" "github.com/duke-git/lancet/v2/mathutil" "github.com/duke-git/lancet/v2/slice" + "github.com/duke-git/lancet/v2/strutil" "github.com/getlantern/systray" "github.com/go-resty/resty/v2" "github.com/wailsapp/wails/v2/pkg/runtime" @@ -104,9 +105,8 @@ func (a *App) domReady(ctx context.Context) { ticker := time.NewTicker(time.Second * time.Duration(interval)) defer ticker.Stop() for range ticker.C { - if isTradingTime(time.Now()) || IsHKTradingTime(time.Now()) { - MonitorStockPrices(a) - } + MonitorStockPrices(a) + } }() @@ -229,6 +229,35 @@ func IsHKTradingTime(date time.Time) bool { return false } +// IsUSTradingTime 判断当前时间是否在美股交易时间内 +func IsUSTradingTime(date time.Time) bool { + // 获取美国东部时区 + est, err := time.LoadLocation("America/New_York") + if err != nil { + logger.SugaredLogger.Errorf("加载时区失败: %s", err.Error()) + return false + } + + // 将当前时间转换为美国东部时间 + estTime := date.In(est) + + // 判断是否是周末 + weekday := estTime.Weekday() + if weekday == time.Saturday || weekday == time.Sunday { + return false + } + + // 获取小时和分钟 + hour, minute, _ := estTime.Clock() + + // 判断是否在9:30到16:00之间 + if (hour == 9 && minute >= 30) || (hour >= 10 && hour < 16) || (hour == 16 && minute == 0) { + return true + } + + return false +} + func MonitorStockPrices(a *App) { dest := &[]data.FollowedStock{} db.Dao.Model(&data.FollowedStock{}).Find(dest) @@ -244,6 +273,16 @@ func MonitorStockPrices(a *App) { stockInfos := GetStockInfos(*dest...) for _, stockInfo := range *stockInfos { + if strutil.HasPrefixAny(stockInfo.Code, []string{"SZ", "SH", "sh", "sz"}) && (!isTradingTime(time.Now())) { + continue + } + if strutil.HasPrefixAny(stockInfo.Code, []string{"hk", "HK"}) && (!IsHKTradingTime(time.Now())) { + continue + } + if strutil.HasPrefixAny(stockInfo.Code, []string{"us", "US", "gb_"}) && (!IsUSTradingTime(time.Now())) { + continue + } + total += stockInfo.ProfitAmountToday price, _ := convertor.ToFloat(stockInfo.Price) if stockInfo.PrePrice != price { diff --git a/app_test.go b/app_test.go index 458fab9..5e9c47b 100644 --- a/app_test.go +++ b/app_test.go @@ -13,3 +13,7 @@ func TestIsHKTradingTime(t *testing.T) { f := IsHKTradingTime(time.Now()) t.Log(f) } + +func TestIsUSTradingTime(t *testing.T) { + t.Log(IsUSTradingTime(time.Now())) +} diff --git a/backend/data/crawler_api_test.go b/backend/data/crawler_api_test.go index df3d5fe..c8692f2 100644 --- a/backend/data/crawler_api_test.go +++ b/backend/data/crawler_api_test.go @@ -2,12 +2,14 @@ package data import ( "context" + "encoding/json" "fmt" "github.com/PuerkitoBio/goquery" "github.com/duke-git/lancet/v2/strutil" "go-stock/backend/db" "go-stock/backend/logger" "go-stock/backend/models" + "os" "strings" "testing" "time" @@ -166,3 +168,143 @@ func TestHk(t *testing.T) { } } + +func TestUpdateUSName(t *testing.T) { + db.Init("../../data/stock.db") + us := &[]models.StockInfoUS{} + db.Dao.Model(&models.StockInfoUS{}).Where("name = ?", "").Order("RANDOM()").Find(us) + + for _, us := range *us { + crawlerAPI := CrawlerApi{} + crawlerBaseInfo := CrawlerBaseInfo{ + Name: "TestCrawler", + Description: "Test Crawler Description", + BaseUrl: "https://stock.finance.sina.com.cn", + Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"}, + } + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo) + + url := fmt.Sprintf("https://stock.finance.sina.com.cn/usstock/quotes/%s.html", us.Code[:len(us.Code)-3]) + logger.SugaredLogger.Infof("url: %s", url) + //waitVisible := "span.quote_title_name" + waitVisible := "div.hq_title > h1" + + htmlContent, ok := crawlerAPI.GetHtml(url, waitVisible, true) + + if !ok { + continue + } + //logger.SugaredLogger.Infof("htmlContent: %s", htmlContent) + document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent)) + if err != nil { + logger.SugaredLogger.Error(err.Error()) + } + name := "" + document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) { + name = strutil.RemoveNonPrintable(selection.Text()) + name = strutil.SplitAndTrim(name, " ", "")[0] + logger.SugaredLogger.Infof("股票名称-:%s", name) + }) + db.Dao.Model(&models.StockInfoUS{}).Where("code = ?", us.Code).Updates(map[string]interface{}{ + "name": name, + "full_name": name, + }) + } + +} +func TestUS(t *testing.T) { + db.Init("../../data/stock.db") + bytes, err := os.ReadFile("../../build/us.json") + if err != nil { + return + } + crawlerAPI := CrawlerApi{} + crawlerBaseInfo := CrawlerBaseInfo{ + Name: "TestCrawler", + Description: "Test Crawler Description", + BaseUrl: "https://quote.eastmoney.com", + Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"}, + } + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute) + defer cancel() + crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo) + + tick := &Tick{} + json.Unmarshal(bytes, &tick) + for i, datum := range tick.Data { + logger.SugaredLogger.Infof("datum: %d, %+v", i, datum) + name := "" + + //https://quote.eastmoney.com/us/AAPL.html + //https://stock.finance.sina.com.cn/usstock/quotes/goog.html + //url := fmt.Sprintf("https://stock.finance.sina.com.cn/usstock/quotes/%s.html", strings.ReplaceAll(datum.C, ".US", "")) + ////waitVisible := "span.quote_title_name" + //waitVisible := "div.hq_title > h1" + // + //htmlContent, ok := crawlerAPI.GetHtml(url, waitVisible, true) + // + //if !ok { + // continue + //} + ////logger.SugaredLogger.Infof("htmlContent: %s", htmlContent) + //document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent)) + //if err != nil { + // logger.SugaredLogger.Error(err.Error()) + //} + //document.Find(waitVisible).Each(func(i int, selection *goquery.Selection) { + // name = strutil.RemoveNonPrintable(selection.Text()) + // name = strutil.SplitAndTrim(name, " ", "")[0] + // logger.SugaredLogger.Infof("股票名称-:%s", name) + //}) + + us := &models.StockInfoUS{ + Code: datum.C + ".US", + EName: datum.N, + FullName: datum.N, + Name: name, + Exchange: datum.E, + Type: datum.T, + } + db.Dao.Create(us) + } +} + +func TestUSSINA(t *testing.T) { + //https://finance.sina.com.cn/stock/usstock/sector.shtml#cm + crawlerAPI := CrawlerApi{} + crawlerBaseInfo := CrawlerBaseInfo{ + Name: "TestCrawler", + Description: "Test Crawler Description", + BaseUrl: "https://quote.eastmoney.com", + Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"}, + } + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute) + defer cancel() + crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo) + + html, ok := crawlerAPI.GetHtml("https://finance.sina.com.cn/stock/usstock/sector.shtml#cm", "div#data", false) + if !ok { + return + } + document, err := goquery.NewDocumentFromReader(strings.NewReader(html)) + if err != nil { + logger.SugaredLogger.Error(err.Error()) + } + document.Find("div#data > table >tbody >tr").Each(func(i int, selection *goquery.Selection) { + tr := selection.Text() + logger.SugaredLogger.Infof("tr: %s", tr) + }) +} + +type Tick struct { + Code int `json:"code"` + Status string `json:"status"` + Data []struct { + C string `json:"c"` + N string `json:"n"` + T string `json:"t"` + E string `json:"e"` + } `json:"data"` +} diff --git a/backend/data/openai_api.go b/backend/data/openai_api.go index a8f4122..c92cae0 100644 --- a/backend/data/openai_api.go +++ b/backend/data/openai_api.go @@ -449,6 +449,9 @@ func SearchGuShiTongStockInfo(stock string, crawlTimeOut int64) *[]string { if strutil.HasPrefixAny(stock, []string{"SZ", "SH", "sh", "sz"}) { url = "https://gushitong.baidu.com/stock/ab-" + RemoveAllNonDigitChar(stock) } + if strutil.HasPrefixAny(stock, []string{"us", "US", "gb_", "gb"}) { + url = "https://gushitong.baidu.com/stock/us-" + strings.Replace(stock, "gb_", "", 1) + } logger.SugaredLogger.Infof("SearchGuShiTongStockInfo搜索股票-%s: %s", stock, url) actions := []chromedp.Action{ @@ -483,6 +486,10 @@ func GetFinancialReports(stockCode string, crawlTimeOut int64) *[]string { 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_", "") + } // 创建一个 chromedp 上下文 timeoutCtx, timeoutCtxCancel := context.WithTimeout(context.Background(), time.Duration(crawlTimeOut)*time.Second) diff --git a/backend/data/openai_api_test.go b/backend/data/openai_api_test.go index c8aac93..17b7285 100644 --- a/backend/data/openai_api_test.go +++ b/backend/data/openai_api_test.go @@ -25,5 +25,6 @@ func TestGetTopNewsList(t *testing.T) { func TestSearchGuShiTongStockInfo(t *testing.T) { SearchGuShiTongStockInfo("hk01810", 60) SearchGuShiTongStockInfo("sh600745", 60) + SearchGuShiTongStockInfo("gb_goog", 60) } diff --git a/backend/data/stock_data_api.go b/backend/data/stock_data_api.go index d88e21f..89c6329 100644 --- a/backend/data/stock_data_api.go +++ b/backend/data/stock_data_api.go @@ -283,11 +283,24 @@ func (receiver StockDataApi) GetStockBaseInfo() { } func (receiver StockDataApi) GetStockCodeRealTimeData(StockCodes ...string) (*[]StockInfo, error) { + + codes := slice.JoinFunc(StockCodes, ",", func(s string) string { + if strings.HasPrefix(s, "us") { + s = strings.Replace(s, "us", "gb_", 1) + } + if strings.HasPrefix(s, "US") { + s = strings.Replace(s, "US", "gb_", 1) + } + return strings.ToLower(s) + }) + + url := fmt.Sprintf(sinaStockUrl, time.Now().Unix(), codes) + //logger.SugaredLogger.Infof("GetStockCodeRealTimeData %s", url) resp, err := receiver.client.R(). SetHeader("Host", "hq.sinajs.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/119.0.0.0 Safari/537.36 Edg/119.0.0.0"). - Get(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), slice.Join(StockCodes, ","))) + Get(url) if err != nil { logger.SugaredLogger.Error(err.Error()) return &[]StockInfo{}, err @@ -391,6 +404,9 @@ func (receiver StockDataApi) GetStockList(key string) []StockBasic { var result3 []models.StockInfoHK db.Dao.Model(&models.StockInfoHK{}).Where("name like ? or code like ?", "%"+key+"%", "%"+key+"%").Find(&result3) + var result4 []models.StockInfoUS + db.Dao.Model(&models.StockInfoUS{}).Where("name like ? or code like ?", "%"+key+"%", "%"+key+"%").Find(&result4) + for _, item := range result2 { result = append(result, StockBasic{ TsCode: item.TsCode, @@ -409,6 +425,14 @@ func (receiver StockDataApi) GetStockList(key string) []StockBasic { Market: "HK", }) } + for _, item := range result4 { + result = append(result, StockBasic{ + TsCode: item.Code, + Name: item.Name, + Fullname: item.Name, + Market: "US", + }) + } return result } @@ -436,6 +460,9 @@ func ParseFullSingleStockData(data string) (*StockInfo, error) { if strutil.ContainsAny(datas[0], []string{"hq_str_hk"}) { result, err = ParseHKStockData(datas) } + if strutil.ContainsAny(datas[0], []string{"hq_str_gb"}) { + result, err = ParseUSStockData(datas) + } //logger.SugaredLogger.Infof("股票数据解析完成: %v", result) marshal, err := json.Marshal(result) @@ -452,6 +479,65 @@ func ParseFullSingleStockData(data string) (*StockInfo, error) { return stockInfo, nil } +func ParseUSStockData(datas []string) (map[string]string, error) { + code := strings.Split(datas[0], "hq_str_")[1] + result := make(map[string]string) + parts := strutil.SplitAndTrim(datas[1], ",", "\"", ";") + //parts := strings.Split(data, ",") + if len(parts) < 30 { + return nil, fmt.Errorf("invalid data format") + } + /* + 谷歌, 0 + 170.2100, 1 现价 + -2.57, 2 涨跌幅 + 2025-02-28 09:38:50, 3 时间 + -4.4900, 4 涨跌额 + 175.9400, 5 今日开盘价 + 176.5900, 6 区间 + 169.7520, 7 区间 + 208.7000, 8 52周区间 + 130.9500, 9 52周区间 + 25930485, 10 成交量 + 17083496, 11 10日均量 + 2074859900000, 12 市值 + 8.13, 13 每股收益 + 20.940000 , 14 市盈率 + 0.00, 15 + 0.00, 16 + 0.20, 17 + 0.00, 18 + 12190000000, 19 + 71, 20 + 170.2000, 21 盘后 + -0.01, 22 + -0.01, 23 + Feb 27 07:59PM EST, 24 + Feb 27 04:00PM EST, 25 + 174.7000, 26 前收盘 + 2917444, 27 + 1, 28 + 2025, 29 + 4456143849.0000, 30 + 176.1200, 31 + 163.7039, 32 + 496605933.1411, 33 + 170.2100, 34 现价 + 174.7000 35 前收盘 + */ + result["股票代码"] = code + result["股票名称"] = parts[0] + result["今日开盘价"] = parts[5] + result["昨日收盘价"] = parts[26] + result["今日最高价"] = parts[6] + result["今日最低价"] = parts[7] + result["当前价格"] = parts[1] + result["日期"] = strutil.SplitAndTrim(parts[3], " ", "")[0] + result["时间"] = strutil.SplitAndTrim(parts[3], " ", "")[1] + logger.SugaredLogger.Infof("美股股票数据解析完成: %v", result) + return result, nil +} + func ParseHKStockData(datas []string) (map[string]string, error) { code := strings.Split(datas[0], "hq_str_")[1] result := make(map[string]string) diff --git a/backend/data/stock_data_api_test.go b/backend/data/stock_data_api_test.go index b2a6a24..c191b44 100644 --- a/backend/data/stock_data_api_test.go +++ b/backend/data/stock_data_api_test.go @@ -26,14 +26,15 @@ func TestGetTelegraph(t *testing.T) { } func TestGetFinancialReports(t *testing.T) { - GetFinancialReports("sz000802", 30) + //GetFinancialReports("sz000802", 30) //GetFinancialReports("hk00927", 30) + GetFinancialReports("gb_aapl", 30) } func TestGetTelegraphSearch(t *testing.T) { //url := "https://www.cls.cn/searchPage?keyword=%E9%97%BB%E6%B3%B0%E7%A7%91%E6%8A%80&type=telegram" - messages := SearchStockInfo("新 希 望", "telegram", 30) + messages := SearchStockInfo("谷歌", "telegram", 30) for _, message := range *messages { logger.SugaredLogger.Info(message) } @@ -82,7 +83,7 @@ func TestParseFullSingleStockData(t *testing.T) { SetHeader("Host", "hq.sinajs.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/119.0.0.0 Safari/537.36 Edg/119.0.0.0"). - Get(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), "sh600584,sz000938,hk01810,hk00856")) + Get(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), "sh600584,sz000938,hk01810,hk00856,gb_aapl")) if err != nil { logger.SugaredLogger.Error(err.Error()) } diff --git a/backend/data/utils.go b/backend/data/utils.go index d596428..1c4ebb7 100644 --- a/backend/data/utils.go +++ b/backend/data/utils.go @@ -17,7 +17,7 @@ var SensitiveWords = strings.Split("戊边、戍边、戌边、边防、李鹏 func ReplaceSensitiveWords(text string) string { for _, word := range SensitiveWords { if strings.Contains(text, word) { - text = strings.ReplaceAll(text, word, "*") + text = strings.ReplaceAll(text, word, "") } } return text diff --git a/backend/models/models.go b/backend/models/models.go index 5c5f270..c76406a 100644 --- a/backend/models/models.go +++ b/backend/models/models.go @@ -176,6 +176,21 @@ func (receiver StockInfoHK) TableName() string { return "stock_base_info_hk" } +type StockInfoUS struct { + gorm.Model + Code string `json:"code"` + Name string `json:"name"` + FullName string `json:"fullName"` + EName string `json:"eName"` + Exchange string `json:"exchange"` + Type string `json:"type"` + IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"` +} + +func (receiver StockInfoUS) TableName() string { + return "stock_base_info_us" +} + type Resp struct { Code int `json:"code"` Message string `json:"message"` diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 896591e..b25998b 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -10,12 +10,12 @@ import { } from '../wailsjs/runtime' import {h, ref} from "vue"; import { RouterLink } from 'vue-router' -import {darkTheme, NIcon, NText,} from 'naive-ui' +import {darkTheme, NGradientText, NIcon, NText,} from 'naive-ui' import { SettingsOutline, ReorderTwoOutline, ExpandOutline, - PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline, + PowerOutline, LogoGithub, MoveOutline, WalletOutline, StarOutline, AlarmOutline, SparklesOutline, } from '@vicons/ionicons5' const content = ref('数据来源于网络,仅供参考;投资有风险,入市需谨慎') const isFullscreen = ref(false) @@ -36,7 +36,7 @@ const menuOptions = ref([ }, } }, - { default: () => '我的自选',} + { default: () => '股票自选',} ), key: 'stock', icon: renderIcon(StarOutline), @@ -49,6 +49,29 @@ const menuOptions = ref([ }, ] }, + { + label: () => + h( + NGradientText, + { + type: 'warning', + style: { + 'text-decoration': 'line-through', + } + }, + { default: () => '基金自选' } + ), + key: 'fund', + icon: renderIcon(SparklesOutline), + children:[ + { + label: ()=> h(NText, {type:realtimeProfit.value>0?'error':'success'},{ default: () => '敬请期待!'}), + key: 'realtimeProfit', + show: realtimeProfit.value, + icon: renderIcon(AlarmOutline), + }, + ] + }, { label: () => h( diff --git a/frontend/src/components/stock.vue b/frontend/src/components/stock.vue index 00e2f5a..f31fc4d 100644 --- a/frontend/src/components/stock.vue +++ b/frontend/src/components/stock.vue @@ -465,7 +465,7 @@ function search(code,name){ //window.open("https://www.cls.cn/stock?code="+code) //window.open("https://quote.eastmoney.com/"+code+".html") //window.open("https://finance.sina.com.cn/realstock/company/"+code+"/nc.shtml") - window.open("https://www.iwencai.com/unifiedwap/result?w="+code) + window.open("https://www.iwencai.com/unifiedwap/result?w="+name) //window.open("https://www.iwencai.com/chat/?question="+code) }, 500) } @@ -486,16 +486,34 @@ function showFenshi(code,name){ data.code=code data.name=name data.fenshiURL='http://image.sinajs.cn/newchart/min/n/'+data.code+'.gif'+"?t="+Date.now() + + if(code.startsWith('hk')){ + data.fenshiURL='http://image.sinajs.cn/newchart/hk_stock/min/'+data.code.replace("hk","")+'.gif'+"?t="+Date.now() + } + if(code.startsWith('gb_')){ + data.fenshiURL='http://image.sinajs.cn/newchart/usstock/min/'+data.code.replace("gb_","")+'.gif'+"?t="+Date.now() + } + modalShow2.value=true } function showK(code,name){ data.code=code data.name=name data.kURL='http://image.sinajs.cn/newchart/daily/n/'+data.code+'.gif'+"?t="+Date.now() + if(code.startsWith('hk')){ + data.kURL='http://image.sinajs.cn/newchart/hk_stock/daily/'+data.code.replace("hk","")+'.gif'+"?t="+Date.now() + } + if(code.startsWith('gb_')){ + data.kURL='http://image.sinajs.cn/newchart/usstock/daily/'+data.code.replace("gb_","")+'.gif'+"?t="+Date.now() + } + //https://image.sinajs.cn/newchart/usstock/daily/dji.gif + //https://image.sinajs.cn/newchart/hk_stock/daily/06030.gif?1740729404273 modalShow3.value=true } + + function updateCostPriceAndVolumeNew(code,price,volume,alarm,formModel){ if(formModel.sort){ diff --git a/main.go b/main.go index fff7f8e..be1a729 100644 --- a/main.go +++ b/main.go @@ -59,6 +59,7 @@ func main() { db.Dao.AutoMigrate(&data.Settings{}) db.Dao.AutoMigrate(&models.AIResponseResult{}) db.Dao.AutoMigrate(&models.StockInfoHK{}) + db.Dao.AutoMigrate(&models.StockInfoUS{}) if stocksBin != nil && len(stocksBin) > 0 { go initStockData()