量化策略配置神器-飞书表格

背景

最近在研究用 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())
}

这个只是简单的实现,实际使用时需要注意几个问题:

  1. APP_IDAPP_SECRET 如果直接写在代码里容易泄露。可以写到 .env 文件里;
  2. TOKEN 2 小时过期,所以可以用一个文件存储 token 和 expire,如果未过期就从文件里读,即快又节省调用次数;

获取表格数据

有了 token 之后,我们来尝试获取一下电子表格的数据,详情可见 官方文档-读取单个范围

这里需要明白几个概念,一个是 spreadsheetToken,一个是 sheetId。这两个值在文档的地址里面就可以获取到,如图:

它们的意义也比较好理解,前者就是这个表格的 ID,后者是表格中某个 sheet 的 ID。另外还要了解 range 的概念,它的格式是 <sheetId>!<开始位置>:<结束位置>,例如:0bdf12!A1:B50bdf12!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

这里需要说明几点:

  1. 为了方便处理,我将读取的数据处理成了 pandas 的 DataFrame 类型,这个类型是用 python 编写量化策略必须熟练运用的,这里就不展开讲了;
  2. 对于日期类型 数据的处理,请求加了 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()

代码很简单,需要注意几点:

  1. values 参数是一个二维数组,因为我们经常只更新一行数据,所以很容易传成一维数组了;
  2. 因为 range 里需要知道行数,所以我们最好在数据里面有体现行数的数据,比如 id
  3. 除了行数,range 还需要写明列,所以我们需要一个根据写入数组长度计算列的字母的功能;
  4. 在回写日期类型数据时,注意要转换成数字,具体规则见下文的代码;

基于上述注意事项,我封装了 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
}

总结

实现读写云表格作为策略配置数据的功能后,生活变得如此简单。无论身在何地,无需登陆服务器,直接用浏览器打开云表格,修改就可以了。

而且它的各种样式也特别友好,下拉选择、日期格式等功能限制,也可以避免出错。实在是太香了!

相关推荐
叫我:松哥6 分钟前
基于python flask的网页五子棋实现,包括多种语言,可以悔棋、重新开始
开发语言·python·算法·游戏·flask
zhixingheyi_tian13 分钟前
Spark 之 SparkSessionExtensions
大数据·分布式·spark
ProtonBase13 分钟前
分布式 Data Warebase - 构筑 AI 时代数据基石
大数据·数据库·数据仓库·人工智能·分布式·数据分析·数据库系统
Mephisto.java16 分钟前
【大数据学习 | Spark-Core】Spark的分区器(HashPartitioner和RangePartitioner)
大数据·elasticsearch·oracle·spark·sqlite·flume·memcached
秋夜白17 分钟前
【排序算法 python实现】
开发语言·python·排序算法
qq_2739002321 分钟前
旋转向量v和旋转矩阵R
人工智能·python·线性代数·矩阵
菜鸟小贤贤34 分钟前
mac安装Pytest、Allure、brew
python·macos·自动化·pytest
licy__1 小时前
Python BeautifulSoup 常用语句详解
开发语言·python·beautifulsoup
叶子上的考拉1 小时前
Spark SQL操作
大数据·sql·spark
游客5201 小时前
利用 Python 和 Selenium 高效启动和管理 Chrome 浏览器
chrome·python·selenium