\d{6}),.+?,(?P.+?),(?P.+?),"`)
49 | if err != nil {
50 | logging.Error(ctx, "regexp error:"+err.Error())
51 | return nil, err
52 | }
53 | matched := reg.FindAllStringSubmatch(strresp, -1)
54 | for _, m := range matched {
55 | results = append(results, SearchFundInfo{
56 | Code: m[1],
57 | Name: m[2],
58 | Type: m[3],
59 | })
60 | }
61 | return
62 | }
63 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/fund_search_test.go:
--------------------------------------------------------------------------------
1 | package eastmoney
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestSearchFund(t *testing.T) {
10 | results, err := _em.SearchFund(_ctx, "半导体")
11 | require.Nil(t, err)
12 | t.Log(results)
13 | }
14 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/historical_pe_list.go:
--------------------------------------------------------------------------------
1 | // 获取历史市盈率
2 |
3 | package eastmoney
4 |
5 | import (
6 | "context"
7 | "errors"
8 | "strconv"
9 | "time"
10 |
11 | "github.com/axiaoxin-com/goutils"
12 | "github.com/axiaoxin-com/logging"
13 | "go.uber.org/zap"
14 | )
15 |
16 | // RespHistoricalPE 历史市盈率接口返回结构
17 | type RespHistoricalPE struct {
18 | Data [][]struct {
19 | Securitycode string `json:"SECURITYCODE"`
20 | Datetype string `json:"DATETYPE"`
21 | Sl string `json:"SL"`
22 | Endate string `json:"ENDATE"`
23 | Value string `json:"VALUE"`
24 | } `json:"data"`
25 | Pe [][]struct {
26 | Securitycode string `json:"SECURITYCODE"`
27 | Pe30 string `json:"PE30"`
28 | Pe50 string `json:"PE50"`
29 | Pe70 string `json:"PE70"`
30 | Total string `json:"TOTAL"`
31 | Rn1 string `json:"RN1"`
32 | Rn2 string `json:"RN2"`
33 | Rn3 string `json:"RN3"`
34 | } `json:"pe"`
35 | }
36 |
37 | // HistoricalPE 历史 pe
38 | type HistoricalPE struct {
39 | Value float64
40 | Date string
41 | }
42 |
43 | // HistoricalPEList 历史 pe 列表
44 | type HistoricalPEList []HistoricalPE
45 |
46 | // GetMidValue 获取历史 pe 中位数
47 | func (h HistoricalPEList) GetMidValue(ctx context.Context) (float64, error) {
48 | values := []float64{}
49 | for _, i := range h {
50 | values = append(values, i.Value)
51 | }
52 | return goutils.MidValueFloat64(values)
53 | }
54 |
55 | // QueryHistoricalPEList 获取历史市盈率
56 | func (e EastMoney) QueryHistoricalPEList(ctx context.Context, secuCode string) (HistoricalPEList, error) {
57 | apiurl := "https://emfront.eastmoney.com/APP_HSF10/CPBD/GZFX"
58 | params := map[string]string{
59 | "code": e.GetFC(secuCode),
60 | "year": "4", // 10 年
61 | "type": "1", // 市盈率
62 | }
63 | logging.Debug(ctx, "EastMoney QueryHistoricalPEList "+apiurl+" begin", zap.Any("params", params))
64 | beginTime := time.Now()
65 | apiurl, err := goutils.NewHTTPGetURLWithQueryString(ctx, apiurl, params)
66 | if err != nil {
67 | return nil, err
68 | }
69 | resp := RespHistoricalPE{}
70 | err = goutils.HTTPGET(ctx, e.HTTPClient, apiurl, nil, &resp)
71 | latency := time.Now().Sub(beginTime).Milliseconds()
72 | logging.Debug(
73 | ctx,
74 | "EastMoney QueryHistoricalPEList "+apiurl+" end",
75 | zap.Int64("latency(ms)", latency),
76 | // zap.Any("resp", resp),
77 | )
78 | if err != nil {
79 | return nil, err
80 | }
81 | result := HistoricalPEList{}
82 | if len(resp.Data) == 0 {
83 | return nil, errors.New("no historical pe data")
84 | }
85 | for _, i := range resp.Data[0] {
86 | value, err := strconv.ParseFloat(i.Value, 64)
87 | if err != nil {
88 | logging.Error(ctx, "QueryHistoricalPEList ParseFloat error:"+err.Error())
89 | continue
90 | }
91 | pe := HistoricalPE{
92 | Date: i.Endate,
93 | Value: value,
94 | }
95 | result = append(result, pe)
96 | }
97 | return result, nil
98 | }
99 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/historical_pe_list_test.go:
--------------------------------------------------------------------------------
1 | package eastmoney
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestPEGetMidValue(t *testing.T) {
10 | d := HistoricalPEList{
11 | HistoricalPE{Date: "1", Value: 6.0},
12 | HistoricalPE{Date: "1", Value: 1.0},
13 | HistoricalPE{Date: "1", Value: 5.0},
14 | HistoricalPE{Date: "1", Value: 2.0},
15 | HistoricalPE{Date: "1", Value: 4.0},
16 | HistoricalPE{Date: "1", Value: 3.0},
17 | }
18 | m, err := d.GetMidValue(_ctx)
19 | require.Nil(t, err)
20 | require.Equal(t, 3.5, m)
21 | }
22 |
23 | func TestQueryHistoricalPEList(t *testing.T) {
24 | d, err := _em.QueryHistoricalPEList(_ctx, "600149.sh")
25 | require.Nil(t, err)
26 | t.Log(d)
27 | }
28 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/hs300.go:
--------------------------------------------------------------------------------
1 | // 沪深300成分股
2 |
3 | package eastmoney
4 |
5 | import (
6 | "context"
7 | "fmt"
8 |
9 | "github.com/axiaoxin-com/goutils"
10 | "github.com/corpix/uarand"
11 | )
12 |
13 | // HS300Item 沪深300成分股信息
14 | type HS300Item struct {
15 | Secucode string `json:"SECUCODE"` // 股票代码.XX
16 | SecurityCode string `json:"SECURITY_CODE"` // 股票代码
17 | SecurityNameAbbr string `json:"SECURITY_NAME_ABBR"` // 股票简称
18 | ClosePrice float64 `json:"CLOSE_PRICE"` // 最新价格
19 | Industry string `json:"INDUSTRY"` // 主营行业
20 | Region string `json:"REGION"` // 地区
21 | Weight float64 `json:"WEIGHT"` // 持仓比例(%)
22 | Eps float64 `json:"EPS"` // 每股收益
23 | Bps float64 `json:"BPS"` // 每股净资产
24 | Roe float64 `json:"ROE"` // 净资产收益率
25 | TotalShares float64 `json:"TOTAL_SHARES"` // 总股本(亿股)
26 | FreeShares float64 `json:"FREE_SHARES"` // 流通股本(亿股)
27 | FreeCap float64 `json:"FREE_CAP"` // 流通市值(亿元)
28 | Type string `json:"TYPE"`
29 | F2 interface{} `json:"f2"`
30 | F3 interface{} `json:"f3"`
31 | }
32 |
33 | // RspHS300 HS300接口返回结构
34 | type RspHS300 struct {
35 | Version string `json:"version"`
36 | Result struct {
37 | Pages int `json:"pages"`
38 | Data []HS300Item `json:"data"`
39 | Count int `json:"count"`
40 | } `json:"result"`
41 | Success bool `json:"success"`
42 | Message string `json:"message"`
43 | Code int `json:"code"`
44 | }
45 |
46 | // HS300 返回沪深300成分股列表
47 | func (e EastMoney) HS300(ctx context.Context) (results []HS300Item, err error) {
48 | apiurl := "https://datacenter-web.eastmoney.com/api/data/v1/get?sortColumns=ROE&sortTypes=-1&pageSize=300&pageNumber=1&reportName=RPT_INDEX_TS_COMPONENT&columns=SECUCODE%2CSECURITY_CODE%2CTYPE%2CSECURITY_NAME_ABBR%2CCLOSE_PRICE%2CINDUSTRY%2CREGION%2CWEIGHT%2CEPS%2CBPS%2CROE%2CTOTAL_SHARES%2CFREE_SHARES%2CFREE_CAP"eColumns=f2%2Cf3"eType=0&source=WEB&client=WEB&filter=(TYPE%3D%221%22)"
49 | header := map[string]string{
50 | "user-agent": uarand.GetRandom(),
51 | }
52 | rsp := RspHS300{}
53 | if err := goutils.HTTPGET(ctx, e.HTTPClient, apiurl, header, &rsp); err != nil {
54 | return nil, err
55 | }
56 | if rsp.Code != 0 {
57 | return nil, fmt.Errorf("HS300 rsp code error, rsp:%+v", rsp)
58 | }
59 | if len(rsp.Result.Data) != 300 {
60 | return nil, fmt.Errorf("HS300 rsp data len != 300, len=%d", len(rsp.Result.Data))
61 | }
62 | return rsp.Result.Data, nil
63 | }
64 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/hs300_test.go:
--------------------------------------------------------------------------------
1 | package eastmoney
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestHS300(t *testing.T) {
10 | results, err := _em.HS300(_ctx)
11 | require.Nil(t, err)
12 | t.Log(results)
13 | }
14 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/index.go:
--------------------------------------------------------------------------------
1 | // 指数信息
2 |
3 | package eastmoney
4 |
5 | import (
6 | "context"
7 | "fmt"
8 |
9 | "github.com/axiaoxin-com/goutils"
10 | "github.com/corpix/uarand"
11 | )
12 |
13 | // IndexData 指数信息
14 | type IndexData struct {
15 | IndexCode string `json:"IndexCode"`
16 | IndexName string `json:"IndexName"`
17 | Newindextexch string `json:"NEWINDEXTEXCH"`
18 | FullIndexName string `json:"FullIndexName"`
19 | NewPrice string `json:"NewPrice"` // 指数点数
20 | NewPriceDate string `json:"NewPriceDate"`
21 | NewCHG string `json:"NewCHG"` // 最新涨幅
22 | Reaprofile string `json:"reaprofile"` // 指数说明
23 | MakerName string `json:"MakerName"` // 指数编制方
24 | Bkid string `json:"BKID"`
25 | BKName string `json:"BKName"` // 板块名称
26 | IsGuess bool `json:"IsGuess"`
27 | IndexvaluaCN string `json:"IndexvaluaCN"` // 估值:低估=-2,较为低估=-1,适中=0,较为高估=1,高估=2
28 | Petim string `json:"Petim"` // 估值PE值
29 | Pep100 string `json:"PEP100"` // 估值PE百分位
30 | Pb string `json:"PB"`
31 | Pbp100 string `json:"PBP100"`
32 | W string `json:"W"` // 近一周涨幅
33 | M string `json:"M"` // 近一月涨幅
34 | Q string `json:"Q"` // 近三月涨幅
35 | Hy string `json:"HY"` // 近六月涨幅
36 | Y string `json:"Y"` // 近一年涨幅
37 | Twy string `json:"TWY"` // 近两年涨幅
38 | Try string `json:"TRY"` // 近三年涨幅
39 | Fy string `json:"FY"` // 近五年涨幅
40 | Sy string `json:"SY"` // 今年来涨幅
41 | StddevW string `json:"STDDEV_W"`
42 | StddevM string `json:"STDDEV_M"`
43 | StddevQ string `json:"STDDEV_Q"`
44 | StddevHy string `json:"STDDEV_HY"`
45 | StddevY string `json:"STDDEV_Y"`
46 | StddevTwy string `json:"STDDEV_TWY"`
47 | PDate string `json:"PDate"`
48 | TopicJJBID interface{} `json:"TopicJJBId"`
49 | Isstatic string `json:"ISSTATIC"`
50 | }
51 |
52 | // IndexValueCN 指数估值
53 | func (i *IndexData) IndexValueCN() string {
54 | switch i.IndexvaluaCN {
55 | case "-2":
56 | return "低估"
57 | case "-1":
58 | return "较为低估"
59 | case "0":
60 | return "适中"
61 | case "1":
62 | return "较为高估"
63 | case "2":
64 | return "高估"
65 | }
66 | return "--"
67 | }
68 |
69 | // RspIndex Index接口返回结构
70 | type RspIndex struct {
71 | Datas IndexData `json:"Datas"`
72 | ErrCode int `json:"ErrCode"`
73 | Success bool `json:"Success"`
74 | ErrMsg interface{} `json:"ErrMsg"`
75 | Message interface{} `json:"Message"`
76 | ErrorCode string `json:"ErrorCode"`
77 | ErrorMessage interface{} `json:"ErrorMessage"`
78 | ErrorMsgLst interface{} `json:"ErrorMsgLst"`
79 | TotalCount int `json:"TotalCount"`
80 | Expansion interface{} `json:"Expansion"`
81 | }
82 |
83 | // Index 返回指数信息
84 | func (e EastMoney) Index(ctx context.Context, indexCode string) (data *IndexData, err error) {
85 | apiurl := fmt.Sprintf(
86 | "https://fundztapi.eastmoney.com/FundSpecialApiNew/FundSpecialZSB30ZSIndex?IndexCode=%s&Version=6.5.5&deviceid=-&pageIndex=1&pageSize=10000&plat=Iphone&product=EFund",
87 | indexCode,
88 | )
89 | header := map[string]string{
90 | "user-agent": uarand.GetRandom(),
91 | }
92 | rsp := RspIndex{}
93 | if err := goutils.HTTPGET(ctx, e.HTTPClient, apiurl, header, &rsp); err != nil {
94 | return nil, err
95 | }
96 | if rsp.ErrCode != 0 {
97 | return nil, fmt.Errorf("Index rsp code error, rsp:%+v", rsp)
98 | }
99 | return &rsp.Datas, nil
100 | }
101 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/index_test.go:
--------------------------------------------------------------------------------
1 | package eastmoney
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestIndex(t *testing.T) {
10 | data, err := _em.Index(_ctx, "000905")
11 | require.Nil(t, err)
12 | t.Log(data)
13 | }
14 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/industry_list.go:
--------------------------------------------------------------------------------
1 | // 获取选股器中的行业列表数据
2 |
3 | package eastmoney
4 |
5 | import (
6 | "context"
7 | "fmt"
8 | "time"
9 |
10 | "github.com/axiaoxin-com/goutils"
11 | "github.com/axiaoxin-com/logging"
12 | "go.uber.org/zap"
13 | )
14 |
15 | // RespIndustryList 接口返回的 json 结构
16 | type RespIndustryList struct {
17 | Result struct {
18 | Count int `json:"count"`
19 | Pages int `json:"pages"`
20 | Data []struct {
21 | Industry string `json:"INDUSTRY"`
22 | FirstLetter string `json:"FIRST_LETTER"`
23 | } `json:"data"`
24 | } `json:"result"`
25 | Success bool `json:"success"`
26 | Message string `json:"message"`
27 | Code int `json:"code"`
28 | }
29 |
30 | // QueryIndustryList 获取行业列表
31 | func (e EastMoney) QueryIndustryList(ctx context.Context) ([]string, error) {
32 | apiurl := "https://datacenter.eastmoney.com/stock/selection/api/data/get/"
33 | reqData := map[string]string{
34 | "source": "SELECT_SECURITIES",
35 | "client": "APP",
36 | "type": "RPTA_APP_INDUSTRY",
37 | "sty": "ALL",
38 | }
39 | logging.Debug(ctx, "EastMoney IndustryList "+apiurl+" begin", zap.Any("reqData", reqData))
40 | beginTime := time.Now()
41 | req, err := goutils.NewHTTPMultipartReq(ctx, apiurl, reqData)
42 | if err != nil {
43 | return nil, err
44 | }
45 | resp := RespIndustryList{}
46 | err = goutils.HTTPPOST(ctx, e.HTTPClient, req, &resp)
47 | latency := time.Now().Sub(beginTime).Milliseconds()
48 | logging.Debug(ctx, "EastMoney IndustryList "+apiurl+" end",
49 | zap.Int64("latency(ms)", latency),
50 | // zap.Any("resp", resp),
51 | )
52 | if err != nil {
53 | return nil, err
54 | }
55 | if resp.Code != 0 {
56 | return nil, fmt.Errorf("%#v", resp)
57 | }
58 | result := []string{}
59 | for _, i := range resp.Result.Data {
60 | result = append(result, i.Industry)
61 | }
62 | return result, nil
63 | }
64 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/industry_list_test.go:
--------------------------------------------------------------------------------
1 | package eastmoney
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestIndustryList(t *testing.T) {
10 | data, err := _em.QueryIndustryList(_ctx)
11 | require.Nil(t, err)
12 | require.Len(t, data, 105)
13 | }
14 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/jiazhipinggu.go:
--------------------------------------------------------------------------------
1 | // 获取智能诊股中的价值评估
2 |
3 | package eastmoney
4 |
5 | import (
6 | "context"
7 | "fmt"
8 | "strings"
9 | "time"
10 |
11 | "github.com/axiaoxin-com/goutils"
12 | "github.com/axiaoxin-com/logging"
13 | "go.uber.org/zap"
14 | )
15 |
16 | // JZPG 价值评估
17 | type JZPG struct {
18 | // 股票名
19 | Secname string `json:"SecName"`
20 | // 行业名
21 | Industryname string `json:"IndustryName"`
22 | Type string `json:"Type"`
23 | // 当前排名
24 | Valueranking string `json:"ValueRanking"`
25 | // 排名总数
26 | Total string `json:"Total"`
27 | // 整体质地
28 | Valuetotalscore string `json:"ValueTotalScore"`
29 | Reportdate string `json:"ReportDate"`
30 | Reporttype string `json:"ReportType"`
31 | // 盈利能力
32 | Profitabilityscore string `json:"ProfitabilityScore"`
33 | // 成长能力
34 | Growupscore string `json:"GrowUpScore"`
35 | // 营运偿债能力
36 | Operationscore string `json:"OperationScore"`
37 | // 现金流
38 | Cashflowscore string `json:"CashFlowScore"`
39 | // 估值
40 | Valuationscore string `json:"ValuationScore"`
41 | }
42 |
43 | // GetValueRanking 当前排名
44 | func (j JZPG) GetValueRanking() string {
45 | return strings.Split(j.Valueranking, "|")[0]
46 | }
47 |
48 | // GetProfitabilityScore 盈利能力
49 | func (j JZPG) GetProfitabilityScore() string {
50 | return strings.Split(j.Profitabilityscore, "|")[0]
51 | }
52 |
53 | // GetGrowUpScore 成长能力
54 | func (j JZPG) GetGrowUpScore() string {
55 | return strings.Split(j.Growupscore, "|")[0]
56 | }
57 |
58 | // GetOperationScore 营运偿债能力
59 | func (j JZPG) GetOperationScore() string {
60 | return strings.Split(j.Operationscore, "|")[0]
61 | }
62 |
63 | // GetCashFlowScore 现金流能力
64 | func (j JZPG) GetCashFlowScore() string {
65 | return strings.Split(j.Cashflowscore, "|")[0]
66 | }
67 |
68 | // GetValuationScore 估值能力
69 | func (j JZPG) GetValuationScore() string {
70 | return strings.Split(j.Valuationscore, "|")[0]
71 | }
72 |
73 | // GetValueTotalScore 整体质地
74 | func (j JZPG) GetValueTotalScore() string {
75 | return strings.Split(j.Valuetotalscore, "|")[0]
76 | }
77 |
78 | func (j JZPG) String() string {
79 | return fmt.Sprintf(
80 | "%s属于%s行业,排名%s/%s。\n盈利能力%s,成长能力%s,营运偿债能力%s,现金流%s,估值%s,整体质地%s。",
81 | j.Secname,
82 | j.Industryname,
83 | j.GetValueRanking(),
84 | j.Total,
85 | j.GetProfitabilityScore(),
86 | j.GetGrowUpScore(),
87 | j.GetOperationScore(),
88 | j.GetCashFlowScore(),
89 | j.GetValuationScore(),
90 | j.GetValueTotalScore(),
91 | )
92 | }
93 |
94 | // RespJiaZhiPingGu 综合评价接口返回结构
95 | type RespJiaZhiPingGu struct {
96 | Result struct {
97 | JiazhipingguGaiyao JZPG `json:"JiaZhiPingGu_GaiYao"`
98 | JiazhipingguWuweitulist []struct {
99 | Reportdate string `json:"ReportDate"`
100 | Reporttype string `json:"ReportType"`
101 | Profitabilityscore string `json:"ProfitabilityScore"`
102 | Growupscore string `json:"GrowUpScore"`
103 | Operationscore string `json:"OperationScore"`
104 | Cashflowscore string `json:"CashFlowScore"`
105 | Valuationscore string `json:"ValuationScore"`
106 | } `json:"JiaZhiPingGu_WuWeiTuList"`
107 | } `json:"Result"`
108 | Status int `json:"Status"`
109 | Message string `json:"Message"`
110 | Otherinfo struct {
111 | } `json:"OtherInfo"`
112 | }
113 |
114 | // QueryJiaZhiPingGu 返回智能诊股中的价值评估
115 | func (e EastMoney) QueryJiaZhiPingGu(ctx context.Context, secuCode string) (JZPG, error) {
116 | fc := e.GetFC(secuCode)
117 | apiurl := "https://emstockdiag.eastmoney.com/api/ZhenGuShouYe/GetJiaZhiPingGu"
118 | reqData := map[string]interface{}{
119 | "fc": fc,
120 | }
121 | logging.Debug(ctx, "EastMoney QueryJiaZhiPingGu "+apiurl+" begin", zap.Any("reqData", reqData))
122 | beginTime := time.Now()
123 | req, err := goutils.NewHTTPJSONReq(ctx, apiurl, reqData)
124 | if err != nil {
125 | return JZPG{}, err
126 | }
127 | resp := RespJiaZhiPingGu{}
128 | err = goutils.HTTPPOST(ctx, e.HTTPClient, req, &resp)
129 | latency := time.Now().Sub(beginTime).Milliseconds()
130 | logging.Debug(
131 | ctx,
132 | "EastMoney QueryJiaZhiPingGu "+apiurl+" end",
133 | zap.Int64("latency(ms)", latency),
134 | // zap.Any("resp", resp),
135 | )
136 | if err != nil {
137 | return JZPG{}, err
138 | }
139 | if resp.Status != 0 {
140 | return JZPG{}, fmt.Errorf("%s %#v", secuCode, resp.Message)
141 | }
142 | return resp.Result.JiazhipingguGaiyao, nil
143 | }
144 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/jiazhipinggu_test.go:
--------------------------------------------------------------------------------
1 | package eastmoney
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestQueryJiaZhiPingGu(t *testing.T) {
10 | data, err := _em.QueryJiaZhiPingGu(_ctx, "002291.sz")
11 | require.Nil(t, err)
12 | t.Logf("%+v", data)
13 | }
14 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/org_rating.go:
--------------------------------------------------------------------------------
1 | // 获取机构评级统计
2 |
3 | package eastmoney
4 |
5 | import (
6 | "context"
7 | "fmt"
8 | "strings"
9 | "time"
10 |
11 | "github.com/axiaoxin-com/goutils"
12 | "github.com/axiaoxin-com/logging"
13 | "go.uber.org/zap"
14 | )
15 |
16 | // RespOrgRating 统计评级接口返回结构
17 | type RespOrgRating struct {
18 | Version string `json:"version"`
19 | Result struct {
20 | Pages int `json:"pages"`
21 | Data []OrgRating `json:"data"`
22 | Count int `json:"count"`
23 | } `json:"result"`
24 | Success bool `json:"success"`
25 | Message string `json:"message"`
26 | Code int `json:"code"`
27 | }
28 |
29 | // OrgRating 机构评级统计
30 | type OrgRating struct {
31 | // 时间段
32 | DateType string `json:"DATE_TYPE"`
33 | // 综合评级
34 | CompreRating string `json:"COMPRE_RATING"`
35 | }
36 |
37 | // OrgRatingList 评级列表
38 | type OrgRatingList []OrgRating
39 |
40 | // String 字符串输出
41 | func (o OrgRatingList) String() string {
42 | s := []string{}
43 | for _, i := range o {
44 | s = append(s, fmt.Sprintf("%s:%s", i.DateType, i.CompreRating))
45 | }
46 | return strings.Join(s, "
")
47 | }
48 |
49 | // QueryOrgRating 获取评级统计
50 | func (e EastMoney) QueryOrgRating(ctx context.Context, secuCode string) (OrgRatingList, error) {
51 | apiurl := "https://datacenter.eastmoney.com/securities/api/data/get"
52 | params := map[string]string{
53 | "source": "SECURITIES",
54 | "client": "APP",
55 | "type": "RPT_RES_ORGRATING",
56 | "sty": "DATE_TYPE,COMPRE_RATING",
57 | "filter": fmt.Sprintf(`(SECUCODE="%s")`, strings.ToUpper(secuCode)),
58 | "sr": "1",
59 | "st": "DATE_TYPE_CODE",
60 | }
61 | logging.Debug(ctx, "EastMoney QueryOrgRating "+apiurl+" begin", zap.Any("params", params))
62 | beginTime := time.Now()
63 | apiurl, err := goutils.NewHTTPGetURLWithQueryString(ctx, apiurl, params)
64 | if err != nil {
65 | return nil, err
66 | }
67 | resp := RespOrgRating{}
68 | err = goutils.HTTPGET(ctx, e.HTTPClient, apiurl, nil, &resp)
69 | latency := time.Now().Sub(beginTime).Milliseconds()
70 | logging.Debug(ctx, "EastMoney QueryOrgRating "+apiurl+" end",
71 | zap.Int64("latency(ms)", latency),
72 | // zap.Any("resp", resp),
73 | )
74 | if err != nil {
75 | return nil, err
76 | }
77 | if resp.Code != 0 {
78 | return nil, fmt.Errorf("%s %#v", secuCode, resp)
79 | }
80 | return resp.Result.Data, nil
81 | }
82 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/org_rating_test.go:
--------------------------------------------------------------------------------
1 | package eastmoney
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestQueryOrgRating(t *testing.T) {
10 | data, err := _em.QueryOrgRating(_ctx, "002459.sz")
11 | require.Nil(t, err)
12 | require.Len(t, data, 3)
13 | }
14 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/profit_predict.go:
--------------------------------------------------------------------------------
1 | // 获取盈利预测
2 |
3 | package eastmoney
4 |
5 | import (
6 | "context"
7 | "fmt"
8 | "strings"
9 | "time"
10 |
11 | "github.com/axiaoxin-com/goutils"
12 | "github.com/axiaoxin-com/logging"
13 | "go.uber.org/zap"
14 | )
15 |
16 | // RespProfitPredict 盈利预测接口返回结构
17 | type RespProfitPredict struct {
18 | Version string `json:"version"`
19 | Result struct {
20 | Pages int `json:"pages"`
21 | Data []ProfitPredict `json:"data"`
22 | Count int `json:"count"`
23 | } `json:"result"`
24 | Success bool `json:"success"`
25 | Message string `json:"message"`
26 | Code int `json:"code"`
27 | }
28 |
29 | // ProfitPredict 盈利预测
30 | type ProfitPredict struct {
31 | // 年份
32 | PredictYear int `json:"PREDICT_YEAR"`
33 | // 预测每股收益
34 | Eps float64 `json:"EPS"`
35 | // 预测市盈率
36 | Pe float64 `json:"PE"`
37 | }
38 |
39 | // ProfitPredictList 预测列表
40 | type ProfitPredictList []ProfitPredict
41 |
42 | func (p ProfitPredictList) String() string {
43 | s := []string{}
44 | for _, i := range p {
45 | s = append(s, fmt.Sprintf("%d | 预测每股收益:%f 预测市盈率:%f", i.PredictYear, i.Eps, i.Pe))
46 | }
47 | return strings.Join(s, "
")
48 | }
49 |
50 | // QueryProfitPredict 获取盈利预测
51 | func (e EastMoney) QueryProfitPredict(ctx context.Context, secuCode string) (ProfitPredictList, error) {
52 | apiurl := "https://datacenter.eastmoney.com/securities/api/data/get"
53 | params := map[string]string{
54 | "source": "SECURITIES",
55 | "client": "APP",
56 | "type": "RPT_RES_PROFITPREDICT",
57 | "sty": "PREDICT_YEAR,EPS,PE",
58 | "filter": fmt.Sprintf(`(SECUCODE="%s")`, strings.ToUpper(secuCode)),
59 | "sr": "1",
60 | "st": "PREDICT_YEAR",
61 | }
62 | logging.Debug(ctx, "EastMoney QueryProfitPredict "+apiurl+" begin", zap.Any("params", params))
63 | beginTime := time.Now()
64 | apiurl, err := goutils.NewHTTPGetURLWithQueryString(ctx, apiurl, params)
65 | if err != nil {
66 | return nil, err
67 | }
68 | resp := RespProfitPredict{}
69 | err = goutils.HTTPGET(ctx, e.HTTPClient, apiurl, nil, &resp)
70 | latency := time.Now().Sub(beginTime).Milliseconds()
71 | logging.Debug(ctx, "EastMoney QueryProfitPredict "+apiurl+" end",
72 | zap.Int64("latency(ms)", latency),
73 | // zap.Any("resp", resp),
74 | )
75 | if err != nil {
76 | return nil, err
77 | }
78 | if resp.Code != 0 {
79 | return nil, fmt.Errorf("%s %#v", secuCode, resp)
80 | }
81 | return resp.Result.Data, nil
82 | }
83 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/profit_predict_test.go:
--------------------------------------------------------------------------------
1 | package eastmoney
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestQueryProfitPredict(t *testing.T) {
10 | data, err := _em.QueryProfitPredict(_ctx, "002459.sz")
11 | require.Nil(t, err)
12 | require.Len(t, data, 3)
13 | }
14 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/query_fund_by_stock.go:
--------------------------------------------------------------------------------
1 | // 天天基金根据股票查询基金
2 |
3 | package eastmoney
4 |
5 | import (
6 | "context"
7 | "fmt"
8 | "time"
9 |
10 | "github.com/axiaoxin-com/goutils"
11 | "github.com/axiaoxin-com/logging"
12 | "github.com/corpix/uarand"
13 | "go.uber.org/zap"
14 | )
15 |
16 | // HoldStockFund 持有指定股票的基金
17 | type HoldStockFund struct {
18 | Fcode string `json:"FCODE"`
19 | Shortname string `json:"SHORTNAME"`
20 | Holdstock string `json:"HOLDSTOCK"`
21 | Stockname string `json:"STOCKNAME"`
22 | Zjzbl float64 `json:"ZJZBL"`
23 | Tsrq string `json:"TSRQ"`
24 | Chgtype string `json:"CHGTYPE"`
25 | Chgnum float64 `json:"CHGNUM"`
26 | SylY float64 `json:"SYL_Y"`
27 | Syl6Y float64 `json:"SYL_6Y"`
28 | Isbuy string `json:"ISBUY"`
29 | Stocktexch string `json:"STOCKTEXCH"`
30 | Newtexch string `json:"NEWTEXCH"`
31 | Zjzblchg float64 `json:"ZJZBLCHG"`
32 | Zjzblchgtype string `json:"ZJZBLCHGTYPE"`
33 | }
34 |
35 | // RespQueryFundByStock QueryFundByStock 原始api返回的结构
36 | type RespQueryFundByStock struct {
37 | Datas struct {
38 | Datas []HoldStockFund `json:"Datas"`
39 | Stocktexch string `json:"STOCKTEXCH"`
40 | Newtexch string `json:"NEWTEXCH"`
41 | } `json:"Datas"`
42 | ErrCode int `json:"ErrCode"`
43 | Success bool `json:"Success"`
44 | ErrMsg interface{} `json:"ErrMsg"`
45 | Message interface{} `json:"Message"`
46 | ErrorCode string `json:"ErrorCode"`
47 | ErrorMessage interface{} `json:"ErrorMessage"`
48 | ErrorMsgLst interface{} `json:"ErrorMsgLst"`
49 | TotalCount int `json:"TotalCount"`
50 | Expansion interface{} `json:"Expansion"`
51 | }
52 |
53 | // QueryFundByStock 根据股票查询基金
54 | func (e EastMoney) QueryFundByStock(ctx context.Context, stockName, stockCode string) ([]HoldStockFund, error) {
55 | apiurl := fmt.Sprintf(
56 | "https://fundztapi.eastmoney.com/FundSpecialApiNew/FundSpecialApiGpGetFunds?pageIndex=1&pageSize=10000&isBuy=1&sortName=ZJZBL&sortType=DESC&deviceid=1&version=6.9.9&product=EFund&plat=Iphone&name=%s&code=%s",
57 | stockName,
58 | stockCode,
59 | )
60 | logging.Debug(ctx, "EastMoney QueryFundByStock "+apiurl+" begin")
61 | beginTime := time.Now()
62 | resp := RespQueryFundByStock{}
63 | header := map[string]string{
64 | "user-agent": uarand.GetRandom(),
65 | }
66 | err := goutils.HTTPGET(ctx, e.HTTPClient, apiurl, header, &resp)
67 | latency := time.Now().Sub(beginTime).Milliseconds()
68 | logging.Debug(
69 | ctx,
70 | "EastMoney QueryFundByStock "+apiurl+" end",
71 | zap.Int64("latency(ms)", latency),
72 | // zap.Any("resp", resp),
73 | )
74 | if err != nil {
75 | return nil, err
76 | }
77 | return resp.Datas.Datas, nil
78 | }
79 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/query_fund_by_stock_test.go:
--------------------------------------------------------------------------------
1 | package eastmoney
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestQueryFundByStock(t *testing.T) {
10 | data, err := _em.QueryFundByStock(_ctx, "金域医学", "603882")
11 | require.Nil(t, err)
12 | require.NotEmpty(t, data)
13 | t.Log("data:", data)
14 | }
15 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/select_stocks_test.go:
--------------------------------------------------------------------------------
1 | package eastmoney
2 |
3 | import (
4 | "encoding/json"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestQuerySelectedStocks(t *testing.T) {
11 | data, err := _em.QuerySelectedStocks(_ctx)
12 | require.Nil(t, err)
13 | require.NotEmpty(t, data)
14 | }
15 |
16 | func TestQuerySelectedStocksWithFilter(t *testing.T) {
17 | filter := DefaultFilter
18 | filter.SpecialSecurityCodeList = []string{"002312"}
19 | data, err := _em.QuerySelectedStocksWithFilter(_ctx, filter)
20 | require.Nil(t, err)
21 | b, _ := json.Marshal(data)
22 | t.Log(string(b))
23 | }
24 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/valuation_status.go:
--------------------------------------------------------------------------------
1 | // 获取估值状态
2 |
3 | package eastmoney
4 |
5 | import (
6 | "context"
7 | "fmt"
8 | "strings"
9 | "time"
10 |
11 | "github.com/axiaoxin-com/goutils"
12 | "github.com/axiaoxin-com/logging"
13 | "go.uber.org/zap"
14 | )
15 |
16 | // RespValuation 估值状态接口返回结构
17 | type RespValuation struct {
18 | Version string `json:"version"`
19 | Result struct {
20 | Pages int `json:"pages"`
21 | Data []struct {
22 | ValationStatus string `json:"VALATION_STATUS"`
23 | } `json:"data"`
24 | Count int `json:"count"`
25 | } `json:"result"`
26 | Success bool `json:"success"`
27 | Message string `json:"message"`
28 | Code int `json:"code"`
29 | }
30 |
31 | // QueryValuationStatus 获取估值状态
32 | func (e EastMoney) QueryValuationStatus(ctx context.Context, secuCode string) (map[string]string, error) {
33 | valuations := map[string]string{}
34 | secuCode = strings.ToUpper(secuCode)
35 | apiurl := "https://datacenter.eastmoney.com/securities/api/data/get"
36 | // 市盈率估值
37 | params := map[string]string{
38 | "type": "RPT_VALUATIONSTATUS",
39 | "sty": "VALATION_STATUS",
40 | "p": "1",
41 | "ps": "1",
42 | "var": "source=DataCenter",
43 | "client": "APP",
44 | "filter": fmt.Sprintf(`(SECUCODE="%s")(INDICATOR_TYPE="1")`, secuCode),
45 | }
46 | beginTime := time.Now()
47 | apiurl1, err := goutils.NewHTTPGetURLWithQueryString(ctx, apiurl, params)
48 | logging.Debug(ctx, "EastMoney QueryValuationStatus "+apiurl1+" begin", zap.Any("params", params))
49 | if err != nil {
50 | return nil, err
51 | }
52 | resp := RespValuation{}
53 | if err := goutils.HTTPGET(ctx, e.HTTPClient, apiurl1, nil, &resp); err != nil {
54 | return nil, err
55 | }
56 | latency := time.Now().Sub(beginTime).Milliseconds()
57 | logging.Debug(
58 | ctx,
59 | "EastMoney QueryValuationStatus "+apiurl1+" end",
60 | zap.Int64("latency(ms)", latency),
61 | // zap.Any("resp", resp),
62 | )
63 | if resp.Code != 0 {
64 | return nil, fmt.Errorf("%s %#v", secuCode, resp)
65 | }
66 | if len(resp.Result.Data) > 0 {
67 | valuations["市盈率"] = resp.Result.Data[0].ValationStatus
68 | }
69 |
70 | // 市净率估值
71 | params["filter"] = fmt.Sprintf(`(SECUCODE="%s")(INDICATOR_TYPE="2")`, secuCode)
72 | beginTime = time.Now()
73 | apiurl2, err := goutils.NewHTTPGetURLWithQueryString(ctx, apiurl, params)
74 | logging.Debug(ctx, "EastMoney QueryValuationStatus "+apiurl2+" begin", zap.Any("params", params))
75 | if err != nil {
76 | return nil, err
77 | }
78 | if err := goutils.HTTPGET(ctx, e.HTTPClient, apiurl2, nil, &resp); err != nil {
79 | return nil, err
80 | }
81 | latency = time.Now().Sub(beginTime).Milliseconds()
82 | logging.Debug(
83 | ctx,
84 | "EastMoney QueryValuationStatus "+apiurl2+" end",
85 | zap.Int64("latency(ms)", latency),
86 | // zap.Any("resp", resp),
87 | )
88 | if resp.Code != 0 {
89 | return nil, fmt.Errorf("%s %#v", secuCode, resp)
90 | }
91 | if len(resp.Result.Data) > 0 {
92 | valuations["市净率"] = resp.Result.Data[0].ValationStatus
93 | }
94 |
95 | // 市销率估值
96 | params["filter"] = fmt.Sprintf(`(SECUCODE="%s")(INDICATOR_TYPE="3")`, secuCode)
97 | beginTime = time.Now()
98 | apiurl3, err := goutils.NewHTTPGetURLWithQueryString(ctx, apiurl, params)
99 | logging.Debug(ctx, "EastMoney QueryValuationStatus "+apiurl3+" begin", zap.Any("params", params))
100 | if err != nil {
101 | return nil, err
102 | }
103 | if err := goutils.HTTPGET(ctx, e.HTTPClient, apiurl3, nil, &resp); err != nil {
104 | return nil, err
105 | }
106 | latency = time.Now().Sub(beginTime).Milliseconds()
107 | logging.Debug(
108 | ctx,
109 | "EastMoney QueryValuationStatus "+apiurl3+" end",
110 | zap.Int64("latency(ms)", latency),
111 | // zap.Any("resp", resp),
112 | )
113 | if resp.Code != 0 {
114 | return nil, fmt.Errorf("%s %#v", secuCode, resp)
115 | }
116 | if len(resp.Result.Data) > 0 {
117 | valuations["市销率"] = resp.Result.Data[0].ValationStatus
118 | }
119 |
120 | // 市现率估值
121 | params["filter"] = fmt.Sprintf(`(SECUCODE="%s")(INDICATOR_TYPE="4")`, secuCode)
122 | beginTime = time.Now()
123 | apiurl4, err := goutils.NewHTTPGetURLWithQueryString(ctx, apiurl, params)
124 | logging.Debug(ctx, "EastMoney QueryValuationStatus "+apiurl4+" begin", zap.Any("params", params))
125 | if err != nil {
126 | return nil, err
127 | }
128 | err = goutils.HTTPGET(ctx, e.HTTPClient, apiurl4, nil, &resp)
129 | latency = time.Now().Sub(beginTime).Milliseconds()
130 | logging.Debug(
131 | ctx,
132 | "EastMoney QueryValuationStatus "+apiurl4+" end",
133 | zap.Int64("latency(ms)", latency),
134 | // zap.Any("resp", resp),
135 | )
136 | if err != nil {
137 | return nil, err
138 | }
139 | if resp.Code != 0 {
140 | return nil, fmt.Errorf("%s %#v", secuCode, resp)
141 | }
142 | if len(resp.Result.Data) > 0 {
143 | valuations["市现率"] = resp.Result.Data[0].ValationStatus
144 | }
145 |
146 | return valuations, nil
147 | }
148 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/valuation_status_test.go:
--------------------------------------------------------------------------------
1 | package eastmoney
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestQueryValuationStatus(t *testing.T) {
10 | data, err := _em.QueryValuationStatus(_ctx, "603043.SH")
11 | require.Nil(t, err)
12 | require.Len(t, data, 4)
13 | t.Log("data:", data)
14 | }
15 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/zonghepingjia.go:
--------------------------------------------------------------------------------
1 | // 获取智能诊股中的综合评价
2 |
3 | package eastmoney
4 |
5 | import (
6 | "context"
7 | "fmt"
8 | "time"
9 |
10 | "github.com/axiaoxin-com/goutils"
11 | "github.com/axiaoxin-com/logging"
12 | "go.uber.org/zap"
13 | )
14 |
15 | // ZHPJ 综合评价
16 | type ZHPJ struct {
17 | Securitycode string `json:"SecurityCode"`
18 | Updatetime string `json:"UpdateTime"`
19 | Totalscore string `json:"TotalScore"`
20 | Totalscorechg string `json:"TotalScoreCHG"`
21 | Leadpre interface{} `json:"LeadPre"`
22 | Risepro interface{} `json:"RisePro"`
23 | // 消息面
24 | Msgcount string `json:"MsgCount"`
25 | // 主力资金
26 | Capitalscore string `json:"CapitalScore"`
27 | // 短期呈现
28 | D1 string `json:"D1"`
29 | // 公司质地
30 | Valuescore string `json:"ValueScore"`
31 | // 市场关注意愿
32 | Marketscorechg string `json:"MarketScoreCHG"`
33 | Status string `json:"Status"`
34 | // 评分
35 | Pingfennum string `json:"PingFenNum"`
36 | // 打败 xxx 的股票
37 | Dabaishichangnum string `json:"DaBaiShiChangNum"`
38 | // 次日上涨概率
39 | Shangzhanggailvnum string `json:"ShangZhangGaiLvNum"`
40 | Checkzhengustatus bool `json:"CheckZhenGuStatus"`
41 | }
42 |
43 | // RespZongHePingJia 综合评价接口返回结构
44 | type RespZongHePingJia struct {
45 | Result struct {
46 | Zonghepingjia ZHPJ `json:"ZongHePingJia"`
47 | } `json:"Result"`
48 | Status int `json:"Status"`
49 | Message string `json:"Message"`
50 | Otherinfo struct {
51 | } `json:"OtherInfo"`
52 | }
53 |
54 | // QueryZongHePingJia 返回智能诊股中的综合评价
55 | func (e EastMoney) QueryZongHePingJia(ctx context.Context, secuCode string) (ZHPJ, error) {
56 | fc := e.GetFC(secuCode)
57 | apiurl := "https://emstockdiag.eastmoney.com/api//ZhenGuShouYe/GetZongHePingJia"
58 | reqData := map[string]interface{}{
59 | "fc": fc,
60 | }
61 | logging.Debug(ctx, "EastMoney QueryZongHePingJia "+apiurl+" begin", zap.Any("reqData", reqData))
62 | beginTime := time.Now()
63 | req, err := goutils.NewHTTPJSONReq(ctx, apiurl, reqData)
64 | if err != nil {
65 | return ZHPJ{}, err
66 | }
67 | resp := RespZongHePingJia{}
68 | err = goutils.HTTPPOST(ctx, e.HTTPClient, req, &resp)
69 | latency := time.Now().Sub(beginTime).Milliseconds()
70 | logging.Debug(
71 | ctx,
72 | "EastMoney QueryZongHePingJia "+apiurl+" end",
73 | zap.Int64("latency(ms)", latency),
74 | // zap.Any("resp", resp),
75 | )
76 | if err != nil {
77 | return ZHPJ{}, err
78 | }
79 | if resp.Status != 0 {
80 | return ZHPJ{}, fmt.Errorf("%s %#v", secuCode, resp.Message)
81 | }
82 | return resp.Result.Zonghepingjia, nil
83 | }
84 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/zonghepingjia_test.go:
--------------------------------------------------------------------------------
1 | package eastmoney
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestQueryZongHePingJia(t *testing.T) {
10 | data, err := _em.QueryZongHePingJia(_ctx, "600809.sh")
11 | require.Nil(t, err)
12 | t.Logf("%+v", data)
13 | }
14 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/zscfg.go:
--------------------------------------------------------------------------------
1 | // 指数成分股
2 |
3 | package eastmoney
4 |
5 | import (
6 | "context"
7 | "fmt"
8 |
9 | "github.com/axiaoxin-com/goutils"
10 | "github.com/corpix/uarand"
11 | )
12 |
13 | // ZSCFGItem 指数成分股信息
14 | type ZSCFGItem struct {
15 | IndexCode string `json:"IndexCode"` // 指数代码
16 | IndexName string `json:"IndexName"` // 指数名称
17 | StockCode string `json:"StockCode"` // 股票代码
18 | StockName string `json:"StockName"` // 股票名称
19 | Snewprice string `json:"SNEWPRICE"` // 最新价格
20 | Snewchg string `json:"SNEWCHG"` // 最新涨幅
21 | Marketcappct string `json:"MARKETCAPPCT"` // 持仓比例(%)
22 | StockTEXCH string `json:"StockTEXCH"`
23 | Dctexch string `json:"DCTEXCH"`
24 | }
25 |
26 | // RspZSCFG ZSCFG接口返回结构
27 | type RspZSCFG struct {
28 | Datas []ZSCFGItem `json:"Datas"`
29 | ErrCode int `json:"ErrCode"`
30 | Success bool `json:"Success"`
31 | ErrMsg interface{} `json:"ErrMsg"`
32 | Message interface{} `json:"Message"`
33 | ErrorCode string `json:"ErrorCode"`
34 | ErrorMessage interface{} `json:"ErrorMessage"`
35 | ErrorMsgLst interface{} `json:"ErrorMsgLst"`
36 | TotalCount int `json:"TotalCount"`
37 | Expansion interface{} `json:"Expansion"`
38 | }
39 |
40 | // ZSCFG 返回指数成分股列表
41 | func (e EastMoney) ZSCFG(ctx context.Context, indexCode string) (results []ZSCFGItem, err error) {
42 | apiurl := fmt.Sprintf(
43 | "https://fundztapi.eastmoney.com/FundSpecialApiNew/FundSpecialZSB30ZSCFG?IndexCode=%s&Version=6.5.5&deviceid=-&pageIndex=1&pageSize=10000&plat=Iphone&product=EFund",
44 | indexCode,
45 | )
46 | header := map[string]string{
47 | "user-agent": uarand.GetRandom(),
48 | }
49 | rsp := RspZSCFG{}
50 | if err := goutils.HTTPGET(ctx, e.HTTPClient, apiurl, header, &rsp); err != nil {
51 | return nil, err
52 | }
53 | if rsp.ErrCode != 0 {
54 | return nil, fmt.Errorf("ZSCFG rsp code error, rsp:%+v", rsp)
55 | }
56 | if len(rsp.Datas) != rsp.TotalCount {
57 | return nil, fmt.Errorf("ZSCFG rsp data len:%d != TotalCount:%d", len(rsp.Datas), rsp.TotalCount)
58 | }
59 | return rsp.Datas, nil
60 | }
61 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/zscfg_test.go:
--------------------------------------------------------------------------------
1 | package eastmoney
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestZSCFG(t *testing.T) {
10 | results, err := _em.ZSCFG(_ctx, "000905")
11 | require.Nil(t, err)
12 | t.Log(results)
13 | }
14 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/zz500.go:
--------------------------------------------------------------------------------
1 | // 中证500成分股
2 |
3 | package eastmoney
4 |
5 | import (
6 | "context"
7 | "fmt"
8 |
9 | "github.com/axiaoxin-com/goutils"
10 | "github.com/corpix/uarand"
11 | )
12 |
13 | // ZZ500Item 中证500成分股信息
14 | type ZZ500Item struct {
15 | Secucode string `json:"SECUCODE"` // 股票代码.XX
16 | SecurityCode string `json:"SECURITY_CODE"` // 股票代码
17 | SecurityNameAbbr string `json:"SECURITY_NAME_ABBR"` // 股票简称
18 | ClosePrice float64 `json:"CLOSE_PRICE"` // 最新价格
19 | Industry string `json:"INDUSTRY"` // 主营行业
20 | Region string `json:"REGION"` // 地区
21 | Weight float64 `json:"WEIGHT"` // 持仓比例(%)
22 | Eps float64 `json:"EPS"` // 每股收益
23 | Bps float64 `json:"BPS"` // 每股净资产
24 | Roe float64 `json:"ROE"` // 净资产收益率
25 | TotalShares float64 `json:"TOTAL_SHARES"` // 总股本(亿股)
26 | FreeShares float64 `json:"FREE_SHARES"` // 流通股本(亿股)
27 | FreeCap float64 `json:"FREE_CAP"` // 流通市值(亿元)
28 | Type string `json:"TYPE"`
29 | F2 interface{} `json:"f2"`
30 | F3 interface{} `json:"f3"`
31 | }
32 |
33 | // RspZZ500 ZZ500接口返回结构
34 | type RspZZ500 struct {
35 | Version string `json:"version"`
36 | Result struct {
37 | Pages int `json:"pages"`
38 | Data []ZZ500Item `json:"data"`
39 | Count int `json:"count"`
40 | } `json:"result"`
41 | Success bool `json:"success"`
42 | Message string `json:"message"`
43 | Code int `json:"code"`
44 | }
45 |
46 | // ZZ500 返回中证500成分股列表
47 | func (e EastMoney) ZZ500(ctx context.Context) (results []ZZ500Item, err error) {
48 | apiurl := "https://datacenter-web.eastmoney.com/api/data/v1/get?sortColumns=ROE&sortTypes=-1&pageSize=500&pageNumber=1&reportName=RPT_INDEX_TS_COMPONENT&columns=SECUCODE%2CSECURITY_CODE%2CTYPE%2CSECURITY_NAME_ABBR%2CCLOSE_PRICE%2CINDUSTRY%2CREGION%2CWEIGHT%2CEPS%2CBPS%2CROE%2CTOTAL_SHARES%2CFREE_SHARES%2CFREE_CAP"eColumns=f2%2Cf3"eType=0&source=WEB&client=WEB&filter=(TYPE%3D%223%22)"
49 | header := map[string]string{
50 | "user-agent": uarand.GetRandom(),
51 | }
52 | rsp := RspZZ500{}
53 | if err := goutils.HTTPGET(ctx, e.HTTPClient, apiurl, header, &rsp); err != nil {
54 | return nil, err
55 | }
56 | if rsp.Code != 0 {
57 | return nil, fmt.Errorf("ZZ500 rsp code error, rsp:%+v", rsp)
58 | }
59 | if len(rsp.Result.Data) != 500 {
60 | return nil, fmt.Errorf("ZZ500 rsp data len != 500, len=%d", len(rsp.Result.Data))
61 | }
62 | return rsp.Result.Data, nil
63 | }
64 |
--------------------------------------------------------------------------------
/datacenter/eastmoney/zz500_test.go:
--------------------------------------------------------------------------------
1 | package eastmoney
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestZZ500(t *testing.T) {
11 | results, err := _em.ZZ500(_ctx)
12 | fmt.Println(err)
13 | require.Nil(t, err)
14 | t.Log(results)
15 | }
16 |
--------------------------------------------------------------------------------
/datacenter/eniu/README.md:
--------------------------------------------------------------------------------
1 | # eniu
2 |
3 | 亿牛网接口封装
4 |
5 | ## 实现功能
6 |
7 | - 获取历史股价信息
8 | - 计算股价历史波动率
9 |
--------------------------------------------------------------------------------
/datacenter/eniu/eniu.go:
--------------------------------------------------------------------------------
1 | // 亿牛网数据源封装
2 |
3 | package eniu
4 |
5 | import (
6 | "net/http"
7 | "time"
8 | )
9 |
10 | // Eniu 亿牛网数据源
11 | type Eniu struct {
12 | // http 客户端
13 | HTTPClient *http.Client
14 | }
15 |
16 | // NewEniu 创建 Eniu 实例
17 | func NewEniu() Eniu {
18 | hc := &http.Client{
19 | Timeout: time.Second * 60 * 5,
20 | }
21 | return Eniu{
22 | HTTPClient: hc,
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/datacenter/eniu/eniu_test.go:
--------------------------------------------------------------------------------
1 | package eniu
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | var (
8 | _e = NewEniu()
9 | _ctx = context.TODO()
10 | )
11 |
--------------------------------------------------------------------------------
/datacenter/eniu/historical_price.go:
--------------------------------------------------------------------------------
1 | // 获取历史股价
2 |
3 | package eniu
4 |
5 | import (
6 | "context"
7 | "errors"
8 | "fmt"
9 | "math"
10 | "strings"
11 | "time"
12 |
13 | "github.com/axiaoxin-com/goutils"
14 | "github.com/axiaoxin-com/logging"
15 | "go.uber.org/zap"
16 | )
17 |
18 | // RespHistoricalStockPrice 历史股价接口返回结构
19 | type RespHistoricalStockPrice struct {
20 | Date []string `json:"date"`
21 | Price []float64 `json:"price"`
22 | }
23 |
24 | // LastYearFinalPrice 获取去年12月份最后一个交易日的股价
25 | func (p RespHistoricalStockPrice) LastYearFinalPrice() float64 {
26 | if len(p.Date) == 0 {
27 | return 0
28 | }
29 | for i := len(p.Date) - 1; i > 0; i-- {
30 | prefix := fmt.Sprintf("%d-12-", time.Now().Year()-1)
31 | date := p.Date[i]
32 | if strings.Contains(date, prefix) {
33 | price := p.Price[i]
34 | logging.Debugf(nil, "date:%s price:%f", date, price)
35 | return price
36 | }
37 | }
38 | return 0
39 | }
40 |
41 | // HistoricalVolatility 计算历史波动率
42 | // 历史波动率计算方法:https://goodcalculators.com/historical-volatility-calculator/
43 | // 1、从市场上获得标的股票在固定时间间隔(如每天DAY、每周WEEK或每月MONTH等)上的价格。
44 | // 2、对于每个时间段,求出该时间段末的股价与该时段初的股价之比的自然对数。
45 | // 3、求出这些对数值的标准差即为历史波动率的估计值
46 | // 4、若将日、周等标准差转化为年标准差,需要再乘以一年中包含的时段数量的平方根(如,选取时间间隔为每天,则若扣除闭市,每年中有250个交易日,应乘以根号250)
47 | func (p RespHistoricalStockPrice) HistoricalVolatility(ctx context.Context, period string) (float64, error) {
48 | priceLen := len(p.Price)
49 | if priceLen == 0 {
50 | return -1.0, errors.New("no historical price data")
51 | }
52 | // 求末初股价比自然对数
53 | logs := []float64{}
54 | for i := priceLen - 1; i >= 1; i-- {
55 | endPrice := p.Price[i]
56 | startPrice := p.Price[i-1]
57 | log := math.Log(endPrice / startPrice)
58 | logs = append(logs, log)
59 | }
60 | // 标准差
61 | stdev, err := goutils.StdDeviationFloat64(logs)
62 | if err != nil {
63 | return -1.0, err
64 | }
65 | logging.Debugs(ctx, "stdev:", stdev)
66 |
67 | periodValue := float64(250)
68 | period = strings.ToUpper(period)
69 | switch period {
70 | case "DAY":
71 | periodValue = 1
72 | case "WEEK":
73 | periodValue = 5
74 | case "MONTH":
75 | periodValue = 21.75
76 | case "YEAR":
77 | periodValue = 250
78 | }
79 | volatility := stdev * math.Sqrt(periodValue)
80 | // 数据异常时全部股价为 0 导致返回 NaN
81 | if math.IsNaN(volatility) {
82 | return -1, errors.New("volatility is NaN")
83 | }
84 | return volatility, nil
85 | }
86 |
87 | // QueryHistoricalStockPrice 获取历史股价,最新数据在最后,有一天的延迟
88 | func (e Eniu) QueryHistoricalStockPrice(ctx context.Context, secuCode string) (RespHistoricalStockPrice, error) {
89 | apiurl := fmt.Sprintf("https://eniu.com/chart/pricea/%s/t/all", e.GetPathCode(ctx, secuCode))
90 | logging.Debug(ctx, "EastMoney QueryOrgRating "+apiurl+" begin")
91 | beginTime := time.Now()
92 | resp := RespHistoricalStockPrice{}
93 | err := goutils.HTTPGET(ctx, e.HTTPClient, apiurl, nil, &resp)
94 | latency := time.Now().Sub(beginTime).Milliseconds()
95 | logging.Debug(ctx, "EastMoney QueryOrgRating "+apiurl+" end", zap.Int64("latency(ms)", latency), zap.Any("resp", resp))
96 | return resp, err
97 | }
98 |
99 | // GetPathCode 返回接口 url path 中的股票代码
100 | func (e Eniu) GetPathCode(ctx context.Context, secuCode string) string {
101 | s := strings.Split(secuCode, ".")
102 | if len(s) != 2 {
103 | return ""
104 | }
105 | return strings.ToLower(s[1]) + s[0]
106 | }
107 |
--------------------------------------------------------------------------------
/datacenter/eniu/historical_price_test.go:
--------------------------------------------------------------------------------
1 | package eniu
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestGetPathCode(t *testing.T) {
10 | code := _e.GetPathCode(_ctx, "002459.SZ")
11 | require.Equal(t, "sz002459", code)
12 | }
13 |
14 | func TestQueryHistoricalStockPrice(t *testing.T) {
15 | data, err := _e.QueryHistoricalStockPrice(_ctx, "002312.SZ")
16 | require.Nil(t, err)
17 | require.NotEmpty(t, data.Date)
18 | v, _ := data.HistoricalVolatility(_ctx, "YEAR")
19 | t.Log("volatility:", v)
20 | }
21 |
22 | func TestHistoricalVolatility(t *testing.T) {
23 | data := RespHistoricalStockPrice{
24 | Price: []float64{
25 | 8.47,
26 | 8.54,
27 | 8.3,
28 | 7.57,
29 | 7.77,
30 | 7.4,
31 | 8.23,
32 | 7.83,
33 | 7.43,
34 | 7.02,
35 | 6.75,
36 | 6.73,
37 | 6.7,
38 | 6.5,
39 | 7.45,
40 | 7.4,
41 | 7.25,
42 | 7.15,
43 | 7.25,
44 | 7.0,
45 | },
46 | }
47 | d, err := data.HistoricalVolatility(_ctx, "DAY")
48 | require.Nil(t, err)
49 | w, err := data.HistoricalVolatility(_ctx, "WEEK")
50 | require.Nil(t, err)
51 | m, err := data.HistoricalVolatility(_ctx, "MONTH")
52 | require.Nil(t, err)
53 | y, err := data.HistoricalVolatility(_ctx, "YEAR")
54 | require.Nil(t, err)
55 | t.Log("day volatility:", d, " week volatility:", w, " month volatility:", m, " year volatility:", y)
56 | }
57 |
--------------------------------------------------------------------------------
/datacenter/qq/README.md:
--------------------------------------------------------------------------------
1 | # qq
2 |
3 | 腾讯证券接口封装
4 |
5 | https://stockapp.finance.qq.com/mstats/#
6 |
7 | ## 实现功能
8 |
9 | - 关键词搜索
10 |
--------------------------------------------------------------------------------
/datacenter/qq/keyword_search.go:
--------------------------------------------------------------------------------
1 | // 关键词搜索
2 |
3 | package qq
4 |
5 | import (
6 | "context"
7 | "fmt"
8 | "strconv"
9 | "strings"
10 | "time"
11 |
12 | "github.com/axiaoxin-com/goutils"
13 | "github.com/axiaoxin-com/logging"
14 | "go.uber.org/zap"
15 | )
16 |
17 | // SearchResult 搜索结果
18 | type SearchResult struct {
19 | // 数字代码
20 | SecurityCode string
21 | // 带后缀的代码
22 | Secucode string
23 | // 股票名称
24 | Name string
25 | }
26 |
27 | // KeywordSearch 关键词搜索, 股票、代码、拼音
28 | func (q QQ) KeywordSearch(ctx context.Context, kw string) (results []SearchResult, err error) {
29 | apiurl := fmt.Sprintf("https://smartbox.gtimg.cn/s3/?v=2&q=%s&t=all&c=1", kw)
30 | logging.Debug(ctx, "QQ KeywordSearch "+apiurl+" begin")
31 | beginTime := time.Now()
32 | resp, err := goutils.HTTPGETRaw(ctx, q.HTTPClient, apiurl, nil)
33 | latency := time.Now().Sub(beginTime).Milliseconds()
34 | logging.Debug(ctx, "QQ KeywordSearch "+apiurl+" end", zap.Int64("latency(ms)", latency), zap.Any("resp", string(resp)))
35 | if err != nil {
36 | return nil, err
37 | }
38 | respMap := map[string]string{}
39 | for _, line := range strings.Split(string(resp), ";") {
40 | lineitems := strings.Split(line, "=")
41 | if len(lineitems) != 2 {
42 | continue
43 | }
44 | k := strings.TrimSpace(lineitems[0])
45 | v := strings.TrimSpace(lineitems[1])
46 | respMap[k] = strings.Trim(v, `"`)
47 | }
48 | logging.Debugf(ctx, "respMap: %#v", respMap)
49 | resultsStrs := strings.Split(respMap["v_hint"], "^")
50 | logging.Debugs(ctx, "resultsStrs:", resultsStrs)
51 | for _, rs := range resultsStrs {
52 | matchedSlice := strings.Split(rs, "~")
53 | if len(matchedSlice) < 3 {
54 | logging.Debugf(ctx, "invalid matchedSlice:%v", matchedSlice)
55 | continue
56 | }
57 | market, securityCode, name := matchedSlice[0], matchedSlice[1], matchedSlice[2]
58 | // unicode -> cn
59 | name, err = strconv.Unquote(strings.Replace(strconv.Quote(string(name)), `\\u`, `\u`, -1))
60 | if err != nil {
61 | return nil, err
62 | }
63 | result := SearchResult{
64 | Secucode: securityCode + "." + market,
65 | SecurityCode: securityCode,
66 | Name: name,
67 | }
68 | results = append(results, result)
69 | }
70 | return
71 | }
72 |
--------------------------------------------------------------------------------
/datacenter/qq/keyword_search_test.go:
--------------------------------------------------------------------------------
1 | package qq
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestKeywordSearch(t *testing.T) {
10 | results, err := _q.KeywordSearch(_ctx, "招商银行")
11 | require.Nil(t, err)
12 | t.Log(results)
13 | }
14 |
--------------------------------------------------------------------------------
/datacenter/qq/qq.go:
--------------------------------------------------------------------------------
1 | // Package qq 腾讯证券接口封装
2 | package qq
3 |
4 | import (
5 | "net/http"
6 | "time"
7 | )
8 |
9 | // QQ 新浪财经数据源
10 | type QQ struct {
11 | // http 客户端
12 | HTTPClient *http.Client
13 | }
14 |
15 | // NewQQ 创建 QQ 实例
16 | func NewQQ() QQ {
17 | hc := &http.Client{
18 | Timeout: time.Second * 60 * 5,
19 | }
20 | return QQ{
21 | HTTPClient: hc,
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/datacenter/qq/qq_test.go:
--------------------------------------------------------------------------------
1 | package qq
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | var (
8 | _q = NewQQ()
9 | _ctx = context.TODO()
10 | )
11 |
--------------------------------------------------------------------------------
/datacenter/sina/keyword_search.go:
--------------------------------------------------------------------------------
1 | // 关键词搜索
2 |
3 | package sina
4 |
5 | import (
6 | "bytes"
7 | "context"
8 | "fmt"
9 | "io/ioutil"
10 | "sort"
11 | "strconv"
12 | "strings"
13 | "time"
14 |
15 | "github.com/axiaoxin-com/goutils"
16 | "github.com/axiaoxin-com/logging"
17 | "github.com/pkg/errors"
18 | "go.uber.org/zap"
19 | "golang.org/x/text/encoding/simplifiedchinese"
20 | "golang.org/x/text/transform"
21 | )
22 |
23 | // SearchResult 搜索结果
24 | type SearchResult struct {
25 | // 数字代码
26 | SecurityCode string
27 | // 带后缀的代码
28 | Secucode string
29 | // 股票名称
30 | Name string
31 | // 股市类型: 11=A股 31=港股 41=美股 103=英股
32 | Market int
33 | }
34 |
35 | // KeywordSearch 关键词搜索, 股票、代码、拼音
36 | func (s Sina) KeywordSearch(ctx context.Context, kw string) (results []SearchResult, err error) {
37 | apiurl := fmt.Sprintf("https://suggest3.sinajs.cn/suggest/key=%s", kw)
38 | logging.Debug(ctx, "Sina KeywordSearch "+apiurl+" begin")
39 | beginTime := time.Now()
40 | resp, err := goutils.HTTPGETRaw(ctx, s.HTTPClient, apiurl, nil)
41 | latency := time.Now().Sub(beginTime).Milliseconds()
42 | logging.Debug(ctx, "Sina KeywordSearch "+apiurl+" end", zap.Int64("latency(ms)", latency), zap.Any("resp", string(resp)))
43 | if err != nil {
44 | return nil, err
45 | }
46 |
47 | trans := transform.NewReader(bytes.NewReader(resp), simplifiedchinese.GBK.NewDecoder())
48 | utf8resp, err := ioutil.ReadAll(trans)
49 | if err != nil {
50 | logging.Error(ctx, "transform ReadAll error:"+err.Error())
51 | }
52 | ds := strings.Split(string(utf8resp), "=")
53 | if len(ds) != 2 {
54 | return nil, errors.New("search resp invalid:" + string(utf8resp))
55 | }
56 | data := strings.Trim(ds[1], `"`)
57 | for _, line := range strings.Split(data, ";") {
58 | lineitems := strings.Split(line, ",")
59 | if len(lineitems) < 9 {
60 | continue
61 | }
62 | market, err := strconv.Atoi(lineitems[1])
63 | if err != nil {
64 | logging.Errorf(ctx, "market:%s atoi error:%v", lineitems[1], err)
65 | }
66 | secucode := lineitems[3][2:] + "." + lineitems[3][:2]
67 | result := SearchResult{
68 | SecurityCode: lineitems[2],
69 | Secucode: secucode,
70 | Name: lineitems[6],
71 | Market: market,
72 | }
73 | results = append(results, result)
74 | }
75 | // 按股市编号排序确保A股在前面
76 | sort.Slice(results, func(i, j int) bool {
77 | return results[i].Market < results[j].Market
78 | })
79 | return
80 | }
81 |
--------------------------------------------------------------------------------
/datacenter/sina/keyword_search_test.go:
--------------------------------------------------------------------------------
1 | package sina
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestKeywordSearch(t *testing.T) {
10 | results, err := _s.KeywordSearch(_ctx, "比亚迪")
11 | require.Nil(t, err)
12 | t.Log(results)
13 | }
14 |
--------------------------------------------------------------------------------
/datacenter/sina/sina.go:
--------------------------------------------------------------------------------
1 | // Package sina 新浪财经接口封装
2 | package sina
3 |
4 | import (
5 | "net/http"
6 | "time"
7 | )
8 |
9 | // Sina 新浪财经数据源
10 | type Sina struct {
11 | // http 客户端
12 | HTTPClient *http.Client
13 | }
14 |
15 | // NewSina 创建 Sina 实例
16 | func NewSina() Sina {
17 | hc := &http.Client{
18 | Timeout: time.Second * 60 * 5,
19 | }
20 | return Sina{
21 | HTTPClient: hc,
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/datacenter/sina/sina_test.go:
--------------------------------------------------------------------------------
1 | package sina
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | var (
8 | _s = NewSina()
9 | _ctx = context.TODO()
10 | )
11 |
--------------------------------------------------------------------------------
/datacenter/zszx/net_inflows.go:
--------------------------------------------------------------------------------
1 | // 获取个股指定时间段内资金净流入数据
2 |
3 | package zszx
4 |
5 | import (
6 | "context"
7 | "errors"
8 | "fmt"
9 | "strconv"
10 | "strings"
11 | "time"
12 |
13 | "github.com/axiaoxin-com/goutils"
14 | "github.com/axiaoxin-com/logging"
15 | "go.uber.org/zap"
16 | )
17 |
18 | // NetInflow 资金净流入详情
19 | type NetInflow struct {
20 | // 交易日期
21 | TrdDt string `json:"TrdDt"`
22 | // 当日股价
23 | ClsPrc string `json:"ClsPrc"`
24 | // 主力净流入(万元)
25 | MainMnyNetIn string `json:"MainMnyNetIn"`
26 | // 超大单净流入(万元)
27 | HugeNetIn string `json:"HugeNetIn"`
28 | // 大单净流入(万元)
29 | BigNetIn string `json:"BigNetIn"`
30 | // 中单净流入(万元)
31 | MidNetIn string `json:"MidNetIn"`
32 | // 小单净流入(万元)
33 | SmallNetIn string `json:"SmallNetIn"`
34 | TTLMnyNetIn string `json:"TtlMnyNetIn"`
35 | }
36 |
37 | // NetInflowList 净流入详情列表
38 | type NetInflowList []NetInflow
39 |
40 | func (n NetInflowList) String() string {
41 | ctx := context.Background()
42 | netInflowsLen := len(n)
43 | netInflow3days := "--"
44 | if netInflowsLen >= 3 {
45 | netInflow3days = fmt.Sprintf("近3日主力资金净流入:%.2f万元", n[:3].SumMainNetIn(ctx))
46 | }
47 | netInflow5days := "--"
48 | if netInflowsLen >= 5 {
49 | netInflow5days = fmt.Sprintf("近5日主力资金净流入:%.2f万元", n[:5].SumMainNetIn(ctx))
50 | }
51 | netInflow10days := "--"
52 | if netInflowsLen >= 10 {
53 | netInflow10days = fmt.Sprintf("近10日主力资金净流入:%.2f万元", n[:10].SumMainNetIn(ctx))
54 | }
55 | netInflow20days := "--"
56 | if netInflowsLen >= 20 {
57 | netInflow20days = fmt.Sprintf("近20日主力资金净流入:%.2f万元", n[:20].SumMainNetIn(ctx))
58 | }
59 | netInflow30days := "--"
60 | if netInflowsLen >= 30 {
61 | netInflow30days = fmt.Sprintf("近30日主力资金净流入:%.2f万元", n[:30].SumMainNetIn(ctx))
62 | }
63 | netInflow40days := "--"
64 | if netInflowsLen >= 40 {
65 | netInflow40days = fmt.Sprintf("近40日主力资金净流入:%.2f万元", n[:40].SumMainNetIn(ctx))
66 | }
67 | return fmt.Sprintf(
68 | "%s
%s
%s
%s
%s
%s",
69 | netInflow3days,
70 | netInflow5days,
71 | netInflow10days,
72 | netInflow20days,
73 | netInflow30days,
74 | netInflow40days,
75 | )
76 | }
77 |
78 | // SumMainNetIn 主力净流入列表求和
79 | func (n NetInflowList) SumMainNetIn(ctx context.Context) float64 {
80 | var netFlowin float64 = 0.0
81 | for _, i := range n {
82 | mainNetIn, err := strconv.ParseFloat(i.MainMnyNetIn, 64)
83 | if err != nil {
84 | logging.Errorf(ctx, "Parse MainMnyNetIn:%v to Float error:%v", i.MainMnyNetIn, err)
85 | }
86 | netFlowin += mainNetIn
87 | }
88 | return netFlowin
89 | }
90 |
91 | // RespMainMoneyNetInflows QueryMainMoneyNetInflows 返回json结构
92 | type RespMainMoneyNetInflows struct {
93 | Success bool `json:"success"`
94 | Message string `json:"message"`
95 | Code int `json:"code"`
96 | Data NetInflowList `json:"data"`
97 | }
98 |
99 | // QueryMainMoneyNetInflows 查询主力资金净流入数据
100 | func (z Zszx) QueryMainMoneyNetInflows(ctx context.Context, secuCode, startDate, endDate string) (NetInflowList, error) {
101 | apiurl := "https://zszx.cmschina.com/pcnews/f10/stkcnmnyflow"
102 | stockCodeAndMarket := strings.Split(secuCode, ".")
103 | if len(stockCodeAndMarket) != 2 {
104 | return nil, errors.New("invalid secuCode:" + secuCode)
105 | }
106 | stockCode := stockCodeAndMarket[0]
107 | market := stockCodeAndMarket[1]
108 | marketCode := "0"
109 | if strings.ToUpper(market) == "SH" {
110 | marketCode = "1"
111 | }
112 | params := map[string]string{
113 | "dateStart": startDate,
114 | "dateEnd": endDate,
115 | "ecode": marketCode,
116 | "scode": stockCode,
117 | }
118 | logging.Debug(ctx, "Zszx QueryMainMoneyNetInflows "+apiurl+" begin", zap.Any("params", params))
119 | beginTime := time.Now()
120 | apiurl, err := goutils.NewHTTPGetURLWithQueryString(ctx, apiurl, params)
121 | if err != nil {
122 | return nil, err
123 | }
124 | resp := RespMainMoneyNetInflows{}
125 | err = goutils.HTTPGET(ctx, z.HTTPClient, apiurl, nil, &resp)
126 | latency := time.Now().Sub(beginTime).Milliseconds()
127 | logging.Debug(
128 | ctx,
129 | "Zszx QueryMainMoneyNetInflows "+apiurl+" end",
130 | zap.Int64("latency(ms)", latency),
131 | zap.Any("resp", resp),
132 | )
133 | if err != nil {
134 | return nil, err
135 | }
136 | if resp.Code != 0 {
137 | return nil, fmt.Errorf("%s %#v", secuCode, resp)
138 | }
139 | return resp.Data, nil
140 | }
141 |
--------------------------------------------------------------------------------
/datacenter/zszx/net_inflows_test.go:
--------------------------------------------------------------------------------
1 | package zszx
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestQueryMainMoneyNetInflows(t *testing.T) {
11 | now := time.Now()
12 | end := now.Format("2006-01-02")
13 | d, _ := time.ParseDuration("-720h")
14 | start := now.Add(d).Format("2006-01-02")
15 | results, err := _z.QueryMainMoneyNetInflows(_ctx, "002028.sz", start, end)
16 | require.Nil(t, err)
17 | require.NotEqual(t, len(results), 0)
18 | last3days := results[:3]
19 | t.Logf("last3days:%#v, sum:%f", last3days, last3days.SumMainNetIn(_ctx))
20 | last5days := results[:5]
21 | t.Logf("last5days:%#v, sum:%f", last5days, last5days.SumMainNetIn(_ctx))
22 | last10days := results[:10]
23 | t.Logf("last10days:%#v, sum:%f", last10days, last10days.SumMainNetIn(_ctx))
24 | }
25 |
--------------------------------------------------------------------------------
/datacenter/zszx/zszx.go:
--------------------------------------------------------------------------------
1 | // Package zszx 招商证券接口封装
2 | // https://zszx.cmschina.com/
3 | package zszx
4 |
5 | import (
6 | "net/http"
7 | "time"
8 | )
9 |
10 | // Zszx 招商证券接口
11 | type Zszx struct {
12 | // http 客户端
13 | HTTPClient *http.Client
14 | }
15 |
16 | // NewZszx 创建 Zszx 实例
17 | func NewZszx() Zszx {
18 | hc := &http.Client{
19 | Timeout: time.Second * 60 * 5,
20 | }
21 | return Zszx{
22 | HTTPClient: hc,
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/datacenter/zszx/zszx_test.go:
--------------------------------------------------------------------------------
1 | package zszx
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | var (
8 | _z = NewZszx()
9 | _ctx = context.TODO()
10 | )
11 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/axiaoxin-com/investool
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/360EntSecGroup-Skylar/excelize/v2 v2.4.0
7 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
8 | github.com/avast/retry-go v3.0.0+incompatible
9 | github.com/axiaoxin-com/goutils v1.1.0
10 | github.com/axiaoxin-com/logging v1.2.11-0.20210710005236-5a960a1422ba
11 | github.com/axiaoxin-com/ratelimiter v1.0.3
12 | github.com/corpix/uarand v0.1.1
13 | github.com/deckarep/golang-set v1.8.0
14 | github.com/fsnotify/fsnotify v1.7.0
15 | github.com/gin-contrib/pprof v1.3.0
16 | github.com/gin-gonic/gin v1.9.1
17 | github.com/go-co-op/gocron v1.6.2
18 | github.com/gocarina/gocsv v0.0.0-20210516172204-ca9e8a8ddea8
19 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
20 | github.com/json-iterator/go v1.1.12
21 | github.com/olekukonko/tablewriter v0.0.5
22 | github.com/pkg/errors v0.9.1
23 | github.com/prometheus/client_golang v1.11.0
24 | github.com/spf13/viper v1.17.0
25 | github.com/stretchr/testify v1.8.4
26 | github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
27 | github.com/swaggo/gin-swagger v1.2.0
28 | github.com/swaggo/swag v1.7.1
29 | github.com/urfave/cli/v2 v2.3.0
30 | go.uber.org/zap v1.21.0
31 | golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
32 | golang.org/x/text v0.13.0
33 | )
34 |
35 | require (
36 | github.com/KyleBanks/depth v1.2.1 // indirect
37 | github.com/PuerkitoBio/purell v1.1.1 // indirect
38 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
39 | github.com/antlabs/strsim v0.0.2 // indirect
40 | github.com/beorn7/perks v1.0.1 // indirect
41 | github.com/bytedance/sonic v1.9.1 // indirect
42 | github.com/cespare/xxhash/v2 v2.1.2 // indirect
43 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
44 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
45 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
46 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
47 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect
48 | github.com/getsentry/sentry-go v0.6.0 // indirect
49 | github.com/gin-contrib/sse v0.1.0 // indirect
50 | github.com/go-openapi/jsonpointer v0.19.5 // indirect
51 | github.com/go-openapi/jsonreference v0.19.5 // indirect
52 | github.com/go-openapi/spec v0.20.3 // indirect
53 | github.com/go-openapi/swag v0.19.14 // indirect
54 | github.com/go-playground/locales v0.14.1 // indirect
55 | github.com/go-playground/universal-translator v0.18.1 // indirect
56 | github.com/go-playground/validator/v10 v10.14.0 // indirect
57 | github.com/go-redis/redis/v8 v8.11.5 // indirect
58 | github.com/goccy/go-json v0.10.2 // indirect
59 | github.com/golang/protobuf v1.5.3 // indirect
60 | github.com/hashicorp/hcl v1.0.0 // indirect
61 | github.com/josharian/intern v1.0.0 // indirect
62 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect
63 | github.com/leodido/go-urn v1.2.4 // indirect
64 | github.com/magiconair/properties v1.8.7 // indirect
65 | github.com/mailru/easyjson v0.7.6 // indirect
66 | github.com/mattn/go-isatty v0.0.19 // indirect
67 | github.com/mattn/go-runewidth v0.0.9 // indirect
68 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
69 | github.com/mitchellh/mapstructure v1.5.0 // indirect
70 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
71 | github.com/modern-go/reflect2 v1.0.2 // indirect
72 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
73 | github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
74 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
75 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect
76 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
77 | github.com/prometheus/client_model v0.2.0 // indirect
78 | github.com/prometheus/common v0.26.0 // indirect
79 | github.com/prometheus/procfs v0.6.0 // indirect
80 | github.com/richardlehane/mscfb v1.0.3 // indirect
81 | github.com/richardlehane/msoleps v1.0.1 // indirect
82 | github.com/robfig/cron/v3 v3.0.1 // indirect
83 | github.com/rs/xid v1.2.1 // indirect
84 | github.com/russross/blackfriday/v2 v2.0.1 // indirect
85 | github.com/sagikazarmark/locafero v0.3.0 // indirect
86 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect
87 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
88 | github.com/sourcegraph/conc v0.3.0 // indirect
89 | github.com/speps/go-hashids v2.0.0+incompatible // indirect
90 | github.com/spf13/afero v1.10.0 // indirect
91 | github.com/spf13/cast v1.5.1 // indirect
92 | github.com/spf13/pflag v1.0.5 // indirect
93 | github.com/subosito/gotenv v1.6.0 // indirect
94 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
95 | github.com/ugorji/go/codec v1.2.11 // indirect
96 | github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 // indirect
97 | go.uber.org/atomic v1.9.0 // indirect
98 | go.uber.org/multierr v1.9.0 // indirect
99 | golang.org/x/arch v0.3.0 // indirect
100 | golang.org/x/crypto v0.13.0 // indirect
101 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
102 | golang.org/x/net v0.15.0 // indirect
103 | golang.org/x/sync v0.3.0 // indirect
104 | golang.org/x/sys v0.12.0 // indirect
105 | golang.org/x/time v0.3.0 // indirect
106 | golang.org/x/tools v0.13.0 // indirect
107 | google.golang.org/protobuf v1.31.0 // indirect
108 | gopkg.in/ini.v1 v1.67.0 // indirect
109 | gopkg.in/yaml.v2 v2.4.0 // indirect
110 | gopkg.in/yaml.v3 v3.0.1 // indirect
111 | gorm.io/gorm v1.23.4 // indirect
112 | )
113 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | //go:generate swag init --dir ./ --generalInfo routes/routes.go --propertyStrategy snakecase --output ./routes/docs
2 |
3 | // Package main investool is my stock bot
4 | package main
5 |
6 | import (
7 | "fmt"
8 | "os"
9 | "time"
10 |
11 | "github.com/axiaoxin-com/investool/cmds"
12 | "github.com/axiaoxin-com/investool/models"
13 | "github.com/axiaoxin-com/investool/version"
14 | "github.com/spf13/viper"
15 | "github.com/urfave/cli/v2"
16 | )
17 |
18 | var (
19 | // DefaultLoglevel 日志级别默认值
20 | DefaultLoglevel = "info"
21 | // ProcessorOptions 要启动运行的进程可选项
22 | ProcessorOptions = []string{cmds.ProcessorChecker, cmds.ProcessorExportor, cmds.ProcessorWebserver, cmds.ProcessorIndex, cmds.ProcessorJSON}
23 | )
24 |
25 | func init() {
26 | viper.SetDefault("app.chan_size", 1)
27 | models.InitGlobalVars()
28 | }
29 |
30 | func main() {
31 | app := cli.NewApp()
32 | app.EnableBashCompletion = true
33 | app.Name = "investool"
34 | app.Usage = "axiaoxin 的股票工具程序"
35 | app.UsageText = `该程序不构成任何投资建议,程序只是个人辅助工具,具体分析仍然需要自己判断。
36 |
37 | 官网地址: http://investool.axiaoxin.com`
38 | app.Version = version.Version
39 | app.Compiled = time.Now()
40 | app.Authors = []*cli.Author{
41 | {
42 | Name: "axiaoxin",
43 | Email: "254606826@qq.com",
44 | },
45 | }
46 | app.Copyright = "(c) 2021 axiaoxin"
47 |
48 | cli.VersionFlag = &cli.BoolFlag{
49 | Name: "version",
50 | Aliases: []string{"v"},
51 | Usage: "show the version",
52 | }
53 |
54 | app.Flags = []cli.Flag{
55 | &cli.StringFlag{
56 | Name: "loglevel",
57 | Aliases: []string{"l"},
58 | Value: DefaultLoglevel,
59 | Usage: "cmd 日志级别 [debug|info|warn|error]",
60 | EnvVars: []string{"INVESTOOL_CMD_LOGLEVEL"},
61 | DefaultText: DefaultLoglevel,
62 | },
63 | }
64 | app.BashComplete = func(c *cli.Context) {
65 | if c.NArg() > 0 {
66 | return
67 | }
68 | for _, i := range ProcessorOptions {
69 | fmt.Println(i)
70 | }
71 | }
72 |
73 | app.Commands = append(app.Commands, cmds.CommandExportor())
74 | app.Commands = append(app.Commands, cmds.CommandChecker())
75 | app.Commands = append(app.Commands, cmds.CommandWebserver())
76 | app.Commands = append(app.Commands, cmds.CommandIndex())
77 | app.Commands = append(app.Commands, cmds.CommandJSON())
78 |
79 | if err := app.Run(os.Args); err != nil {
80 | fmt.Println(err.Error())
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/misc/.gitignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/misc/.gitignore
--------------------------------------------------------------------------------
/misc/README.md:
--------------------------------------------------------------------------------
1 | 开发部署相关的工具及脚本或其他物料存放目录
2 |
--------------------------------------------------------------------------------
/misc/configs/README.md:
--------------------------------------------------------------------------------
1 | 其他配置文件存放目录
2 |
--------------------------------------------------------------------------------
/misc/configs/nginx.conf:
--------------------------------------------------------------------------------
1 | upstream pink_lady_api {
2 | server 0.0.0.0:4869;
3 | keepalive 600;
4 | }
5 |
6 | server {
7 | listen 80;
8 | server_name example.com;
9 |
10 |
11 | access_log /path_to_project/logs/nginx.access.log;
12 | error_log /path_to_project/logs/nginx.error.log;
13 |
14 | location / {
15 | proxy_pass http://pink_lady_api;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/misc/configs/supervisor.conf:
--------------------------------------------------------------------------------
1 | [program:investool]
2 | directory=/srv/investool
3 | command=/srv/investool/investool webserver -c ./config.prod.toml
4 | process_name=%(program_name)s
5 | redirect_stderr=false
6 | stdout_logfile=/srv/investool/logs/%(program_name)s.stdout.log
7 | stderr_logfile=/srv/investool/logs/%(program_name)s.stderr.log
8 | autorestart=true
9 | stdout_logfile_maxbytes=10MB ; max # logfile bytes b4 rotation (default 50MB)
10 | stdout_logfile_backups=10 ; # of stdout logfile backups (default 10)
11 | stdout_capture_maxbytes=10MB ; number of bytes in 'capturemode' (default 0)
12 | stdout_events_enabled=true ; emit events on stdout writes (default false)
13 | stderr_logfile_maxbytes=10MB ; max # logfile bytes b4 rotation (default 50MB)
14 | stderr_logfile_backups=10 ; # of stderr logfile backups (default 10)
15 | stderr_capture_maxbytes=10MB ; number of bytes in 'capturemode' (default 0)
16 | stderr_events_enabled=true ; emit events on stderr writes (default false)
17 |
--------------------------------------------------------------------------------
/misc/docs/checker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/misc/docs/checker.png
--------------------------------------------------------------------------------
/misc/docs/checker2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/misc/docs/checker2.png
--------------------------------------------------------------------------------
/misc/docs/历史波动率分析使用简介.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/misc/docs/历史波动率分析使用简介.pdf
--------------------------------------------------------------------------------
/misc/pics/gin_arch.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/misc/pics/gin_arch.pdf
--------------------------------------------------------------------------------
/misc/pics/gin_arch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/misc/pics/gin_arch.png
--------------------------------------------------------------------------------
/misc/pics/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/misc/pics/logo.png
--------------------------------------------------------------------------------
/misc/scripts/README.md:
--------------------------------------------------------------------------------
1 | 工具类的脚本存放目录
2 |
--------------------------------------------------------------------------------
/misc/scripts/app.min.js.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | # 编译替换app.js
3 |
4 | realpath() {
5 | [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
6 | }
7 |
8 | # PATHS
9 | PROJECT_PATH=$(dirname $(dirname $(dirname $(realpath $0))))
10 | SRC_PATH=${PROJECT_PATH}/statics/js
11 | NOW=$(date "+%Y%m%d-%H%M%S")
12 | NEW_NAME=app.min.${NOW}.js
13 |
14 | echo "压缩js..."
15 | uglifyjs --compress --mangle --output ${SRC_PATH}/${NEW_NAME} -- ${SRC_PATH}/app.js && \
16 |
17 | echo "替换html..."
18 | sed -i '' -e "s|investool/js/app.*.js\"{{ else }}|investool/js/${NEW_NAME}\"{{ else }}|g" ${PROJECT_PATH}/statics/html/base.html
19 |
--------------------------------------------------------------------------------
/misc/scripts/bumpversion.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # PATHS
4 | PROJECT_PATH=$(dirname $(dirname $(dirname $(realpath $0))))
5 | SRC_PATH=${PROJECT_PATH}/version
6 |
7 | VERSION=`git describe --abbrev=0 --tags`
8 |
9 | #replace . with space so can split into an array
10 | VERSION_BITS=(${VERSION//./ })
11 |
12 | #get number parts and increase last one by 1
13 | VNUM1=${VERSION_BITS[0]}
14 | VNUM2=${VERSION_BITS[1]}
15 | VNUM3=${VERSION_BITS[2]}
16 | VNUM3=$((VNUM3+1))
17 |
18 | #create new tag
19 | DEFAULT_TAG="$VNUM1.$VNUM2.$VNUM3"
20 |
21 | echo -ne "Updating $VERSION to new tag[${DEFAULT_TAG}]: "
22 | read NEW_TAG
23 | if [ "$NEW_TAG" == "" ]; then
24 | NEW_TAG=${DEFAULT_TAG}
25 | fi
26 |
27 | #get current hash and see if it already has a tag
28 | GIT_COMMIT=`git rev-parse HEAD`
29 | NEEDS_TAG=`git describe --contains $GIT_COMMIT 2>/dev/null`
30 |
31 | #only tag if no tag already
32 | if [ -z "$NEEDS_TAG" ]; then
33 | # https://github.com/x-motemen/gobump
34 | # 使用 git tag 更新 main.go 中的 VERSION ,去掉前缀 v
35 | gobump set ${NEW_TAG/#v} -w ${SRC_PATH} && \
36 | bash ${PROJECT_PATH}/misc/scripts/gen_apidocs.sh && \
37 | git commit -am "bump verision to $NEW_TAG" && \
38 | git tag $NEW_TAG && \
39 | echo "Tagged with $NEW_TAG"
40 | else
41 | echo "Already a tag on this commit"
42 | fi
43 |
--------------------------------------------------------------------------------
/misc/scripts/dist.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | # 编译打包二进制文件
3 |
4 | realpath() {
5 | [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
6 | }
7 |
8 | # PATHS
9 | PROJECT_PATH=$(dirname $(dirname $(dirname $(realpath $0))))
10 | SRC_PATH=${PROJECT_PATH}
11 | DIST_PATH=${PROJECT_PATH}/dist
12 | NOW=$(date "+%Y%m%d-%H%M%S")
13 |
14 |
15 | # ERROR CODE
16 | TESTING_FAILED=-1
17 | BUILDING_FAILED=-2
18 |
19 | # vars
20 | BINARY_NAME=apiserver
21 | CONFIGFILE=config.default.toml
22 | TARNAME=investool
23 |
24 | # clean dist dir
25 | clean() {
26 | rm -rf ${DIST_PATH}
27 | }
28 |
29 | # running go test
30 | tests() {
31 | # Running tests
32 | echo -e "Running tests"
33 | cd ${SRC_PATH}
34 | if !(go test -race ./...)
35 | then
36 | echo -e "Tests failed."
37 | cd -
38 | exit ${TESTING_FAILED}
39 | fi
40 | cd -
41 | }
42 |
43 | # gen apidocs and go build
44 | build() {
45 | echo -ne "Enter release binary name [${BINARY_NAME}]: "
46 | read binary_name
47 | if [ "${binary_name}" != "" ]; then
48 | BINARY_NAME=${binary_name}
49 | fi
50 |
51 | echo -e "Will build binary name to be ${BINARY_NAME}"
52 |
53 | # Update docs
54 | echo "Updating swag docs"
55 | # check swag
56 | if !(swag > /dev/null 2>&1); then
57 | echo -e "Need swag to generate API docs. Installing swag..."
58 | go get -u github.com/swaggo/swag/cmd/swag
59 | fi
60 | echo -e "Generating API docs..."
61 | bash ${PROJECT_PATH}/misc/scripts/gen_apidocs.sh
62 |
63 | # Building
64 | echo -e "Building..."
65 | if [ ! -d ${DIST_PATH} ]; then
66 | mkdir ${DIST_PATH}
67 | fi
68 | cd ${SRC_PATH}
69 | GOOS=linux GOARCH=amd64 go build -o ${DIST_PATH}/${BINARY_NAME} ${SRC_PATH}
70 | if [ $? -ne 0 ]
71 | then
72 | echo -e "Build failed."
73 | exit ${BUILDING_FAILED}
74 | fi
75 | cd -
76 | }
77 |
78 | # tar bin and config file
79 | tarball() {
80 | echo -ne "Enter your configfile[${CONFIGFILE}]: "
81 | read cf
82 | if [ "${cf}" != "" ]; then
83 | CONFIGFILE=${cf}
84 | fi
85 |
86 | echo -e "tar binary file and config file..."
87 | tardir=${DIST_PATH}/${TARNAME}
88 | mkdir ${tardir}
89 | mv ${DIST_PATH}/${BINARY_NAME} ${tardir}
90 | cp ${SRC_PATH}/${CONFIGFILE} ${tardir}
91 | tar czvf ${tardir}.tar.gz -C ${DIST_PATH} ${TARNAME} && rm -rf ${tardir}
92 | }
93 |
94 | main() {
95 | echo -e "This tool will help you to release your app.\nIt will run tests then update apidocs and build the binary file and tar it with configfile as tar.gz file."
96 | clean
97 | tests
98 | build
99 | tarball
100 | }
101 |
102 | main
103 |
--------------------------------------------------------------------------------
/misc/scripts/docker_run.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | # 编译并docker中运行
3 |
4 | realpath() {
5 | [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
6 | }
7 |
8 | PROJECT_PATH=$(dirname $(dirname $(dirname $(realpath $0))))
9 |
10 | source ${PROJECT_PATH}/misc/scripts/dist.sh
11 |
12 | docker build -t investool ${PROJECT_PATH}
13 | docker run -p 4869:4869 -p 4870:4870 investool
14 |
--------------------------------------------------------------------------------
/misc/scripts/gen_apidocs.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | # 生成swag api文档
3 |
4 | realpath() {
5 | [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
6 | }
7 |
8 | PROJECT_PATH=$(dirname $(dirname $(dirname $(realpath $0))))
9 | SRC_PATH=${PROJECT_PATH}
10 |
11 | # swag init必须在main.go所在的目录下执行,否则必须用--dir参数指定main.go的路径
12 | # go get -u github.com/swaggo/swag/cmd/swag
13 | swag init --dir ${SRC_PATH}/ --generalInfo routes/routes.go --propertyStrategy snakecase --output ${SRC_PATH}/routes/docs
14 |
--------------------------------------------------------------------------------
/misc/scripts/new_project.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 |
3 | main() {
4 | gopath=`go env GOPATH`
5 | if [ $? = 127 ]; then
6 | echo "GOPATH not exists"
7 | exit -1
8 | fi
9 | echo -e "New project will be create in ${gopath}/src/"
10 | echo -ne "Enter your new project full name: "
11 | read projname
12 |
13 | # get template project
14 | echo -e "Downloading the template..."
15 | # git clone https://github.com/axiaoxin-com/pink-lady.git ${gopath}/src/${projname}
16 | rm -
17 | if !(curl https://codeload.github.com/axiaoxin-com/pink-lady/zip/master -o /tmp/pink-lady.zip && unzip /tmp/pink-lady.zip -d /tmp)
18 | then
19 | echo "Downloading failed."
20 | exit -2
21 | fi
22 |
23 | echo -e "Generating the project..."
24 | mv /tmp/pink-lady-master ${gopath}/src/${projname} && cd ${gopath}/src/${projname}
25 |
26 | if [ `uname` = 'Darwin' ]; then
27 | sed -i '' -e "s|github.com/axiaoxin-com/pink-lady|${projname}|g" `grep "pink-lady" --include "swagger.*" --include ".travis.yml" --include "*.go" --include "go.*" -rl .`
28 | else
29 | sed -i "s|github.com/axiaoxin-com/pink-lady|${projname}|g" `grep "pink-lady" --include "swagger.*" --include ".travis.yml" --include "*.go" --include "go.*" -rl .`
30 | fi
31 |
32 | if [ $? -ne 0 ]
33 | then
34 | echo -e "set project name failed."
35 | exit -3
36 | fi
37 |
38 | echo -e "Create project ${projname} in ${gopath}/src succeed."
39 |
40 | # init a git repo
41 | echo -ne "Do you want to init a git repo[N/y]: "
42 | read initgit
43 | if [ "${initgit}" == "y" ] || [ "${rmdemo}" == "Y" ]; then
44 | cd ${gopath}/src/${projname} && git init && git add . && git commit -m "init project with pink-lady"
45 | cp ${gopath}/src/${projname}/misc/scripts/pre-push.githook ${gopath}/src/${projname}/.git/hooks/pre-push
46 | chmod +x ${gopath}/src/${projname}/.git/hooks/pre-push
47 | fi
48 | }
49 | main
50 |
--------------------------------------------------------------------------------
/misc/scripts/pre-push.githook:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # An example hook script to verify what is about to be committed.
4 | # Called by "git commit" with no arguments. The hook should
5 | # exit with non-zero status after issuing an appropriate message if
6 | # it wants to stop the commit.
7 | #
8 | # To enable this hook, rename this file to "pre-commit".
9 | realpath() {
10 | [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
11 | }
12 |
13 | hooks_path=$(dirname "`realpath $0`")
14 | git_path=$(dirname "$hooks_path")
15 | project_path=$(dirname "$git_path")
16 |
17 | cd $project_path
18 | go fmt ./... && go vet ./... && go test -race ./...
19 |
--------------------------------------------------------------------------------
/misc/sqls/README.md:
--------------------------------------------------------------------------------
1 | sql文件存放目录
2 |
--------------------------------------------------------------------------------
/misc/sqls/export_struct.sh:
--------------------------------------------------------------------------------
1 | if [ $# != 1 ]; then
2 | echo "需要指定表名"
3 | exit -1
4 | fi
5 | table2struct --tag_gorm --db_host localhost --db_port 3306 --db_user root --db_pwd roooooot --db_name test $1
6 |
--------------------------------------------------------------------------------
/models/fund_test.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "testing"
7 |
8 | "github.com/axiaoxin-com/investool/datacenter/eastmoney"
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | func TestNewFund(t *testing.T) {
13 | ctx := context.TODO()
14 | efund, err := eastmoney.NewEastMoney().QueryFundInfo(ctx, "260104")
15 | require.Nil(t, err)
16 | fund := NewFund(ctx, efund)
17 | b, err := json.Marshal(fund)
18 | require.Nil(t, err)
19 | t.Log(string(b))
20 | }
21 |
--------------------------------------------------------------------------------
/models/global.go:
--------------------------------------------------------------------------------
1 | // Package models
2 | // 全局变量
3 |
4 | package models
5 |
6 | import (
7 | "encoding/json"
8 | "io/ioutil"
9 | "time"
10 |
11 | "github.com/axiaoxin-com/investool/datacenter/eastmoney"
12 | "github.com/axiaoxin-com/logging"
13 | )
14 |
15 | var (
16 | // StockIndustryList 东方财富股票行业列表
17 | StockIndustryList []string
18 | // FundTypeList 基金类型列表
19 | FundTypeList []string
20 | // Fund4433TypeList 4433基金类型列表
21 | Fund4433TypeList []string
22 | // FundAllList 全量基金列表
23 | FundAllList FundList
24 | // Fund4433List 满足4433法则的基金列表
25 | Fund4433List FundList
26 | // FundManagers 基金经理列表
27 | FundManagers eastmoney.FundManagerInfoList
28 | // SyncFundTime 基金数据同步时间
29 | SyncFundTime = time.Now()
30 | // RawFundAllListFilename api返回的原始结果
31 | RawFundAllListFilename = "./eastmoney_funds_list.json"
32 | // FundAllListFilename 基金列表数据文件
33 | FundAllListFilename = "./fund_all_list.json"
34 | // Fund4433ListFilename 4433基金列表数据文件
35 | Fund4433ListFilename = "./fund_4433_list.json"
36 | // IndustryListFilename 行业列表数据文件
37 | IndustryListFilename = "./industry_list.json"
38 | // FundTypeListFilename 基金类型数据文件
39 | FundTypeListFilename = "./fund_type_list.json"
40 | // FundManagersFilename 基金经理数据文件
41 | FundManagersFilename = "./fund_managers.json"
42 | // AAACompanyBondSyl AAA公司债当期收益率
43 | AAACompanyBondSyl = -1.0 // datacenter.ChinaBond.QueryAAACompanyBondSyl(context.Background())
44 | )
45 |
46 | // InitGlobalVars 初始化全局变量
47 | func InitGlobalVars() {
48 | if err := InitIndustryList(); err != nil {
49 | logging.Error(nil, "init models global vars error:"+err.Error())
50 | }
51 | if err := InitFundAllList(); err != nil {
52 | logging.Error(nil, "init models global vars error:"+err.Error())
53 | }
54 | if err := InitFund4433List(); err != nil {
55 | logging.Error(nil, "init models global vars error:"+err.Error())
56 | }
57 | if err := InitFundTypeList(); err != nil {
58 | logging.Error(nil, "init models global vars error:"+err.Error())
59 | }
60 | if err := InitFundManagers(); err != nil {
61 | logging.Error(nil, "init models global vars error:"+err.Error())
62 | }
63 | // 更新同步时间
64 | SyncFundTime = time.Now()
65 | }
66 |
67 | // InitIndustryList 初始化行业列表
68 | func InitIndustryList() error {
69 | indlist, err := ioutil.ReadFile(IndustryListFilename)
70 | if err != nil {
71 | return err
72 | }
73 | return json.Unmarshal(indlist, &StockIndustryList)
74 | }
75 |
76 | // InitFundAllList 从json文件加载基金列表
77 | func InitFundAllList() error {
78 | fundlist, err := ioutil.ReadFile(FundAllListFilename)
79 | if err != nil {
80 | return err
81 | }
82 | return json.Unmarshal(fundlist, &FundAllList)
83 | }
84 |
85 | // InitFund4433List 从json文件加载基金列表
86 | func InitFund4433List() error {
87 | fundlist, err := ioutil.ReadFile(Fund4433ListFilename)
88 | if err != nil {
89 | return err
90 | }
91 | if err := json.Unmarshal(fundlist, &Fund4433List); err != nil {
92 | return err
93 | }
94 | Fund4433List.Sort(FundSortTypeWeek)
95 | Fund4433TypeList = Fund4433List.Types()
96 | return nil
97 | }
98 |
99 | // InitFundTypeList 从json文件加载基金类型
100 | func InitFundTypeList() error {
101 | types, err := ioutil.ReadFile(FundTypeListFilename)
102 | if err != nil {
103 | return err
104 | }
105 | return json.Unmarshal(types, &FundTypeList)
106 | }
107 |
108 | // InitFundManagers 初始化基金经理列表
109 | func InitFundManagers() error {
110 | m, err := ioutil.ReadFile(FundManagersFilename)
111 | if err != nil {
112 | return err
113 | }
114 | return json.Unmarshal(m, &FundManagers)
115 | }
116 |
--------------------------------------------------------------------------------
/models/models.go:
--------------------------------------------------------------------------------
1 | // Package models 定义数据库 model
2 | package models
3 |
--------------------------------------------------------------------------------
/models/models_test.go:
--------------------------------------------------------------------------------
1 | package models
2 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # git tag first
3 | echo "release..."
4 | goreleaser release --rm-dist
5 |
--------------------------------------------------------------------------------
/routes/about.go:
--------------------------------------------------------------------------------
1 | // 关于
2 |
3 | package routes
4 |
5 | import (
6 | "net/http"
7 |
8 | "github.com/axiaoxin-com/investool/version"
9 | "github.com/gin-gonic/gin"
10 | "github.com/spf13/viper"
11 | )
12 |
13 | // About godoc
14 | func About(c *gin.Context) {
15 | data := gin.H{
16 | "Env": viper.GetString("env"),
17 | "Version": version.Version,
18 | "PageTitle": "InvesTool | 关于",
19 | "HostURL": viper.GetString("server.host_url"),
20 | }
21 | c.HTML(http.StatusOK, "about.html", data)
22 | return
23 | }
24 |
--------------------------------------------------------------------------------
/routes/comment.go:
--------------------------------------------------------------------------------
1 | // 评论留言
2 |
3 | package routes
4 |
5 | import (
6 | "net/http"
7 |
8 | "github.com/axiaoxin-com/investool/version"
9 | "github.com/gin-gonic/gin"
10 | "github.com/spf13/viper"
11 | )
12 |
13 | // Comment godoc
14 | func Comment(c *gin.Context) {
15 | data := gin.H{
16 | "Env": viper.GetString("env"),
17 | "Version": version.Version,
18 | "PageTitle": "InvesTool | 留言",
19 | "HostURL": viper.GetString("server.host_url"),
20 | }
21 | c.HTML(http.StatusOK, "comment.html", data)
22 | return
23 | }
24 |
--------------------------------------------------------------------------------
/routes/docs/docs.go:
--------------------------------------------------------------------------------
1 | // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
2 | // This file was generated by swaggo/swag
3 |
4 | package docs
5 |
6 | import (
7 | "bytes"
8 | "encoding/json"
9 | "strings"
10 |
11 | "github.com/alecthomas/template"
12 | "github.com/swaggo/swag"
13 | )
14 |
15 | var doc = `{
16 | "schemes": {{ marshal .Schemes }},
17 | "swagger": "2.0",
18 | "info": {
19 | "description": "{{.Description}}",
20 | "title": "{{.Title}}",
21 | "contact": {},
22 | "version": "{{.Version}}"
23 | },
24 | "host": "{{.Host}}",
25 | "basePath": "{{.BasePath}}",
26 | "paths": {
27 | "/x/ping": {
28 | "get": {
29 | "security": [
30 | {
31 | "ApiKeyAuth": []
32 | },
33 | {
34 | "BasicAuth": []
35 | }
36 | ],
37 | "description": "返回 server 相关信息,可以用于健康检查",
38 | "consumes": [
39 | "application/json"
40 | ],
41 | "produces": [
42 | "application/json"
43 | ],
44 | "tags": [
45 | "x"
46 | ],
47 | "summary": "默认的 Ping 接口",
48 | "parameters": [
49 | {
50 | "type": "string",
51 | "description": "you can set custom trace id in header",
52 | "name": "trace_id",
53 | "in": "header"
54 | }
55 | ],
56 | "responses": {
57 | "200": {
58 | "description": "OK",
59 | "schema": {
60 | "$ref": "#/definitions/response.Response"
61 | }
62 | }
63 | }
64 | }
65 | }
66 | },
67 | "definitions": {
68 | "response.Response": {
69 | "type": "object",
70 | "properties": {
71 | "code": {
72 | "type": "object"
73 | },
74 | "data": {
75 | "type": "object"
76 | },
77 | "msg": {
78 | "type": "string"
79 | }
80 | }
81 | }
82 | }
83 | }`
84 |
85 | type swaggerInfo struct {
86 | Version string
87 | Host string
88 | BasePath string
89 | Schemes []string
90 | Title string
91 | Description string
92 | }
93 |
94 | // SwaggerInfo holds exported Swagger Info so clients can modify it
95 | var SwaggerInfo = swaggerInfo{
96 | Version: "",
97 | Host: "",
98 | BasePath: "",
99 | Schemes: []string{},
100 | Title: "",
101 | Description: "",
102 | }
103 |
104 | type s struct{}
105 |
106 | func (s *s) ReadDoc() string {
107 | sInfo := SwaggerInfo
108 | sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1)
109 |
110 | t, err := template.New("swagger_info").Funcs(template.FuncMap{
111 | "marshal": func(v interface{}) string {
112 | a, _ := json.Marshal(v)
113 | return string(a)
114 | },
115 | }).Parse(doc)
116 | if err != nil {
117 | return doc
118 | }
119 |
120 | var tpl bytes.Buffer
121 | if err := t.Execute(&tpl, sInfo); err != nil {
122 | return doc
123 | }
124 |
125 | return tpl.String()
126 | }
127 |
128 | func init() {
129 | swag.Register(swag.Name, &s{})
130 | }
131 |
--------------------------------------------------------------------------------
/routes/docs/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "contact": {}
5 | },
6 | "paths": {
7 | "/x/ping": {
8 | "get": {
9 | "security": [
10 | {
11 | "ApiKeyAuth": []
12 | },
13 | {
14 | "BasicAuth": []
15 | }
16 | ],
17 | "description": "返回 server 相关信息,可以用于健康检查",
18 | "consumes": [
19 | "application/json"
20 | ],
21 | "produces": [
22 | "application/json"
23 | ],
24 | "tags": [
25 | "x"
26 | ],
27 | "summary": "默认的 Ping 接口",
28 | "parameters": [
29 | {
30 | "type": "string",
31 | "description": "you can set custom trace id in header",
32 | "name": "trace_id",
33 | "in": "header"
34 | }
35 | ],
36 | "responses": {
37 | "200": {
38 | "description": "OK",
39 | "schema": {
40 | "$ref": "#/definitions/response.Response"
41 | }
42 | }
43 | }
44 | }
45 | }
46 | },
47 | "definitions": {
48 | "response.Response": {
49 | "type": "object",
50 | "properties": {
51 | "code": {
52 | "type": "object"
53 | },
54 | "data": {
55 | "type": "object"
56 | },
57 | "msg": {
58 | "type": "string"
59 | }
60 | }
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/routes/docs/swagger.yaml:
--------------------------------------------------------------------------------
1 | definitions:
2 | response.Response:
3 | properties:
4 | code:
5 | type: object
6 | data:
7 | type: object
8 | msg:
9 | type: string
10 | type: object
11 | info:
12 | contact: {}
13 | paths:
14 | /x/ping:
15 | get:
16 | consumes:
17 | - application/json
18 | description: 返回 server 相关信息,可以用于健康检查
19 | parameters:
20 | - description: you can set custom trace id in header
21 | in: header
22 | name: trace_id
23 | type: string
24 | produces:
25 | - application/json
26 | responses:
27 | "200":
28 | description: OK
29 | schema:
30 | $ref: '#/definitions/response.Response'
31 | security:
32 | - ApiKeyAuth: []
33 | - BasicAuth: []
34 | summary: 默认的 Ping 接口
35 | tags:
36 | - x
37 | swagger: "2.0"
38 |
--------------------------------------------------------------------------------
/routes/materials.go:
--------------------------------------------------------------------------------
1 | // 学习资料页面
2 |
3 | package routes
4 |
5 | import (
6 | "encoding/json"
7 | "net/http"
8 |
9 | "github.com/axiaoxin-com/investool/statics"
10 | "github.com/axiaoxin-com/investool/version"
11 | "github.com/axiaoxin-com/logging"
12 | "github.com/gin-gonic/gin"
13 | "github.com/spf13/viper"
14 | )
15 |
16 | // MaterialItem 学习资料具体信息
17 | type MaterialItem struct {
18 | Name string `json:"name"`
19 | DownloadURL string `json:"download_url"`
20 | Desc string `json:"desc"`
21 | }
22 |
23 | // MaterialSeries 某一个系列的资料
24 | //
25 | // {
26 | // "飙股在线等": [
27 | // MaterialItem, ...
28 | // ]
29 | // }
30 | type MaterialSeries map[string][]MaterialItem
31 |
32 | // TypedMaterialSeries 对MaterialSeries进行分类,如:视频、电子书等
33 | //
34 | // {
35 | // "videos": [
36 | // MaterialSeries, ...
37 | // ],
38 | // "ebooks": [
39 | // MaterialSeries, ...
40 | // ]
41 | // }
42 | type TypedMaterialSeries map[string][]MaterialSeries
43 |
44 | // AllMaterialsList 包含全部资料信息的大JSON列表
45 | // [
46 | //
47 | // TypedMaterialSeries, ...
48 | //
49 | // ]
50 | type AllMaterialsList []TypedMaterialSeries
51 |
52 | // MaterialsFilename 资料JSON文件路径
53 | var MaterialsFilename = "materials"
54 |
55 | // Materials godoc
56 | func Materials(c *gin.Context) {
57 | data := gin.H{
58 | "Env": viper.GetString("env"),
59 | "HostURL": viper.GetString("server.host_url"),
60 | "Version": version.Version,
61 | "PageTitle": "InvesTool | 资料",
62 | }
63 | f, err := statics.Files.ReadFile(MaterialsFilename)
64 | if err != nil {
65 | logging.Errorf(c, "Read MaterialsFilename:%v err:%v", MaterialsFilename, err)
66 | data["Error"] = err
67 | c.HTML(http.StatusOK, "materials.html", data)
68 | return
69 | }
70 | var mlist AllMaterialsList
71 | if err := json.Unmarshal(f, &mlist); err != nil {
72 | logging.Errorf(c, "json Unmarshal AllMaterialsList err:%v", err)
73 | data["Error"] = err
74 | c.HTML(http.StatusOK, "materials.html", data)
75 | return
76 | }
77 | data["AllMaterialsList"] = mlist
78 | c.HTML(http.StatusOK, "materials.html", data)
79 | return
80 | }
81 |
--------------------------------------------------------------------------------
/routes/ping.go:
--------------------------------------------------------------------------------
1 | // 默认实现的 ping api
2 |
3 | package routes
4 |
5 | import (
6 | "github.com/axiaoxin-com/investool/routes/response"
7 | "github.com/axiaoxin-com/investool/version"
8 |
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | // Ping godoc
13 | // @Summary 默认的 Ping 接口
14 | // @Description 返回 server 相关信息,可以用于健康检查
15 | // @Tags x
16 | // @Accept json
17 | // @Produce json
18 | // @Success 200 {object} response.Response
19 | // @Security ApiKeyAuth
20 | // @Security BasicAuth
21 | // @Param trace_id header string false "you can set custom trace id in header"
22 | // @Router /x/ping [get]
23 | func Ping(c *gin.Context) {
24 | data := gin.H{
25 | "version": version.Version,
26 | }
27 | response.JSON(c, data)
28 | return
29 | }
30 |
--------------------------------------------------------------------------------
/routes/ping_test.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/axiaoxin-com/goutils"
7 | "github.com/gin-gonic/gin"
8 | "github.com/spf13/viper"
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestPing(t *testing.T) {
13 | gin.SetMode(gin.ReleaseMode)
14 | r := gin.New()
15 | viper.Set("basic_auth.username", "admin")
16 | viper.Set("basic_auth.password", "admin")
17 | defer viper.Reset()
18 | Register(r)
19 | recorder, err := goutils.RequestHTTPHandler(
20 | r,
21 | "GET",
22 | "/x/ping",
23 | nil,
24 | map[string]string{"Authorization": "Basic YWRtaW46YWRtaW4="},
25 | )
26 | assert.Nil(t, err)
27 | assert.Equal(t, recorder.Code, 200)
28 | }
29 |
--------------------------------------------------------------------------------
/routes/query_fund_by_stock.go:
--------------------------------------------------------------------------------
1 | // 根据股票查基金
2 |
3 | package routes
4 |
5 | import (
6 | "net/http"
7 |
8 | "github.com/axiaoxin-com/goutils"
9 | "github.com/axiaoxin-com/investool/core"
10 | "github.com/axiaoxin-com/investool/version"
11 | "github.com/gin-gonic/gin"
12 | "github.com/spf13/viper"
13 | )
14 |
15 | // ParamQueryFundByStock QueryFundByStock 请求参数
16 | type ParamQueryFundByStock struct {
17 | Keywords string `form:"keywords" binding:"required"`
18 | }
19 |
20 | // QueryFundByStock 股票选基
21 | func QueryFundByStock(c *gin.Context) {
22 | data := gin.H{
23 | "Env": viper.GetString("env"),
24 | "HostURL": viper.GetString("server.host_url"),
25 | "Version": version.Version,
26 | "PageTitle": "InvesTool | 基金 | 股票选基",
27 | "Error": "",
28 | }
29 |
30 | param := ParamQueryFundByStock{}
31 | if err := c.ShouldBind(¶m); err != nil {
32 | data["Error"] = err.Error()
33 | c.JSON(http.StatusOK, data)
34 | return
35 | }
36 | keywords := goutils.SplitStringFields(param.Keywords)
37 | searcher := core.NewSearcher(c)
38 | dlist, err := searcher.SearchFundByStock(c, keywords...)
39 | if err != nil {
40 | data["Error"] = err.Error()
41 | c.JSON(http.StatusOK, data)
42 | return
43 | }
44 | data["Funds"] = dlist
45 | c.HTML(http.StatusOK, "hold_stock_fund.html", data)
46 | return
47 | }
48 |
--------------------------------------------------------------------------------
/routes/register.go:
--------------------------------------------------------------------------------
1 | // @contact.name API Support
2 | // @contact.url http://github.com/axiaoxin-com/investool
3 | // @contact.email 254606826@qq.com
4 |
5 | // @license.name Apache 2.0
6 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html
7 |
8 | // @securityDefinitions.basic BasicAuth
9 |
10 | // @securityDefinitions.apikey ApiKeyAuth
11 | // @in header
12 | // @name apikey
13 |
14 | package routes
15 |
16 | import (
17 | "net/http"
18 |
19 | "github.com/axiaoxin-com/investool/routes/docs"
20 | "github.com/axiaoxin-com/investool/statics"
21 | "github.com/axiaoxin-com/investool/version"
22 | "github.com/axiaoxin-com/investool/webserver"
23 | "github.com/axiaoxin-com/logging"
24 | "github.com/gin-contrib/pprof"
25 |
26 | // docs is generated by Swag CLI, you have to import it.
27 | _ "github.com/axiaoxin-com/investool/routes/docs"
28 | "github.com/spf13/viper"
29 |
30 | "github.com/gin-gonic/gin"
31 | swaggerFiles "github.com/swaggo/files"
32 | ginSwagger "github.com/swaggo/gin-swagger"
33 | )
34 |
35 | const (
36 | // DisableGinSwaggerEnvkey 设置该环境变量时关闭 swagger 文档
37 | DisableGinSwaggerEnvkey = "DISABLE_GIN_SWAGGER"
38 | )
39 |
40 | // Register 在 gin engine 上注册 url 对应的 HandlerFunc
41 | func Register(httpHandler http.Handler) {
42 | app, ok := httpHandler.(*gin.Engine)
43 | if !ok {
44 | panic("HTTP handler must be *gin.Engine")
45 | }
46 |
47 | // api 文档变量设置,注意这里依赖 viper 读配置,需要保证在 main 中已预先加载这些配置项
48 | docs.SwaggerInfo.Title = viper.GetString("apidocs.title")
49 | docs.SwaggerInfo.Description = viper.GetString("apidocs.desc")
50 | docs.SwaggerInfo.Version = version.Version
51 | docs.SwaggerInfo.Host = viper.GetString("apidocs.host")
52 | docs.SwaggerInfo.BasePath = viper.GetString("apidocs.basepath")
53 | docs.SwaggerInfo.Schemes = viper.GetStringSlice("apidocs.schemes")
54 |
55 | // Group x 默认 url 路由
56 | x := app.Group("/x", webserver.GinBasicAuth())
57 | {
58 | if viper.GetBool("server.pprof") {
59 | pprof.RouteRegister(x, "/pprof")
60 | }
61 | if viper.GetBool("server.metrics") {
62 | x.GET("/metrics", webserver.PromExporterHandler())
63 | }
64 | // ginSwagger 生成的在线 API 文档路由
65 | x.GET("/apidocs/*any", ginSwagger.DisablingWrapHandler(swaggerFiles.Handler, DisableGinSwaggerEnvkey))
66 | // 默认的 ping 方法,返回 server 相关信息
67 | x.Any("/ping", Ping)
68 | }
69 |
70 | // 注册 favicon.ico 和 robots.txt
71 | app.GET("/favicon.ico", func(c *gin.Context) {
72 | file, err := statics.Files.ReadFile("favicon.ico")
73 | if err != nil {
74 | logging.Error(c, "read favicon file error:"+err.Error())
75 | }
76 | c.Data(http.StatusOK, "image/x-icon", file)
77 | return
78 | })
79 | app.GET("/robots.txt", func(c *gin.Context) {
80 | file, err := statics.Files.ReadFile("robots.txt")
81 | if err != nil {
82 | logging.Error(c, "read robots file error:"+err.Error())
83 | }
84 | c.Data(http.StatusOK, "text/plain", file)
85 | return
86 | })
87 | app.GET("/ads.txt", func(c *gin.Context) {
88 | file, err := statics.Files.ReadFile("ads.txt")
89 | if err != nil {
90 | logging.Error(c, "read ads file error:"+err.Error())
91 | }
92 | c.Data(http.StatusOK, "text/plain", file)
93 | return
94 | })
95 | app.GET("/apple-touch-icon-120x120-precomposed.png", func(c *gin.Context) {
96 | file, err := statics.Files.ReadFile("img/sidenav_icon.png")
97 | if err != nil {
98 | logging.Error(c, "read favicon file error:"+err.Error())
99 | }
100 | c.Data(http.StatusOK, "image/x-icon", file)
101 | return
102 | })
103 | app.GET("/apple-touch-icon-120x120.png", func(c *gin.Context) {
104 | file, err := statics.Files.ReadFile("img/sidenav_icon.png")
105 | if err != nil {
106 | logging.Error(c, "read favicon file error:"+err.Error())
107 | }
108 | c.Data(http.StatusOK, "image/x-icon", file)
109 | return
110 | })
111 | app.GET("/apple-touch-icon-precomposed.png", func(c *gin.Context) {
112 | file, err := statics.Files.ReadFile("img/sidenav_icon.png")
113 | if err != nil {
114 | logging.Error(c, "read favicon file error:"+err.Error())
115 | }
116 | c.Data(http.StatusOK, "image/x-icon", file)
117 | return
118 | })
119 | app.GET("/apple-touch-icon.png", func(c *gin.Context) {
120 | file, err := statics.Files.ReadFile("img/sidenav_icon.png")
121 | if err != nil {
122 | logging.Error(c, "read favicon file error:"+err.Error())
123 | }
124 | c.Data(http.StatusOK, "image/x-icon", file)
125 | return
126 | })
127 |
128 | // 注册其他 gin HandlerFunc
129 | Routes(app)
130 | }
131 |
--------------------------------------------------------------------------------
/routes/register_test.go:
--------------------------------------------------------------------------------
1 | package routes
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/axiaoxin-com/goutils"
7 | "github.com/gin-gonic/gin"
8 | "github.com/spf13/viper"
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestRegisterRoutes(t *testing.T) {
13 | gin.SetMode(gin.ReleaseMode)
14 | r := gin.New()
15 | // Register 中的 basic auth 依赖 viper 配置
16 | viper.Set("basic_auth.username", "admin")
17 | viper.Set("basic_auth.password", "admin")
18 | viper.Set("env", "localhost")
19 | defer viper.Reset()
20 |
21 | Register(r)
22 | recorder, err := goutils.RequestHTTPHandler(
23 | r,
24 | "GET",
25 | "/x/ping",
26 | nil,
27 | map[string]string{"Authorization": "Basic YWRtaW46YWRtaW4="},
28 | )
29 | assert.Nil(t, err)
30 | assert.Equal(t, recorder.Code, 200)
31 | }
32 |
--------------------------------------------------------------------------------
/routes/response/README.md:
--------------------------------------------------------------------------------
1 | # response
2 |
3 | ## 功能
4 |
5 |
6 | ## 用法
7 |
--------------------------------------------------------------------------------
/routes/response/errcode.go:
--------------------------------------------------------------------------------
1 | // 业务错误码定义
2 |
3 | package response
4 |
5 | import (
6 | "strings"
7 |
8 | "github.com/axiaoxin-com/goutils"
9 | )
10 |
11 | // 错误码中的 code 定义
12 | const (
13 | failure = iota - 1
14 | success
15 | invalidParam
16 | notFound
17 | unknownError
18 | )
19 |
20 | // 错误码对象定义
21 | var (
22 | CodeSuccess = goutils.NewErrCode(success, "Success")
23 | CodeFailure = goutils.NewErrCode(failure, "Failure")
24 | CodeInvalidParam = goutils.NewErrCode(invalidParam, "Invalid Param")
25 | CodeNotFound = goutils.NewErrCode(notFound, "Not Fount")
26 | CodeInternalError = goutils.NewErrCode(unknownError, "Unknown Error")
27 | )
28 |
29 | // IsInvalidParamError 判断错误信息中是否包含:参数错误
30 | func IsInvalidParamError(err error) bool {
31 | return strings.Contains(err.Error(), "Invalid Param")
32 | }
33 |
--------------------------------------------------------------------------------
/routes/response/errcode_test.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestErrCode(t *testing.T) {
10 | assert.Equal(t, CodeSuccess.Code(), success)
11 | }
12 |
--------------------------------------------------------------------------------
/routes/response/response.go:
--------------------------------------------------------------------------------
1 | // Package response 提供统一的 JSON 返回结构,可以通过配置设置具体返回的 code 字段为 int 或者 string
2 | package response
3 |
4 | import (
5 | "fmt"
6 | "net/http"
7 |
8 | "github.com/axiaoxin-com/goutils"
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | // Response 统一的返回结构定义
13 | type Response struct {
14 | Code interface{} `json:"code"`
15 | Msg string `json:"msg"`
16 | Data interface{} `json:"data"`
17 | }
18 |
19 | // JSON 返回 HTTP 状态码为 200 的统一成功结构
20 | func JSON(c *gin.Context, data interface{}) {
21 | Respond(c, http.StatusOK, data, CodeSuccess)
22 | }
23 |
24 | // ErrJSON 返回 HTTP 状态码为 200 的统一失败结构
25 | func ErrJSON(c *gin.Context, err error, extraMsgs ...interface{}) {
26 | Respond(c, http.StatusOK, nil, err, extraMsgs...)
27 | }
28 |
29 | // Respond encapsulates c.JSON
30 | // debug mode respond indented json
31 | func Respond(c *gin.Context, status int, data interface{}, errcode error, extraMsgs ...interface{}) {
32 | // 初始化 code 、 msg 为失败
33 | code, msg, _ := CodeFailure.Decode()
34 |
35 | if ec, ok := errcode.(*goutils.ErrCode); ok {
36 | // 如果是返回码,正常处理
37 | code, msg, _ = ec.Decode()
38 | // 存在 errs 则将 errs 信息添加的 msg
39 | if len(ec.Errs()) > 0 {
40 | msg = fmt.Sprint(msg, " ", ec.Error())
41 | }
42 | } else {
43 | // 支持 errcode 参数直接传 error ,如果是 error ,则将 error 信息添加到 msg
44 | msg = fmt.Sprint(msg, " ", errcode.Error())
45 | }
46 |
47 | // 将 extraMsgs 添加到 msg
48 | if len(extraMsgs) > 0 {
49 | msg = fmt.Sprint(msg, "; ", extraMsgs)
50 | }
51 |
52 | resp := Response{
53 | Code: code,
54 | Msg: msg,
55 | Data: data,
56 | }
57 | c.Header("x-response-code", fmt.Sprint(code))
58 | if gin.Mode() == gin.ReleaseMode {
59 | c.JSON(status, resp)
60 | } else {
61 | c.IndentedJSON(status, resp)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/routes/response/response_test.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "net/http/httptest"
7 | "strings"
8 | "testing"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/stretchr/testify/require"
12 | )
13 |
14 | func TestJSON(t *testing.T) {
15 | gin.SetMode(gin.ReleaseMode)
16 | responseWriter := httptest.NewRecorder()
17 | c, _ := gin.CreateTestContext(responseWriter)
18 | JSON(c, gin.H{"k": "v"})
19 | if c.Writer.Status() != 200 {
20 | t.Fatal("http status code error")
21 | }
22 | require.Equal(t, c.Writer.Status(), 200)
23 |
24 | j := responseWriter.Body.Bytes()
25 | r := Response{}
26 | err := json.Unmarshal(j, &r)
27 | require.Nil(t, err)
28 | require.Equal(t, r.Data.(map[string]interface{})["k"].(string), "v")
29 | }
30 |
31 | func TestErrJSON(t *testing.T) {
32 | gin.SetMode(gin.ReleaseMode)
33 | responseWriter := httptest.NewRecorder()
34 | c, _ := gin.CreateTestContext(responseWriter)
35 | ErrJSON(c, CodeInvalidParam)
36 | require.Equal(t, c.Writer.Status(), 200)
37 | j := responseWriter.Body.Bytes()
38 | r := Response{}
39 | err := json.Unmarshal(j, &r)
40 | require.Nil(t, err)
41 | }
42 |
43 | func TestRespond(t *testing.T) {
44 | gin.SetMode(gin.ReleaseMode)
45 | responseWriter := httptest.NewRecorder()
46 | c, _ := gin.CreateTestContext(responseWriter)
47 | Respond(c, 200, gin.H{"k": "v"}, CodeSuccess)
48 | require.Equal(t, c.Writer.Status(), 200)
49 | j := responseWriter.Body.Bytes()
50 | r := Response{}
51 | err := json.Unmarshal(j, &r)
52 | require.Nil(t, err)
53 | require.Equal(t, r.Data.(map[string]interface{})["k"].(string), "v")
54 | }
55 |
56 | func TestRespondWithExtraMsg(t *testing.T) {
57 | gin.SetMode(gin.ReleaseMode)
58 | responseWriter := httptest.NewRecorder()
59 | c, _ := gin.CreateTestContext(responseWriter)
60 | Respond(c, 200, gin.H{"k": "v"}, CodeSuccess, "xxx")
61 | require.Equal(t, c.Writer.Status(), 200)
62 | j := responseWriter.Body.Bytes()
63 | r := Response{}
64 | err := json.Unmarshal(j, &r)
65 | require.Nil(t, err)
66 | require.Equal(t, r.Data.(map[string]interface{})["k"].(string), "v")
67 | require.Equal(t, strings.Contains(r.Msg, "xxx"), true)
68 | }
69 |
70 | func TestRespondWithError(t *testing.T) {
71 | gin.SetMode(gin.ReleaseMode)
72 | responseWriter := httptest.NewRecorder()
73 | c, _ := gin.CreateTestContext(responseWriter)
74 | Respond(c, 200, gin.H{"k": "v"}, errors.New("errxx"), "xxx")
75 | require.Equal(t, c.Writer.Status(), 200)
76 | j := responseWriter.Body.Bytes()
77 | r := Response{}
78 | err := json.Unmarshal(j, &r)
79 | require.Nil(t, err)
80 | require.Equal(t, r.Data.(map[string]interface{})["k"].(string), "v")
81 | require.Equal(t, strings.Contains(r.Msg, "errxx"), true)
82 | }
83 |
--------------------------------------------------------------------------------
/routes/routes.go:
--------------------------------------------------------------------------------
1 | // 在这个文件中注册 URL handler
2 |
3 | package routes
4 |
5 | import "github.com/gin-gonic/gin"
6 |
7 | // Routes 注册 API URL 路由
8 | func Routes(app *gin.Engine) {
9 | app.GET("/", FundIndex)
10 | app.GET("/stock", StockIndex)
11 | app.POST("/selector", StockSelector)
12 | app.POST("/checker", StockChecker)
13 | app.GET("/fund", FundIndex)
14 | app.GET("/fund/filter", FundFilter)
15 | app.POST("/fund/check", FundCheck)
16 | app.GET("/about", About)
17 | app.GET("/comment", Comment)
18 | app.GET("/fund/similarity", FundSimilarity)
19 | app.GET("/materials", Materials)
20 | app.POST("/fund/query_by_stock", QueryFundByStock)
21 | app.GET("/fund/managers", FundManagers)
22 | }
23 |
--------------------------------------------------------------------------------
/statics/README.md:
--------------------------------------------------------------------------------
1 | 静态资源文件目录
2 |
--------------------------------------------------------------------------------
/statics/ads.txt:
--------------------------------------------------------------------------------
1 | google.com, pub-3022214826355647, DIRECT, f08c47fec0942fa0
2 |
--------------------------------------------------------------------------------
/statics/css/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/statics/css/README.md
--------------------------------------------------------------------------------
/statics/css/app.css:
--------------------------------------------------------------------------------
1 | body {
2 | display: flex;
3 | min-height: 100vh;
4 | flex-direction: column;
5 | }
6 |
7 | main {
8 | flex: 1 0 auto;
9 | }
10 |
11 |
12 |
13 | h1 {
14 | font-weight: bold;
15 | font-size: 32px;
16 | }
17 |
18 | h2 {
19 | font-weight: bold;
20 | font-size: 26px;
21 | }
22 |
23 | h3 {
24 | font-weight: bold;
25 | font-size: 22px;
26 | }
27 |
28 | h4 {
29 | font-weight: bold;
30 | font-size: 20px;
31 | }
32 |
33 | ins.adsbygoogle[data-ad-status="unfilled"] {
34 | display:none !important
35 | }
36 |
--------------------------------------------------------------------------------
/statics/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/statics/favicon.ico
--------------------------------------------------------------------------------
/statics/font/exportor.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/statics/font/exportor.ttf
--------------------------------------------------------------------------------
/statics/html/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/statics/html/README.md
--------------------------------------------------------------------------------
/statics/html/checker_form.html:
--------------------------------------------------------------------------------
1 | {{ define "checker_form_content" }}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | 仅针对银行股检测生效
56 |
57 |
58 |
59 |
60 | 仅针对银行股检测生效
61 |
62 |
63 |
64 |
65 | 仅针对银行股检测生效
66 |
67 |
68 |
69 |
70 | 仅针对银行股检测生效
71 |
72 |
73 |
74 |
75 |
79 |
83 |
87 |
91 |
95 |
99 |
103 |
107 |
111 |
112 | {{ end }}
113 |
--------------------------------------------------------------------------------
/statics/html/comment.html:
--------------------------------------------------------------------------------
1 | {{ template "header" . }}
2 |
3 | 留言评论
4 |
5 |
6 | 愿您如K线记录股价般凡走过必留下痕迹,欢迎留下您的建议与评论
7 |
8 |
9 |
10 |
12 |
18 |
21 |
22 |
23 |
24 |
25 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
58 |
59 | {{ template "footer" . }}
60 |
--------------------------------------------------------------------------------
/statics/html/fund_filter.html:
--------------------------------------------------------------------------------
1 | {{ template "header" . }}
2 |
3 | 4433基金筛选结果
4 | 以下所有数据与信息仅供参考,不构成投资建议
5 |
6 |
12 |
15 |
16 |
17 | {{ template "fundtable" . }}
18 |
19 | {{ template "footer" . }}
20 |
--------------------------------------------------------------------------------
/statics/html/fund_similarity.html:
--------------------------------------------------------------------------------
1 | {{ template "header" . }}
2 |
3 | 基金持仓相似度help_outline
4 | 以下所有数据与信息仅供参考,不构成投资建议
5 |
6 |
7 |
8 |
9 | 基金名称
10 | 重复持仓
11 | 持仓相似度
12 |
13 |
14 |
15 | {{ range .Result }}
16 |
17 |
18 | {{ .Fund.Name }}({{ .Fund.Code }})
19 |
20 |
21 | {{ range $i, $s := .SameStocks }}
22 | {{ $i }}.{{ $s }}
23 | {{end}}
24 |
25 |
26 | {{ .SimilarityValue }}
27 |
28 |
29 | {{ end }}
30 |
31 |
32 |
33 |
34 |
35 |
36 | 持仓相似度
37 | 采用Jaccard相似系数计算基金持仓相似度,
38 | 相似度越接近1表示重复持仓的股票越多。
39 | 0 表示持仓股票完全不同,
40 | 1 表示持仓股票完全相同。
41 |
42 |
43 |
44 | {{ template "footer" . }}
45 |
--------------------------------------------------------------------------------
/statics/html/hold_stock_fund.html:
--------------------------------------------------------------------------------
1 | {{ template "header" . }}
2 |
3 | 股票选基查询结果
4 | 以下所有数据与信息仅供参考,不构成投资建议
5 |
6 |
7 |
8 | {{ range .Funds }}
9 |
10 |
11 |
12 | {{ .Shortname }}({{ .Fcode }})
13 | content_copy
14 |
15 |
16 |
17 | {{ else }}
18 | 没有找到同时持仓指定股票的基金
19 | {{ end }}
20 |
21 |
22 |
23 | {{ template "footer" . }}
24 |
--------------------------------------------------------------------------------
/statics/html/materials.html:
--------------------------------------------------------------------------------
1 | {{ template "header" . }}
2 | 投资学习资料
3 |
4 |
5 |
12 |
13 | {{ range $TypedMaterialSeries := .AllMaterialsList }}
14 |
15 | {{ range $TypeName, $MaterialSeriesList := $TypedMaterialSeries }}
16 | {{ $TypeName }}
17 |
18 |
24 |
27 |
28 |
29 | {{ range $i, $MaterialSeries := $MaterialSeriesList }}
30 | -
31 | {{ range $SeriesName, $MaterialItemList := $MaterialSeries }}
32 |
{{ $SeriesName }}
33 |
34 | {{ range $MaterialItem := $MaterialItemList }}
35 |
36 |
37 | {{ $MaterialItem.Desc }}
38 |
39 | {{ end }}
40 |
41 | {{ end }}
42 |
43 | {{ end }}
44 |
45 |
46 |
53 | {{ end }}
54 |
55 | {{ end }}
56 | {{ template "footer" . }}
57 |
--------------------------------------------------------------------------------
/statics/html/modal.html:
--------------------------------------------------------------------------------
1 | {{ define "modal" }}
2 |
3 |
10 |
11 |
9 |
12 |
20 |
21 |
16 |
19 |
22 |
30 | {{ end }}
31 |
--------------------------------------------------------------------------------
/statics/img/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/statics/img/README.md
--------------------------------------------------------------------------------
/statics/img/alipay.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/statics/img/alipay.jpg
--------------------------------------------------------------------------------
/statics/img/sidenav_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/statics/img/sidenav_bg.png
--------------------------------------------------------------------------------
/statics/img/sidenav_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/statics/img/sidenav_icon.png
--------------------------------------------------------------------------------
/statics/img/wxpay.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/statics/img/wxpay.jpg
--------------------------------------------------------------------------------
/statics/js/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axiaoxin-com/investool/2323bfdd2ba785cee851f74bb8a3b4a255ddc7d0/statics/js/README.md
--------------------------------------------------------------------------------
/statics/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow:/
3 |
--------------------------------------------------------------------------------
/statics/statics.go:
--------------------------------------------------------------------------------
1 | // Package statics embed 静态文件
2 | package statics
3 |
4 | import "embed"
5 |
6 | // Files 静态文件资源
7 | //go:embed favicon.ico robots.txt ads.txt materials
8 | //go:embed css/* font/* html/* img/* js/*
9 | var Files embed.FS
10 |
--------------------------------------------------------------------------------
/version/version.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | // Version 版本号
4 | var Version = "1.3.4"
5 |
--------------------------------------------------------------------------------
/webserver/README.md:
--------------------------------------------------------------------------------
1 | # webserver 包的使用方法
2 |
3 | 大致流程如下,具体参考 [main.go](./main.go)
4 |
5 | 1. 加载配置文件,根据配置信息个性化 web server
6 |
7 | ```
8 | webserver.InitWithConfigFile(path/to/configfile)
9 | ```
10 |
11 | 2. 创建 app 路由
12 |
13 | ```
14 | app := webserver.NewGinEngine(nil)
15 | ```
16 |
17 | 3. 启动 server
18 |
19 | ```
20 | webserver.Run(app)
21 | ```
22 |
--------------------------------------------------------------------------------
/webserver/gin.go:
--------------------------------------------------------------------------------
1 | package webserver
2 |
3 | import (
4 | "html/template"
5 | "net/http"
6 |
7 | "github.com/axiaoxin-com/goutils"
8 | "github.com/axiaoxin-com/investool/statics"
9 | "github.com/gin-gonic/gin"
10 | "github.com/gin-gonic/gin/binding"
11 | "github.com/json-iterator/go/extra"
12 | "github.com/spf13/viper"
13 | )
14 |
15 | func init() {
16 | // 替换 gin 默认的 validator,更加友好的错误信息
17 | binding.Validator = &goutils.GinStructValidator{}
18 | // causes the json binding Decoder to unmarshal a number into an interface{} as a Number instead of as a float64.
19 | binding.EnableDecoderUseNumber = true
20 |
21 | // jsoniter 启动模糊模式来支持 PHP 传递过来的 JSON。容忍字符串和数字互转
22 | extra.RegisterFuzzyDecoders()
23 | // jsoniter 设置支持 private 的 field
24 | extra.SupportPrivateFields()
25 | }
26 |
27 | // NewGinEngine 根据参数创建 gin 的 router engine
28 | // middlewares 需要使用到的中间件列表,默认不为 engine 添加任何中间件
29 | func NewGinEngine(middlewares ...gin.HandlerFunc) *gin.Engine {
30 | // set gin mode
31 | gin.SetMode(viper.GetString("server.mode"))
32 |
33 | engine := gin.New()
34 | // ///a///b -> /a/b
35 | engine.RemoveExtraSlash = true
36 |
37 | // use middlewares
38 | for _, middleware := range middlewares {
39 | engine.Use(middleware)
40 | }
41 |
42 | // load html template
43 | tmplPath := viper.GetString("statics.tmpl_path")
44 | if tmplPath != "" {
45 | // add temp func for template parse
46 | // template func usage: {{ funcname xx }}
47 | t := template.Must(template.New("").Funcs(TemplFuncs).ParseFS(&statics.Files, tmplPath))
48 | engine.SetHTMLTemplate(t)
49 | }
50 | // register statics
51 | staticsURL := viper.GetString("statics.url")
52 | if staticsURL != "" {
53 | engine.StaticFS(staticsURL, http.FS(&statics.Files))
54 | }
55 |
56 | return engine
57 | }
58 |
--------------------------------------------------------------------------------
/webserver/gin_middlewares.go:
--------------------------------------------------------------------------------
1 | package webserver
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net"
7 | "net/http"
8 | "os"
9 | "runtime/debug"
10 | "strings"
11 | "time"
12 |
13 | "github.com/axiaoxin-com/goutils"
14 | "github.com/axiaoxin-com/logging"
15 | "github.com/axiaoxin-com/ratelimiter"
16 | "github.com/gin-gonic/gin"
17 | "github.com/spf13/viper"
18 | )
19 |
20 | // GinBasicAuth gin 的基础认证中间件
21 | // 加到 gin app 的路由中可以对该路由添加 basic auth 登录验证
22 | // 传入 username 和 password 对可以替换默认的 username 和 password
23 | func GinBasicAuth(args ...string) gin.HandlerFunc {
24 | username := viper.GetString("basic_auth.username")
25 | password := viper.GetString("basic_auth.password")
26 | if len(args) == 2 {
27 | username = args[0]
28 | password = args[1]
29 | }
30 | logging.Debug(nil, "Basic auth username:"+username+" password:"+password)
31 | return gin.BasicAuth(gin.Accounts{
32 | username: password,
33 | })
34 | }
35 |
36 | // GinRecovery gin recovery 中间件
37 | // save err in context and abort with recoveryHandler
38 | func GinRecovery(
39 | recoveryHandler ...func(c *gin.Context, status int, data interface{}, err error, extraMsgs ...interface{}),
40 | ) gin.HandlerFunc {
41 | return func(c *gin.Context) {
42 | defer func() {
43 | status := c.Writer.Status()
44 | if err := recover(); err != nil {
45 | // Check for a broken connection, as it is not really a
46 | // condition that warrants a panic stack trace.
47 | status = http.StatusInternalServerError
48 | var brokenPipe bool
49 | if ne, ok := err.(*net.OpError); ok {
50 | if se, ok := ne.Err.(*os.SyscallError); ok {
51 | if strings.Contains(strings.ToLower(se.Error()), "broken pipe") ||
52 | strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
53 | brokenPipe = true
54 | }
55 | }
56 | }
57 | if brokenPipe {
58 | // save err in context
59 | c.Error(fmt.Errorf("Broken pipe: %v\n%s", err, string(debug.Stack())))
60 | if len(recoveryHandler) > 0 {
61 | c.Abort()
62 | recoveryHandler[0](c, status, nil, errors.New(http.StatusText(status)))
63 | return
64 | }
65 | c.AbortWithStatus(status)
66 | return
67 | }
68 |
69 | // save err in context when panic
70 | c.Error(fmt.Errorf("Recovery from panic: %v\n%s", err, string(debug.Stack())))
71 | }
72 |
73 | // 状态码大于 400 的以 status handler 进行响应
74 | if status >= 400 {
75 | // 有 handler 时使用 handler 返回
76 | if len(recoveryHandler) > 0 {
77 | c.Abort()
78 | recoveryHandler[0](c, status, nil, errors.New(http.StatusText(status)))
79 | return
80 | }
81 | // 否则只返回状态码
82 | c.AbortWithStatus(status)
83 | return
84 | }
85 | }()
86 | c.Next()
87 | }
88 | }
89 |
90 | // GinLogMiddleware 日志中间件
91 | // 可根据实际需求自行修改定制
92 | func GinLogMiddleware() gin.HandlerFunc {
93 | logging.CtxLoggerName = logging.Ctxkey("ctx_logger")
94 | logging.TraceIDKeyname = logging.Ctxkey("trace_id")
95 | logging.TraceIDPrefix = "logging_"
96 | loggerMiddleware := logging.GinLoggerWithConfig(logging.GinLoggerConfig{
97 | SkipPaths: viper.GetStringSlice("logging.access_logger.skip_paths"),
98 | SkipPathRegexps: viper.GetStringSlice("logging.access_logger.skip_path_regexps"),
99 | EnableDetails: viper.GetBool("logging.access_logger.enable_details"),
100 | EnableContextKeys: viper.GetBool("logging.access_logger.enable_context_keys"),
101 | EnableRequestHeader: viper.GetBool("logging.access_logger.enable_request_header"),
102 | EnableRequestForm: viper.GetBool("logging.access_logger.enable_request_form"),
103 | EnableRequestBody: viper.GetBool("logging.access_logger.enable_request_body"),
104 | EnableResponseBody: viper.GetBool("logging.access_logger.enable_response_body"),
105 | SlowThreshold: viper.GetDuration("logging.access_logger.slow_threshold") * time.Millisecond,
106 | TraceIDFunc: nil,
107 | InitFieldsFunc: nil,
108 | Formatter: nil,
109 | })
110 | return loggerMiddleware
111 | }
112 |
113 | // GinRatelimitMiddleware 限频中间件
114 | // 需先实现对应的 TODO ,可根据实际需求自行修改定制
115 | func GinRatelimitMiddleware() gin.HandlerFunc {
116 | limiterConf := ratelimiter.GinRatelimiterConfig{
117 | // TODO: you should implement this function by yourself
118 | LimitKey: ratelimiter.DefaultGinLimitKey,
119 | // TODO: you should implement this function by yourself
120 | LimitedHandler: ratelimiter.DefaultGinLimitedHandler,
121 | // TODO: you should implement this function by yourself
122 | TokenBucketConfig: func(*gin.Context) (time.Duration, int) {
123 | return time.Second * 1, 20
124 | },
125 | }
126 |
127 | // 根据 viper 中的配置信息选择限流类型
128 | var limiterMiddleware gin.HandlerFunc
129 | limiterType := strings.ToLower(viper.GetString("ratelimiter.type"))
130 | logging.Info(nil, "enable ratelimiter with type: "+limiterType)
131 | if strings.HasPrefix(limiterType, "redis.") {
132 | which := strings.TrimPrefix(limiterType, "redis.")
133 | if rdb, err := goutils.RedisClient(which); err != nil {
134 | panic("redis ratelimiter does not work. get redis client error:" + err.Error())
135 | } else {
136 | limiterMiddleware = ratelimiter.GinRedisRatelimiter(rdb, limiterConf)
137 | }
138 | } else {
139 | limiterMiddleware = ratelimiter.GinMemRatelimiter(limiterConf)
140 | }
141 | return limiterMiddleware
142 | }
143 |
--------------------------------------------------------------------------------
/webserver/gin_templ_func_map.go:
--------------------------------------------------------------------------------
1 | // Functions from Go's strings package usable as template actions
2 |
3 | package webserver
4 |
5 | import (
6 | "html/template"
7 | "strings"
8 | "unicode"
9 |
10 | "github.com/axiaoxin-com/goutils"
11 | )
12 |
13 | // TemplFuncs is a template.FuncMap with functions that can be used as template actions.
14 | var TemplFuncs = template.FuncMap{
15 | "StrContains": func(s, substr string) bool { return strings.Contains(s, substr) },
16 | "StrContainsAny": func(s, chars string) bool { return strings.ContainsAny(s, chars) },
17 | "StrContainsRune": func(s string, r rune) bool { return strings.ContainsRune(s, r) },
18 | "StrCount": func(s, sep string) int { return strings.Count(s, sep) },
19 | "StrEqualFold": func(s, t string) bool { return strings.EqualFold(s, t) },
20 | "StrFields": func(s string) []string { return strings.Fields(s) },
21 | "StrFieldsFunc": func(s string, f func(rune) bool) []string { return strings.FieldsFunc(s, f) },
22 | "StrHasPrefix": func(s, prefix string) bool { return strings.HasPrefix(s, prefix) },
23 | "StrHasSuffix": func(s, suffix string) bool { return strings.HasSuffix(s, suffix) },
24 | "StrIndex": func(s, sep string) int { return strings.Index(s, sep) },
25 | "StrIndexAny": func(s, chars string) int { return strings.IndexAny(s, chars) },
26 | "StrIndexByte": func(s string, c byte) int { return strings.IndexByte(s, c) },
27 | "StrIndexFunc": func(s string, f func(rune) bool) int { return strings.IndexFunc(s, f) },
28 | "StrIndexRune": func(s string, r rune) int { return strings.IndexRune(s, r) },
29 | "StrJoin": func(a []string, sep string) string { return strings.Join(a, sep) },
30 | "StrLastIndex": func(s, sep string) int { return strings.LastIndex(s, sep) },
31 | "StrLastIndexAny": func(s, chars string) int { return strings.LastIndexAny(s, chars) },
32 | "StrLastIndexFunc": func(s string, f func(rune) bool) int { return strings.LastIndexFunc(s, f) },
33 | "StrMap": func(mapping func(rune) rune, s string) string { return strings.Map(mapping, s) },
34 | "StrRepeat": func(s string, count int) string { return strings.Repeat(s, count) },
35 | "StrReplace": func(s, old, new string, n int) string { return strings.Replace(s, old, new, n) },
36 | "StrSplit": func(s, sep string) []string { return strings.Split(s, sep) },
37 | "StrSplitAfter": func(s, sep string) []string { return strings.SplitAfter(s, sep) },
38 | "StrSplitAfterN": func(s, sep string, n int) []string { return strings.SplitAfterN(s, sep, n) },
39 | "StrSplitN": func(s, sep string, n int) []string { return strings.SplitN(s, sep, n) },
40 | "StrTitle": func(s string) string { return strings.Title(s) },
41 | "StrToLower": func(s string) string { return strings.ToLower(s) },
42 | "StrToLowerSpecial": func(_case unicode.SpecialCase, s string) string { return strings.ToLowerSpecial(_case, s) },
43 | "StrToTitle": func(s string) string { return strings.ToTitle(s) },
44 | "StrToTitleSpecial": func(_case unicode.SpecialCase, s string) string { return strings.ToTitleSpecial(_case, s) },
45 | "StrToUpper": func(s string) string { return strings.ToUpper(s) },
46 | "StrToUpperSpecial": func(_case unicode.SpecialCase, s string) string { return strings.ToUpperSpecial(_case, s) },
47 | "StrTrim": func(s string, cutset string) string { return strings.Trim(s, cutset) },
48 | "StrTrimFunc": func(s string, f func(rune) bool) string { return strings.TrimFunc(s, f) },
49 | "StrTrimLeft": func(s string, cutset string) string { return strings.TrimLeft(s, cutset) },
50 | "StrTrimLeftFunc": func(s string, f func(rune) bool) string { return strings.TrimLeftFunc(s, f) },
51 | "StrTrimPrefix": func(s, prefix string) string { return strings.TrimPrefix(s, prefix) },
52 | "StrTrimRight": func(s string, cutset string) string { return strings.TrimRight(s, cutset) },
53 | "StrTrimRightFunc": func(s string, f func(rune) bool) string { return strings.TrimRightFunc(s, f) },
54 | "StrTrimSpace": func(s string) string { return strings.TrimSpace(s) },
55 | "StrTrimSuffix": func(s, suffix string) string { return strings.TrimSuffix(s, suffix) },
56 | "IsStrInSlice": goutils.IsStrInSlice,
57 | "YiWanString": goutils.YiWanString,
58 | "mod": func(i, j int) bool { return i%j == 0 },
59 | }
60 |
--------------------------------------------------------------------------------
/webserver/gin_test.go:
--------------------------------------------------------------------------------
1 | package webserver
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "testing"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/spf13/viper"
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestNewGinEngine(t *testing.T) {
14 | viper.SetDefault("server.mode", "release")
15 | defer viper.Reset()
16 | app := NewGinEngine(nil)
17 | assert.NotNil(t, app)
18 | }
19 |
20 | func TestGinBasicAuth(t *testing.T) {
21 | viper.Set("basic_auth.username", "axiaoxin")
22 | viper.Set("basic_auth.password", "axiaoxin")
23 | defer viper.Reset()
24 | gin.SetMode(gin.ReleaseMode)
25 | c, _ := gin.CreateTestContext(httptest.NewRecorder())
26 | c.Request, _ = http.NewRequest("GET", "/get", nil)
27 | auth := GinBasicAuth()
28 | auth(c)
29 | assert.Equal(t, c.Writer.Status(), http.StatusUnauthorized, "request without basic auth should return StatusUnauthorized")
30 | }
31 |
--------------------------------------------------------------------------------
/webserver/prom.go:
--------------------------------------------------------------------------------
1 | // promethues
2 |
3 | package webserver
4 |
5 | import (
6 | "time"
7 |
8 | "github.com/axiaoxin-com/logging"
9 | "github.com/gin-gonic/gin"
10 | "github.com/prometheus/client_golang/prometheus"
11 | "github.com/prometheus/client_golang/prometheus/promauto"
12 | "github.com/prometheus/client_golang/prometheus/promhttp"
13 | )
14 |
15 | var (
16 | // prometheus namespace
17 | promNamespace = "webserver"
18 | promUptime = promauto.NewCounterVec(
19 | prometheus.CounterOpts{
20 | Namespace: promNamespace,
21 | Name: "server_uptime",
22 | Help: "gin server uptime in seconds",
23 | }, nil,
24 | )
25 | )
26 |
27 | // PromExporterHandler return a handler as the prometheus metrics exporter
28 | func PromExporterHandler(collectors ...prometheus.Collector) gin.HandlerFunc {
29 | for _, collector := range collectors {
30 | if err := prometheus.Register(collector); err != nil {
31 | logging.Error(nil, "Register collector error:"+err.Error())
32 | }
33 | }
34 |
35 | // uptime
36 | go func() {
37 | for range time.Tick(time.Second) {
38 | promUptime.WithLabelValues().Inc()
39 | }
40 | }()
41 | return gin.WrapH(promhttp.Handler())
42 | }
43 |
--------------------------------------------------------------------------------
/webserver/webserver.go:
--------------------------------------------------------------------------------
1 | package webserver
2 |
3 | import (
4 | "context"
5 | "net"
6 | "net/http"
7 | "os"
8 | "os/signal"
9 | "path"
10 | "strings"
11 | "syscall"
12 | "time"
13 |
14 | "github.com/axiaoxin-com/goutils"
15 | "github.com/axiaoxin-com/logging"
16 | "github.com/fsnotify/fsnotify"
17 | "github.com/gin-gonic/gin"
18 | "github.com/spf13/viper"
19 | )
20 |
21 | // InitWithConfigFile 根据 webserver 配置文件初始化 webserver
22 | func InitWithConfigFile(configFile string) {
23 | // 加载配置文件内容到 viper 中以便使用
24 | configPath, file := path.Split(configFile)
25 | if configPath == "" {
26 | configPath = "./"
27 | }
28 | ext := path.Ext(file)
29 | configType := strings.Trim(ext, ".")
30 | configName := strings.TrimSuffix(file, ext)
31 | logging.Infof(nil, "load %s type config file %s from %s", configType, configName, configPath)
32 |
33 | if err := goutils.InitViper(configFile, func(e fsnotify.Event) {
34 | logging.Warn(nil, "Config file changed:"+e.Name)
35 | logging.SetLevel(viper.GetString("logging.level"))
36 | }); err != nil {
37 | // 文件不存在时 1 使用默认配置,其他 err 直接 panic
38 | if _, ok := err.(viper.ConfigFileNotFoundError); ok {
39 | panic(err)
40 | }
41 | logging.Error(nil, "Init viper error:"+err.Error())
42 | }
43 |
44 | // 设置 viper 中 webserver 配置项默认值
45 | viper.SetDefault("env", "localhost")
46 |
47 | viper.SetDefault("server.addr", ":4869")
48 | viper.SetDefault("server.mode", gin.ReleaseMode)
49 | viper.SetDefault("server.pprof", true)
50 |
51 | viper.SetDefault("apidocs.title", "pink-lady swagger apidocs")
52 | viper.SetDefault("apidocs.desc", "Using pink-lady to develop gin app on fly.")
53 | viper.SetDefault("apidocs.host", "localhost:4869")
54 | viper.SetDefault("apidocs.basepath", "/")
55 | viper.SetDefault("apidocs.schemes", []string{"http"})
56 |
57 | viper.SetDefault("basic_auth.username", "admin")
58 | viper.SetDefault("basic_auth.password", "admin")
59 |
60 | // 打印viper配置
61 | logging.Infof(nil, "viper load all settings:%v", viper.AllSettings())
62 |
63 | // 初始化 sentry 并创建 sentry 客户端
64 | sentryDSN := viper.GetString("sentry.dsn")
65 | if sentryDSN == "" {
66 | sentryDSN = os.Getenv(logging.SentryDSNEnvKey)
67 | }
68 | sentryDebug := viper.GetBool("sentry.debug")
69 | if viper.GetString("server.mode") == "release" {
70 | sentryDebug = false
71 | }
72 | logging.Debug(nil, "Sentry use dns: "+sentryDSN)
73 | sentry, err := logging.NewSentryClient(sentryDSN, sentryDebug)
74 | if err != nil {
75 | logging.Error(nil, "Sentry client create error:"+err.Error())
76 | }
77 |
78 | // 根据配置创建 logging 的 logger 并将 logging 的默认 logger 替换为当前创建的 logger
79 | outputs := viper.GetStringSlice("logging.output_paths")
80 | var lumberjackSink *logging.LumberjackSink
81 | for _, output := range outputs {
82 | if strings.HasPrefix(output, "logrotate://") {
83 | filename := strings.Split(output, "://")[1]
84 | maxAge := viper.GetInt("logging.logrotate.max_age")
85 | maxBackups := viper.GetInt("logging.logrotate.max_backups")
86 | maxSize := viper.GetInt("logging.logrotate.max_size")
87 | compress := viper.GetBool("logging.logrotate.compress")
88 | localtime := viper.GetBool("logging.logrotate.localtime")
89 | lumberjackSink = logging.NewLumberjackSink("logrotate", filename, maxAge, maxBackups, maxSize, compress, localtime)
90 | }
91 | }
92 | logger, err := logging.NewLogger(logging.Options{
93 | Level: viper.GetString("logging.level"),
94 | Format: viper.GetString("logging.format"),
95 | OutputPaths: outputs,
96 | DisableCaller: viper.GetBool("logging.disable_caller"),
97 | DisableStacktrace: viper.GetBool("logging.disable_stacktrace"),
98 | AtomicLevelServer: logging.AtomicLevelServerOption{
99 | Addr: viper.GetString("logging.atomic_level_server.addr"),
100 | Path: viper.GetString("logging.atomic_level_server.path"),
101 | Username: viper.GetString("basic_auth.username"),
102 | Password: viper.GetString("basic_auth.password"),
103 | },
104 | SentryClient: sentry,
105 | LumberjackSink: lumberjackSink,
106 | })
107 | if err != nil {
108 | logging.Error(nil, "Logger create error:"+err.Error())
109 | } else {
110 | logging.ReplaceLogger(logger)
111 | }
112 | }
113 |
114 | // Run 以 viper 加载的 app 配置启动运行 http.Handler 的 app
115 | // 注意:这里依赖 viper ,必须在外部先对 viper 配置进行加载
116 | func Run(app http.Handler) {
117 | // 判断是否加载 viper 配置
118 | if !goutils.IsInitedViper() {
119 | panic("Running server must init viper by config file first!")
120 | }
121 |
122 | // 创建 server
123 | addr := viper.GetString("server.addr")
124 | srv := &http.Server{
125 | Addr: addr,
126 | Handler: app,
127 | ReadTimeout: 5 * time.Minute,
128 | WriteTimeout: 10 * time.Minute,
129 | }
130 | // Shutdown 时需要调用的方法
131 | srv.RegisterOnShutdown(func() {
132 | // TODO
133 | })
134 |
135 | // 启动 http server
136 | go func() {
137 | var ln net.Listener
138 | var err error
139 | if strings.ToLower(strings.Split(addr, ":")[0]) == "unix" {
140 | ln, err = net.Listen("unix", strings.Split(addr, ":")[1])
141 | if err != nil {
142 | panic(err)
143 | }
144 | } else {
145 | ln, err = net.Listen("tcp", addr)
146 | if err != nil {
147 | panic(err)
148 | }
149 | }
150 | if err := srv.Serve(ln); err != nil {
151 | logging.Error(nil, err.Error())
152 | }
153 | }()
154 | logging.Infof(nil, "Server is running on %s", srv.Addr)
155 |
156 | // 监听中断信号, WriteTimeout 时间后优雅关闭服务
157 | // syscall.SIGTERM 不带参数的 kill 命令
158 | // syscall.SIGINT ctrl-c kill -2
159 | // syscall.SIGKILL 是 kill -9 无法捕获这个信号
160 | quit := make(chan os.Signal, 1)
161 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
162 | <-quit
163 | logging.Infof(nil, "Server is shutting down.")
164 |
165 | // 创建一个 context 用于通知 server 3 秒后结束当前正在处理的请求
166 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
167 | defer cancel()
168 | if err := srv.Shutdown(ctx); err != nil {
169 | logging.Error(nil, "Server shutdown with error: "+err.Error())
170 | }
171 | logging.Info(nil, "Server exit.")
172 | }
173 |
--------------------------------------------------------------------------------
/webserver/webserver_test.go:
--------------------------------------------------------------------------------
1 | package webserver
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/axiaoxin-com/goutils"
7 | "github.com/spf13/viper"
8 | )
9 |
10 | func TestViperConfig(t *testing.T) {
11 | InitWithConfigFile("../config.toml")
12 | defer viper.Reset()
13 | if !goutils.IsInitedViper() {
14 | t.Error("init viper failed")
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
26 |
29 |