基于FMZ量化的订单同步管理系统设计(1)

Author: 小小梦, Created: 2022-02-14 19:46:30, Updated: 2023-09-15 20:44:11

img

基于FMZ量化的订单同步管理系统设计(1)

在FMZ文库的往期文章中,我们设计过几种订单、持仓同步策略。

这些是把参考账户和同步账户放在一个策略中管理实现订单、持仓同步的。今天我们来尝试一点不一样的设计,基于FMZ量化交易平台的强大扩展API接口,我们来设计一个订单同步管理系统。

设计思路

首先我们需要有一些好的建议、需求。以上两个往期的订单、持仓同步策略就有几个明显的痛点,我们一起来讨论:

  • 1、同步策略实盘的实施者,必须有参考账户的交易所API KEY、同步账户的交易所API KEY。 这个问题对于使用场景是:自己其它交易所账户跟随自己的某个账户是没问题的。但是对于参考账户和同步账户不是一个所有者的场景就会很麻烦。同步账户的拥有者有时候基于安全考虑,不愿意提供自己交易所账号的API KEY。但是不提供API KEY怎么同步下单交易呢?

    解决方案: 使用FMZ的扩展API接口,同步账户的拥有者(跟单者)只需要注册FMZ量化交易平台,然后运行一个策略(本文设计的系统中的:订单同步管理系统(Synchronous Server)策略实盘)。然后把FMZ的扩展API KEY(注意,不是交易所账户的API KEY)、订单同步管理系统(Synchronous Server)实盘ID提供给参考账户的拥有者(带单者)就可以了。 当参考账户拥有者(带单者)的实盘(本文设计的系统中的订单同步管理系统类库(Single Server))发出信号,同步账户拥有者的实盘就会收到交易信号,后续自动下单。

  • 2、很多开发者有比较好的策略,没法使用上面描述的2个往期订单、持仓同步策略。因为那样需要把自己的策略和这些同步策略融合,可能策略就需要大改,费事费力。有没有好的方法让自己的一些成熟策略直接升级上订单同步功能呢? 解决方案: 可以设计一个订单同步模板类库(本文设计的系统中的订单同步管理系统类库(Single Server)策略),让参考账户的拥有者(带单者)直接把这个模板类库嵌入自己的策略即可实现订单、持仓同步功能。

  • 3、减少一个额外的实盘。 最后一个痛点就是,如果用上面描述的2个往期订单、持仓同步策略。需要额外开一个实盘监控参考账户的持仓(带单账户)。 解决方案: 使用模板类库,把功能嵌入参考账户策略中。

所以这个系统由2部分构成: 1、订单同步管理系统类库(Single Server) 2、订单同步管理系统(Synchronous Server)

明确了需求,那就开始动手设计吧!

设计1:订单同步管理系统类库(Single Server)

注意,这并不是一个策略。而是一个FMZ的模板类库,关于模板类库的概念可以在FMZ API文档中搜索到,这里不再赘述。

模板类库代码:

// 全局变量
var keyName_label = "label"
var keyName_robotId = "robotId"
var keyName_extendAccessKey = "extendAccessKey"
var keyName_extendSecretKey = "extendSecretKey"
var fmzExtendApis = parseConfigs([config1, config2, config3, config4, config5])
var mapInitRefPosAmount = {}

function parseConfigs(configs) {
    var arr = []
    _.each(configs, function(config) {
        if (config == "") {
            return 
        }
        var strArr = config.split(",")
        if (strArr.length != 4) {
            throw "configs error!"
        }
        var obj = {}
        obj[keyName_label] = strArr[0]
        obj[keyName_robotId] = strArr[1]
        obj[keyName_extendAccessKey] = strArr[2]
        obj[keyName_extendSecretKey] = strArr[3]
        arr.push(obj)
    })
    return arr 
}

function getPosAmount(pos, ct) {
    var longPosAmount = 0
    var shortPosAmount = 0
    _.each(pos, function(ele) {
        if (ele.ContractType == ct && ele.Type == PD_LONG) {
            longPosAmount = ele.Amount
        } else if (ele.ContractType == ct && ele.Type == PD_SHORT) {
            shortPosAmount = ele.Amount
        }
    })
    var timestamp = new Date().getTime()
    return {ts: timestamp, long: longPosAmount, short: shortPosAmount}
}

function sendCommandRobotMsg (robotId, accessKey, secretKey, msg) {
    // https://www.fmz.com/api/v1?access_key=xxx&secret_key=yyyy&method=CommandRobot&args=[186515,"ok12345"]
    var url = "https://www.fmz.com/api/v1?access_key=" + accessKey + "&secret_key=" + secretKey + "&method=CommandRobot&args=[" + robotId + ',"' + msg + '"]'
    Log(url)
    var ret = HttpQuery(url)
    return ret 
}

function follow(nowPosAmount, symbol, ct, type, delta) {
    var msg = ""
    var nowAmount = type == PD_LONG ? nowPosAmount.long : nowPosAmount.short
    if (delta > 0) {
        // 开仓
        var tradeDirection = type == PD_LONG ? "buy" : "sell"
        // 发送信号
        msg = symbol + "," + ct + "," + tradeDirection + "," + Math.abs(delta)        
    } else if (delta < 0) {
        // 平仓
        var tradeDirection = type == PD_LONG ? "closebuy" : "closesell"
        if (nowAmount <= 0) {
            Log("未检测到持仓")
            return 
        }
        // 发送信号
        msg = symbol + "," + ct + "," + tradeDirection + "," + Math.abs(delta)
    } else {
        throw "错误"
    }
    if (msg) {
        _.each(fmzExtendApis, function(extendApiConfig) {
            var ret = sendCommandRobotMsg(extendApiConfig[keyName_robotId], extendApiConfig[keyName_extendAccessKey], extendApiConfig[keyName_extendSecretKey], msg)
            Log("调用CommandRobot接口,", "label:", extendApiConfig[keyName_label], ", msg:", msg, ", ret:", ret)
            Sleep(1000)
        })
    }
}

$.PosMonitor = function(exIndex, symbol, ct) {    
    var ts = new Date().getTime()
    var ex = exchanges[exIndex]
    // 判断ex类型
    var exName = ex.GetName()
    var isFutures = exName.includes("Futures_")
    var exType = isFutures ? "futures" : "spot"
    if (!isFutures) {
        throw "仅支持期货跟单"
    }

    if (exType == "futures") {
        // 缓存 symbol ct
        var buffSymbol = ex.GetCurrency()
        var buffCt = ex.GetContractType()

        // 切换到对应的交易对、合约代码
        ex.SetCurrency(symbol)
        if (!ex.SetContractType(ct)) {
            throw "SetContractType failed"
        }

        // 监控持仓
        var keyInitRefPosAmount = "refPos-" + exIndex + "-" + symbol + "-" + ct    // refPos-exIndex-symbol-contractType
        var initRefPosAmount = mapInitRefPosAmount[keyInitRefPosAmount]
        if (!initRefPosAmount) {
            // 没有初始化数据,初始化          
            mapInitRefPosAmount[keyInitRefPosAmount] = getPosAmount(_C(ex.GetPosition), ct)
            initRefPosAmount = mapInitRefPosAmount[keyInitRefPosAmount]
        }

        // 监控
        var nowRefPosAmount = getPosAmount(_C(ex.GetPosition), ct)
        // 计算仓位变动
        var longPosDelta = nowRefPosAmount.long - initRefPosAmount.long
        var shortPosDelta = nowRefPosAmount.short - initRefPosAmount.short

        // 检测变动
        if (!(longPosDelta == 0 && shortPosDelta == 0)) {
            // 执行多头动作
            if (longPosDelta != 0) {
                Log(ex.GetName(), ex.GetLabel(), symbol, ct, "执行多头跟单,变动量:", longPosDelta)
                follow(nowRefPosAmount, symbol, ct, PD_LONG, longPosDelta)
            }
            // 执行空头动作
            if (shortPosDelta != 0) {
                Log(ex.GetName(), ex.GetLabel(), symbol, ct, "执行空头跟单,变动量:", shortPosDelta)
                follow(nowRefPosAmount, symbol, ct, PD_SHORT, shortPosDelta)
            }

            // 执行跟单操作后,更新
            mapInitRefPosAmount[keyInitRefPosAmount] = nowRefPosAmount
        }

        // 恢复 symbol ct
        ex.SetCurrency(buffSymbol)
        ex.SetContractType(buffCt)
    } else if (exType == "spot") {
        // 现货
    }
}

$.getTbl = function() {
    var tbl = {
        "type" : "table", 
        "title" : "同步数据", 
        "cols" : [], 
        "rows" : []
    }
    // 构造表头
    tbl.cols.push("监控账户:refPos-exIndex-symbol-contractType")
    tbl.cols.push(`监控持仓:{"时间戳":xxx,"多头持仓量":xxx,"空头持仓量":xxx}`)
    _.each(fmzExtendApis, function(extendApiData, index) {
        tbl.cols.push(keyName_robotId + "-" + index)
    })
    
    // 写入数据
    _.each(mapInitRefPosAmount, function(initRefPosAmount, key) {
        var arr = [key, JSON.stringify(initRefPosAmount)]
        _.each(fmzExtendApis, function(extendApiData) {
            arr.push(extendApiData[keyName_robotId])
        })
        tbl.rows.push(arr)
    })

    return tbl
}

// 引用该模板类库的策略调用范例
function main() {
    // 清除所有日志
    LogReset(1)

    // 切换到OKEX 模拟盘测试
    exchanges[0].IO("simulate", true)

    // 设置合约
    exchanges[0].SetCurrency("ETH_USDT")
    exchanges[0].SetContractType("swap")

    // 定时交易时间间隔
    var tradeInterval = 1000 * 60 * 3        // 三分钟交易一次,用于观察跟单信号
    var lastTradeTS = new Date().getTime()
    
    while (true) {
        // 策略其它逻辑...

        // 用于测试的模拟交易触发
        var ts = new Date().getTime()
        if (ts - lastTradeTS > tradeInterval) {
            Log("模拟带单策略发生交易,持仓变化", "#FF0000")
            exchanges[0].SetDirection("buy")
            exchanges[0].Buy(-1, 1)
            lastTradeTS = ts
        }

        // 使用模板的接口函数
        $.PosMonitor(0, "ETH_USDT", "swap")    // 可以设置多个监控,监控带单策略上的不同的exchange对象  
        var tbl = $.getTbl()
        
        // 显示状态栏
        LogStatus(_D(), "\n" + "`" + JSON.stringify(tbl) + "`")
        Sleep(1000)
    }
}

设计上十分简单,这个类库有2个功能函数。当FMZ平台上的一个程序化交易策略引用了订单同步管理系统类库(Single Server)模板类库之后。这个策略就可以使用以下函数。

  • $.PosMonitor 该函数的作用是监控策略中的交易所对象的持仓变动,然后向模板:订单同步管理系统类库(Single Server)的参数中设置的实盘发送交易信号。

  • $.getTbl 返回监控的同步数据。

使用例子就在:订单同步管理系统类库(Single Server)模板的main函数中:

// 引用该模板类库的策略调用范例
function main() {
    // 清除所有日志
    LogReset(1)

    // 切换到OKEX 模拟盘测试
    exchanges[0].IO("simulate", true)

    // 设置合约
    exchanges[0].SetCurrency("ETH_USDT")
    exchanges[0].SetContractType("swap")

    // 定时交易时间间隔
    var tradeInterval = 1000 * 60 * 3        // 三分钟交易一次,用于观察跟单信号
    var lastTradeTS = new Date().getTime()
    
    while (true) {
        // 策略其它逻辑...

        // 用于测试的模拟交易触发
        var ts = new Date().getTime()
        if (ts - lastTradeTS > tradeInterval) {
            Log("模拟带单策略发生交易,持仓变化", "#FF0000")
            exchanges[0].SetDirection("buy")
            exchanges[0].Buy(-1, 1)
            lastTradeTS = ts
        }

        // 使用模板的接口函数
        $.PosMonitor(0, "ETH_USDT", "swap")    // 可以设置多个监控,监控带单策略上的不同的exchange对象  
        var tbl = $.getTbl()
        
        // 显示状态栏
        LogStatus(_D(), "\n" + "`" + JSON.stringify(tbl) + "`")
        Sleep(1000)
    }
}

一个模板类库本身也可以创建策略实盘,通常用来测试模板类库。例如该模板的测试。您可以理解模板中的main函数就是您自己某个策略的main函数。

测试代码编写为使用OKEX模拟盘测试,需要在FMZ上配置OKEX 模拟盘的API KEY作为参考账户(带单),main函数中开始切换为模拟盘。然后设置交易对为ETH_USDT,在设置合约为永续(swap)。然后进入一个while循环。循环中每间隔3分钟进行一次下单交易,用来模拟策略交易触发。while循环中调用了$.PosMonitor(0, "ETH_USDT", "swap"),调用的这个函数第一个参数传入0,表示监控exchanges[0]这个交易所对象,监控ETH_USDT交易对,swap合约。然后调用$.getTbl()获取图表信息,使用LogStatus(_D(), "\n" + "`" + JSON.stringify(tbl) + "`")让图表数据显示在状态栏上。

所以你看,只要在某个引用了该模板的策略中使用了$.PosMonitor(0, "ETH_USDT", "swap"),就可以让策略具有监控某个品种的持仓,持仓变动去推送消息的功能。

测试之前说明一下订单同步管理系统类库(Single Server)策略的参数设计: 刚才讲了如何使用模板的接口函数,让某个策略升级具有带单功能。那么持仓变动时发送的信号,发送给谁呢? 发送给谁这个问题就由订单同步管理系统类库(Single Server)的参数来配置了。

img

可以看到参数有5个,最多支持5个推送(需要增加可以自行扩展),参数默认是空字符串,即不处理。配置字符串格式:label,robotId,accessKey,secretKey

  • label 同步账户的标签,用来给某个账户标记,名字可以随便设置。

  • robotId 实盘ID,同步账户的拥有者创建的订单同步管理系统(Synchronous Server)实盘的ID。

  • accessKey FMZ的扩展API的accessKey

  • secretKey FMZ的扩展API的secretKey

接下来我们就可以进行简单的测试了。

订单同步管理系统类库(Single Server)实盘运行:

img

订单同步管理系统(Synchronous Server)实盘收到了信号: 订单同步管理系统(Synchronous Server)目前我们还没设计完成,我们先用一个简单的代码实现,不做交易,只打印信号:

订单同步管理系统(Synchronous Server)临时代码:

function main() {
    LogReset(1)
    while (true) {
        var cmd = GetCommand()
        if (cmd) {
            // cmd: ETH_USDT,swap,buy,1
            Log("cmd: ", cmd)
        }
        Sleep(1000)
    }
}

img

可以看到同步账户拥有者的实盘收到了信息:ETH_USDT,swap,buy,1。 这样下一步就可以根据信息中的交易对、合约代码、交易方向、数量进行自己的自动跟单了。

目前订单同步管理系统(Synchronous Server)仅为临时代码,我们下一期继续探讨它的设计。


Related

More

mingxi1005 要实现跟单,还是需要两个实盘,一个是类库实盘,一个是订单管理系统实盘

mingxi1005 按教程弄的,显示配置错误

阿乐 反向跟单需要改哪些参数

阿乐 自己跟单自己也要开两个实盘,一个发信号一个收信号,这两个能合并一起实盘用么

小小梦 您可能没看明白文章,这个类库是一个工具,可以在带单者策略行直接嵌入,然后这个策略就有带单功能了,就会给设置好的跟单账户发信息,跟单机器人就会收到消息跟单了。 简单说就是这样的场景。

小小梦 可以看下文章,配置信息: 标签,实盘ID,accesskey,secretkey 。 报这个错误应该就是您信息配置错了,您再检查下。注意使用英文逗号间隔。

mingxi1005 错误 configs error!,在订单同步管理系统类库(Single Server)中,把带单者实盘和2个KEY都填进去了,然后再实盘中引用了订单同步管理系统类库(Single Server),报错,错误 configs error!

mingxi1005 错误 configs error!

小小梦 要看具体报什么错误信息。

小小梦 需要改策略。

小小梦 代码公开的,您可以根据需求修改一下,就可以实现。