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

商品期货量化交易实践系列课程

Author: 雨幕(youquant), Created: 2023-09-12 09:54:22, Updated: 2023-12-18 16:43:42

头动量阈值。可以使用数组的some函数。可以看到,使用array的一些函数可以很方便的帮我们进行一些数组的操作,省去了需要for或者while循环的轮子。

接下来就到了开平仓的操作了,获取当前仓位,如果持仓为空,并且两个信号为真,进行开多仓的操作;如果判断持有仓位,在出现两个信号中有一个不成立的情况下,我们及时的进行平仓。

这就是第二个策略的编写,我们同样使用白糖进行一下回测,可以看到相对于第一个策略,这个策略策略采取的操作更少,但是取得了8000多元的收益。这波涨势抓的是不错的。换一个品种,螺纹钢,不出意外的出现意外,收益为-1200多元。

第三个策略,RSI-IBS 策略,这个策略在rsi指标之外,新加了一个指标IBS,表示内部K线强度),指标计算公式如下:

(Close - Low) / (High - Low)

IBS指标的波动范围从0到1,测量收盘价相对于日内高低点的位置,较低的值被认为是看涨的,而较高的值则是短期看跌的,IBS的基本假设是市场具有均值回归的特性。

策略交易逻辑为:

  1. 开仓:IBS低于0.25、RSI低于45
  2. 平仓:当日收盘价高于昨日收盘价

我们来看下具体代码的编写,同样的首先设置参数rsiWindow是21,rsiThreshold是45,增加一个指标ibs_entry是0.25。在while循环里,在rsi指标计算之外,增加ibs指标的计算。然后我们就可以进行开仓信号的判断了,在rsi值小于rsiThreshold,并且ibs小于0.25情况下,定义longSignal。 接着进行交易的操作,满足开仓条件进行入场;对于平仓的逻辑是今日的收盘价大于昨日,或者持仓天数超过9天。我们来看下回测的结果。

同样的使用白糖,可以看到相对于第一个策略在整体的时间段都有开平仓的操作,第二个策略主要在上升浪的过程中进行开平仓的操作,这个策略是在上升浪之外,也就是更多的在震荡的时间,进行一些开平仓的操作。这时候我们聪明的小脑瓜开始活动了,如果我们合成第二个第三个策略,是不是会取得更好的效果呢?

其实原始策略的最后一部分就是合成使用这三个策略的指标进行开平仓的操作,但是我们看到第一个策略信号很容易触发,所以可能会进行更多的无效操作,所以我们来自作主张,只合成第二个第三个指标,看看策略的运行效果。

/*backtest
start: 2023-01-03 09:00:00
end: 2023-10-25 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["symbol","SR888"]]
*/

function rsiRangeMom(r) {
    var rsiPeriod = 14
    var rsiWindow = 30
    var rsi_lower = 40
    var rsi_upper = 100
    var rsi_highest = 70

    var rsi = TA.RSI(r, rsiPeriod)
    var rsiList = rsi.slice(-rsiWindow)
    
    var rangeSignal = rsiList.every(value => value >= rsi_lower && value <= rsi_upper)
    var momSignal = rsiList.some(value => value > rsi_highest)

    var longSignal = rangeSignal && momSignal
    var coverSignal = rangeSignal == false || momSignal == false
    return [longSignal, coverSignal]
}


function rsiIbs(r) {
    var rsiPeriod = 21
    var rsiThreshold = 45
    var ibs_entry = 0.25
    
    
    var rsi = TA.RSI(r, rsiPeriod)
    var preClose = r[r.length - 2].High
    var ibs = (r[r.length - 1].Close - r[r.length - 1].Low) / (r[r.length - 1].High - r[r.length - 1].Low)
    var longSignal = rsi[rsi.length - 1] < rsiThreshold && ibs < ibs_entry
    var coverSingal = r[r.length - 1].Close > preClose

    return [longSignal, coverSingal ]          
}

function main() {
    var maxHolding = 9
    var openTime = null
    var p = $.NewPositionManager()
    var initAccount = exchange.GetAccount()
    
    while(true){
        if(exchange.IO('status')){
            LogStatus('已连接交易所')
            exchange.SetContractType(symbol)
            var r = exchange.GetRecords()
            var nowTime = r[r.length - 1].Time

            var rsiRangeMomIndicator = rsiRangeMom(r)
            var rsiIbsIndicator = rsiIbs(r)
 
            var pos = exchange.GetPosition()
            Log(rsiRangeMomIndicator[0],rsiRangeMomIndicator[0])
            Log(rsiIbsIndicator[0],rsiIbsIndicator[1])
            if(pos.length == 0 && (rsiRangeMomIndicator[0] == true || rsiIbsIndicator[0] == true)){
                p.OpenLong(symbol, 1)
                openTime = nowTime
            } else if(pos.length == 1 && ((rsiRangeMomIndicator[1] == true && rsiIbsIndicator[1] == true) || nowTime - openTime >= 1000*60*60*24*maxHolding)){
                p.Cover(symbol, 1)
                openTime = null
            }


            var posStatus = {
                type: "table",
                title: "持仓信息",
                cols: ["合约名称", "持仓方向", "持仓均价", "持仓数量", "持仓盈亏"],
                rows: []
            }

            var posDir = pos.length > 0 ? (pos[0].Type == PD_LONG || pos[0].Type == PD_LONG_YD) ? '多头' : '空头' : ''
            var posPrice = pos.length > 0 ? pos[0].Price : 0
            var posAmount = pos.length > 0 ? pos[0].Amount : 0
            var posProfit = pos.length > 0 ? pos[0].Profit : 0

            posStatus.rows.push([symbol, posDir, posPrice, posAmount, posProfit])

            lastStatus = '`' + JSON.stringify([posStatus]) + '`'
            LogStatus(lastStatus)

            var accountInfo = exchange.GetAccount()

            var totalProfit = accountInfo.Balance - initAccount.Balance
            LogProfit(totalProfit)

        }else{
            LogStatus('等待连接交易所')
        }
        Sleep(1000)
    }
}

根据原始的策略原理:这里我们来定义如果第二第三个策略开仓信号任意成立一个,我们进行开仓的操作;对于平仓,我们的信号判断是,如果第二第三个策略平仓信号同时成立的话,进行平仓的操作。所以我们第一步就是需要计算两个策略的开仓信号和平仓信号。将两个策略的变量一起放入主函数,会有些杂乱,并且两个策略的参数名是一致的,但是参数值是不一致的,所以我们可以定义两个函数,用来返回两个策略的开平仓信号。这里来分别定义rsiRangeMom和rsiIbs函数,参数都是k线。在两个函数体内,我们根据两个策略的内容,删掉交易操作的部分,计算出开仓信号和平仓信号,最后使用return进行返回。需要注意的是,第三个策略的最大持仓时间,需要在开仓后才能统计,所以我们将这部分放在主函数中进行定义。

在主函数中,我们进行开平仓的操作。如果持仓为空,并且两个策略的开仓信号任意成立一个,进行入场;对于平仓,如果两个平仓信号成立,或者持仓时间达到上限,我们进行平仓。同样的使用白糖,我们进行策略的验证。可以看到,组合策略吸收了两个策略的优点,在趋势和震荡时间段,同时具有交易的操作,所以盈利部分也获得了一定的增加。

当然这个策略,作为实盘肯定是不足的。我们可以加上一些可视化的展示。加上持仓状态的展示和实时的收益展示。这就是我们可以运行的实盘策略,我们跑下看看。

以上呢,就是我们根据热门策略,在优宽平台使用JS语言实现,通过这几个策略的编写,可以发现无论是数据获取,代码编写和策略运行和回测展示,在优宽平台似乎更加方便一点。当然如果大家有好的建议,也欢迎大家留言评论区,我们会积极的改进,大家有好的想改编的策略,也欢迎投稿,我们会热心帮大家实现~

视频参考链接:

《RSI多头短线择时策略》

21.行情收集(Mysql数据库)

最近接到大家的反馈,怎样保存实盘的k线数据到数据库当中,从而搭建属于自己的量化数据库。解决方法呢,也很简单,我们可以创建一个实盘数据收集器,用来保存我们的实时数据,这样我们可以在本地连接我们的数据库,进而获取数据进行数据分析和策略探索的一些工作。今天我就来教大家怎样进行实现。

本节课我们的示例是在服务器上实现的,如果你的托管者在本地,那么我相信实现起来更为容易。我们的实时数据获取是在托管者上的,如果想在本地获取,那么我们需要一个中间的媒介,其实优宽拥有专门的数据库工具,DBExec,可以很方便的使用sql语言进行数据的存储和管理。但是有不少熟悉mysql的小伙伴想使用mysql进行搭建,今天呢,我们就以mysql为工具建立一个数据收集器。其实实现起来并不是十分的复杂,但是针对于我们的初学者用户,实现起来中间确实有一些波泽,所以本节课进行实操展示,大家可以参考学习一下。

首先我们需要在服务器端安装mysql软件。这里的服务器我使用的是轻量服务器,基本的交易操作和数据存储都是满足的。由于服务器系统和mysql软件版本有很多,大家可能在安装的过程中出现种种的坑。经过我多次踩坑,最后为大家推荐的教程是这篇。Mysql安装这里面的坑很少,大家一步步安装就可以。

第二部分是远程访问的设置。我们需要进入mysql中进行管理员权限的更改,让远程也可以访问数据库。

最后一部分是服务器安全组的设置,作用是放通mysql的端口,但是这篇文章使用的是标准的服务器,最后一部分安全组的设置开放Mysql的外接端口,在轻量服务器中没有相关的设置,我们需要在这里的防火墙设置中进行端口的放通。防火墙这里添加规则,点击mysql端口。以上呢,就是我们需要安装mysql和设置远程访问的大致步骤。

我们现在可以在远程测试下连接数据库。这里我使用的是python语言,因为在python语言进行数据的清洗,处理和分析都是很方便的。这里我们需要下载一个pymysql的包,安装成功后。打开jupyter notebook,首先import mysql.connector,然后创建 MySQL 连接对象,这里的user和password使我们刚才在mysql中创建的管理员的账户和密码,host填写为服务器的公网地址。然后创建MySQL游标对象,执行 SQL 查询,这里我们要写sql语句,'show databases’展示所有的数据库,然后执行sql语句,接着打印查询到的数据库的名称,成功打印后,证明我们的远程数据库连接成功。这里最后加上游标的关闭和数据库的关闭,防止资源的过渡占用。

import mysql.connector

# 创建 MySQL 连接对象
cnx = mysql.connector.connect(user='????', password='????',
                              host='????')
                              
# 创建 MySQL 游标对象
cursor = cnx.cursor()

# 执行 SQL 查询
query = 'show databases'
cursor.execute(query)

for row in cursor:
    print(row)
    
cursor.close()

cnx.close()

Mysql数据库就基本搭建好了,接下来我们在实盘中需要创建策略,实时的搜集数据,并保存到服务器的Mysql数据库当中。这样在本地,我们可以连接服务器上的数据库获取实时数据进行分析。因为有些数据在回测系统中是不提供的,比如期权的数据,我们需要实时的搜集。

import pymysql

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

接着我们来写策略创建我们的行情收集器。在这个策略里面,我们需要连接本地数据库,创建数据表,获取实时数据并写入数据表。这里使用到了一些基本的python语言,如果大家感到有一点陌生的话,可以询问chatgpt的帮助。

进入我们的代码,在开头,是同样的步骤,import pymysql,稍微提醒一下,这是服务器上的python环境,所以这个包我们在服务器上需要重新安装一下,其实我们安装很多我们常用的包到我们的服务器当中,比如机器学习或者深度学习的包,这样我们也可以结合深度学习进行策略的编写。回到本节课,这里创建 MySQL 连接对象和游标对象,然后我们需要创建一个新的databases。接下来,就要使用这个databases来实时保存数据。这里为了举例,我们保存下期货和相应期权的k线数据。首先执行sql语句"USE my_database",然后在my_database中创建future_kline和option_kline表,具体表中每栏的设置,我们可以根据返回的k线属性,包括timestamp时间戳,高开低收四个价格,成交量和持仓量,具体的每一列的数据属性,还有NOT NULL表示不允许出现空值。这里还设置了每一行数据的id,作为主key,是一个自增的数字。这样初始的数据库连接和表的设置就完成了,接下来到主函数我们进行实时数据的搜集和写入。

和Javascript语言一样,定义main主函数,还有我们的固定框架,在while循环前,定义preBarTime是先前k线的时间,然后分别设置合约,进行k线数据的获取,定义为futureR和optionR变量,因为CTP协议对期权数据返回的速度比较慢,这里我们设置了k线的周期是五分钟PERIOD_M5,是足够我们进行数据分析的。这里我们收集的是已经完成的k线,也就是倒数第二根k线,所以在开头我们需要判断期货或者期权数据的长度是否大于2,否则跳过本次循环。然后定义变量newfutureR和newoptionR作为期货k线和期权k线倒数第二根的数据,是已完成的k线数据。

接着我们就要数据的收集了,因为返回的时间不一致,所以我们需要判断两根k线的时间戳是否相等,然后判断两根k线的时间戳是否不等于prebartime,证明k线更新了,这三个条件都需要满足以后,我们进行数据的写入,首先将prebartime定义为最新k线的时间戳,然后将这根k线的数据写入表中,这里执行insert语句进行对应属性数值的写入,然后使用commit提交事务进行永久保存。

最后我们设置程序的休眠时间是10秒,因为收集的数据是5分钟的k线,所以可以设置的大一点。总体来说,这份代码并不复杂,结合了sql语句和k线属性的特点,一些地方稍加注意就可以。

我们运行实盘看一下,这里是我们实盘的日志,可以看到等到k线更新,执行写入的工作。这样我们的行情收集器就设置好了。

接下来我们在本地验证下数据库的写入工作。回到我们刚才的代码中,这里需要填写需要的databse,是我们刚才创建完成的my_database,然后这里执行"SHOW TABLES",可以看到我们刚才创建的两个表,然后打开其中的一个期权k线表,可以看到最新导入的数据,我们可以把这个数据转换为dataframe,然后画一个k线图看下。

import mysql.connector

cnx = mysql.connector.connect(user='????', password='????',
                              host='????',
                             database='my_database')

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

cursor.execute("SHOW TABLES")

# 获取查询结果
tables = cursor.fetchall()
tables

cursor.execute("SELECT * FROM option_kline")

# 获取查询结果
results = cursor.fetchall()

results

cursor.close()

cnx.close()

接下来我们在本地验证下数据库的写入工作。回到我们刚才的代码中,这里需要增加一个参数database,填写我们刚才创建完成的my_database,然后打开其中的一个期权k线表,可以看到最新导入的数据,一共有86根k线,我们可以把这个数据转换为dataframe,然后画一个k线图看下。另外,我们也可以进行很多有趣的数据分析工作,大家有兴趣都可以探索一下。

其实行情收集器的应用场景有很多,通过建立自己的量化数据库,我们可以给多个实盘提供数据源,另外可以让实盘启动时,当对需要的K线BAR数量达到一定要求的时候,我们可以直接使用行情收集器里的数据,我们再也不用担心实盘起始的时候K线BAR数量不足了。

这样呢,我们就完成了利用实盘进行实时的数据获取和写入工作,希望可以满足那位小伙伴的疑问。为了教学讲解,这里的sql语言可能写的简单,如果各位有sql大佬,也欢迎进行优化。下节课,我们就将使用我们收集的数据作为自定义数据源,在优宽平台进行回测,有感兴趣的小伙伴不要错过。

视频参考链接:

《腾讯云Linux服务器安装Mysql8并实现远程访问》

22.回测系统自定义数据源实践

上节课我们手把手教大家实现了一个行情收集器,那么收集的行情数据接下来怎么使用呢?当然是用于回测系统了,这里依托于优宽量化交易平台回测系统的自定义数据源功能,我们就可以直接把收集到的数据作为回测系统的数据源,这样我们就可以让回测系统应用于任何我们想回测历史数据的市场了。注意是任何数据哦,包括外汇,商品现货,股票等等。

因此,我们可以给「行情收集器」来一个升级!让行情收集器同时可以作为自定义数据源给回测系统提供数据。下面我们来看具体怎样实现。

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,这样我们就可以从指定的数据库和表中获取我们需求的数据。

但是获取到的数据是我们需求的格式吗,我们需要进行一下处理。我们来看下回测系统需求的数据格式。在API文档里,可以看到需求的字段包括三个,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线数据,而我们可以随时获取这些收集的数据直接在回测系统回测了。当然我们的课程属于抛砖引玉,各位大神还可以继续扩展,例如支持多品种、多市场数据收集等等功能,可以真正的搭建一个属于自己的量化数据库。

视频参考链接:

《手把手教你给行情收集器升级回测自定义数据源功能(数字货币版)》

23.经典剥头皮策略JavaScript语言版

成为一名成功的交易者,一个重要方法是寻找到适合我们的交易技巧或策略,例如日内交易、剥头皮交易、波段交易等。每一种交易方法都会带来不同程度的风险,它们对交易技巧的要求也有所不同。如果你是位忙碌又想规避风险的交易者,持仓时间较短,想从几个小型的交易中寻求稳定且持续的利润,那么最适合你的就是剥头皮交易了

剥头皮交易策略(Scalping Strategy)是一种短期交易策略,旨在通过迅速进出市场来捕捉小幅价格波动获取利润。这个策略通常适用于高流动性的市场,如外汇市场或流动性较强的期货市场。剥头皮策略的核心思想是利用市场的短期波动性,寻找短暂的价格变动,并迅速进出仓位以获取利润。

剥头皮交易者会多次获得小额的利润,且不会持续持有盈利的仓位,这为的是在收益出现时及时把握住收益。换而言之,其目标是通过大量盈利的仓位而非少数大幅盈利的成功交易来实现成功的交易策略。剥头皮交易依靠的是降低敞口风险的理念,由于在每笔交易上实际所花的时间相当短,所以能够降低不利事件导致大幅波动后所面临的风险。此外剥头皮交易者认为,相比幅度较大的波动,幅度较小的波动较容易能够把握住并且发生的频率较高。

尽管存在一些困难,剥头皮交易非常适合交易新手。如果掌握了这个策略,那么我们可以进行超级短线交易。成为一名剥头皮交易者,并不需要对基本面分析有深入的了解。所以这种机械化的交易机制,非常适合使用量化语言进行表达。

剥头皮策略其实相当于是一个交易机制,并不是单指使用某种具体的指标和策略。总体来说,剥头皮交易的分析模式可以大致分为三组:

    1. 保守型剥头皮交易

    在这种模式下,交易者可以等待较长时间以获取最佳的入场点。对于保守的日内交易策略,会使用各种技术分析工具,例如移动平均线、图表形态式、技术指标和震荡指标、支撑和阻力水平等。

    1. 中性剥头皮交易

    剥头皮交易者在一个小时内进行几十次交易。这里剩下的时间用于市场分析很少,更多地依靠反应和直觉。交易者会监控订单流、市场对新闻的反应以及上涨和下跌市场价格行为的动态。

    1. 激进型剥头皮交易

    在这种情况下,头寸在开仓几秒钟后立即平仓。这种交易策略旨在获取几个点的利润。

而具体的剥头皮策略使用的指标有很多,比如随机指标策略,移动平均线策略,抛物线转向指标策略和相对强弱指标策略等等。今天我们介绍的剥头皮策略来源于油管的一位博主,这个策略使用场景是外汇市场,我们看能否改造下,适用于商品期货市场。

我们首先大致介绍下这个策略的交易逻辑。这个策略依靠大小两个周期确定具体的入场点位,止盈和止损系数确定具体的出场点位。在期货市场中,有一句老手经验,“大周期定方向,小周期定买卖。”在这个策略当中,我们使用小时级别EMA均线确定大周期的趋势,如果大周期快线大于慢线,并且为了预防突发的情况,最新k线收盘价也要大于慢线,我们定义大周期为多头;相反情况下如果大周期快线小于慢线,并且最新收盘价小于慢线,我们定义为空头。这样大周期我们就确定完成。

下面来看小周期具体交易的确定,小周期使用到了三根均线,这里也要确定小周期的方向,如果快线大于中线,中线大于慢线,确定小周期为多头;相反情况,快线小于中线,中线小于慢线,确定为空头。那么怎么确定具体的交易点位呢,在期货交易中,有一个词叫做“回踩”。如果前一根k线的最低价回踩到小周期的快线之下,但是不能超过慢线,我们可以下多单,挂单的价格是最近五根k线的最高价加上三个点位,因为这个价格一般大于目前的价格,根据交易机制,在一个较高的价格可以保证立即成交,所以在一定程度上可以提高当时交易成功的概率。对于做空也是一样,出现回踩,使用最近最近五根k线的最低价减去3个点位进行挂单。

这里面比较特殊的一点是重复挂单的处理,因为这里使用的是挂单方式等待入场,而不是直接入场,所以有时候会出现重复挂单,这个问题我们需要在程序中设计完善。

对于离场的设置,这里使用的是两个止盈区间和止损区间,但是在期货市场中经过测试,由于波动性强于外汇市场,所以止盈的区间远大于止损的区间,这种止盈止损的方法不太适合于期货市场,我们采用通过设置止盈系数和止损系数,当达到固定点位我们进行离场。

以上呢,就是这个剥头皮策略的整体逻辑。现在大家有没有想法使用代码自己实现一下?大家可以暂停,手动编写一下这个策略。下面呢,我们将讲解下在优宽平台使用Js语言进行一下实现。

打开策略编辑页面,固定框架安排。首先我们需要设置几个参数,包括目标合约,趋势判断周期,包括小时级快线和慢线周期,5分钟级快线,中线和慢线周期,还有交易参数,包括入场点周期,止盈和止损系数,这个策略里面的参数较多,所以我们将策略参数进行分组,可以很方便的在不同分组中找到我们需要的参数进行修改,还记得怎样进行分组吗?使用括号加问号的方式,里面进行分组的命名。这样就可以清晰的展示不同分组的参数了。

接着我们来设置主函数,首先初始化几个变量,包括小时级趋势指标,5分钟级趋势指标,和最终的判断指标,先前的挂单价格,和止盈和止损的价格。这里为了平仓的方便,使用交易类库单品种控制对象。

下面进入我们的主循环,设置好合约,我们需要进行两个不同时间级别趋势的判断,首先是小时级别的,

使用GetRecords获取k线,参数填写为PERIOD_H1,这里先做一个判断,如果收集的k线数目小于计算慢线需要的数量,跳过这次循环,等收集足够的数量以后,使用TA.EMA内置函数计算小时级别的快线和慢线,然后进行小时级别也就是大周期趋势的判断。

这里的判断条件是快线大于慢线,并且当前k线的收盘价也是需要大于慢线,定义大周期趋势为1,也就是多头;相反情况下,如果快线小于慢线,并且最新的收盘价小于慢线,定义趋势为空,赋值为-1。

接下来进行小周期的判断,设置参数为PERIOD_M5,然后获取5分钟级别,也就是小周期的快线,中线和慢线,接着判断小周期的趋势,当快线大于中线,并且中线大于慢线,定义小周期趋势为多;另一种相反情况,定义为空,赋值为-1。

两个周期的趋势判断好以后,进行总的趋势的判断,使用三元表达式,如果两个趋势都是多,那么总体趋势finalSignal也是多,如果两个都是空,那么判断总体趋势也是空。

接下来我们要进行下单的设置了,获取当前仓位,如果持仓为空,当趋势明显并且出现回踩情况的情况下,我们就要进行相应方向的开仓。第一种情况,如果总体趋势判断为多,回踩的判断是,如果当k线价格小于快线,但是也不能偏离的太多,所以还需要判断收盘价需要大于慢线,这时候定义为入场的信号。入场挂单的价格在原版的策略中,是最近5根周期的最高价加上三点,我们这里将这个值设置为一个参数,enterPeriod,计算指定周期内k线的最高价,可以使用TA.Highest内置函数,然后加上3点,定义为挂单价格,我们设置好方向,进行挂单。但是当挂单以后,不一定能迅速的成交,仓位依然为0,所以依然会进入这个条件,当信号重复出现的时候,这个时候会出现多次挂单,所以这里我们需要加一个判断,记录每次的挂单价格为preOrderPrice,如果preOrderPrice不等于orderPrice,我们再次进行挂单,这样就避免了多次挂单的情况。

对于空头的处理逻辑也是一致的,如果总体趋势为空,出现向上的回踩。设置挂单价格,在判断不是重复挂单的情况下,进行挂单。

挂单完成以后,我们来解决上一步留下的问题,解决多余挂单的问题。什么算是多余挂单呢?这个我们需要定义清楚,第一种情况,如果挂单成交,已经持有仓位,那么就需要取消所有的未完成的挂单;第二种情况,仓位没有成交,但是挂单价格发生了变化,所以导致挂了多手仓位,这时候我们需要取消先前的挂单;这个时候我们就只剩下了一手的挂单了,但是这时候如果总体趋势发生变化,我们需要及时的取消掉挂单,防止带来亏损。这就是我们需要取消挂单的三种情况。

我们来看下怎样使用代码进行实现。获取所有未完成的订单函数大家还记得吗?使用GetOrders获取到。

第一种情况,在判断挂单成交,也就是仓位列表长度不为空的情况下,如果这时候还存在挂单的话,取消所有的挂单;

第二种情况,如果没有持仓,但是持有多个挂单,我们需要删除前面的挂单,只保留最新的挂单,这个i的定义是挂单列表的长度减1,只保留最新的挂单,撤销掉前面多余的挂单;

第三种情况,如果未完成的挂单列表长度为1,我们做一个判断,使用order的Type属性和Offset属性,ORDER_TYPE_BUY代表买单,ORDER_OFFSET_OPEN代表开仓,但是如果趋势不是1多头的话,就要取消当前的挂单;另一种情况,如果Type是ORDER_TYPE_SELL代表卖单,Offset也要是开仓,如果finalSignal不是-1,取消当前挂单。

这里为了理解的方便,所以取消挂单的设计比较直接一点,这里可以优化简洁的地方有很多,大家可以设计的更加完善一点。

下面我们进入止盈止损的设计,这里就没有那么多的特殊情况了,根据仓位的type属性判断是多仓还是空仓,如果持仓为多头,并且最新的收盘价大于开仓的价格加上止盈区间参数,我们进行止盈,或者小于开仓价格减去止损区间,我们进行止损,使用Cover函数直接平仓;对于空头也是一样,如果最新价格小于止盈价格或者大于止损价格,进行平仓。但是,在持有仓位以后,不忘要了重置先前的挂单价格。

以上呢,就是剥头皮策略的交易逻辑部分的设置,我们回测跑下看下效果,这里我们使用纯碱主力合约,时间为1周,使用真实的tick数据,参数的设置为原始推荐的参数,我们看到一共取得了103元的收益,策略的平仓盈亏是860元,但是手续费差不多是600元,确实很符合这个策略的特性。

作为一个实践课程,策略仅能在回测系统运行肯定是不够的,我们需要一个实盘级别的策略,所以在策略的plus版本中,代码增加了状态栏的显示,包括趋势判断和持仓状态,还有止盈止损次数统计,并且还有收益曲线和大小周期均线的画图。我们实盘运行一下,可以看到一个标准的实盘级策略可以多方面的了解策略的运行状态。

需要注意的是,本策略重点是要来讲解策略主体的交易逻辑,所以对于参数的选择和优化我们并没有涉及太多,原始的参数是针对于外汇市场的,针对于期货市场,我们可以进行一下调参,并且策略讲解中对于止盈止损的设计提出了很多好的想法,大家也可以根据这个策略为模版,进行更一步的探索和优化。

完整版代码:

/*backtest
start: 2023-09-01 09:00:00
end: 2023-09-07 23:20:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["symbol","SA888"],["smallFastMA",12],["smallMiddleMA",16],["smallSlowMA",26],["enterPeriod",3]]
*/

function main() {
    var initAccount = exchange.GetAccount()
    var trendBigSignal = 0 
    var trendSmallSignal = 0
    var finalSignal = 0
    var preOrderPrice = 0
    var orderDir = ''
    var orderPrice = 0
    var posPrice = 0
    var posType = ''
    var posProfit = 0
    var stopProfitPrice = 0
    var stopLossPrice = 0
    var winCount = 0
    var lossCount = 0
    var curProfit = 0
    var preProfit = 0

    var p = $.NewPositionManager()
    
    while(true){
        if(exchange.IO('status')){
            exchange.SetContractType(symbol)
            var bigR = exchange.GetRecords(PERIOD_H1)
            
            var fastMABig = TA.EMA(bigR, bigFastMA)
            var slowMABig = TA.EMA(bigR, bigSlowMA)

            if(bigR.length < 21){
                continue
            }

            if(fastMABig[fastMABig.length - 1] > slowMABig[slowMABig.length - 1] && bigR[bigR.length - 1].Close > slowMABig[slowMABig.length - 1]){
                trendBigSignal = 1
            }else if(fastMABig[fastMABig.length - 1] < slowMABig[slowMABig.length - 1] && bigR[bigR.length - 1].Close < slowMABig[slowMABig.length - 1]){
                trendBigSignal = -1
            }

            var smallR = exchange.GetRecords(PERIOD_M5)
            var curPrice = smallR[smallR.length - 1].Close
            var fastMASmall = TA.EMA(smallR, smallFastMA)
            var middleMASmall = TA.EMA(smallR, smallMiddleMA)
            var slowMASmall = TA.EMA(smallR, smallSlowMA)

            if(fastMASmall[fastMASmall.length - 1] > middleMASmall[middleMASmall.length - 1] && middleMASmall[middleMASmall.length - 1] > slowMASmall[slowMASmall.length - 1]){
                trendSmallSignal = 1
            }else if(fastMASmall[fastMASmall.length - 1] < middleMASmall[middleMASmall.length - 1] && middleMASmall[middleMASmall.length - 1] < slowMASmall[slowMASmall.length - 1]){
                trendSmallSignal = -1
            }
            
            var finalSignal = trendBigSignal == 1 && trendSmallSignal == 1 ? 1 : trendBigSignal == -1 && trendSmallSignal == -1 ? -1 : 0

            var posInfo = exchange.GetPosition()

            // 下单

            if(posInfo.length == 0){

                posPrice = 0
                posType = ''
                posProfit = 0
                stopProfitPrice = 0

更多内容