├── .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
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 |