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

Python商品期货量化入门教程

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

。有些同学们可能会好奇,这里我们不设置chart更新,和while循环吗?这里需要解释的是,我们这里进行的图像展示,是一个瞬时的差价快照,可以帮助我们迅速的判断当前的差价状态,所以没有设置chart更新和while循环。

chart = {
    "__isStock": True,
    "title": {"text": '差价分析图'},
    "xAxis": {"type": 'datetime'},
    "yAxis": {
        "title": {"text": '差价'},
        "opposite": False,
    },
    "series": [
        {"name": "diff", "data": []},
        {"name": "meandiff", "data": []},
    ]
}

def main():
    exchange.SetContractType(Contract_A)
    recordsA = exchange.GetRecords(1)
    exchange.SetContractType(Contract_B)
    recordsB = exchange.GetRecords(1)

    rlength = min(len(recordsA), len(recordsB))
    difflist = []

    for i in range(rlength):
        diff = recordsA[len(recordsA) - rlength + i] - recordsB[len(recordsB) - rlength + i]
        chart["series"][0]["data"].append([recordsA[recordsA.length - rlength + i].Time, diff])
        difflist.append(diff)
        meandiff = sum(difflist[-20:]) / min(len(difflist), 20) if len(difflist) >= 20 else sum(difflist) / len(difflist)
        chart["series"][1]["data"].append([recordsA[recordsA.length - rlength + i].Time, meandiff])

    return chart

5.2.2 一键对冲开仓插件

这样我们的第一个插件就设置好了,接下来我们来看第二个插件的设置:一键对冲开仓插件。同样的,首先设置策略的参数,在策略参数里,我们设置好交易合约A和B,开仓数量,滑价和正套还是反套的交易选项。这里给大家稍微解释下,当差价大的时候,我们预测价差会回归,从而进行反套的交易,reverse设置为true,进行空A多B的操作;而当差价小的时候,预测价差会变大,进行正套的交易,reverse设置为false,进行多A空B的操作。

所以在主函数里,这里我们首先设置开多的交易操作,使用reverse判断进行正套还是反套的交易,使用if表达式设置获取相应合约,然后获取ticker数据,这里还进行了容错的处理,如果没有获取到ticker数据,直接报错;SetDirection是buy,因为我们想快速的成交,所以设置的价格为当前的卖价加上滑价,这样一个较高的价格可以提升多仓快速成交的成功率;于此相反的,对于空头的操作,和多头的操作刚好相反就可以,设置SetDirection是sell,价格设置是当前较低的买价再减去滑价。这样就完成了开仓的操作。

开仓完成以后,我们想在插件中呈现两个开仓的品种,方向,价格和差价,我们这里设置一个展示的tbl表格,包含需要呈现的开仓信息。接着,在下面的代码中,设置开仓完成以后,休息1s,接下来我们获取仓位的数据。如果判断有仓位,就是开仓成功,使用轮询,然后使用if找到我们需要的品种,获取对应的品种名称,开仓方向和价格,然后计算开仓是的差价,push到tbl中,最后使用return进行呈现。在插件中,我们不想呈现多余的信息,所以SetErrorFilter设置一下,过滤多余的错误信息。这样一键对冲开仓插件就设置好了。

import time

def main():
    tbl = {
        'type': 'table',
        'title': '开仓信息 ' + str(time.strftime("%Y-%m-%d %H:%M:%S")),
        'cols': ['合约A', '合约A价格', '合约A方向', '合约B', '合约B价格', '合约B方向', '价差'],
        'rows': []
    }

    contract_type = Contract_B if Reverse else Contract_A

    SetErrorFilter("login|ready|流控|连接失败|初始|Timeout|CancelOrder")
    exchange.SetContractType(contract_type)
    ticker_A = exchange.GetTicker()
    if not ticker_A:
        return '无法获取数据'
    
    exchange.SetDirection('buy')
    exchange.Buy(ticker_A['Sell'] + Slip, Amount)

    exchange.SetContractType(Contract_A if Reverse else Contract_B)
    ticker_B = exchange.GetTicker()
    if not ticker_B:
        return '无法获取数据'

    exchange.SetDirection('sell')
    exchange.Sell(ticker_B['Buy'] - Slip, Amount)

    time.sleep(1)

    pos = exchange.GetPosition()
    if pos:
        for position in pos:
            if position['ContractType'] == Contract_A:
                contractA = position['ContractType']
                typeA = '多头' if position['Type'] == 0 else '空头'
                priceA = position['Price']
            if position['ContractType'] == Contract_B:
                contractB = position['ContractType']
                typeB = '多头' if position['Type'] == 0 else '空头'
                priceB = position['Price']
        
        diff = priceA - priceB
        tbl['rows'].append([contractA, priceA, typeA, contractB, priceB, typeB, diff])
        return tbl

5.2.3 一键对冲平仓插件

接下来是最后一个插件的设置一键平仓。首先我们获取持仓的信息,如果没有持仓,直接返回“当前无持仓”信息提醒;在判断有持仓情况下,进行相应的平仓的操作。

def main():
    SetErrorFilter("login|ready|流控|连接失败|初始|Timeout|CancelOrder") 

    pos_start = _C(exchange.GetPosition)
    if not pos_start or len(pos_start) == 0:
        return '已无持仓'
    
    for pos in pos_start:
        if pos["Type"] == 0:
            direction = 'closebuy_today'
        elif pos["Type"] == 1:
            direction = 'closesell_today'
        elif pos["Type"] == 2:
            direction = 'closebuy'
        elif pos["Type"] == 3:
            direction = 'closesell'
        else:
            return '未知持仓类型'
        
        exchange.SetContractType(pos["ContractType"])
        ticker = exchange.GetTicker()
        if not ticker:
            return '无法获取ticker'
        
        if direction == 'closesell' or direction == 'closesell_today':
            exchange.SetDirection(direction)
            exchange.Buy(ticker["Sell"] + 2, pos["Amount"])
        elif direction == 'closebuy' or direction == 'closebuy_today' :
            exchange.SetDirection(direction)
            exchange.Sell(ticker["Buy"] - 2, pos["Amount"])
        else:
            return '无效的操作方向'
        
    Sleep(2000)
    pos_end = exchange.GetPosition()
    if not pos_end or len(pos_end) == 0:
        return '平仓完成'

这样我们三个小插件就编写完成了,当然这个插件还是比较简陋的,容错性还需要提高,大家在使用的过程中,如果遇到错误,可以留言,我们可以及时改正。不过对于基本的使用还是没有问题的,在交易终端里,我们使用N视界仿真账户,设置好目标合约,首先查看差价,等到入场时机设置reverse对应开仓,到达心理盈利点位或者止损点位,进行平仓,使用起来还是比较丝滑的,比手动进行开仓确实方便多了。看了这个插件的使用,我们应该也有了自己的想法,大家可以根据自己的交易习惯,不妨写成插件方便自己的手动交易。大家如果有好的想法和需求,也可以提出来,我们尽力也会为你实现!

5.3 数据探索模块

优宽量化团队对数据探索模块进行了全面升级。新版可以帮助大家快速获取各种类型的重要数据,更重要的是,它提供了强大的分析工具,使大家能够深入研究基本面数据与不同品种走势之间的关系。这样,大家不仅能够获得所需数据,还能更有效地分析和利用这些数据,从而做出更明智的投资决策。

image

数据探索模块是优宽量化自研开发的DATADATA平台应用模块。作为优宽量化平台精心打造的商品期货研究利器,DATADATA不仅全面汇总了多元化的期货宏观数据和深入的基本面信息,还实时更新期货,期权以及股票行情动态,确保用户第一时间掌握市场动态。通过这一平台,投资者和研究人员可以更加便捷地搭建起针对商品期货品种的系统性研究框架,从而在复杂多变的期货市场中做出更加明智和精准的决策。无论是对于初入市场的新手,还是对于资深交易者,DATADATA都将成为不可或缺的研究和分析工具。在后续的开发工作中,我们也会在策略编写当中提供基本面获取的API接口。这样呢,我们就可以在探索基本面信息和品种走势关系的基础上,在自己的策略当中,应用这些关系构建一个更加全面的交易系统。

image

5.2.1 数据条目

我们来看下怎样进行使用数据探索模块进行基本面数据的获取和分析操作。首先打开优宽量化网页版,进入控制中心,进入数据探索模块。可以看到这里进行了全新的改版,点击数据搜索框,会出现宏观数据,期货期权,Tick数据。并且如果大家有想要分析的数据条目也可以自行上传。

image

宏观数据

宏观数据栏包括期货数据和行情数据两大类。我们打开看一下,一共包含数十个条目,整体汇总了包含宏观数据,比如央行汇率,GDP,CPI,PMI等,还有期货的基本面数据,包括期货合约对应期货代码,期货标准仓单,基差表等。行情数据中包含了股票每日行情,期货每日行情和黄金现货日行情的数据。

期货期权

期货期权数据包括六大交易所包含的期货品种信息,行情信息以及相应的期权行情信息。通过对期货合约的行情数据分析,我们可以更好地把握市场趋势、价格波动情况和交易机会。同时,期权行情信息的分析可以帮助大家评估期权的价格水平、波动性预期以及期权合约的风险收益特征。

Tick数据

优宽量化平台悉心收集了各个品种详尽的Tick数据,作为量化研究人员和交易员们获取洞察的必需工具。Tick数据作为盘口交易数据,具有高频、实时的特点,对于深度分析和实时监控市场至关重要。通过对Tick数据的深度分析,交易者们可以揭示市场的隐藏规律和特点,包括价格走势、交易量分布、成交价与成交量之间的关系等。这种深度分析有助于交易者更准确地判断市场趋势的强弱和转折点,及时调整交易策略并做出相应的交易决策。

大家可以这里的搜索框寻找自己想要的数据,如果找不到自己想要的数据,大家可以留言,我们呢工程师会后续的不断完善数据库,更好的帮助大家方便快捷的获取到更多的数据。

image

5.2.2 操作指南

数据查询

在数据探索模块,数据是采用SQL语句进行查询的。通过灵活的SQL查询,我们可以自由组合各种查询条件,以满足不同分析的查询需求。我们举例示范一下,比如我们选择基差表,可以看到该条目下具体包含的列,有ID,时间戳,商品,基差等一系列数据,方便大家从不同维度研究商品品种的基差状况。点击这里的图表,这是数据预览模块,可以看到保存数据的具体样式,可以看到这里的数据是使用SQL查询的,只返回了10条数据。并且这里包含了sql的示范语句,我们可以使用更多的SQL数据进行数据的查询。

接下来我们来进行自定义数据的获取,我们来看右边上方的小方框,这里呢用来运行SQL语句进行数据查询。比如我们运行一下刚才示例的SQL语句,可以看到返回了10条的数据。如果我们想获得固定日期,或者固定品种的品种信息,只需要改变SQL语句就好。例如这里想查询沪铜的基差信息,输入语句:

select * from futures_data.basis where product_name = '沪铜' ORDER BY date

可以看到立即进行返回了沪铜品种从2011至最新的基差数据。这里需要注意的是,需要把SQL的语句写正确,否则会返回SQL语句解析错误。文字的图表大家可能觉得不太直观,于是贴心的优宽量化也提供了可视化的选项。

可视化分析

通过点击右下角的可视化,这里有一系列的图表进行选择,包括观察面积占比的饼图,矩形树图;时间流变化的折线图,面积图和散点图等,还有一些高端的旭日图,热力图和桑葚图等,针对于不同类型的数据,我们可以选择不同的图表进行更加丰富可视化的呈现。

image

这里我们举一个简单的范例,使用折线图对玻璃品种近200日的基差表进行分析,首先写好SQL语句。

SELECT *
FROM futures_data.basis
WHERE product_name = '玻璃'
ORDER BY date DESC
LIMIT 200;

我们点击折线图图标,进入绘图选项的页面。首先选择数据模块,第一个维度选择的是X轴(时间),第二个选择系列,也就是Y轴,这里可以选择不同的列,定义折线的颜色;点击三点符号这里选择折线样式,比如左侧还是右侧,这里当使用两个数据时候,可以定义两个Y轴;还有折线的样式等;第二栏定义轴线,包括具体轴线的设置;第三栏定义具体的显示设置,包括堆叠,图例,还有数据缩放的设置。

image

如果这里我们想画现货价格,期货价格,以及具体的基差,可以这样操作。首先选择现货价格,再添加主力合约价格。最后我们系列里面设置,右侧y轴可以加上差价图,点击添加,然后将y轴定义为右侧。这样我们可以看到最近200日的玻璃期货,现货,和差价的走势图。由此呢,我们可以判断玻璃的升贴水情况,为我们的交易做一些参考。

image

另外呢,在SQL语句中也可以添加外部的参数,这样我们就可以探索不同品种的基差走势图。

image

实盘回放

优宽量化平台为了满足用户的需求,结合流数据分布式表同步回放功能,开发出了盘口实时回放功能。这项功能可以帮助交易者在真实市场环境下模拟交易,并实时观察市场的动态变化。通过不断地实践和观察,交易者可以逐步培养自己的交易节奏,提高对市场的敏感度和把握能力。

首先我们需要进入优宽量化平台官网,点击进入数据探索模块,依次通过选择Tick数据,编写Sql代码选择目标期货品种,然后点击可视化,选择实盘回放功能,这样就可以进入仿真实盘回放模块。不需要任何额外工具的导入,我们可以使用优宽量化平台自带的数据源进行实盘回放的功能,既方便又快捷。

image

我们来观察下实盘回放面板,可以看到它包含三个功能模块,上面是价格分时图,可以观察目标品种的价格实时走势;左下方是逐笔交易历史,由于期货交易所特殊的数据返回机制,并不会返回每笔的交易记录,所以这是根据Tick数据反推出来的交易历史;右下方是盘口的实时数据,动态呈现买一价和卖一价,以及相应的申请数量。

image

最后我们要介绍一下,现在社区板块也支持数据探索模块进行数据引用和图表呈现。点击创建主题,进入编辑页面,选择这里的折线图按钮,就可以进入数据探索的模块。当我们书写好SQL语句获取数据,然后进行可视化设置好图表,点击保存。这样,大家实时探索出来的研究结果就可以在自己的文章中进行呈现。

image

在优宽量化平台上,丰富的数据资源为我们提供了广阔的分析空间。通过利用数据探索模块,我们能够运用SQL语句进行灵活查询,深入挖掘基本面信息与期货品种走势之间的潜在联系。这种深入的数据分析使我们能够更全面地理解市场动态,进而在构建策略模型时确保其更加合理和全面。这样,我们不仅能够更好地把握市场趋势,还能在制定投资策略时更加精准和有效。

5.4 自定义数据源

依托于优宽量化交易平台回测系统的自定义数据源功能,我们可以直接把收集到的外部数据作为回测系统的数据源,这样可以让回测系统应用于任何我们想回测历史数据的市场了,包括外汇,商品现货,股票等等。

5.4.1 数据格式

系统用GET方法请求自定义的URL(可公开可访问的网址)来获取外部数据源进行回测,附加的请求参数如下:

参数 意义 说明
symbol 品种名 如: futures.MA888
eid 交易所 如: Futures_CTP
round 价格精度 如3 那么返回的数据里的价格都要乘于1000取整
vround 数量精度 如2 那么返回的数据里的数量都要乘于100取整
period bar周期(毫秒) 比如60000为请求分钟bar
depth 深度档数 1-20
trades 是否需要分笔数据 true/false
from 开始时间 unix时间戳
to 结束时间 unix时间
custom
detail
ttl

round与vround是为了避免网络传输过程中浮点数的精度丢失设计的两个参数,价格数据和成交量、订单量数据都采用整型传输。

一个拼接后的数据的例子:

http://customserver:80/data?symbol=futures.MA888&eid=Futures_CTP&round=3&vround=3&period=900000&from=1564315200&to=1567267200

数据返回的格式必须为以下两种格式其中之一(系统自动识别):

模拟级 Tick

普通的Bar级别回测,以下是数据范例:

{
    "schema":["time", "open", "high", "low", "close", "vol"],
    "data":[
        [1564315200000, 9531300, 9531300, 9497060, 9497060, 787],
        [1564316100000, 9495160, 9495160, 9474260, 9489460, 338]
    ]
}

实盘级 Tick

Tick级回测的数据(包含盘口深度信息,深度格式为[价格, 量]的数组。可有多级深度,asks为价格升序,bids为价格倒序),以下是数据范例:

{
    "schema":["time", "asks", "bids", "trades", "close", "vol"],
    "data":[
        [1564315200000, [[9531300, 10]], [[9531300, 10]], [[1564315200000, 0, 9531300, 10]], 9497060, 787],
        [1564316100000, [[9531300, 10]], [[9531300, 10]], [[1564316100000, 0, 9531300, 10]], 9497060, 787]
    ]
}

特殊的列属性asks、bids、trades:

字段 说明 备注
asks/bids [[价格, 数量], …] 例如实盘级 Tick数据范例中的数据:[[9531300, 10]]
trades [[时间, 方向(0:买,1:卖), 价格, 数量], …] 例如实盘级 Tick数据范例中的数据:[[1564315200000, 0, 9531300, 10]]

5.4.2 自定义数据源范例

这里我们通过结合Mysql数据库,将数据库保存的数据进行导出定义为自定义数据源,导入至回测系统进行使用,下面我们来看具体怎样实现。

import pymysql
import _thread
import json
import math
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import parse_qs, urlparse

def url2Dict(url):
    query = urlparse(url).query  
    params = parse_qs(query)  
    result = {key: params[key][0] for key in params}  
    return result

class Provider(BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            self.send_response(200)
            self.send_header("Content-type", "application/json")
            self.end_headers()

            dictParam = url2Dict(self.path)

            Log("自定义数据源服务接收到请求,self.path:", self.path, "query 参数:", dictParam)
                                      
            databaseName = dictParam["database"]
            tableName = dictParam["table"]
             
            # 连接数据库
            Log("连接数据库服务,获取数据,数据库:", databaseName, "表:", tableName)
            # 创建 MySQL 连接对象
            cnx = pymysql.connect(
                host=????,  # 数据库主机地址
                user=????,  # 数据库用户名
                password=????,
                database=databaseName
            )

            # 创建 MySQL 游标对象
            cursor = cnx.cursor()

            # 获取到表数据
            cursor.execute("SELECT * FROM " + tableName)

            # 获取查询结果
            results = cursor.fetchall()
            
            # 要求应答的数据
            data = {
                "schema" : ["time", "open", "high", "low", "close", "vol"],
                "data" : [],
                "detail": {'PriceTick': 1, 'VolumeMultiple': 10, 'ExchangeID': 'CZCE', 'LongMarginRatio': 0.12, 'ShortMarginRatio': 0.12, "InstrumentID": 'SA888'}
            }

            for item in results:
                # 将数值部分添加到data.data中的列表
                data["data"].append([item[1], item[2], item[3], item[4], item[5], item[6]])

            Log("数据:", data, "响应回测系统请求。")
            # 写入数据应答
            self.wfile.write(json.dumps(data).encode())

            cursor.close()
            cnx.close()
        except BaseException as e:
            Log("Provider do_GET error, e:", e)


def createServer(host):
    try:
        server = HTTPServer(host, Provider)
        Log("Starting server, listen at: %s:%s" % host)
        server.serve_forever()
    except BaseException as e:
        Log("createServer error, e:", e)
        raise Exception("stop")

# 创建 MySQL 连接对象
cnx = pymysql.connect(
    host='????',  # 数据库主机地址
    user='????',  # 数据库用户名
    password='????' # 数据库密码
)

# 创建 MySQL 游标对象
cursor = cnx.cursor()

# 创建一个新的databases
cursor.execute("CREATE DATABASE IF NOT EXISTS my_database")

# 在新的databases中创建table来保存期货的k线数据,K线数据格式{'Time': 1673884800000, 'Open': 2127.0, 'High': 2148.0, 'Low': 2127.0, 'Close': 2148.0, 'Volume': 2150.0, 'OpenInterest': 3742.0},分割成列进行保存
cursor.execute("USE my_database")
cursor.execute("CREATE TABLE IF NOT EXISTS future_kline ( \
                id INT AUTO_INCREMENT PRIMARY KEY, \
                timestamp BIGINT NOT NULL, \
                open FLOAT NOT NULL, \
                high FLOAT NOT NULL, \
                low FLOAT NOT NULL, \
                close FLOAT NOT NULL, \
                volume FLOAT NOT NULL, \
                open_interest FLOAT NOT NULL)")

# 在新的databases中创建table来保存期权的k线数据,K线数据格式{'Time': 1673884800000, 'Open': 2127.0, 'High': 2148.0, 'Low': 2127.0, 'Close': 2148.0, 'Volume': 2150.0, 'OpenInterest': 3742.0},分割成列进行保存
cursor.execute("USE my_database")
cursor.execute("CREATE TABLE IF NOT EXISTS option_kline ( \
                id INT AUTO_INCREMENT PRIMARY KEY, \
                timestamp BIGINT NOT NULL, \
                open FLOAT NOT NULL, \
                high FLOAT NOT NULL, \
                low FLOAT NOT NULL, \
                close FLOAT NOT NULL, \
                volume FLOAT NOT NULL, \
                open_interest FLOAT NOT NULL)")

def main():
    preBarTime = 0
    Log('测试')

    try:
        # _thread.start_new_thread(createServer, (("localhost", 9090), ))     # 本机测试
        _thread.start_new_thread(createServer, (("0.0.0.0", 9090), ))         # VPS服务器上测试
        Log("开启自定义数据源服务线程", "#FF0000")
    except BaseException as e:
        Log("启动自定义数据源服务失败!")
        Log("错误信息:", e)
        raise Exception("stop")

    while True:                            # 执行策略逻辑循环
        if exchange.IO("status"):          # 检测是否与期货公司服务器连接,登录成功
            LogStatus(_D(), "已经连接")      
            exchange.SetContractType("SA401")    # 设置要操作的合约,这里设置为SA401,也可以做成参数,由策略参数上进行设置
            futureR = exchange.GetRecords(PERIOD_M5)
            exchange.SetContractType("SA401P1500")    # 设置要操作的合约,这里设置为SA401P1500,也可以做成参数,由策略参数上进行设置
            optionR = exchange.GetRecords(PERIOD_M5)

            Log(_D(futureR[-1].Time/1000), '期货时间')
            Log(_D(optionR[-1].Time/1000), '期权时间')

            if len(futureR) < 2 or len(optionR) < 2:
                continue
            
            newfutureR = futureR[-2]
            newoptionR = optionR[-2]

            if newfutureR.Time == newoptionR.Time and preBarTime != newfutureR.Time and preBarTime != newoptionR.Time:
                Log('更新时间到')
                Log(_D(newfutureR.Time/1000), '期货')
                Log(_D(newoptionR.Time/1000), '期权')
                preBarTime = newfutureR.Time

                # 如果获取到futureR 和 optionR,添加到新创建的两个表中
                cursor.execute("USE my_database")
                cursor.execute("INSERT INTO future_kline (timestamp, open, high, low, close, volume, open_interest) VALUES (%s, %s, %s, %s, %s, %s, %s)", \
                                (newfutureR.Time, newfutureR.Open, newfutureR.High, newfutureR.Low, newfutureR.Close, newfutureR.Volume, newfutureR.OpenInterest))
                cnx.commit()
                
                cursor.execute("USE my_database")
                cursor.execute("INSERT INTO option_kline (timestamp, open, high, low, close, volume, open_interest) VALUES (%s, %s, %s, %s, %s, %s, %s)", \
                                (newoptionR.Time, newoptionR.Open, newoptionR.High, newoptionR.Low, newoptionR.Close, newoptionR.Volume, newoptionR.OpenInterest))
                cnx.commit()
          
        else:
            LogStatus(_D(), "未连接")      # 如果未连接上期货公司服务器,在机器人状态栏上显示时间,和未连接信息
        Sleep(1000*10)

程序开头导入了一些需要使用的模块,包括 pymysql、_thread、json、math、http.server 和 urllib.parse。这里面有几个包,给大家稍微解释一下作用。_thread:这是 Python 的内置模块,用于支持多线程编程。它提供了一些函数,如 start_new_thread(),用于创建新的线程并执行指定的函数。http.server:内置模块,用于创建基于 HTTP 协议的服务器。它提供了一些类,用于处理 HTTP 请求和启动 HTTP 服务器。urllib.parse:这是 Python 的内置模块,用于解析 URL。它提供了一些函数,如 urlparse()parse_qs(),用于解析 URL 字符串,并提取其中的各个部分,如协议、主机、路径、查询参数等。

下面我们定义第一个函数。url2Dict这个函数接收一个 URL 字符串作为参数,并返回一个包含查询参数的字典。具体来说,它使用 urlparse 函数解析 URL,然后使用 parse_qs 函数将查询参数解析为字典。最后,它返回一个包含查询参数的字典。当我们向数据库发送请求url的时候,这个函数帮助我们进行解析。我们来看下url具体包含的参数包括品种名,交易所,价格精度,数量精度,bar周期(毫秒),深度档数,是否需要分笔数据,开始时间和结束时间。当然我们也可以根据数据库类型的区别,定义别的参数,例如在本例中,我们定义了数据库名称和具体的表名称,方便我们在数据库中获取到指定的数据。

接着我们定义一个名为 Provider 的类,它继承自 BaseHTTPRequestHandler 类,可以方便地创建自定义的 HTTP 服务器,而不需要从头开始编写处理 HTTP 请求的逻辑。在 do_GET 方法中,首先发送一个 HTTP 响应头,指定响应状态码为 200,200是HTTP 协议中一个常见的状态码,表示请求成功。然后设置响应内容的 MIME 类型为 application/json。然后解析 URL 中的查询参数,并将其转换为字典形式。然后,打印日志信息,包括 self.path 和 dictParam 的值,用于调试和记录请求的路径和查询参数。之后连接数据库,获取对于参数需求的数据。首先MySQL进行 数据库的连接,这里的参数包括主机地址,用户名,密码,和数据的名称,这里将数据库的名称定义为需求的参数名称databaseName,需求的数据库连接好以后,接下来执行一条 SELECT 查询语句,这里的参数名是我们的第二个参数,tableName,这样我们就可以从指定的数据库和表中获取我们需求的数据。

但是获取到的数据是我们需求的格式吗,我们需要进行一下处理。我们来看下回测系统需求的数据格式,具体需求的字段包括三个,schema是指定data数组里列的属性,data是具体数据的数值,还有detail,是商品期货的品种需要提供的属性,具体包括PriceTick,VolumeMultiple,ExchangeID,LongMarginRatio,ShortMarginRatio和InstrumentID。所以在代码里我们首先创建好需求的格式,其中 schema 键对应一个列表,表示数据的字段名,这是规定好的,包含时间戳,高开低收,和交易量数据;data 键对应一个列表,表示数据的具体内容;第三个detail键对应期货的属性信息,这里我们定义为纯碱的期货信息,PriceTick是1,合约乘数是10,交易所是郑商所,做多和做空保证金比率是12%,还有合约名称。接着我们进行data数据的填写。这样我们返回数据的格式和具体内容就填写完成了。

最后,将构造好的数据以 JSON 格式写入 HTTP 响应体,并发送给客户端。查询完成以后,进行游标和数据库连接的关闭。如果在整个过程中发生了异常,将捕获异常并打印错误日志。总之,这段代码实现了一个简单的自定义数据源服务,通过接收 HTTP GET 请求,连接到 MySQL 数据库,并将查询结果以 JSON 格式返回给请求方。

下面是createServer(host) 函数:这个函数接收一个主机地址作为参数,并创建一个 HTTP 服务器。它使用 HTTPServer 类创建服务器对象,并指定处理请求的类为 Provider。然后,它调用 serve_forever 方法启动服务器。这样我们的Http服务器设置已经完成了。下面我们需要在主函数中启动一个线程来创建 HTTP 服务器。这里使用 _thread.start_new_thread() 方法来创建一个新的线程,用于运行 createServer() 函数。createServer() 是我们定义的函数,用于启动 HTTP 服务器并监听指定的地址和端口。

在这里,通过传入一个包含地址和端口的元组参数 ((“0.0.0.0”, 9090), ),来指定 HTTP 服务器监听的地址和端口。其中,0.0.0.0 表示监听所有的网络接口,9090 是指定的端口号。在启动线程之前,使用 try…except 语句来捕获可能发生的异常。如果启动线程失败,则会打印错误信息,并抛出一个 Exception 异常,以终止程序的执行。最后,如果线程成功启动,则会打印一条日志信息,表示自定义数据源服务线程已经开启。使用这样多线程的方式来启动 HTTP 服务器,可以让程序在后台持续运行,并且不会阻塞主线程的执行。这样就可以实现同时处理多个客户端请求的功能。以上这些工作呢,就可以外部数据返回到需求的端口中。其实呢,这里http框架如果大家不太熟悉的话,可以了解这里的url参数设置和数据库参数的设置,其余的框架大家照搬就好。

实盘建立好以后,下面我们打开一个测试策略,进行回测。策略代码很简单,首先设置合约,打印合约的信息,然后GetRecords打印k线数据。在回测界面,更多参数这里设置指定数据源,这里的url是这样编写的,包含我们需要的两个参数,database和table。因为我们获取的k线周期是5分钟,所以这里k线的周期和底层k线的周期都设置为5分钟。还有回测时间的设置,需要是我们收集到的数据中包含的一段时间,否则会返回错误。这里的精度设置为0,否则会返回小数。

我们来验证一下,点击开始回测。可以看到回测系统利用获取到的k线进行画图的展示,对比默认的数据源可以发现是一致的。在实盘日志里,可以看到具体的数据提供流程。另外,最重要的就是使用自定义的数据源进行策略回测的功能,这里我们定义买入一手多仓,可以看到回测系统自动为我们进行了收益的统计和指标的展示。这样就可以让服务器上的实盘自己收集K线数据,而我们可以随时获取这些收集的数据直接在回测系统回测了。当然我们的课程属于抛砖引玉,各位大神还可以继续扩展,例如支持多品种、多市场数据收集等等功能,可以真正的搭建一个属于自己的量化数据库。

5.5 本地回测引擎

优宽量化平台为满足用户离线进行量化分析的需要,开发本地回测引擎,使用户能够在本地环境中进行回测,提高回测效率和灵活性,确保数据安全和隐私保护。本地回测引擎的功能和网页的回测系统基本上是一致的,除了不能满足实盘交易的功能,我们可以做一些有意思的工作,比如全品种历史数据的下载,可视化分析和策略的回测。本节课我们将学习从0开始搭建属于你自己的量化回测系统。

我们打开API文档,这里介绍了优宽量化交易平台开源了 JavaScript 语言和 Python 语言的本地回测引擎。我们点击 Python 语言的,看一下对应模块的介绍。打开页面,可以看到这里有详细的安装步骤和简单的例子。我们复制这行代码到终端进行回测引擎的安装。安装成功以后,我们复制这个实例到 Python 的编辑器里面。我们首先看下这个代码,在代码开头是回测参数的配置,这里面设置了策略起始和结束的时间,k线周期,还有交易所的设置,这里就相当于在网页端保存回测设置的结果,然后我们导入优宽量化回测引擎模块,接下来初始化回测引擎。这样我们就可以编写我们的策略了,第一步使用GetAccount打印了账户信息,第二步打印了ticker的信息,最后打印回测结果,并画图展示回测的结果。我们运行一下,看下返回结果。

'''backtest
start: 2018-02-19 00:00:00
end: 2018-03-22 12:00:00
period: 15m
exchanges: [{"eid":"Bitfinex","currency":"BTC_USD","balance":10000,"stocks":0}]
'''
from fmz import *
task = VCtx(__doc__) # initialize backtest engine from __doc__
print(exchange.GetAccount())
print(exchange.GetTicker())
print(task.Join(True)) # print backtest result
task.Show() # or show backtest chart

可以看到,和网页的函数功能是一致的,首先返回了包括余额等的账户的信息,接下来返回的是ticker数据,包括时间戳,高开低收,买价和卖价,交易量和持仓量,接下来backtest result返回的是,策略运行对应的,每日收盘价,账户余额,持有仓位,手续费和净值。最后是回测图表的展示,包括定义的回测时间段内收益的变化等,用来动态展示策略的运行效果。

当然这里的展示还不够全面,我们将使用两个例子来展示优宽量化本地回测引擎的强大。第一个例子,我们用来获取某一期货品种的历史数据。数据的获取,是编写量化策略开始的第一步,相对于其他历史数据的获取方式,有的需要付费的账户,有的需要繁杂的api设置,还有的需要下载数据到本地,然后上传才能进行数据分析,使用优宽量化本地回测引擎。这些烦恼都可以解决。只需要开启回测引擎,就可以获取到和网页端一样的历史数据。

5.5.1 获取数据信息

我们来看下数据获取的具体方法。首先我们进行回测时间的设置,为了省去手工设置,填写参数的烦恼,我们可以到优宽量化的回测页面,选择2016年,然后到最近的时间,2023年11月1号,选择商品期货,点击添加。然后到策略编辑页面,点击保持回测设置,这样我们回测的参数就设置好了。我们复制到 Python 当中。

'''backtest
start: 2020-01-01 09:00:00
end: 2023-11-02 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
'''

task = VCtx(__doc__) # 初始化引擎

rlist = []
rlisttime = []
prebartime = 0

while True:
    try:
        exchange.SetContractType('FG888')
        r = exchange.GetRecords()
        if r[-1].Time != prebartime:
            for i in range(len(r) - 1):
                if r[i].Time not in rlisttime:
                    rlist.append(r[i])
                    rlisttime.append(r[i].Time)
            prebartime = r[-1].Time
        else:
            continue
    except:
        print('数据读取完成')
        break

同样的,我们首先启动回测引擎。接下来我们获取k线数据,和网页上的回测系统一样,在策略运行的期间,使用GetRecords会持续不断的获取k线数据,我们想获得的k线数据是不包括重复时间戳的,所以设置三个变量,第一个 rlist 用来保持k线数据,第二个 rlisttime 用来保存k线的时间戳,第三个是 prebartime ,是前一根k线bar的时间戳,只有最新k线更新,我们进行k线的保存。

这里呢,我们不需要定义main函数了,使用while True循环,首先设置合约,当然需要设置一个在2016年已经上市的合约,这里我们设置玻璃的主力合约,然后使用GetRecords获取k线数据。接着判断如果k线的最新一根时间戳不等于prebartime的时候,我们使用for循环,到倒数第二根k线的位置,是已经完成的k线,接着判断如果rlisttime不包含该根k线时间戳的时候,使用append进行添加,同样的rlisttime也添加相应的时间戳,这里还需要重新赋值prebartime为最新的时间戳。这里我们使用try和except的框架包住我们的策略,在try中运行策略,如果运行完毕,在except中打印数据读取完成,直接break。这样,我们需求的数据就获取完成,我们看下获取到的数据,是每日的k线数据,包括高开低收,成交量和持仓量。

5.5.2 回测量化策略

接下来,我们来讲解第二个例子,使用本地的回测引擎,测试在本地运行量化策略。我们在策略广场中找到这个经典的MACD策略。复制到这里。这里我们需要做一些小小的改动。首先重新设置回测引擎。然后在while true里面,加上try和except的结构。最后我们运行这个main函数。稍微等待一下,等到返回策略运行完成,代表回测结束。

'''backtest
start: 2019-01-01 00:00:00
end: 2021-01-01 00:00:00
period: 1d
basePeriod: 1d
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
'''
task = VCtx(__doc__) # 初始化引擎
mp = 0  # 定义一个全局变量,用于控制虚拟持仓
    
# 程序主函数
def onTick():
    _C(exchange.SetContractType, "rb000")   # 订阅期货品种
    bar = _C(exchange.GetRecords)   # 获取K线数组
    if len(bar) < 100:      # 如果K线数组长度太小,就直接返回跳过
        return
    macd = TA.MACD(bar, 5, 50, 15)          # 计算MACD值
    dif = macd[0][-2]                   # 获取DIF的值
    dea = macd[1][-2]                   # 获取DEA的值
    depth = exchange.GetDepth()
    ask = depth['Asks'][0]['Price']
    bid = depth['Bids'][0]['Price']
    global mp                           # 全局变量,用于控制虚拟持仓
    if mp == 1 and dif < dea:
        Log('多平信号成立,挂单价格为:', bid)
        exchange.SetDirection("closebuy")   # 设置交易方向和类型
        id = exchange.Sell(bid, 1)  # 平多单
        mp = 0                                  # 设置虚拟持仓的值,即空仓
    if mp == -1 and dif > dea:
        Log('空平信号成立,挂单价格为:', ask)
        exchange.SetDirection("closesell")      # 设置交易方向和类型
        id = exchange.Buy(ask, 1)       # 平空单
        mp = 0                                  # 设置虚拟持仓的值,即空仓
    if mp == 0 and dif > dea:
        Log('多开信号成立,挂单价格为:', ask)
        exchange.SetDirection("buy")        # 设置交易方向和类型
        id = exchange.Buy(ask, 1)       # 开多单
        mp = 1                                  # 设置虚拟持仓的值,即有多单
    if mp == 0 and dif < dea:
        Log('空开信号成立,挂单价格为:', bid)
        exchange.SetDirection("sell")       # 设置交易方向和类型
        id = exchange.Sell(bid, 1)      # 开空单
        mp = -1                                 # 设置虚拟持仓的值,即有空单
        
def main():
    while True:
        try:
            onTick()
            Sleep(1000)
        except:
            print('策略运行完成')
            break
            
if __name__ == "__main__":
    main()
    
print(task.Join(True)) # print backtest result
task.Show() # or show backtest chart

怎样查看策略回测的效果呢,使用我们开头讲过的task.Join和task.Show,运行一下。可以看到策略的实时的资金变化,还有收益的曲线。这个策略确实不太理想哈,赔到最后只剩1600多元了,确实是一个真实的量化效果。我们经常可以看到,使用MACD指标一年翻几十倍的爆炸标题视频,现在呢,我们就可以使用这个策略使用优宽量化本地回测引擎,针对于我们关注的品种,真实的验证传闻策略的真假。

我们在网页界面回测一下,看看是否得出同样的回测效果。点击复制并进行在线回测,点击开始回测,但是这里的初始金额需要和本地的一致,改为10000元,可以看到回测的结果,最后的净值也是1600多元,证明本地回测的结果是可信的。当然这只是一个试验的策略,大家可以在本地继续优化它,直到做出来一个满意的策略。

第 6 章 CTA 之趋势跟踪策略

CTA 是一种多样性的投资方法,一般指商品期货和金融期货策略,它并不拘谨于是主观交易还是量化交易,只要其交易方法相对规则化、系统化都可以称为 CTA 策略。本章将结合不同的策略理论来开发 CTA 策略。

6.1 什么是 CTA 策略

CTA 的英文全称是 Commodity Trading Advisor,直译为商品交易顾问,通常指专业的资金管理人或机构。CTA 最初活跃于商品市场,随着金融市场的发展,其投资领域逐渐拓展到股票、国债、外汇等等。

6.1.1 CTA 策略的分类

CTA 策略的交易周期主要以分钟、小时和日线等数据为主,也有少部分使用 1 分钟以下周期的数据。CTA 策略有多种类型,以策略持仓周期可以分为中长线策略、短线策略、高频策略。以交易方法可以分为趋势策略和反转策略。在量化 CTA 策略实际交易中,这两类策略并不是独立运行,有时候会根据市场状况和各自的优劣进行策略组合,相对于单一类型的策略而言,这种优化组合和分散部署策略的方法,经常能取到更好的效果。注意:CTA 策略总体可以分为趋势策略和反转策略两大类。

6.1.2 趋势策略

用“守株待兔”这个词形容趋势策略再恰当不过了,这种策略的理念是顺势而为。大多时候不需要对未来价格进行预测,而是利用一些技术指标守在趋势发生的必经之路上,买入并持有直到趋势消失时退出。趋势策略的特点是经常亏小钱,但一次就赚个大的。资金曲线呈现脉冲状态,一般回撤较大,所以该策略对资产管理、风险控制,以及交易者的心理素质要求有比较高,通常是叠加多个交易品种,或利用策略多样性来降低风险。

6.1.3 反转策略

反转策略其实就是低买高卖赚取差价,它的交易方法与趋势策略相对应,在价格低时买入,在价格高时卖出。其特点是经常赚小钱,但一旦遇到趋势行情就赔个大的,资金曲线呈现阶梯状。由于市场大部分时间都在一个价格范围内上下波动,反转策略又非常适合这种行情,因此有很多人使用这种策略。反转策略既可以在两个相同品种的价差上套利交易,又可以利用价格网格进行低买高卖。

6.1.4 量化 CTA 策略

当然 CTA 策略绝不仅限于此,根据基本面、产业链调研、操盘经验等主观判断价格走势决定买卖也属于 CTA 策略。不过随着计算机科学的发展,涌现出很多量化 CTA 策略,主要是通过数据建模分析,发掘潜在交易机会。包括:自然语言处理、循环神经网络、随机森林模型等技术。但是总体来看,量化 CTA 中用的比较多的策略是趋势策略,相对于其他策略而言,量化趋势策略的优点是: 1、入门简单,做过交易的人大概都知道什么是技术指标,这些技术指标不需要太多的编程技巧就可以轻易转换为量化趋势策略。 2、无惧牛熊,尤其是在期货这种双向可以交易的市场,趋势策略在价格上涨或下跌的行情中都能获利,特别是在牛市熊市快速转换或趋势明显的时候。 3、由于每个交易者对风险承受力都不一样,随着行情加剧变化,亏损会使反向交易者出现非理性的踩踏式平仓,这一点非常有利于量化趋势策略。

6.2 经典 MACD 交易策略

前面的章节中我们学习了量化交易基础知识、Python编程语言基础语法以及优宽量化平台使用方法。虽然内容很枯燥,但这是你实现交易策略的必备知识。接下来几节课我们就用之前所学到的知识,趁热打铁从最简单的策略开始边学边用,一步一步帮助大家实现交易策略。

6.2.1 MACD 简介

相信做过交易的人对MACD都不陌生,这是一个非常古老的技术指标,它是由查拉尔·阿佩尔(Geral Appel)在上个世纪70年代发明的,全称指数平滑异同移动平均线。顾名思义这个指标是通过均线来对趋势进行判断。

如下图所示MACD主要由两条和中间的红绿柱组成。第一条线是DIF,反映的是一段时间内价格的变化情况。第二条线叫做DEA,它是DIF的均线,所以相对要平缓一些。而红绿柱叫做BAR柱,反映的是DIF与DEA两线之间的距离。注意:MACD是一种中长线的趋势指标,在市场反复震荡时,可能会出现错误信号。

image

6.2.2 MACD 原理

严格来讲MACD是均线的延伸,其意义与均线基本相同,只是它在计算时赋予了权重,时间越近赋予权重越大。同时它在图形上展示时非常直观,观察起来也一目了然。最重要的是其巧妙的利用短期指数移动平均线与长期指数移动平均线之间的聚合与分离状况,来判断市场状态。 具体来说,它是运用快速和慢速移动平均线之差,并加以双重平滑运算得来。这样不仅去除了一部分普通移动平均线频繁发出的假信号,而且还保留了移动平均线判断趋势行情的效果。因此MACD指标比均线更有趋势性和稳定性。具体来说就是:

  • 第1步:先对杂乱的K线均值处理,即EMA12和EMA26。EMA其实是另一种复杂均线,与普通均线不同的是,其价格权重以指数形式逐渐缩小,随着时间的增加,价格的权重越大,更能及时反映近期价格波动情况。
  • 第2步:为了解决信号滞后和频繁的无效信号问题,对两根均线差值处理,即DIF。均线差值可以灵活反映两根均线的相互关系,DIF上升往往意味着短期成本的涨速高于长期成本的涨速,市场短期内资金买入的意愿更强。
  • 第3步:重复第1步,对DIF均值处理,即DEA。
  • 第4步:重复第2步,对DIF、DEA差值处理,即MACD直方图(Histogram),也就是我们常说的红绿柱子。

6.2.3 MACD 计算方法

第 1 步:计算 EMA12 和 EMA26

  • EMA12 = XAverage(Close, 12)
  • EMA26 = XAverage(Close, 26)

第 2 步:计算 DIF

  • DIF = EMA12 - EMA26

第 3 步:计算 DEA

  • DEA = XAverage(DIF, 9)

第 4 步:计算 MACD 直方图(Histogram)

  • Histogram = DIF - DEA

6.2.4 MACD 使用方法

网上关于 MACD 的使用方法层出不穷,有利用 DIF 与 DEA 金叉死叉做趋势的,有结合价格看顶底背离做抄底的,也有辅助其他技术分析工具的等等,这些方法只在特定的时间有效。交易的关键不是用了哪个万能指标,而是制定一个正期望的交易策略,然后重复执行。其实对于大部分交易者来说,做趋势策略要好过做震荡策略,因为趋势策略的容错率更低。

传统的使用方法是看 DIF 与零线的位置关系,或者是 DIF 与 DEA 的交叉状态,来判断价格走势。一般来说 DIF 大于 0 表示上涨,小于 0 表示下跌;或者当 DIF 向上突破 DEA 时,形成买入信号,当 DIF 向下突破 DEA 时,形成卖出信号。但也有一部分人用红绿柱的高低,再结合价格走势判断 MACD 的顶背离和底背离,这是一种典型的反转交易方法。其理论是价格与 MACD 是趋于同向的,当价格上涨,MACD 也跟着上涨。如果价格逐步下跌,但 MACD 并没有跟着下跌,则逐步上涨,


更多内容

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

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