At the request of community users who want to have a multi-variety double-EMA strategy for design reference. In this article, we will implement a multi-variety double-EMA strategy. Comments will be written on the strategy code to for convenient understanding and learning. Let more newcomers of programming and quantitative trading get a quick start.
Strategy ideas
The logic of the double-EMA strategy is very simple, that is, two EMAs. An EMA (fast line) with a small parameter period and an EMA (slow line) with a large parameter period. If the two lines have a golden cross (the fast line goes through the slow line from the bottom to the top), then we buy and go long; and if the two lines have a dead cross (the fast line goes through the slow line from the top to the bottom), then we sell and go short. We use EMA here.
However, the strategy should be designed as multi-variety, so the parameters of each variety may be different (different varieties use different EMA parameters), so a "parameter group" method should be used to design parameters.
The parameters are designed in the string form, with each parameter comma separated. Parse these strings when the strategy starts running. The execution logic match to each variety (trading pair). The strategy rotated detects the market of each variety, the triggering of trading conditions, chart printing, etc. After all varieties are rotated once, summarize the data and display the table information on the status bar.
The strategy is designed to be very simple and suitable for newcomers' learning, with only 200+ lines of code in total.
Strategy code
// Function: cancel all takers of the current trading pair function cancelAll(e) { while (true) { var orders = _C(e.GetOrders) if (orders.length == 0) { break } else { for (var i = 0 ; i < orders.length ; i++) { e.CancelOrder(orders[i].Id, orders[i]) Sleep(500) } } Sleep(500) } } // Functionn: calculate the profit/loss in real-time function getProfit(account, initAccount, lastPrices) { // account is the current account information, initAccount is the initial account information, lastPrices is the latest price of all varieties var sum = 0 _.each(account, function(val, key) { // Iterate through all current assets, calculate the currency difference of assets other than USDT, and the amount difference if (key != "USDT" && typeof(initAccount[key]) == "number" && lastPrices[key + "_USDT"]) { sum += (account[key] - initAccount[key]) * lastPrices[key + "_USDT"] } }) // Return to the profit and loss of the asset based on the current prices return account["USDT"] - initAccount["USDT"] + sum } // Function: generate chart configuration function createChartConfig(symbol, ema1Period, ema2Period) { // symbol is the trading pair, ema1Period is the first EMA period, ema2Period is the second EMA period var chart = { __isStock: true, extension: { layout: 'single', height: 600, }, title : { text : symbol}, xAxis: { type: 'datetime'}, series : [ { type: 'candlestick', // K-line data series name: symbol, id: symbol, data: [] }, { type: 'line', // EMA data series name: symbol + ',EMA1:' + ema1Period, data: [], }, { type: 'line', // EMA data series name: symbol + ',EMA2:' + ema2Period, data: [] } ] } return chart } function main() { // Reset all data if (isReset) { _G(null) // Clear data of all persistent records LogReset(1) // Clear all logs LogProfitReset() // Clear all return logs LogVacuum() //Release the resources occupied by the real bot database Log("Reset all data", "#FF0000") // Print messages } // Parameter analysis var arrSymbols = symbols.split(",") // Comma-separated string of trading varieties var arrEma1Periods = ema1Periods.split(",") // Parameter string for splitting the first EMA var arrEma2Periods = ema2Periods.split(",") // Parameter string for splitting the second EMA var arrAmounts = orderAmounts.split(",") // Splitting the amount of orders placed for each variety var account = {} // Variables used for recording current asset messages var initAccount = {} // Variables used for recording initial asset messages var currTradeMsg = {} // Variables used for recording whether current BAR trades var lastPrices = {} // Variables used for recording the latest price of monitored varieties var lastBarTime = {} // Variable used for recording the time of the last BAR, used to judge the update of BAR when drawing var arrChartConfig = [] // Used for recording chart configuration message and draw if (_G("currTradeMsg")) { // For example, restore currTradeMsg data when restarting currTradeMsg = _G("currTradeMsg") Log("Restore records", currTradeMsg) } // Initialize account _.each(arrSymbols, function(symbol, index) { exchange.SetCurrency(symbol) var arrCurrencyName = symbol.split("_") var baseCurrency = arrCurrencyName[0] var quoteCurrency = arrCurrencyName[1] if (quoteCurrency != "USDT") { throw "only support quoteCurrency: USDT" } if (!account[baseCurrency] || !account[quoteCurrency]) { cancelAll(exchange) var acc = _C(exchange.GetAccount) account[baseCurrency] = acc.Stocks account[quoteCurrency] = acc.Balance } // Initialize chart-related data lastBarTime[symbol] = 0 arrChartConfig.push(createChartConfig(symbol, arrEma1Periods[index], arrEma2Periods[index])) }) if (_G("initAccount")) { initAccount = _G("initAccount") Log("Restore initial account records", initAccount) } else { // Initialize the initAccount variable with the current asset information _.each(account, function(val, key) { initAccount[key] = val }) } Log("account:", account, "initAccount:", initAccount) // Print asset information // Initialize the chart object var chart = Chart(arrChartConfig) // Chart reset chart.reset() // Strategy main loop logic while (true) { // Iterate through all varieties and execute the double-EMA logic one by one _.each(arrSymbols, function(symbol, index) { exchange.SetCurrency(symbol) // Switch the trading pair to the trading pair of symbol string record var arrCurrencyName = symbol.split("_") // Split the trading pairs with the "_" symbol var baseCurrency = arrCurrencyName[0] // String for trading currencies var quoteCurrency = arrCurrencyName[1] // String for denominated currency // Obtain the EMA parameters of the current trading pair according to the index var ema1Period = parseFloat(arrEma1Periods[index]) var ema2Period = parseFloat(arrEma2Periods[index]) var amount = parseFloat(arrAmounts[index]) // Obtain the K-line data of the current trading pair var r = exchange.GetRecords() if (!r || r.length < Math.max(ema1Period, ema2Period)) { // Return directly if K-line length is insufficient Sleep(1000) return } var currBarTime = r[r.length - 1].Time // Record the current BAR timestamp lastPrices[symbol] = r[r.length - 1].Close // Record the latest current price var ema1 = TA.EMA(r, ema1Period) // Calculate EMA indicators var ema2 = TA.EMA(r, ema2Period) // Calculate EMA indicators if (ema1.length < 3 || ema2.length < 3) { // The length of EMA indicator array is too short, return directly Sleep(1000) return } var ema1Last2 = ema1[ema1.length - 2] // EMA on the penultimate BAR var ema1Last3 = ema1[ema1.length - 3] // EMA on the third from the last BAR var ema2Last2 = ema2[ema2.length - 2] var ema2Last3 = ema2[ema2.length - 3] // Write data to the chart var klineIndex = index + 2 * index // Iterate through the K-line data for (var i = 0 ; i < r.length ; i++) { if (r[i].Time == lastBarTime[symbol]) { // Draw the chart, update the current BAR and indicators // update chart.add(klineIndex, [r[i].Time, r[i].Open, r[i].High, r[i].Low, r[i].Close], -1) chart.add(klineIndex + 1, [r[i].Time, ema1[i]], -1) chart.add(klineIndex + 2, [r[i].Time, ema2[i]], -1) } else if (r[i].Time > lastBarTime[symbol]) { // Draw the charts, add BARs and indicators // add lastBarTime[symbol] = r[i].Time // Update timestamp chart.add(klineIndex, [r[i].Time, r[i].Open, r[i].High, r[i].Low, r[i].Close]) chart.add(klineIndex + 1, [r[i].Time, ema1[i]]) chart.add(klineIndex + 2, [r[i].Time, ema2[i]]) } } if (ema1Last3 < ema2Last3 && ema1Last2 > ema2Last2 && currTradeMsg[symbol] != currBarTime) { // Golden cross var depth = exchange.GetDepth() // Obtain the depth data of current order book var price = depth.Asks[Math.min(takeLevel, depth.Asks.length)].Price // Take the 10th grade price, taker if (depth && price * amount <= account[quoteCurrency]) { // Obtain deep data normally with enough assets to place an order exchange.Buy(price, amount, ema1Last3, ema2Last3, ema1Last2, ema2Last2) // Place a buy order cancelAll(exchange) // Cancel all makers var acc = _C(exchange.GetAccount) // Obtain account asset information if (acc.Stocks != account[baseCurrency]) { // Detect changes in account assets account[baseCurrency] = acc.Stocks // Update assets account[quoteCurrency] = acc.Balance // Update assets currTradeMsg[symbol] = currBarTime // Record that the current BAR has been traded _G("currTradeMsg", currTradeMsg) // Persistent records var profit = getProfit(account, initAccount, lastPrices) // Calculate profits if (profit) { LogProfit(profit, account, initAccount) // Print profits } } } } else if (ema1Last3 > ema2Last3 && ema1Last2 < ema2Last2 && currTradeMsg[symbol] != currBarTime) { // 死叉 var depth = exchange.GetDepth() var price = depth.Bids[Math.min(takeLevel, depth.Bids.length)].Price if (depth && amount <= account[baseCurrency]) { exchange.Sell(price, amount, ema1Last3, ema2Last3, ema1Last2, ema2Last2) cancelAll(exchange) var acc = _C(exchange.GetAccount) if (acc.Stocks != account[baseCurrency]) { account[baseCurrency] = acc.Stocks account[quoteCurrency] = acc.Balance currTradeMsg[symbol] = currBarTime _G("currTradeMsg", currTradeMsg) var profit = getProfit(account, initAccount, lastPrices) if (profit) { LogProfit(profit, account, initAccount) } } } } Sleep(1000) }) // Table variables in the status bar var tbl = { type : "table", title : "Account Information", cols : [], rows : [] } // Write data into the status bar table structure tbl.cols.push("--") tbl.rows.push(["initial"]) tbl.rows.push(["current"]) _.each(account, function(val, key) { if (typeof(initAccount[key]) == "number") { tbl.cols.push(key) tbl.rows[0].push(initAccount[key]) // initial tbl.rows[1].push(val) // current } }) // Show status bar table LogStatus(_D(), "\n", "profit:", getProfit(account, initAccount, lastPrices), "\n", "`" + JSON.stringify(tbl) + "`") } }
Strategy backtest
It can be seen that ETH, LTC and ETC are triggered according to the Golden Cross and Dead Cross of EMA, and tradings have occurred.
We can also take a simulation bot for testing.
Strategy source code: https://www.fmz.com/strategy/333783
The strategy is used for backtesting, learning strategy design only, and it should be used with caution in the real bot.