封装的 utils.py 如何继承 QMT 宿主的所有全局变量 globals

316 阅读5分钟

问题描述

笔者最近在尝试写 QMT 的量化策略。QMT 自带的交易函数 passorder 很复杂,比较难用,所以我打算把它封装一下。在 main.py 策略文件里,把 passorder 方法封装进 buy_stock_by_money 函数很成功。

但是当我想让代码更简洁,把 buy_stock_by_money 提取到 utils.py 文件,然后让 main.py(宿主)引用 utils.py 的时候,就出问题了。

# utils.py
def buy_stock_by_money(code, money, C, strategyName=None):
    # passorder 是 main.py 运行时的全局变量
    passorder(23, 1102, C.accID, code, 5, -1, money, strategyName, C)


# main.py
import utils
# ERROR: 调用的时候会报错,paassorder 未定义
utils.buy_stock_by_money('5100300.SH', 50000, ContextInfo, 'Test Strategy')

原因是 passorder 是 QMT 内置 Python 的方法,在策略文件里能够访问到,但是在模块里是访问不到的。

所以我们需要把 passorder 传入 utils.py 才可以,询问过 ChatGPT 后,得到了下面几个解决方案,个人推荐最后的「终极方案」,可以一劳永逸。

解决方案

方案一:显性传入

最直接,最简单的方法,就是把依赖的全局函数 passorder 作为参数传入,如下:

# utils.py
def buy_stock_by_money(passorder, code, money, C, strategyName=None):
    # passorder 作为参数传入
    passorder(23, 1102, C.accID, code, 5, -1, money, strategyName, C)


# main.py
import utils
# 调用的时候直接传入 passorder
utils.buy_stock_by_money(passorder, '5100300.SH', 50000, ContextInfo, 'Test Strategy')

这种方法的优点就是让 buy_stock_by_money 保持无副作用,也就是纯函数。也就是无论在任何环境使用 utils.py 都不用担心它的上下文。

缺点也比较明显,除了 passorder 外,还有很多其它的全局变量 order_lotsorder_valueorder_percent 等等……如果 buy_stock_by_money 依赖多个全局函数,那就要把它们都作为变量,全部显示传入,可能会变成下面这样,无论声明还是调用,都比较麻烦:

# utils.py
def buy_stock_by_money(passorder, order_lots, order_value, order_percent, code, money, C, strategyName=None):
    # 依赖多个全局函数
    if strategyName == 'order_lots':
        order_lots(23, 1102, C.accID, code, 5, -1, money, strategyName, C)
    elif strategyName == 'order_value':
        order_value(23, 1102, C.accID, code, 5, -1, money, strategyName, C)
    elif strategyName == 'order_percent':
        order_percent(23, 1102, C.accID, code, 5, -1, money, strategyName, C)
    else:
        passorder(23, 1102, C.accID, code, 5, -1, money, strategyName, C)
    
# main.py
import utils
# 调用时要把所有依赖传入
utils.buy_stock_by_money(passorder, order_lots, order_value, order_percent, '5100300.SH', 50000, ContextInfo, 'Test Strategy')

相信这个不是我们想要的,起码在这种可能依赖太多宿主全局变量的时候,这种方法是不太合适的。

方案二:一次性传入全部依赖

上面的方法不行,那就想办法把 main.py 的全局变量也变成 utils.py 的全局变量,示例代码如下:


# utils.py
def init(locals):
    global passorder, order_lots, order_value, order_percent
    passorder = locals['passorder']
    order_lots = locals['order_lots']
    order_value = locals['order_value']
    order_percent = locals['order_percent']

# 不再需要显性传入所有依赖
def buy_stock_by_money(code, money, C, strategyName=None):
    if strategyName == 'order_lots':
        order_lots(23, 1102, C.accID, code, 5, -1, money, strategyName, C)
    elif strategyName == 'order_value':
        order_value(23, 1102, C.accID, code, 5, -1, money, strategyName, C)
    elif strategyName == 'order_percent':
        order_percent(23, 1102, C.accID, code, 5, -1, money, strategyName, C)
    else:
        passorder(23, 1102, C.accID, code, 5, -1, money, strategyName, C)
    
# main.py
import utils
# 调用 init,将全局变量注入 utils.py
utils.init({
    'passorder': passorder,
    'order_lots': order_lots,
    'order_value': order_value,
    'order_percent': order_percent
})
# 不再需要传入所有依赖
utils.buy_stock_by_money('5100300.SH', 50000, ContextInfo, 'Test Strategy')

这种方式虽然引入了副作用,但是对于我们的使用场景来说,收益绝对大于成本。所有依赖只需要一次性传入即可。

但是这里还有个小瑕疵,那就是一旦 utils.py 又依赖了新全局函数,要同步修改 main.pyutils.py,还是稍微有点麻烦。直到我把这两段代码丢给了 AI(Codeium),问能不能简化一下,于是得到了最后的「终极方案」。

方案三:终极方案

我用 Codeium 选中了 init 的代码,然后让它帮我简化一下,结果竟然有惊喜:

image.png

image.png

那么一大堆代码,直接一句 globals().update(locals) 就完事了?!?!试了一下还真是。

同理我又试着简化了一下调用的时候的代码,结果直接 utils.init(locals()) 就搞定了。

最关键的是,这种写法把所有全局变量都传进去了,不用再一个一个的声明了,不得不说,AI 编程真的 NB!!

终极代码如下:

# main.py
import utils as u
import imp

imp.reload(u) # 可选:每次重新加载,防止缓存
u.init(globals()) # 精彩!!!!

u.buy_stock_by_money('5100300.SH', 50000, ContextInfo, 'Test Strategy')

# utils.py
def init(locals):
    globals().update(locals) # 精彩!!!

def buy_stock_by_money(code, money, C, strategyName=None):
    passorder(23, 1102, C.accID, code, 5, -1, money, strategyName, C)

不但代码清爽了很多,连扩展性都解决了,完美!!

一个小坑

我在运行 QMT 策略的时候发现如果策略名称是中文的话,会报加载 utils 错误,而且用了 imp.reload(u) 有时候也不好使。所以我现在的策略文件名,都是英文,现在我用的模板代码是这样的,仅供参考:

#coding:gbk
import sys
if 'qmt_utils' in sys.modules:
    del sys.modules['qmt_utils']
import qmt_utils as u
import datetime as dt

class a():
    pass

A = a()
A.canceled = False

def init(C):
    global A
    global logger
    
    u.init(C, globals())
    logger = u.MyLogger(C)
    today = dt.datetime.now().strftime('%Y-%m-%d')
    C.run_time("Test", '3nSecond', today + ' 00:00:00', 'SH')

def Test(C):
    now_date = dt.datetime.now().strftime("%Y%m%d")
    bar_date = u.get_bar_date(C)
    if not C.do_back_test and bar_date < now_date:
        return
    logger.log(f'AccID: {C.accID}, AccType: {C.accType}', 'DEBUG', False)

def handlebar(C):
    if not C.do_back_test and not C.is_last_bar():
        return

总结

笔者的主语言是 JS/TS,Python 应用只能算是入门级,但是最起码的代码审美还是有的。AI 的加持真的可以让我这种人受益匪浅,毫无疑问,学习效率提高了不知道多少倍,关键代码质量也可以直接拉满,这种感觉真的太爽了。

实现了 utils.py 的提取,未来开发策略无论是效率还是质量,肯定会有极大提升,后续有任何更新再继续。