背景
最近在研究用 QMT 实现条件单(比如某只股票如果到达某个价格就卖出)。策略实现肯定是要本着「数据和逻辑完全解耦」的原则去写的。所以条件单被提取成了一个数组,每个元素就是一个条件单,包括股票代码、触发价、交易数量等数据,算是策略的配置项。如下图所示:
PS:交互看起来是不是特别友好,谁用谁知道,真香!
逻辑实现起来倒是不难,但是这些配置如果写在代码里,每次都需要去改代码,很麻烦,而且我的策略都是跑在服务器上的,需要登录服务器去改,更麻烦。
后来我把这些配置放到了一个 csv
文件里,虽然不用改代码了,但是每次还是要登录到服务器上去修改数据,服务器上还没有安装 wps、office 之类的软件,只能用记事本修改,很容易犯错。
最后心一横,花了 2 天时间实现了用飞书电子表格作为策略配置项的功能,现记录如下。
正文
访问凭证
只要是使用云服务,访问凭证(后文简称 TOKEN)是必须要先搞定的东西,飞书文档也不例外。TOKEN 的说明见 官方文档,我们这里用 tenant_access_token
就可以了,获取方法文档里说的也比较清晰。
先在飞书后台新建一个 APP,并且开通它操作电子表格的 API 权限。
然后拿它的 AppID、AppSecret 调用 API 即可,python 版的示例代码如下:
py
import requests
import time
res = requests.post(
url="https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={ "app_id": APP_ID, "app_secret": APP_SECRET }
)
res_data = res.json()
tenant_access_token = res_data["tenant_access_token"]
expire = res_data["expire"]
data = {
"token": tenant_access_token,
"expire": expire + int(time.time())
}
这个只是简单的实现,实际使用时需要注意几个问题:
APP_ID
和APP_SECRET
如果直接写在代码里容易泄露。可以写到.env
文件里;TOKEN
2 小时过期,所以可以用一个文件存储 token 和 expire,如果未过期就从文件里读,即快又节省调用次数;
获取表格数据
有了 token 之后,我们来尝试获取一下电子表格的数据,详情可见 官方文档-读取单个范围。
这里需要明白几个概念,一个是 spreadsheetToken
,一个是 sheetId
。这两个值在文档的地址里面就可以获取到,如图:
它们的意义也比较好理解,前者就是这个表格的 ID,后者是表格中某个 sheet 的 ID。另外还要了解 range
的概念,它的格式是 <sheetId>!<开始位置>:<结束位置>
,例如:0bdf12!A1:B5
、0bdf12!A:B
。详见 官方文档。
了解完之后,直接看代码就好了:
py
def fetch_feishu_sheet_datas(sheet_token, range):
access_token = _fetch_feishu_token()
url = f'https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{sheet_token}/values/{range}?dateTimeRenderOption=FormattedString'
headers = { 'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json; charset=utf-8' }
response = requests.get(url, headers=headers).json()
values = response['data']['valueRange']['values']
df = pd.DataFrame(values[1:], columns=values[0])
return df
这里需要说明几点:
- 为了方便处理,我将读取的数据处理成了
pandas
的 DataFrame 类型,这个类型是用 python 编写量化策略必须熟练运用的,这里就不展开讲了; - 对于日期类型 数据的处理,请求加了
dateTimeRenderOption=FormattedString
参数,这样返回的数据就是 "20241124" 而不是一个整数,更多详情见 官方文档-读取单个范围 的参数说明;
更新表格数据
读取数据完事了,接下来就是回写数据了,详情可见 官方文档-向单个范围写入数据。
代码实现如下:
py
def update_feishu_sheet_datas(sheet_token, range, values):
'''
data = { "valueRange": { "range": "1QXD0s!A1:B2",
"values": [
[ "Hello", 1 ],
[ "World", 1 ]
]}}
'''
access_token = _fetch_feishu_token()
url = f'{FEISHU_OPEN_API_SHEET_PREFIX}/v2/spreadsheets/{sheet_token}/values'
headers = { 'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json; charset=utf-8' }
data = { "valueRange": { "range": range, "values": values }}
response = requests.put(url, headers=headers, json=data)
return response.json()
代码很简单,需要注意几点:
- values 参数是一个二维数组,因为我们经常只更新一行数据,所以很容易传成一维数组了;
- 因为 range 里需要知道行数,所以我们最好在数据里面有体现行数的数据,比如
id
; - 除了行数,range 还需要写明列,所以我们需要一个根据写入数组长度计算列的字母的功能;
- 在回写日期类型数据时,注意要转换成数字,具体规则见下文的代码;
基于上述注意事项,我封装了 2 个工具函数:
py
def get_nth_uppercase_letter(n):
"""
返回第 n 列 Excel 表格的字母
"""
if n < 1:
return ""
result = []
while n > 0:
n -= 1
result.append(chr(n % 26 + ord('A')))
n //= 26
return ''.join(result[::-1])
def time_to_float(date_string: str, format='%Y%m%d') -> float:
"""
将时间字符串转换为自 1899 年 12 月 30 日以来的天数,整数部分为天数,小数部分为时间占 24 小时的份额。
"""
base_date = dt.datetime(1899, 12, 30)
input_datetime = dt.datetime.strptime(date_string, format)
delta_days = (input_datetime - base_date).days
time_fraction = (input_datetime.hour * 3600 + input_datetime.minute * 60 + input_datetime.second) / (24 * 3600)
return delta_days + time_fraction
其中 get_nth_uppercase_letter
是方便根据写入数组的长度,自动计算 range
中的结束列。
time_to_float
则是方便把日期字符串转换成数字类型的原始数据,再写回去,否则表格里面会报错。因为表格里虽然展示的是字符串,但是底层的数据还是数字,具体的规则也在注释里面写了。
具体使用的 Demo 如下:
py
def update_original_orders(order):
row = order['id'] + 1
keys = list(headers.keys())
last_column = u.get_nth_uppercase_letter(len(keys))
values = []
for key in keys:
if order[key] and (key in ['start_date', 'end_date']):
values.append(u.time_to_float(order[key]))
else:
values.append(order[key])
u.update_feishu_sheet_datas(sheet_token, f'{sheet_id}!A{row}:{last_column}', [values])
其中 headers
是对表格首行的顺序声明,举例如下:
py
headers = {
'id': int, 'code': str, 'start_date': str, 'end_date': str,
'opt_type': str, 'order_type': str, 'volume': int,
'trigger_price': float, 'change_rate': float, 'status': str,
'highest': float, 'lowest': float, 'order_time': str, 'order_price': float
}
总结
实现读写云表格作为策略配置数据的功能后,生活变得如此简单。无论身在何地,无需登陆服务器,直接用浏览器打开云表格,修改就可以了。
而且它的各种样式也特别友好,下拉选择、日期格式等功能限制,也可以避免出错。实在是太香了!