"use strict"

class PhemexTestnet extends Exchange {
	static meta = {
		aliases: [
			"PHEMEXTESTNET",
			"PHEMEX-TESTNET",
		],
		endpoint: "https://testnet-api.phemex.com",
		fields: {
			public: {
				label: "ID",
			},
			private: {
				label: "API Secret",
			},
		},
		name: "Phemex Testnet",
		patterns: [],
		permissions: {
			origins: [
				"https://testnet-api.phemex.com/*"
			],
		},
		subscriptions: {
			active: [],
			inactive: [],
		},
		ignoreCode: [
			10002,
		],
		ignoreMsg: [
			"OM_ORDER_NOT_FOUND",
			"Error in query account position"
		],
		website: "https://testnet.phemex.com/register?referralCode=DMCKT",
		recvWindow: 30 * 1000,
		desc: "Phemex is the fastest Crypto exchange and Crypto Futures trading platform. Trade Bitcoin with Zero Fees. You can also trade perpetual contracts with 100x.",
		defaultSymbol: "USDT",
		testSymbol: "SBTCUSDT",
		margin: true, spot: true, posMode: true, marginType: true, stopsOnFill: true,
		isHedged: sym => sym && sym.endsWith("USDT"),
	}
	constructor() {
		super(PhemexTestnet.meta)
	}

	*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 crypt = (payload, key) => {
				const sha = new jsSHA("SHA-256", "TEXT")
				sha.setHMACKey(key, "TEXT")
				sha.update(payload)
				return sha.getHMAC("HEX")
			}
			const credentials = this.getCredentials("private")
			const ts = Math.round((this.getTimestamp() + PhemexTestnet.meta.recvWindow) / 1000)
			headers["x-phemex-access-token"] = credentials.public
			headers["x-phemex-request-expiry"] = ts
			headers["x-phemex-request-signature"] = crypt(resource + query + ts + params, credentials.private)
		} else this.hasAccess()

		const response = yield request.bind(this, method, this.meta.endpoint + resource + (query ? '?' : '') + query, params, headers)
		if (!response || (response.code === undefined && response.error === undefined)) {
			throw new Error("Unknown connection error with API endpoint!")
		}
		if (response.code || response.error) {
			if (!this.meta.ignoreCode.includes(Number(response.code)) && !this.meta.ignoreMsg.includesNoCase(response.error || response.msg)) {
				throw new Error((response.error || response.msg || "Unknown data error with API endpoint!") + (response.code?` (Error Code ${response.code})`:''))
			}
		}
		return response.data || response.result
	}

	*time() {
		return (yield* this.req('GET', "/public/time", undefined, false)).serverTime
	}

	*account(currency, isMargin, posCache) {
		let balances = {}
		const isTest = this.isTest()

		if (isMargin || isTest) {
			const currencies = isTest || !currency ? [...new Set(Object.mapAsArray(yield* this.symbolInfo(), (sym, info) => info.settleCurrency))] : [currency]
			//const currencies = isTest || !currency ? [...new Set(Object.values(yield* this.symbolInfo()).map(info => info.settleCurrency))] : [currency]
			for (const currency of currencies) {
				if (!currency || currency === 'USD') continue
				const isHedged = currency === 'USDT'
				const result = (yield* this.req('GET', `/${isHedged ? "g-":''}accounts/accountPositions`, {currency: currency})) || {}
				const info = result.account
				if (!info) {
					this.info(`unable to get ${currency} margin account info!`)
					continue
				}
				if (posCache) posCache.positions = result.positions
				const scale = isHedged ? 1 : currency !== 'USD' ? 100000000 : 10000
				const suffix = isHedged ? 'Rv' : 'Ev'
				balances[isMargin ? currency : currency+marginSuffix] = {
					available: (info['accountBalance'+suffix] - info['totalUsedBalance'+suffix]) / scale,
					balance: info['accountBalance'+suffix] / scale
				}
			}
		}
		if (!isMargin) {
			const wallets = yield* this.req('GET', "/spot/wallets")
			if (!wallets || !wallets.forEach) {
				this.info("unable to get spot account info!")
				return balances
			}
			wallets.forEach(wallet => {
				if (wallet.balanceEv || wallet.currency == currency) {
					balances[wallet.currency] = {
						available: (wallet.balanceEv - wallet.lockedTradingBalanceEv) / 100000000,
						balance: wallet.balanceEv / 100000000
					}
				}
			})
		}
		isTest && currency && (balances[currency] = balances[currency] || {available: 0, balance: 0})
		return balances
	}

	*symbolInfo(symbol) {
		const symbolCache = this.getSymbolCache()
		if (symbolCache._refresh) {
			const response = (yield* this.req('GET', "/public/products", null, false))
			response.products.concat(response.perpProductsV2 || []).forEach(info => {
				if (info.status && info.status !== "Listed") return
				if (info.type === "Spot") {
					info.isSpot = true
					info.minimumTradeSize = info.minOrderValueEv / 100000000
					info.maxOrderQty = info.maxOrderValueEv / 100000000
					info.maxOrderQtyBase = info.maxBaseOrderSizeEv / 100000000
					info.baseTickSize = info.baseTickSizeEv / 100000000
					info.quoteTickSize = info.quoteTickSizeEv / 100000000
					if (!info.baseTickSize.toString().endsWith('1')) info.baseQtyPrecision += info.baseTickSize
					if (!info.quoteTickSize.toString().endsWith('1')) info.pricePrecision += info.quoteTickSize
				} else {
					info.isHedged = info.type && info.type.endsWith("V2")
					info.isInverse = !info.isHedged && info.settleCurrency !== "USD"
					info.type = info.isHedged ? "USDⓈ-M" : info.isInverse ? "COIN-M" : "USD-M"
					info.minimumTradeSize = info.lotSize || Number(info.minOrderValueRv)
					info.maxOrderQty = info.maxOrderQty || Number(info.maxOrderQtyRq)
					info.quantityPrecision = info.qtyPrecision || decimals(info.lotSize)
					if (info.qtyStepSize && !info.qtyStepSize.toString().endsWith('1')) info.quantityPrecision += Number(info.qtyStepSize)
					if (!info.tickSize.toString().endsWith('1')) info.pricePrecision += Number(info.tickSize)
				}
				info.defaultLeverage = Number(info.defaultLeverage) || 1
				info.priceScale = info.isHedged ? 1 : Math.pow(10, info.priceScale || 8)
				symbolCache[this.sym(info.symbol).toUpperCase()] = info
			})
			this.updatedSymbolCache()
		}
		if (!symbol) return symbolCache

		const lookup = this.sym(symbol).toUpperCase()
		if (!symbolCache[lookup]) {
			throw new Error(`Unknown market symbol: ${symbol} – Use the 'Symbols' button in the exchange settings or alert editor to browse for valid symbols!`)
		}
		return symbolCache[lookup]
	}

	*symbolTickerImpl(symbol) {
		if (!symbol) return null
		const market = yield* this.symbolInfo(symbol)
		let ticker = yield* this.req('GET', `/md/${market.isHedged?'v2/':''}orderbook`, {symbol: market.symbol}, false)
		ticker = ticker && (ticker.orderbook_p || ticker.book)

		return ticker && {
			ask: Number(ticker.asks[0][0]) / market.priceScale,
			bid: Number(ticker.bids[0][0]) / market.priceScale,
			mid: (Number(ticker.asks[0][0]) + Number(ticker.bids[0][0])) / 2 / market.priceScale
		}
	}

	*getPosMode(market, posCache) {
		const def = market.defaultLeverage
		const posMode = {Long: Math.abs(def), Short: Math.abs(def), hedge: market.isHedged, cross: def <= 0}
		try {
			if (market.isHedged) {
				const result = (posCache && posCache.positions || (yield* this.req('GET', `/g-accounts/accountPositions`, {currency: market.settleCurrency})).positions).filter(pos => pos.symbol === market.symbol)
				result.forEach(pos => {
					const lev = Math.abs(pos.leverageRr || def)
					if (pos.posSide === "Merged") {
						posMode.Long = posMode.Short = lev
					} else {
						posMode[pos.posSide] = lev
					}
					posMode.cross = pos.leverageRr <= 0
					posMode.hedge = pos.posMode === "Hedged"
				})
			} else {
				const pos = (posCache && posCache.positions || (yield* this.req('GET', `/accounts/accountPositions`, {currency: market.settleCurrency})).positions).filter(pos => pos.symbol === market.symbol)[0]
				posMode.Long = posMode.Short = Math.abs((pos.leverageEr / 100000000) || def)
				posMode.cross = (pos.leverageEr || def) <= 0
			}
		} catch (ex) {}
		return posMode
	}

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

		const currency = cmd.currency = !market.isSpot ? market.settleCurrency : cmd.isBid ? market.quoteCurrency : market.baseCurrency
		if (!market.isSpot) cmd.currency += marginSuffix
		const quantityPrecision = !market.isSpot ? market.quantityPrecision : cmd.isBid ? market.quoteQtyPrecision : market.baseQtyPrecision
		const posCache = {}
		let balance = {available: 0, balance: 0}
		if ((cmd.y !== "possize" && cmd.q.wasPercent()) || cmd.ub) {
			balance = cmd.bc && !cmd.ub && this.getBalance(currency, !market.isSpot)
			const balances = !balance && (yield* this.account(currency, !market.isSpot, posCache))
			balance = balance || balances[currency]
			this.checkBalance(cmd, balance, currency)
			if (!cmd.bc || cmd.ub) {
				this.updateBalance(balances, currency, !market.isSpot)
				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.fp.resolve(market.pricePrecision) : cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp).resolve(market.pricePrecision)
		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
		const posMode = market.isSpot ? {} : (yield* this.getPosMode(market, posCache))
		const doMode = (cmd.pm === "normal" && posMode.hedge) || (cmd.pm === "hedge" && !posMode.hedge)
		const cross = cmd.mt ? cmd.mt === "crossed" : posMode.cross || cmd.l === 0
		let leverage = cmd._lev = market.isSpot ? 1 : (cmd.l === 0 ? Math.abs(market.defaultLeverage) : cmd.l) * (this.ohlc(cmd, ohlc, 'lr') || 1) || posMode[cmd.isBid ? "Long" : "Short"]
		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] || available * (cmd.lc || leverage) * (market.isInverse ? price : !market.isSpot ? 1/price : 1) / (market.contractSize || 1) * cf
		const scale = !market.isSpot ? 1 : 100000000
		const qty = !market.isSpot ? 'orderQty'+(market.isHedged ? 'Rq':'') : cmd.isBid ? 'quoteQtyEv' : 'baseQtyEv'

		let order = {
			symbol: market.symbol,
			side: cmd.isBid ? "Buy" : "Sell",
			ordType: cmd.t === "market" ? "Market" : "Limit",
			timeInForce: ({fok: "FillOrKill", ioc: "ImmediateOrCancel", post: "PostOnly"})[cmd.t] || "GoodTillCancel",
			[qty]: cmd.q._().mul(!cmd.q.wasPercent() ? (!market.isSpot ? cmd.lc || leverage : 1) / (cmd.u === "currency" ? price : 1) : 1)
				.reference(contracts).mul(this.ohlc(cmd, ohlc, 'qr') || 1).minmax(cmd.minq, cmd.maxq).resolve(quantityPrecision) * scale
		}
		if (market.isSpot) order.qtyType = cmd.isBid ? 'ByQuote' : 'ByBase'
		else order.reduceOnly = cmd.r ? true : false
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			order[qty] = new NumberObject(ohlc.left).resolve(quantityPrecision) * scale
		}
		if (scale > 1) order[qty] = Math.floor(order[qty])

		// baseConv => multiply by to get to quote qty
		const baseConv = (market.isSpot && cmd.isBid) || market.isInverse ? 1 : price
		const maxQty = market.isSpot && !cmd.isBid ? market.maxOrderQtyBase : market.maxOrderQty// / baseConv
		if (market.minimumTradeSize && order[qty] * baseConv < market.minimumTradeSize) {
			this.warn(`order quantity below instrument minimum of ${toPrecision(market.minimumTradeSize / baseConv, quantityPrecision) || market.qtyStepSize} – use minq= to set minimum!`)
		} else if (maxQty && order[qty]/scale > 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.isHedged) {
 			order.posSide = (!doMode && !posMode.hedge) || (doMode && posMode.hedge) ? "Merged" : cmd.isBid ? (!cmd.r ? "Long" : "Short") : (!cmd.r ? "Short" : "Long")
		}
		const suffix = market.isHedged ? "Rp" : "Ep"
		if (cmd.t !== "market") {
			order["price"+suffix] = price * market.priceScale
			if (market.priceScale > 1) order["price"+suffix] = Math.floor(order["price"+suffix])
		}
		order.clOrdID = this.uniqueID(cmd.id)

		if (cmd.so) {
			const act = cmd.so._().relative(this.ohlc(cmd, ohlc, 'soref', ticker, price) || first).resolve(market.pricePrecision)
			order.ordType = cmd.isBid && act < ticker.bid || cmd.isAsk && act > ticker.ask ? `${order.ordType}IfTouched` : cmd.t !== "market" ? 'StopLimit' : 'Stop'
			order["stopPx"+suffix] = act * market.priceScale
			if (market.priceScale > 1) order["stopPx"+suffix] = Math.floor(order["stopPx"+suffix])
			if (market.isSpot) order.trigger = "ByLastPrice"
			else order.triggerType = `By${ucfirst(cmd.sr)}Price`
		}
		if (!market.isSpot) {
			if (cmd.tp) {
				order["takeProfit"+suffix] = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.pricePrecision) * market.priceScale
				if (market.priceScale > 1) order["takeProfit"+suffix] = Math.floor(order["takeProfit"+suffix])
				order.tpTrigger = `By${ucfirst(cmd.sr)}Price`
			}
			if (cmd.sl) {
				order["stopLoss"+suffix] = cmd.sl._().relative(this.ohlc(cmd, ohlc, 'slref', ticker, price) || first).resolve(market.pricePrecision) * market.priceScale
				if (market.priceScale > 1) order["stopLoss"+suffix] = Math.floor(order["stopLoss"+suffix])
				order.slTrigger = `By${ucfirst(cmd.sr)}Price`
			}
		}

		this.info(`placing ${currency} order${market.isSpot?'':` @ ${leverage.toFixed(2).replace('.00','')}x${cross?' cross':' isolated'}${order.posSide && order.posSide !== "Merged"?' (hedged)':''}`}: `+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}

		if (market.isHedged && doMode) {
			this.info(`setting position mode to ${cmd.pm}...`)
			try {
				yield* this.req('PUT', "/g-positions/switch-pos-mode-sync", {symbol: market.symbol, targetPosMode: cmd.pm !== "hedge" ? "OneWay" : "Hedged"})
			} catch(ex) {
				this.warn(`couldn't change position mode, please change manually! (Error: ${ex.error || ex.message})`)
			}
		} else if (doMode) {
			this.warn(`this market does not support different position modes!`)
		}

		const doLev = cmd.l !== undefined && leverage != posMode[order.posSide && order.posSide !== "Merged" ? order.posSide : cmd.isBid ? "Long" : "Short"]
		const doType = cross != posMode.cross
		if (!market.isSpot && (doLev || doType)) {
			this.info(`setting contract leverage to ${leverage}x${cross?' cross':' isolated'}...`)
			cross && (leverage = -leverage)
			try {
				if (market.isHedged) {
					yield* this.req('PUT', "/g-positions/leverage", order.posSide === "Merged" ? {symbol: market.symbol, leverageRr: leverage} : {symbol: market.symbol, 
							longLeverageRr: order.posSide === "Long" ? leverage : cross ? -posMode.Long : posMode.Long, 
							shortLeverageRr: order.posSide === "Short" ? leverage : cross ? -posMode.Short : posMode.Short})
				} else {
					yield* this.req('PUT', "/positions/leverage", {symbol: market.symbol, leverage: leverage, leverageEr: Math.floor(leverage * 100000000)})
				}
			} catch(ex) {
				this.warn(`couldn't set leverage, please set manually! (Error: ${ex.error || ex.message})`)
			}
		}

		this.checkOrder(order, 264889806, -10)
		if (maxQty && order[qty]/scale > 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)...`)
					while((yield* sleep(opt.splitDelay)) == null);
				}
				order[qty] = Math.min(leftQty, maxQty*scale)
				order.clOrdID = this.uniqueID(cmd.id, i)
				this.info(`posting split order #${i++} of size ${order[qty]}...`)
				result = yield* this.req('POST', !market.isSpot ? `/${market.isHedged ? 'g-':''}orders` : "/spot/orders", order)
				if (i < 2 && cmd.t === 'post' && cmd.ifr && result && result.orderID) {
					while((yield* sleep(0.5)) == null);
					const order = (yield* this.req('GET', `/api-data/${market.isSpot ? 'spots' : market.isHedged ? 'g-futures' : 'futures'}/orders/by-order-id`, {symbol: market.symbol, orderID: result.orderID})).rows[0]
					if (order && (order.ordStatus === 'Canceled' || order.ordStatus === 8)) {
						throw new Error(errRejected)
					}
				}
				order.posSide && result && result.orderID && this.cacheId(result.orderID, order.posSide)
				leftQty -= order[qty]
			} while(leftQty > 0)
			return result
		}
		const result = yield* this.req('POST', !market.isSpot ? `/${market.isHedged ? 'g-':''}orders` : "/spot/orders", order)
		order.posSide && result && result.orderID && this.cacheId(result.orderID, order.posSide)
		if (cmd.t === 'post' && cmd.ifr && result && result.orderID) {
			while((yield* sleep(0.5)) == null);
			const order = (yield* this.req('GET', `/api-data/${market.isSpot ? 'spots' : market.isHedged ? 'g-futures' : 'futures'}/orders/by-order-id`, {symbol: market.symbol, orderID: result.orderID})).rows[0]
			if (order && (order.ordStatus === 'Canceled' || order.ordStatus === 8)) {
				throw new Error(errRejected)
			}
		}
		return result
	}

	*ordersCancel(cmd, ohlc = {}) {
		const market = yield* this.symbolInfo(cmd.s)
		cmd._sym = market.symbol
		if (!cmd.ch && !cmd.d && !cmd.b && !cmd.tp && !cmd.sl && !cmd.ts && !cmd.fp && cmd.r === undefined && !cmd.id && cmd.cm.wasPercent(true) && !cmd.gt && !cmd.gte && !cmd.lt && !cmd.lte) {
			// 1 = stop, 2 = limit, 3 = all
			const type = !cmd.so && cmd.t !== "close" ? (!cmd.has('t') ? 3 : 2) : 1
			let orders = []
			const prefix = market.isHedged ? 'g-' : market.isSpot ? 'spot/' : ''
			if (type > 1) {
				this.info(`canceling all ${market.symbol} limit orders in bulk!`)
	 			try {
					const result = yield* this.req('DELETE', `/${prefix}orders/all`, {symbol: market.symbol, untriggered: false})
					market.isSpot && (orders = orders.concat(new Array(result.total)))
				} catch(ex) {
/*					if (ex.code === 10118) {
						this.msgOrdersNone()
						return false
					}*/
				}
			}
			if (type !== 2) {
				this.info(`canceling all ${market.symbol} stop orders in bulk!`)
	 			try {
					const result = yield* this.req('DELETE', `/${prefix}orders/all`, {symbol: market.symbol, untriggered: true})
					market.isSpot && (orders = orders.concat(new Array(result.total)))
				} catch(ex) {
/*					if (ex.code === 10118) {
						this.msgOrdersNone()
						return false
					}*/
				}
			}
			if (!market.isSpot) return true
			if (!orders.length) {
				this.msgOrdersNone()
				return false
			}
			return orders
		}

		let orders = (yield* this.req('GET', !market.isSpot ? `/${market.isHedged ? 'g-':''}orders/activeList` : "/spot/orders", {symbol: market.symbol})) || []
		orders.rows && (orders = orders.rows)
		if (!orders || !orders.filter) {
			this.msgOrderErr()
			return false
		}
		orders = orders.filter(order => order.symbol === market.symbol && order.execStatus !== "PendingCancel")
		if (!orders.length) {
			this.msgOrdersNone()
			return false
		}

		const ids = (cmd.id || "").split(',')
		const hasType = cmd.has('t') && (cmd.t === "limit" || cmd.t === "market")
		const match = cmd.cr ? false : true
		const tickers = {}
		yield* 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
			}
			order.price = order.price || order.priceRp || (order.priceEp / market.priceScale)
			if (cmd.fp && cmd.fp.compare(order.price)) {
				return !match
			}
			if (cmd.tp && !(order.orderType || order.ordType).endsWith("IfTouched") || cmd.sl && !(order.orderType || order.ordType).startsWith("Stop") || 
				cmd.ts && (!order.pegPriceType || order.pegPriceType === "UNSPECIFIED")) {
				return !match
			}
			if (hasType && (cmd.t === "limit" && !(order.orderType || order.ordType).toLowercase().includes("limit") || 
							cmd.t === "market" && (order.orderType || order.ordType).toLowercase().includes("limit"))) {
				return !match
			}
			if (cmd.r !== undefined && !cmd.r != !order.reduceOnly) {
				return !match
			}
			if (cmd.t === "open" && Number(order.stopPx || order.stopPxRp || order.stopPxEp) || 
				cmd.t === "close" && !Number(order.stopPx || order.stopPxRp || order.stopPxEp)) {
				return !match
			}
			if (cmd.id && (!order.clOrdID || !order.clOrdID.startsWithAny(ids, true))) {
				return !match
			}
			if (!this.filterOrder(cmd, ohlc, tickers, order)) {
				return !match
			}
			order.orderQty = order.orderQty || order.orderQtyRq || ((order.quoteQtyEv || order.baseQtyEv) / 100000000)
			order.leavesQty = order.leavesQty || order.leavesQtyRq || ((order.leavesQuoteQtyEv || order.leavesBaseQtyEv) / 100000000)
			return match
		})

		if (cmd.cm._().reference(orders.length).getMax() < orders.length) {
			const sort = {
				"newest": 	["actionTimeNs", true],
				"oldest": 	["actionTimeNs", false],
				"highest": 	["price", true],
				"lowest": 	["price", false],
				"biggest": 	["orderQty", true],
				"smallest": ["orderQty", 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 (!orders.length || cmd.ch || cmd.d) {
			orders.length && this.msgDisabled(cmd)
			return orders
		}

		if (!market.isSpot) {
			if (!market.isHedged) {
				return (yield* this.req('DELETE', `/orders`, {symbol: market.symbol, orderID: orders.map(order => order.orderID).join(',')})) || []
			}

			const resultAll = []
			for (const type of ['Long', 'Short', 'Merged']) {
				const orderIds = orders.filter(order => {
					const posSide = order.posSide || this.getIdCache(order.orderID)
					return !posSide || posSide === type
				}).map(order => order.orderID).join(',')

				if (orderIds) {
					const result = (yield* this.req('DELETE', `/g-orders`, {symbol: market.symbol, orderID: orderIds, posSide: type})) || []
					resultAll.concat(result.filter(order => order.orderID))
				}
			}
			resultAll.forEach(order => this.removeId(order.orderID))
			return resultAll
		}

		let cancelOrders = []
		for (const order of orders) {
			const result = yield* this.req('DELETE', "/spot/orders", {symbol: market.symbol, orderID: order.orderID})
			cancelOrders.push(result)
		}
		return cancelOrders
	}

	*positionsClose(cmd, pos, i, ohlc = {}, market) {
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.symbol)) || (yield* this.symbolTicker(cmd.up, market.symbol))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.name} is not available!`)
		}
		ticker.pos = pos.avgEntryPrice
		ticker.liq = pos.liquidationPrice
		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.fp.resolve(market.pricePrecision) : cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp).resolve(market.pricePrecision)
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated, use minp= to set minimum)")
		}

		const isLong = pos.side === 'Buy'
		const leverage = pos.leverage || market.defaultLeverage || 1
		const qty = 'orderQty'+(market.isHedged ? 'Rq':'')

		let order = {
			symbol: market.symbol,
			side: isLong ? 'Sell' : 'Buy',
			ordType: cmd.t === "market" ? "Market" : "Limit",
			timeInForce: ({fok: "FillOrKill", ioc: "ImmediateOrCancel", post: "PostOnly"})[cmd.t] || "GoodTillCancel",
			[qty]: cmd.q._().mul(!cmd.q.wasPercent() ? (cmd.lc || leverage) / (cmd.u === "currency" ? price : 1) : 1)
				.reference(pos.size).mul(this.ohlc(cmd, ohlc, 'qr') || 1).minmax(cmd.minq, cmd.maxq).resolve(market.quantityPrecision),
			reduceOnly: cmd.r === undefined ? true : cmd.r
		}
		if (order[qty] <= 0) this.warn("your q parameter might be incorrect! (quantity of 0 calculated, use minq= to set minimum)")
		const suffix = market.isHedged ? "Rp" : "Ep"
		if (cmd.t !== "market") {
			order["price"+suffix] = price * market.priceScale
			if (market.priceScale > 1) order["price"+suffix] = Math.floor(order["price"+suffix])
		}
		order.clOrdID = this.uniqueID(cmd.id, i)

		const stop = cmd.tp || cmd.sl || cmd.so
		if (stop || cmd.ts) {
			const param = cmd.tp && 'tpref' || cmd.sl && 'slref' || cmd.so && 'soref'
			const last = isLong ? ticker.bid : ticker.ask
			const act = stop ? stop._().relative(this.ohlc(cmd, ohlc, param, ticker, price) || first).resolve(market.pricePrecision) : last
			order["stopPx"+suffix] = act * market.priceScale
			if (market.priceScale > 1) order["stopPx"+suffix] = Math.floor(order["stopPx"+suffix])
			order.triggerType = `By${ucfirst(cmd.sr)}Price`
			if (cmd.ts) {
				order.ordType = "Stop"
				order.pegPriceType = isLong && act > last || !isLong && act < last ? "TrailingTakeProfitPeg" : "TrailingStopPeg"
				order["pegOffsetValue"+suffix] = cmd.ts._().reference(this.ohlc(cmd, ohlc, 'tsref', ticker, price) || first).abs().mul(isLong ? -1 : 1).resolve(market.pricePrecision) * market.priceScale
				if (market.priceScale > 1) order["pegOffsetValue"+suffix] = Math.floor(order["pegOffsetValue"+suffix])
				order["stopPx"+suffix] += order["pegOffsetValue"+suffix]
			} else {
				order.ordType = order.side === 'Buy' && act < ticker.bid || order.side === 'Sell' && act > ticker.ask ? `${order.ordType}IfTouched` : cmd.t !== "market" ? 'StopLimit' : 'Stop'
			}
		}

		if (order.reduceOnly && cmd.q.wasPercent(true)) {
			delete order[qty]
			delete order.reduceOnly
			order.closeOnTrigger = true
		}
		if (market.isHedged) order.posSide = pos.posSide

		!cmd.ch && this.info(`placing Close Position order @ ${leverage}x: `+stringify(order))
		if (cmd.ch || cmd.d) {
			this.msgDisabled(cmd)
			return order
		}
		this.checkOrder(order, 264889806, -10)
		const result = yield* this.req('POST', `/${market.isHedged ? 'g-':''}orders`, order)
		if (cmd.t === 'post' && cmd.ifr && result && result.orderID) {
			while((yield* sleep(0.5)) == null);
			const order = (yield* this.req('GET', `/api-data/${market.isSpot ? 'spots' : market.isHedged ? 'g-futures' : 'futures'}/orders/by-order-id`, {symbol: market.symbol, orderID: result.orderID})).rows[0]
			if (order && (order.ordStatus === 'Canceled' || order.ordStatus === 8)) {
				throw new Error(errRejected)
			}
		}
		return result
	}

	*positionsCloseAll(cmd, ohlc, positions) {
		const isAll = !cmd.s || cmd.s === '*'
		const symbols = isAll ? yield* this.symbolInfo() : {[cmd.s]: yield* this.symbolInfo(cmd.s)}
		if (!positions) {
			positions = []
			const currencies = [...new Set(Object.mapAsArray(symbols, (key, info) => info.settleCurrency))]
			//const currencies = [...new Set(Object.values(symbols).map(info => info.settleCurrency))]
			for (const currency of currencies) {
				if (!currency || currency === 'USD') continue
				const isHedged = currency === 'USDT'
				const result = ((yield* this.req('GET', `/${isHedged ? "g-":''}accounts/accountPositions`, {currency: currency})) || {}).positions
				if (!result || !result.filter) {
					if (isAll) continue
					this.msgPosErr()
					return false
				}
				positions = positions.concat(result)
			}
			positions = positions.filter(pos => (pos.size = Number(pos.sizeRq || pos.size)) && ((pos._sym = pos.symbol.toUpperCase()) === cmd.s || isAll))
			if (!positions.length) {
				this.msgPosNone()
				return false
			}
		}

		const markPrice = {}
		if (cmd.up || cmd.minpl || cmd.maxpl || !cmd.s) for (const pos of positions) {
			if (!markPrice[pos._sym]) {
				const market = symbols[pos._sym] || {}
				try {
					const ticker = yield* this.req('GET', `/md/v${market.isHedged ? '2' : '1'}/ticker/24hr`, {symbol: pos._sym}, false)
					markPrice[pos._sym] = ticker.markPriceRp || ticker.markPriceEp / market.priceScale
				} catch(ex){}
			}
		}

		cmd.isMargin = true
		const match = cmd.cr ? false : true
		const tickers = {}
		yield* 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
			}
			const market = symbols[pos._sym] || {}
			const scale = market.isHedged ? 1 : market.isInverse ? 100000000 : 10000
			pos.currency = cmd.currency = market.settleCurrency + marginSuffix
			pos.avgEntryPrice = toPrecision(pos.avgEntryPriceRp || pos.avgEntryPriceEp / market.priceScale, market.pricePrecision)
			if (cmd.fp && cmd.fp.compare(pos.avgEntryPrice)) {
				return !match
			}
			pos.leverage = Math.abs(pos.leverageRr || pos.leverageEr / 100000000)
			if (cmd.l && pos.leverage !== cmd.l) {
				return !match
			}
			pos.isolated = (pos.leverageRr || pos.leverageEr) > 0
			pos.usedBalance = pos.usedBalanceRv || pos.usedBalanceEv / scale
			pos.markPrice = markPrice[pos._sym] || (pos.markPriceRp || pos.markPriceEp / market.priceScale)
			pos.liquidationPrice = toPrecision(pos.liquidationPriceRp || pos.liquidationPriceEp / market.priceScale, market.pricePrecision)
			pos._sizeCoin = pos.usedBalance / (market.isInverse ? 1 : pos.markPrice)
			pos._sizeUSD = pos.usedBalance * (market.isInverse ? pos.markPrice : 1)
			pos.pnl = pos.side === 'Buy' ? pos.markPrice / pos.avgEntryPrice - 1 : -pos.markPrice / pos.avgEntryPrice + 1
			pos.unrealisedPnl = pos.pnl * (market.isInverse ? pos._sizeCoin : pos._sizeUSD) * pos.leverage
			//pos.unrealisedPnl = pos.unRealisedPnlRv || pos.unRealisedPnlEv / scale
			//pos.pnl = pos.unrealisedPnl / (market.isInverse ? pos._sizeCoin : pos._sizeUSD) / pos.leverage
			pos.unrealisedPnl = toPrecision(pos.unrealisedPnl, 8)
			pos.markPrice = toPrecision(pos.markPrice, market.pricePrecision)
			pos.transactTimeNs && (pos.transactTimeNs /= 1000000)
			if (!this.filterPos(cmd, ohlc, tickers, pos, balance => balance * pos.leverage * 
					(market.isInverse ? pos.avgEntryPrice : 1 / pos.avgEntryPrice) / (market.contractSize || 1), market.quantityPrecision, true)) {
				return !match
			}
			return match
		})

		if (cmd.cm._().reference(positions.length).getMax() < positions.length) {
			if (["newest", "oldest"].includes(cmd.cmo)) {
				this.warn(`Close Maximum Order option "${cmd.cmo}" is currently not supported!`)
			} else {
				const sort = {
					"highest": 	["avgEntryPrice", true],
					"lowest": 	["avgEntryPrice", false],
					"biggest": 	["size", true],
					"smallest": ["size", 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 = yield* this.positionsClose(cmd, pos, i++, ohlc, symbols[pos._sym])
			if (order) {
				closeOrders.push(order)
				Object.assign(closeOrders.last(), Object.without(pos, ["size", "sizeRq", "side"]))
			}
		}
		return closeOrders
	}
}

Broker.add(new PhemexTestnet())
