FMZ 퀀트 트레이딩 플랫폼은 너무 일찍 설립되었다. 그 당시에는 매우 제한된 거래소와 화폐가 있었고 거래 방식도 거의 없었다. 따라서 초기 API 디자인은 단순하고 단일 통화 거래 전략에 초점을 맞추었다. 몇 년의 반복, 특히 최신 버전 후, 그것은 비교적 완전해졌으며 일반적으로 사용되는 교환 API는 캡슐화 된 기능으로 완료 될 수 있다. 특히 다화폐 전략에 대해 시장 조건, 계정 및 거래를 얻는 것은 이전보다 훨씬 간단하고, IO 기능은 더 이상 거래소 API 인터페이스에 액세스 할 필요가 없다. 백테스팅을 위해, 그것은 또한 라이브 거래와 호환되도록 업그레이드되었다. 요컨대, 당신의 전략이 원래 많은 전용 방법을 가지고 있었고 여러 거래소와 백테스팅에 사용할 수 없다면, 당신은 업그레이드 할 수 있다. 이 기사에서는 현재의 조합에서 전략 아키텍처를 소개할 것이다.
도커는 완전히 지원하기 위해 3.7으로 업그레이드해야합니다. API 인터페이스의 새로운 기능에 대한 정보는 FMZ 퀀트 트레이딩 플랫폼의 API 문서에 업데이트되었습니다:
문법 안내: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"
}
}
}
}
멀티 제품 전략을 설계하려면 전체 시장의 시장 정보를 얻어야 합니다. 이 집계된 시장 정보 인터페이스는 필수적입니다. GetTickers 기능은 대부분의 주류 거래소를 지원합니다.
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);
}
}
퓨처스 계정 정보에 주식 필드와 UPnL 필드가 추가되어 추가 처리로 인한 호환성이 필요 없게 되었습니다. GetPositions 함수는 또한 모든 포지션을 얻는 것을 지원합니다. 한 세부 사항은 실제 숫자를 얻기 위해 포지션 수를 계약의 값으로 곱해야 한다는 것입니다. 예를 들어, OKX 영구 계약은 웹 페이지에서 양으로 거래 할 수 있지만 API는 계약 수를 기반으로합니다.
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를 사용하여 더 편리하고 빠르게 호환 가능한 전략을 구축 할 수 있습니다. 시도 할 가치가 있습니다.