A plataforma de negociação FMZ Quant foi estabelecida muito cedo. Naquela época, havia trocas e moedas muito limitadas, e havia poucos modos de negociação. Portanto, o design inicial da API era simples e focado em estratégias de negociação de moeda única. Após anos de iteração, especialmente a versão mais recente, ele foi relativamente completo, e as APIs de troca comumente usadas podem ser completadas com funções encapsuladas. Especialmente para estratégias de múltiplas moedas, obter condições de mercado, contas e transações é muito mais simples do que antes, e as funções de IO não são mais necessárias para acessar a interface API da exchange. Para backtesting, também foi atualizado para ser compatível com negociação ao vivo. Em suma, se sua estratégia originalmente tinha muitos métodos dedicados e não poderia ser usada para várias trocas e backtesting, você pode atualizá-la. Neste artigo, apresentaremos a estratégia com a arquitetura atual da combinação.
Informações sobre novos recursos da interface da API foram atualizadas para a documentação da API da FMZ Quant Trading Platform:
Guia de sintaxe:https://www.fmz.com/syntax-guideGuia do utilizador:https://www.fmz.com/user-guide
Atualmente, a API tem uma função unificada para obter precisão, que é introduzida aqui usando um contrato perpétuo como exemplo.
//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.
var Info = { trade_symbols: SYMBOLS.split(","), base_coin: QUOTO, ticker: {}, order: {}, account: {}, precision: {},
position: {}, time:{}, count:{}, interval:INTERVAL}
function InitInfo() {
//Initialization strategy
if (!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 accuracy
function GetPrecision() {
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.swap
if (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].PricePrecision
Info.precision[symbol].amount_precision = exchange_info[pair].AmountPrecision
Info.precision[symbol].min_qty = exchange_info[pair].MinQty
Info.precision[symbol].max_qty = exchange_info[pair].MaxQty
Info.precision[symbol].min_notional = exchange_info[pair].MinNotional
Info.precision[symbol].ctVal = exchange_info[pair].CtVal; //Contract value, for example, 1 piece represents 0.01 coin
if (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"
}
}
}
}
Para projetar uma estratégia multi-produto, você precisa obter as informações de mercado de todo o mercado.
function UpdateTicker() {
//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);
}
}
O campo Equity e UPnL foram adicionados às informações da conta de futuros, eliminando a necessidade de incompatibilidade causada por processamento adicional. A função GetPositions também suporta a obtenção de todas as posições. Um detalhe é que o número de posições deve ser multiplicado pelo valor de um contrato para obter o número real.
function UpdateAccount() {
//Update account
if (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 balance
Info.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);
}
function UpdatePosition() {
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 of Info.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);
}
A última função CreateOrder é usada aqui para processar pedidos, o que é muito mais conveniente.
function Order(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");
}
}
function Trade(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 order
CancelOrder(symbol, direction, Info.order[symbol][direction].id);
Info.order[symbol][direction].id = 0;
}
if ( //The delay is too high and the order is not placed
Date.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);
}
}
Em geral, são apresentadas duas tabelas: informações sobre a conta e informações sobre os pares de negociação.
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 in Info.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(new Date(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();
}
}
Depois que o andaime é configurado, o código de lógica de negociação do núcleo é simples.
function MakeOrder() {
for (let i in Info.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);
}
}
}
function OnTick() {
try {
UpdateTicker();
UpdatePosition();
MakeOrder();
UpdateStatus();
} catch (error) {
Log("Loop error: " + error);
}
}
function main() {
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);
}
}
Este artigo fornece uma estrutura de negociação de multi-moeda de contrato perpétuo simples. Usando a mais recente API, você pode construir estratégias compatíveis de forma mais conveniente e rápida. Vale a pena tentar.