资源加载中... loading...

FMZ升级后,如何快捷构建通用多币种交易策略

Author: 小草, Created: 2024-09-30 15:22:55, Updated: 2024-11-05 17:46:43

img

前言

发明者量化交易平台成立的时间实在太早了,当时交易所和币种都非常有限,交易模式也不多,因此初始API设计比较简单,专注与单币种交易策略。经过多年的迭代,特别是最近的版本,已经比较完善,常用交易所API都可以用封装好的函数完成了。特别是多币种策略,获取行情、账户和交易都比以前简化了很多,不再需要IO函数访问交易所的API接口了。对于回测,也进行了升级,和实盘兼容。总之,如果你的策略原来有许多专用的方法,无法兼用多交易所和回测,不妨升级一下。本文将结合策略,介绍下目前的策略架构。

托管者需要升级到3.7才能完整支持,有关API接口新增功能等信息已同步更新至发明者量化交易平台的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 "[trans]FMZ平台升级API,需要下载最新托管者|Update to neweset docekr[/trans]";
    }
    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 "[trans]不支持币本位|Don't support coin margin type[/trans]"
            }
        }
    }
}

获取行情

对于设计多品种策略,需要获取全市场行情。这种聚合行情接口是必不可少,GetTickers函数支持大部分主流交易所.

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

获取账户仓位信息

期货账户信息新增了Equity总权益字段和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("[trans]更新账户失败|Fail to get account[/trans]");
        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("[trans]更新仓位超时|Fail to get position[/trans]");
        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 "[trans]需要单向持仓|Position need net Mode:[/trans]";
        }
        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,
                "[trans]仓位更新:|Position update:[/trans]",
                _N(Info.position[symbol].value, 1),
                " -> ",
                _N(position_info[symbol].amount * Info.ticker[symbol].last, 1),
                direction == 1 ? "[trans], 买|. Buy[/trans]" : "[trans], 卖|, Sell[/trans]",
                "[trans], 成交价:| Deal price: [/trans]",
                deal_price,
                "[trans], 成本价:| Hold price: [/trans]",
                _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, "[trans]下单异常|Error on order[/trans]");
    }
}

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, "[trans]下单量太低|amount is too small[/trans]", 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: "[trans]账户信息|Account info[/trans]",
        cols: [
            "[trans]初始余额|Initial Balance[/trans]",
            "[trans]钱包余额|Wallet balance[/trans]",
            "[trans]保证金余额|Margin balance[/trans]",
            "[trans]已用保证金|Used margin[/trans]",
            "[trans]可用保证金|Avaiable margin[/trans]",
            "[trans]总收益|Profit[/trans]",
            "[trans]收益率|Profit rate[/trans]",
            "[trans]未实现收益|Unrealised profit[/trans]",
            "[trans]总持仓|Total value[/trans]",
            "[trans]已用杠杆|Leverage-used[/trans]",
            "[trans]循环延时|Delay[/trans]",
        ],
        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: "[trans]交易对信息|Symbol info[/trans]",
        cols: [
            "[trans]币种|Symbol[/trans]",
            "[trans]方向|Direction[/trans]",
            "[trans]数量|Amount[/trans]",
            "[trans]持仓价格|Hold price[/trans]",
            "[trans]持仓价值|Value[/trans]",
            "[trans]现价|Price[/trans]",
            "[trans]挂单买价|Buy price[/trans]",
            "[trans]挂单卖价|Sell price[/trans]",
            "[trans]未实现盈亏|Unrealised profit[/trans]",
        ],
        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(
        "[trans]初始化时间: | Initial date: [/trans]" + _D(new Date(Info.time.start_time)) + "\n",
        "`" + JSON.stringify(table1) + "`" + "\n" + "`" + JSON.stringify(table2) + "`\n",
        "[trans]最后执行时间: |Last run date: [/trans]" + _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("[trans]循环出错: | Loop error: [/trans]" + 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,可以更方便快捷的构建兼容策略,值得尝试。


More

张小宝lre /upload/asset/2ff0c8463217e861599ae.png 你好请问这个问题,是由于我托管者等级低的原因吗?还是什么问题呢?

ianzeng123 感谢,了解了这里面的门道有多深!

小草 SYMBOLS是策略里定义的全局变量,需要在策略参数中定义好。