3
Suivre
1444
Abonnés

Comment créer rapidement une stratégie de trading multidevises universelle après la mise à niveau FMZ

Créé le: 2024-09-30 15:22:55, Mis à jour le: 2024-11-05 17:46:43
comments   4
hits   1439

Comment créer rapidement une stratégie de trading multidevises universelle après la mise à niveau FMZ

Préface

La plateforme de trading quantitative Inventor a été créée trop tôt. À cette époque, les échanges et les devises étaient très limités et il n’y avait pas beaucoup de modèles de trading. Par conséquent, la conception initiale de l’API était relativement simple et axée sur les stratégies de trading à devise unique. Après des années d’itération, en particulier la dernière version, il est devenu relativement complet et les API d’échange couramment utilisées peuvent être complétées par des fonctions encapsulées. Surtout pour les stratégies multidevises, l’obtention d’informations sur le marché, les comptes et les transactions est beaucoup plus simple qu’auparavant, et les fonctions IO ne sont plus nécessaires pour accéder à l’interface API de la bourse. Le backtesting a également été mis à niveau pour être compatible avec le trading réel. En bref, si votre stratégie comporte à l’origine de nombreuses méthodes dédiées et ne peut pas être utilisée pour plusieurs échanges et backtests, vous souhaiterez peut-être la mettre à niveau. Cet article présentera le cadre stratégique actuel en combinaison avec les stratégies.

Les dépositaires doivent effectuer une mise à niveau vers la version 3.7 pour la prendre en charge intégralement. Les informations sur les nouvelles fonctionnalités de l’interface API ont été mises à jour dans la documentation API de la plateforme de trading quantitatif Inventor :

Guide de syntaxe : https://www.fmz.com/syntax-guide Guide d’utilisation : https://www.fmz.com/user-guide

Obtenez de la précision

Actuellement, l’API dispose d’une fonction unifiée pour obtenir la précision, qui est présentée ici en utilisant un contrat perpétuel comme exemple.

//全局的变量,储存数据,SYMBOLS代表要交易的币种,格式如"BTC,ETH,LTC", QUOTO为基础货币,永续合约常见的有USDT,USDC,INTERVAL代表循环的间隔。
var Info = { trade_symbols: SYMBOLS.split(","), base_coin: QUOTO, ticker: {}, order: {}, account: {}, precision: {}, 
            position: {}, time:{}, count:{}, interval:INTERVAL}
function InitInfo() {
    //初始化策略
    if (!IsVirtual() && Version() < 3.7){
        throw "FMZ平台升级API,需要下载最新托管者|Update to neweset docekr";
    }
    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] =  {};
    }
}
//获取精度
function GetPrecision() {
    let exchange_info = exchange.GetMarkets();
    for (let pair in exchange_info) {
        let symbol = pair.split('_')[0]; //永续合约交易对的格式为 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; //合约价值,如1张代表0.01个币
            if (exchange_info[pair].CtValCcy != symbol){ //价值的计价货币,这里不处理币本位的情况,如1张价值100美元
                throw "不支持币本位|Don't support coin margin type"
            }
        }
    }
}

Obtenir des devis

Pour concevoir une stratégie multivariée, il est nécessaire d’obtenir les conditions complètes du marché. Cette interface de marché agrégée est essentielle et la fonction GetTickers prend en charge la plupart des échanges traditionnels.

function UpdateTicker() {
    //更新价格
    let ticker = exchange.GetTickers();
    if (!ticker) {
        Log("获取行情失败|Fail to get market", 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 || !ticker[i].Symbol.endsWith('swap')) {
            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);
    }
}

Obtenir des informations sur la position du compte

Les champs Equity et UPnL ont été ajoutés aux informations des comptes à terme pour éviter les incompatibilités causées par un traitement supplémentaire. La fonction GetPositions permet également d’obtenir toutes les positions. Un détail est que le nombre de positions doit être multiplié par la valeur d’un contrat pour obtenir le nombre réel. Par exemple, bien que le contrat perpétuel OKX puisse être négocié par quantité sur la page Web, l’API est basée sur le nombre de contrats.

function UpdateAccount() {
    //更新账户
    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("更新账户失败|Fail to get account");
        return;
    }
    Info.account.margin_used = _N(account.Equity - account.Balance, 2);
    Info.account.margin_balance = _N(account.Equity, 2); //当前余额
    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("更新仓位超时|Fail to get 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
        }; //有的交易所没有仓位返回空
    }
    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 "需要单向持仓|Position need net Mode:";
        }
        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",
                ", 成交价:| Deal price: ",
                deal_price,
                ", 成本价:| Hold 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);
}

commerce

La transaction doit également tenir compte de la question du nombre de contrats. La dernière fonction CreateOrder est utilisée ici pour traiter les commandes, ce qui est beaucoup plus pratique.

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, "下单异常|Error on 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) { //两次订单有差价了才撤单
        new_order = true;
    }
    if (amount <= 0 || Info.order[symbol][direction].id == 0) { //传入amount为0 撤单
        new_order = true;
    }
    if (new_order) {
        if (Info.order[symbol][direction].id) { //原有订单撤销
            CancelOrder(symbol, direction, Info.order[symbol][direction].id);
            Info.order[symbol][direction].id = 0;
        }
        if ( //延时过高不下单
            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, "下单量太低|amount is too small", price * amount);
            return;
        }
        Order(symbol, direction, price, amount, msg);
    }
}

Affichage de l’état

Généralement, deux tableaux sont affichés : les informations sur le compte et les informations sur les paires de transactions.

unction 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",
            "总收益|Profit",
            "收益率|Profit rate",
            "未实现收益|Unrealised profit",
            "总持仓|Total value",
            "已用杠杆|Leverage-used",
            "循环延时|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: "交易对信息|Symbol info",
        cols: [
            "币种|Symbol",
            "方向|Direction",
            "数量|Amount",
            "持仓价格|Hold price",
            "持仓价值|Value",
            "现价|Price",
            "挂单买价|Buy price",
            "挂单卖价|Sell price",
            "未实现盈亏|Unrealised profit",
        ],
        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 run date: " + _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();
    }
}

Logique de transaction

Une fois l’échafaudage mis en place, le code logique de trading de base est simple. Voici la stratégie d’ordre iceberg la plus simple.

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);
        }
    }
}

Boucle principale

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);
    }
}

Résumer

Cet article fournit un cadre de trading multidevises simple à contrat perpétuel. En utilisant la dernière API, vous pouvez créer des stratégies compatibles plus facilement et plus rapidement. Cela vaut la peine d’essayer.