3
집중하다
1444
수행원

FMZ 업그레이드 후 범용 다중 통화 거래 전략을 빠르게 구축하는 방법

만든 날짜: 2024-09-30 15:22:55, 업데이트 날짜: 2024-11-05 17:46:43
comments   4
hits   1439

FMZ 업그레이드 후 범용 다중 통화 거래 전략을 빠르게 구축하는 방법

머리말

Inventor Quantitative Trading Platform은 너무 일찍 설립되었습니다. 당시에는 거래소와 통화가 매우 제한적이었고 거래 모델도 많지 않았습니다. 따라서 초기 API 설계는 비교적 단순했고 단일 통화 거래 전략에 집중했습니다. 수년간의 반복 작업, 특히 최신 버전을 거쳐서 이제는 비교적 완벽해졌고, 일반적으로 사용되는 거래소 API는 캡슐화된 함수로 완성될 수 있습니다. 특히 다중 통화 전략의 경우 시장 정보, 계좌 및 거래를 얻는 것이 이전보다 훨씬 간단해졌으며, 거래소의 API 인터페이스에 액세스하는 데 IO 기능이 더 이상 필요하지 않습니다. 백테스팅도 실제 거래와 호환되도록 업그레이드되었습니다. 간단히 말해서, 원래 전략에 전용 방법이 많고 여러 거래소와 백테스팅에 사용할 수 없는 경우 전략을 업그레이드하는 것이 좋습니다. 이 글에서는 전략과 결합한 현재의 전략 프레임워크를 소개합니다.

관리자는 3.7로 업그레이드하여 완벽하게 지원해야 합니다. API 인터페이스의 새로운 기능에 대한 정보가 Inventor Quantitative Trading Platform의 API 문서로 업데이트되었습니다.

구문 가이드: https://www.fmz.com/syntax-guide 사용자 가이드: https://www.fmz.com/user-guide

정확도를 얻으세요

현재 API는 정확성을 얻기 위한 통합된 기능을 갖추고 있으며, 여기서는 영구 계약을 예로 들어 이를 소개합니다.

//全局的变量,储存数据,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"
            }
        }
    }
}

견적 받기

다양한 종류의 전략을 설계하려면 전체 시장 상황을 파악하는 것이 필요합니다. 이러한 통합된 시장 인터페이스는 필수적이며, GetTickers 기능은 대부분의 주요 거래소를 지원합니다.

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

계정 위치 정보 가져오기

추가 처리로 인해 발생하는 비호환성을 방지하기 위해 선물 계좌 정보에 주식 필드와 UPnL 필드가 추가되었습니다. GetPositions 함수는 모든 포지션을 가져오는 것도 지원합니다. 한 가지 세부 사항은 실제 숫자를 얻으려면 포지션 수를 한 계약의 가치로 곱해야 한다는 것입니다. 예를 들어, OKX 영구 계약은 웹 페이지에서 수량별로 거래할 수 있지만, API는 계약 수를 기반으로 합니다.

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

거래

거래는 또한 계약 수 문제를 처리해야 합니다. 최신 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, "下单异常|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);
    }
}

상태 표시

일반적으로 계정 정보와 거래 쌍 정보라는 두 개의 표가 표시됩니다.

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

거래 로직

스캐폴딩이 설정되면 핵심 거래 로직 코드는 간단합니다. 가장 간단한 빙산 주문 전략은 다음과 같습니다.

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를 사용하면 호환 전략을 더 편리하고 빠르게 구축할 수 있습니다. 시도해 볼 가치가 있습니다.