量化交易学习(二十三)用飞书群机器人搭建消息通知平台

对于一个量化交易系统而言,消息通知是非常重要的功能。比如在出买卖点、量化系统自动下单、出现异常情况时及时发送消息通知,可以让我们在第一时间掌握系统的运行情况。

完全依靠自己写一个消息通知平台不太现实,我们可以借助现有的一些聊天软件实现。现在常用的聊天软件有微信、QQ、钉钉、飞书等,在一番对比后,我发现飞书的操作最简单门槛最低。

飞书群机器人可以自动向群组成员发送消息。它可以用来发送各种类型的消息,包括文本、图片、卡片等。利用飞书群机器人,可以轻松搭建一个消息通知平台,用于将各种消息通知到群组成员。

在飞书中建一个只有自己一个人的群然后再拉一个群聊机器人进来,通过调用群聊机器人的webhook接口就可以实现消息的实时发送了。

下面以创建一个每天定时发送天气的群机器人为例,介绍下群聊机器人的使用方法:

首先在电脑版飞书上创建一个群:

26314494e69b2c2d07e825412b08176a.png

创建时把自己拉到群里就可以了:

6a46197e4337ee9da6d21903b2cc09d5.png

创建好群之后,点击右上角的『…』符号,再点击『设备』按钮

107de446fb5488ac44d4a9adf7680939.png

在侧边栏中点一下群机器人

fb9dafa48b69e79c35449f7bb0644430.png

然后再点击『添加机器人』

87e3902fc3f0a24365c8d9972b38fe5a.png

点击第一个『自定义机器人』

64172a5e2c8669fefebe4d1c23ac7b40.png

然后输入机器人的名字及描述,最后点击『添加』按钮

bdd3d9b19314d090b33104bc858c472a.png

添加好机器人后,会显示 Webhook 地址,复制这串地址。

c95e9b4aa46e5286bbbe1293399a3205.png

官方的使用文档在这:https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot

最简单的发送消息的方式是用curl,这是windows、mac、linux都能直接用的一个方案:

windows cmd:

1
curl -X POST -H "Content-Type: application/json" -d "{\"msg_type\":\"text\",\"content\":{\"text\":\"request example\"}}" https://open.feishu.cn/open-apis/bot/v2/hook/****

windows powershell:

1
curl.exe -X POST -H "Content-Type: application/json" -d '{\"msg_type\":\"text\",\"content\":{\"text\":\"requestexample\"}}' https://open.feishu.cn/open-apis/bot/v2/hook/****

mac/linux:

1
2
3
curl -X POST -H "Content-Type: application/json" \
-d '{"msg_type":"text","content":{"text":"request example"}}' \
https://open.feishu.cn/open-apis/bot/v2/hook/****

请求体为一个json对象:

1
2
3
4
5
6
{
"msg_type": "text",
"content": {
"text": "新更新提醒"
}
}

下面是我的天气预报机器人发送数据的一个效果:

b0ac25de624f061993627a40cd614e53.png

群机器人支持多种消息格式,除了文本消息外,还支持富文本消息、群名片、图片、消息卡片等。

具体的消息格式文档可以参考官方文档:https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot#5a997364

下面来介绍一下用『卡片搭建工具』快速搭建消息卡片:

飞书提供了一个卡片搭建工具,可以非常方便地辅助我们创建消息卡片,它的访问地址:https://open.feishu.cn/tool/cardbuilder

进入卡片搭建工具页面后,点击『新建卡片』

839814cfceea31f3afa6f481ae72519b.png

然后点击『新建空白卡片』

fd44e43dcaebc691194b478c29de9be5.png

填写卡片名称,再点击『保存』

f2ad38cea9a8d07c0dd4c517d702b662.png

点击左侧边栏中的『模块组件』,然后向卡片中加入相关组件即可

587fb7f191a11dfd5164eafe6e9ec3ff.png

这是我的卡片的样子:

8f4289c7e0061966f95ced2b24491db5.png

设计好卡片模板后,点击『编辑卡片』模块的『代码图标』就可以展示这个卡片对应的json数据了,如下图:

35894d7372181dd40adc027a3e242a54.png

80fe0b75125fd2dfd4478a926dfd15e9.png

然后复制这串json,把它加到消息卡片json中card部分就可以了:

1
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
{
"msg_type": "interactive",
"card": {
"header": {
"template": "blue",
"title": {
"tag": "plain_text",
"content": "最近三天天气"
}
},
"elements": [
{
"tag": "div",
"text": {
"content": "${date0}",
"tag": "plain_text"
}
},
{
"tag": "div",
"text": {
"content": "${t0}",
"tag": "plain_text"
}
},
{
"tag": "div",
"text": {
"content": "${day0}",
"tag": "plain_text"
}
},
{
"tag": "div",
"text": {
"content": "${night0}",
"tag": "plain_text"
}
},
{
"tag": "hr"
},
... // 限于篇幅,后面的不展示了
}
}

用自己的数据填入卡片中相应位置,然后在程序中调用这个接口就可以了。

实际效果如下:

f36bc1087ff18e3d351548b872152d9f.png

天气数据我用的是『和风天气』的接口,地址:https://dev.qweather.com/

我用go语言写了一个获取天气并调用群机器人接口发送消息的程序,然后在服务器的cron 中设置每天7点执行,就实现了每天定时发送天气预报的功能。

代码如下,和风天气及群机器人接口的地址改成你自己申请的就能用了:

1
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
package main

import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
)

func main() {
url := "https://devapi.qweather.com/v7/weather/3d?location=101010100&key=xxxxxxxxxxxxx"
method := "GET"

client := &http.Client{}
req, err := http.NewRequest(method, url, nil)

if err != nil {
fmt.Println(err)
return
}
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()

body, err := io.ReadAll(res.Body)
var weather WeatherInfo
json.Unmarshal(body, &weather)

FeishuNotify(weather)
if err != nil {
fmt.Println(err)
return
}
}

func BuildWeatherString(weather WeatherInfo, idx int) string {
s1 := fmt.Sprintf("%v —— 最高温:%v,最低温:%v,白天:%v %v %v级,夜间:%v %v %v级",
weather.Daily[idx].FxDate, weather.Daily[idx].TempMax, weather.Daily[idx].TempMin,
weather.Daily[idx].TextDay, weather.Daily[idx].WindDirDay, weather.Daily[idx].WindScaleDay,
weather.Daily[idx].TextNight, weather.Daily[idx].WindDirNight, weather.Daily[idx].WindScaleNight)
return s1
}

type WeatherInfo struct {
Code string `json:"code"`
UpdateTime string `json:"updateTime"`
FxLink string `json:"fxLink"`
Daily []struct {
FxDate string `json:"fxDate"`
Sunrise string `json:"sunrise"`
Sunset string `json:"sunset"`
Moonrise string `json:"moonrise"`
Moonset string `json:"moonset"`
MoonPhase string `json:"moonPhase"`
MoonPhaseIcon string `json:"moonPhaseIcon"`
TempMax string `json:"tempMax"`
TempMin string `json:"tempMin"`
IconDay string `json:"iconDay"`
TextDay string `json:"textDay"`
IconNight string `json:"iconNight"`
TextNight string `json:"textNight"`
Wind360Day string `json:"wind360Day"`
WindDirDay string `json:"windDirDay"`
WindScaleDay string `json:"windScaleDay"`
WindSpeedDay string `json:"windSpeedDay"`
Wind360Night string `json:"wind360Night"`
WindDirNight string `json:"windDirNight"`
WindScaleNight string `json:"windScaleNight"`
WindSpeedNight string `json:"windSpeedNight"`
Humidity string `json:"humidity"`
Precip string `json:"precip"`
Pressure string `json:"pressure"`
Vis string `json:"vis"`
Cloud string `json:"cloud"`
UvIndex string `json:"uvIndex"`
} `json:"daily"`
Refer struct {
Sources []string `json:"sources"`
License []string `json:"license"`
} `json:"refer"`
}

func FeishuNotify(weather WeatherInfo) {
url := "https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxxx"
method := "POST"

var msg WeatherCard

msg.MsgType = "interactive"
msg.Card.Config.WideScreenMode = true
msg.Card.Header.Template = "blue"
msg.Card.Header.Title.Tag = "plain_text"
msg.Card.Header.Title.Content = "最近三天天气"

buildColumn := func(idx int) []InnerElement {
return []InnerElement{
{
Tag: "div",
Text: Text{
Content: weather.Daily[idx].FxDate,
Tag: "plain_text",
},
},
{
Tag: "div",
Text: Text{
Content: fmt.Sprintf("最高温:%v,最低温:%v", weather.Daily[idx].TempMax, weather.Daily[idx].TempMin),
Tag: "plain_text",
},
},
{
Tag: "div",
Text: Text{
Content: fmt.Sprintf("白天:%v %v %v级", weather.Daily[idx].TextDay, weather.Daily[idx].WindDirDay, weather.Daily[idx].WindScaleDay),
Tag: "plain_text",
},
},
{
Tag: "div",
Text: Text{
Content: fmt.Sprintf("夜间:%v %v %v级", weather.Daily[idx].TextNight, weather.Daily[idx].WindDirNight, weather.Daily[idx].WindScaleNight),
Tag: "plain_text",
},
},
}
}
msg.Card.Elements = append(msg.Card.Elements, buildColumn(0)...)
msg.Card.Elements = append(msg.Card.Elements, InnerElement{
Tag: "hr",
})
msg.Card.Elements = append(msg.Card.Elements, buildColumn(1)...)
msg.Card.Elements = append(msg.Card.Elements, InnerElement{
Tag: "hr",
})
msg.Card.Elements = append(msg.Card.Elements, buildColumn(2)...)
//msg.MsgType = "text"
//msg.Content.Text = text
b, _ := json.Marshal(msg)
payload := bytes.NewReader(b)

client := &http.Client{}
req, err := http.NewRequest(method, url, payload)

if err != nil {
fmt.Println(err)
return
}
req.Header.Add("Content-Type", "application/json")

res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()

body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}

type WeatherCard struct {
MsgType string `json:"msg_type"`
Card struct {
Config struct {
WideScreenMode bool `json:"wide_screen_mode"`
} `json:"config"`
Header struct {
Template string `json:"template"`
Title struct {
Tag string `json:"tag"`
Content string `json:"content"`
} `json:"title"`
} `json:"header"`
Elements []InnerElement
} `json:"card"`
}

type InnerElement struct {
Tag string `json:"tag"`
Text Text `json:"text"`
}

type Text struct {
Content string `json:"content"`
Tag string `json:"tag"`
}


这一篇就到这里啦。欢迎大家点赞、转发、私信。还没有关注我的朋友可以关注 江达小记

江达小记