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

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

成准备工作后,终于来到我们开仓逻辑的设置。

这里有一个重要的变量,opcode代表操作代码,一共有四种操作,0:空闲,不进行任何的操作;1:开多,2:开空,3:全部平仓。

lastPrice是最新k线返回的价格,并赋值给obj对象。

如果当前海龟策略控制对象的加仓次数为0,就是没有没持仓。分别给止损价,离市价,上限和下限进行初始的赋值,然后使用for循环两次,用来检测2个突破系统的触发。如果是第一次循环,并且上次突破没有失败,就是成功突破,就需要跳过本次循环,这是为了避免重复进入条件不满足的情况。用 ? : 三元条件表达式,选择使用的突破系统参数,即当 i == 0 时 使用系统A,否则使用系统B。然后限制当前K线周期bar长度必须大于突破系统的入市周期加1。

接着计算enterPeriod周期内所有最高价的最大值定义highest变量,和最低价的最小值定义lowest变量。

然后取两次系统A和系统B获取的highest中最小的值,lowest中最大的值,定义为obj.status.upLine和downLine。

然后进行突破信号的判断,如果最新的价格向上突破对应周期内的最高价,opCode定义为1;如果最新的价格向下突破对应周期内的最低价,opCode定义为2。

如果opCode不等于0,就是存在突破情况,就会根据当前使用的突破系统确定离市周期obj.leavePeriod的值,并跳出循环。

这里的设计是很巧妙的,大家可以暂停一下,慢慢思考一下。

接下来,如果持有仓位的话,首先计算价差,计算单价盈亏,这里我们设置如果盈利的话,对于多仓和空仓都是负值,亏损的时候,设置为是正值,因为下面要和止损价的对比。

计算止损价,当做多的时候:止损价是比开仓价低的,所以使用开仓价减去,N值乘以止损系数;做空,使用用开仓价加上N值乘以止损系数。

然后检测单价盈亏是否大于设定的盈亏限制(就是止损系数 * N值),请注意,当亏损的时候,无论空仓还是多仓,spread都是正值,所以可以直接比较。如果spread大于设定的盈亏限制,则触发止损操作。此时,操作代码(opCode)赋值为3,表示触发了止损操作,并标记上次突破失败为真(obj.preBreakoutFailure = true)。然后st,代表止损次数,增加1。这里的日志信息里,最后是suffix符号,代表推送消息到app或者邮箱。在下面重要的信息里,我们也添加了这个符号。

spread当盈利的时候是负的,所以在第二个条件中,使用-spread判断和加仓系数乘以N值的大小,判断是否触发加仓操作,这里还有一个限制条件,目前的加仓次数需要小于最大加仓次数。根据持仓方向不同,操作代码赋值为1或2,表示进行加多仓还是加空仓的操作。

最后离场平仓的判断,是当opCode等于0,并且K线周期长度大于离市周期,可以计算离市价格,如果持仓方向为做多,则离市价格被设定为过去离市周期内最低价格,如果是空仓,离市价格是过去离市周期内最高价格。

接着判断触发,如果最新价格小于离市价格(多仓情况下),或者最新价格大于离市价格(空仓情况下),就会触发正常平仓操作。此时,操作代码赋值为3,并标记上次突破失败为假(obj.preBreakoutFailure = false)。status.cover正常平仓次数增加1。

下面的片段是程序的交易部分,用于根据opcode进行相应的交易操作。如果opCode为0,就会直接返回。如果opCode的值为3,表示需要进行平仓操作(包括正常的平仓和止损平仓)。在这种情况下,代码会调用”coverall”方法平掉该品种的所有仓位,并在平仓后重置相关变量,并记录平仓后的权益信息,显示到收益曲线。

如果opCode不是0或3,说明需要进行开仓或者加仓的操作。代码首先判断当前的加仓次数是否大于最大的加仓次数。接着计算了ATR(平均真实波幅)和确定了N值。

然后根据我们上节课讲过的公式计算开仓的手数,使用初始的金额减去预留保证金,然后乘以riskratio,在除以N,和合约乘数,就是一个头寸的大小单位。然后,获取账户信息的余额,预留保证金、做多仓或者空仓的保证金率,最新价格的1.2倍和合约的乘数,计算出可以开的手数。取两者之间的最小值,就是我们本次开仓的数量。

这个时候还需要做一个比较,如果计算出的单位手数小于合约允许的最小限价单手数,就要设置错误信息并返回。

如果单位手数符合要求,使用pushtask函数进行下单。成功下单后,代码会记录N值,开仓价openPrice,持有均价holdPrice,,持有数量holdAmount,如果是第一次开仓(marketPosition == 0),使用open记录下来开仓次数,如果是加仓,根据opCode为1或者2的时候,使用marketPosition记录加仓的数量。

最后使用status.vm记录下来不同合约的状态变量,并使用_G进行保存。

以上,就是使用程序化的语言将我们的海龟策略逻辑进行创建。这里的设计是很巧妙的,如果哪里理解的不是太清晰,大家可以暂停一下,慢慢思考这里的设计背后的原因。TTManager仅仅是创建了一个交易对象,至于具体的多品种的相关的参数设置,交易操作和执行,画图的展示等还需要进入我们的主函数中进行介绍,我们下节课再见。

35: 多品种海龟交易策略的设计(三)

上节课我们学习了海龟交易逻辑对象TTManager。作为一个交易逻辑对象的构造函数,整个的「海龟交易法则」用代码表达的部分都封装在这个对象中。下面我们在主函数中,来看怎样使用TTManager构造不同品种的海龟交易策略控制对象,并且运行海龟策略的。

function main() {
    SetErrorFilter("login|ready|流控|连接失败|初始|Timeout"); //过滤错误日志。参数值:字符串类型。被此正则表达式匹配的错误日志将不上传到日志系统,可多次调用设置多个过滤条件
    exchange.IO("mode", 0);
    while (!exchange.IO("status")) {
        Sleep(3000);
        LogStatus("正在等待与交易服务器连接, " + _D());
    }
    var positions = _C(exchange.GetPosition);
    if (positions.length > 0) {
        Log("检测到当前持有仓位, 系统将开始尝试恢复进度...");
        Log("持仓信息", positions);
    }
    Log("风险系数:", RiskRatio, "N值周期:", ATRLength, "系统1: 入市周期", EnterPeriodA, "离市周期", LeavePeriodA, "系统二: 入市周期", EnterPeriodB, "离市周期", LeavePeriodB, "加仓系数:", IncSpace, "止损系数:", StopLossRatio, "单品种最多开仓:", MaxLots, "次");
    

    var tts = [];
    var filter = [];
    var arr = Instruments.split(',');

    var initAccount = _q.GetAccount(exchange);
    var initMargin = JavaScriptON.parse(exchange.GetRawJavaScriptON()).CurrMargin;
    var realInitBalance = initAccount.Balance + initMargin;
    var keepBalance = _N(realInitBalance * (KeepRatio/100), 3);
    Log("当前资产信息", initAccount, "保留资金:", keepBalance);

    var arrChart = [];
    var index = 0;

    for (var i = 0; i < arr.length; i++) {
        var symbol = arr[i].replace(/^\s+/g, "").replace(/\s+$/g, "");
        if (typeof(filter[symbol]) !== 'undefined') {
            Log(symbol, "已经存在, 系统已自动过滤");
            continue;
        }
        filter[symbol] = true;
        var hasPosition = false;
        for (var j = 0; j < positions.length; j++) {
            if (positions[j].ContractType == symbol) {
                hasPosition = true;
                break;
            }
        }
        var obj = TTManager.New(hasPosition, symbol, realInitBalance, keepBalance, RiskRatio, ATRLength, EnterPeriodA, LeavePeriodA, EnterPeriodB, LeavePeriodB, IncSpace, StopLossRatio, MaxLots, index);
        index += 3;
        tts.push(obj);
        arrChart.push(obj.Chart);
    }

    chart = Chart(arrChart);
    chart.reset();

    var tblAssets = null;
    var nowAccount = null;
    var lastStatus = '';
    while (true) {
        while (!exchange.IO("status")) {
            Sleep(3000);
            LogStatus("正在等待与交易服务器连接, " + _D() + "\n" + lastStatus);
        }
        var tblStatus = {
            type: "table",
            title: "持仓信息",
            cols: ["合约名称", "持仓方向", "持仓均价", "持仓数量", "持仓盈亏", "加仓次数", "开仓次数", "止损次数", "成功次数", "当前价格", "N"],
            rows: []
        };
        var tblMarket = {
            type: "table",
            title: "运行状态",
            cols: ["合约名称", "合约乘数", "保证金率", "交易时间", "移仓次数", "柱线长度", "上线", "下线", "止损价", "离市价", "异常描述", "发生时间"],
            rows: []
        };
        var totalHold = 0;
        var vmStatus = {};
        var ts = new Date().getTime();
        var holdSymbol = 0;
        var tradingCount = 0;
        for (var i = 0; i < tts.length; i++) {
            tts[i].Poll();
            var d = tts[i].Status();
            if (d.holdAmount > 0) {
                vmStatus[d.symbol] = d.vm;
                holdSymbol++;
            }
            if (d.isTrading) {
                tradingCount++;
            }

            tblStatus.rows.push([d.symbolDetail.InstrumentID, d.holdAmount == 0 ? '--' : (d.marketPosition > 0 ? '多' : '空'), d.holdPrice, d.holdAmount, d.holdProfit, Math.abs(d.marketPosition), d.open, d.st, d.cover, d.lastPrice, d.N]);
            tblMarket.rows.push([d.symbolDetail.InstrumentID, d.symbolDetail.VolumeMultiple, _N(d.symbolDetail.LongMarginRatio, 4) + '/' + _N(d.symbolDetail.ShortMarginRatio, 4), (d.isTrading ? '是#0000ff' : '否#ff0000'), d.switchCount, d.recordsLen, d.upLine, d.downLine, d.stopPrice, d.leavePrice, d.lastErr, d.lastErrTime]);
            
            totalHold += Math.abs(d.holdAmount);
            tts[i].PlotRecords(chart);
        }

        var now = new Date();
        var elapsed = now.getTime() - ts;

        if (tradingCount > 0 || !nowAccount) {
            tblAssets = _q.GetAccount(exchange, true);
            nowAccount = _q.Account();

            if (tblAssets.rows.length > 10) {
                // replace AccountId
                tblAssets.rows[0] = ["InitAccount", "初始资产", realInitBalance];
            } else {
                tblAssets.rows.unshift(["NowAccount", "当前可用", nowAccount], ["InitAccount", "初始资产", realInitBalance]);
            }

            if (totalHold > 0) {
                tblAssets.rows.push(["手动恢复字符串", {body:JavaScriptON.stringify(vmStatus), colspan: 2}]);
            }
        }

        lastStatus = '`' + JavaScriptON.stringify([tblStatus, tblMarket, tblAssets]) + '`\n轮询耗时: ' + elapsed + ' 毫秒, 当前时间: ' + _D() + ', 星期' + 
                    ['日', '一', '二', '三', '四', '五', '六'][now.getDay()] + ", 持有品种个数: " + holdSymbol + ", 交易任务: " + _q.size();
        LogStatus(lastStatus);
        _q.poll();
        Sleep( 1000);
    }
}

来到我们的主函数,首先是内置函数SetErrorFilter,它用来过滤错误日志。参数值是字符串类型。当错误日志匹配到这个正则表达式的时候,将不会上传到日志系统,可多次调用设置多个过滤条件。这里我们过滤了一些常规错误,因此在日志信息里可以清晰明了的翻阅我们需要的信息。

设定行情模式为立即返回模式,参数填写为0。

检测我们的服务器连接状态,获取仓位的信息。如果检测到仓位的话,我们将会尝试恢复进度。

接下来就要创建不同合约的海龟交易逻辑对象。

创建tts数组,用于存储海龟交易策略控制对象;filter,这是过滤用的数组;arr,对合约列表使用逗号进行分隔,构成合约的数组。

对于TTManager对象,除去这些策略的外部参数,我们还需要进行一些参数的计算和设置。

这里是计算初始资金realInitBalance和预留保证金keepBalance参数。使用GetAccount获取账户信息,调用GetRawJavaScriptON函数获取”CurrMargin”代表”当前保证金总额”,然后使用账户可用资金加上当前保证金,就是真正的初始资金realInitBalance,再乘以预留保证金比例,就是keepBalance参数。

还有画图需要的参数,arrChart存储不同合约图表的对象,index是不同合约的索引,用来给不同品种的图表数据进行按顺序的添加,初始值我们设置为0,然后伴随不同的合约进行更新。

接下来我们进入循环,进行不同品种的海龟交易对象的创建。

使用for循环遍历分隔后的数组,首先正则表达式匹配,可以保证品种名称的格式正确,消除不必要的空格,使其符合代码逻辑的要求。接下来进行品种的过滤,这是为了避免,重复创建相同品种的海龟交易策略控制对象。通过检查过滤数组filter中是否已存在名为symbol的属性。如果存在,表示该品种已经处理过,直接跳过该品种的处理。如果过滤数组filter中不存在名为symbol的属性,则将symbol添加到过滤数组中,设置属性值为true,表示该品种已经处理过。

然后初始化hasPosition变量,false代表没有持仓。遍历获取到的持仓信息,如果有持仓信息合约名称和symbol一样的,给hasPosition赋值true代表有持仓。这时候,TTManager.New所有需要的参数我们都已经获取到,创建obj,添加进入tts数组中。图表对象也添加进入arrChart数组。当一个合约添加完成以后,这个时候需要更新index,因为每个合约图表对象有三组数据(k线,离场价格和止损价格),所以index递增3。

在这里我们创建图表对象。

然后我们创建tblAssets,nowAccount,和lastStatus的初始变量,分别用来存储资产信息的表格,当前账户可用资金和上一次状态信息。这三个变量用来跟踪和记录状态信息,在while循环中进行使用和更新。

在不同合约的海龟交易逻辑对象创建完成以后,我们就要带入主循环进行交易策略的运行。

来到我们的主循环,这里我们不仅要运行我们的交易逻辑,还有很多的重点将放在如何全面的,及时的展现和更新该策略的运行进度和运行状态。

首先检测交易服务器连接状态;然后定义两个对象tblStatus和tblMarket,它们是用于存储持仓信息的表格,包括合约名称、持仓方向、持仓均价、持仓数量、持仓盈亏、加仓次数、开仓次数、止损次数、成功次数(也就是正常平仓次数)、当前价格和N值,和运行状态的表格数据,包括合约名称、合约乘数、保证金率、交易时间、移仓次数、柱线长度、上线、下线、止损价、离市价、异常描述和发生时间。

下面继续创建一些变量:

totalHold:表示当前持仓数量的总和。

vmStatus:用于存储持仓品种的持仓状态信息。

ts:代表一个时间戳,用于记录当前循环开始的时间。它通过new Date().getTime()获取当前时间的毫秒数来初始化。

holdSymbol:记录当前持有仓位的品种数量。

tradingCount:表示当前正在进行交易的品种数量。

接下来对不同品种的海龟交易策略控制对象的数组tts进行操作。首先调用每个合约的海龟管理对象的Poll方法,进行交易的操作;然后调用对象的Status()方法,获取该对象的当前交易状态,并将其赋值给变量d。

如果当前对象有持仓,就会给空对象vmStatus,将 d.symbol 对应的值赋给 vmStatus 对象中的对应属性,接着持有的合约品种数量holdSymbol也会累计。

如果变量d的isTrading为真,那么将tradingCount自增1。

接着分别使用状态的信息更新tblStatus和tblMarket表格数据,

这里是累加所有品种的持仓数量的绝对值,用来计算总持仓数量totalHold。

调用tts[i]对象的PlotRecords()方法,将不同合约的图表数据进行添加和更新。

为了展示策略的运行效率,我们计算了轮询的间隔elapsed,使用轮询后的时间now减去轮询前的时间ts。

下面是更新资产信息表格tblAssets。 如果存在正在交易的品种(tradingCount > 0)或者nowAccount变量为空(即第一次循环),就会调用_q.GetAccount(exchange, true)获取账户的资产信息,并将结果赋值给tblAssets变量。

然后调用_q.Account()获取当前账户的可用资金,并将结果赋值给nowAccount变量。

这里我们调整了一下tblAssets,如果获取的表格的行数大于10,那么设置索引0的行数为初始资金信息,否则的话,在图表开始的位置插入当前可用资产,和初始的资产。

接下来我们进行”手动恢复字符串”的设置。如果存在持仓(totalHold > 0),将”手动恢复字符串”行信息添加到tblAssets表格中。该行包含不同持仓品种的持仓状态信息,通过JavaScriptON.stringify(vmStatus)将其转换为字符串,并设置colspan为2,用来跨越两列。如果我们选择手动恢复模式,可以使用这里的字符串。

我们整理状态栏的数据为lastStatus变量,包括三个图表。还有一行信息显示当前策略执行情况和状态(包括轮询耗时,当前时间,持有交易品种holdSymbol,和交易的任务数量)。使用LogStatus进行状态栏展示。

最后pushtaskpoll()函数大家不要遗忘。

在回测系统里,使用这五个品种我们模拟运行了多品种的海龟策略。我们看到持仓信息,运行状态和账户信息三个图表,和最后一行的策略状态信息的实时概览。另外,还有收益曲线的展示和各个合约的实时k线图,止损价和离市价的数据展示,这些图表和信息提供了全方位和多维度的视角,帮助我们深入了解策略的运行状态、资金情况和收益表现,方便我们进行策略优化和调整。

在本节课中,我们继续介绍策略主函数的设置和操作执行,更清晰地了解整个策略的运行逻辑。此外,我们还使用图表展示交易进度和账户资产信息等,使得策略的运行过程更加直观。通过这些完善的设计,希望能够有效地帮助大家理解多品种海龟交易策略。

海龟交易逻辑并不复杂,但是怎样将这种逻辑使用程序的语言一步步搭建起来,确实考验我们的能力和耐心,我们要做好各个品种,各个交易环节,程序的容错以及结果呈现等一系列的工作,希望通过这三节课的学习,可以帮助大家了解一下一个真正的实盘级别的大模型应该怎样搭建。当然,这对比于一个工业级的实盘大模型确实比较稚嫩,这里重要的是,理解并掌握这里的程序设计,学会一个复杂模型的搭建方式,希望大家都有所领会。


更多内容