FMZ Quant Trading Platform была создана слишком рано. В то время было очень мало бирж и валют, и было мало режимов торговли. Поэтому первоначальный дизайн API был простым и ориентирован на одновалютные торговые стратегии. После многих лет итерации, особенно последней версии, он был относительно полным, и широко используемые API для обмена могут быть завершены с помощью инкапсулированных функций. Особенно для многовалютных стратегий, получение рыночных условий, счетов и транзакций намного проще, чем раньше, и функции IO больше не требуются для доступа к интерфейсу API exchange
Докеры должны обновиться до 3.7, чтобы полностью поддержать его.
Руководство по синтаксису:https://www.fmz.com/syntax-guideРуководство пользователя:https://www.fmz.com/user-guide
В настоящее время API имеет единую функцию для получения точности, которая представлена здесь с использованием вечного контракта в качестве примера.
//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"
}
}
}
}
Для разработки стратегии с использованием нескольких продуктов необходимо получить информацию о рынке всего рынка.
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);
}
}
Поле Equity и UPnL были добавлены к информации о фьючерсном счете, устраняя необходимость несовместимости, вызванной дополнительной обработкой. Функция GetPositions также поддерживает получение всех позиций. Одна деталь заключается в том, что количество позиций должно быть умножено на стоимость контракта, чтобы получить фактическое число.
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);
}
Для обработки заказов здесь используется новейшая функция CreateOrder, что намного удобнее.
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);
}
}
Как правило, на сайте отображаются две таблицы: информация о счете и информация о торговых парах.
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();
}
}
После установки леса, основной логический код торговли прост.
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);
}
}
Эта статья предоставляет простую структуру для торговли многовалютными контрактами. Используя новейший API, вы можете создавать совместимые стратегии более удобно и быстро.