资源加载中... loading...

Python商品期货量化入门教程

Author: 雨幕(youquant), Created: 2024-09-12 09:11:33, Updated: 2024-09-12 09:17:23

通过 Python自检发现错误。但实际上 Python 并不能主动找出所有的错误,有一些错误只有在运行过程中才能被发现,所有就需要用一种恰当的方式将错误源及信息呈现出来,并对错误进行修正以提高策略的健壮性。

3.10.1 语法错误

语法错误通常是初学者经常遇到的情况,例如少写了括号、布尔值 True 字符 T 需要大写等等。不过这种错误在 Python 启动时,通过对代码的解析会自动终止程序,并报出错误位置和信息。例如:

def main() ##应该是`main():`
    Log(1)

输出结果为:

Traceback (most recent call last): 
File "<string>", line 1481, in Run 
File "<string>", line 1 def main() ^ SyntaxError: invalid syntax

3.10.2 异常错误

异常错误比较隐蔽,通常在策略运行中才能被发现。例如:除法运算时,除数为 0。把一个值为空值的变量(None),当做字典使用。整型变量和字符串相加使用未定义的变量参与运算。例如:

def main():
    Log(10 / 0)

输出结果为:

Traceback (most recent call last): 
File "<string>", line 1481, in Run 
File "<string>", line 7, in <module> 
File "<string>", line 2, in main 
ZeroDivisionError: division by zero

3.10.3 异常捕获

为了检索隐藏的异常错误,或者为了避免异常错误的发生,导致正在运行的策略异常停止。可以使用 try…except 捕获异常。当执行 try 后的代码块时,如果发生异常错误,会被异常检测捕获,asException 这个异常类型的错误信息附加到 as 后的 e 这个变量中。例如:

def main():
    try:
        Log(10 / 0)
    except Exception as e:
        Log('错误', e)
    Log('hello FMZ')

运行结果为:

错误 division by zero
hello FMZ

上面的例子,在程序运算 10/0 时,并没有引发程序停止,而是打印了一条日志,并且最后一条日志 Log("hello FMZ") 也执行了。异常捕获不仅可以提示代码错误的原因,还可以防止程序因为异常导致终止运行。

第 4 章 优宽量化语法手册

本章节将详细介绍优宽量化平台提供的内置变量、结构体和内置函数。这些内容是实际编写量化交易程序所必需的基础。通过深入了解这些内容,能够更加灵活地运用优宽量化平台的功能,编写出高效、可靠的量化交易策略。本节内容将为大家提供具体的使用方法、示例代码以及常见问题的解答。

4.1 全局常量和数据结构

在优宽量化交易平台中,有很多API函数,每个函数都有各自的功能,它们返回的数据也不尽相同。通过本节对全局常量和数据结构的学习,可以知道这些函数返回的结果都有哪些意义。

4.1.1 exchange 交易所对象

exchange交易所对象是在编写策略时最常用的,因为绝大多数API函数都是该对象的方法。exchange交易所对象在策略代码中就代指了在创建实盘时或者回测时,添加的交易所。这些已经添加到平台的交易所,在添加时都绑定了交易所的API KEY(访问密钥)或者资金账号、资金密码(对于商品期货)。所以在使用例如:exchange.GetAccount()函数获取账户信息时,可以访问到对应期货账户的信息数据。exchange即添加的第一个交易所对象。

在添加了一个交易所对象之后,可以在策略代码中写入代码,打印该交易所对象的名字、标签信息,回测实盘均可:

def main():
    Log("实盘机器人页面或者回测页面上,添加的第一个交易所对象名称:", exchange.GetName(), ",标签:", exchange.GetLabel())

image

4.1.2 exchanges 交易所对象列表

学习完了exchange的概念,exchanges的概念就更加容易理解了。exchanges就是一系列的交易所对象放在一个列表中(数组),因为在优宽量化交易平台上一个策略可以设计成多交易所、多账户的架构,所以可以添加多个交易所对象。exchange的数组,包含多个交易所对象,exchanges[0]即是exchange。添加的交易所对象对应策略代码中的exchanges[0]、exchanges[1]、exchanges[2]…,以此类推。同样,在实盘或者回测可以使用以下代码测试,遍历exchanges交易所对象数组,逐个打印交易所对象的名称、标签信息。

def main():
    for i in range(len(exchanges)):
        Log("添加的交易所对象索引(第一个为0以此类推):", i, "名称:", exchanges[i].GetName(), "标签:", exchanges[i].GetLabel())

在实盘中,添加两个交易所"国泰君安"、“华安期货”,可以看到实盘策略依次输入两个交易所的索引和标签。

image

4.1.3 Order 结构

订单结构由exchange.GetOrder()exchange.GetOrders()函数返回。优宽量化平台定义、封装的订单数据结构,其中属性StatusTypeOffset的值为固定的几种取值。

{
    Info: {...}, # 请求交易所接口返回的原始数据,回测时无此属性
    Id: 123456, # 交易单唯一标识
    Symbol: rb2410, # 合约名称
    Price: 1000, # 下单价格
    Amount: 10, # 下单数量
    DealAmount: 10, # 成交数量
    AvgPrice: 1000, # 成交均价
    Status: 1, # 订单状态
    Type: 0, # 订单类型
    Offset: 0, # 订单的开平仓方向
    ContractType: "" # 订单的合约代码
}
  • Info 属性:接口返回的原始数据即封装之前的数据内容。
  • Symbol属性: 合约名称
  • Id 属性:订单的 ID,用于取消某个订单,查询某个订单时用作参数。
  • Price 属性:订单的委托价格。
  • Amount 属性:订单的委托数量。
  • DealAmount 属性:订单成交部分的数量。
  • AvgPrice 属性:订单的成交均价。
  • Status 属性:订单状态属性,例如挂单状态、完全成交状态、撤销状态。
  • Type 属性:标记订单是买单还是卖单。
  • Offset 属性:在期货交易时,标记订单是开仓单还是平仓单。
  • ContractType 属性:订单的合约代码。

以下表格定义了 Order 结构中的Status属性,分别表示了订单未完成、已完成、已取消、未知状态等 4 个值。

常量名 定义
ORDER_STATE_PENDING 未完成 0
ORDER_STATE_CLOSED 已完成 1
ORDER_STATE_CANCELED 已取消 2
ORDER_STATE_UNKNOWN 未知状态 3

在编写代码时,可以直接使用ORDER_STATE_PENDING判断订单状态,因为ORDER_STATE_PENDING非常容易看明白,判断的状态是挂单状态,而用 0 则非常不直观。

# 第一种写法
if order["Status"] == ORDER_STATE_PENDING:
    Log("订单状态值:", order["Status"])
# 第二种写法
if order["Status"] == 0:
    Log("订单状态值:", order["Status"])

以下表格定义了 Order 结构中的Type属性,分别表示订单为买单、订单为卖单,当订单为买单时值为 0,当订单为卖单时值为 1。

常量名 定义
ORDER_TYPE_BUY 订单为买单 0
ORDER_TYPE_SELL 订单为卖单 1

下面的表格定义了 Order 结构中的Offset属性,分别表示订单开仓方向、订单平仓方向,当订单为开仓方向时值为 0,当订单为平仓方向值为 1。

常量名 定义
ORDER_OFFSET_OPEN 订单为开仓方向 0
ORDER_OFFSET_CLOSE 订单为平仓方向 1

注意GetOrder需要传入订单号参数,获取的是某一个订单的 Order 结构体。而GetOrders不需要传入参数,获取所有未完成的订单。返回值的是 Order 结构体数组。如果当前交易对没有挂单时,调用exchange.GetOrders()返回空数组,即:[]

4.1.4 Position 结构

Position 结构是期货交易中的持有仓位信息,由exchange.GetPositions()函数返回此结构数组。

{
    Info: {...}, # 请求交易所接口返回的原始数据,回测时无此属性
    Symbol: rb2410, # 合约名称
    MarginLevel: 10, # 杆杠大小
    Amount: 100, # 持仓量
    FrozenAmount: 0, # 仓位冻结数量
    Price: 10000, # 持仓均价
    Profit: 0, # 持仓浮动盈亏
    Type: 0, # 持仓方向
    ContractType: "quarter", # 持仓的合约代码
    Margin: 1 # 仓位占用的保证金
}
  • Info 属性:同上。
  • Symbol: 合约名称
  • MarginLevel 属性:持仓的杠杆数值。
  • Amount 属性:该仓位持仓数量。
  • FrozenAmount 属性:仓位冻结数量。
  • Price 属性:持仓均价,持仓后,加仓会影响该值。
  • Profit 属性:持仓盈亏。
  • Type 属性:仓位类型,多头仓位、空头仓位。
  • ContractType 属性:合约代码。
  • Margin 属性:保证金。

GetPosition()函数返回的是 Position 结构数组,其中的Type属性代表持仓方向,PD_LONG代表多头仓位,PD_SHORT代表空头仓位,PD_LONG_YD代表昨日多头仓位,PD_SHORT_YD代表昨日空头仓位。

常量名 定义
PD_LONG 多头仓位 0
PD_SHORT 空头仓位 1
PD_LONG_YD 昨日多头仓位 2
PD_SHORT_YD 昨日空头仓位 3

注意GetPosition函数获取的是所有持仓品种的持仓信息,如果没有持仓返回空数组,所以引用前要先判断。

4.1.5 Trade 结构

获取所有交易历史(非自己),由exchange.GetTrades()函数返回。这个是整个市场最近时间的成交记录。

{
    Time: 1567736576000, # 时间(Unix timestamp 毫秒)
    Price: 1000, # 价格
    Amount: 1, # 数量
    Type: 0 # 订单类型
}
  • Time 属性:毫秒时间戳,记录市场上这笔成交的时间。
  • Price 属性:市场上这笔成交记录的成交价格。
  • Amount 属性:市场上这笔成交记录的数量。
  • Type 属性:标记这笔成交是买单主动成交,还是卖单主动成交。

注意:商品期货市场不返回 Trade 结构数据。

4.1.6 Ticker 结构

市场行情由exchange.GetTicker()函数返回。在商品期货中,通常情况下每秒返回2个 Tick 数据,通常指的是盘口数据。

{
    Info : {...}, # 请求交易所接口返回的原始数据,回测时无此属性
    Symbol: rb2410, # 合约名称
    High : 1000, # 最高价
    Low : 500, # 最低价
    Sell : 900, # 卖一价
    Buy : 899, # 买一价
    Last : 900, # 最后成交价
    Open : 550, # 开盘价
    Volume : 10000000, # 最近成交量
    OpenInterest: 500000, # 持仓量    
    Time : 1567736576000 # 毫秒级别时间戳
}
  • Info 属性:同上。
  • High 属性:一般为24小时内的最高价。
  • Low 属性:一般为24小时内的最低价。
  • Sell 属性:当前的卖一价格。
  • Buy 属性:当前的买一价格。
  • Last 属性:当前的最新成交价。
  • Volume 属性:最新成交量。
  • OpenInterest 属性:持仓量。
  • Time 属性:毫秒级别时间戳,用于标记时间。

4.1.7 Record 结构

标准 OHLC 结构数据包含了开盘价、最高价、最低价、收盘价、成交量、时间等数据,它是组成 K 线的最基本数据,由exchange.GetRecords()函数返回此结构数组。其中每个 Record 结构数据都代表一个 K 线。

{
    Time: 1567736576000, # K 线时间戳
    Open: 1000, # 开盘价
    High: 1500, # 最高价
    Low: 900, # 最低价
    Close: 1200, # 收盘价
    OpenInterest: 500000, # 持仓量    
    Volume: 1000000 # 交易量
}
  • Time 属性:毫秒级别时间戳,对于一个Record结构,其Time属性值为这根K线Bar的周期的起始时间戳。
  • Open 属性:开盘价。
  • High 属性:最高价。
  • Low 属性:最低价。
  • Close 属性:收盘价。
  • OpenInterest 属性:持仓量。
  • Volume 属性:成交量。

4.1.8 Depth 结构

市场深度,由exchange.GetDepth()函数返回。返回值是 Depth 结构体,结构体包含两个结构体数组,分别是 Asks[]和 Bids[],其中每个数组中包含价格 Price、数量 Amount 以及时间戳 Time。

{
    Asks : [...], # 卖单数组,MarketOrder 数组,按价格从低向高排序
    Bids : [...], # 买单数组,MarketOrder 数组,按价格从高向低排序
    Time : 1567736576000 # 毫秒级别时间戳
}

Depth 数据结构的 Asks 键,为卖单列表,列表中每个数据均为 MarketOrder 数据。Depth 数据结构的 Bids 键,为买单列表,列表中每个数据均为 MarketOrder 数据。

注意:不同交易所返回的深度数据是不同的,对于上期所和上能源,可以返回5档数据;对于其他交易所,仅支持返回1档数据。

4.1.9 Account 结构

Account 结构是由exchange.GetAccount()函数返回的账户信息,主要包含3个数据:账户余额、账户冻结余额,以及请求交易所接口返回的原始数据。

{
    Info: {...}, # 请求交易所接口返回的原始数据,回测时无此属性
    Balance: 1000, # 账户余额
    FrozenBalance: 0, # 账户冻结的余额
    Stocks: 0, 
    FrozenStocks: 0,
    Equity: 1000, #账户权益
    UPnL: 0 #持仓盈亏
}
  • Info 属性:交易所接口返回的原始数据,回测时无此属性。。
  • Balance 属性:可用资金数量,CTP商品期货中,该属性为可用钱数。
  • FrozenBalance 属性:冻结资金数量,如果下单后,订单未成交,则冻结该订单用于交易的资金,FrozenBalance 即为冻结的资金数量。
  • Stocks 传统期货、股票证券此属性固定为0。
  • FrozenStocks 传统期货、股票证券此属性固定为0。
  • Equity属性:账户权益,包含:可用资产余额、持仓保证金、持仓盈亏等。
  • UPnL属性:所有持有仓位的持仓盈亏。

4.2 获取 Tick、深度、历史 K 线数据

在商品期货量化交易中,需要使用各种不同类型的数据,如交易所原始 Tick 数据、订单薄深度数据,以及常用的 K 线数据。让我们看看如何使用 API 函数获取这些数据,以及常用的商品期货策略框架。

4.2.1 exchange.GetTicker()

Tick 数据俗称交易快照,是交易所内的数据截面。国内商品期货每秒有 2 个 Tick 数据。exchange.GetTicker()函数用于获取实时 tick 数据,返回 Ticker 结构。在回测系统中,该函数返回的 Ticker 数据中 High、Low 是模拟值,取自当时盘口的卖一价和买一价。在实盘中,则是交易所 Tick 接口定义的一定周期内的最高价和最低价。

def main():
    exchange.SetContractType("MA888")
    Log(exchange.GetTicker())

注意:在调用任何访问交易所接口的 API 函数时(如exchange.GetTicker()exchange.Buy(Price, Amount)exchange.CancelOrder(Id)等),都有可能由于各种原因导致访问失败。因此,需要对这些函数的调用做容错处理。例如:

def main():
    exchange.SetContractType("MA888")
    ticker = exchange.GetTicker()
    if not ticker:
        ticker = exchange.GetTicker()

升级API改动中,exchange.GetTicker()函数增加了symbol参数,可以直接在请求行情数据时指定品种。

  • exchange.GetTicker("rb2410") 直接查询rb2410合约的行情数据。

4.2.2 exchange.GetDepth()

exchange.GetDepth()函数用于获取交易所订单薄(深度数据),返回值是 Depth 结构体。Depth 结构体包含两个结构体数组,分别是 Asks[] 和 Bids[],其中 Asks 和 Bids 包含以下结构体变量:

  • Price:价格
  • Amount:数量

例如,要获取当前卖二价,可以这样写代码:

def main():
    exchange.SetContractType("MA888")
    depth = exchange.GetDepth()
    price = depth["Asks"][1]["Price"]
    Log("卖二价为:", price)

注意:商品期货涨停时,卖单卖一的价格是涨停价格,订单量是 0。跌停时,买单买一的价格是跌停价格,订单量是 0。通过判断买一、卖一的订单量数量,可以判断是否涨跌停。

升级API改动中,exchange.GetDepth()函数也增加了symbol参数,可以直接在请求深度数据时指定品种。

  • exchange.GetDepth("rb2410") 直接查询rb2410合约的深度数据。

4.2.3 exchange.GetRecords()

exchange.GetRecords()除了支持symbol参数直接指定请求的K线数据的品种信息。保留了原有的period参数用来指定K线周期,还增加了一个limit参数用来指定请求时期望的K线长度。同时也兼容旧版本的GetRecords函数只传入period周期参数的调用方式。

exchange.GetRecords()函数的调用方式:

  • exchange.GetRecords() 不指定任何参数时请求当前合约代码对应的品种的K线数据,K线周期是策略回测界面或者实盘时设置的默认K线周期。
  • exchange.GetRecords(60 * 15) 仅指定K线周期参数时,请求当前合约代码对应的品种的K线数据。
  • exchange.GetRecords(“rb2410”) 仅指定品种信息时,请求指定品种的K线数据,K线周期是策略回测界面或者实盘时设置的默认K线周期。
  • exchange.GetRecords(“MA888”, 60 * 60) 指定品种信息,指定具体K线周期请求K线数据。
  • exchange.GetRecords(“i888”, 60, 1000) 指定品种信息,指定具体K线周期,指定期望获取的K线长度请求K线数据。
def main():
    while not exchange.IO("status"):
        Sleep(1000)
    Log(exchange.SetContractType("rb888"))
    
    records = exchange.GetRecords(PERIOD_H1)
    Log("第一根k线数据为,Time:", records[0]["Time"], "Open:", records[0]["Open"], "High:", records[0]["High"])
    Log("第二根k线数据为,Time:", records[1]["Time"], "Close:", records[1]["Close"])
    Log("当前K线(最新)", records[-1], "上一根K线", records[-2])

4.2.4 商品期货策略框架

商品期货策略需要检测与期货公司前置机连接状态,在获取行情之前需要订阅合约,这样才能获取订阅的行情。在优宽量化交易平台上,回测时模拟如同实盘一样的连接机制。

以上我们了解、学习的行情接口因为篇幅有限,为了容易理解,并没有写设置合约、检测与前置机连接状态等代码,那么一个完整的商品期货策略框架是什么样的呢?完整的框架:

def main():
    while True:
        if exchange.IO("status"):
            exchange.SetContractType("rb888")
            ticker = exchange.GetTicker()
            depth = exchange.GetDepth()
            trades = exchange.GetTrades()
            records = exchange.GetRecords()
            Log("rb888 ticker Last:", ticker["Last"])
            Log("rb888 depth:", depth)
            Log("rb888 trades:", trades)
            Log("rb888 records:", records)
            LogStatus(_D(), "已经连接 CTP ! ")
        else:
            LogStatus(_D(), "未连接 CTP ! ")

其中我们陌生的代码也只有:

  • exchange.IO("status")
  • exchange.SetContractType("rb888")
  • LogStatus(_D(), "已经连接 CTP ! ")

exchange.IO("status")函数可以判断当前是否和期货公司前置机连接,如果连接返回 1,如果非连接状态返回 0。

exchange.SetContractType("rb888")函数调用是把当前合约设置为 rb888 并订阅该合约,rb888 是螺纹钢主力合约。如果想使用指数合约,可以用 rb000。

LogStatus(_D(), "已经连接 CTP ! ")函数的作用是在机器人状态栏上显示时间信息和文字,_D()函数返回当前时间的字符串。该策略代码会不停循环执行打印行情数据。

4.3 获取和取消订单、获取当前挂单

有时下单之后,可能会因为行情或者价格的原因,导致订单不能完全成交或者订单只成交了一部分。所以就需要下单之后了解订单状态,以及对未成交的订单撤单处理。

4.3.1 exchange.SetContractType(ContractType)

在商品期货中要想获取行情和下单交易,首先要先订阅合约代码,才能进行下一步操作。exchange.SetContractType(ContractType)函数用于设置合约类型,参数值:string类型。如同我们上节课学习的,商品期货策略框架中设置合约,并且订阅该合约。我们一起来看一个新例子:

def main():
    while True:
        if exchange.IO("status"):
            ret = exchange.SetContractType("MA888")
            Log("订阅的合约的详细信息: ", ret)
            break
        else:
            LogStatus(_D(), "未连接")

exchange.SetContractType("MA888")函数返回合约的详细信息,赋值给ret变量。输出ret变量结果:

{
    'CombinationType': 48,
    'CreateDate': 0,
    'DeliveryMonth': 4,
    'DeliveryYear': 0,
    'EndDelivDate': 0,
    'ExchangeID': 'CZCE',
    'ExchangeInstID': 'MA005',
    'ExpireDate': 0,
    'InstLifePhase': 49,
    'InstrumentID': 'MA005',
    'InstrumentName': 'MA 连续',
    'IsTrading': 1,
    'LongMarginRatio': 0.07,
    'MaxLimitOrderVolume': 1000,
    'MaxMarginSideAlgorithm': 48,
    'MaxMarketOrderVolume': 1000,
    'MinLimitOrderVolume': 1,
    'MinMarketOrderVolume': 1,
    'OpenDate': 0,
    'OptionsType': 0,
    'PositionDateType': 50,
    'PositionType': 50,
    'PriceTick': 1,
    'ProductClass': 49,
    'ProductID': 'MA',
    'ShortMarginRatio': 0.07,
    'StartDelivDate': 0,
    'StrikePrice': 0,
    'UnderlyingInstrID': '',
    'UnderlyingMultiple': 1,
    'VolumeMultiple': 10
}

可以看到有不少信息在我们写策略时是可以用的,比如合约乘数即一手合约是多少商品,例如MA甲醇,一手是10吨。

注意:当前合约设置为MA888之后,就可以获取当前MA主力合约的行情,对当前主力合约下单等操作。我们在所有操作前首先要确保和期货公司前置机(服务器)连接,再者要明确当前操作哪个合约。

4.3.2 exchange.SetDirection(Direction)

SetDirection函数可以设置期货下单方向,参数常用的有四种:buyclosebuysellclosesell。商品期货多出closebuy_today,与closesell_today,指平今仓,默认为closebuy/closesell为平昨仓。

  • exchange.SetDirection("buy") 买入开多仓
  • exchange.SetDirection("sell") 卖出开空仓
  • exchange.SetDirection("closebuy") 卖出平多仓
  • exchange.SetDirection("closesell") 买入平空仓
  • exchange.SetDirection("closebuy_today") 卖出平今日多仓
  • exchange.SetDirection("closesell_today") 买入平今日空仓

4.3.3 exchange.Buy(Price, Amount)

下买单函数,第一个参数为下单价格,第二个参数为下单量。下单成功后返回一个订单ID。测试例子:

def main():
    while True:
        if exchange.IO("status"):
            # 如果交易所连接正常,则执行交易操作
            ret = exchange.SetContractType("MA888")
            ticker = exchange.GetTicker()
            exchange.SetDirection("buy")
            id = exchange.Buy(ticker.Buy, 1)
            Log(id)
            break  # 执行完交易操作后退出循环
        else:
            # 如果交易所未连接,则记录当前时间并打印状态消息
            LogStatus(_D(), "未连接")

输出结果为:

image

以上例子用当时的行情中的买一价作为下单价格,下单量1手,下了一个开多仓订单。我们可以看到如果开多仓,exchange.Buy(ticker.Buy, 1)是和exchange.SetDirection("buy")配合使用的,那么下单函数和exchange.SetDirection()函数都有哪些组合呢?

下单函数 SetDirection参数设置方向 备注
exchange.Buy “buy” 买入开多仓
“closesell” 买入平空仓
“closesell_today” 买入平今日空仓
exchange.Sell “sell” 卖出开空仓
“closebuy” 卖出平多仓
“closebuy_today” 卖出平今日多仓

4.3.4 exchange.Sell(Price, Amount)

下卖单,返回订单编号,可用于查询订单信息和取消订单。期货下单时必须注意交易方向是否设置正确。

def main():
    while True:
        if exchange.IO("status"):
            ret = exchange.SetContractType("MA888")
            ticker = exchange.GetTicker()
            exchange.SetDirection("sell")
            id = exchange.Sell(ticker.Sell, 1)
            Log("开空仓订单 ID: ", id)
            break
        else:
            LogStatus(_D(), "未连接")

4.3.5 exchange.CreateOrder()

exchange.CreateOrder()函数用于下单。该函数最大的功能是直接在该函数的参数中指定下单的品种、方向。这样就不再依赖系统当前设置的交易对、合约代码、交易方向等设置了。

在多品种交易下单场景中、并发场景中极大程度的降低了设计复杂度。exchange.CreateOrder()函数的四个参数分别是symbol、side、price、amount。用来指定订单的合约代码、方向、价格、数量。

def main():
    while not exchange.IO("status"):
        Sleep(1000)
    id = exchange.CreateOrder("rb2410", "buy", 3500, 1)
    Log(id)

4.3.6 exchange.CancelOrder(orderId)

exchange.CancelOrder(orderId)函数可以根据订单ID取消订单。如下面的代码:首先订阅了MA888合约,然后获取Tick行情,接着设置下单方向,并使用Sell函数下单。变量id接收了下单后返回的订单编号,最后使用CancelOrder函数传入id参数来取消这个订单。在撤单后加break是为了让撤单后就跳出循环,否则会不停下单撤单。

def main():
    while True:
        if exchange.IO("status"):
            ret = exchange.SetContractType("MA888")
            ticker = exchange.GetTicker()
            exchange.SetDirection("sell")
            id = exchange.Sell(ticker.Sell, 1)
            Log("开空仓订单 ID: ", id)
            Sleep(5000)
            exchange.CancelOrder(id)
            break
        else:
            LogStatus(_D(), "未连接")

输出结果为:

image

4.3.7 exchange.GetOrders()

获取所有未完成的订单。返回值: Order结构体数组。当交易所对象exchange代表的账户当前没有挂单时,调用exchange.GetOrders()返回空数组,即:[]。

def main():
    contractTypeList = ["MA888", "rb888", "i888"]
    while True:
        if exchange.IO("status"):
            for i in range(len(contractTypeList)):
                ret = exchange.SetContractType(contractTypeList[i])
                ticker = exchange.GetTicker()
                exchange.SetDirection("sell")
                id = exchange.Sell(ticker.Sell, 1)
                Log(contractTypeList[i], "开空仓订单 ID: ", id)
            orders = exchange.GetOrders()
            for i in range(len(orders)):
                Log(orders[i])
            break
        else:
            LogStatus(_D(), "未连接")

三种合约逐个切换,下单。然后调用exchange.GetOrders()函数获取当前挂单。然后逐个打印,回测运行,输出结果为:

image

三个订单的Status返回的值都是0,代表“未完成”,从上面的例子中我们可以看出,GetOrders函数的返回值是不区分当前设置的合约的。它返回的是所有未完成的订单。

升级API改动中,exchange.GetOrders()函数也增加了symbol参数,可以直接指定查询当前未完成订单(挂单)的合约代码。

  • exchange.GetOrders(“rb888”) 查询rb888合约的所有未完成订单。

4.3.8 exchange.GetOrder(orderId)

根据订单号获取订单详情,参数值:orderid为要获取的订单号,string类型或数值类型。返回值:Order结构体。

def main():
    while True:
        if exchange.IO("status"):
            ret = exchange.SetContractType("MA888")
            ticker = exchange.GetTicker()
            exchange.SetDirection("sell")
            id = exchange.Sell(ticker.Sell, 1)
            Log("开空仓订单 ID: ", id)
            Sleep(5000)
            exchange.CancelOrder(id)
            Sleep(5000)
            Log(exchange.GetOrder(id))
            break
        else:
            LogStatus(_D(), "未连接")

上面的例子是下单之后获取订单的id,然后使用CancelOrder取消这个id的订单,最后使用GetOrder函数获取这个id的当前订单状态,可以看到打印出的订单信息,其中Status属性为2(“已撤销”)。输出结果为:

image

4.3.9 exchange.GetHistoryOrders

exchange.GetHistoryOrders()函数用于获取当前交易日内的所有合约的历史订单,支持查询指定合约的历史订单。

exchange.GetHistoryOrders()函数有两种调用形式:

  • exchange.GetHistoryOrders() 当不传任何参数时,获取所有合约的历史订单。
  • exchange.GetHistoryOrders("rb2410") 当指定具体合约代码时,获取具体合约的历史订单。
def main():
    while not exchange.IO("status"):
        Sleep(1000)
        
    exchange.SetContractType("rb888")
    exchange.SetDirection("sell")

    order1 = exchange.Sell(99999, 1) # 准备撤销订单
    order2 = exchange.Sell(exchange.GetTicker().Buy - 5, 1) # 成交订单

    exchange.CancelOrder(order1)
    
    #不设置合约参数
    orders = exchange.GetHistoryOrders()
    Log(orders)
    #设置合约参数
    rborder = exchange.GetHistoryOrders('rb888')
    Log(rborder)

注:

回测系统和实盘系统该函数返回结果有所区别:

  • 回测系统:返回策略运行期间所有完成订单,可使用since参数模拟本交易日生成订单;
  • 实盘系统:仅返回本交易日完成订单。

4.4 IO 扩展函数

4.4.1 IO 函数切换行情模式

商品期货行情是推送机制,在优宽量化交易平台,我们可以使用exchange.IO()函数切换行情模式。一共有三种模式可以切换。

  • exchange.IO("mode", 0): 立即返回模式,如果当前还没有接收到交易所最新的行情数据推送,就立即返回旧的行情数据,如果有新的数据就返回新的数据,设置为该模式后,行情接口调用时会立即返回,用于非阻塞的策略架构设计,例如多品种策略。
  • exchange.IO("mode", 1): 缓存模式(默认模式),如果当前还没有收到交易所最新的行情数据(同上一次接口获取的数据比较),就等待接收然后再返回,如果调用该函数之前收到了最新的行情数据,就立即返回最新的数据。设置该模式后,在没有收到最新行情时,会阻塞在该函数。通常用于单品种的交易策略,因为只用处理一个合约的行情,处理最新行情即可,其他时间可以阻塞等待。
  • exchange.IO("mode", 2): 强制更新模式,进入等待一直到接收到交易所下一次的最新推送数据后返回。这种模式,只使用最新获取的行情数据,即强制等待下一次数据推送过来。

注意:一般情况下使用默认的缓存模式。

4.4.2 IO 函数判断与期货公司前置机连接状态

exchange.IO("status")

这个函数调用我们应该并不陌生了,在之前的课程中,我们已经使用过了,该函数非常简单,使用时判断其返回值即可,返回值为真,代表和期货公司前置机服务器连接成功,返回值为假,代表和期货公司前置机服务器未连接。

4.4.3 IO 函数获取交易所所有合约

IO函数的instruments参数可以获取交易所所有合约,返回交易所所有合约的列表,只支持实盘,完整的查询范例:

def main():
    while not exchange.IO("status"):
        LogStatus("正在等待与交易服务器连接, " + _D())
    Log("开始获取所有合约")
    instruments = _C(exchange.IO, "instruments")
    Log("合约列表获取成功")
    length = 0
    for i in range(len(instruments)):
        length += 1
    Log("合约列表长度为:", length)

类似的调用还有:

  • exchange.IO("products")返回交易所所有产品的列表,只支持实盘。
  • exchange.IO("subscribed")返回已订阅行情的合约,格式同上,只支持实盘。
  • exchange.IO("settlement")结算单查询,只支持实盘。

4.4.4 exchange.IO("api", …)

优宽量化的CTP(商品期货)终端提供了完整的全API实现,当优宽量化平台的API满足不了你需要的功能时。可以用exchange.IO函数进行更深层的系统调用,完全兼容官方的Api名称。CTP的IO直接扩展函数调用请求,将会在收到第一个isLast标记为true的响应包后返回。

注意:该方法不支持回测和模拟交易,只支持实盘交易。

  • 查询投资者信息:
def main():
    while not exchange.IO("status"):
        LogStatus("正在等待与交易服务器连接, " + _D())
    Log(exchange.IO("api", "ReqQryInvestor"))
  • 修改密码:
def main():
    Sleep(6000)
    exchange.IO("api", "ReqUserPasswordUpdate", {"BrokerID": "9999", "UserID": "11111", "OldPassword": "oldpass", "NewPassword": "newpass"})
  • 查询结算单:
def main():
    while not exchange.IO("status"):
        LogStatus("正在等待与交易服务器连接, " + _D())
    r = exchange.IO("api", "ReqQrySettlementInfo", {"TradingDay": "20190506"})
    s = ''
    for i in range(len(r)):
        for ii in range(len(r[i])):
            if r[i][ii]["Name"] == "CThostFtdcSettlementInfoField":
                s += r[i][ii]["Value"]["Content"]
    Log(s)

4.4.5 exchange.IO("wait")

exchange.IO("wait")函数可以使程序在有最新事件时进行响应,执行程序逻辑,在没有新事件触发时,该函数会阻塞,可以实现回调机制的策略设计。当前交易所有任何品种更新行情信息或订单成交时才返回,返回EventTick/OrderEvent结构。

只支持商品期货实盘。在使用exchange.IO("wait")时,必须至少已经订阅了一个当前处于交易状态的合约(已经交割的过期合约,不会再有行情数据),否则会阻塞在该函数(由于没有任何行情、订单更新)。

一个简单实现回调机制的例子:

def on_tick(symbol, ticker):
    Log("symbol:", symbol, "update")
    # 数据结构: https://www.youquant.com/api#ticker
    Log("ticker:", ticker)

def on_order(order):
    Log("order update", order)

def main():
    while not exchange.IO("status"):
        Sleep(10)

    exchange.IO("mode", 0)

    _C(exchange.SetContractType, "MA001")

    while True:
        e = exchange.IO("wait")
        if e:
            if e.Event == "tick":
                on_tick(e['Symbol'], e['Ticker'])
            elif e.Event == "order":
                on_order(e['Order'])

以上我们通过一个例子来说明exchange.IO("wait")的使用方法,该方法的特点是当前交易所有任何品种更新行情信息或订单成交时才返回,所以非常适合用于单品种、多品种的回调机制策略设计。

注意:该方法不支持回测和模拟交易,只支持实盘交易。

4.5 账户API获取账户和持仓信息

账户信息和持仓信息关乎着策略逻辑,是策略逻辑的必须条件,在优宽量化平台中,可以使用GetAccount函数获取账户信息,用GetPosition函数获取持仓信息。

4.5.1 exchange.GetAccount()

exchange.GetAccount()函数返回交易所账户信息。通常使用返回的数据中的Balance属性,即账户可用资金,以及FrozenBalance属性,即挂单冻结的资金。如果需要使用其他数据计算,例如当前总权益、保证金等,这些数据保存在Info属性中,Info属性内保存的数据为CTP接口返回的原始数据。Info属性仅实盘有效,回测时无此属性。

下面我们一起来看一个简单的例子:

def main():
    while True:
        if exchange.IO("status"):
            exchange.SetContractType("rb888")
            account = exchange.GetAccount()
            Log("挂单前↑ ")
            Log("账户可用资金, Balance", account["Balance"])
            Log("账户挂单冻结资金, FrozenBalance:", account["FrozenBalance"])
            ticker = exchange.GetTicker()
            exchange.SetDirection("buy")
            exchange.Buy(ticker.Buy - 10, 1)
            account = exchange.GetAccount()
            Log("挂单后↑ ")
            Log("账户可用资金, Balance", account["Balance"])
            Log("账户挂单冻结资金, FrozenBalance:", account["FrozenBalance"])
            LogStatus(_D(), "已经连接 CTP ! ")
            break
        else:
            LogStatus(_D(), "未连接 CTP ! ")

例子中,设置当前操作的合约为rb888即螺纹钢主力合约,在下单前,获取一次账户资产信息,打印可用资金,打印挂单冻结资金。然后获取行情,设置交易方向为开多仓,根据行情当前的买一价格,下单一手多单螺纹钢合约。然后再次获取当前账户资产信息并打印。然后为了方便观察,使用break语句跳出循环,策略程序执行完毕。

image

4.5.2 exchange.GetPosition()

exchange.GetPosition()函数用于获取当前持仓信息,返回值为position结构体数组。position结构体数组包括:交易所接口应答的原始数据、杠杆大小、持仓量、仓位冻结、持仓均价、持仓浮动盈亏、持仓方向、合约代码、仓位占用保证金等等。 注意:返回的数组中包含当前交易所对象绑定的账户所有的持仓,并非当前设置的合约的持仓数据。

我们一起来看一个例子:

def main():
    ctList = ["rb888", "i888", "MA888", "pp888",]
    while True:
        if exchange.IO("status"):
            for i in range(len(ctList)):
                ret = exchange.SetContractType(ctList[i])
                t = exchange.GetTicker()
                exchange.SetDirection("sell")
                exchange.Sell(t.Buy - 10, 1, "合约:", ctList[i], "->", ret["InstrumentID"])
                orders = exchange.GetOrders()
                Log("orders length:", len(orders), "orders:", orders)
                pos = exchange.GetPosition()
                for i in range(len(pos)):
                    Log(pos[i])
                break
        else:
            LogStatus(_D(), "未连接 CTP ! ")

我们把要操作的合约代码写在一个数组(列表)中,然后通过一个for循环去进行每一个合约的下单操作。下单价格为当前买一价格减去10元,下开空仓的订单,由于价格比当前买一还低10元,所以马上就成交了。然后我们使用exchange.GetOrders()函数,获取当前所有挂单,并且打印。用于观察订单是不是还处于未成交状态,如果exchange.GetOrders()函数返回的是一个空数组即:[],说明订单都已经成交了。然后调用exchange.GetPosition()函数,获取当前所有持仓,并且遍历持仓数据数组,逐个打印持仓信息。

image

细心的同学可能发现,为何此处exchange.Sell函数传入了6个参数。这里我们讲解一下优宽量化交易平台可以输出日志的函数的特性,下单函数就是一个可以输出日志的函数,可以看到当前例子的运行截图中,有下单日志打印,所有可以产生日志的函数,都是可以在必要参数后增加一些附带参数,用于打印一些附带说明信息。例如本例中,下单日志中附带了「合约:pp888 -> pp2305」这样的信息。就是要说明当前合约代码设置的为pp888,实际映射到具体交易的合约为pp2305,在回测时间2023-04-12时,pp2305为主力合约。从例子代码中可以看到这个映射合约可以从exchange.SetContractType("pp888")函数返回时,返回的数据中获取到。注意:映射合约支持商品指数(pp000)映射到pp2305,也支持商品主力连续(pp888)映射到pp2305。

4.5.3 exchange.GetPositions()

为了更加贴合函数命名语义,增加了新的获取持仓函数:exchange.GetPositions()

exchange.GetPositions()函数有两种调用形式:

  • exchange.GetPositions() 当不传任何参数时,获取所有合约的持仓数据。
  • exchange.GetPositions("rb2410") 当指定具体合约代码时,获取具体合约的持仓数据。
def main():
    while not exchange.IO("status"):
        Sleep(1000)

    info = exchange.SetContractType("rb888")
    ticker = exchange.GetTicker()
    exchange.SetDirection("buy")
    exchange.Buy(ticker["Last"] + info["PriceTick"] * 20, 2)
    #不设置合约参数
    position = exchange.GetPositions()
    if len(position) > 0:
        Log("Amount:", position[0]["Amount"], "FrozenAmount:", position[0]["FrozenAmount"], "Price:", 
            position[0]["Price"], "Profit:", position[0]["Profit"], "Type:", position[0]["Type"], 
            "ContractType:", position[0]["ContractType"])
    #设置合约参数
    rbposition = exchange.GetPositions('rb888')
    Log(rbposition)

4.6 常用日志信息函数

日志可以记录量化交易中策略运行状态信息,同时还可以监控策略中指定的事件,还可以通过日志检查错误发生的原因。

4.6.1 Log(…)

Log()函数用于打印日志信息,参数可以传入多个,参数可以传入任意类型。支持使用十六进制颜色代码着色。支持消息推送。

def main():
    Log("优宽量化你好 !@")

Log()函数最后一个参数写"@"即可实现该条日志信息推送,可以推送到优宽量化APP、邮箱、监听WebHook的服务程序。

在编写策略时,Log()函数通常用于打印一些提示信息、输出数据。也可以用于策略程序调试、逻辑流程分析。Log支持打印base64编码后的图片。

def main():
    Log("`data:image/png;base64,AAAA`")

Log支持直接打印Python的matplotlib.pyplot对象,只要对象包含savefig方法就可以打印。

import matplotlib.pyplot as plt

def main():
    plt.plot([3, 6, 2, 4, 7, 1])
    Log(plt)

Log函数支持语言切换,Log函数输出文本,会根据平台页面上语言设置自动切换为对应的语言。

def main():
    Log("[trans]中文|abc[/trans]")

在完整的商品期货框架中使用Log()函数:

def main():
    ctList = ["rb888", "i888", "MA888", "pp888",]
    while True:
        if exchange.IO("status"):
            for i in range(len(ctList)):
                Log("遍历 ctList,当前合约为: ", ctList[i])
                break
        else:
            LogStatus(_D(), "未连接 CTP ! ")

4.6.2 LogProfit(Profit)

LogProfit函数用来在系统日志打印一条收益信息,并且会自动在收益曲线图表上打印一个收益点。该函数同样为可以打印日志的函数,可以在必要参数后传附带参数,用于显示一些打印收益时需要同时记录的信息,例如在打印收益时,同时输出当前账户资产信息,可以用于核对记录。因为打印的收益数据是需要在策略里面主动计算的,并非系统自动计算的。所以需要注意的是如果你写的收益算法不对,打印的收益信息也就是没有意义的错误信息,此时附带一些当时的资产数据,方便核算。LogProfit函数如果以字符&结尾,只绘制收益图表,不打印收益日志。

4.6.3 LogStatus(Msg)

LogStatus(Msg)函数是在设计、编写策略时很重要的一个函数,用来控制策略机器人页面状态栏的显示。参数Msg不保存到日志列表里,只更新当前机器人的状态信息,在日志上方显示,可多次调用,更新状态。LogStatus函数有非常多的功能,可以在状态栏上显示各种数据,显示表格,显示图片。比较常用的是显示当前时间,显示策略的相关数据信息。

例如,在状态栏显示表格,写入一些信息数据:

import json

def main():
    while True:
        if exchange.IO("status"):
            tab1 = {
                "type": "table",
                "title": "行情数据",
                "cols": ["项目", "数据"],
                "rows": []
            }
            tab2 = {
                "type": "table",
                "title": "账户数据",
                "cols": ["项目", "数据"],
                "rows": []
            }
            tab3 = {
                "type": "table",
                "title": "持仓数据",
                "cols": ["项目", "数据"],
                "rows": []
            }
            exchange.SetContractType("rb888")
            t = exchange.GetTicker()
            a = exchange.GetAccount()
            p = exchange.GetPosition()
            tab1["rows"].append(["tick 数据", json.dumps(t)])
            tab2["rows"].append(["账户数据", json.dumps(a)])
            tab3["rows"].append(["持仓数据", json.dumps(p)])
            LogStatus(_D(), "\n`" + json.dumps(tab1) + "`\n" +
                            "`" + json.dumps(tab2) + "`\n" +
                            "`" + json.dumps(tab3) + "`")
        else:
            LogStatus(_D(), "未连接")
        Sleep(1000)

回测运行结果为:

image

注意:编写设计策略时不用刻意追求UI显示方面要多么华丽,把需要显示的信息显示正确即可。更多的LogStatus函数用法、例子可以参考API文档。

4.6.4 Chart(…)

Chart自定义图表画图函数,专门用于绘制各种类型的交互式图表。它支持折线图、区域图、柱状图、饼图等多种类型的图表,并提供了丰富的工具和选项,可以满足各种需求。

我们来讲下Chart函数的使用方法,在使用Chart画图函数时,我们需要使用数据和选项的配置对象来控制图表的显示和交互效果。

选项配置对象

选项配置对象用于控制图表的显示效果、交互效果等。以下是选项配置对象的常见属性:

  • chart: 表示图表的整体配置选项,包括类型(如折线图、柱状图、饼图等)、背景色、边框等。
  • title: 表示图表的标题配置选项,包括文本、样式等。
  • subtitle: 表示图表的副标题配置选项,包括文本、样式等。
  • legend: 表示图例(Legend)的配置选项,包括位置、样式等。
  • tooltip: 表示提示框(Tooltip)的配置选项,包括触发方式、内容格式等。
  • plotOptions: 表示系列(Series)的配置选项,包括类型、颜色、标签等。

下面我们示范一下,首先我们可以定义选项配置对象:

chart = {
    # 标记是否为一般图表,有兴趣的可以改成false运行看看
    "__isStock": True,
    # 缩放工具
    "tooltip": {"xDateFormat": "%Y-%m-%d %H:%M:%S, %A"},
    # 标题
    "title": {"text": ""},
    # 坐标轴横轴即:x轴,当前设置的类型是:时间
    "xAxis": {"type": ""},
    # 坐标轴纵轴即:y轴,默认数值随数据大小调整
    "yAxis": {
        # 标题
        "title": {"text": ""},
        # 是否启用右边纵轴
        "opposite": False
    }
}

该选项配置对象包含了对图表的完整配置。其中__isStock属性表示是否为一般图表,选择 true 为 Highstocks , Highstocks 是一个专门用来创建交互式股票图表和金融图表库。它是 Highcharts 图表库的一部分,并提供了更多的功能,包括支持股票指标、数据区域缩小、鼠标拖拽和滚轮缩放等。 tooltip 属性定义了缩放工具的格式, title 属性定义了图表的标题, xAxis 属性定义了X轴的配置, yAxis 属性定义了Y轴的配置。

数据配置对象

数据配置对象用于控制图表的数据源。其中,最重要的属性是 series ,它是一个数组,每个元素表示一个系列(Series)的数据。数据配置对象:

series = [
    {"name": "", "id": "", "data": []},
]

该数据配置对象包含name、id、data属性。 name 属性用于图例显示的名称, id 属性为数据系列的唯一标识符, data 属性则为数据系列的数据。

双均线实例

我们将数据配置对象和选项配置对象的属性合并到一个名为 chart 的对象中。这个对象包含了所有的配置选项,从而实现了用一个对象控制整个图表的效果。这里我们举例一个双均线画图的配置。

chart = {
    # 标记是否为一般图表,有兴趣的可以改成false运行看看
    "__isStock": True,
    # 缩放工具
    "tooltip": {"xDateFormat": "%Y-%m-%d %H:%M:%S, %A"},
    # 标题
    "title": {"text": "均线"},
    # 坐标轴横轴即:x轴,当前设置的类型是:时间
    "xAxis": {"type": "datetime"},
    # 坐标轴纵轴即:y轴,默认数值随数据大小调整
    "yAxis": {
        # 标题
        "title": {"text": "均线"},
        # 是否启用右边纵轴
        "opposite": False
    },
    # 数据系列,该属性保存的是各个数据系列(线,K线图,标签等...)
    "series": [
        # 索引为0,data数组内存放的是该索引系列的数据
        {"name": "line1", "id": "线1,五日均线", "data": []},
        # 索引为1,设置了dashStyle:'shortdash'即:设置虚线
        {"name": "line2", "id": "线2,十日均线", "dashStyle": "shortdash", "data": []}
    ]
}

在 chart 对象设置完成以后,接着我们需要往里面添加数据。

import time

def main():
    # 调用 Chart 函数,初始化图表
    obj_chart = Chart(chart)
    # 清空
    obj_chart.reset()
    
    while True:
        exchange.SetContractTyp

更多内容

by2022 企业微信加不上啊啊啊啊啊啊

雨幕(youquant) 您好,企业微信满了,您加这个微信: https://www.youquant.com/upload/asset/1780ac4e8b9064c9d7d9a.png