商品期貨多品種均線策略

均線策略作爲最簡單的趨勢策略,通常是程序化、量化交易入門的必修課。本篇文章我們不探究策略原理,我們從策略設計層面入手,剖析一個多品種策略的架構設計,學習一些策略架構設計的經驗。

多品種策略設計的優點在於使用方便,一個策略程序控制交易多個品種,可以統一信息狀態顯示。交易多個品種相對分散了風險,增加了交易機會。缺點在於設計比較複雜,各個品種之間不能相互影響,對程序執行效率要求比較高。所以設計難度遠大於設計一個單品種策略。第一種方式比較簡單,在發明者量化交易平臺可以使用「商品期貨交易類庫中的CTA函數」輕鬆實現多品種策略。第二種方式就是直接寫策略,好在發明者量化交易平臺上提供了大量策略範例,給我們提供了豐富的參考代碼,設計思路。

我們就使用「CTP商品期貨多品種海龜交易策略」策略作爲參考,修改成我們需要的多品種策略,從簡單入手,比如修改成一個多品種均線策略。

節選部分代碼:

/*backtest
start: 2019-07-01 09:00:00
end: 2020-03-25 15:00:00
period: 1d
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
*/

var _bot = $.NewPositionManager();

var Manager = {
    New: function(needRestore, symbol, keepBalance, fastPeriod, slowPeriod) {
        var symbolDetail = _C(exchange.SetContractType, symbol);
        if (symbolDetail.VolumeMultiple == 0 || symbolDetail.MaxLimitOrderVolume == 0 || symbolDetail.MinLimitOrderVolume == 0 || symbolDetail.LongMarginRatio == 0 || symbolDetail.ShortMarginRatio == 0) {
            Log(symbolDetail);
            throw "合約信息異常";
        } else {
            Log("合約", symbolDetail.InstrumentName, "一手", symbolDetail.VolumeMultiple, "份, 最大下單量", symbolDetail.MaxLimitOrderVolume, "保證金率:", _N(symbolDetail.LongMarginRatio), _N(symbolDetail.ShortMarginRatio), "交割日期", symbolDetail.StartDelivDate);
        }

        // 聲明枚舉量
        var ACT_IDLE = 0;
        var ACT_LONG = 1;
        var ACT_SHORT = 2;
        var ACT_COVER = 3;


        var ERR_SUCCESS = 0;
        var ERR_SET_SYMBOL = 1;
        var ERR_GET_ORDERS = 2;
        var ERR_GET_POS = 3;
        var ERR_TRADE = 4;
        var ERR_GET_DEPTH = 5;
        var ERR_NOT_TRADING = 6;
        var errMsg = ["成功", "切換合約失敗", "獲取訂單失敗", "獲取持倉失敗", "交易下單失敗", "獲取深度失敗", "不在交易時間"];

        // 構造對象
        var obj = {
            symbol: symbol,
            keepBalance: keepBalance,
            // 設置策略相關參數
            fastPeriod: fastPeriod,
            slowPeriod: slowPeriod
        };

        // 初始化任務對象
        obj.task = {
            action: ACT_IDLE,
            // 篇幅太長,省略部分
            ...
        }

        obj.lastPrice = 0;
        obj.symbolDetail = symbolDetail;

        // 持倉狀態信息
        obj.status = {
            symbol: symbol,
            recordsLen: 0,
            vm: [],
            open: 0,
            cover: 0,
            st: 0,
            marketPosition: 0,
            lastPrice: 0,
            holdPrice: 0,
            holdAmount: 0,
            holdProfit: 0,

            symbolDetail: symbolDetail,
            lastErr: "",
            lastErrTime: "",
            // 可以增加一些顯示的數值屬性
            
            isTrading: false
        };

        // 設置錯誤的功能函數
        obj.setLastError = function(err) {
            if (typeof(err) === 'undefined' || err === '') {
                obj.status.lastErr = "";
                obj.status.lastErrTime = "";
                return;
            }
            var t = new Date();
            obj.status.lastErr = err;
            obj.status.lastErrTime = t.toLocaleString();
        };

        // 恢複函數
        obj.reset = function(marketPosition) {
            if (typeof(marketPosition) !== 'undefined') {
                obj.marketPosition = marketPosition;
                // 例如多品種海龜策略,需要恢復N值,入市週期、離市週期等程序運行中的數據,可以在此處恢復,如不清楚,可以參考原版多品種海龜策略此處的代碼
                
                var pos = _bot.GetPosition(obj.symbol, marketPosition > 0 ? PD_LONG : PD_SHORT);
                if (pos) {
                    obj.holdPrice = pos.Price;
                    obj.holdAmount = pos.Amount;
                    Log(obj.symbol, "倉位", pos);
                } else {
                    throw "恢復" + obj.symbol + "的持倉狀態出錯, 沒有找到倉位信息";
                }
                
                // 可以根據策略,打印一些恢復後的數據,參考原版多品種海龜策略
                Log("恢復", obj.symbol, "持倉均價:", obj.holdPrice, "持倉數量:", obj.holdAmount);
                obj.status.vm = [obj.marketPosition];
            } else {
                obj.marketPosition = 0;
                obj.holdPrice = 0;
                
                obj.holdAmount = 0;
                obj.holdProfit = 0;
            }
            obj.holdProfit = 0;
            obj.lastErr = "";
            obj.lastErrTime = "";
        };

        // 更新狀態,返回用於顯示的狀態數據
        obj.Status = function() {
            
            obj.status.marketPosition = obj.marketPosition;
            obj.status.holdPrice = obj.holdPrice;
            obj.status.holdAmount = obj.holdAmount;
            obj.status.lastPrice = obj.lastPrice;
            if (obj.lastPrice > 0 && obj.holdAmount > 0 && obj.marketPosition !== 0) {
                // 計算收益
                obj.status.holdProfit = _N((obj.lastPrice - obj.holdPrice) * obj.holdAmount * symbolDetail.VolumeMultiple, 4) * (obj.marketPosition > 0 ? 1 : -1);
            } else {
                obj.status.holdProfit = 0;
            }
            return obj.status;
        };

        // 處理交易的邏輯層面,設置交易任務的函數
        obj.setTask = function(action, amount, onFinish) {
            // 篇幅太長省略
            ...
        };

        // 處理交易任務的函數
        obj.processTask = function() {
            // 篇幅太長省略
            ...  
        };

        // 策略邏輯執行函數
        obj.Poll = function(subroutine) {
        	// 判斷交易時段
            obj.status.isTrading = $.IsTrading(obj.symbol);
            if (!obj.status.isTrading) {
                return;
            }

            // 執行下單交易任務
            if (obj.task.action != ACT_IDLE) {
                var retCode = obj.processTask();
                if (obj.task.action != ACT_IDLE) {
                    obj.setLastError("任務沒有處理成功: " + errMsg[retCode] + ", " + obj.task.desc + ", 重試: " + obj.task.retry);
                } else {
                    obj.setLastError();
                }
                return;
            }
            // 調用Poll時如果設置了subroutine參數,只運行到此處,這個是程序設計的一個小技巧
            if (typeof(subroutine) !== 'undefined' && subroutine) {
                return;
            }

            // 根據參數設置微信推送
            var suffix = WXPush ? '@' : '';
            // switch symbol
            _C(exchange.SetContractType, obj.symbol);

            // 獲取K線數據
            var records = exchange.GetRecords();
            if (!records) {
                obj.setLastError("獲取K線失敗");
                return;
            }
            obj.status.recordsLen = records.length;

            if (records.length < (obj.fastPeriod + 2) || records.length < (obj.slowPeriod +2)) {
                obj.setLastError("K線長度小於 均線週期:" + obj.fastPeriod + "或" + obj.slowPeriod);
                return;
            }

            var opCode = 0; // 0: IDLE 空閒, 1: LONG 做多, 2: SHORT 做空, 3: CoverALL 當前合約全部平倉。opCode 是操作碼,以下策略邏輯檢測到條件後設置對應的操作碼
            var lastPrice = records[records.length - 1].Close;
            obj.lastPrice = lastPrice;

            var fastMA = TA.EMA(records, obj.fastPeriod)
            var slowMA = TA.EMA(records, obj.slowPeriod)
            
            // 策略邏輯
            if (obj.marketPosition === 0) {         // 不持倉時
                // 根據交易邏輯賦值opCode信號,程序後續根據信號處理
                if(fastMA[fastMA.length - 3] < slowMA[slowMA.length - 3] && fastMA[fastMA.length - 2] > slowMA[slowMA.length - 2]) {           // long   均線金叉做多
                    opCode = 1
                } else if (fastMA[fastMA.length - 3] > slowMA[slowMA.length - 3] && fastMA[fastMA.length - 2] < slowMA[slowMA.length - 2]) {   // short  均線死叉做空
                    opCode = 2
                }
            } else {                                 // 持倉時
                if(obj.marketPosition < 0 && fastMA[fastMA.length - 3] < slowMA[slowMA.length - 3] && fastMA[fastMA.length - 2] > slowMA[slowMA.length - 2]) {           // 持空頭倉位,金叉,平倉
                    opCode = 3
                } else if (obj.marketPosition > 0 && fastMA[fastMA.length - 3] > slowMA[slowMA.length - 3] && fastMA[fastMA.length - 2] < slowMA[slowMA.length - 2]) {   // 持多頭倉位,死叉,平倉
                    opCode = 3
                }
                
                // 可以設計更加複雜的交易邏輯,加倉、減倉、止損、止盈等,可以參考原版多品種海龜策略
            }
            
            // 如果不觸發任何條件,操作碼爲0,返回
            if (opCode == 0) {
                return;
            }

            // 執行平倉
            if (opCode == 3) {
                obj.setTask(ACT_COVER, 0, function(ret) {   // 設置回調,清空保存的持久化數據
                    obj.reset();
                    _G(obj.symbol, null);
                });
                return;
            }

            // 參考原版多品種海龜交易策略
            /*
            if (Math.abs(obj.marketPosition) >= obj.maxLots) {
                obj.setLastError("禁止開倉, 超過最大持倉 " + obj.maxLots);
                return;
            }
            */

            var account = _bot.GetAccount();
            var canOpen = parseInt((account.Balance-obj.keepBalance) / (opCode == 1 ? obj.symbolDetail.LongMarginRatio : obj.symbolDetail.ShortMarginRatio) / (lastPrice * 1.2) / obj.symbolDetail.VolumeMultiple);
            var unit = Math.min(1, canOpen);    // 開倉數量處理

            // 設置交易任務
            obj.setTask((opCode == 1 ? ACT_LONG : ACT_SHORT), unit, function(ret) {
                if (!ret) {
                    obj.setLastError("下單失敗");
                    return;
                }

                // 下單成功後,打印一些信息,保存一些數據,參考原版多品種海龜交易策略
                
                obj.holdPrice = ret.position.Price;
                obj.holdAmount = ret.position.Amount;
                obj.marketPosition += opCode == 1 ? 1 : -1;
                obj.status.vm = [obj.marketPosition];
                _G(obj.symbol, obj.status.vm);
            });
        };

        // 對象構造函數New函數的其它處理工作
        var vm = null;
        if (RMode === 0) {
            vm = _G(obj.symbol);
        } else {
            vm = JSON.parse(VMStatus)[obj.symbol];
        }
        if (vm) {
            Log("準備恢復進度, 當前合約狀態爲", vm);
            // 恢復進度,和reset函數傳入參數相關,當前策略只有一個持倉參數,可以參考原版多品種海龜策略加以比較學習
            obj.reset(vm[0]);
        } else {
            if (needRestore) {
                Log("沒有找到" + obj.symbol + "的進度恢復信息");
            }
            obj.reset();
        }
        return obj;
    }
};

function onexit() {
    Log("已退出策略...");
}

function main() {
    if (exchange.GetName().indexOf('CTP') == -1) {
        throw "只支持商品期貨CTP";
    }
    SetErrorFilter("login|ready|流控|連接失敗|初始|Timeout");
    var mode = exchange.IO("mode", 0);
    if (typeof(mode) !== 'number') {
        throw "切換模式失敗, 請更新到最新託管者!";
    }
    while (!exchange.IO("status")) {
        Sleep(3000);
        LogStatus("正在等待與交易服務器連接, " + new Date());
    }
    var positions = _C(exchange.GetPosition);
    if (positions.length > 0) {
        Log("檢測到當前持有倉位, 系統將開始嘗試恢復進度...");
        Log("持倉信息", positions);
    }
    // 可以打印一些策略設置的參數

    var initAccount = _bot.GetAccount();
    var initMargin = JSON.parse(exchange.GetRawJSON()).CurrMargin;
    var keepBalance = _N((initAccount.Balance + initMargin) * (KeepRatio/100), 3);
    Log("資產信息", initAccount, "保留資金:", keepBalance);
    
    var tts = [];
    var filter = [];
    var arr = Instruments.split(',');
    var arrFastPeriod = FastPeriodArr.split(',')
    var arrSlowPeriod = SlowPeriodArr.split(',')
    if (arr.length != arrFastPeriod.length || arr.length != arrSlowPeriod.length) {
        throw "均線週期參數與添加合約數量不匹配,請檢查參數!"
    }
    for (var i = 0; i < arr.length; i++) {
        var symbol = arr[i].replace(/^\s+/g, "").replace(/\s+$/g, "");
        if (typeof(filter[symbol]) !== 'undefined') {
            throw symbol + "已經存在,請檢查參數!"
        }
        filter[symbol] = true;
        var hasPosition = false;
        for (var j = 0; j < positions.length; j++) {
            if (positions[j].ContractType == symbol) {
                hasPosition = true;
                break;
            }
        }
        var fastPeriod = parseInt(arrFastPeriod[i])
        var slowPeriod = parseInt(arrSlowPeriod[i])
        var obj = Manager.New(hasPosition, symbol, keepBalance, fastPeriod, slowPeriod)
        tts.push(obj);
    }
    
    var preTotalHold = -1;
    var lastStatus = '';
    while (true) {
        if (GetCommand() === "暫停/繼續") {
            Log("暫停交易中...");
            while (GetCommand() !== "暫停/繼續") {
                Sleep(1000);
            }
            Log("繼續交易中...");
        }
        while (!exchange.IO("status")) {
            Sleep(3000);
            LogStatus("正在等待與交易服務器連接, " + new Date() + "\n" + lastStatus);
        }

        var tblStatus = {
            type: "table",
            title: "持倉信息",
            cols: ["合約名稱", "持倉方向", "持倉均價", "持倉數量", "持倉盈虧", "加倉次數", "當前價格"],
            rows: []
        };
        var tblMarket = {
            type: "table",
            title: "運行狀態",
            cols: ["合約名稱", "合約乘數", "保證金率", "交易時間", "柱線長度", "異常描述", "發生時間"],
            rows: []
        };
        var totalHold = 0;
        var vmStatus = {};
        var ts = new Date().getTime();
        var holdSymbol = 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++;
            }
            tblStatus.rows.push([d.symbolDetail.InstrumentName, d.holdAmount == 0 ? '--' : (d.marketPosition > 0 ? '多' : '空'), d.holdPrice, d.holdAmount, d.holdProfit, Math.abs(d.marketPosition), d.lastPrice]);
            tblMarket.rows.push([d.symbolDetail.InstrumentName, d.symbolDetail.VolumeMultiple, _N(d.symbolDetail.LongMarginRatio, 4) + '/' + _N(d.symbolDetail.ShortMarginRatio, 4), (d.isTrading ? '是#0000ff' : '否#ff0000'), d.recordsLen, d.lastErr, d.lastErrTime]);
            totalHold += Math.abs(d.holdAmount);
        }

        // 顯示狀態欄信息
        var now = new Date();
        var elapsed = now.getTime() - ts;
        var tblAssets = _bot.GetAccount(true);
        var nowAccount = _bot.Account();
       
        if (tblAssets.rows.length > 10) {
            // replace AccountId
            tblAssets.rows[0] = ["InitAccount", "初始資產", initAccount];
        } else {
            tblAssets.rows.unshift(["NowAccount", "當前可用", nowAccount], ["InitAccount", "初始資產", initAccount]);
        }
        lastStatus = '`' + JSON.stringify([tblStatus, tblMarket, tblAssets]) + '`\n輪詢耗時: ' + elapsed + ' 毫秒, 當前時間: ' + now.toLocaleString() + ', 星期' + ['日', '一', '二', '三', '四', '五', '六'][now.getDay()] + ", 持有品種個數: " + holdSymbol;
        if (totalHold > 0) {
            lastStatus += "\n手動恢復字符串: " + JSON.stringify(vmStatus);
        }
        LogStatus(lastStatus);
        if (preTotalHold > 0 && totalHold == 0) {
            LogProfit(nowAccount.Balance - initAccount.Balance - initMargin);
        }
        preTotalHold = totalHold;
        Sleep(LoopInterval * 1000);
    }
}

策略參數:
/upload/asset/16f89718df7233d465a5.png

和原版「CTP商品期貨多品種海龜交易策略」一樣,該策略也要引用商品期貨交易類庫。

策略代碼比較長,每段關鍵位置,都有註釋,說明了這段代碼的用途。耐心看完對比下原版的「CTP商品期貨多品種海龜交易策略」會發現,其實沒有什麼改動,僅僅是修改了策略交易部分的邏輯代碼,策略其他部分代碼完全複用了。這不得不說得益於原版策略設計的巧妙,讓策略交易邏輯和策略下單處理邏輯等其它與策略不相關的功能代碼分離的很好。這些代碼耦合很低,所以非常容易修改(前提是通讀過策略,完全理解策略架構之後)。其實完全可以把原版策略中和交易策略相關的內容分離出來刪除掉,只留下一個多品種策略框架,就可以根據自己的需求隨意開發了。

隨便設置了一組參數,回測最近的行情:

策略地址:https://www.fmz.com/strategy/193043

策略僅供參考學習,實盤慎用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章