"use strict"

class BybitDemo extends Exchange {
	static meta = {
		aliases: [
			"BYBITV5DEMO",
			"BYBITV5-DEMO",
			"BYBITDEMO",
			"BYBIT-DEMO",
			"BYBIT5DEMO",
			"BYBIT5-DEMO",
		],
		endpoint: "https://api-demo.bybit.com/v5/",
		fields: {
			public: {
				label: "API Key",
			},
			private: {
				label: "API Secret",
			},
			cache: {
				label: "Use Cache",
				message: "Cache market states (hedge, margin type & leverage) for faster entries – avoid manual state changes!",
				type: "switch",
				optional: true, default: true
			}
		},
		name: "Bybit Demo",
		permissions: {
			origins: [
				"https://api-demo.bybit.com/*"
			],
		},
		subscriptions: {
			active: [],
			inactive: [],
		},
		ignoreCode: [
			30083,	// position mode not modified
			30084,	// Isolated not modified
			34015,	// cannot set leverage which is same to the old leverage
			34036,	// leverage not modified
			110001,	// order does not exist
			110043,	// set leverage not modified
			130127,	// not modified
			140043,	// leverage not modified
		],
		ignoreMsg: [
			//"params error: usdc not supported temporarily",	// 10001
			//"params error: USDC Perpetual & Futures contracts and Options can only be settled using a Unified Trading Account (UTA). Please upgrade to a UTA before trying to trade USDC contracts.",	// 10001
			"position mode not modified",	// 30083
			"isolated not modified",		// 30084
			"cannot set leverage which is same to the old leverage",	// 34015
			"leverage not modified",		// 34036
			"order does not exist",			// 110001
			"set leverage not modified",	// 110043
			"not modified",					// 130127
			"unknown result",
		],
		recvWindow: 30 * 1000, noOrigin: 2,
		website: "https://www.bybit.com/register?affiliate_id=777",
		desc: "Bybit is the safest, fastest, most transparent, and user friendly Bitcoin and Ethereum trading platform offering cryptocurrency perpetual contracts.",
		margin: true, spot: true, posMode: true, marginType: true, stopsOnFill: true,
		testSymbol: "BTCUSDTS",
		defaultSymbol: "BTC",
		multiAsset: "USD"+marginSuffix,
	}
	constructor(meta = BybitDemo.meta) {
		super(meta)
	}

	async req(method, resource, params, sign = true) {
		const headers = {"Content-Type": "application/json"}
		const query = params && method !== 'POST' ? serialize(params).replace(/%2C/g, ',') : ''
		params = params && !query ? JSON.stringify(params) : ''

		if (sign) {
			const credentials = this.getCredentials("private")
			const ts = this.getTimestamp()
			headers["X-BAPI-TIMESTAMP"] = ts
			headers["X-BAPI-API-KEY"] = credentials.public
			headers["X-BAPI-RECV-WINDOW"] = this.meta.recvWindow
			headers["X-BAPI-SIGN"] = CryptoJS.HmacSHA256(ts + credentials.public + this.meta.recvWindow + (query || params), credentials.private).toString(CryptoJS.enc.Hex)
			headers["X-BAPI-SIGN-TYPE"] = "2"
		} else this.hasAccess()

		const response = await request.call(this, method, this.meta.endpoint + resource + (query ? '?'+query:''), params, headers)
		if (!response) {
			throw new Error("Unknown connection error with API endpoint!")
		}
		if (response.retCode !== 0) {
			const reason = response.result && response.result.reasons && response.result.reasons[0]
			const code = Number(reason && reason.reasonCode || response.retCode)
			const msg = reason && reason.reasonMsg || response.retMsg
			if (!this.meta.ignoreCode.includes(code) && !this.meta.ignoreMsg.includesNoCase(msg) && !(code === 10001 && msg.toLowerCase().startsWith("params error: usdc"))) {
				throw new Error(msg ? `${msg} (Code ${code})` : "Unknown connection error with API endpoint!")
			}
		}
		return response.result
	}

	async time() {
		return Math.round((await request.call(this, 'GET', this.meta.endpoint + "market/time", null, null)).result.timeNano / 1000000)
	}

	async account(currency, isMargin, isUTA = this.getBalanceRaw().isUTA ?? 1) {
		const isTest = this.isTest()
		isMargin = isMargin || currency.endsWith(marginSuffix)
		currency = currency && currency.replace(marginSuffix, '')
		const isCt = currency && currency.endsWith(ctSuffix)
		currency = currency && currency.replace(ctSuffix, '')
		let balances = {}, hasSpotPermission = true

		if (!isCt || isTest) {
			balances.isUTA = isUTA
			let info = {}, type = balances.isUTA ? "UNIFIED" : "SPOT"
			try {
				info = (await this.req('GET', "account/wallet-balance", !isTest && currency ? {coin: currency === "USD" ? "USDT" : currency, accountType: type} : {accountType: type})).list[0]
			} catch(ex) {
				// accountType "UNIFIED" && !UTA => error code 404
				// accountType "SPOT" && UTA => "accountType only support UNIFIED and CONTRACT."
				balances.isUTA = balances.isUTA ? 0 : 1, type = balances.isUTA ? "UNIFIED" : "SPOT"
				try {
					info = (await this.req('GET', "account/wallet-balance", !isTest && currency ? {coin: currency === "USD" ? "USDT" : currency, accountType: type} : {accountType: type})).list[0]
				} catch(ex) {
					// Invalid API-key, IP, or permissions for action. (Code 10005)
					if (!isTest) throw(ex)
					hasSpotPermission = false
					this.warn('seems account has no spot trading permissions!')
				}
			}
			if (balances.isUTA) balances.isCross = info.totalMarginBalance !== ""
			if (!isMargin && info.coin) {
				for (const wallet of info.coin) {
					const equity = Number(wallet.walletBalance)
					if (equity || wallet.coin === currency) {
						const free = wallet.free !== undefined && wallet.free !== "" ? Number(wallet.free) || 0 : 
								wallet.availableToWithdraw !== undefined && wallet.availableToWithdraw !== "" ? Number(wallet.availableToWithdraw) || 0 : 
								equity - (Number(wallet.totalOrderIM) || 0) - (Number(wallet.totalPositionIM) || 0)
						balances[wallet.coin] = {available: free, balance: equity}
					}
				}
			}
			if (info.totalMarginBalance) balances[this.meta.multiAsset] = {
				available: Number(info.totalAvailableBalance),
				balance: Number(info.totalMarginBalance)
			}
		}
		if (isCt || isTest) {
			let contract = {}
			try {
				contract = (await this.req('GET', "account/wallet-balance", !isTest && currency ? {coin: currency === "USD" ? "USDT" : currency, accountType: "CONTRACT"} : {accountType: "CONTRACT"})).list[0]
			} catch(ex) {
				if (ex.message && ex.message.startsWith('accountType only support UNIFIED')) {
					// "accountType only support UNIFIED. (Code 10001)" => UTA 2.0
					// (demo account also throws this error even though it is UTA 1.0!)
					balances.isUTA = 2
				} else {
					// Permission denied, please check your API key permissions. (Code 10005)
					if (!isTest || !hasSpotPermission) throw(ex)
					this.warn('seems account has no contract trading permissions!')
				}
			}
			if (contract.coin) for (const wallet of contract.coin) {
				if (balances.isUTA === undefined) {
					balances.isUTA = wallet.marginCollateral !== undefined
				}
				const equity = Number(wallet.walletBalance)
				if (equity || wallet.coin === currency) {
					balances[wallet.coin+ctSuffix+marginSuffix] = {available: Number(wallet.availableToWithdraw) || 0, balance: equity}
				}
			}
		}
		if (isTest) {
			this.clearState()
			this.info(`detected account type ${!balances.isUTA ? 'Classic / Non-UTA' : balances.isUTA > 1 ? 'UTA 2.0' : 'UTA 1.0'}${balances.isUTA ? ` (${balances.isCross ? 'Cross' : 'Isolated'} Margin)`:''}...`)
			currency && (balances[currency] = balances[currency] || {available: 0, balance: 0})
		}
		return balances
	}

	async symbolInfoImpl(cache) {
		for (const category of ["spot", "linear", "inverse", "option"]) {
			const response = await this.req('GET', "market/instruments-info", {category, status: "Trading", limit: 1000}, false)
			response?.list?.forEach(info => {
				const tickSize = info.priceFilter?.tickSize
				info.pricePrecision = decimals(tickSize) + (!tickSize.toString().endsWith('1') ? tickSize % 1 : 0)
				if (category === "spot") {
					info.basePrecision = decimals(info.lotSizeFilter.basePrecision)
					info.quotePrecision = decimals(info.lotSizeFilter.quotePrecision)
					info.baseMinSize = info.lotSizeFilter && Number(info.lotSizeFilter.minOrderQty)
					info.baseMaxSize = info.lotSizeFilter && Number(info.lotSizeFilter.maxOrderQty)
					info.quoteMinSize = info.lotSizeFilter && Number(info.lotSizeFilter.minOrderAmt)
					info.quoteMaxSize = info.lotSizeFilter && Number(info.lotSizeFilter.maxOrderAmt)
					info.type = "spot"
					info.isSpot = true
					info.isMargin = info.marginTrading !== "none"
					info.maxLeverage = info.isMargin ? 10 : 1
					info.contractType = "Spot"+(info.isMargin ? " Margin" : "")
					cache[this.sym(info.symbol)+'S'] = info
				} else {
					const qtyStep = info.lotSizeFilter && info.lotSizeFilter.qtyStep
					info.quantityPrecision = decimals(qtyStep) + (!qtyStep.toString().endsWith('1') ? qtyStep % 1 : 0)
					info.minSize = info.lotSizeFilter && Number(info.lotSizeFilter.minOrderQty)
					info.maxSize = info.lotSizeFilter && Number(info.lotSizeFilter.maxOrderQty)
					// $$$ minNotionalValue
					info.maxPostSize = info.lotSizeFilter && Number(info.lotSizeFilter.postOnlyMaxOrderQty)
					info.maxMarketSize = info.lotSizeFilter && Number(info.lotSizeFilter.maxMktOrderQty)
					info.maxLeverage = info.leverageFilter && Number(info.leverageFilter.maxLeverage)
					if (info.contractType === "LinearPerpetual") {
						info.type = "linear"
						info.isLinear = true
						info.contractType = info.settleCoin+" Perpetual"
					} else if (info.contractType === "InverseFutures") {
						info.type = "inverse"
						info.isInverse = info.isFuture = true
						info.contractType = "Inverse Futures"
					} else if (info.contractType === "InversePerpetual") {
						info.type = "inverse"
						info.isInverse = true
						info.contractType = "Inverse Perpetual"
					} else {
						info.type = "option"
						info.isOption = true
						info.contractType = info.settleCoin+" Options"
					}
					cache[this.sym(info.symbol)] = info
				}
			})
		}
	}

	async symbolTickerImpl(symbol) {
		const market = await this.symbolInfo(symbol)
		const ticker = (await this.req('GET', "market/tickers", {symbol: market.symbol, category: market.type}, false)).list[0]

		return ticker && {
			ask: Number(ticker.ask1Price),
			bid: Number(ticker.bid1Price),
			mid: (Number(ticker.ask1Price) + Number(ticker.bid1Price)) / 2,
			last: Number(ticker.lastPrice),
			mark: Number(ticker.markPrice),
			index: Number(ticker.indexPrice)
		}
	}

	async getPosMode(market) {
		const posMode = {Buy: 10, Sell: 10, isHedge: false, isCross: !market.isInverse, isCopy: false, isUTA: 1, isPro: false}
		try {
			const result = await this.req('GET', "account/info")
			posMode.isCross = result.marginMode !== "ISOLATED_MARGIN"
			posMode.isCopy = result.isMasterTrader
			// 3 = UTA 1.0, 4 = UTA 1.0 Pro, 5 = UTA 2.0, 6 = UTA 2.0 Pro
			posMode.isUTA = result.unifiedMarginStatus > 1 ? (result.unifiedMarginStatus > 4 ? 2 : 1) : 0
			posMode.isPro = result.unifiedMarginStatus === 4 || result.unifiedMarginStatus === 6
		} catch (ex) {}
		try {
			const result = (await this.req('GET', "position/list", {category: market.type, symbol: market.symbol, limit: 2})).list
			result.forEach(pos => {
				if (pos.positionIdx === 0) {
					posMode.Buy = posMode.Sell = Number(pos.leverage) || 1
				} else {
					posMode[pos.positionIdx === 2 ? "Sell" : pos.positionIdx === 1 ? "Buy" : pos.side] = Number(pos.leverage) || 1
					posMode.isHedge = true
				}

				if (market.isInverse && posMode.isUTA < 2 || !posMode.isUTA) {
					posMode.isCross = pos.tradeMode === 0
				} else if (Number(pos.positionBalance)) {
					posMode.isCross = false
				}
			})
		} catch (ex) {}
		return posMode
	}

	async getSpotMode(market) {
		const posMode = {Buy: 1, Sell: 1}
		try {
			const result = await this.req('GET', "spot-margin-trade/state")
			if (result.spotMarginMode) {
				posMode.Buy = posMode.Sell = Number(result.spotLeverage) || 1
			} else {
				this.warn('this account does not have spot margin mode activated!')
			}
		} catch(ex) {}
		return posMode
	}

	async trade(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.isSpot ? this.sym(cmd.s) : market.symbol
		const ticker = (cmd.pc && !cmd.up && this.getPrice(cmd._sym)) || (await this.symbolTicker(cmd.up, cmd.s))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.symbol} is not available!`)
		}
		this.checkSlip(cmd, ohlc, cmd._sym, ticker)
		if (cmd.up) {
			this.updatePrice(cmd._sym, ticker)
			if (!cmd.b && !cmd.ub) {
				return ticker
			}
		}

		if (market.isSpot && cmd.isMargin && !market.isMargin) {
			this.warn('this market does not support spot margin trading!')
		}
		const posMode = market.isSpot ? (cmd.isMargin && market.isMargin ? (await this.cacheState(market, this.getSpotMode)) : {}) : (await this.cacheState(market, this.getPosMode))
		const doMode = (cmd.pm === "normal" && posMode.isHedge) || (cmd.pm === "hedge" && !posMode.isHedge)
		const cross = cmd.mt ? cmd.mt === "crossed" : posMode.isCross || cmd.l === 0
		const doType = cross != posMode.isCross
		if (!market.isSpot && doType && (posMode.isUTA === 1 && !market.isInverse || posMode.isUTA > 1)) {
			this.info(`setting margin mode to ${cross?'cross':'isolated'}...`)
			if (!cmd.d) try {
				await this.req('POST', "account/set-margin-mode", {setMarginMode: cross ? "REGULAR_MARGIN" : "ISOLATED_MARGIN"})
				this.clearState(market) // $$$ necessary or can be patched? (check: only cross changes? what happens to hedge & leverages?)
			} catch(ex) {
				this.warn(`couldn't set margin mode, please set manually! (Error: ${ex.message})`)
			}
		}
		const isInverse = market.isInverse && posMode.isUTA < 2
		const currency = cmd.currency = market.isSpot ? (cmd.isBid ? market.quoteCoin : market.baseCoin) : isInverse || posMode.isUTA === 0 ? market.settleCoin+ctSuffix+marginSuffix : cross ? this.meta.multiAsset : market.settleCoin
		const quantityPrecision = market.isSpot ? (cmd.isBid && cmd.t === "market" ? market.quotePrecision : market.basePrecision) : market.quantityPrecision

		let balance = {available: 0, balance: 0}
		let balances = null
		if ((cmd.y !== "possize" && cmd.q.wasPercent()) || cmd.ub) {
			if (market.isSpot && cmd.isMargin && market.isMargin && cmd.y !== "equity") {
				const info = await this.req('GET', "order/spot-borrow-check",  {category: "spot", symbol: market.symbol, side: cmd.isBid ? "Buy" : "Sell"})
				balance.balance = balance.available = Number(info.maxTradeAmount) / posMode[cmd.isBid ? "Buy" : "Sell"]
				this.notice(`available spot cross margin balance (with borrow): `+stringify({[info.borrowCoin]: balance}))
			} else {
				balance = cmd.bc && !cmd.ub && this.getBalance(currency, isInverse || cross)
				balances = !balance && (await this.account(currency, isInverse || cross, posMode.isUTA))
				balance = balance || balances[currency]
			}
			this.checkBalance(cmd, balance, currency, posMode.isUTA === 0 ? (market.isSpot ? "Non-UTA Spot Wallet" : "Non-UTA Derivatives Wallet") : 
						isInverse ? "Inverse Derivatives Wallet" : cross ? "Unified Trading Margin Balance – use mt=iso or switch margin mode!" : "Unified Trading Wallet")
			if (!cmd.bc || cmd.ub) {
				balances && this.updateBalance(balances, currency, isInverse || cross)
				if (cmd.ub) {
					return Object.assign(ticker, balance)
				}
			}
		}

		const first = ticker[cmd.pr] || this.ohlc(cmd, ohlc, 'pr') || ((cmd.isBid && cmd.t !== "market") || (cmd.isAsk && cmd.t === "market") ? ticker.bid : ticker.ask)
		let price = (cmd.fp || cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp)).resolve(market.pricePrecision)
		if (market.priceFilter && market.priceFilter.minPrice && price < market.priceFilter.minPrice) {
			this.warn(`price below instrument minimum of ${market.priceFilter.minPrice} – use minp= to set minimum!`)
		} else if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated, use minp= to set minimum)")
		}

		const available = cmd.y === "equity" ? balance.balance : balance.available
		let cf = 1.0
		let leverage = cmd._lev = market.isSpot && (!cmd.isMargin || !market.isMargin) ? 1 : (cmd.l === 0 ? 10 : cmd.l) * (this.ohlc(cmd, ohlc, 'lr') || 1) || posMode[cmd.isBid ? "Buy" : "Sell"]
		if (leverage > market.maxLeverage) {
			cmd.l && (cf = market.maxLeverage / leverage)
			leverage = cmd._lev = market.maxLeverage
			this.warn(`leverage limited to ${leverage}x by instrument!`)
		}
		const contracts = ohlc[cmd.y] || (
				market.isSpot ? available / (cmd.isBid && cmd.t !== "market" ? price : 1) : 
				market.isLinear ? available / price : 
				market.isInverse && !cross || market.isOption ? available * price : available) * (cmd.lc || leverage) * cf

		let order = {
			category: market.type,
			symbol: market.symbol,
			side: cmd.isBid ? "Buy" : "Sell",
			orderType: cmd.t === "market" ? "Market" : "Limit",
			qty: cmd.q._().mul(cmd.u === "currency" && !cmd.q.wasPercent() ? (market.isLinear ? 1 / price : price) * (cmd.lc || leverage) * cf : 1)
				.reference(contracts).mul(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc)).minmax(cmd.minq, cmd.maxq).resolve(quantityPrecision),
			timeInForce: ({fok: "FOK", ioc: "IOC", post: "PostOnly"})[cmd.t] || "GTC",
			closeOnTrigger: false,
			reduceOnly: false
		}
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			order.qty = new NumberObject(ohlc.left).resolve(quantityPrecision)
		}
		const maxQty = cmd.t === "post" && market.maxPostSize || cmd.t === "market" && market.maxMarketSize || market.maxSize
		if (market.minSize && order.qty < market.minSize) {
			this.warn(`order quantity below instrument minimum of ${market.minSize} – use minq= to set minimum!`)
		} else if (maxQty && order.qty > maxQty) {
			this.warn(`order quantity above instrument maximum of ${maxQty} – use maxq= to set maximum!`)
		} else if (order.qty <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated, use minq= to set minimum)")
		}
		if (market.isLinear || (market.isFuture && posMode.isUTA < 2)) {
			order.positionIdx = (!doMode && !posMode.isHedge) || (doMode && posMode.isHedge) ? 0 : cmd.isBid ? (!cmd.r ? 1 : 2) : (!cmd.r ? 2 : 1)
		}
		if (cmd.t !== "market") order.price = price
		if (cmd.r) order.closeOnTrigger = order.reduceOnly = true
		if (cmd.id) order.orderLinkId = this.uniqueID(cmd.id)

		if (cmd.so) {
			order.triggerPrice = cmd.so._().relative(this.ohlc(cmd, ohlc, 'soref', ticker, price) || first).resolve(market.pricePrecision)
			if (!market.isSpot) {
				order.triggerDirection = ticker.last < order.triggerPrice ? 1 : 2
				order.triggerBy = ucfirst(cmd.sr) + "Price"
			} else {
				order.orderFilter = "StopOrder"
			}
		}
		if (cmd.tp) {
			order[market.isSpot ? "triggerPrice" : "takeProfit"] = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.pricePrecision)
			if (!market.isSpot) {
				order.tpTriggerBy = ucfirst(cmd.sr) + "Price"
			} else {
				order.orderFilter = "tpslOrder"
			}
		}
		if (cmd.sl) {
			order[market.isSpot ? "triggerPrice" : "stopLoss"] = cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(market.pricePrecision)
			if (!market.isSpot) {
				order.slTriggerBy = ucfirst(cmd.sr) + "Price"
			} else {
				order.orderFilter = "tpslOrder"
			}
		}
		if (market.isSpot) {
			delete order.closeOnTrigger
			delete order.reduceOnly
			order.isLeverage = cmd.isMargin && market.isMargin ? 1 : 0
		}
		if (!order.triggerPrice && cmd.t === "market" && cmd.msle) {	// support linear, inverse, spot trading - take profit, stoploss, conditional orders are not supported!
			order.slippageToleranceType = "Percent"
			order.slippageTolerance = cmd.msle._().reference(100).minmax(0.05, 2.00).resolve(2)
		}

		this.info(`placing ${currency} ${market.type}${order.isLeverage ? ' cross margin':''} order${market.isSpot ? (order.isLeverage ? ` @ ${leverage}x`:'') : 
			` @ ${leverage}x ${cross ? 'cross' : 'isolated'} (${order.positionIdx ? 'hedged' : 'one-way'} / ${posMode.isUTA ? `UTA ${posMode.isUTA}.0` : 'Non-UTA'})`}: `+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}

		if ((market.isFuture && posMode.isUTA < 2 || market.isLinear) && doMode) {
			this.info(`setting position mode to ${cmd.pm}...`)
			try {
				await this.req('POST', "position/switch-mode", {category: market.type, symbol: market.symbol, mode: cmd.pm !== "hedge" ? 0 : 3})
				this.clearState(market) // $$$ necessary or can be patched? (check: only hedge/non-hedge changes? what happens to cross & leverages?)
			} catch(ex) {
				this.warn(`couldn't change position mode, please change manually! (Error: ${ex.message})`)
			}
		} else if (doMode) {
			this.warn(`this market does not support different position modes!`)
		}

		const posSide = order.positionIdx === 2 ? "Sell" : order.positionIdx === 1 ? "Buy" : order.side
		const doLev = cmd.l !== undefined && leverage != posMode[posSide]
		if (!market.isSpot && (doLev || (doType && (market.isInverse && posMode.isUTA < 2 || !posMode.isUTA)))) {
			this.info(`setting contract leverage to ${leverage}x${cross?' cross':' isolated'}...`)
			posMode[posSide] = leverage
			if ((!doMode && !posMode.isHedge) || (doMode && cmd.pm !== "hedge")) {
				posMode.Buy = posMode.Sell = leverage
			}
			try {
				const doSwitch = doType && (market.isInverse && posMode.isUTA < 2 || !posMode.isUTA)
				await this.req('POST', doSwitch ? "position/switch-isolated" : "position/set-leverage", 
					{category: market.type, symbol: market.symbol, tradeMode: cross ? 0 : 1, buyLeverage: posMode.Buy.toString(), sellLeverage: posMode.Sell.toString()})
				if (doSwitch) {
					this.clearState(market) // $$$ necessary or can be patched? (check: only cross and leverages change? what happens to hedge?)
				} else {
					posMode.isCross = cross
					this.updateState(market, posMode)
				}
			} catch(ex) {
				this.warn(`couldn't set leverage, please set manually! (Error: ${ex.message})`)
			}
		}
		if (market.isSpot && cmd.isMargin && market.isMargin && doLev) {
			this.info(`setting spot cross margin leverage to ${leverage}x...`)
			try {
				await this.req('POST', "spot-margin-trade/set-leverage", {leverage: leverage.toString()})
				posMode.Buy = posMode.Sell = leverage
				this.updateState(market, posMode)
			} catch(ex) {
				this.warn(`couldn't set leverage, please set manually! (Error: ${ex.message})`)
			}
		}

		if (maxQty && order.qty > maxQty) {
			let origOrder = Object.assign({}, order)
			let leftQty = order.qty, result = null, i = 1
			do {
				if (i > 1 && opt.splitDelay) {
					this.info(`waiting for ${opt.splitDelay}sec (split order delay)...`)
					await sleep(opt.splitDelay)
				}
				order.qty = new NumberObject(Math.min(leftQty, maxQty)).resolve(market.quantityPrecision)
				if (cmd.id) order.orderLinkId = this.uniqueID(cmd.id, i)
				this.info(`posting split order #${i++} of size ${order.qty}...`)
				result = await this.req('POST', "order/create", order)
				if (i < 2 && cmd.t === 'post' && cmd.ifr && result && result.orderId) {
					const order = (await this.req('GET', "order/realtime", {category: market.type, symbol: market.symbol, orderId: result.orderId, limit: 1})).list[0]
					if (order && (order.orderStatus === 'Rejected' || order.orderStatus === 'Cancelled')) {
						throw new Error(errRejected)
					}
				}
				leftQty -= order.qty
			} while(leftQty > 0)
			return Object.assign(origOrder, result)
		}
		const result = Object.assign(order, await this.req('POST', "order/create", order))
		if (cmd.t === 'post' && cmd.ifr && result && result.orderId) {
			const order = (await this.req('GET', "order/realtime", {category: market.type, symbol: market.symbol, orderId: result.orderId, limit: 1})).list[0]
			if (order && (order.orderStatus === 'Rejected' || order.orderStatus === 'Cancelled')) {
				throw new Error(errRejected)
			}
		}
		return result
	}

	async ordersCancel(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.isSpot ? this.sym(cmd.s) : market.symbol
		// $$$ check if isSpot && (cmd.tp && !cmd.sl) || (!cmd.tp && cmd.sl) ?
		if (!cmd.ch && !cmd.d && !cmd.b && !cmd.fp && !cmd.id && cmd.cm.wasPercent(true) && cmd.r === undefined && (!market.isSpot || !cmd.ts) && !cmd.gt && !cmd.gte && !cmd.lt && !cmd.lte) {
			// 1 = stop, 2 = limit, 3 = all
			const type = !cmd.tp && !cmd.sl && !cmd.ts && !cmd.so && cmd.t !== "close" ? (!cmd.has('t') ? 3 : 2) : 1
			const params = {category: market.type, symbol: market.symbol}
			if (type < 3) {
				if (type < 2) {
					params.orderFilter = market.isSpot && (cmd.tp || cmd.sl) ? "tpslOrder" : "StopOrder"
					if (market.isLinear || market.isInverse) {
						params.stopOrderType = cmd.tp ? "TakeProfit" : cmd.sl ? "StopLoss" : cmd.ts ? "TrailingStop" : "Stop"
					}
				} else {
					params.orderFilter = "Order"
				}
			}
			this.info(`canceling all ${market.symbol} ${params.stopOrderType ? params.stopOrderType+' order' : type === 2 ? 'limit order' : params.orderFilter || 'order'}s in bulk!`)
			const result = await this.req('POST', "order/cancel-all", params)
			if (market.isSpot && type > 2) {
				params.orderFilter = "tpslOrder"
				const tpslResult = await this.req('POST', "order/cancel-all", params)
				params.orderFilter = "StopOrder"
				const stopResult = await this.req('POST', "order/cancel-all", params)
				if (result) {
					result.list = (result.list || []).concat(tpslResult && tpslResult.list || []).concat(stopResult && stopResult.list || [])
				}
			}
			if (!result) {
				this.msgOrderErr()
				return false
			} else if (!result.list || !result.list.length) {
				this.msgOrdersNone()
				return false
			}
			return result.list
		}

		const params = {category: market.type, symbol: market.symbol, openOnly: 0, limit: 50}
		let orders = (await this.req('GET', "order/realtime", params)).list
		if (market.isSpot) {
			params.orderFilter = "tpslOrder"
			orders = orders.concat((await this.req('GET', "order/realtime", params)).list || [])
			params.orderFilter = "StopOrder"
			orders = orders.concat((await this.req('GET', "order/realtime", params)).list || [])
		}
		if (!orders.length) {
			this.msgOrdersNone()
			return false
		}

		const ids = (cmd.id || "").split(',')
		const hasType = cmd.has('t') && (cmd.t === "limit" || cmd.t === "market") && ucfirst(cmd.t)
		const match = cmd.cr ? false : true
		const tickers = {}
		await this.filterOrderStart(cmd, ohlc, tickers, orders)
		const totalOrders = orders.length
		orders = orders.filter(order => {
			if ((cmd.isBid && order.side !== 'Buy') || (cmd.isAsk && order.side !== 'Sell')) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(order.price)) {
				return !match
			}
			if (hasType && order.orderType !== hasType) {
				return !match
			}
			if ((cmd.t === "open" && order.triggerPrice) || ((cmd.t === "close" || cmd.tp || cmd.sl || cmd.ts || cmd.so) && !order.triggerPrice)) {
				return !match
			}
			if ((cmd.so && order.stopOrderType !== "Stop") || (cmd.ts && order.stopOrderType !== "TrailingStop")) {
				return !match
			}
			if (cmd.r !== undefined && !cmd.r != !order.reduceOnly) {
				return !match
			}
			if (cmd.id && (!order.orderLinkId || !order.orderLinkId.startsWithAny(ids, true))) {
				return !match
			}
			if (!this.filterOrder(cmd, ohlc, tickers, order)) {
				return !match
			}
			return match
		})

		if (cmd.cm._().reference(orders.length).getMax() < orders.length) {
			const sort = {
				"newest": 	["createdTime", true],
				"oldest": 	["createdTime", false],
				"highest": 	["price", true],
				"lowest": 	["price", false],
				"biggest": 	["qty", true],
				"smallest": ["qty", false]
			}
			cmd.cmo === 'random' ? shuffle(orders) : sortByIndex(orders, sort[cmd.cmo][0], sort[cmd.cmo][1])
			orders = orders.slice(0, cmd.cm.resolve(0))
		}

		this.msgOrders(cmd, orders, totalOrders)
		if (cmd.ch || cmd.d) {
			orders.length && this.msgDisabled(cmd)
			return orders
		}

		let cancelOrders = []
		// $$$ implement batch-cancel! (unless UTA 1.0 && isInverse)
		for (const order of orders) {
			const params = {category: market.type, symbol: market.symbol, orderId: order.orderId}
			if (market.isSpot && order.stopOrderType) params.orderFilter = order.stopOrderType
			const result = await this.req('POST', "order/cancel", params)
			if (result && result.orderId) {
				if (cmd.sv && !order.triggerPrice) {
					this.info("checking for updated fill info...")
					const rt = (await this.req('GET', "order/realtime", {category: market.type, symbol: market.symbol, orderId: result.orderId, limit: 1}))?.list?.[0]
					if (rt?.cumExecQty && rt.cumExecQty != order.cumExecQty) {
						this.info(`found new fill info! (was: ${order.cumExecQty}, new: ${rt.cumExecQty})`)
						order.cumExecQty = rt.cumExecQty
						order.leavesQty = order.qty - rt.cumExecQty
					} else if ((rt?.leavesQty || rt?.leavesQty === 0) && rt.leavesQty != order.leavesQty) {
						this.info(`found new left info! (was: ${order.leavesQty}, new: ${rt.leavesQty})`)
						order.leavesQty = rt.leavesQty
						order.cumExecQty = order.qty - rt.leavesQty
					}
				}
				cancelOrders.push(order)
			}
		}
		return cancelOrders
	}

	async positionsClose(cmd, pos, i, ohlc = {}, ticker, market) {
		ticker = (cmd.pc && !cmd.up && this.getPrice(market.symbol)) || ticker || (await this.symbolTicker(cmd.up, market.symbol))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.symbol} is not available!`)
		}
		ticker.pos = pos.avgPrice
		ticker.liq = pos.liqPrice
		this.checkSlip(cmd, ohlc, market.symbol, ticker)
		cmd.up && this.updatePrice(market.symbol, ticker)

		const first = ticker[cmd.sl || cmd.tp || cmd.ts || cmd.so ? 'pos' : cmd.pr] || this.ohlc(cmd, ohlc, 'pr') || ((cmd.isBid && cmd.t !== "market") || (cmd.isAsk && cmd.t === "market") ? ticker.bid : ticker.ask)
		let price = (cmd.fp || cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp)).resolve(market.pricePrecision)
		if (market.priceFilter && market.priceFilter.minPrice && price < market.priceFilter.minPrice) {
			this.warn(`price below instrument minimum of ${market.priceFilter.minPrice} – use minp= to set minimum!`)
		} else if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated, use minp= to set minimum)")
		}

		const contracts = pos.quantity
		let order = {
			category: market.type,
			symbol: market.symbol,
			side: pos.side === 'Buy' ? 'Sell' : 'Buy',
			orderType: cmd.t === "market" ? "Market" : "Limit",
			qty: cmd.q._().mul(cmd.u === "currency" && !cmd.q.wasPercent() ? (market.isLinear ? 1 / price : price) * (cmd.lc || pos.leverage || 1) : 1)
				.reference(contracts).mul(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc, pos)).minmax(cmd.minq, cmd.maxq).resolve(market.quantityPrecision),
			timeInForce: ({fok: "FOK", ioc: "IOC", post: "PostOnly"})[cmd.t] || "GTC",
			closeOnTrigger: cmd.r ?? true,
			reduceOnly: cmd.r ?? true,
			positionIdx: pos.positionIdx,
		}
		if (cmd.t !== "market") order.price = price

		if (((cmd.tp || cmd.sl) && (/*!cmd.q.wasPercent(true) ||*/ cmd.t === "limit" || cmd.id)) || (cmd.so && !cmd.ts)) {
			const param = cmd.tp && 'tpref' || cmd.sl && 'slref' || cmd.so && 'soref'
			const stop = this.ohlc(cmd, ohlc, param, ticker, price) || first
			order.triggerPrice = (cmd.tp || cmd.sl || cmd.so)._().relative(stop).resolve(market.pricePrecision)
			order.triggerDirection = ticker.last < order.triggerPrice ? 1 : 2
			order.triggerBy = ucfirst(cmd.sr) + "Price"
		} else if ((cmd.tp || cmd.sl || cmd.ts) && !cmd.ch) {
			let req = {
				category: market.type,
				symbol: market.symbol,
				tpslMode: cmd.q.wasPercent(true) ? "Full" : "Partial",
				positionIdx: pos.positionIdx,
			}
			if (!cmd.q.wasPercent(true)) {
				req.tpSize = req.slSize = order.qty
			}
			if (cmd.tp) {
				req.takeProfit = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.pricePrecision)
				req.tpTriggerBy = ucfirst(cmd.sr) + "Price"
			}
			if (cmd.sl) {
				req.stopLoss = cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(market.pricePrecision)
				req.slTriggerBy = ucfirst(cmd.sr) + "Price"
			}
			if (cmd.ts) {
				const act = this.ohlc(cmd, ohlc, 'soref', ticker, price) || first
				if (cmd.so) {
					req.activePrice = cmd.so._().relative(act).resolve(market.pricePrecision)
				}
				req.trailingStop = cmd.ts._().reference(this.ohlc(cmd, ohlc, 'tsref', ticker, price) || act || first).abs().resolve(market.pricePrecision)
			}
			cmd.id && this.warn("does not support custom IDs for Trading Stop position attributes – use so= instead to place a normal stop order!")
			this.info("setting Trading Stop: "+stringify(req))
			if (cmd.d) {
				this.msgDisabled(cmd)
				return req
			}
			return Object.assign(req, await this.req('POST', "position/trading-stop", req))
		}
		if (cmd.id) order.orderLinkId = this.uniqueID(cmd.id, i)

		!cmd.ch && this.info(`placing Close Position order @ ${pos.leverage}x: `+stringify(order))
		if (cmd.ch || cmd.d) {
			this.msgDisabled(cmd)
			return order
		}

		const maxQty = (cmd.t === "post" && market.maxPostSize) || market.maxSize
		if (maxQty && order.qty > maxQty) {
			let leftQty = order.qty, result = null, i = 1
			do {
				if (i > 1 && opt.splitDelay) {
					this.info(`waiting for ${opt.splitDelay}sec (split order delay)...`)
					await sleep(opt.splitDelay)
				}
				order.qty = Math.min(leftQty, maxQty)
				if (cmd.id) order.orderLinkId = this.uniqueID(cmd.id, i)
				this.info(`posting split order #${i++} of size ${order.qty}...`)
				result = await this.req('POST', "order/create", order)
				if (i < 2 && cmd.t === 'post' && cmd.ifr && result && result.orderId) {
					const order = (await this.req('GET', "order/realtime", {category: market.type, symbol: market.symbol, orderId: result.orderId, limit: 1})).list[0]
					if (order && (order.orderStatus === 'Rejected' || order.orderStatus === 'Cancelled')) {
						throw new Error(errRejected)
					}
				}
				leftQty -= order.qty
			} while(leftQty > 0)
			return Object.assign(order, result)
		}
		const result = Object.assign(order, await this.req('POST', "order/create", order))
		if (cmd.t === 'post' && cmd.ifr && result && result.orderId) {
			const order = (await this.req('GET', "order/realtime", {category: market.type, symbol: market.symbol, orderId: result.orderId, limit: 1})).list[0]
			if (order && (order.orderStatus === 'Rejected' || order.orderStatus === 'Cancelled')) {
				throw new Error(errRejected)
			}
		}
		return result
	}

	async positionsCloseAll(cmd, ohlc, positions) {
		let isUTA = this.getBalanceRaw().isUTA
		const isAll = !cmd.s || cmd.s === '*'
		const sym = this.sym(cmd.s)
		const symbols = isAll ? await this.symbolInfo() : {[sym]: await this.symbolInfo(cmd.s)}
		if (!positions) {
			if (isAll) {
				positions = []
				const linearCoins = [... new Set(Object.mapAsArray(symbols, (sym, info) => info.isLinear && info.settleCoin))]
				for (const coin of linearCoins) {
					if (!coin) continue
					const result = (await this.req('GET', "position/list", {category: "linear", settleCoin: coin, limit: 200})).list
					if (!result || !result.filter) continue
					positions = positions.concat(result)
				}
				if (isUTA > 1) {
					const result = (await this.req('GET', "position/list", {category: "inverse", limit: 200})).list
					if (result && result.filter) positions = positions.concat(result)
				} else {
					try {
						const contract = (await this.req('GET', "account/wallet-balance", {accountType: "CONTRACT"})).list[0]
						if (contract && contract.coin) {
							const inverseCoins = contract.coin.map(wallet => Number(wallet.totalPositionIM) && wallet.coin)
							for (const coin of inverseCoins) {
								if (!coin || coin === "USDT") continue
								const result = (await this.req('GET', "position/list", {category: "inverse", settleCoin: coin, limit: 200})).list
								if (!result || !result.filter) continue
								positions = positions.concat(result)
							}
						}
					} catch (ex) {
						if (ex.message && ex.message.startsWith('accountType only support UNIFIED')) {
							// "accountType only support UNIFIED. (Code 10001)" => UTA 2.0
							// (demo account also throws this error even though it is UTA 1.0!)
							if (!isUTA || isUTA < 2) {
								isUTA = 2
								// $$$$$ SAVE isUTA !!!		this.getBalanceRaw().isUTA
								const result = (await this.req('GET', "position/list", {category: "inverse", limit: 200})).list
								if (result && result.filter) positions = positions.concat(result)
							}
						}
					}
				}
			} else {
				const market = symbols[sym]
				positions = (await this.req('GET', "position/list", {category: market.type, symbol: market.symbol, limit: 2})).list
				if (!positions || !positions.filter) {
					this.msgPosErr()
					return false
				}
			}
			positions = positions.filter(pos => pos.side && pos.side !== 'None' && Number(pos.size))
			if (!positions.length) {
				this.msgPosNone()
				return false
			}
		}

		const match = cmd.cr ? false : true
		const tickers = {}
		await this.filterPosStart(cmd, ohlc, tickers, positions)
		const totalPos = positions.length
		positions = positions.filter(pos => {
			if ((cmd.isBid && pos.side !== 'Buy') || (cmd.isAsk && pos.side !== 'Sell')) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(pos.avgPrice)) {
				return !match
			}
			if (cmd.l && pos.leverage !== cmd.l) {
				return !match
			}
			const market = symbols[this.sym(pos.symbol)] || {}
			cmd._sym = market.symbol
			const cross = market.isFuture || isUTA === 0 ? pos.tradeMode == 0 : !Number(pos.positionBalance)
			const isInverse = market.isInverse && isUTA < 2
			pos.currency = cmd.currency = isInverse || isUTA === 0 ? market.settleCoin+ctSuffix+marginSuffix : cross ? this.meta.multiAsset : market.settleCoin
			pos.quantity = Math.abs(pos.size)
			pos.isolated = !cross
			pos.unrealisedPnl = toPrecision(pos.unrealisedPnl || 0, 8)
			pos.margin = Number(pos.positionBalance) || Number(pos.positionIM)
			pos.pnl = pos.unrealisedPnl / pos.margin / pos.leverage
			pos._sizeCoin = market.isLinear ? pos.quantity / pos.leverage : pos.margin
			pos._sizeUSD = market.isLinear ? pos.margin : pos.quantity / pos.leverage
			pos.avgPrice = toPrecision(pos.avgPrice || 0, market.pricePrecision)
			if (!this.filterPos(cmd, ohlc, tickers, pos, balance => (market.isLinear ? balance / pos.avgPrice : market.isInverse && !cross || market.isOption ? balance * pos.avgPrice : balance) * pos.leverage, market.quantityPrecision)) {
				return !match
			}
			return match
		})

		if (cmd.cm._().reference(positions.length).getMax() < positions.length) {
			const sort = {
				"newest": 	["createdTime", true],
				"oldest": 	["createdTime", false],
				"highest": 	["avgPrice", true],
				"lowest": 	["avgPrice", false],
				"biggest": 	["quantity", true],
				"smallest": ["quantity", false]
			}
			cmd.cmo === 'random' ? shuffle(positions) : sortByIndex(positions, sort[cmd.cmo][0], sort[cmd.cmo][1])
			positions = positions.slice(0, cmd.cm.resolve(0))
		}
		if (!positions.length || cmd.ch) {
			this.msgPos(cmd, positions, totalPos)
			return positions
		}

		let closeOrders = [], i = 0
		for (const pos of positions) {
			const order = await this.positionsClose(cmd, pos, i++, ohlc, undefined, symbols[this.sym(pos.symbol)])
			if (order) {
				closeOrders.push(order)
				Object.assign(closeOrders.last(), Object.without(pos, ["quantity", "size", "side", "takeProfit", "stopLoss", "trailingStop"]))
			}
		}
		return closeOrders
	}

/*	async transfer(cmd, ohlc) {
		const response = await this.req('GET', "/subaccounts")
		if (!response || !response.filter) {
			this.info("unable to get sub account information!")
			return false
		}
		if (!response.length) {
			this.info("no sub accounts found!")
			return false
		}
		let subs = {}
		response.forEach(info => subs[info.nickname.toLowerCase()] = info)

		const y = cmd.y.split(':')
		let src = y.length > 1 ? y[0] : 'main'
		let dest = y.length > 1 ? y[1] : y[0]

		if (!subs[src] && src !== 'main') {
			throw new ReferenceError(`Sub account with nickname "${src}" not found! (available: ${Object.mapAsArray(subs, (sub, info) => info.nickname).join(', ')})`)
		} else if (!subs[dest] && dest !== 'main') {
			throw new ReferenceError(`Sub account with nickname "${dest}" not found! (available: ${Object.mapAsArray(subs, (sub, info) => info.nickname).join(', ')})`)
		}
		src = src !== 'main' ? subs[src].nickname : src
		dest = dest !== 'main' ? subs[dest].nickname : dest

		let balances = {}
		if (cmd.q.wasPercent()) {
			if (src === 'main') {
				balances = await this.account(cmd.tb[0])
				this.updateBalance(balances, cmd.tb[0])
			} else {
				const response = await this.req('GET', `/subaccounts/${src}/balances`)
				if (!response || !response.filter || !response.length) {
					this.info(`unable to get sub account balances for "${src}"!`)
					return false
				}
				response.forEach(info => balances[info.coin] = info)
			}
		}

		ohlc[cmd.qr] && this.info(`using custom quantity multiplier: ${ohlc[cmd.qr]} (${cmd.qr})`)
		let orders = []
		for (const currency of cmd.tb) {
			if (cmd.q.wasPercent() && !balances[currency]) {
				throw new ReferenceError(`Account Balance ${currency} not available in "${src}"!`)
			}
			let balance = balances[currency] || {}
			balance = balance.free || balance.available || 0
			const order = {
				coin: currency,
				size: cmd.q._().reference(balance).mul(ohlc[cmd.qr] || 1).resolve(8),
				source: src,
				destination: dest
			}
			this.info(`transferring ${order.size}${balance ? ` of ${balance}` : ''} ${currency} from "${src}" to "${dest}"!`)

			if (cmd.d) {
				this.msgDisabled(cmd)
				orders.push(order)
			} else {
				orders.push(await this.req('POST', "/subaccounts/transfer", order))
			}
		}
		return orders
	}
*/
	async query(cmd, ohlc) {
		if (cmd.query[0] !== 'PNL' || cmd.query.length > 1) {
			throw new ReferenceError(`${this.getName()} currently only supports querying past PnL!`)
		}

		const isAll = !cmd.s || cmd.s === '*'
		const sym = this.sym(cmd.s)
		const symbols = isAll ? await this.symbolInfo() : {[sym]: await this.symbolInfo(cmd.s)}
		const params = {limit: 100}
		if (cmd.cmo === 'newest' && !cmd.cm._().isPercent()) {
			params.limit = cmd.cm.resolve(0)
		}
		const eod = new Date().setHours(0, 0, 0, 0)
		const since = ({
			"bar": cmd._bartime,
			"cbar": cmd._cbartime,
			"fired": cmd._firetime,
			"eod": eod, "eodutc": eod+tzo
		})[cmd.since] || Date.now()
		if (cmd.from && cmd.from.reference) params.startTime = since + cmd.from._().reference(cmd._res * 60).resolve() * 1000
		if (cmd.to   && cmd.to.reference  ) params.endTime   = since +   cmd.to._().reference(cmd._res * 60).resolve() * 1000

		this.info(`querying PnL records for ${cmd.s||'*'}${params.startTime ? ` from ${formatDateS(params.startTime)}`:''}${params.endTime ? ` till ${formatDateS(params.endTime)}`:''}...`)

		const categories = isAll ? ["linear", "inverse"] : [symbols[sym].type]
		if (!isAll) params.symbol = symbols[sym].symbol

		let pnls = []
		for (const category of categories) {
			params.category = category
			try {
				pnls = pnls.concat((await this.req('GET', "position/closed-pnl", params)).list || [])
			} catch(ex) {
				if (!ex.message || !ex.message.contains("not supported")) throw(ex)
			}
		}
		if (!pnls || !pnls.length) {
			this.info(`no PnL records found!`)
			return false
		}

		if (cmd.cmo !== 'newest') {
			const sort = {
				"newest": 	["createdTime", true],
				"oldest": 	["createdTime", false],
				"highest": 	["avgEntryPrice", true],
				"lowest": 	["avgEntryPrice", false],
				"biggest": 	["qty", true],
				"smallest": ["qty", false]
			}
			cmd.cmo === 'random' ? shuffle(pnls) : sortByIndex(pnls, sort[cmd.cmo][0], sort[cmd.cmo][1])
		}
		if (cmd.cm._().reference(pnls.length).getMax() < pnls.length) {
			pnls = pnls.slice(0, cmd.cm.resolve(0))
		}

		const isUTA = this.getBalanceRaw().isUTA ?? 1

		const match = cmd.cr ? false : true
		const tickers = {}
		await this.filterPNLStart(cmd, ohlc, tickers, pnls)
		const totalPNLs = pnls.length
		pnls = pnls.filter(pnl => {
			if ((cmd.isBid && pnl.side !== 'Sell') || (cmd.isAsk && pnl.side !== 'Buy')) {
				return false
			}
			if (cmd.l && pnl.leverage !== cmd.l) {
				return !match
			}
			const market = symbols[this.sym(pnl.symbol)] || {}
			cmd._sym = market.symbol
			const cross = this.getBalanceRaw().isCross ?? true
			const isInverse = market.isInverse && isUTA < 2
			pnl.currency = cmd.currency = market.isSpot ? (cmd.isBid ? market.quoteCoin : market.baseCoin) : isInverse || isUTA === 0 ? market.settleCoin+ctSuffix+marginSuffix : cross ? this.meta.multiAsset : market.settleCoin
			const balance = this.getBalance(pnl.currency, isInverse || cross, true) || {}
			if (!this.filterPNL(cmd, ohlc, tickers, pnl, balance, market.quantityPrecision)) {
				return !match
			}
			return match
		})
		this.msgPNL(cmd, pnls, totalPNLs)
		return pnls
	}
}

class BybitTestnet extends BybitDemo {
	static meta = Object.assign({}, super.meta, {
		aliases: [
			"BYBITV5TESTNET",
			"BYBITV5-TESTNET",
			"BYBITTESTNET",
			"BYBIT-TESTNET",
			"BYBIT5TESTNET",
			"BYBIT5-TESTNET",
		],
		endpoint: "https://api-testnet.bybit.com/v5/",
		name: "Bybit Testnet",
		permissions: {
			origins: [
				"https://api-testnet.bybit.com/*"
			],
		},
		website: "https://testnet.bybit.com/app/register?ref=VOQ1B",
	})
	constructor(meta = BybitTestnet.meta) {
		super(meta)
	}
}

class Bybit extends BybitDemo {
	static meta = Object.assign({}, super.meta, {
		aliases: [
			"BYBITV5",
			"BYBIT",
			"BYBIT5",
		],
		endpoint: "https://api.bybit.com/v5/",
		name: "Bybit",
		permissions: {
			origins: [
				"https://api.bybit.com/*"
			],
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"bybit1",
				"bybit2",
			],
		},
	})
	constructor(meta = Bybit.meta) {
		super(meta)
	}
}

Broker.add(new Bybit())
Broker.add(new BybitDemo())
Broker.add(new BybitTestnet())
