网格可以自定义方向 先买后卖: 网格会从首价格开始向下挂买单, 每个买单间隔 “价格间隔” 这个参数, 挂单数量为"单笔数量", 挂够 “总数量” 个买单, 有任意买单成交以后, 程序会在买价基础上加 “价差(元)” 这个参数的的值的价格挂出卖单, 卖出, 卖出以后,重新按原来这个网格的价格挂买入单 先卖后买: 操作刚好相反
策略最大的风险就是单边行情, 价格波动超出网格范围.
网格带有自动止损和移动功能
// Grid can customize direction // Basic trading operation: buy first and then sell. // The grid will start to send buying order at the price that below the first price, which is the price follow // by the first price (second latest buying price, third latest buying price…and so on). Each buying order is separated by // the "price interval" parameter. The number of pending orders is "single quantity", and will send the order total until // the "total quantity" is filled. // After any buying order is completed, the program will be on the basis of the buying price, add the price of the "price // difference" parameter to the sell price, after the order has been sold, and then re-start the progress of this // grid strategy (checking, place order, wait until it executed, sell) // Selling short first and then buy to cover: the operation is just the opposite // The biggest risk of this strategy is when the market trend is unilateral moving, and the price fluctuations are exceeding the grid. // The following code has made the grid with automatic stop loss and movement function. // Comments: // The strategy uses a virtual pending order design, which provides a great deal of processing for the exchange to limit the number // of pending orders, and solves the problem flexibly. // The grid logic is flexible in design and clever in structure. // Profit and loss calculation, each numerical statistical algorithm can be used for reference, and each condition detection design // is rigorous. (for minimize the possibility of BUG) // The source code is very worth learning. // Source Code: /* Interface parameters (shown as global variables in the code) OpType Grid Direction Drop-down box (selected) Buy first then sell | Sell first then buy FirstPriceAuto initial price automatic boolean (true/false) true FirstPrice@!FirstPriceAuto initial price numerical (number) 100 AllNum total number numerical (number) 10 PriceGrid Price Interval numerical (number) 1 PriceDiff spread numerical (number) 2 AmountType order size drop-down box (selected) buy and sell the same amount | custom amount AmountOnce@AmountType==0 Single transaction quantity numerical (number) 0.1 BAmountOnce@AmountType==1 Buying Order Size numerical (number) 0.1 SAmountOnce@AmountType==1 Selling order size numerical (number) 0.1 AmountCoefficient@AmountType==0 Quantity difference String (string) *1 AmountDot The decimal point numerical (number) 3 EnableProtectDiff Turn on spread protection Boolean (true/false) false ProtectDiff@EnableProtectDiff Entry spread Price Protection numerical (number) 20 CancelAllWS stop cancels all pending orders Boolean (true/false) true CheckInterval polling interval number numerical (number) 2000 Interval failure retry interval numerical (number) 1300 RestoreProfit restores last profit Boolean (true/false) false LastProfit@RestoreProfit Last Profit numerical (number) 0 ProfitAsOrg@RestoreProfit Last profit counted as average price Boolean (true/false) false EnableAccountCheck enable balance verification Boolean (true/false) true EnableStopLoss@EnableAccountCheck open Stop Loss Boolean (true/false) false StopLoss@EnableStopLoss maximum floating loss numerical (number) 100 StopLossMode@EnableStopLoss Post-stop loss operation Drop-down box (selected) Recycle and exit | Recycle and re-cast EnableStopWin@EnableAccountCheck Turn on Take Profit Boolean (true/false) false StopWin@EnableStopWin Maximum floating profit Number type (number) 120 StopWinMode@EnableStopWin post-take profit operation drop-down box (selected) Recycle and exit | Recycle and re-cast AutoMove@EnableAccountCheck auto Move Boolean (true/false) false MaxDistance@AutoMove maximum distance numerical (number) 20 MaxIdle@AutoMove maximum idle (seconds) numerical (number) 7200 EnableDynamic Turns on dynamic pending orders Boolean (true/false) false DynamicMax@EnableDynamic order expiration distance Number (number) 30 ResetData clears all data at startup Boolean (true/false) true Precision price decimal length numerical (number) 5 */ function hasOrder(orders, orderId) { for (var i = 0; i < orders.length; i++) { if (orders[i].Id == orderId) { return true; } } return false; } function cancelPending() { var ret = false; while (true) { if (ret) { Sleep(Interval); } var orders = _C(exchange.GetOrders); if (orders.length == 0) { break; } for (var j = 0; j < orders.length; j++) { exchange.CancelOrder(orders[j].Id, orders[j]); ret = true; } } return ret; } function valuesToString(values, pos) { var result = ''; if (typeof(pos) === 'undefined') { pos = 0; } for (var i = pos; i < values.length; i++) { if (i > pos) { result += ' '; } if (values[i] === null) { result += 'null'; } else if (typeof(values[i]) == 'undefined') { result += 'undefined'; } else { switch (values[i].constructor.name) { case 'Date': case 'Number': case 'String': case 'Function': result += values[i].toString(); break; default: result += JSON.stringify(values[i]); break; } } } return result; } function Trader() { var vId = 0; var orderBooks = []; var hisBooks = []; var orderBooksLen = 0; this.Buy = function(price, amount, extra) { if (typeof(extra) === 'undefined') { extra = ''; } else { extra = valuesToString(arguments, 2); } vId++; var orderId = "V" + vId; orderBooks[orderId] = { Type: ORDER_TYPE_BUY, Status: ORDER_STATE_PENDING, Id: 0, Price: price, Amount: amount, Extra: extra }; orderBooksLen++; return orderId; }; this.Sell = function(price, amount, extra) { if (typeof(extra) === 'undefined') { extra = ''; } else { extra = valuesToString(arguments, 2); } vId++; var orderId = "V" + vId; orderBooks[orderId] = { Type: ORDER_TYPE_SELL, Status: ORDER_STATE_PENDING, Id: 0, Price: price, Amount: amount, Extra: extra }; orderBooksLen++; return orderId; }; this.GetOrders = function() { var orders = _C(exchange.GetOrders); for (orderId in orderBooks) { var order = orderBooks[orderId]; if (order.Status !== ORDER_STATE_PENDING) { continue; } var found = false; for (var i = 0; i < orders.length; i++) { if (orders[i].Id == order.Id) { found = true; break; } } if (!found) { orders.push(orderBooks[orderId]); } } return orders; } this.GetOrder = function(orderId) { if (typeof(orderId) === 'number') { return exchange.GetOrder(orderId); } if (typeof(hisBooks[orderId]) !== 'undefined') { return hisBooks[orderId]; } if (typeof(orderBooks[orderId]) !== 'undefined') { return orderBooks[orderId]; } return null; }; this.Len = function() { return orderBooksLen; }; this.RealLen = function() { var n = 0; for (orderId in orderBooks) { if (orderBooks[orderId].Id > 0) { n++; } } return n; }; this.Poll = function(ticker, priceDiff) { var orders = _C(exchange.GetOrders); for (orderId in orderBooks) { var order = orderBooks[orderId]; if (order.Id > 0) { var found = false; for (var i = 0; i < orders.length; i++) { if (order.Id == orders[i].Id) { found = true; } } if (!found) { order.Status = ORDER_STATE_CLOSED; hisBooks[orderId] = order; delete(orderBooks[orderId]); orderBooksLen--; continue; } } var diff = _N(order.Type == ORDER_TYPE_BUY ? (ticker.Buy - order.Price) : (order.Price - ticker.Sell)); var pfn = order.Type == ORDER_TYPE_BUY ? exchange.Buy : exchange.Sell; if (order.Id == 0 && diff <= priceDiff) { var realId = pfn(order.Price, order.Amount, order.Extra + "(距离: " + diff + (order.Type == ORDER_TYPE_BUY ? (" 买一: " + ticker.Buy) : (" 卖一: " + ticker.Sell))+")"); if (typeof(realId) === 'number') { order.Id = realId; } } else if (order.Id > 0 && diff > (priceDiff + 1)) { var ok = true; do { ok = true; exchange.CancelOrder(order.Id, "不必要的" + (order.Type == ORDER_TYPE_BUY ? "买单" : "卖单"), "委托价:", order.Price, "量:", order.Amount, ", 距离:", diff, order.Type == ORDER_TYPE_BUY ? ("买一: " + ticker.Buy) : ("卖一: " + ticker.Sell)); Sleep(200); orders = _C(exchange.GetOrders); for (var i = 0; i < orders.length; i++) { if (orders[i].Id == order.Id) { ok = false; } } } while (!ok); order.Id = 0; } } }; } function balanceAccount(orgAccount, initAccount) { cancelPending(); var nowAccount = _C(exchange.GetAccount); var slidePrice = 0.2; var ok = true; while (true) { var diff = _N(nowAccount.Stocks - initAccount.Stocks); if (Math.abs(diff) < MinStock) { break; } var depth = _C(exchange.GetDepth); var books = diff > 0 ? depth.Bids : depth.Asks; var n = 0; var price = 0; for (var i = 0; i < books.length; i++) { n += books[i].Amount; if (n >= Math.abs(diff)) { price = books[i].Price; break; } } var pfn = diff > 0 ? exchange.Sell : exchange.Buy; var amount = Math.abs(diff); var price = diff > 0 ? (price - slidePrice) : (price + slidePrice); Log("开始平衡", (diff > 0 ? "卖出" : "买入"), amount, "个币"); if (diff > 0) { amount = Math.min(nowAccount.Stocks, amount); } else { amount = Math.min(nowAccount.Balance / price, amount); } if (amount < MinStock) { Log("资金不足, 无法平衡到初始状态"); ok = false; break; } pfn(price, amount); Sleep(1000); cancelPending(); nowAccount = _C(exchange.GetAccount); } if (ok) { LogProfit(_N(nowAccount.Balance - orgAccount.Balance)); Log("平衡完成", nowAccount); } } var STATE_WAIT_OPEN = 0; var STATE_WAIT_COVER = 1; var STATE_WAIT_CLOSE = 2; var ProfitCount = 0; var BuyFirst = true; var IsSupportGetOrder = true; var LastBusy = 0; function setBusy() { LastBusy = new Date(); } function isTimeout() { if (MaxIdle <= 0) { return false; } var now = new Date(); if (((now.getTime() - LastBusy.getTime()) / 1000) >= MaxIdle) { LastBusy = now; return true; } return false; } function onexit() { if (CancelAllWS) { Log("正在退出, 尝试取消所有挂单"); cancelPending(); } Log("策略成功停止"); Log(_C(exchange.GetAccount)); } function fishing(orgAccount, fishCount) { setBusy(); var account = _C(exchange.GetAccount); Log(account); var InitAccount = account; var ticker = _C(exchange.GetTicker); var amount = _N(AmountOnce); var amountB = [amount]; var amountS = [amount]; if (typeof(AmountType) !== 'undefined' && AmountType == 1) { for (var idx = 0; idx < AllNum; idx++) { amountB[idx] = BAmountOnce; amountS[idx] = SAmountOnce; } } else { for (var idx = 1; idx < AllNum; idx++) { switch (AmountCoefficient[0]) { case '+': amountB[idx] = amountB[idx - 1] + parseFloat(AmountCoefficient.substring(1)); break; case '-': amountB[idx] = amountB[idx - 1] - parseFloat(AmountCoefficient.substring(1)); break; case '*': amountB[idx] = amountB[idx - 1] * parseFloat(AmountCoefficient.substring(1)); break; case '/': amountB[idx] = amountB[idx - 1] / parseFloat(AmountCoefficient.substring(1)); break; } amountB[idx] = _N(amountB[idx], AmountDot); amountS[idx] = amountB[idx]; } } if (FirstPriceAuto) { FirstPrice = BuyFirst ? _N(ticker.Buy - PriceGrid, Precision) : _N(ticker.Sell + PriceGrid, Precision); } // Initialize fish table var fishTable = {}; var uuidTable = {}; var needStocks = 0; var needMoney = 0; var actualNeedMoney = 0; var actualNeedStocks = 0; var notEnough = false; var canNum = 0; for (var idx = 0; idx < AllNum; idx++) { var price = _N((BuyFirst ? FirstPrice - (idx * PriceGrid) : FirstPrice + (idx * PriceGrid)), Precision); needStocks += amountS[idx]; needMoney += price * amountB[idx]; if (BuyFirst) { if (_N(needMoney) <= _N(account.Balance)) { actualNeedMondy = needMoney; actualNeedStocks = needStocks; canNum++; } else { notEnough = true; } } else { if (_N(needStocks) <= _N(account.Stocks)) { actualNeedMondy = needMoney; actualNeedStocks = needStocks; canNum++; } else { notEnough = true; } } fishTable[idx] = STATE_WAIT_OPEN; uuidTable[idx] = -1; } if (!EnableAccountCheck && (canNum < AllNum)) { Log("警告, 当前资金只可做", canNum, "个网格, 全网共需", (BuyFirst ? needMoney : needStocks), "请保持资金充足"); canNum = AllNum; } if (BuyFirst) { if (EnableProtectDiff && (FirstPrice - ticker.Sell) > ProtectDiff) { throw "首次买入价比市场卖1价高" + _N(FirstPrice - ticker.Sell, Precision) + ' 元'; } else if (EnableAccountCheck && account.Balance < _N(needMoney)) { if (fishCount == 1) { throw "资金不足, 需要" + _N(needMoney) + "元"; } else { Log("资金不足, 需要", _N(needMoney), "元, 程序只做", canNum, "个网格 #ff0000"); } } else { Log('预计动用资金: ', _N(needMoney), "元"); } } else { if (EnableProtectDiff && (ticker.Buy - FirstPrice) > ProtectDiff) { throw "首次卖出价比市场买1价高 " + _N(ticker.Buy - FirstPrice, Precision) + ' 元'; } else if (EnableAccountCheck && account.Stocks < _N(needStocks)) { if (fishCount == 1) { throw "币数不足, 需要 " + _N(needStocks) + " 个币"; } else { Log("资金不足, 需要", _N(needStocks), "个币, 程序只做", canNum, "个网格 #ff0000"); } } else { Log('预计动用币数: ', _N(needStocks), "个, 约", _N(needMoney), "元"); } } var trader = new Trader(); var OpenFunc = BuyFirst ? exchange.Buy : exchange.Sell; var CoverFunc = BuyFirst ? exchange.Sell : exchange.Buy; if (EnableDynamic) { OpenFunc = BuyFirst ? trader.Buy : trader.Sell; CoverFunc = BuyFirst ? trader.Sell : trader.Buy; } var ts = new Date(); var preMsg = ""; var profitMax = 0; while (true) { var now = new Date(); var table = null; if (now.getTime() - ts.getTime() > 5000) { if (typeof(GetCommand) == 'function' && GetCommand() == "收网") { Log("开始执行命令进行收网操作"); balanceAccount(orgAccount, InitAccount); return false; } ts = now; var nowAccount = _C(exchange.GetAccount); var ticker = _C(exchange.GetTicker); if (EnableDynamic) { trader.Poll(ticker, DynamicMax); } var amount_diff = (nowAccount.Stocks + nowAccount.FrozenStocks) - (InitAccount.Stocks + InitAccount.FrozenStocks); var money_diff = (nowAccount.Balance + nowAccount.FrozenBalance) - (InitAccount.Balance + InitAccount.FrozenBalance); var floatProfit = _N(money_diff + (amount_diff * ticker.Last)); var floatProfitAll = _N((nowAccount.Balance + nowAccount.FrozenBalance - orgAccount.Balance - orgAccount.FrozenBalance) + ((nowAccount.Stocks + nowAccount.FrozenStocks - orgAccount.Stocks - orgAccount.FrozenStocks) * ticker.Last)); var isHold = Math.abs(amount_diff) >= MinStock; if (isHold) { setBusy(); } profitMax = Math.max(floatProfit, profitMax); if (EnableAccountCheck && EnableStopLoss) { if ((profitMax - floatProfit) >= StopLoss) { Log("当前浮动盈亏", floatProfit, "利润最高点: ", profitMax, "开始止损"); balanceAccount(orgAccount, InitAccount); if (StopLossMode == 0) { throw "止损退出"; } else { return true; } } } if (EnableAccountCheck && EnableStopWin) { if (floatProfit > StopWin) { Log("当前浮动盈亏", floatProfit, "开始止盈"); balanceAccount(orgAccount, InitAccount); if (StopWinMode == 0) { throw "止盈退出"; } else { return true; } } } var distance = 0; if (EnableAccountCheck && AutoMove) { if (BuyFirst) { distance = ticker.Last - FirstPrice; } else { distance = FirstPrice - ticker.Last; } var refish = false; if (!isHold && isTimeout()) { Log("空仓过久, 开始移动网格"); refish = true; } if (distance > MaxDistance) { Log("价格超出网格区间过多, 开始移动网格, 当前距离: ", _N(distance, Precision), "当前价格:", ticker.Last); refish = true; } if (refish) { balanceAccount(orgAccount, InitAccount); return true; } } var holdDirection, holdAmount = "--", holdPrice = "--"; if (isHold) { if (RestoreProfit && ProfitAsOrg) { if (BuyFirst) { money_diff += LastProfit; } else { money_diff -= LastProfit; } } holdAmount = amount_diff; holdPrice = (-money_diff) / amount_diff; if (!BuyFirst) { holdAmount = -amount_diff; holdPrice = (money_diff) / -amount_diff; } holdAmount = _N(holdAmount, 4); holdPrice = _N(holdPrice, Precision); holdDirection = BuyFirst ? "多" : "空"; } else { holdDirection = "--"; } table = { type: 'table', title: '运行状态', cols: ['动用资金', '持有仓位', '持仓大小', '持仓均价', '总浮动盈亏', '当前网格盈亏', '撒网次数', '网格偏移', '真实委托', '最新币价'], rows: [ [_N(actualNeedMondy, 4), holdDirection, holdAmount, holdPrice, _N(floatProfitAll, 4) + ' ( ' + _N(floatProfitAll * 100 / actualNeedMondy, 4) + ' % )', floatProfit, fishCount, (AutoMove && distance > 0) ? ((BuyFirst ? "向上" : "向下") + "偏离: " + _N(distance) + " 元") : "--", trader.RealLen(), ticker.Last] ] }; } var orders = _C(trader.GetOrders); if (table) { if (!EnableDynamic) { table.rows[0][8] = orders.length; } LogStatus('`' + JSON.stringify(table) + '`'); } for (var idx = 0; idx < canNum; idx++) { var openPrice = _N((BuyFirst ? FirstPrice - (idx * PriceGrid) : FirstPrice + (idx * PriceGrid)), Precision); var coverPrice = _N((BuyFirst ? openPrice + PriceDiff : openPrice - PriceDiff), Precision); var state = fishTable[idx]; var fishId = uuidTable[idx]; if (hasOrder(orders, fishId)) { continue; } if (fishId != -1 && IsSupportGetOrder) { var order = trader.GetOrder(fishId); if (!order) { Log("获取订单信息失败, ID: ", fishId); continue; } if (order.Status == ORDER_STATE_PENDING) { //Log("订单状态为未完成, ID: ", fishId); continue; } } if (state == STATE_WAIT_COVER) { var coverId = CoverFunc(coverPrice, (BuyFirst ? amountS[idx] : amountB[idx]), (BuyFirst ? '完成买单:' : '完成卖单:'), openPrice, '量:', (BuyFirst ? amountB[idx] : amountS[idx])); if (typeof(coverId) === 'number' || typeof(coverId) === 'string') { fishTable[idx] = STATE_WAIT_CLOSE; uuidTable[idx] = coverId; } } else if (state == STATE_WAIT_OPEN || state == STATE_WAIT_CLOSE) { var openId = OpenFunc(openPrice, BuyFirst ? amountB[idx] : amountS[idx]); if (typeof(openId) === 'number' || typeof(openId) === 'string') { fishTable[idx] = STATE_WAIT_COVER; uuidTable[idx] = openId; if (state == STATE_WAIT_CLOSE) { ProfitCount++; var account = _C(exchange.GetAccount); var ticker = _C(exchange.GetTicker); var initNet = _N(((InitAccount.Stocks + InitAccount.FrozenStocks) * ticker.Buy) + InitAccount.Balance + InitAccount.FrozenBalance, 8); var nowNet = _N(((account.Stocks + account.FrozenStocks) * ticker.Buy) + account.Balance + account.FrozenBalance, 8); var actualProfit = _N(((nowNet - initNet)) * 100 / initNet, 8); if (AmountType == 0) { var profit = _N((ProfitCount * amount * PriceDiff) + LastProfit, 8); Log((BuyFirst ? '完成卖单:' : '完成买单:'), coverPrice, '量:', (BuyFirst ? amountS[idx] : amountB[idx]), '平仓收益', profit); } else { Log((BuyFirst ? '完成卖单:' : '完成买单:'), coverPrice, '量:', (BuyFirst ? amountS[idx] : amountB[idx])); } } } } } Sleep(CheckInterval); } return true; } function main() { if (ResetData) { LogProfitReset(); LogReset(); } exchange.SetPrecision(Precision, XPrecision) if (typeof(AmountType) === 'undefined') { AmountType = 0; } if (typeof(AmountDot) === 'undefined') { AmountDot = 3; } if (typeof(EnableDynamic) === 'undefined') { EnableDynamic = false; } if (typeof(AmountCoefficient) === 'undefined') { AmountCoefficient = "*1"; } if (typeof(EnableAccountCheck) === 'undefined') { EnableAccountCheck = true; } BuyFirst = (OpType == 0); IsSupportGetOrder = exchange.GetName().indexOf('itstamp') == -1; if (!IsSupportGetOrder) { Log(exchange.GetName(), "不支持GetOrder, 可能影响策略稳定性."); } SetErrorFilter("502:|503:|S_U_001|unexpected|network|timeout|WSARecv|Connect|GetAddr|no such|reset|http|received|refused|EOF|When"); exchange.SetRate(1); Log('已经禁用汇率转换, 当前货币为', exchange.GetBaseCurrency()); if (!RestoreProfit) { LastProfit = 0; } var orgAccount = _C(exchange.GetAccount); var fishCount = 1; while (true) { if (!fishing(orgAccount, fishCount)) { break; } fishCount++; Log("第", fishCount, "次重新撒网..."); FirstPriceAuto = true; Sleep(1000); } }