Biblioteca de transacciones en idioma Pine


Fecha de creación: 2022-04-20 13:45:35 Última modificación: 2026-01-08 16:19:56
Copiar: 0 Número de Visitas: 16
6
Seguir
880
Seguidores

Esta estrategia es una implementación concreta de la base de datos de transacciones de la estrategia de transacciones en lenguaje Mac. El código abierto es para que algunos programadores que quieren entender cómo funciona la lógica de transacción de la lengua de Mac

Código Fuente de la Estrategia

let NewGUI = function(name, period) {
    let chart = Chart({__isCandle: true})
    let preTime = _G("preTime")
    if (!preTime) {
        preTime = 0
    }
    if (!AutoRecover) {
        chart.reset();
        preTime = 0
    }
    return {
        feed: function(bar, runtime) {
            if ((bar.Time < preTime) || (AutoRecover && bar.Time <= preTime && RunMode == 0)) {
                return;
            }
            let obj = {
            timestamp: bar.Time,
            open: bar.Open,
            high: bar.High,
            low: bar.Low,
            close: bar.Close,
            volume: bar.Volume,
            }
            if (runtime) {
                obj.runtime = runtime
            }
            if (bar.Time == preTime) {
                chart.add(0, obj, -1)
            } else {
                chart.add(0, obj)
            }
            preTime = obj.timestamp;
            _G("preTime", obj.timestamp);
        }
    }
}

let OpMode = 0;
let MaxSpace = 0.5;

function TrySleep(n) {
    if (IsVirtual()) {
        return;
    }
    Sleep(n);
}

function isFuturesLaw(s) {
    return s == 'Futures_CTP' || s == 'Futures_Esunny' || s == 'Futures_XTP' || s == 'Futures_Futu' || s == 'Futures_IB';
}

function isFuturesCrypto(s) {
    return s.indexOf("Futures_") == 0 && !isFuturesLaw(s);
}

function SafeRetry(method) {
    let r;
    let isShow = false;
    while (!(r = method.apply(this, Array.prototype.slice.call(arguments).slice(1)))) {
        if (isFuturesLaw(exchange.GetName())) {
            while (!exchange.IO("status")) {
                if (!isShow) {
                    isShow = true;
                    Log("正在等待与期货服务器连接....");
                }
                // sleep always
                Sleep(500);
            }
        }
        TrySleep(1000);
    }
    if (isShow) {
        Log("连接成功...");
    }
    return r;
}

function DumpAccount(acc) {
    return {
        Stocks: acc.Stocks,
        Balance: acc.Balance,
        FrozenStocks: acc.FrozenStocks,
        FrozenBalance: acc.FrozenBalance,
    }
}

function TryDepth(e) {
    return SafeRetry(e.GetDepth);
}

function GetOrders2(e) {
    let eName = e.GetName()
    let orders = null;
    if (eName == 'DeepCoin') {
        orders = e.GetOrders(e.GetCurrency())
    } else if (eName == 'Futures_DeepCoin') {
        orders = e.GetOrders(e.GetCurrency()+'.'+e.GetContractType())
    } else {
        orders = e.GetOrders();
    }
    return orders;
}

function CancelPendingOrders(e, orderType) {
    let eName = e.GetName()
    while (true) {
        let orders = GetOrders2(e);
        if (!orders) {
            TrySleep(RetryDelay);
            continue;
        }
        let processed = 0;
        for (let j = 0; j < orders.length; j++) {
            if (typeof(orderType) === 'number' && orders[j].Type !== orderType) {
                continue;
            }
            e.CancelOrder(orders[j].Id, orders[j]);
            processed++;
            if (j < (orders.length - 1)) {
                TrySleep(RetryDelay);
            }
        }
        if (processed === 0) {
            break;
        }
    }
}

function GetAccount(e, waitFrozen) {
    if (typeof(waitFrozen) == 'undefined') {
        waitFrozen = false;
    }
    let account = null;
    let alreadyAlert = false;
    while (true) {
        account = _C(e.GetAccount);
        if (!waitFrozen || (account.FrozenStocks < MinStock && account.FrozenBalance < 0.01)) {
            break;
        }
        if (!alreadyAlert) {
            alreadyAlert = true;
            Log(_T("发现账户有冻结的钱或币|Find Frozen Asset"), account);
        }
        TrySleep(RetryDelay);
    }
    // Hide AccountID
    if (typeof(account.Info) !== 'undefined' && typeof(account.Info.AccountID) !== 'undefined') {
        delete account.Info.AccountID;
    }
    account.Time = _D();
    return account;
}


function StripOrders(e, orderId) {
    let order = null;
    if (typeof(orderId) == 'undefined') {
        orderId = null;
    }
    while (true) {
        let dropped = 0;
        let orders = _C(GetOrders2, e);
        for (let i = 0; i < orders.length; i++) {
            if (orders[i].Id == orderId) {
                order = orders[i];
            } else {
                let extra = "";
                if (orders[i].DealAmount > 0) {
                    extra = _T("成交: |Deal: ") + orders[i].DealAmount;
                } else {
                    extra = _T("未成交|Unfilled");
                }
                e.CancelOrder(orders[i].Id, orders[i].Type == ORDER_TYPE_BUY ? _T("买单|Buy order") : _T("卖单|Sell order"), extra);
                dropped++;
            }
        }
        if (dropped === 0) {
            break;
        }
        TrySleep(RetryDelay);
    }
    return order;
}

function getDeepAmount(books, price, t) {
    let n = 0;
    for (let i = 0; i < books.length; i++) {
        if (i == 0 || (t == ORDER_TYPE_BUY && price >= books[i].Price) || (t == ORDER_TYPE_SELL && price <= books[i].Price)) {
            n += books[i].Amount;
        } else {
            break;
        }
    }
    return n;
}


// mode = 0 : direct buy, 1 : buy as buy1
function Trade(e, tradeType, tradeAmount, mode, slideTick, maxAmount, maxSpace, retryDelay) {
    let initAccount = GetAccount(e, true);
    let nowAccount = initAccount;
    let orderId = null;
    let prePrice = 0;
    let dealAmount = 0;
    let diffMoney = 0;
    let isFirst = true;
    let tradeFunc = tradeType == ORDER_TYPE_BUY ? e.Buy : e.Sell;
    let isBuy = tradeType == ORDER_TYPE_BUY;
    let slidePrice = Math.pow(10, -ZPrecision) * slideTick;
    let totalDealNet = 0
    let totalDealAmount = 0
    let fetchOrderFail = false
    while (true) {
        let depth = TryDepth(e);
        let depthSell = depth.Asks[0].Price;
        let depthBuy = depth.Bids[0].Price;
        let tradePrice = 0;
        if (isBuy) {
            tradePrice = _N((mode === 0 ? depthSell : depthBuy) + slidePrice, ZPrecision);
            if (tradePrice <= 0) {
                tradePrice = depth.Asks[0].Price;
            }
        } else {
            tradePrice = _N((mode === 0 ? depthBuy : depthSell) - slidePrice, ZPrecision);
            if (tradePrice <= 0) {
                tradePrice = depth.Bids[0].Price;
            }
        }
        if (!orderId) {
            if (isFirst) {
                isFirst = false;
            } else {
                nowAccount = GetAccount(e, true);
            }

            let doAmount = 0;
            if (isBuy) {
                diffMoney = _N(initAccount.Balance - nowAccount.Balance, ZPrecision);
                dealAmount = nowAccount.Stocks - initAccount.Stocks;
                doAmount = _N(Math.min(maxAmount, tradeAmount - dealAmount, nowAccount.Balance / (tradePrice * (1 + TradeFee))), XPrecision);
            } else {
                diffMoney = _N(nowAccount.Balance - initAccount.Balance, ZPrecision);
                dealAmount = initAccount.Stocks - nowAccount.Stocks;
                doAmount = _N(Math.min(maxAmount, tradeAmount - dealAmount, nowAccount.Stocks), XPrecision);
            }
            if (doAmount < MinStock) {
                break;
            }
            
            let deepAmount = _N(getDeepAmount((isBuy ? depth.Asks : depth.Bids), tradePrice, isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), XPrecision);
            // spot market Amount may less than MinStock
            if (deepAmount > 0) {
                doAmount = Math.min(doAmount, deepAmount)
            }
            
            prePrice = tradePrice;
            orderId = tradeFunc(tradePrice, doAmount/*, "Depth", depth[tradeType == ORDER_TYPE_SELL ? "Asks" : "Bids"][0]*/)
            if (!orderId) {
                CancelPendingOrders(e, tradeType);
            }
        } else {
            let calcOrderId = orderId
            if (mode === 0 || (Math.abs(tradePrice - prePrice) > maxSpace)) {
                orderId = null;
            }
            let order = StripOrders(e, orderId);
            if (!order) {
                orderId = null;
            }
            // order complete or cancelled
            if (!orderId && !fetchOrderFail) {
                let historyOrder = null
                for (let i = 0; i < 10; i++) {
                    historyOrder = exchange.GetOrder(calcOrderId)
                    if (historyOrder) {
                        break
                    } else {
                        let lastErr = GetLastError()
                        if (lastErr && lastErr.toLowerCase().indexOf("not support") != -1) {
                            break
                        }
                        Log("Retry GetOrder", calcOrderId)
                        Sleep(1000)
                    }
                }
                if (historyOrder) {
                    if (historyOrder.AvgPrice > 0 && historyOrder.DealAmount > 0) {
                        totalDealNet += historyOrder.AvgPrice * historyOrder.DealAmount
                        totalDealAmount += historyOrder.DealAmount
                    }
                } else {
                    fetchOrderFail = true
                }
            }
        }
        TrySleep(SyncDelay);
    }

    // adjust diffMoney for calc real price
    if (totalDealNet > 0 && !fetchOrderFail) {
        diffMoney = totalDealNet
        dealAmount = totalDealAmount
    }

    if (dealAmount <= 0) {
        return null;
    }

    return {
        direction: tradeType == ORDER_TYPE_BUY ? PD_LONG : PD_SHORT,
        price: _N(diffMoney / dealAmount, ZPrecision),
        amount: _N(dealAmount, XPrecision),
        account: nowAccount,
    };
}

function NewFuturesTrader(e, symbol) {
    let arr = symbol.split('/');
    if (arr.length == 2 && arr[1].length > 0) {
        // 使用映射合约
        symbol = arr[1];
    }

    function GetPosition(e, contractType, direction, positions) {
        let allCost = 0;
        let allAmount = 0;
        let allProfit = 0;
        let allFrozen = 0;
        let allMargin = 0;
        let posMargin = 0;
        if (typeof(positions) === 'undefined' || !positions) {
            positions = SafeRetry(e.GetPosition);
        }
        let vPositions = []
        positions.forEach(function(pos) {
            if (pos.ContractType == contractType) {
                if (pos.Symbol && pos.Symbol.includes('.') && (!contractType.includes(".")) && pos.Symbol != e.GetCurrency() + '.' + contractType) {
                    return
                }
                vPositions.push(pos)
            }
        })

        if (typeof(direction) === 'undefined') {
            for (let i = 0; i < vPositions.length; i++) {
                let pos = vPositions[i]
                let posDirection = (pos.Type == PD_LONG || pos.Type == PD_LONG_YD) ? PD_LONG : PD_SHORT;
                if (typeof(direction) !== 'undefined' && direction != posDirection) {
                    throw "不允许持有多空双仓位";
                }
                direction = posDirection;
            }
        }
        for (let i = 0; i < vPositions.length; i++) {
            let pos = vPositions[i]
            if ((((pos.Type == PD_LONG || pos.Type == PD_LONG_YD) && direction == PD_LONG) || ((pos.Type == PD_SHORT || pos.Type == PD_SHORT_YD) && direction == PD_SHORT))
            ) {
                posMargin = pos.MarginLevel;
                allCost += (pos.Price * pos.Amount);
                allAmount += pos.Amount;
                allProfit += pos.Profit;
                allFrozen += pos.FrozenAmount;
                if (typeof(pos.Margin) === 'number') {
                    allMargin += pos.Margin;
                }
            }
        }
        if (allAmount === 0) {
            return null;
        }
        return {
            MarginLevel: posMargin,
            Margin: allMargin,
            FrozenAmount: allFrozen,
            Price: _N(allCost / allAmount, ZPrecision),
            Amount: allAmount,
            Profit: allProfit,
            Type: direction,
            ContractType: contractType
        };
    }

    function SetContractType(e, ct) {
        let eName = e.GetName();
        let insDetail = SafeRetry(e.SetContractType, ct);
        let margin = 1 / MarginLevel;
        if (isFuturesLaw(eName)) {
            if (typeof(insDetail.MinLimitOrderVolume) === 'undefined') {
                insDetail.MinLimitOrderVolume = 1
            }
            if (typeof(insDetail.MaxLimitOrderVolume) != 'number' || insDetail.MaxLimitOrderVolume == 0) {
                insDetail.MaxLimitOrderVolume = 50
                insDetail.MinLimitOrderVolume = 1
            }
            // force set to 1 some are not correct
            if (insDetail.MinLimitOrderVolume > 5) {
                insDetail.MinLimitOrderVolume = 1
            }
            if (typeof(insDetail.PriceTick) === 'number') {
                let arr = insDetail.PriceTick.toString().split('.')
                if (arr.length == 2 && ZPrecision != arr[1].length) {
                    ZPrecision = arr[1].length;
                    Log("重置定价货币精度为", ZPrecision);
                }
            }
            if (IsVirtual()) {
                insDetail.InstrumentID = ct
            }
            return insDetail;
        } else if (eName == "Futures_BitMEX") {
            return {
                InstrumentID: ct,
                MaxLimitOrderVolume: 1000000,
                MinLimitOrderVolume: 1,
                VolumeMultiple: 1,
                LongMarginRatio: margin,
                ShortMarginRatio: margin,
                IsTrading: true,
                PriceTick: Math.max(0.5, Math.pow(10, -ZPrecision))
            }
        } else {
            let baseCurrency = exchange.GetCurrency().split('_')[0]
            let info = {
                InstrumentID: ct,
                MaxLimitOrderVolume: 1000000,
                MinLimitOrderVolume: 0.001,
                IsTrading: true,
                LongMarginRatio: margin,
                ShortMarginRatio: margin,
                VolumeMultiple: eName == "Futures_Deribit" ? 10 : (baseCurrency == 'BTC' ? 100 : 10),
                PriceTick: Math.pow(10, -ZPrecision)
            }
            if (eName == 'Futures_Binance' && ct == 'swap') {
                MinLot = 0.001
            }
            if (exchange.GetCurrency().indexOf("_USDT") != -1) {
                info.VolumeMultiple = 1
            } else {
                info.VolumeMultiple = -info.VolumeMultiple
            }
            return info
        }
    }

    function Open(contractType, direction, opAmount, slideTick) {
        if (typeof(slideTick) !== 'number') {
            slideTick = 2;
        }
        let insDetail = SetContractType(e, contractType);
        if (typeof(insDetail.MaxLimitOrderVolume) != 'number' || insDetail.MaxLimitOrderVolume == 0) {
            insDetail.MaxLimitOrderVolume = 50
            insDetail.MinLimitOrderVolume = 1
        }
        let tradeSymbol = insDetail.InstrumentID;
        
        // 切换到真正的主力合约上
        if (tradeSymbol != contractType) {
            SetContractType(e, tradeSymbol);
        }


        let positions = SafeRetry(e.GetPosition);
        for (let i = 0; i < positions.length; i++) {
            if (positions[i].ContractType != tradeSymbol) {
                continue;
            }
            if (((positions[i].Type == PD_LONG || positions[i].Type == PD_LONG_YD) && direction != PD_LONG) ||
                ((positions[i].Type == PD_SHORT || positions[i].Type == PD_SHORT_YD) && direction != PD_SHORT)) {
                throw _T("发现有相反仓位, 终止开仓|Discover the opposite position and terminate opening");
            }
        }
        let initPosition = GetPosition(e, tradeSymbol, direction, positions);
        let isFirst = true;
        let initAmount = initPosition ? initPosition.Amount : 0;
        let positionNow = initPosition;
        let retryCount = 0;
        while (true) {
            let needOpen = opAmount;
            if (isFirst) {
                isFirst = false;
            } else {
                positionNow = GetPosition(e, tradeSymbol, direction);
                if (positionNow) {
                    needOpen = opAmount - (positionNow.Amount - initAmount);
                }
            }
            //Log("初始持仓", initAmount, "当前持仓", positionNow, "需要加仓", needOpen);
            if (needOpen < insDetail.MinLimitOrderVolume) {
                if (needOpen > 0) {
                    Log(tradeSymbol, "交易忽略, 交易所限制最小下单量", insDetail.MinLimitOrderVolume, "当前", needOpen, "当前持仓", positionNow, "#676767");
                }
                break
            }
            let err = GetLastError();
            if (err.indexOf('资金不足') != -1) {
                Log(err);
                break;
            }
            let depth = TryDepth(e);
            let amount = Math.min(MaxAmountOnce, Math.min(insDetail.MaxLimitOrderVolume, needOpen));
            e.SetDirection(direction == PD_LONG ? "buy" : "sell");
            let orderId;
            let deepAmount;
            if (direction == PD_LONG) {
                let buyPrice = _N(depth.Asks[0].Price + (insDetail.PriceTick * slideTick), ZPrecision);
                deepAmount = getDeepAmount(depth.Asks, buyPrice, ORDER_TYPE_BUY);
                orderId = e.Buy(buyPrice, _N(Math.min(amount, deepAmount), XPrecision), tradeSymbol, 'ask', depth.Asks[0]);
            } else {
                let sellPrice = _N(depth.Bids[0].Price - (insDetail.PriceTick * slideTick), ZPrecision);
                if (sellPrice <= 0) {
                    sellPrice = depth.Bids[0].Price
                }
                deepAmount = getDeepAmount(depth.Bids, sellPrice, ORDER_TYPE_SELL);
                orderId = e.Sell(sellPrice, _N(Math.min(amount, deepAmount), XPrecision), tradeSymbol, 'bid', depth.Bids[0]);
            }
            if (!orderId) {
                retryCount++;
            }
            if (retryCount > MaxRetry) {
                break;
            }
            // CancelPendingOrders
            while (true) {
                TrySleep(SyncDelay);
                let orders = SafeRetry(GetOrders2, e);
                if (orders.length === 0) {
                    break;
                }

                let nowDepth = TryDepth(e)
                if (nowDepth.Asks[0].Price == depth.Asks[0].Price && nowDepth.Bids[0].Price == depth.Bids[0].Price) {
                    continue
                }
    
                for (let j = 0; j < orders.length; j++) {
                    e.CancelOrder(orders[j].Id);
                    if (j < (orders.length - 1)) {
                        TrySleep(SyncDelay);
                    }
                }
            }
        }

        if (!positionNow) {
            return null;
        }
        let ret = {
            price: 0,
            amount: 0,
            direction: direction,
            position: positionNow,
            symbol: tradeSymbol,
        };
        if (!initPosition) {
            ret.price = positionNow.Price;
            ret.amount = positionNow.Amount;
        } else {
            ret.amount = _N(positionNow.Amount - initPosition.Amount, XPrecision);
            ret.price = _N(((positionNow.Price * positionNow.Amount) - (initPosition.Price * initPosition.Amount)) / ret.amount);
        }
        if (ret.amount == 0) {
            return null;
        }
        return ret;
    }

    function Cover(contractType, coverType, lots, slideTick) {
        if (typeof(slideTick) !== 'number') {
            slideTick = 2;
        }
        let insDetail = SetContractType(e, contractType);
        if (typeof(insDetail.MaxLimitOrderVolume) != 'number' || insDetail.MaxLimitOrderVolume == 0) {
            insDetail.MaxLimitOrderVolume = 50
            insDetail.MinLimitOrderVolume = 1
        }
        let tradeSymbol = insDetail.InstrumentID;
        if (tradeSymbol != contractType) {
            SetContractType(e, tradeSymbol);
        }
        let isCTP = e.GetName() == "Futures_CTP" || e.GetName() == 'Futures_Esunny';
        let initAmount = 0;
        let firstLoop = true;
        let totalDealAmount = 0;
        let totalDealNet = 0;
        while (true) {
            let n = 0;
            let positions = SafeRetry(e.GetPosition);
            let nowAmount = 0;
            for (let i = 0; i < positions.length; i++) {
                if (positions[i].ContractType != tradeSymbol) {
                    continue;
                }
                if (typeof(coverType) === 'undefined' || (coverType == PD_LONG && (positions[i].Type == PD_LONG || positions[i].Type == PD_LONG_YD)) ||
                    (coverType == PD_SHORT && (positions[i].Type == PD_SHORT || positions[i].Type == PD_SHORT_YD))) {
                    nowAmount += positions[i].Amount;
                }
            }
            if (firstLoop) {
                initAmount = nowAmount;
                firstLoop = false;
            }
            let amountChange = initAmount - nowAmount;
            if (typeof(lots) == 'number' && amountChange >= lots) {
                break;
            }

            let orderIds = [];
            let depth = null;
            for (let i = 0; i < positions.length; i++) {
                if (positions[i].ContractType != tradeSymbol) {
                    continue;
                }

                let canCover = typeof(coverType) === 'undefined' ||
                    (coverType == PD_LONG && (positions[i].Type == PD_LONG || positions[i].Type == PD_LONG_YD)) ||
                    (coverType == PD_SHORT && (positions[i].Type == PD_SHORT || positions[i].Type == PD_SHORT_YD));
                if (!canCover) {
                    continue;
                }
                let amount = Math.min(MaxAmountOnce, insDetail.MaxLimitOrderVolume, positions[i].Amount);
                let deepAmount = 0
                let opPrice = 0;
                let orderId = null;
                if (positions[i].Type == PD_LONG || positions[i].Type == PD_LONG_YD) {
                    depth = TryDepth(e);
                    opPrice = depth.Bids[0].Price - (insDetail.PriceTick * slideTick);
                    if (opPrice <= 0) {
                        opPrice = depth.Bids[0].Price
                    }
                    deepAmount = getDeepAmount(depth.Bids, opPrice, ORDER_TYPE_SELL);
                } else if (positions[i].Type == PD_SHORT || positions[i].Type == PD_SHORT_YD) {
                    depth = TryDepth(e);
                    opPrice = depth.Asks[0].Price + (insDetail.PriceTick * slideTick);
                    deepAmount = getDeepAmount(depth.Asks, opPrice, ORDER_TYPE_BUY)
                }
                let opAmount = amount;
                if (typeof(lots) === 'number') {
                    opAmount = Math.min(opAmount, lots - (initAmount - nowAmount));
                }
                opPrice = _N(opPrice, ZPrecision);
                opAmount = _N(opAmount, XPrecision);
                if (opAmount >= MinLot || positions[i].Amount < MinLot) {
                    if (deepAmount > 0) {
                        opAmount = Math.min(opAmount, deepAmount);
                    }
                    // FIXED
                    if ((positions[i].Amount - opAmount) < MinLot) {
                        opAmount = positions[i].Amount
                    }
                    if (positions[i].Type == PD_LONG || positions[i].Type == PD_LONG_YD) {
                        e.SetDirection((isCTP && positions[i].Type == PD_LONG) ? "closebuy_today" : "closebuy");
                        orderId = e.Sell(opPrice, opAmount, tradeSymbol, (isCTP ? (positions[i].Type == PD_LONG ? "平今" : "平昨") : ''), 'bid', depth.Bids[0]);
                    } else {
                        e.SetDirection((isCTP && positions[i].Type == PD_SHORT) ? "closesell_today" : "closesell");
                        orderId = e.Buy(opPrice, opAmount, tradeSymbol, (isCTP ? (positions[i].Type == PD_SHORT ? "平今" : "平昨") : ''), 'ask', depth.Asks[0]);
                    }
                    if (orderId) {
                        orderIds.push(orderId)
                    }
                    n++
                }
                // break to check always
                if (typeof(lots) === 'number') {
                    break;
                }
            }
            if (n === 0) {
                break;
            }
            while (true) {
                TrySleep(SyncDelay);
                let orders = SafeRetry(GetOrders2, e);
                if (orders.length === 0) {
                    break;
                }
                if (depth) {
                    let nowDepth = TryDepth(e)
                    if (nowDepth.Asks[0].Price == depth.Asks[0].Price && nowDepth.Bids[0].Price == depth.Bids[0].Price) {
                        continue
                    }
                }
                for (let j = 0; j < orders.length; j++) {
                    e.CancelOrder(orders[j].Id);
                    if (j < (orders.length - 1)) {
                        TrySleep(SyncDelay);
                    }
                }
            }
            // calcAvgPrice
            if (orderIds.length > 0) {
                orderIds.forEach(function(id) {
                    let order = SafeRetry(exchange.GetOrder, id)
                    if (order.DealAmount > 0) {
                        totalDealNet += order.AvgPrice * order.DealAmount
                        totalDealAmount += order.DealAmount
                    }
                })
            }
        }
        if (totalDealAmount <= 0) {
            return null
        }
        return {
            direction: coverType == PD_LONG ? PD_SHORT : PD_LONG,
            price: _N(totalDealNet / totalDealAmount, ZPrecision),
            amount: _N(totalDealAmount, XPrecision)
        };
    }
    let obj = {};
    let lastTradeKey = "lastTradeSymbol";
    obj.symbol = symbol;
    obj.lastSync = 0;
    obj.insDetail = SetContractType(e, symbol);
    
    if (e.GetName() == 'Futures_XTP' && obj.insDetail.VolumeMultiple > 1) {
        TradeAmount = TradeAmount * obj.insDetail.VolumeMultiple;
    }
    obj.isAbstract = isFuturesLaw(e.GetName()) && (symbol.indexOf('888') != -1 || symbol.indexOf('000') != -1);
    obj.positions = null;
    obj.lastTradeSymbol = _G(lastTradeKey) || obj.insDetail.InstrumentID;
    
    obj.GetTradeSymbol = function() {
        return obj.lastTradeSymbol;
    }
    obj.Poll = function() {
        // Update Symbol for main symbol or IsTrading exchange
        obj.insDetail = SetContractType(e, obj.symbol);

        if (!obj.isAbstract) {
            return;
        }
        if (obj.lastTradeAmount == 0 || !obj.positions) {
            return;
        }

        if (obj.insDetail.InstrumentID != obj.lastTradeSymbol) {
            // Try Switch symbol
            let pos = GetPosition(e, obj.lastTradeSymbol, undefined, null);
            if (pos && pos.Amount > 0) {
                Log("主力合约切换", obj.lastTradeSymbol, "->", obj.insDetail.InstrumentID, "移仓数量", pos.Amount, "#0000ff");
                Cover(pos.ContractType, pos.Type, pos.Amount, SlideTick);
                Log(pos.ContractType, "平仓结束");
                let r = Open(obj.insDetail.InstrumentID, pos.Type, pos.Amount, SlideTick);
                Log("移仓完成:", r);
            }
            _G(lastTradeKey, obj.insDetail.InstrumentID);
            obj.lastTradeSymbol = obj.insDetail.InstrumentID;
        }
    }
    
    obj.Sync = function(force) {
        if ((new Date().getTime() - obj.lastSync) > (SyncTime * 1000) || (typeof(force) !== 'undefined' && force)) {
            obj.nowAccount = SafeRetry(e.GetAccount);
            obj.nowAccount.Time = _D();
            obj.positions = SafeRetry(e.GetPosition);
            obj.lastSync = new Date().getTime();
        }
    }
    obj.GetInitAccount = function() {
        return obj.initAccount;
    }
    obj.GetAccount = function() {
        obj.Sync();
        return obj.nowAccount;
    }

    obj.GetPosition = function() {
        let insDetail = SetContractType(e, obj.symbol);
        let tradeSymbol = insDetail.InstrumentID;
        obj.Sync();
        return GetPosition(e, tradeSymbol, undefined, obj.positions)
    }
    obj.OpenShort = function(amount) {
        let r = Open(obj.symbol, PD_SHORT, amount, SlideTick);
        if (r) {
            obj.lastTradeSymbol = r.symbol;
        }
        obj.Sync(true);
        return r;
    }
    obj.OpenLong = function(amount) {
        let r = Open(obj.symbol, PD_LONG, amount, SlideTick);
        if (r) {
            obj.lastTradeSymbol = r.symbol;
        }
        obj.Sync(true);
        return r;
    }
    obj.CloseShort = function(amount) {
        let r = Cover(obj.symbol, PD_SHORT, amount, SlideTick);
        obj.Sync(true);
        return r
    }
    obj.CloseLong = function(amount) {
        let r = Cover(obj.symbol, PD_LONG, amount, SlideTick);
        obj.Sync(true);
        return r
    }
    obj.GetInsDetail = function() {
        return {
            MARGIN: Math.max(obj.insDetail.LongMarginRatio, obj.insDetail.ShortMarginRatio),
            UNIT: obj.insDetail.VolumeMultiple,
            MINPRICE: obj.insDetail.PriceTick
        };
    }

    obj.IsTrading = function() {
        let eName = e.GetName()
        if (eName == 'Futures_IB') {
            obj.insDetail = SafeRetry(e.SetContractType, obj.symbol, "liquid")
            if (typeof(obj.insDetail.IsTrading) !== 'undefined') {
                return obj.insDetail.IsTrading
            }
        } else if (eName == 'Futures_CTP') {
            let d = new Date();
            let day = d.getDay();
            let hour = d.getHours();
            let minute = d.getMinutes();
            if (day === 0 || (day == 6 && hour >= 3)) {
                return false;
            }
        
            let period = [
                [8, 55, 11, 35],
                [12, 55, 15, 20],
                [20, 55, 24, 0],
                [0, 0, 2, 35]
            ];
        
            for (let i = 0; i < period.length; i++) {
                let p = period[i];
                if ((hour > p[0] || (hour == p[0] && minute >= p[1])) && (hour < p[2] || (hour == p[2] && minute < p[3]))) {
                    return true;
                }
            }
            return false; 
        }
        return true
    }

    if (isFuturesCrypto(exchange.GetName())) {
        exchange.SetMarginLevel(MarginLevel);
    }

    // init
    obj.initAccount = GetAccount(e);
    obj.nowAccount = obj.GetAccount();
    let accountRecover = false;
    if (AutoRecover && !IsVirtual()) {
        let s = _G("_account");
        if (s) {
            obj.initAccount = JSON.parse(s);
            Log(_T("恢复账户信息成功|Restore account success"), obj.initAccount);
            accountRecover = true;
        } else {
            Log(_T("没有发现恢复信息, 初始化运行|Init account success"));
        }
        if (e.GetName().indexOf("Futures_") == -1) {
            let pos = _G("_pos");
            if (pos) {
                obj.pos = JSON.parse(pos);
                Log(_T("恢复持仓信息成功|Restore position success"));
            } else {
                Log(_T("没有发现持仓信息|Init position success"));
            }
        }
    }

    if (!accountRecover) {
        let eName = e.GetName();
        if (eName.indexOf('Futures_') == -1) {
            let ele = obj.GetPosition()
            if (ele) {
                obj.initAccount.Balance += ele.Margin + ele.Profit;
            }
        } else {
            let positions = _C(exchange.GetPositions);
            positions.forEach(function(ele) {
                if (isFuturesLaw(eName)) {
                    obj.initAccount.Balance += ele.Margin + ele.Profit;
                } else {
                    let realMargin = ele.Margin;
                    if (eName == 'Futures_BitMEX') {
                        realMargin = ele.Amount/ele.Price/ele.MarginLevel;
                    }
                    obj.initAccount.Stocks += realMargin + ele.Profit;
                }
            });
        }
    }
    _G("_account", JSON.stringify(obj.initAccount));
    Log(_T("账户信息初始化成功|Initialize success"), obj.initAccount);
    if (typeof(e.GetAssets) === 'function') {
        Log('Assets:', e.GetAssets())
    }
    return obj;
}


$.NewTradeManager = function() {
    let e = exchange;
    if (e.GetName().indexOf("Futures") !== -1) {
        return NewFuturesTrader(e, ContractType);
    }
    let obj = {};
    obj.lastSync = 0;
    obj.pos = {Amount: 0, Net: 0, Type: PD_LONG};
    obj.Sync = function(r, t, isCover) {
        if (!r) {
            return;
        }
        if (isCover) {
            let diff = (r.account.Stocks + r.account.FrozenStocks) - (obj.initAccount.Stocks + obj.initAccount.FrozenStocks);
            if (Math.abs(diff) <= 0 || Math.abs(diff) < MinStock) {
                obj.pos.Amount = 0;
                obj.pos.Net = 0;
            }
            return;
        }
        // reset
        if (obj.pos.Type != t) {
            obj.pos.Amount = 0;
            obj.pos.Net = 0;
            obj.pos.Type = t;
        }
        obj.pos.Amount += r.amount;
        obj.pos.Net += r.amount * r.price;
        _G("_pos", JSON.stringify(obj.pos));
    }
    obj.GetInitAccount = function() {
        return obj.initAccount;
    }
    obj.GetAccount = function() {
        let now = new Date().getTime();
        if ((now - obj.lastSync) > (SyncTime * 1000)) {
            obj.nowAccount = GetAccount(e);
            obj.lastSync = now;
        }
        return obj.nowAccount;
    }
    obj.GetTradeSymbol = function() {
        return exchange.GetCurrency();
    }

    obj.GetPosition = function() {
        obj.GetAccount();
        let diff = (obj.nowAccount.Stocks + obj.nowAccount.FrozenStocks) - (obj.initAccount.Stocks + obj.initAccount.FrozenStocks);
        if (Math.abs(diff) <= 0 || Math.abs(diff) < MinStock) {
            return null;
        }
        let amount = _N(Math.abs(diff), XPrecision);
        let price = _N(((obj.nowAccount.Balance + obj.nowAccount.FrozenBalance) - (obj.initAccount.Balance + obj.initAccount.FrozenBalance)) / -diff, ZPrecision);
        let margin = price * amount;
        
        if (obj.pos.Amount > 0) {
            price = _N(obj.pos.Net / obj.pos.Amount, ZPrecision);
        }

        if (diff > 0) {
            return {
                Amount: amount,
                Type: PD_LONG,
                Price: price,
                Margin: margin
            };
        } else {
            return {
                Amount: amount,
                Type: PD_SHORT,
                Price: price,
                Margin: margin
            };
        }
        return null;
    }

    obj.Buy = function(amount) {
        let r = Trade(e, ORDER_TYPE_BUY, amount, OpMode, SlideTick, MaxAmountOnce, MaxSpace, RetryDelay);
        if (r) {
            obj.nowAccount = r.account;
        }
        return r;
    };

    obj.Sell = function(amount) {
        let r = Trade(e, ORDER_TYPE_SELL, amount, OpMode, SlideTick, MaxAmountOnce, MaxSpace, RetryDelay);
        if (r) {
            obj.nowAccount = r.account;
        }
        return r;
    };

    obj.OpenShort = function(amount) {
        let r = obj.Sell(amount);
        obj.Sync(r, PD_SHORT, false);
        return r;
    }
    
    obj.CloseLong = function(amount) {
        let r = obj.Sell(amount);
        obj.Sync(r, PD_LONG, true);
        return r;
    }
    
    obj.OpenLong = function(amount) {
        let r = obj.Buy(amount);
        obj.Sync(r, PD_LONG, false);
        return r;
    }
    obj.CloseShort = function(amount) {
        let r = obj.Buy(amount);
        obj.Sync(r, PD_SHORT, true);
        return r;
    }

    obj.GetInsDetail = function() {
        return {
            MARGIN: 1,
            UNIT: Lot,
            MINPRICE: Math.pow(10, -ZPrecision)
        };
    }

    obj.IsTrading = function() {
        return true
    }
    
    obj.Poll = function() {
    }

    // init
    obj.initAccount = GetAccount(e);
    obj.nowAccount = GetAccount(e);
    if (AutoRecover && !IsVirtual()) {
        let s = _G("_account");
        if (s) {
            obj.initAccount = JSON.parse(s);
            Log(_T("恢复账户信息成功|Restore Account success"), obj.initAccount);
        } else {
            Log(_T("没有发现恢复信息, 初始化运行|init Account success"));
        }
    }
    _G("_account", JSON.stringify(obj.initAccount));
    Log(_T("账户信息初始化成功|Initialize success"), obj.initAccount);
    return obj;
}

$.GetContractType = function() {
    if (exchange.GetName().indexOf('Futures') != -1) {
        if (typeof(ContractType) !== 'string' || ContractType == '') {
            throw _T("请先在参数里设置合约|Please set contracttype first");
        }
        // 返回数据合约
        return ContractType.split('/')[0];
    }
    return ContractType;
}

$.GetTradeArgs = function() {
    return {qty: TradeAmount, minQty: (exchange.GetName().indexOf("Futures_") == 0 ? MinLot : MinStock), zPrecision: ZPrecision, xPrecision: XPrecision}
}

$.SaveCtx = function(ctx) {
    _G("_ctx", JSON.stringify(ctx));
};

$.GetCtx = function(ctx) {
    let r = null;
    if (AutoRecover && !IsVirtual()) {
        let s = _G("_ctx");
        if (s) {
            r = JSON.parse(s);
            Log(_T("恢复状态信息成功..|Restore status success..."));
        } else {
            Log(_T("没有发现有需要恢复的状态信息|No status to restore"));
        }
    }
    return r;
}

$.GetMode = function() {
    return RunMode;
}

$.GetInterval = function() {
    if (typeof(LoopInterval) === 'undefined') {
        LoopInterval = 500;
    }
    return LoopInterval;
}

$.GetSettings = function() {
    return {
        maxBarLen: MaxCacheLen,
    }
}


function init() {
    if (exchanges.length == 0) {
        throw _T("没有添加交易所||No exchange found")
    }
    if (typeof(SwitchBase) !== 'undefined' && SwitchBase) {
        BaseAddr = BaseAddr.replace(/\/$/, '')
        exchange.SetBase(BaseAddr);
        Log("切换其地址到", BaseAddr);
    }
    let eid = exchange.GetName();


    let recoverKey = eid+'/'+exchange.GetPeriod()+'/'+exchange.GetCurrency()+'/'+exchange.GetContractType();
    if (AutoRecover && (_G("ename") !== recoverKey)) {
        //Log(_T("交易配置有变化, 禁用恢复模式|Exchange or symbol or period is changed, set recover mode to init"));
        AutoRecover = false;
    }
    if (!AutoRecover) {
        LogReset();
        LogProfitReset();
        LogStatus("Ready")
        Chart({}).reset();
        _G(null);
        if (!IsVirtual()) {
            Log(_T("已重置所有信息..|Reset Everything"));
        }
    }
    _G("ename", recoverKey);
    if (eid == 'Futures_Futu' || eid == 'Futures_IB') {
        ContractType = StockType
    }

    // calc MaxBarLen
    let basePeriods = [PERIOD_M1, PERIOD_M15, PERIOD_D1];
    let period = exchange.GetPeriod()
    let basePeriod = period
    for (let i = basePeriods.length-1; i>=0; i--) {
        if (period >= basePeriods[i]) {
            basePeriod = basePeriods[i]
        }
    }

    exchange.SetMaxBarLen(Math.ceil((period / basePeriod) * 1.1 * MaxCacheLen))
    
    if (UseProxy && typeof(ProxyAddr) === 'string' && ProxyAddr.indexOf('socks5') !== -1) {
        exchange.SetProxy(ProxyAddr);
        Log(_T("设置代理成功..|Set Proxy success"));
    }

    let isSetPrecision = false;
    if (AutoSetPrecision) {
        if (eid.indexOf("Futures_") == 0) {
            exchange.SetContractType(ContractType)
        }
        let markets = exchange.GetMarkets();
        let detail = markets[ContractType] || markets[exchange.GetCurrency()] || markets[exchange.GetCurrency() + '.' + ContractType]
        if (detail) {
            ZPrecision = detail.PricePrecision
            XPrecision = detail.AmountPrecision
            isSetPrecision = true;
            exchange.SetPrecision(ZPrecision, XPrecision);
            Log("Auto set ZPrecision: ", ZPrecision, "XPrecision:", XPrecision, detail)
        } else {
            Log("AutoSetPrecision failed")
        }
    }

    if (!isSetPrecision) {
        if (eid == 'Futures_BitMEX' && (ZPrecision != 1 || XPrecision != 8)) {
            ZPrecision = 1;
            XPrecision = 8;
            exchange.SetPrecision(ZPrecision, XPrecision);
            Log(_T("已经强制指定BitMEX精度|Force set bitmex precision success"));
        } else if (exchange.GetCurrency().indexOf("_BTC") !== -1) {
            ZPrecision = 8;
            XPrecision = 8;
            exchange.SetPrecision(ZPrecision, XPrecision);
            Log(_T("已经强制指定币币交易精度|Force set precision success"));
        } else if (isFuturesLaw(eid)) {
            XPrecision = 0;
            Log(_T("已经强制指定XPrecision, 交易精度为0|Force set precision success"));
        } else {
            if (!IsVirtual()) {
                Log("ZPrecision: ", ZPrecision, "XPrecision:", XPrecision)
            }
        }
    }
    if (HideError) {
        SetErrorFilter("not login|not ready|settlement|初始化|timeout|初始化|未就绪|Too Many");
    }
}

let gui = null;
let preSignalTime = _G("preSignalTime") || 0;
let _preHold = 0;
let _statusInit = false
$.UpdateStatus = function(scope) {
    if (WXNotify) {
        if (scope.tradeCtx.preSignalObj && scope.tradeCtx.preSignalObj.time != preSignalTime) {
            preSignalTime = scope.tradeCtx.preSignalObj.time;
            _G("preSignalTime", preSignalTime);
            
            Log(_T("信号|Signal"), scope.tradeCtx.preSignalObj.direction, (scope.tradeCtx.preSignalObj.direction=='exit' ? '' : _T('数量|Qty') + ': ' + scope.tradeCtx.preSignalObj.qty), "@");
        }
    }
    let forceGUI = typeof(scope.gui) !== 'undefined' && scope.gui;

    if ((!IsVirtual()) || forceGUI) {
        if (!gui) {
            gui = NewGUI(exchange.GetName() + "_" + scope.env.symbol, exchange.GetPeriod());
        }
        /*
        _.each(scope.cache.bar_signal, function(ele) {
            scope.runtime.push([scope.line, ele]);
        });
        */
        gui.feed(scope.bar, scope.runtime);
    }
    
    if (_statusInit && !scope.canTrade) {
        return;
    }
    
    if (!_statusInit) {
        _statusInit = true
    }
    
    let trader = scope.env ? scope.env.trader : scope.ctx.trader;

    let initAccount = trader.GetInitAccount();
    let account = trader.GetAccount();
    let quoteCurrency = exchange.GetQuoteCurrency();
    let isUBase = quoteCurrency == 'USDT' || quoteCurrency == 'BUSD' || quoteCurrency == 'USDC';
    let str_s = _T('初始|Init');
    let str_n = _T('当前|Current')+'#4d99eb';
    let str_diff = _T('差异|Diff')+'#d24a3d';
    let str_free = _T('可用的|Free');
    let str_frozen = _T('冻结的|Frozen');
    let str_update = _T('数据时间|Update');
    let str_label = exchange.GetLabel();
    let tAsset = {
        type: 'table',
        title: _T('资金信息|Asset'),
        cols: [str_label, str_free + quoteCurrency, str_frozen + quoteCurrency, str_update],
        rows: [
            [str_n, _N(account.Balance, ZPrecision), _N(account.FrozenBalance, ZPrecision), account.Time],
            [str_s, _N(initAccount.Balance, ZPrecision), _N(initAccount.FrozenBalance, ZPrecision), initAccount.Time],
            [str_diff, _N(account.Balance - initAccount.Balance, ZPrecision) + '#ff0000', _N(account.FrozenBalance - initAccount.FrozenBalance, ZPrecision), _D()]
        ]
    };
    let tPending = {
        type: 'table',
        title: _T('计划订单|Schedule orders'),
        cols: ['id', 'direction', 'limit', 'stop', 'trailPrice', 'trailOffset', 'qty', 'time'],
        rows: []
    };
    let tOpenOrders = {
        type: 'table',
        title: _T('入场订单|Open orders'),
        cols: ['id', 'direction', 'avgPrice', 'highest', 'lowest', 'qty', 'qtyRemain', 'profit', 'time'],
        rows: []
    };
    scope.tradeCtx.status.orders.forEach(function(order) {
        tOpenOrders.rows.push([order.id, order.direction, _N(order.avgPrice, ZPrecision), _N(order.highestPrice, ZPrecision), _N(order.lowestPrice, ZPrecision), _N(order.qty, XPrecision), _N(order.qtyRemain, XPrecision), _N(order.realizeProfit+order.unrealizeProfit, ZPrecision), _D(order.time)]);
    })
    
    let ids = Object.keys(scope.tradeCtx.status.pending)
    ids.forEach(function(id) {
        let order = scope.tradeCtx.status.pending[id]
        tPending.rows.push([order.id, order.direction + (order.fromEntry ? (' [' + order.fromEntry + ']') : ''), (typeof(order.limit) == 'number' ? order.limit : ''), (typeof(order.stop) == 'number' ? order.stop : ''), 
        (typeof(order.trailPrice) == 'number' ? order.trailPrice : ''),
        (typeof(order.trailOffset) == 'number' ? order.trailOffset : ''), order.qty,  _D(order.time)])
    })
    
    if (!isFuturesLaw(exchange.GetName())) {
        let arr = exchange.GetCurrency().split('_');
        let stockName = arr[0];
        if (exchange.GetName().indexOf("Futures_") !== -1) {
            if (!isUBase) {
                tAsset.cols = [str_label, str_free + stockName, str_frozen + stockName, str_update];
                tAsset.rows = [
                    [str_n, _N(account.Stocks, XPrecision), _N(account.FrozenStocks, XPrecision), account.Time],
                    [str_s, _N(initAccount.Stocks, XPrecision), _N(initAccount.FrozenStocks, XPrecision), initAccount.Time],

                    [str_diff, _N(account.Stocks - initAccount.Stocks, XPrecision) + '#ff0000', _N(account.FrozenStocks - initAccount.FrozenStocks, XPrecision), _D()]

                ];
            }
        } else {
            tAsset.cols = [str_label, str_free + quoteCurrency, str_frozen + quoteCurrency, str_free + stockName, str_frozen + stockName, str_update];
            tAsset.rows = [
                [str_n, _N(account.Balance, ZPrecision), _N(account.FrozenBalance, ZPrecision), _N(account.Stocks, XPrecision), _N(account.FrozenStocks, XPrecision), account.Time],
                [str_s, _N(initAccount.Balance, ZPrecision), _N(initAccount.FrozenBalance, ZPrecision), _N(initAccount.Stocks, XPrecision), _N(initAccount.FrozenStocks, XPrecision), initAccount.Time],
                [str_diff, _N(account.Balance - initAccount.Balance, ZPrecision) + '#ff0000', _N(account.FrozenBalance - initAccount.FrozenBalance, ZPrecision),
                    _N(account.Stocks - initAccount.Stocks, XPrecision) + '#ff0000', _N(account.FrozenStocks - initAccount.FrozenStocks, XPrecision), _D()
                ]
            ];
        }
    }

    LogStatus('`' + JSON.stringify([tAsset, tOpenOrders, tPending]) + '`');
    
    if (forceGUI || !IsVirtual()) {
        let profit = null;
        let preAccTime = _G("_accTime");
        let accTime = AccInterval == 0 ? new Date().getHours() : new Date().getDate();
        let pos = scope.env.trader.GetPosition()
        let nowHold = pos ? pos.Amount : 0
        let isCover = _preHold > 0 && nowHold == 0;
        _preHold = nowHold;
        if (isCover || (accTime != preAccTime)) {
            // Draw Profit
            if (exchange.GetName().indexOf("Futures") == -1) {
                // SPOT
                profit = _N((account.Balance + account.FrozenBalance - initAccount.Balance - initAccount.FrozenBalance) +
                    (account.Stocks + account.FrozenStocks - initAccount.Stocks - initAccount.FrozenStocks) * scope.bar.Close, ZPrecision);
            } else if (isFuturesLaw(exchange.GetName())) {
                // CTP
                if (IsVirtual()) {
                    if (nowHold == 0) {
                        profit = _N(account.Balance - initAccount.Balance, 2);
                    } else {
                        profit = null;
                    }
                } else {
                    profit = _N(account.Info.Balance - initAccount.Info.Balance, 2);
                }
            } else {
                if (nowHold == 0) {
                    if (isUBase) {
                        profit = _N(account.Balance - initAccount.Balance, 8);
                    } else {
                        profit = _N(account.Stocks - initAccount.Stocks, 8);
                    }
                } else {
                    profit = _N(scope.getMoney(true) - (isUBase ? (initAccount.Balance+initAccount.FrozenBalance) : (initAccount.Stocks+initAccount.FrozenStocks)), 8);
                }
            }
            if (profit != null) {
                let preProfit = _G('_profit');
                if (profit != preProfit) {
                    LogProfit(profit, (isCover ? DumpAccount(account) : '&'));
                    preAccTime = accTime;
                    _G('_profit', profit)
                    _G("_accTime", accTime);
                }
            }
        }
    }
};

function main() {
    let gui = NewGUI(exchange.GetName(), exchange.GetCurrency());
    let signals = [
        {direction: 'long', qty: 10},
    ]
    _.each(exchange.GetRecords(), function(bar, idx) {
        gui.feed(bar, idx == 10 ?  {signals: signals} : {});
    });
}