基差的布林带阈值,当基差超过上线的时候,证明期货价格过高,进行卖出的操作;当回归到布林带中线,进行相应的平仓;与此对立,当基差下穿下线的时候,证明期货期货价格过低,进行买入的操作,同样当回归到布林带中线,进行多头的平仓。
下面我们具体使用代码将具体的程序化交易进行实现。首先注册并登陆优宽量化官网,点击控制中心,点击策略库+新建策略。在左上角下拉菜单中选择Python,并填入策略的名字。
第一步:编写策略框架
# 策略主函数
def onTick():
pass
# 策略入口
def main():
while True: # 进入循环模式
onTick() # 执行策略主函数
Sleep(1000 * 60 * 60 * 24) # 策略休眠一天
第一步编写策略框架,策略框架是两个函数,main
函数是策略的入口,主要功能是交易之前的预处理,程序会先从main
函数开始执行,然后进入无限循环模式,重复执行onTick
函数,onTick
函数是策略的主函数,主要执行核心代码。
第二步:增加图表功能
# 全局变量
# 期现图表
cfgA = {
"extension": {
"layout": 'single',
"col": 6,
"height": "500px",
},
"title": {
"text": "期现图表"
},
"xAxis": {
"type": "datetime"
},
"series": [{
"name": "期货价格",
"data": [],
}, {
"name": "现货价格",
"data": [],
}]
}
# 基差图表
cfgB = {
"extension": {
"layout": 'single',
"col": 6,
"height": "500px",
},
"title": {
"text": "基差图表"
},
"xAxis": {
"type": "datetime"
},
"series": [{
"name": "基差价格",
"data": [],
}]
}
chart = Chart([cfgA, cfgB]) # 创建一个图表对象
# 策略主函数
def onTick():
chart.add(0, []) # 绘制图表
chart.add(1, []) # 绘制图表
chart.add(2, []) # 绘制图表
chart.update([cfgA, cfgB]) # 更新图表
# 策略入口
def main():
LogReset() # 运行前先清空之前的 Log 日志信息
chart.reset() # 运行前先清空之前的图表信息
while True: # 进入循环模式
onTick() # 执行策略主函数
Sleep(1000 * 60 * 60 * 24) # 策略休眠一天
在这个策略中,一共创建了 2 个图表,并左右分布排列。其中左图cfgA
是期现图表,包含期货价格和现货价格,右图cfgB
是基差图表。然后创建一个chart
对象。最后是在onTick
函数中实时更新图表中的数据。
第三步:获取数据
我们一共需要获取三种数据:期货价格、现货价格、基差价格。获取期货价格很简单,直接使用SetContractType
函数订阅期货品种,再使用GetRecords
函数就可以获取 K 线的收盘价。
基差作为外部数据,当引入到策略当中需要一系列的繁杂操作(API获取,数据清理等),为了解决这一痛点,优宽量化开发的DATADATA平台已经内置了商品期货基本面数据,包括期货相关的各种宏观数据。因此我们首先需要在DATADATA平台编写Sql代码,获取基本面数据。
打开DATADATA平台,点击创建图表,写入sql代码,保存数据,然后点击铅笔按钮,设置自动更新,接着点击获取API。
WITH DATA1 AS (
SELECT
*,
EXTRACT(EPOCH FROM time)::bigint * 1000 AS time_milliseconds
FROM
futures_data.basis
WHERE
product_name = '生猪'
ORDER BY
date
)
SELECT
time_milliseconds AS time,
date,
json_build_object('spot_price', spot_price) AS data
FROM
DATA1;
通过设置“定时刷新”,我们运行策略可以获取最新的数据,基差数据是每日刷新的,因此定制刷新的频率为“每天”,基差时间更新的具体时间点为八点,所以选择时间为"08:00 - 09:00"就可以。
接着回到策略编写界面,使用GetData
获取数据,里面填写的参数为上面设置完成的 API ,返回的是包含时间戳的字典数据,我们进行一下简单的处理。当检查到DATADATA平台数据更新,就可以获取最新的基差数据进行相应的画图和套利操作。
basisList = [] # 基差列表
obj = exchange.GetData("https://www.datadata.cn/api/v1/query/94af6e1e-2efa-4e0c-8c08-f7853b2bf969/data")
if obj["Data"] != pre_data:
info = json.loads(obj["Data"])
spot = info['spot_price']
pre_data = obj["Data"]
basis = futures_price - info['spot_price']
basisList.append(basis) # 基差列表数据的收集
第四步:计算基差上下限 获取到基差数据之后,进行基差数据的布林带上中下限阈值的计算。
if len(basisList) < pigPeriod + 2:
return
basisBoll = TA.BOLL(basisList, pigPeriod, 2)
exbasisBoll = TA.BOLL(basisList, pigPeriod, 3)
第五步:进行基差套利操作
前期信号指标计算完毕以后,当检查到实时基差上穿布林带上轨的时候,证明基差过大,进行期货空头开仓的操作;开仓完毕,当检查到回归布林带中轨,进行空头平仓。当检查到实时基差下穿布林带下轨,证明基差过小,进行多头开仓,回归中轨进行平仓。
if basis > basisBoll[0][-2] and mp == 0:
p.OpenShort('lh888', 1)
mp = -1
if mp == -1 and basis < basisBoll[1][-2]:
p.Cover('lh888')
mp = 0
if basis < basisBoll[2][-2] and mp == 0:
p.OpenLong('lh888', 1)
mp = 1
if mp == 1 and basis > basisBoll[1][-2]:
p.Cover('lh888')
mp = 0
根据策略运行结果,可以看到在2024年1月至4月末,策略取得了良好的收益,证明生猪品种具有较平稳的基差回归特性,我们可以以此为依据进行相应的期现套利操作。
俗话说分久必合合久必分,在期货市场也有这种现象,没有只涨不跌的品种也没有只跌不涨的品种。但是什么时候分什么时候合,这就要看乖离率了。本节我们将使用乖离率构建一个简单的策略。
乖离率 BIAS 是由移动平均线衍生出来的一种技术指标,它主要是以百分比的形式,衡量价格在波动中与移动平均线的偏离程度。如果说均线是交易者的平均成本,那么乖离率就是交易者的平均回报率。注意:相对来说乖离率是一个比较冷门的技术指标,但在量化交易中使用乖离率可以增加策略的多样性。
乖离率的理论基础是对交易者的心里分析,当价格大于市场平均成本太多时,表示多头交易者获利越丰厚,容易萌生赚钱就走的念头,进而会造成价格下跌。当价格小于市场平均成本太多时,表示空头交易者获利丰厚,容易萌生赚钱就走的念头,进而会造成价格上涨。
虽然移动平均线是由价格计算而来,但从外在形式上价格一定会向移动平均线靠拢,或者说价格总是围绕着移动平均线上下波动。如果价格偏离均线太远,不管价格是在均线之上还是之下,最后都可能趋向于均线,而乖离率正是表示价格偏离均线的百分比值。
乖离率 = [(当日收盘价 - N 日平均价) / N 日平均价] * 100%
其中,N 是移动均线参数,由于 N 的周期不同,乖离率的计算结果也不同。一般情况下 N 的取值是:6、12、24、36 等等。在实际使用中,也可以根据不同的品种动态调整。但参数的选择十分重要,如果参数过小,乖离率就会过于敏感,如果参数过大,乖离率就会过于迟钝。乖离率的计算结果有正负之分,正的乖离率越大,代表多头获利越大,价格回调的概率越大。负的乖离率越大,代表空头获利越大,价格反弹的概率越大。
由于乖离率是另一种均线的表现形式,那么我们也可以根据双均线策略改编一个双乖离率策略。通过短期乖离率与长期乖离率的位置关系,判断当前的市场状态。
第 1 步:编写策略框架
# 策略主函数
def onTick():
pass
# 程序入口
def main():
while True: # 进入无限循环模式
onTick() # 执行策略主函数
Sleep(1000) # 休眠 1 秒
优宽量化采用轮询模式,首先需要定义一个 main
函数和一个 onTick
函数,main
函数是策略的入口函数,程序会从 main
函数开始逐行执行代码。在 main
函数中,写入 while
循环,重复执行 onTick
函数,所有的策略核心代码都写在 onTick
函数中。
第 2 步:定义虚拟持仓和外部变量
short = 10
long = 50
mp = 0
虚拟持仓的好处是编写简单,快速迭代策略更新,一般用于回测环境中,假设每一笔订单都完全成交,但在实际交易中常用的还是真实持仓。由于虚拟持仓是记录开平仓后的状态,所以需要定义成全局变量。
第 3 步:获取 K 线
exchange.SetContractType('rb000') # 订阅期货品种
bars_arr = exchange.GetRecords() # 获取 K 线数组
if len(bars_arr) < long + 1: # 如果 K 线数量过小
return
使用优宽量化的 SetContractType
,传入"rb000"
就可以订阅螺纹钢指数合约,但在回测和实盘中,是以螺纹钢指数为数据,使用具体的主力合约下单。接着使用 GetRecords
函数就可以获取螺纹钢指数的 K 线数据了。
注意:由于在计算乖离率时需要一定周期,所以为了避免程序出错,使用 if
语句过滤。
第 4 步:计算乖离率
close = bars_arr[-2]['Close'] # 获取上一根 K 线收盘价
ma1 = TA.MA(bars_arr, short)[-2] # 计算上一根 K 线短期均线值
bias1 = (close - ma1) / ma1 * 100 # 计算短期乖离率值
ma2 = TA.MA(bars_arr, long)[-2] # 计算上一根 K 线长期均线值
bias2 = (close - ma2) / ma2 * 100 # 计算长期乖离率值
根据乖离率计算公式,首先获取收盘价, 在这个策略中我们使用的是上一根 K 线收盘价,也就是当前 K 线信号成立,下根 K 线发单。接着使用优宽量化内置的 talib
库计算均线。
第 5 步:下单交易
global mp # 全局变量
current_price = bars_arr[-1]['Close'] # 最新价格
if mp > 0: # 如果持有多单
if bias2 <= bias1: # 如果长期乖离率小于等于短期乖离率
exchange.SetDirection("closebuy") # 设置交易方向和类型
exchange.Sell(current_price - 1, 1) # 平多单
mp = 0 # 重置虚拟持仓
elif mp < 0: # 如果持有空单
if bias2 >= bias1: # 如果长期乖离率大于等于短期乖离率
exchange.SetDirection("closesell") # 设置交易方向和类型
exchange.Buy(current_price + 1, 1) # 平空单
mp = 0 # 重置虚拟持仓
elif mp == 0: # 如果无持仓
if bias2 > bias1: # 长期乖离率大于短期乖离率
exchange.SetDirection("buy") # 设置交易方向和类型
exchange.Buy(current_price + 1, 1) # 开多单
mp = 1 # 重置虚拟持仓
elif bias2 < bias1: # 长期乖离率小于短期乖离率
exchange.SetDirection("sell") # 设置交易方向和类型
exchange.Sell(current_price - 1, 1) # 开空
单
mp = -1 # 重置虚拟持仓
由于我们在 while
循环外部定义了一个全局变量 mp
,用于接收当前的持仓状态,所以在使用这个变量的时候,需要先用 global
引入这个全局变量。另外还需要获取当前的最新价格用于开平仓。
# 外部参数和全局变量
short = 10
long = 50
mp = 0
# 策略主函数
def onTick():
# 获取数据
exchange.SetContractType('rb888') # 订阅期货品种
bars_arr = exchange.GetRecords() # 获取 K 线数组
if len(bars_arr) < long + 1: # 如果 K 线数量过小
return
# 计算 BIAS
close = bars_arr[-2]['Close'] # 获取上一根 K 线收盘价
ma1 = TA.MA(bars_arr, short)[-2] # 计算上一根 K 线短期均线值
bias1 = (close - ma1) / ma1 * 100 # 计算短期乖离率值
ma2 = TA.MA(bars_arr, long)[-2] # 计算上一根 K 线长期均线值
bias2 = (close - ma2) / ma2 * 100 # 计算长期乖离率值
# 下单交易
global mp # 全局变量
current_price = bars_arr[-1]['Close'] # 最新价格
if mp > 0: # 如果持有多单
if bias2 <= bias1: # 如果长期乖离率<=短期乖离率
exchange.SetDirection("closebuy") # 设置交易方向和类型
exchange.Sell(current_price - 1, 1) # 平多单
mp = 0 # 重置虚拟持仓
elif mp < 0: # 如果持有空单
if bias2 >= bias1: # 如果长期乖离率>=短期乖离率
exchange.SetDirection("closesell") # 设置交易方向和类型
exchange.Buy(current_price + 1, 1) # 平空单
mp = 0 # 重置虚拟持仓
elif mp == 0: # 如果无持仓
if bias2 > bias1: # 长期乖离率大于短期乖离率
exchange.SetDirection("buy") # 设置交易方向和类型
exchange.Buy(current_price + 1, 1) # 开多单
mp = 1 # 重置虚拟持仓
elif bias2 < bias1: # 长期乖离率小于短期乖离率
exchange.SetDirection("sell") # 设置交易方向和类型
exchange.Sell(current_price - 1, 1) # 开空单
mp = -1 # 重置虚拟持仓
# 程序入口函数
def main():
while True: # 循环
onTick() # 执行策略主函数
Sleep(1000) # 休眠 1 秒
本节我们学习了乖离率的原理,以及使用乖离率构建了一个简单的交易策略。在实际交易中乖离率是一种简单有效的交易工具,能为交易者提供有效的参考。
网格策略也称“渔网策略”,就行渔夫捕鱼一样,它是以某个价格为基准,在其上下分别设置价格线(撒网待鱼),每当价格触发价格线时,通过加减仓操作尽可能获利。网格策略属于左侧交易,不需要预测价格涨跌方向,不想右侧交易一样追涨杀跌,而是逆势而为,在价格下跌时买入,价格上涨时卖出。
网格交易本质上是一种空间变时间的玩法,其秉持的原则是“仓位策略比择时策略更重要”。简单的网格是以某个价位为基准点,当价格上涨戓下跌一定的点数或者一定的比例,挂N手数量空单戓多单,每一个格子即是盈利点位,但通常并不设置止损,当价格朝向与持仓有利的方向运动并达到网格点位时获利平仓,并且在该点位挂同样的买单戓卖单。这样这些交易订单就像渔网一样阵列,在行情的波动中来回盈利。在设计网格之前,首先需要根据历史数据确定网格的上限和下限,然后根据品种的波动率情况设置网格的宽度,还要根据自己的资金实力设计网格的数量,网格数量越多,则需要加仓的点位就越多,以及计算好补仓资金份额,防止潜在的风险导致破网。
网格编号 | 网格价格 | 多头持仓 | 空头持仓 |
---|---|---|---|
0 | 5000 | 1 | |
1 | 4950 | 1 | 2 |
2 | 4900 | 2 | 3 |
3 | 4850 | 3 | 4 |
4 | 4800 | 4 | 5 |
5 | 4750 | 5 | 6 |
6 | 4700 | 6 | 7 |
7 | 4650 | 7 | 8 |
8 | 4600 | 8 | 9 |
9 | 4550 | 9 | 10 |
10 | 4500 | 10 |
如上面的表格所示:网格的上限为5000元,下限为4500元,起始价格为5000元,网格的间距是50元,一共有10个网格,多头和空头最大的持仓量为10手。
好的,让我逐个解释每个函数的逻辑:
cancel_order()
:
exchange.GetOrders()
获取当前挂单列表。exchange.CancelOrder(order.Id)
取消订单。trade(type, price, unit)
:
type
参数设置交易方向。exchange.Buy()
或 exchange.Sell()
。on_bar()
:
top
,则执行卖出操作开空头。bottom
,则执行平空头止盈操作。initialize()
:
main()
:
initialize()
进行初始化。on_bar()
执行交易逻辑,并通过 Sleep(1000)
控制循环频率。def cancel_order():
orders = exchange.GetOrders()
if orders and len(orders) > 0:
for order in orders:
# 获取订单信息
order_info = exchange.GetOrder(order.Id)
# 获取订单类型描述
order_description = "多头开仓" if order_info['Type'] == 0 and order_info['Offset'] == 0 else \
"多头平仓" if order_info['Type'] == 1 and order_info['Offset'] == 1 else \
"空头开仓" if order_info['Type'] == 1 and order_info['Offset'] == 0 else \
"空头平仓" if order_info['Type'] == 0 and order_info['Offset'] == 1 else \
"未知交易类型"
# 取消订单
exchange.CancelOrder(order.Id)
# 记录撤销订单信息
Log(f"撤销挂单 价格:{order_info['Price']}, 类型:{order_description}")
def trade(type, price, unit):
exchange.SetDirection(type)
if type == 'buy' or type == 'closesell':
exchange.Buy(price, unit)
elif type == 'sell' or type == 'closebuy':
exchange.Sell(price, unit)
def on_bar():
global buy_line, sell_line
position = exchange.GetPosition()
depth = exchange.GetDepth()
if not depth:
return
ask = depth['Asks'][0].Price
bid = depth['Bids'][0].Price
if len(position) == 0:
if bid > top:
trade('sell', bid, unit * init_amount)
Log(contract_code, '到达开空区域, 买入空头底仓')
else:
trade('buy', ask, unit * init_amount)
Log(contract_code, '到达开多区域, 买入多头底仓')
if len(position) != 1:
return
if position[0]["Type"] == 1:
if ask < bottom:
Log(contract_code, '空单全部止盈反手')
trade('closesell', ask, position[0].Amount)
else:
orders = exchange.GetOrders()
if len(orders) == 0:
trade('sell', sell_line, unit)
trade('closesell', buy_line, unit)
if len(orders) == 1:
if orders[0]["Type"] %2 == 1: #止盈成交
Log(contract_code, '网格减仓, 当前份数:', position[0].Amount)
cancel_order()
buy_line = buy_line - space
sell_line = sell_line - space
if orders[0]["Type"] %2 == 0:
Log(contract_code, '网格加仓, 当前份数:', position[0].Amount)
cancel_order()
buy_line = buy_line + space
sell_line = sell_line + space
if position[0]["Type"] == 0:
if bid > top:
Log(contract_code, '多单全部止盈反手')
trade('closebuy', bid, position[0].Amount)
else:
orders = exchange.GetOrders()
if len(orders) == 0:
trade('buy', buy_line, unit)
trade('closebuy', sell_line, unit)
if len(orders) == 1:
if orders[0]["Type"] %2 == 0:
Log(contract_code, '网格减仓, 当前份数:', position[0].Amount)
cancel_order()
buy_line = buy_line + space
sell_line = sell_line + space
if orders[0]["Type"] %2 == 1:
Log(contract_code, '网格加仓, 当前份数:', position[0].Amount)
cancel_order()
buy_line = buy_line - space
sell_line = sell_line - space
def initialize():
global buy_line, sell_line
while not exchange.IO("status"):
Sleep(1000)
while not exchange.SetContractType(contract_code):
Sleep(1000)
while True:
ticker = exchange.GetTicker()
if ticker:
break
Sleep(1000)
buy_line = ticker["Last"] - space
sell_line = ticker["Last"] + space
Log('初始化网格', '#FF0000')
def main():
initialize()
while True:
on_bar()
Sleep(1000)
任何策略是有适用范围的,网格策略也不例外,商品期货属于杠杆交易,尤其是网格交易有逆势加仓的特点,风险巨大!因此在使用网格策略交易商品期货时有两个建议: 1、只做多不做空,这样在买入时确定最大成本。因为商品本身自有价值属性,其归零的可能性几乎没有,所以导致商品易涨难跌,长期来看价格上涨空间大,下跌空间小。 2、在期货期权上买入一定张数的虚沽期权进行保护。因为买入虚沽期权是时间价值,同时卖出平值认沽进行时间价值的对冲。那么如果期货价格上涨,期权因为是时间价值对冲,所以不会亏钱,而期货的多单是能赚钱的;如果期货价格下跌,因为有期权虚沽,所以还能赚钱。 3、使用跨期或跨品种价差进行网格交易。假如单个品种的历史走势有很强的规律性,如鸡蛋品种常年在3500~4500之间震荡,但是这仅仅是历史数据,以历史回测的最大单边幅度去设计网格的承受极限都是不可靠的,历史总是会被打破,谁也不能保证其价格会不会走出长期趋势行情。那么安全期间,我们可以利用主力合约与次主力合约的价差或者有相关性的跨品种价差做网格策略,相比于单品种价差的波动是相对稳定的。
前面章节我们集中于测试策略的讲解和编写,本节内容我们将专注于实盘级别策略。相对于Pine语言和My语言作为专业的金融语言,Python语言我们需要从底层编写架构应对实盘策略运行中可能出现的问题与解决办法。本章内容在首先介绍一些策略优化方法的基础上,后续为大家讲解一些实盘策略的编写思路和注意事项。
相对于固定策略中的一些变量,我们可以设置外部参数的方式从而对策略进行更好的回测和调试。策略界面上设置的策略参数,在策略代码中是以全局变量形式体现的。Python策略的函数中修改全局变量、策略界面参数时需要使用global关键字。
变量 | 描述 | 备注 | 类型 | 默认值 | 说明 |
---|---|---|---|---|---|
number | 数值类型 | 备注 | 数字型(number) | 1 | C++策略为浮点型。 |
string | 字符串 | 备注 | 字符串(string) | Hello FMZ | 默认值输入时不需要加引号,输入均作为字符串处理。 |
combox | 下拉框 | 备注 | 下拉框(selected) | 1|2|3 | combox变量本身是数值,代表下拉框控件选择的栏目的索引,第一个下拉框栏目内容是1,其索引值是0,依次类推。 |
bool | 勾选项 | 备注 | 布尔型(true/false) | true | 勾选上,变量bool为true,不勾选,变量bool为false。 |
secretString | 加密字符串 | 备注 | 加密串(string) | passWord | 使用和字符串相同,加密字符串会被加密发送,不会明文传输。 |
界面参数在策略代码中的变量名不要设置为当前编程语言的保留字(关键字)。
变量 | 描述 | 类型 | 默认值 |
---|---|---|---|
numberA | 数值A | 数字型(number) | 2 |
isShowA | 是否显示numberA参数 | 布尔型(true/false) | false |
可以设置一个参数,让另一个参数基于该参数的选择实现显示与隐藏。比如我们设置参数numberA,numberA是一个数值类型的界面参数。我们让numberA基于另一个参数:isShowA(布尔类型)的真假决定numberA显示与隐藏。
需要把numberA变量在界面参数上设置为:numberA@isShowA(策略编辑页面中策略参数栏,设置“变量”的控件中填写)。这样不勾选isShowA参数,numberA参数就隐藏了。
变量 | 描述 | 类型 | 默认值 |
---|---|---|---|
numberA | 数值A | 数字型(number) | 2 |
combox | 下拉框 | 下拉框(selected) | 1|2|3 |
对于下拉框控件类型的参数,参数依赖部分为判断是否等于下拉框某个选项的索引值。同样以numberA参数为例,在参数设置变量时写为:numberA@combox==2。numberA参数就基于combox参数是否选择为第三个选项3来进行显示或隐藏(索引0对应第一个选项,索引1对应第二个选项,索引2对应第三个选项)。
变量 | 描述 | 类型 | 默认值 |
---|---|---|---|
numberA | (?第一组)数值A | 数字型(number) | 2 |
isShowA | 是否显示numberA参数 | 布尔型(true/false) | false |
combox | (?第二组)下拉框 | 下拉框(selected) | 1|2|3 |
在开始分组的参数的描述(策略编辑页面中策略参数栏,设置“描述”的控件)开头加上(?第一组)前缀即可设置从该参数开始向下的参数(直到遇到其它分组结束)归纳于命名的“第一组”这个分组中。
在回测时如果希望将策略参数保存,可以在策略参数修改后点击「保存回测设置」按钮。运行实盘时需要保存实盘配置的参数数据,可以点击策略实盘页面中「参数设置」选项,再点击「导出参数」按钮,导出的策略参数将以json
文件保存。
导出的策略参数配置也可以再次导入实盘,点击「导入参数」按钮即可把保存的策略实盘参数导入到当前实盘,导入后点击「更新参数」按钮保存生效。
参数的调优是使用指标函数时非常重要的一步。在实际操作中,不同的交易市场和品种都有自己的特点,因此同样的指标函数,在不同的市场和品种中可能需要不同的参数设置,以达到最佳的效果。这也是为什么在量化交易中,参数的调优显得尤为重要。通过对历史数据进行分析和模拟回测,可以找到最适合当前市场的参数设置,提高交易策略的成功率和盈利水平。
下面这段代码中,我们设置可调参数为布林带周期’period’和布林带宽度’width’,在模拟回测界面我们设置参数调优的范围和步伐,接着我们就可以获取最优的参数设置。这里我们也可以设置优化过滤器,更好的设置不同参数的范围,防止无效的调参。
def main():
while True:
exchange.SetContractType('rb888')
r = exchange.GetRecords()
if len(r) < period + 1:
continue
boll = TA.BOLL(r, period, width) # 计算布林带指标
upLine, midLine, downLine = boll[:3] # 获取布林带的上轨、中轨和下轨数组
upPrice, midPrice, downPrice = upLine[-2], midLine[-2], downLine[-2] # 获取上上根K线的上轨、中轨和下轨价格
recclose = r[-1].Close # 获取上根K线的收盘价
posInfo = exchange.GetPosition()
# 开多条件:无持仓,并且收盘价大于上轨
if not posInfo and recclose > upPrice:
exchange.SetDirection("buy") # 设置下单方向为买入
exchange.Buy(recclose, 1) # 以收盘价开一手多头仓位
# 开空条件:无持仓,并且收盘价小于下轨
if not posInfo and recclose < downPrice:
exchange.SetDirection("sell") # 设置下单方向为卖出
exchange.Sell(recclose, 1) # 以收盘价开一手空头仓位
# 平多条件:持多头仓位,并且收盘价小于中轨
if posInfo and (posInfo[0].Type == PD_LONG or posInfo[0].Type == PD_LONG_YD) and recclose < midPrice:
exchange.SetDirection("closebuy") # 设置下单方向为平多
exchange.Sell(recclose - 5, 1) # 以收盘价减5作为限价平多仓位
# 平空条件:持空头仓位,并且收盘价大于中轨
if posInfo and (posInfo[0].Type == PD_SHORT or posInfo[0].Type == PD_SHORT_YD) and recclose > midPrice:
exchange.SetDirection("closesell") # 设置下单方向为平空
exchange.Buy(recclose + 5, 1) # 以收盘价加5作为限价平空仓位
Sleep(1000 * 60) # 休眠一分钟,等待下一个K线数据
根据对布林带周期’period’和布林带宽度’width’参数调试的结果,可以发现当布林带周期为24,布林带宽度为2.25的时候,我们取得了最优的收益。需要注意的是,最优参数可能会陷入过拟合的风险,所以并不是一直有效的。我们需要该参数进行更多样本外的测试验证参数的有效性。
在策略运行过程中,如果我们想实时调整策略参数,获取状态变量等交互的操作,我们可以交互控件来实现。下面我们来看下交互控件怎样设置。
和策略参数一样,我们可以根据需要进行不同类型交互控件参数的设置。策略交互控件具有五种类型,数字,布尔,字符,下拉框和按钮。我们举例示范下,我们可以设置一个按钮型,命名为action,描述为一个操作;设置一个数值型,actionnumber,来定义指定操作的数量;布尔型ifaction,是或者否的选择;selected,下拉框,进行多项的选择;str,字符型,可以输入字符;在交互预览页面,我们就可以看到交互控件呈现的页面。
交互控件设置完成以后,在策略中我们通过全局函数GetCommand()
,获取交互命令字符串。GetCommand()
函数会获取策略交互界面发来的命令并清空缓存,没有命令就会返回空字符串。返回的命令格式为按钮名称加上参数,如果交互控件没有参数(例如不带输入框的按钮控件),那么命令就是按钮名称。
交互控件是在策略运行时才可以使用的,这就意味着在回测中,交互控件是不支持的。因此,首先我们创建实盘,定义策略标签,选择托管主机和运行策略,定义策略周期,添加交易平台,这样实时运行的实盘就创建成功了。我们在实盘中运行一下这个控件测试代码,点开策略交互,显示出来我们刚才设置的五个按钮,我们分别点击一下。可以在日志信息中打印出来两个按钮的名称,如果有默认参数,就会附带上。按钮名称和按钮参数中间使用“:”进行分隔。如果我们只想打印参数,可以使用split函数。
这里我们先不设置控件需要进行的操作,只是使用Log
函数打印出来控件对应的操作,如果需要打印参数的话,使用相应的索引。
def main():
cmd = GetCommand()
if cmd:
Log("cmd:", cmd)
arr = cmd.split(":")
if arr[0] == "action":
Log("操作,该控件不带数量")
elif arr[0] == "actionnum":
Log("操作,该控件带数量:", arr[1])
elif arr[0] == "ifaction":
Log("是否进行操作", arr[1])
elif arr[0] == "selected":
Log("下拉框", arr[1])
elif arr[0] == "str":
Log("字符型", arr[1])
我们在实盘中运行一下这个控件测试代码,点开策略交互,显示出来我们刚才设置的五个按钮,我们分别点击一下。可以在日志信息中打印出来两个按钮的名称,如果有默认参数,就会附带上。按钮名称和按钮参数中间使用“:”进行分隔。如果我们只想打印参数,可以使用split函数。这里我们先不设置控件需要进行的操作,只是使用Log
函数打印出来控件对应的操作,如果有数量的话,再加上数量。可以看到,在实盘里,我们点击action按钮,会出现“买入,该控件不带数量”;点击actionnum,并设置数量2,会打印“操作,该控件带数量:2” 。其他的按钮大家可以自己修改探索一下。关于具体的使用场景,我们下面展开介绍。
我们来看第一个应用场景:半自动化交易。半自动化交易是一种结合量化交易和人工决策的交易方式。在半自动化交易中,交易者使用计算机程序或交易平台上的工具和功能来计算交易信号,但最终的交易操作需要交易者自己做出。交互控件在半自动化交易活动中尤其友好。半自动化交易需要快速决策和执行,交互控件的快速选择交易品种功能非常有用。通过简洁直观的界面设计,交易者可以迅速浏览可交易的品种列表或使用搜索功能快速找到所需的品种。
交互控件还允许交易者预设常用的交易参数,如数量、价格、止损止盈等。预设参数功能极大地简化了操作流程,我们只需在初始设置时输入一次参数,之后就能够随时使用这些预设参数进行快速下单,提升了下单的速度和效率。同时,交互控件还具备错误提示和确认功能,为半自动化交易提供了额外的安全保障。在输入有误或存在潜在风险时,交互控件可以及时提醒并帮助纠正错误。此外,在交易最终执行前,交互控件要求交易者再次确认交易指令,以确保下单操作的准确性和安全性。这有助于半自动化交易避免因输入错误或误操作而造成的交易错误,进一步减少了风险。总体而言,交互控件为半自动化交易提供了便利、效率和安全性。
def main():
contractlist = ['i2509', 'rb2509', 'hc2509']
p = ext.NewPositionManager()
while True:
if exchange.IO("status"):
LogStatus(_D())
cmd = GetCommand()
if cmd:
Log("cmd:", cmd)
arr = cmd.split(":")
if arr[0] == "selected":
contract = contractlist[int(arr[1])]
Log("买入期货品种", contract)
elif arr[0] == "buy":
p.OpenLong(contract, 1)
elif arr[0] == "covernum":
pos = p.GetPosition(contract, PD_LONG)
if pos is None or pos.Amount < int(arr[1]):
Log('没有足够的多仓可平')
else:
Log("平多仓数量", arr[1])
p.Cover(contract, int(arr[1]))
Sleep(1000)
else:
LogStatus(_D(), "未连接CTP!")
Sleep(500)
这里我们举一个例子来示范使用交互控件来选择不同的期货合约,并进行开多仓和平多仓操作。这段代码首先创建一个合约列表 contractlist
,其中包含了三个期货合约代码:‘i2509’, ‘rb2509’, ‘hc2509’。使用交易类库,创建一个新的单品种控制对象 p。接着获取用户输入的命令,并将其存储在变量 cmd 中。如果存在命令 (cmd),则执行以下逻辑:记录命令信息到日志,输出命令内容。并将命令内容用冒号分割成一个数组 arr,这样为了获取不同命令的参数。如果命令的第一个元素是 “selected”,表示选择了某个期货品种,根据命令的对应元素从合约列表中获取对应的合约代码,打印到日志中。
如果命令的第一个元素是 “buy”,表示买入期货品种,使用单品种控制对象 p 执行开多仓的操作,买入数量为1。如果命令的第一个元素是 “covernum”,表示平多仓,首先通过 p 获取当前持仓的信息,如果持仓量为空或者持有的多仓数量不足,则输出 “没有足够的多仓可平”;如果持有足够的多仓,先打印出来需要平多仓的数量,并使用cover执行平多仓的操作,平仓数量为命令的第二个元素。这里需要注意的是我们获取的arr[1]是一个字符型,需要转换为数值型。关于每一个控件对应的操作我们就设置完成了,接下来我们要在实盘中查看一下控件的运行。
我们在实盘中,首先选择期货品种,日志信息显示选择成功,然后点击buy,点击一次开一手多仓,点击两次,开两手;然后我们使用covernum进行平仓,这里我们首先填写数字3,因为我们目前持有的多仓数目为2,所有会显示没有足够的多仓可平,我们改为数字1,日志信息中呈现我们平了一手多仓。这就是使用交互控件进行交易的一个简单的例子。上面的代码示例演示了如何在策略中使用交互控件来实现期货交易的功能。通过设置不同类型的交互控件,我们可以提供给交易者一个直观、灵活的交互界面。用户可以通过点击按钮、选择下拉框等方式来输入指令,然后策略根据接收到的指令执行相应的交易操作。
请注意,这里只是为了展示控件的使用,还有很多不完善的地方。对于半自动化交易,使用交互控件可以使交易过程更加友好和灵活。通过在代码中集成交互控件,你可以与机器人进行交互,实现对交易的监控和操作,并根据需要进行自定义设置。例如,你可以添加一个交互控件来设置交易的执行时间,让用户选择在何时执行买入或平仓操作。这样,你可以根据市场的波动和行情状况,智能地调整交易时机,以提高交易的成功率和盈利能力。
在前面的课程中,我们介绍了策略参数的使用,主要针对于策略运行中的不同类型的参数进行控制。策略参数在策略运行中是不能被改动的,如果需要改动,则必须停掉策略修改参数才可以;而有的同学如果希望在交易过程中对策略实时的进行一定程度的控制和修改,交互控件可以帮助你完成这一目标。让我们举一个例子试试看。假如你的日内策略是使用20秒均线作为基准。然而由于美联储加息,你预感今天市场波动比较剧烈,所以你希望改变你的策略,使用10作为参考基准。如果你不设置交互控件,改变参数需要你停掉实盘,修改参数,然后重启实盘;在美联储加息平稳后,重新修改均线参数为20。而如果你使用交互控件,这一个问题变得简单了起来。
首先在策略交互中设置一个交互控件period为数值型,默认值是20。回到代码,默认的period为20,因此策略在起始阶段,会使用默认周期为20,然后判断k线的数量是否满足计算均值,然后打印“当前周期为:20”,输出20周期的均值;然后在代码中书写交互控件控制语句,当点击交互控件修改period周期以后,就会调整均线计算的周期,然后打印出来对应period的值,进行新的period均线的计算。
def main():
period = 20
while True:
if exchange.IO("status"):
LogStatus(_D(), "已连接CTP!")
exchange.SetContractType('rb888')
bars = exchange.GetRecords()
Log('当前周期为:', period)
if not bars or len(bars) <= period:
continue
base = TA.MA(bars, period)
Log('均线period: ', base[-1])
cmd = GetCommand()
if cmd:
Log("cmd:", cmd)
arr = cmd.split(":")
period = int(arr[1])
Log('修改周期为:', period, '#FF0000')
Sleep(1000)
else:
LogStatus(_D(), "未连接CTP!")
Sleep(500)
我们在实盘中看下,为了快速呈现结果,我们使用秒为周期,策略开始,日志输出的是20周期的均线,这里我们修改一下,周期变为10,可以看到日志信息中开始输出10周期的均线。
这就是实时修改参数的一个小例子,确实实现了不需要停掉实盘,重新修改策略的参数。这种实时修改参数的技术可以帮助你根据市场情况、风险偏好等因素进行灵活的调整,从而优化和改进你的交易策略。与停止机器人并重新编译代码相比,它提供了更方便快捷的方式来调整参数,同时也可以避免中断实盘交易。但需要注意的是,在实时修改参数时,要确保对参数的修改是合理和稳妥的。过于频繁或不恰当的参数调整可能会导致策略性能下降或产生意外的结果。因此,在使用这类功能时,建议谨慎评估和测试相关参数的变化对策略表现的影响,并确保在真实环境下具备足够的安全保障和风险控制措施。
量化策略的实时调试功能可以帮助你在开发和优化策略过程中快速验证和调整策略逻辑,以及观察策略的实际表现。在策略运行过程中,你可以插入调试语句,查询关键变量、指标和决策点的值,这样就可以查看日志并分析策略逻辑的执行路径,以找出潜在的问题和改进点。结合exec()
函数,交互控件可以实现实时调试功能。exec()
可以执行任意语句(包括赋值、函数定义等),这样就相当于给策略一个后门,你可以随时调用语句修改,或者查询策略的状态。当然这要在确保没有异常的情况下。
这里涉及到了交互控件的字符型控件。首先我们设置这个交互控件。定义为字符型。回到代码主体,我们首先设置价格和数量都为0;在主函数中,首先打印初始的价格和数量;然后获取界面交互控件的消息。在判断有交互信息的时候,输出调试代码并打印出来。
控件代码的运行用到了try和except语句。try和except常常被用于异常处理。try关键字:try关键字标记一个代码块,表示要检测异常的范围。在这个代码块中,你可以放置可能会抛出异常的代码。这里我们执行exec
函数。在这里,exec
传入的参数是一个字符串形式的代码。except关键字:如果在try中的代码执行过程中发生了异常,控制流将会跳转到except块中。except块用于捕获并处理异常。e参数:在except块中,e是一个表示异常的变量。通过使用e,你可以访问到抛出的异常对象,从而获取异常的相关信息。整体来说,当exec(code)
执行时,如果调试代码发生了异常,程序将会跳转到except块,except块中的代码将被执行。通过捕获异常并输出错误信息,你可以对异常进行处理,例如打印错误日志或采取其他适当的措施。
price = 0
amount = 0
def main():
Log("初始 price:", price, "初始 amount", amount)
while True:
cmd = GetCommand()
if cmd:
code = cmd.split(':', 1)[1]
Log("执行调试代码:", code)
try:
exec(code)
except Exception as e:
Log("Exception", e)
Sleep(1000)
我们在实盘中进行测试,启动实盘,可以看到首先打印出来原始设置的价格和数量,都为0。点击控件进行调试,在这里修改控件语句,设置新的价格和数量信息:
price = 100; amount = 1;
现在我们查看下新的价格和数量是多少,日志里是不会呈现的,继续使用控件语句查看我们的修改是否成功:
Log(price, amount)
可以看到日志信息打印出来了新的价格和数量。我们也可以设置一个错误语句试试看:
log(price, amount)
这里使用了小写的log,是一个错误的语句。所以会抛出异常Exception name 'log' is not defined
。这就是一个简单的在线调试功能,但需要谨慎使用,因为存在安全风险。在上面的代码中,通过exec(code)
执行了传入的代码,如果代码有异常,则会被except块捕获,并输出异常信息。虽然exec()
函数提供了便利的功能,但由于它可以执行任意调试代码,所以可能会被滥用和攻击。你可以实现更多的功能,大家可以挖掘下。
如果你认为交互控件的界面太过于死板,没关系,我们还可以定制个性化多功能交互控件页面,这里可以使用状态栏制定多功能交互面板。通过连接交互控件功能,我们就可以在状态栏进行我们需要的交互操作。
我们举一个比较粗糙的例子,在状态栏进行开仓和平仓的操作。
import json
def main():
p = ext.NewPositionManager()
while True:
tbl = {
"type": "table",
"title": "状态栏交互",
"cols": ["操作", "按钮"],
"rows": [
["开仓操作", {"type": "button", "cmd": "open", "name": "开仓", "input": {"name": "开仓数量", "type": "number", "defValue": 1}}],
["平仓操作", {"type": "button", "cmd": "coverAll", "name": "全部平仓"}]
]
}
log_msg = f"{_D()}\n`{json.dumps(tbl)}`"
LogStatus(log_msg)
while not exchange.IO('status'):
Log('未连接CTP')
Sleep(1000)
cmd = GetCommand()
if cmd:
arr = cmd.split(":")
if arr[0] == "open":
Log('收到开仓指令')
p.OpenLong('rb2409', int(arr[1]))
elif arr[0] == "coverAll":
Log('收
by2022 企业微信加不上啊啊啊啊啊啊
雨幕(youquant) 您好,企业微信满了,您加这个微信: https://www.youquant.com/upload/asset/1780ac4e8b9064c9d7d9a.png