該策略是好久之前適用數字貨幣796期貨交易所的一個策略,期貨合約是幣本位,即保證金扣除的是幣(例如BTC合約扣除BTC),合約下單量能夠是小數,相似幣安的幣本位合約。
該策略拿出來學習策略設計,邏輯處理仍是很不錯的,策略以學習爲主。javascript
策略代碼註釋
var FirstTradeType = [ORDER_TYPE_BUY, ORDER_TYPE_SELL][OpType]; // 首次開倉方向,根據參數OpType開倉方向肯定,全局變量FirstTradeType的值爲ORDER_TYPE_BUY或者ORDER_TYPE_SELL var OrgAccount = null; // 全局變量,記錄帳戶資產 var Counter = {s : 0, f: 0}; // 聲明一個變量Counter,值初始化爲一個對象(相似的結構python叫字典),s表明勝的次數,f表明負的次數 var LastProfit = 0; // 最近盈虧 var AllProfit = 0; // 總盈虧 var _Failed = 0; // 止損次數 // 取消掛單列表中某個ID的訂單以外的全部訂單,當不傳orderId參數時,取消當前交易對全部掛單,參數e爲交易所對象的引用,例如傳入exchange做爲參數,此刻e就是exchange的別名 function StripOrders(e, orderId) { var order = null; // 初始化變量 order 爲空 if (typeof(orderId) == 'undefined') { // 若是參數 orderId 傳入時,沒有寫,則typeof(orderId) == 'undefined'成立,執行if語句的代碼塊,orderId 賦值爲null orderId = null; } while (true) { // 處理循環 var dropped = 0; // 處理標記次數 var orders = _C(e.GetOrders); // 調用GetOrders獲取當前掛單(未徹底成交的訂單),賦值給orders for (var i = 0; i < orders.length; i++) { // 遍歷未成交訂單列表 orders if (orders[i].Id == orderId) { // 若是訂單ID和參數上傳入的訂單ID orderId 相同則給函數內局部變量order賦值orders[i],orders[i]即遍歷時當前的訂單結構 order = orders[i]; } else { // 若是ID不相同,執行撤銷操做 var extra = ""; // 根據部分紅交狀況,設置擴展信息extra if (orders[i].DealAmount > 0) { extra = "成交: " + orders[i].DealAmount; } else { extra = "未成交"; } e.SetDirection(orders[i].Type == ORDER_TYPE_BUY ? "buy" : "sell"); e.CancelOrder(orders[i].Id, orders[i].Type == ORDER_TYPE_BUY ? "買單" : "賣單", extra); // 撤單操做,附帶輸出extra信息,在日誌上會顯示 dropped++; // dropped 計數累計 } } if (dropped == 0) { // 當遍歷完成時,dropped 等於0,即遍歷時沒有一次撤銷處理(沒有須要撤銷的訂單了),即爲撤銷處理工做完成,跳出while循環 break; } Sleep(300); // 防止輪轉頻率過快,每次間隔必定時間 } return order; // 返回要查找的訂單order } var preMsg = ""; // 記錄緩存信息的變量 function GetAccount(e, waitFrozen) { // 獲取帳戶資產信息,參數e亦是exchange的引用,參數waitFrozen控制是否等待凍結 if (typeof(waitFrozen) == 'undefined') { // 若是調用時不傳入waitFrozen參數,給參數waitFrozen賦值false,即默認不等待凍結 waitFrozen = false; } var account = null; var alreadyAlert = false; // 標記是否已經提醒過的變量 while (true) { // 獲取當前帳戶信息,檢測凍結,若是不等待凍結,則會直接跳出while循環 account = _C(e.GetAccount); if (!waitFrozen || account.FrozenStocks < MinStock) { break; } if (!alreadyAlert) { alreadyAlert = true; // 觸發提醒一次,就重置alreadyAlert,避免重複不停的提醒 Log("發現帳戶有凍結的錢或幣", account); // 輸出提醒日誌 } Sleep(Interval); } msg = "成功: " + Counter.s + " 次, 失敗: " + Counter.f + " 次, 當前帳戶 幣: " + account.Stocks; if (account.FrozenStocks > 0) { msg += " 凍結的幣: " + account.FrozenStocks; } if (msg != preMsg) { // 檢測當前信息是否和上次信息不一樣,不一樣的話更新在狀態欄上 preMsg = msg; LogStatus(msg, "#ff0000"); } return account; // 函數返回帳戶信息 account結構 } function GetPosition(e, orderType) { // 獲取持倉,或者獲取指定方向的持倉 var positions = _C(e.GetPosition); // 獲取持倉 if (typeof(orderType) == 'undefined') { // orderType 參數爲指定要獲取的持倉類型,若是沒有傳入orderType參數,直接返回全部持倉 return positions; } for (var i = 0; i < positions.length; i++) { // 遍歷持倉列表 if (positions[i].Type == orderType) { // 若是當前遍歷的持倉數據是須要找的方向(orderType) return positions[i]; // 返回 orderType 類型的持倉 } } return null; } function GetTicker(e) { // 獲取ticker 行情數據 while (true) { var ticker = _C(e.GetTicker); // 獲取tick行情 if (ticker.Buy > 0 && ticker.Sell > 0 && ticker.Sell > ticker.Buy) { // 檢查行情數據可靠性 return ticker; // 返回 ticker數據 } Sleep(100); } } // mode = 0 : direct buy, 1 : buy as buy1 function Trade(e, tradeType, tradeAmount, mode, slidePrice, maxSpace, retryDelay) { // 交易函數 // e 交易所對象引用, tradeType 交易方向(買/賣), tradeAmount 交易數量, mode 交易模式, slidePrice 滑價, maxSpace 最大掛單距離, retryDelay 重試時間間隔 var initPosition = GetPosition(e, tradeType); // 獲取指定方向的持倉數據,記做 initPosition var nowPosition = initPosition; // 聲明另外一個變量nowPosition 用initPosition賦值 var orderId = null; var prePrice = 0; // 上次循環時的下單價格 var dealAmount = 0; // 已經交易的數量 var diffMoney = 0; var isFirst = true; // 循環首次執行標記 var tradeFunc = tradeType == ORDER_TYPE_BUY ? e.Buy : e.Sell; // 下單函數,根據參數 tradeType 而定是調用 e.Buy 仍是 e.Sell var isBuy = tradeType == ORDER_TYPE_BUY; // 是不是買入的標記 while (true) { // while循環 var account = _C(e.GetAccount); // 獲取當前帳戶資產數據 var ticker = GetTicker(e); // 獲取當前行情數據 var tradePrice = 0; // 根據 mode 參數制定交易價格 if (isBuy) { tradePrice = _N((mode == 0 ? ticker.Sell : ticker.Buy) + slidePrice, 4); } else { tradePrice = _N((mode == 0 ? ticker.Buy : ticker.Sell) - slidePrice, 4); } if (orderId == null) { if (isFirst) { // 根據 isFirst 標記變量判斷,若是是第一次執行,什麼都不作 isFirst = false; // isFirst 標記設置爲false ,表明已經不是第一次執行 } else { // 非第一次執行,更新持倉數據 nowPosition = GetPosition(e, tradeType); } dealAmount = _N((nowPosition ? nowPosition.Amount : 0) - (initPosition ? initPosition.Amount : 0), 6); // 根據最初的持倉數據和當前的持倉數據,計算已經成交的數量 var doAmount = Math.min(tradeAmount - dealAmount, account.Stocks * MarginLevel, 4); // 根據已經成交的數量、帳戶可用資產,計算剩餘須要交易的數量 if (doAmount < MinStock) { // 若是算出的交易數量小於最小交易數量,終止邏輯,跳出while循環 break; } prePrice = tradePrice; // 緩存當前循環時的交易價格 e.SetDirection(tradeType == ORDER_TYPE_BUY ? "buy" : "sell"); // 設置期貨交易方向 orderId = tradeFunc(tradePrice, doAmount); // 下單交易,參數爲算出的價格,本次下單數量 } else { // 當記錄訂單的變量 orderId 不爲null時,則說明已經下過訂單 if (mode == 0 || Math.abs(tradePrice - prePrice) > maxSpace) { // 若是是掛單模式,當前價格與上一次緩存的價格超出最大掛單區間 orderId = null; // 重置orderId 爲空值,就會在下一輪循環從新下單 } var order = StripOrders(exchange, orderId); // 調用StripOrders查找掛單列表中的ID爲orderId的訂單 if (order == null) { // 若是查找不到,也重置orderId 爲空值,繼續下一輪的下單操做 orderId = null; } } Sleep(retryDelay); // 暫定必定時間,起到控制循環頻率的效果 } if (dealAmount <= 0) { // 在while循環結束後,若是已經交易的量dealAmount小於等於0,說明交易失敗返回空值 return null; } return nowPosition; // 正常狀況返回最新的持倉數據 } function coverFutures(e, orderType) { // 平倉函數 var coverAmount = 0; // 聲明一個變量coverAmount,初始賦值0,用來記錄已經平倉的數量 while (true) { var positions = _C(e.GetPosition); // 獲取持倉 var ticker = GetTicker(e); // 獲取當前行情 var found = 0; // 查找標記 for (var i = 0; i < positions.length; i++) { // 遍歷持倉數組positions if (positions[i].Type == orderType) { // 找到須要的持倉 if (coverAmount == 0) { coverAmount = positions[i].Amount; // 初始時記錄持倉數量,即要平倉的數量 } if (positions[i].Type == ORDER_TYPE_BUY) { // 根據持倉類型,執行平倉操做 e.SetDirection("closebuy"); // 設置期貨交易方向 e.Sell(ticker.Buy, positions[i].Amount); // 下單函數 } else { e.SetDirection("closesell"); e.Buy(ticker.Sell, positions[i].Amount); } found++; // 標記累計 } } if (found == 0) { // 若是標記變量found爲0,則沒有倉位須要處理,跳出while循環 break; } Sleep(2000); // 間隔2秒 StripOrders(e); // 撤銷當前全部掛單 } return coverAmount; // 返回平倉的數量 } function loop(pos) { var tradeType = null; // 初始化交易方向 if (typeof(pos) == 'undefined' || !pos) { // 判斷是不是首輪執行 tradeType = FirstTradeType; pos = Trade(exchange, tradeType, OpAmount, OpMode, SlidePrice, MaxSpace, Interval); // 首筆交易 if (!pos) { throw "出師不利, 開倉失敗"; } else { Log(tradeType == ORDER_TYPE_BUY ? "開多倉完成" : "開空倉完成", "均價:", pos.Price, "數量:", pos.Amount); } } else { tradeType = pos.Type; // 根據持倉方向繼續指定交易方向 } var holdPrice = pos.Price; // 持倉價格 var holdAmount = pos.Amount; // 持倉數量 var openFunc = tradeType == ORDER_TYPE_BUY ? exchange.Buy : exchange.Sell; // 多頭持倉,開倉爲買,不然開倉爲賣 var coverFunc = tradeType == ORDER_TYPE_BUY ? exchange.Sell : exchange.Buy; // 多頭持倉,平倉爲賣,不然平倉位買 var reversePrice = 0; // 反手價格 var coverPrice = 0; // 平倉價格 var canOpen = true; // 可開倉標記 if (tradeType == ORDER_TYPE_BUY) { reversePrice = _N(holdPrice * (1 - StopLoss), 4); // 止損價格 coverPrice = _N(holdPrice * (1 + StopProfit), 4); // 止盈價格 } else { reversePrice = _N(holdPrice * (1 + StopLoss), 4); coverPrice = _N(holdPrice * (1 - StopProfit), 4); } var coverId = null; var msg = "持倉價: " + holdPrice + " 止損價: " + reversePrice; for (var i = 0; i < 10; i++) { // 控制最多下單10次 if (coverId) { // 訂單ID爲空,不觸發break,繼續循環,直到10次 break; } if (tradeType == ORDER_TYPE_BUY) { // 根據方向下單,掛平倉單,即止盈的訂單 exchange.SetDirection("closebuy"); coverId = exchange.Sell(coverPrice, holdAmount, msg); } else { exchange.SetDirection("closesell"); coverId = exchange.Buy(coverPrice, holdAmount, msg); } Sleep(Interval); } if (!coverId) { // 10次下單還失敗拋出錯誤,策略中止 StripOrders(exchange); // 撤銷全部掛單 Log("下單失敗", "@") // 增長推送提醒 throw "下單失敗"; // 拋出錯誤,讓機器人中止 } while (true) { // 進入檢測反手的循環 Sleep(Interval); var ticker = GetTicker(exchange); // 獲取最新的行情 if ((tradeType == ORDER_TYPE_BUY && ticker.Last < reversePrice) || (tradeType == ORDER_TYPE_SELL && ticker.Last > reversePrice)) { // 檢測觸發止損即反手 StripOrders(exchange); // 掛單所有撤單 var coverAmount = coverFutures(exchange, tradeType); // 持倉全平 if (_Failed >= MaxLoss) { // 若是超過最大止損次數(反手次數),跳出循環從新開始 Counter.f++; // 計一次失敗 Log("超過最大失敗次數", MaxLoss); break; // 跳出循環 } var reverseAmount = _N(coverAmount * ReverseRate, 4); // 根據平倉的量,進行交易量加倍 var account = GetAccount(exchange, true); // 更新帳戶信息,此時不能有資產凍結 // 檢測帳戶資產是否足夠,不足跳出循環,從新開始,如同_Failed >= MaxLoss if (_N(account.Stocks * MarginLevel, 4) < reverseAmount) { // 檢測資產是否足夠 Log("沒有幣反手開倉, 須要開倉: ", reverseAmount, "個, 只有", account.Stocks, "個幣"); Counter.f++; break; } var reverseType = tradeType; // 記錄反轉操做類型,默認順倉 if (ReverseMode == 0) { // 反手模式影響的調整,即若是參數設置了反倉,這裏調整 reverseType = tradeType == ORDER_TYPE_BUY ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; // 反倉就是指,剛纔持倉是多,此次反手就作空,剛纔是作空,此次就作多 } var pos = Trade(exchange, reverseType, reverseAmount, OpMode, SlidePrice, MaxSpace, Interval); // 加倍的下單操做 if (pos) { // 檢測交易邏輯執行以後的持倉 Log(reverseType == ORDER_TYPE_BUY ? "多倉" : "空倉", "加倍開倉完成"); } return pos; // 返回持倉結構 } else { // 沒有觸發反手時執行的邏輯 var orders = _C(exchange.GetOrders); // 當止盈掛單成交,記錄勝次數加1 if (orders.length == 0) { Counter.s++; var account = GetAccount(exchange, true); // 更新帳戶資產 LogProfit(account.Stocks, account); // 打印帳戶資產 break; } } } // 非while循環內正常return 的,返回null,例如止盈成功,超過失敗次數,資產不足 return null; } function onexit() { // 機器人中止時,執行掃尾函數onexit StripOrders(exchange); // 撤銷全部掛單 Log("Exit"); } function main() { if (exchange.GetName().indexOf("Futures") == -1) { // 檢測當前添加的第一個交易所對象是否是期貨交易所 throw "只支持期貨, 現貨暫不支持"; } // EnableLogLocal(SaveLocal); if (exchange.GetRate() != 1) { // 不啓用匯率轉換 Log("已禁用匯率轉換"); exchange.SetRate(1); } StopProfit /= 100; // 參數處理爲小數,假設StopProfit爲1表示要1%止盈,從新計算賦值,StopProfit的值爲0.01即1% StopLoss /= 100; // 止損(反手)同上 var eName = exchange.GetName(); if (eName == "Futures_CTP") { // 檢測添加的第一個交易所對象是否爲商品期貨,若是是,拋出錯誤信息,讓機器人中止 throw "暫只支持數字貨幣期貨" } exchange.SetContractType(Symbol); // 設置數字貨幣合約代碼,即要交易、操做的合約 exchange.SetMarginLevel(MarginLevel); // 設置槓桿 Interval *= 1000; // 輪詢間隔參數由秒轉換爲毫秒 SetErrorFilter("502:|503:|unexpected|network|timeout|WSARecv|Connect|GetAddr|no such|reset|http|received|EOF"); // 設置屏蔽的錯誤類型 StripOrders(exchange); // 取消全部掛單 OrgAccount = GetAccount(exchange, true); // 獲取當前帳戶信息 LogStatus("啓動成功"); // 更新狀態欄信息 var pos = null; // 初始化main函數內的局部變量pos爲null,用來記錄持倉數據結構 var positions = GetPosition(exchange); // 獲取當前持倉,調用的是封裝後的GetPosition不帶orderType參數是要獲取所有持倉,注意調用的並不是是API接口exchange.GetPosition if (positions.length == 1) { // 若是開始時有持倉,賦值給pos變量 pos = positions[0]; Log("發現一個倉位, 已經自動恢復進度"); } else if (positions.length > 1) { // 有多個持倉時,爲策略不可運行狀態,策略拋出錯誤讓機器人中止 throw "發現持倉超過1個"; } while (true) { // 策略主循環 pos = loop(pos); // 執行交易邏輯主要的函數loop,pos做爲參數,而且返回新的持倉數據結構 if (!pos) { // 該條件觸發,返回null的狀況,例如止盈成功,超過失敗次數,資產不足 _Failed = 0; // 重置止損次數爲0,重來 } else { _Failed++; // 累計止損次數 } Sleep(Interval); } }
策略邏輯
讀完策略代碼能夠發現,策略邏輯其實並不複雜,代碼也並不算多,可是設計上可謂匠心獨運,不少地方能夠借鑑參考。
策略交易邏輯的主要函數爲loop
函數,在main
函數的主循環中反覆調用,當loop
函數開始執行時,首先下單持倉,而後掛單止盈,等待止盈訂單成交。以後進入檢測狀態,檢測兩項內容。java
- 檢測掛出的止盈單是否成交,止盈單成交,即盈利,退出檢測循環,重置邏輯,從新開始。
- 檢測是否觸發止損(反手),觸發止損,即取消全部掛單,平掉倉位,而後根據參數設置是反手順倉仍是反手逆倉進行加倍反手下單交易。產生持倉,繼續掛出止盈單,而且再次進入檢測狀態(監測止盈、反手)。
策略邏輯簡單描述如此,可是仍是有一些其它細節的,好比最大反手次數的設置,帳戶資產可用的檢測,下單失敗最大次數10次的處理等。
策略中有些函數都作了根據參數不一樣而行爲差別化的設計,例如:StripOrders
函數,GetAccount
函數,GetPosition
函數。這些函數根據參數傳入差別,有不一樣的行爲。這樣很好的複用了代碼,避免了代碼冗餘,讓策略設計簡潔易懂。python
原策略:https://www.fmz.com/strategy/3648數組
反手加倍有必定的風險,特別是在期貨上,策略僅爲學習,實盤慎用,歡迎留言討論。緩存