使用JavaScript入门商品期货量化交易

Author: 雨幕(youquant), Created: 2023-08-15 17:04:31, Updated: 2024-09-12 09:04:45

用一个三元表达式进行判断,如果没有仓位,那么仓位的浮盈和保证金都是0;如果有仓位,调用calculateTotalProfit计算浮盈和保证金。

输入变量定义好以后,接下来我们回到指标计算的函数,来具体解释一下函数中各个指标的计算步骤。

累计收益率(totalReturns):

通过profits索引获取最后一个收益点的收益。 将最后一个收益除以总资产,得到累计收益率。

年化收益率(annualizedReturns):

计算年交易日数的毫秒数:yearDays * 86400000。 将累计收益率乘以年交易日数的毫秒数。 再除以投资时长(te - ts),得到按年化的预期收益率。

接下来我们来计算:

maxDrawdown是最大回撤。它表示从最高峰值到最低谷值之间的资产损失的最大百分比。

maxDrawdownTime表示达到最大回撤时的时间戳,资产达到最低谷的时刻。

maxAssetsTime表示达到最大净值时的时间戳,资产达到最高峰值的时刻。

maxDrawdownStartTime表示最大回撤开始的时间戳,从最高峰值开始计算最大回撤的起始时刻。

winningRate表示胜率,在所有收益记录点中盈利的比例。

这些指标通过遍历profits数组来计算和更新。

首先,使用一个循环遍历profits数组中的每个收益记录点。在循环开始时,初始化一些变量,包括最大回撤(maxDrawdown)为0,最大资产(maxAssets)为初始净值(totalAssets),以及一些相关的时间戳变量。

在循环中,首先检查当前收益记录点的索引。如果是第一个点(i == 0),则判断其收益是否大于0,表示盈利,如果是则将胜利次数(winningResult)加1。对于其他点,只需要判断当前点的收益是否大于前一个点的收益,如果是,则将胜利次数加1。

接下来,通过比较当前收益记录点与最大资产的和与之前记录过的最大资产的大小,来更新最大资产和对应的时间戳。如果当前的和大于最大资产,则更新最大资产和最大资产时间。

然后,在确保最大资产大于0的情况下,计算当前回撤率(drawDown),即(1 - 当前资产和最大资产的比值)。如果当前回撤率大于之前记录的最大回撤,则更新最大回撤、最大回撤时间和最大回撤开始时间。

最后,如果profits数组不为空,则计算胜率(winningRate),即胜利次数除以总的收益记录点的数量。

通过循环遍历profits数组,并根据每个收益记录点更新相关变量,最终得到了最大回撤、最大回撤时间、最大净值时间、最大回撤开始时间和胜率这些指标的值。

这里的难点可能是夏普比率(sharpeRatio)的计算:

首先,进行trim profits,目的是计算和整理利润数据。

代码初始化了一些变量,包括 i、datas、sum、preProfit、perRatio 和 rangeEnd。其中 i 用于迭代遍历 profits 数组,datas 用于存储每个时间段的收益率,sum 则是所有收益率之和。

然后,根据投资时长范围 ts 和 te 以及时间段长度 period,把rangeEnd处理为period的整倍数。

接下来,通过循环计算每个时间段的收益率。循环从 ts 开始,每次增加 period,直到达到 rangeEnd。在每个时间段内,通过遍历 profits 数组,找到符合条件的收益点,并将其收益加到 dayProfit 中。同时更新 preProfit 为当前收益点的收益值。然后,根据公式 计算该时间段的收益率,并将其累加到 sum 中,并添加到 datas 数组中。

接下来,代码计算夏普比率。在判断datas数组的长度是否大于0的情况下,进入计算夏普比率的逻辑。首先计算收益率的平均值 avg,然后计算标准差 std。接着,根据公式计算volatility波动率。最后,如果波动率不为0,则根据公式

sharpeRatio = (annualizedReturns - freeProfit) / volatility

计算夏普比率。

最终通过函数返回这样的输出结果:

totalAssets:初始净值
yearDays:交易天数
totalReturns:累计收益率
annualizedReturns:年华收益率
sharpeRatio:夏普比率
volatility:波动率
maxDrawdown:最大回撤
maxDrawdownTime:最大回撤时的时间戳
maxAssetsTime:最大净值时的时间戳
maxDrawdownStartTime:最大回撤开始时间
winningRate:胜率

我们来举一个例子实际示范下回测指标的计算。

function main(){
    var mp = 0; // 设置持仓信息

    // 设置returnAnalyze输入变量
    var profits = [];          // 后续随着交易过程添加
    var totalAssets = 1000000; // 初始金额
    var period = 86400000;     // 交易周期毫秒数
    var yearDays = 252;        // 年度交易天数
    const initialTs = new Date().getTime(); // 策略起始时间ts变量

    while(true){
        exchange.SetContractType('rb888')
        var r = exchange.GetRecords(); // 获取K线数组

        if (r.length < 20){            // 需要超过K线的长度20
            return;} 
     
        var boll = TA.BOLL(r, 20, 2); //计算布林带指标

        var upLine = boll[0];   // 获取上轨数组
        var midLine = boll[1];  // 卖取中轨数组
        var downLine = boll[2]; // 获取下轨数组
        var upPrice = upLine[upLine.length - 3];
            // 获取上上根K线上轨数值
        var midPrice = midLine[midLine.length - 3];
            // 获取上上根K线中轨数值
        var downPrice = downLine[downLine.length - 3]; 
            // 获取上上根K线下轨数值

        recclose = r[r.length - 2].Close;    // 获取上根K线收盘价

        if(mp == 0 && recclose > upPrice){   // 如果无持仓,并且收盘价大于上轨,开多
                // 设置下单方向
            exchange.SetDirection("buy");
            exchange.Buy(recclose,1)    
            mp = 1;} 

        if(mp == 0  && recclose < downPrice){ // 如果无持仓,并且收盘价小于下轨,开空
                // 设置下单方向
            exchange.SetDirection("sell");
            exchange.Sell(recclose,1)    
            mp = -1;} 

        if(mp == 1 && (recclose < midPrice)){ // 如果持多,并且收盘价小于中轨,平多
                // 设置下单方向
            exchange.SetDirection("closebuy");
            exchange.Sell(recclose-5,1);      // 这里为了保证交易,所以设置限价单为最新的价格减5
            mp = 0;} 

        if(mp == -1 && (recclose > midPrice )){ // 如果持空,并且收盘价大于中轨,平空
                // 设置下单方向
            exchange.SetDirection("closesell");
            exchange.Buy(recclose+5,1);         // 同样的,为保证交易,这里设置限价单
            mp = 0;} 


        // profits数组的计算,每个元素包含时间和收益
        // 重点是profits数组的计算,因为原始的GetAccount和GetPosition都不能直接获取策略在运行期间的总体收益
        // 通过组合GetAccount和GetPosition的数据,我使用了Balance + pos_profit + pos_margin - totalAssets方法计算实时的收益
        
        var account = exchange.GetAccount();
        var position = exchange.GetPosition();

        function calculateTotalProfit(data) {
            var totalProfit = 0;
            var totalMargin = 0;

            for (let i = 0; i < data.length; i++) {
                totalProfit += data[i].Profit;
                totalMargin += data[i].Margin;
            }

            return [totalProfit, totalMargin];
        }

        var pos_profit = position.length == 0 ?  0 : calculateTotalProfit(position)[0]
        var pos_margin = position.length == 0 ?  0 : calculateTotalProfit(position)[1]

        profit = account.Balance + pos_profit + pos_margin  - totalAssets

        time = new Date().getTime();

        profits.push([time, profit]);

        // 定义ts和te时间戳
        const ts = initialTs;
        te = time;
        
        // 输入变量获取完成,带入函数进行计算
        ret = returnAnalyze(totalAssets, profits, ts, te, period, yearDays)
        

        // 格式化指标为保留三位小数的百分比形式
        var totalReturnsPercent = (ret.totalReturns * 100).toFixed(3) + "%";
        var annualizedReturnsPercent = (ret.annualizedReturns * 100).toFixed(3) + "%";
        var sharpeRatio = (ret.sharpeRatio).toFixed(3);
        var volatilityPercent = (ret.volatility * 100).toFixed(3) + "%";
        var maxDrawdownPercent = (ret.maxDrawdown * 100).toFixed(3) + "%";
        var winningRatePercent = (ret.winningRate * 100).toFixed(3) + "%";

        // 构建结果的表格数据
        var table = {
            type: "table",
            title: "分析结果",
            cols: ["指标", "值"],
            rows: [
                ["总资产", ret.totalAssets.toString()],
                ["年交易天数", ret.yearDays.toString()],
                ["总收益率", totalReturnsPercent],
                ["年化收益率", annualizedReturnsPercent],
                ["夏普比率", sharpeRatio],
                ["波动率", volatilityPercent],
                ["最大回撤", maxDrawdownPercent],
                ["最大回撤时间", _D(ret.maxDrawdownTime)],
                ["最大资产时间", _D(ret.maxAssetsTime)],
                ["最大回撤起始时间", _D(ret.maxDrawdownStartTime)],
                ["胜率", winningRatePercent]
            ]
        };

        // 状态栏进行输出
        LogStatus("`" + JavaScriptON.stringify(table) + "`");

        // 注意:由于原始指标计算的精度是天,这个策略计算的精度更小一点,因此个别计算出来的指标和回测系统的指标不一致。         
        Sleep(1000)
    }

}

首先定义好回测系统指标计算的函数,returnAnalyze。

接着在主函数中,定义我们的交易策略是我们以前讲过的布林带交易策略。注意,为了计算各个回测指标,需要设置一些returnAnalyze输入变量。

在交易逻辑定义完成以后,我们需要获取returnAnalyze需要实时更新的te时间变量和profits收益变量。

接着我们来定义这些指标的输出格式,为了呈现的更加清晰,我们不使用日志,而使用状态栏进行指标的输出。首先格式化指标为保留三位小数的百分比形式,然后构建结果的表格数据,最后使用LogStatus函数进行输出。

我们点击回测,对比一下我们的计算结果和YOUQUANT内置回测系统计算出来的结果。可以发现有些指标不太一致,因为回测指标计算的精度是天,减少了日内的波动,而我们策略计算的精度更小一点,方便实时的查看,因此个别计算出来的指标和回测系统的指标不一致。

以上就是回测评价系统中的指标计算,大家初看起来,可能比较复杂,不过大家细细剖析一下,可以发现条理是非常清晰的,并且可修改的空间也很大,比如这里的胜率计算,我们是按照本时刻的收益是否大于前一时刻的收益来计算的,而我们更习惯的是,计算逐笔的胜率,就是一笔交易从开仓到平仓是否盈利。因此代码可以修改一下,打造出符合我们交易逻辑的回测指标评价系统。

27:策略进度恢复的设计(一)

在先前的课程中,我们讲解的策略都是运行在模拟回测系统中,策略直接出来回测结果,不受外部物理条件的干扰,是一种比较理性化的策略运行状态。然而,在实际实盘运行中,当我们部署托管者在自己的电脑上时,不经意的停电或者断网会造成我们实盘策略的停止;而部署在云服务上的托管者,也有一定的可能性收到DDOS攻击造成断网,造成实盘的停止。因此,怎样处理这种突发的情况,是我们实践性课程需要重点讲解的部分。

量化交易中的策略进度恢复是指在遇到系统故障、网络中断或其他异常情况导致交易程序中断时,恢复并继续执行交易策略的过程。策略进度恢复是保证交易系统的稳定性和连续性的关键环节。

策略进度恢复可以分为以下几种类型:

冷启动恢复:当整个交易系统需要重启时,包括策略引擎、数据接口、交易接口等,需要重新加载和初始化所有的策略和相关数据。

数据恢复:在交易过程中,如果数据源出现问题或中断,需要通过补齐缺失的数据或重新获取数据来使策略的计算和决策不受影响。

策略状态恢复:当交易程序因为故障或异常情况中断时,需要将策略的状态保存下来,以便在恢复后继续执行。这包括已经计算的指标、持仓信息、订单状态等。

交易状态恢复:如果交易接口中断,导致未完成的订单无法提交或确认,需要在恢复后重新查询和处理这些未完成的订单,确保交易的完整性和一致性。

策略进度恢复的重要性不言而喻。在量化交易中,每一次交易决策都可能产生利润或者亏损,因此无论是策略还是交易状态的丢失都可能导致损失。策略进度恢复能够保证交易系统的连续性和稳定性,避免因系统故障而产生额外的风险和错误交易。同时,对于高频交易或需要实时响应市场变化的策略来说,策略进度恢复更加重要,它能够尽快将系统恢复到正常运行状态,减少交易信号的延迟和错过交易机会的风险。

优宽量化作为专业的量化交易平台,非常重视策略进度恢复的设计和实现。而作为专业进阶的量化交易人,这些问题也需要未雨绸缪。相当于Pine语言和麦语言,这种高度封装的交易语言,可以自动实现策略的进度恢复,而JavaScript语言和python语言作为从底层搭建的量化系统,需要我们手动实现策略的进度恢复。因此,本节课程我们将讲解JavaScript语言的策略进度恢复的设计。

_G(K, V)函数的使用

首先,来介绍我们的策略进度恢复的好帮手,_G函数。作为一个优宽量化平台的内置函数,它的数据结构为KV表,K必须为字符串,它不区分大小写,V可以为任何可以JavaScriptON序列化的内容。它可以永久保存在本地文件,所以该函数实现了一个可保存的全局字典功能。它在回测和实盘中都是支持的。在模拟回测系统中,回测结束后,_G函数保存的数据会被清除。而在实盘系统中,每个实盘单独一个数据库,重启或者托管者退出后,_G函数保存的数据是一直存在的。所以它的使用很灵活,可以放入任何我们想要储存的数据,状态变量和持仓状态等。

我们举例示范一下。这里由于不使用接口获取数据的测试,就不需要使用exchange.IO(“status”)函数判断连接状态,也不用设置合约代码,因为这里仅仅是测试_G函数。首先,在_G函数中定义键K为“num”的字符串,然后值V为数字1,使用_G(“num”)可以打印该字典键对应的值1;当然键对应的值也可以更改,这里我们重新定义”num”键对应的值是字符串”ok”,然后我们重新调用键,会返回新的值”ok”;如果需要删除这个值对应的值,填入null就可以;如果想删除所有全局变量,可以直接填入null;在实盘运行中当调用_G()函数并且不传任何参数时,_G()函数返回当前实盘的ID。

function main(){
    
    // 设置一个全局变量num,值为1
    _G("num", 1)
    Log(_G("num")) // 返回1
    // 更改一个全局变量num,值为字符串ok
    _G("num", "ok") 
    Log(_G("num")) // 返回"ok"
    // 删除全局变量num
    _G("num", null) // 返回null
    // 返回全局变量num的值
    Log(_G("num"))  // 返回null
    // 删除所有全局变量
    _G(null)
    Log(_G()) // 返回实盘ID
}

持仓状态的恢复

在了解完_G函数以后,我们来进入第一个场景,持仓状态的恢复。持仓状态恢复的重要性在于确保交易系统的连续性和准确性。当交易程序中断时,在程序化策略中持仓信息可能会丢失,造成持仓信息和实际的仓位不一致的状况,这可能导致交易系统的错误决策和风险暴露。

通过持仓状态恢复,我们可以将中断前的持仓信息重新加载到交易系统中,确保交易系统在恢复后能够基于正确的持仓信息进行进一步的决策和风险管理。

此外,持仓状态恢复还对于交易流程的完整性和可追溯性至关重要。通过持仓状态的恢复,我们可以准确地记录交易系统的行为和决策过程,使得后续的回测、风险控制和复盘等工作能够进行。

在以往编写的策略中,为了展示的方便,我们大多使用的是虚拟持仓变量,在策略中断的时候,这个虚拟持仓变量会丢失,这样可能会造成实际仓位和虚拟持仓的不符,造成策略的运行冲突,出现错误。

另外,还有一种情况,就是在策略运行期间,即使我们保存了持仓的信息,我们手动的在交易软件中开仓或者平仓,同样会造成记录持仓和实际持仓的冲突。因此,我们需要考虑这两方面的问题。

所以呢,我们针对上述的问题,可以提出解决方案,这个方案首先可以时刻保持策略的持仓状态,即使策略中断也可以;第二,在策略开始的时候,需要检查策略保存的持仓状态和实际的持仓状态,是否有冲突,在有冲突的计划下,针对于不同类型的策略,我们需要制定好备用方案,比如修改策略保存的持仓变量,然后继续运行策略;或者发送警告,立即停止策略等等。

下面我们针对第一种场景,针对持仓状态的恢复进行设计,我们举例示范一下,布林带策略的持仓状态恢复的策略,我们这里举例示范的交易策略都很简单,重点是让大家可以了解在实盘运作中出现问题,我们解决的思路是什么,相信各位聪明的小伙伴们也许还有更好的解决方法,如果有兴趣的话,大家可以分享到我们优宽量化平台的文库或者社区板块,供大家一起瞻仰。

首先固定程序,连接交易所,获取k线数据,计算布林带指标。然后根据布林带指标和持有的仓位进行开平仓的操作。这里碰到我们的第一个重点,怎么获取和保存持仓状态变量。

使用以前的方法,在循环体之前,设置虚拟持仓变量mp=0,然后伴随策略运行,mp变量不断更新,但是突然遇到实盘停止,虚拟持仓变量将会丢失,当我们重启实盘,mp将会重新设置为0;如果目前的实际仓位是不为空的时候,会造成仓位信息的不一致,从而进行错误的操作,导致不必要的损失。

因此,我们可以将持仓状态变量始终保存在这个实盘当中,每次交易信号出现,使用这个持仓状态变量进行相应的操作。来让我们设置一下:

在策略开始的时候,我们直接获取持仓状态变量getPos,这个时候,这个_G函数的键值对还没有定义,因此返回的值会是一个空值,所以我们使用一个三元表达式,当为空值的时候,getPos是0,而当有对应键值的时候,返回有效值,然后将它赋值为mp变量;接下来我们使用保存的持仓状态变量mp进行交易的操作,并在每次操作完毕后,重新定义mp变量;最后不要忘了使用_G函数保存新的持仓状态变量。

我们在实盘中测试下,可以看到策略开始的时候,getPos变量返回0,而伴随交易的操作,getPos变量不断更新;接下来我们手动停掉实盘看下,然后重新打开,可以看到getPos的变量没有重置,依旧是实盘停掉之前的状态。这样,我们的第一个问题就解决了。即使我们短暂的停掉实盘,修改一下参数,然后我们重新开启实盘,当前的持仓状态变量不变,策略可以继续运行。

// 策略持仓状态恢复
function main() {
    var p = $.NewPositionManager();
    var symbol = 'FG401';

    while (true) {
        if (exchange.IO("status")) {
            LogStatus(_D(), "已经连接到CTP!");
            exchange.SetContractType(symbol);
            var records = exchange.GetRecords();

            if (!records || records.length < 20) {
                Sleep(1000); // 等待获取足够多的K线数据
                continue;
            }

            // 以防实盘运行停止或者手动平仓,需要保存策略持仓状态变量和对比实际的仓位
            getPos = _G('getPos');
            getPos = getPos == null ? 0 : getPos;
            Log('持仓保存状态:', getPos);
            

            mp = getPos; // 赋值mp为getPos

            if (records.length < 20){           // 需要超过K线的长度20
                return;} 
     
            var boll = TA.BOLL(records, 20, 2); //计算布林带指标

            var upLine = boll[0];   // 获取上轨数组
            var midLine = boll[1];  // 卖取中轨数组
            var downLine = boll[2]; // 获取下轨数组
            var upPrice = upLine[upLine.length - 3];
                // 获取上上根K线上轨数值
            var midPrice = midLine[midLine.length - 3];
                // 获取上上根K线中轨数值
            var downPrice = downLine[downLine.length - 3]; 
                // 获取上上根K线下轨数值

            recclose = records[records.length - 2].Close; // 获取上根K线收盘价

            if(mp == 0 && recclose > upPrice){            // 如果无持仓,并且收盘价大于上轨,开多
                // 设置下单方向 
                p.OpenLong(symbol, 1);
                mp = 1;
            }

            if (mp == 1 && (recclose < midPrice)) {   // 如果持多,并且收盘价小于中轨,平多
                p.Cover(symbol);
                mp = 0;
            }

            if (mp == 0  && recclose < downPrice) {   // 如果无持仓,并且收盘价小于下轨,开空
                p.OpenShort(symbol, 1);
                mp = -1;
            }

            if (mp == -1 && (recclose > midPrice )) { // 如果持空,并且收盘价大于中轨,平空
                p.Cover(symbol);
                mp = 0;
            }

            _G('getPos', mp); // 使用_G更新持仓状态
            Log('更新持仓状态:',_G('getPos'));

            Sleep(5000);

        } else {
            LogStatus(_D(), "未连接CTP!");

            Sleep(1000);
        }
    }
}

Position中需要使用的属性:

{
    Amount          : number,       // 持仓量
    Type            : 0,
    ContractType    : symbol,       // 合约代码
    ...
}

下面,我们来探讨下第二个问题,在策略运行期间,我们手动的造成了实际持有的仓位和保存的仓位不符合的情况,应该怎样做?其实这也是一个仓位检查的问题,在策略运行初始,我们需要对照下实际仓位和保存仓位的信息是否一致。我们可以添加下面的代码进行仓位的检查。

首先GetPosition函数获取实际的仓位信息,GetPosition函数会有一些特殊的情况,我们需要处理下:当为空仓的时候,GetPosition函数也会返回空值,因此我们定义真实持仓变量realPos是0,第二种情况,GetPosition函数会返回很多品种的仓位信息,每一个品种的仓位信息占据一个索引位置,所以在检查到posList不为0 的情况下,我们需要使用轮询找到我们策略的目标品种(symbol),然后我们打印对应的品种,方向(Type),这里需要注意到是,当Type为0或者2,代表多头方向;Type为1或者3,代表空头方向;然后是打印amount,这里需要注意的是,仓位信息返回的amount都是正数;最后我们进行真实仓位realpos的计算,因为posList返回的Amount持仓数量都是正数,因此我们需要使用Type属性进行判断,当Type属性是0或者2,可以将realPos定义正数1乘以Amount;否则当Type属性返回1或者3的时候,将realPos定义为负数1乘以Amount。

然后我们使用持仓状态变量和真实持仓变量进行对比,如果两者是一致的,mp就赋值为持仓状态变量getPos,然后使用绿色字体打印正常信息;如果两者状态不一致,就要制定我们的PlanB,这里我们B计划是使用红色打印警告信息,直接使用throw抛错误。温馨提示,我们也可以使用别的计划,不抛出错误,而是将’getPos’键重新赋值为真实持仓状态变量,然后继续运行策略。针对于不同类型的策略,我们可以根据自己的需求进行进一步的优化。

下面的逻辑就一致了,赋值mp为更新后的持仓状态变量,然后执行交易逻辑,最后保存新的持仓状态变量。

我们在实盘中测试下,首先为策略正常运行状态,可以看到持仓状态变量和真实持仓是一致的,然后,我们停掉我们的实盘。来到YOUQUANT的交易终端,这是YOUQUANT搭建的一个实时的交易平台,可以手动进行,期货的真实或者模拟的实盘交易。在仓位列表中,可以看到目前的仓位为空,然后点击开一手仓位;开仓成功后,回到我们的持仓状态恢复策略中,可以看到红色字体显示两个仓位不一致,然后立刻发出警告,停止了实盘。

这就是我们策略进度恢复中,持仓状态恢复的一个简单的设计。当然还有很多不完善的地方。作为一个专业的量化交易人,如果我们想打造更加完美的交易系统,在尝试探索的过程中,一定会遇到问题和阻碍,其实把问题的细节一一列举清楚,通过查找资料和学习,发挥自己的主观能动性,相信大部分问题都可以解决,大家一起加油!

28:策略进度恢复的设计(二)

经过上节课的学习,我们了解了优宽量化平台的_G函数,以及如何使用它进行持仓进度的恢复。本节课,我们继续讲解策略进度恢复的设计中的状态变量的恢复。

状态变量的恢复

在量化交易中,状态变量是指用于驱动和执行交易策略的各种数据和指标。这些状态变量可以包括已经计算的指标、和交易信号等。状态变量的恢复指的是在交易程序中断后,通过保存已经计算的指标值,以便在程序恢复后能够继续使用这些指标进行决策和交易。

状态变量的恢复在量化交易中非常重要,它具有以下含义和重要性:

保持策略的连续性:量化交易策略通常基于历史数据和指标进行计算和决策。如果在中断后丢失了这些已经计算的指标,就无法保持策略的连续性。通过恢复状态变量,可以确保策略在中断前后的计算基础保持一致,避免中断导致的断层和错误。

准确的交易决策:已经计算的指标在量化交易中对决策非常重要。这些指标可以用于识别市场趋势、波动性、买卖信号等。如果在中断后丢失了这些指标,就无法做出准确的交易决策。通过恢复状态变量,可以保证策略在中断前后继续使用这些指标进行决策,提高交易的准确性和效果。

避免重复计算:某些指标的计算可能比较耗时,例如复杂的统计指标或大量历史数据的计算。如果在中断后需要重新计算这些指标,将浪费时间和计算资源。通过恢复状态变量,可以避免重复计算已经计算过的指标,提高交易程序的效率和响应速度。

这里我们举例示范,一个需要状态变量恢复的策略。这一类的策略需要我们及时保存,策略运行过程中的状态变量,这些状态变量,会在策略运行过程中不断地更新,比如我们的老朋友,移动止盈止损类策略。这个策略需要及时的保存我们的买入价,盈利等级比例和止盈止损率,如果遇到实盘突然的停止,这三个变量也会丢失,因此对应的止盈止损的操作无法完成。

上节课我们作为策略状态恢复的演示,所以展示的策略比较简洁。本节课我们将细化我们的讲解细节,努力实现一个实盘级的应用策略。

相信在上一节课的基础上,我们对于这个的问题的解决思路,可以更加清晰。我们在先前的止盈止损策略中,设置了三个实时更新的变量,开仓价,盈利等级和止盈止损率,他们都会伴随策略的进度进行实时的更新。如果没有进行状态变量的保存的设计,实盘一旦停止,这样造成的后果就是无法在实盘停止后,恢复先前的进度和状态。由于之前没有进行状态变量的保存,当实盘停止后,这三个伴随策略进度更新的变量会重新初始化,而且无法记录之前已经进行到的步骤。这可能导致需要从头开始执行这个策略,丢失了之前的进展和计算结果。当然,持仓状态的恢复也是必须的,不然如果实时的仓位和保存的仓位不一致,可能会导致错误的操作或者损失。

首先我们设置我们的合约是最新的甲醛主力合约,然后是全局变量虚拟持仓mp,实时保存仓位getPos,和三个状态变量买入价buy_price,盈利等级率profitLevelRatio,和止盈止损比率stopLossRatio。

接下来我们就要CTP连接的检查。设置我们的合约。

前面的工作完成以后,我们来进入我们的第一个选择,是否要选择恢复策略进度。如果我们想重新初始化策略,可以选择不恢复策略进度,我们可以平掉我们的仓位,初始化状态变量,重启策略。因此我们设置了一个策略的参数,recovPro。

如果选择恢复策略进度,相对于上一节课,为了方便的展示,我们将持仓状态检查放在while循环中,这次我们将只在策略开头部分,分别进行持仓状态的检查和状态变量的获取。

首先是持仓状态的获取和与真实仓位的比较,首先我们调用_G(‘getPos’)查询是否保存有持仓状态变量,如果有将它赋值为getPos变量。然后我们获取我们的真实仓位realPos,利用我们上节课阐述的思路进行仓位的检查;根据真实仓位和保存仓位的一致状况,我们进行选择,如果仓位一致,继续运行策略;如果两个仓位不一致,可以选择红色警告,然后停止实盘;最后将虚拟持仓变量mp赋值为getPos.

接下来我们进行状态变量的恢复,同样先检查状态变量是否存在,不存在的话我们可以使用我们的初始设置变量;当然如果存在,使用_G进行状态变量的读取,然后分别使用索引0,1,2,恢复至保存的三个状态变量buy_price,profitLevelRatio和stopLossRatio。

这样,我们就可以在策略开头的部分完成持仓状态和状态变量的获取。

当然,不要忘了我们的第二个选择,不进行策略进度的恢复,也就是策略参数recovPro选择false。我们首先打印“重启实盘,状态变量重置” ,接着使用_G(null)清空所有状态变量,我们的目标品种仓位也要选择平掉。这样就可以解决,在上一步,如果出现仓位检查不一致的状况下,重新开启我们的策略。

在策略进度恢复设计完成以后,我们就要进入我们的策略主题循环部分了;

首先判断在连接前置机的状态下,否则我们的策略是无法运行的;

移动止盈止损类策略我们的思路还是一致的,伴随策略的进度,三个状态变量(开仓价,盈利等级和止盈止损率)也在实时的更新,重要的是最后我们要使用_G函数实时快照我们的状态变量,这样就可以在实盘遇到突发情况时,恢复我们的策略进度。

为了实时的展示这三个状态变量,我们首先整理成为table格式,然后使用LogStatus函数进行,这三个状态变量的实时展示。

这就是我们以前的移动止盈止损策略的加强版,它具有更多的灵活性,不仅可以实现策略的进度恢复,还增强了更多的容错能力。即使在实盘遭到暂停或其他干扰时,策略可以选择重启,也可以选择继续交易进度,提高了策略的实用性和交易稳定性。

我们在实盘中看下,首先我们设置不恢复策略进度,然后我们开启实盘,可以看到‘重启实盘,状态变量重置’字样,然后自动为我们进行了策略目标品种的平仓,和重置了状态变量的信息,接下来这个策略就可以顺利的进行;

然后我们停止策略,重新选择“恢复策略进度”,勾选为true,开启实盘,可以看到“开始恢复策略进度”字样,然后获取和检查实时的仓位信息,获取状态变量信息,呈现在状态栏中。

其实除了移动止盈止损类策略外,及时保存状态变量在大量的策略运行中非常重要的一步。比如套利策略,它是基于市场的价格差异进行交易,需要保存当前的套利机会、相关资产的价格和交易量等状态变量。在中断后,可以利用保存的状态变量来重新计算套利机会并执行相应的交易策略。

以上呢,就是状态变量恢复的一个简单设计,针对于不同类型的策略,我们可以选择不同的处理方法。希望本节课能给大家启发,帮助大家构建符合自己交易理念的状态变量恢复的设计。

29:策略进度恢复的设计(三)

前面两节课,我们学习了策略进度恢复中的持仓状态恢复和状态变量恢复,本节课我们来学习策略进度恢复设计中,数据恢复的应用:K线的存储和拼接。

数据的恢复

在交易过程中,数据源问题或中断可能会影响不同类型的策略。这里提供一个实际例子,说明数据恢复对于基于历史数据的策略的重要性:

假设有一个量化策略使用技术指标进行交易决策,其中包括计算移动平均线(MA),作为信号指标。该策略会根据短期MA和长期MA的交叉来进行买卖决策。

然而,在交易过程中,如果数据源出现问题或中断,导致历史数据缺失或不完整,那么计算移动平均线所需的数据将无法得到。这可能导致以下几种影响:

无法计算指标:策略所依赖的移动平均线指标无法计算,导致无法进行准确的买卖信号生成。

错误的信号判断:由于缺失了部分历史数据,可能导致移动平均线的计算结果与实际情况不符,从而导致信号判断错误。例如,如果缺失了最新的几个交易日的数据,可能导致移动平均线的计算结果滞后或不准确。

无法及时应对市场变化:如果数据源中断较长时间或数据缺失较多,策略可能错过了一些重要的市场变化或信号,无法及时调整仓位或执行交易操作。

为了解决这些问题,可以通过数据储存和恢复,来补齐缺失的历史数据,继续策略的顺利进行。

在优宽量化平台,在策略运行开始时,系统会为我们,提供了500根k线历史数据,方便我们使用历史数据,进行策略的运行。

注意: 需要指定固定的k线周期,比如1分钟,5分钟等等,才能拿到平台返回的k线数。

如果我们需要使用的k线周期是2000根。在策略开始的时候,我们需要等待1500根k线,加上平台开始提供的500根,需要收集够2000根k线以后,策略开始运行。而如果实盘一旦停掉再开启,我们需要再次等待1500根k线,才可以再次运行我们的策略。

这样确实很麻烦,有没有好的解决办法?这里同样可以使用我们的好帮手_G函数,帮助我们保留K线数据,然后等到策略重新开启的时候,就可以使用我们保留的k线,和优宽量化提供的k线进行衔接匹配,如果间隔的时间不长,就可以拼接我们所需要的k线数量,然后继续实盘的运行。

我们首先来学习下_G()函数怎么保存k线这种数据结构的。

什么情况下会需要2000根k线呢? 这对于小周期大参数的策略非常重要,例如我们想使用日线均线的波动,但是日线的k线不够灵敏,我们就可以使用小的分钟周期,大的均线参数,模拟分钟级日线的波动,可以更加灵敏。

function main() {
    //设定合约;
    symbol = 'rb2310';

    //rTbl数据结构对象
    var rTbl = {
        type: "table",
        title: "数据",
        cols: ["strTime", "Time", "High", "Open", "Low", "Close", "Volume"],
        rows: []
    };

    //时间戳数组
    var timezone = []; 

    //连接CTP
    while(!exchange.IO("status")) {
        LogStatus(_D(), "未连接CTP!");
        Sleep(1000);
    }
    
    //获取k线
    exchange.SetContractType(symbol);
    var r = _C(exchange.GetRecords);

    //依次添加k线,保留时间戳
    for (var i = 0; i < r.length; i++) {
        var bar = r[i];
        rTbl.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]);
        timezone.push(bar.Time);
    }
    Log('返回k线储存长度', rTbl.rows.length);

    //实时k线的保存
    while (true) {
        if (exchange.IO("status")) {
            LogStatus(_D(), "已经连接到CTP!\n", 'k线更新中');

            var r = _C(exchange.GetRecords);
            var bar = r[r.length - 2]; //添加倒数第二根,以防最新的k线没有走完

            if (!timezone.includes(bar.Time)) {
                rTbl.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]);
                timezone.push(bar.Time);
                _G('rTbl', rTbl);
                
                //实时验证保存效果
                rData = _G('rTbl');
                Log('k线储存时间戳: ',_D(rData.rows[rData.rows.length - 1][1]));
                Log('k线储存长度: ', rData.rows.length);
            }
        } else {
            LogStatus(_D(), "未连接CTP!");
        }

        Sleep(1000);

    }
}

首先,定义了一个名为symbol的变量,其值为字符串’rb2310’,表示期货合约名称。

然后,创建了一个名为rTbl的对象,它是一个表格的结构,用来保存k线结构的数据。rTbl对象有以下属性:

type:字符串,定义表格类型为"table"。
title:字符串,定义表格的标题为"数据"。
cols:数组,定义表格的列名为["strTime"具体的时间, "Time"时间戳, "High", "Open", "Low", "Close", "Volume"]。
rows:空数组,用于存储具体的k线数据。

接下来,定义了一个空数组timezone,用于存储已经出现过的时间戳。

因为在策略开始的时候,系统会一次性的返回500根的k线数据,我们可以进行添加。在连接CTP后,设置期货合约。然后获取k线数据,使用轮询依次进行数据的添加,和时间戳的保存。

在初始的500根历史k线保存完以后,我们使用while循环进行实时k线的添加。

首先检查CTP连接状态。

调用GetRecords()函数获取最新的K线数据。然后获取实时的k线bar,这里为了防止最新的k线没有走完,我们设置需要添加的k线bar是倒数第二根k线。

伴随k线的更新,使用if检查,当前时间戳bar.Time,是否在timezone数组中,如果不存在,表示是最新更新的k线,将k线数据,和最新的时间戳分别添加到rtbl和timezone当中。然后立即使用_G函数保存rTbl。

为了验证保存效果,这里我们使用_G函数进行保存k线的读取,打印最新保存的一根k线的时间戳,显示当前已保存的K线数量。

这个循环会不断从CTP获取最新的K线数据,并将新的K线数据添加到表格数据中。同时,它会记录已经出现过的时间戳,确保不会重复存储相同的K线数据。

请注意,这里只是实现了k线的一次性保存工作,并没有实现k线的永久保存,因为每次停止实盘,再开启,rTbl数组都会重新更新,它是一个空的数组,所以_G保存的始终是最新的k线数据,这里只是让大家了解下怎样使用_G保存K线结构的数据。

在实盘中可以看到,首先我们获取到了最新系统返回的500根k线数据,然后伴随while循环,最新的时间戳k线不断添加,这里可以看到,我们每次添加的都是倒数第二根k线数据。通过这样的方法,我们就实现了k线这种数据结构的实时保存。

下面我们来举一个完整的例子,来实现k线数据的保存和拼接的策略。

/*backtest
start: 2022-07-27 09:00:00
end: 2022-07-27 09:05:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
*/

function main() {
    var symbol = 'MA309';
    var rTbl = {
        type: "table",
        title: "数据",
        cols: ["strTime", "Time", "High", "Open", "Low", "Close", "Volume"],
        rows: []
    };

    var timezone = []; //实时k线时间戳
    var gtimezone = []; //保存k线时间戳

    //连接CTP
    while(!exchange.IO("status")) {
        LogStatus(_D(), "未连接CTP!");
        Sleep(1000);
    }
    
    exchange.SetContractType(symbol);
    var r = _C(exchange.GetRecords);

    //读取保存k线;
    var getRdata = _G('rTbl');

    //保存k线为空;
    if (getRdata == null) {
        Log('无存储数据');
        
        for (var i = 0; i < r.length; i++) {
            var bar = r[i];
            rTbl.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]);
            timezone.push(bar.Time);
        }

        Log('rTbl.rows储存长度', rTbl.rows.length);
        _G('rTbl', rTbl);
        Log('已储存新的r数据到rTbl中');
    }

    if (getRdata != null) {

        Log('已有存储数据');
        for (var j = 0; j < getRdata.rows.length; j++) {
            var gBartime = getRdata.rows[j][1];
            gtimezone.push(gBartime);
        }

        for (var i = 0; i < r.length; i++) {
            var bar = r[i];
            timezone.push(bar.Time);
        }

        var gLastTime = gtimezone[gtimezone.length - 1];
        var nLastTime = timezone[timezone.length - 1];

        Log('gtimezone时间', gLastTime);
        Log('timezone时间', nLastTime);

        if (gLastTime != nLastTime) {
            Log('k线有空缺,开始进行k线拼接');

            var overlapIndex = -1; //交叉索引flag;
            for (var k = timezone.length - 2; k >= 0; k--) {
                if (timezone[k] == gLastTime) {
                    overlapIndex = k;
                    Log('找到最后重叠时间节点:' + timezone[overlapIndex] + ',开始拼接');
                    break;
                }
            }

            Log('停止时间:', _D(timezone[overlapIndex]))
            Log('最新时间:', _D(timezone[timezone.length - 1]))
            Log('间隔时间:', (nLastTime - timezone[overlapIndex])/60000)

            if (overlapIndex === -1) {
                throw('实盘暂停时间过长,无法进行K线拼接');
            } else {
                for (var m = overlapIndex + 1; m < timezone.length; m++) { 
                    var bar = r[m];
                    getRdata.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]);
                }
                _G('rTbl', getRdata);
                Log('拼接长度:', getRdata.rows.length);
                Log('k线拼接已完成');
            }

        } else {
            Log('储存数据和k线数据已同步,不需要进行k线拼接');
        }
    }

    while (true) {
        if (exchange.IO("status")) {
            LogStatus(_D(), "已经连接到CTP!\n", '储存数据和k线数据同步工作已完成');
            var getRdata = _G('rTbl');

            var r = _C(exchange.GetRecords);
            var bar = r[r.length - 2]; //添加倒数第二根,以防最新的k线没有走完

            if (!timezone.includes(bar.Time)) {
                getRdata.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]);
                timezone.push(bar.Time);
                _G('rTbl', getRdata);
                Log('rTbl.rows更新长度', getRdata.rows.length);
            }

            //策略逻辑
            //...

        }else{
            LogStatus(_D(), "未连接CTP!");
        }
        Sleep(1000);
    }
}

相对于固定的持仓状态变量和状态变量,k线数据作为时间流数据,它是实时更新的。因此当实盘停止以后,再次运行策略,当拿到储存的k线数据的时候,是不能直接使用的,因为储存的k线和最新的k线之间,具有k线的缺失,就是实盘停止运行那段时间的缺失,因此我们使用我们储存的k线和实时返回的k线,需要进行k线的拼接,。

下面我们来讲下K线拼接的算法。首先我们讲解下思路。比如我们储存k线的时间戳是1,2,3,4,5,这五个时刻的k线数据,然而由于突发原因,实盘停止了6,7两个时刻,再重新开始策略,系统自动为我们返回了3,4,5,6,7这五个时刻的k线,因此我们需要找到这两者之间最后一个重叠的时间戳,也就是实盘停止的时刻,5,然后将最新返回的6,7这两个时刻的数据,也就是实盘停止的时间段,添加到我们存储的k线中,实现k线的拼接:时刻1到时刻7,接下来伴随新的k线的更新,最新的k线也不断的被存储起来。

思路了解以后,我们来使用代码进行实现,作为一个完整的算法,这个策略同时实现了k线的存储和拼接。

在策略的开始,对比于存储k线的策略,我们这里另外设置一个时间戳数组,gtimezone,代表储存的时间戳,然后同样的程序,连接CTP,设置好合约,获取k线。

为了进行k线的拼接,首先我们使用_G(‘rTbl’)获取保存的k线数据;

这时候,我们进入我们的第一个选择,查看保存的k线数据是否为空?

如果我们储存的k线数据getRdata是null,就要开始储存r的数据到空的数组tTbl中;和刚才保存k线的算法一样,使用for循环对每根k线进行添加,然后使用_G进行k线的保存。最后提示信息’已储存新的r数据到rTbl中’。

如果getrdata不是null,证明我们先前在策略中进行过k线的保存,可以展开下面的操作:

第一步,获取两个时间戳数组,分别是保存k线的时间戳gtimezone,和最新返回的k线的时间戳timezone;然后打印两个时间戳数组最新的时刻;我们的数据拼接工作,就是要根据这两个时间戳之间有没有差异进行更新;

第二步,这里使用if判断两个时间戳中最新的时刻,是否一致,如果一致,证明储存的数据和k线数据是同步的,我们不需要进行数据的拼接。 如果两个最新的时间戳不是一致的,证明我们保存的k线具有缺失的部分,这里我们需要进入一个新的选择,数据能否恢复呢?

第三步,这需要比较gtimezone和timezone有没有重叠的部分,如果没有就说明系统最新返回的数据和我们保存的数据的间隔时间过长,返回的500根k线没有完全涵盖全部的缺失时间,所以无法进行有效的拼接; 如果gtimezone和timezone有重叠的部分,证明最新返回的timezone涵盖了缺失的时间,我们就要进行拼接;

这里我们首先设置交叉索引flag,overlapIndex变量,初始值定义为-1,然后通过在timezone中逆向查询,如果在timezone中找到gtimezone最新的时刻gLastTime,证明找到最后重叠时间节点,可以进行拼接,这个时候将overlapIndex定义为这个元素的位置。

然后将timezone中重叠索引,也就是overlapIndex以后的k线数据,添加到getRdata中,这样就可以完成k线的拼接,不要忘了及时的保存,这个时候需要保存的是getRdata变量。

另外一种情况,在timezone中找不到和gtimezone重叠的元素,overlapIndex一直为-1,证明没有找到重叠的部分,无法进行拼接。

这里的的重点关键在于查找两个时间戳中的重叠部分和重叠的元素索引,具体的实现细节,大家可以在代码中找到解决的思路。

以上这些都是策略开始的时刻,需要进行的工作,当k线拼接工作完成以后,伴随策略进度的更新,在while循环中,首先读取保存的k线数据,然后通过检查时间戳进行最新k线的保存工作。这里需要保存的位置是getRdata。

然后我们可以开始我们策略的运行,这里需要一个小小的注意,我们策略需要使用的k线是getRdata,不是GetRecords返回的r数据。

我们在实盘中看下,可以看到在没有保存k线的情况下,首先开始保存系统返回的500根k线,然后伴随k线更新,新的k线不断的被保存;

然后我们进入第二种情况,停止实盘,造成保存的k线缺失;重启实盘,首先检查两个最新的时间戳是否一致,然后检查两个时间戳数组是否有重叠部分,如果有重叠部分,证明可以进行缺失k线的填充,提示信息:实盘停止时间,也就是保存的k线最后的时间,最新时间,也就是返回k线最新的时间,两者的时间间隔,就是我们要填充的k线数量,然后我们进行数据拼接的工作,完成后会显示拼接长度和“k线拼接已完成”;如果没有重叠部分,证明实时返回的k线和保存的k线中间间隔过大,我们就会throw错误,停掉实盘。

请注意,这个策略为了方便教学,因此个别地方写的比较繁琐,相信各位小伙伴们对这个策略一定有更好的优化方法,大家可以大胆的改动。

这样完成了使用_G函数实现了k线数组的保存和拼接工作。这样在实盘停止以后,我们可以连接先前保存的k线数据,和新的优宽量化平台提供的数据,确保策略的无缝衔接。

这种情况对于小周期大参数的策略非常有效,比如我们需要2000根k线,当我们收集够足够的k线以后,进行策略的运行;而实盘一旦停止,我们可以使用我们保存的k线和最新的返回k线进行时间的匹配,计算我们需要的信号,不需要额外的等待。再提醒一下哈,


更多内容