Preface
The FMZ Quant Trading Platform was established too early. At that time, there were very limited exchanges and currencies, and there were few trading modes. Therefore, the initial API design was simple and focused on single-currency trading strategies. After years of iteration, especially the latest version, it has been relatively complete, and commonly used exchange APIs can be completed with encapsulated functions. Especially for multi-currency strategies, obtaining market conditions, accounts, and transactions are much simpler than before, and IO functions are no longer required to access the exchange's API interface. For backtesting, it has also been upgraded to be compatible with live trading. In short, if your strategy originally had many dedicated methods and could not be used for multiple exchanges and backtesting, you can upgrade it. In this article, we will introduce the current strategy architecture in combination with the strategy.
Dockers need to upgrade to 3.7 to fully support it. Information about new features of the API interface has been updated to the API documentation of the FMZ Quant Trading Platform:
Syntax Guide: https://www.fmz.com/syntax-guide
User Guide: https://www.fmz.com/user-guide
Get Accuracy
At present, the API has a unified function for obtaining accuracy, which is introduced here using a perpetual contract as an example.
//Global variables store data. SYMBOLS represents the currency to be traded, and the format is "BTC,ETH,LTC". QUOTO is the base currency. Common perpetual contracts include USDT and USDC. INTERVAL represents the interval of the cycle.varInfo = { trade_symbols: SYMBOLS.split(","), base_coin: QUOTO, ticker: {}, order: {}, account: {}, precision: {}, position: {}, time:{}, count:{}, interval:INTERVAL} functionInitInfo() { //Initialization strategyif (!IsVirtual() && Version() < 3.7){ throw"FMZ platform upgrades API, you need to download the latest docker"; } Info.account = {init_balance:0}; Info.time = { update_ticker_time: 0, update_pos_time: 0, update_profit_time: 0, update_account_time: 0, update_status_time: 0, last_loop_time:0, loop_delay:0, }; for (let i = 0; i < Info.trade_symbols.length; i++) { let symbol = Info.trade_symbols[i]; Info.ticker[symbol] = { last: 0, ask: 0, bid: 0 }; Info.order[symbol] = { buy: { id: 0, price: 0, amount: 0 }, sell: { id: 0, price: 0, amount: 0 } }; Info.position[symbol] = { amount: 0, hold_price: 0, unrealised_profit: 0, open_time: 0, value: 0 }; Info.precision[symbol] = {}; } } //Get accuracyfunctionGetPrecision() { let exchange_info = exchange.GetMarkets(); for (let pair in exchange_info) { let symbol = pair.split('_')[0]; //The format of perpetual contract trading pairs is BTC_USDT.swapif (Info.trade_symbols.indexOf(symbol) > -1 && pair.split('.')[0].endsWith(Info.base_coin) && pair.endsWith("swap")) { Info.precision[symbol].tick_size = exchange_info[pair].TickSize; Info.precision[symbol].amount_size = exchange_info[pair].AmountSize; Info.precision[symbol].price_precision = exchange_info[pair].PricePrecisionInfo.precision[symbol].amount_precision = exchange_info[pair].AmountPrecisionInfo.precision[symbol].min_qty = exchange_info[pair].MinQtyInfo.precision[symbol].max_qty = exchange_info[pair].MaxQtyInfo.precision[symbol].min_notional = exchange_info[pair].MinNotionalInfo.precision[symbol].ctVal = exchange_info[pair].CtVal; //Contract value, for example, 1 piece represents 0.01 coinif (exchange_info[pair].CtValCcy != symbol){ //The currency used to denominate the value. This does not include currency-based situations, for example, 1 note worth 100 USD.throw"No support for currency-based" } } } }
Get Tickers
To design a multi-product strategy, you need to obtain the market information of the entire market. This aggregated market information interface is essential. The GetTickers function supports most mainstream exchanges.
functionUpdateTicker() { //Updated prices let ticker = exchange.GetTickers(); if (!ticker) { Log("Failed to obtain market information", GetLastError()); return; } Info.time.update_ticker_time = Date.now(); for (let i = 0; i < ticker.length; i++) { let symbol = ticker[i].Symbol.split('_')[0]; if (!ticker[i].Symbol.split('.')[0].endsWith(Info.base_coin) || Info.trade_symbols.indexOf(symbol) < 0) { continue; } Info.ticker[symbol].ask = parseFloat(ticker[i].Sell); Info.ticker[symbol].bid = parseFloat(ticker[i].Buy); Info.ticker[symbol].last = parseFloat(ticker[i].Last); } }
Get Account Position Information
The Equity field and UPnL field have been added to the futures account information, eliminating the need for incompatibility caused by additional processing. The GetPositions function also supports obtaining all positions. One detail is that the number of positions must be multiplied by the value of a contract to obtain the actual number. For example, although OKX perpetual contracts can be traded by quantity on the web page, the API is based on the number of contracts.
functionUpdateAccount() { //Update accountif (Date.now() - Info.time.update_account_time < 60 * 1000) { return; } Info.time.update_account_time = Date.now(); let account = exchange.GetAccount(); if (account === null) { Log("Failed to update account"); return; } Info.account.margin_used = _N(account.Equity - account.Balance, 2); Info.account.margin_balance = _N(account.Equity, 2); //Current balanceInfo.account.margin_free = _N(account.Balance, 2); Info.account.wallet_balance = _N(account.Equity - account.UPnL, 2); Info.account.unrealised_profit = _N(account.UPnL, 2); if (!Info.account.init_balance) { if (_G("init_balance") && _G("init_balance") > 0) { Info.account.init_balance = _N(_G("init_balance"), 2); } else { Info.account.init_balance = Info.account.margin_balance; _G("init_balance", Info.account.init_balance); } } Info.account.profit = _N(Info.account.margin_balance - Info.account.init_balance, 2); Info.account.profit_rate = _N((100 * Info.account.profit) / init_balance, 2); } functionUpdatePosition() { let pos = exchange.GetPositions(Info.base_coin + ".swap"); if (!pos) { Log("Timeout for updating position"); return; } Info.time.update_pos_time = Date.now(); let position_info = {}; for (let symbol ofInfo.trade_symbols) { position_info[symbol] = { amount: 0, hold_price: 0, unrealised_profit: 0 }; //Some exchanges have no positions and return empty } for (let k = 0; k < pos.length; k++) { let symbol = pos[k].Symbol.split("_")[0]; if (!pos[k].Symbol.split(".")[0].endsWith(Info.base_coin) || Info.trade_symbols.indexOf(symbol) < 0) { continue; } if (position_info[symbol].amount != 0){ throw"One-way position required"; } position_info[symbol] = { amount: pos[k].Type == 0 ? pos[k].Amount * Info.precision[symbol].ctVal : -pos[k].Amount * Info.precision[symbol].ctVal, hold_price: pos[k].Price, unrealised_profit: pos[k].Profit }; } Info.count = { long: 0, short: 0, total: 0, leverage: 0 }; for (let symbol in position_info) { let deal_volume = Math.abs(position_info[symbol].amount - Info.position[symbol].amount); let direction = position_info[symbol].amount - Info.position[symbol].amount > 0 ? 1 : -1; if (deal_volume) { let deal_price = direction == 1 ? Info.order[symbol].buy.price : Info.order[symbol].sell.price; Log( symbol, "position update:", _N(Info.position[symbol].value, 1), " -> ", _N(position_info[symbol].amount * Info.ticker[symbol].last, 1), direction == 1 ? ", buy" : ", sell", ", transaction price:", deal_price, ", cost price:", _N(Info.position[symbol].hold_price, Info.precision[symbol].price_precision), ); } Info.position[symbol].amount = position_info[symbol].amount; Info.position[symbol].hold_price = position_info[symbol].hold_price; Info.position[symbol].value = _N(Info.position[symbol].amount * Info.ticker[symbol].last, 2); Info.position[symbol].unrealised_profit = position_info[symbol].unrealised_profit; Info.count.long += Info.position[symbol].amount > 0 ? Math.abs(Info.position[symbol].value) : 0; Info.count.short += Info.position[symbol].amount < 0 ? Math.abs(Info.position[symbol].value) : 0; } Info.count.total = _N(Info.count.long + Info.count.short, 2); Info.count.leverage = _N(Info.count.total / Info.account.margin_balance, 2); }
Transaction
The transaction also needs to deal with the issue of the number of contracts. The latest CreateOrder function is used here to process orders, which is much more convenient.
functionOrder(symbol, direction, price, amount, msg) { let ret = null; let pair = symbol + "_" + Info.base_coin + ".swap" ret = exchange.CreateOrder(pair, direction, price, amount, msg) if (ret) { Info.order[symbol][direction].id = ret; Info.order[symbol][direction].price = price; }else { Log(symbol, direction, price, amount, "abnormal order"); } } functionTrade(symbol, direction, price, amount, msg) { price = _N(price - (price % Info.precision[symbol].tick_size), Info.precision[symbol].price_precision); amount = amount / Info.precision[symbol].ctVal; amount = _N(amount - (amount % Info.precision[symbol].amount_size), Info.precision[symbol].amount_precision); amount = Info.precision[symbol].max_qty > 0 ? Math.min(amount, Info.precision[symbol].max_qty) : amount; let new_order = false; if (price > 0 && Math.abs(price - Info.order[symbol][direction].price) / price > 0.0001) { //The order will be cancelled only if there is a price difference between the two orders. new_order = true; } if (amount <= 0 || Info.order[symbol][direction].id == 0) { //The amount passed in is 0 to cancel the order new_order = true; } if (new_order) { if (Info.order[symbol][direction].id) { //Cancellation of existing orderCancelOrder(symbol, direction, Info.order[symbol][direction].id); Info.order[symbol][direction].id = 0; } if ( //The delay is too high and the order is not placedDate.now() - Info.time.update_pos_time > 2 * Info.interval * 1000 || Date.now() - Info.time.update_ticker_time > 2 * Info.interval * 1000 || ) { return; } if (price * amount <= Info.precision[symbol].min_notional || amount < Info.precision[symbol].min_qty) { Log(symbol, "the order quantity is too small", price * amount); return; } Order(symbol, direction, price, amount, msg); } }
Status Display
Generally two tables are displayed: account information and trading pair information.
function UpdateStatus() { if (Date.now() - Info.time.update_status_time < 4000) { return; } Info.time.update_status_time = Date.now(); let table1 = { type: "table", title: "Account info", cols: [ "Initial balance", "Wallet balance", "Margin balance", "Used margin", "Avaiable margin", "Total profit", "Profit rate", "Unrealised profit", "Total position", "Leverage-used", "Loop delay", ], rows: [ [ Info.account.init_balance, Info.account.wallet_balance, Info.account.margin_balance, Info.account.margin_used, Info.account.margin_free, Info.account.profit, Info.account.profit_rate + "%", _N(Info.account.unrealised_profit, 2), _N(Info.count.total, 2), Info.count.leverage, Info.time.loop_delay + "ms", ], ], }; let table2 = { type: "table", title: "Trading pair information", cols: [ "Symbol", "Direction", "Amount", "Position price", "Position value", "Current price", "Buy price", "Sell price", "Unrealised profit / loss", ], rows: [], }; for (let i inInfo.trade_symbols) { let symbol = Info.trade_symbols[i]; table2.rows.push([ symbol, Info.position[symbol].amount > 0 ? "LONG" : "SHORT", _N(Info.position[symbol].amount, Info.precision[symbol].amount_precision+2), _N(Info.position[symbol].hold_price, Info.precision[symbol].price_precision), _N(Info.position[symbol].value, 2), _N(Info.ticker[symbol].last, Info.precision[symbol].price_precision), Info.order[symbol].buy.price, Info.order[symbol].sell.price, _N(Info.position[symbol].unrealised_profit, 2), ]); } LogStatus( "Initial date: " + _D(newDate(Info.time.start_time)) + "\n", "`" + JSON.stringify(table1) + "`" + "\n" + "`" + JSON.stringify(table2) + "`\n", "Last execution time: " + _D() + "\n", ); if (Date.now() - Info.time.update_profit_time > 5 * 60 * 1000) { UpdateAccount(); LogProfit(_N(Info.account.profit, 3)); Info.time.update_profit_time = Date.now(); } }
Trading Logic
After the scaffolding is set up, the core trading logic code is simple. Here is the simplest iceberg order strategy.
functionMakeOrder() { for (let i inInfo.trade_symbols) { let symbol = Info.trade_symbols[i]; let buy_price = Info.ticker[symbol].bid; let buy_amount = 50 / buy_price; if (Info.position[symbol].value < 2000){ Trade(symbol, "buy", buy_price, buy_amount, symbol); } } }
Main Loop
functionOnTick() { try { UpdateTicker(); UpdatePosition(); MakeOrder(); UpdateStatus(); } catch (error) { Log("Loop error: " + error); } } functionmain() { InitInfo(); while (true) { let loop_start_time = Date.now(); if (Date.now() - Info.time.last_loop_time > Info.interval * 1000) { OnTick(); Info.time.last_loop_time = Date.now(); Info.time.loop_delay = Date.now() - loop_start_time; } Sleep(5); } }
Summary
This article provides a simple perpetual contract multi-currency trading framework. By using the latest API, you can build compatible strategies more conveniently and quickly. It is worth trying.