├── .gitignore ├── .travis.yml ├── README.md ├── const.go ├── go.mod ├── go.sum ├── main.go └── render.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | /.idea/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.19.1 4 | script: 5 | - go build && ./vlight 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vlight![](https://travis-ci.com/pinple/vlight.svg?token=ATFZGq5q9tbJu6KjZsyy&branch=master) 2 | 基金涨跌监控,每天14:30定时抓取基金行情,如果超过自己设置的阈值,则发邮件给自己,根据行情做相应的加仓/建仓动作。解放你的注意力,避免因为频繁查看行情而**影响自己的情绪**,从而冲动操作,买基金最重要就是:选好一支基金并且长期持有。 3 | # 创作动机 4 | [如何在理财中胜出](https://pinple.me/post/rational-investment/) 5 | # 如何构建 6 | ## 一、无服务器 7 | 使用[Travis](https://www.travis-ci.com/)构建,在Travis后台设置邮箱的环境变量,EMAIL_NAME和EMAIL_PASSWORD,然后将构建时间设置为【每天】。 8 | ## 二、有自己的服务器 9 | 建cron任务。 -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | FundJsUrl = "http://fundgz.1234567.com.cn/js/" 5 | FundHTMLUrl = "http://fund.eastmoney.com/" 6 | MIN_RISE_NUM = 0.1 7 | MAX_FALL_NUM = -0.1 8 | 9 | DailyTitle = ` 10 | 11 | 基金名称 12 | 估算涨幅 13 | 当前估算净值 14 | 昨日单位净值 15 | 估算时间 16 | 17 | ` 18 | WeeklyTitle = ` 19 | 20 | 基金名称 21 | 近1周净值变化 22 | 23 | ` 24 | MonthlyTitle = ` 25 | 26 | 基金名称 27 | 近1月净值变化 28 | 29 | ` 30 | ) 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pinple/vlight 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/PuerkitoBio/goquery v1.8.0 7 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df 8 | ) 9 | 10 | require ( 11 | github.com/andybalholm/cascadia v1.3.1 // indirect 12 | golang.org/x/net v0.33.0 // indirect 13 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= 2 | github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= 3 | github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= 4 | github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= 5 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 6 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 7 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 8 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 9 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 10 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 11 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 12 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 13 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= 14 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= 15 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= 16 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/PuerkitoBio/goquery" 7 | "gopkg.in/gomail.v2" 8 | "io" 9 | "log" 10 | "net/http" 11 | "os" 12 | "regexp" 13 | "strings" 14 | ) 15 | 16 | var ( 17 | fundCodeStr, watchWeekDay, watchMonthDay, emailName, emailPassword string 18 | ) 19 | var ( 20 | fundCodeSlice []string 21 | ) 22 | 23 | func init() { 24 | fundCodeStr = os.Getenv("WATCH_FUND_CODE") 25 | //fundCodeStr = "005827,012414,003095,161005" 26 | fundCodeSlice = strings.Split(fundCodeStr, ",") 27 | watchWeekDay = os.Getenv("WATCH_WEEK_DAY") 28 | watchMonthDay = os.Getenv("WATCH_MONTH_DAY") 29 | emailName = os.Getenv("EMAIL_NAME") 30 | emailPassword = os.Getenv("EMAIL_PASSWORD") 31 | } 32 | 33 | func fetchFund(codes []string) []map[string]string { 34 | var fundResult []map[string]string 35 | var weeklyChange string 36 | var oneMonthChange string 37 | fmt.Printf("codes: %#v\n", codes) 38 | for _, code := range codes { 39 | fundJsUrl := FundJsUrl + code + ".js" 40 | resp1, err := http.Get(fundJsUrl) 41 | if err != nil { 42 | panic(err) 43 | } 44 | defer resp1.Body.Close() 45 | 46 | fundHTMLUrl := FundHTMLUrl + code + ".html" 47 | fmt.Printf("fundHTMLUrl: %s\n", string(fundHTMLUrl)) 48 | resp2, err := http.Get(fundHTMLUrl) 49 | if err != nil { 50 | panic(err) 51 | } 52 | defer resp2.Body.Close() 53 | 54 | re, _ := regexp.Compile("jsonpgz\\((.*)\\);") 55 | data, err := io.ReadAll(resp1.Body) 56 | if err != nil { 57 | panic(err) 58 | } 59 | ret := re.FindSubmatch(data) 60 | fundData := ret[1] 61 | fmt.Printf("fundData: %s\n", string(fundData)) 62 | 63 | doc, err2 := goquery.NewDocumentFromReader(resp2.Body) 64 | if err != nil { 65 | log.Fatal(err2) 66 | } 67 | doc.Find("#increaseAmount_stage > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(2)").Each(func(i int, s *goquery.Selection) { 68 | s.Find("td > div").Each(func(j int, k *goquery.Selection) { 69 | change := k.Text() 70 | switch j { 71 | case 1: 72 | weeklyChange = change 73 | case 2: 74 | oneMonthChange = change 75 | } 76 | }) 77 | }) 78 | var fundDataMap map[string]string 79 | if err := json.Unmarshal(fundData, &fundDataMap); err == nil { 80 | fundDataMap["weeklyChange"] = weeklyChange 81 | fundDataMap["oneMonthChange"] = oneMonthChange 82 | fundResult = append(fundResult, fundDataMap) 83 | } 84 | } 85 | 86 | fmt.Printf("fundResult: %#v\n", fundResult) 87 | return fundResult 88 | } 89 | 90 | func sendEmail(content string) { 91 | if content == "" { 92 | return 93 | } 94 | m := gomail.NewMessage() 95 | m.SetHeader("From", emailName) 96 | m.SetHeader("To", emailName) 97 | m.SetHeader("Subject", "基金涨跌监控") 98 | m.SetBody("text/html", content) 99 | d := gomail.NewDialer("smtp.qq.com", 587, emailName, emailPassword) 100 | if err := d.DialAndSend(m); err != nil { 101 | panic(err) 102 | } 103 | } 104 | 105 | func main() { 106 | fundResult := fetchFund(fundCodeSlice) 107 | content := renderHTML(fundResult) 108 | sendEmail(content) 109 | } 110 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | func renderHTML(fundResult []map[string]string) string { 10 | var dailyElements []string 11 | var dailyContent string 12 | var weeklyElements []string 13 | var weeklyContent string 14 | var oneMonthElements []string 15 | var oneMonthContent string 16 | var dailyText string 17 | var weeklyText string 18 | var oneMonthText string 19 | now := time.Now() 20 | for _, fund := range fundResult { 21 | gszzl, err := strconv.ParseFloat(fund["gszzl"], 32) 22 | if err != nil { 23 | panic(err) 24 | } 25 | if gszzl > 0 { 26 | fund["gszzl"] = "+" + strconv.FormatFloat(gszzl, 'f', -1, 32) 27 | } 28 | // 每日涨幅,涨跌幅度超出设定值才发出通知 29 | if (gszzl > 0 && gszzl >= MIN_RISE_NUM) || (gszzl < 0 && gszzl <= MAX_FALL_NUM) { 30 | dailyElement := ` 31 | 32 | ` + fund["name"] + ` 33 | ` + fund["gszzl"] + `% 34 | ` + fund["gsz"] + ` 35 | ` + fund["dwjz"] + ` 36 | ` + fund["gztime"] + ` 37 | 38 | ` 39 | dailyElements = append(dailyElements, dailyElement) 40 | } 41 | // 一周涨幅 42 | if now.Weekday().String() == watchWeekDay { 43 | weeklyElement := ` 44 | 45 | ` + fund["name"] + ` 46 | ` + fund["weeklyChange"] + ` 47 | 48 | ` 49 | weeklyElements = append(weeklyElements, weeklyElement) 50 | } 51 | // 月度涨幅 52 | monthNum, err := strconv.Atoi(watchMonthDay) 53 | if now.Day() == monthNum { 54 | oneMonthElement := ` 55 | 56 | ` + fund["name"] + ` 57 | ` + fund["oneMonthChange"] + ` 58 | 59 | ` 60 | oneMonthElements = append(oneMonthElements, oneMonthElement) 61 | } 62 | } 63 | dailyContent = strings.Join(dailyElements, "\n") 64 | weeklyContent = strings.Join(weeklyElements, "\n") 65 | oneMonthContent = strings.Join(oneMonthElements, "\n") 66 | if dailyContent != "" || weeklyContent != "" || oneMonthContent != "" { 67 | if dailyContent != "" { 68 | dailyText = ` 69 | 70 | ` + DailyTitle + dailyContent + ` 71 |


` 72 | } 73 | if weeklyContent != "" { 74 | weeklyText = ` 75 | 76 | ` + WeeklyTitle + weeklyContent + ` 77 |


` 78 | } 79 | if oneMonthContent != "" { 80 | oneMonthText = ` 81 | 82 | ` + MonthlyTitle + oneMonthContent + ` 83 |


` 84 | } 85 | html := ` 86 | 87 | 88 | 89 | 90 | 91 |
92 |

基金涨跌监控:

93 |
94 | ` + dailyText + weeklyText + oneMonthText + ` 95 |
96 |
97 | 98 | ` 99 | 100 | return html 101 | } 102 | 103 | return "" 104 | } 105 | --------------------------------------------------------------------------------