"use strict"

class BinanceFuturesTestnet extends Exchange {
	static meta = {
		aliases: [
			"BINANCEFUTURESTESTNET",
			"BINANCEFUTURES-TESTNET",
			"BINANCEFTTESTNET",
			"BINANCEFT-TESTNET",
		],
		endpoint: "https://testnet.binancefuture.com/fapi/",
		endpoint2: "https://testnet.binancefuture.com/dapi/",
		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: "Binance Futures Testnet",
		permissions: {
			origins: [
				"https://testnet.binancefuture.com/*",
			],
		},
		subscriptions: {
			active: [],
			inactive: [],
		},
		recvWindow: 30 * 1000,
		website: "https://testnet.binancefuture.com/?ref=AOBJU8Z4",
		desc: "Binance Futures is now letting traders choose up to 125x leverage on their futures trades.",
		defaultAsset: "USDT",
		multiAsset: "USD"+marginSuffix,
		testSymbol: "BTCUSDT",
		margin: true, posMode: true, marginType: true,
	}
	constructor(meta = BinanceFuturesTestnet.meta) {
		super(meta)
	}

	async req(method, resource, delivery = false, params, sign = true) {
		const headers = !params ? {"Content-Type": "application/json"} : {}
		if (sign) {
			const credentials = this.getCredentials("private")
			headers["X-MBX-APIKEY"] = credentials.public
			params = params || {}
			params.timestamp = this.getTimestamp()
			params.recvWindow = this.meta.recvWindow
			params.signature = CryptoJS.HmacSHA256(serialize(params), credentials.private).toString(CryptoJS.enc.Hex)
		} else this.hasAccess()

		const result = await request.call(this, method, (delivery ? sign && this.meta.endpoint4 || this.meta.endpoint2 : sign && this.meta.endpoint3 || this.meta.endpoint) + resource, params, headers)
		if (result && result[0] && result[0].code && result[0].code < 0) {
			throw new Error(result[0].msg || "Unknown connection error with API endpoint!")
		}
		return result
	}

	async time() {
		return (await request.call(this, 'GET', this.meta.endpoint + "v1/time", null, null)).serverTime
	}

	async account(currency, isMargin) {
		let balances = {}
		const isTest = this.isTest()
		const isFutures = ['USDT', 'BUSD', this.meta.multiAsset].includes(currency) || isMargin

		const futures = isTest || !currency || isFutures ? (await this.req('GET', "v2/account", false)) : {assets:[]}
		if (!futures || !futures.assets || !futures.assets.forEach) {
			this.info("unable to get account info! (Futures / USDⓈ-M)")
			return balances
		}
		const delivery = isTest || !currency || !isFutures ? (await this.req('GET', "v1/balance", true)) : []
		if (!delivery || !delivery.forEach) {
			this.info("unable to get account info! (Delivery / COIN-M)")
			return balances
		}

		;[...futures.assets, ...delivery].forEach(item => Number(item.balance || item.marginBalance) && (balances[item.asset] = {
			available: Number(item.availableBalance) || Number(item.withdrawAvailable || item.maxWithdrawAmount) || 0,
			balance: Number(item.balance || item.marginBalance) || 0
		}))
		isTest && (balances[this.meta.defaultAsset] = balances[this.meta.defaultAsset] || {available: 0, balance: 0})
		if (futures.totalMarginBalance) {
			balances[this.meta.multiAsset] = {
				available: Number(futures.availableBalance) || Number(futures.maxWithdrawAmount) || 0,
				balance: Number(futures.totalMarginBalance) || 0,
				isMulti: futures.multiAssetsMargin || (Number(futures.totalMarginBalance) || 0) > (balances.USDT?.balance || 0)
			}
		}
		return isMargin ? balances[this.meta.multiAsset] : balances
	}

	async symbolInfoImpl(cache) {
		const futures = (await this.req('GET', "v1/exchangeInfo", false, undefined, false)).symbols || []
		futures.forEach && futures.forEach(info => info.type = "USDⓈ-M")
		const delivery = (await this.req('GET', "v1/exchangeInfo", true, undefined, false)).symbols || []
		delivery.forEach && delivery.forEach(info => {
			info.isDelivery = true; info.type = "COIN-M"
			if (info.contractType === "CURRENT_QUARTER" || info.contractType === "NEXT_QUARTER") {
				const dd = new Date(info.deliveryDate)
				cache[info.pair + 'FGHJKMNQUVXZ'[dd.getMonth()] + (dd.getYear()+1900)] = info
			}
		})
		;[...futures, ...delivery].forEach(info => {
			let filters = {}
			info.filters.forEach(filter => filters[filter.filterType] = filter)
			info.pricePrecision = filters.PRICE_FILTER ? decimals(filters.PRICE_FILTER.tickSize) : info.pricePrecision	// minPrice maxPrice
			info.quantityPrecision = filters.LOT_SIZE ? decimals(filters.LOT_SIZE.stepSize) : info.quantityPrecision
			info.minQty = filters.LOT_SIZE && Number(filters.LOT_SIZE.minQty) || 0
			info.maxQty = filters.LOT_SIZE && Number(filters.LOT_SIZE.maxQty) || 0
			info.minNotional = filters.MIN_NOTIONAL ? Number(filters.MIN_NOTIONAL.notional || filters.MIN_NOTIONAL.minNotional) : 0
			info.filters = filters
			cache[this.sym(info.symbol)] = info
		})
	}

	async symbolTickerImpl(symbol, getMark) {
		const market = await this.symbolInfo(symbol)
		let ticker = await this.req('GET', "v1/ticker/bookTicker", market.isDelivery, {symbol: market.symbol}, false)
		ticker = ticker && (ticker[0] || ticker)
		let premium = null
		getMark && (premium = await this.req('GET', "v1/premiumIndex", market.isDelivery, {symbol: market.symbol}, false))
		premium = premium && (premium[0] || premium)

		return ticker && Object.assign({
			ask: Number(ticker.askPrice),
			bid: Number(ticker.bidPrice),
			mid: (Number(ticker.askPrice) + Number(ticker.bidPrice)) / 2
		}, premium && {
			mark: Number(premium.markPrice),
			index: Number(premium.indexPrice)
		})
	}

	async getPosMode(market) {
		let leverage = 5
		const posMode = {BUY: leverage, SELL: leverage, isCross: true, isHedge: false}
		try {
			const result = (await this.req('GET', `${market.isDelivery?'v1':'v2'}/positionRisk`, market.isDelivery, 
				market.isDelivery ? {pair: market.pair} : {symbol: market.symbol})).filter(pos => pos.symbol == market.symbol)
			for (const pos of result) {
				posMode[({LONG: "BUY", SHORT: "SELL"})[pos.positionSide] || (Number(pos.positionAmt) < 0 ? "SELL" : "BUY")] = leverage = Number(pos.leverage) || 1
				if (pos.isolatedMargin != 0) posMode.isCross = false
			}
			posMode.isHedge = result.length > 1 //(await this.req('GET', "v1/positionSide/dual", market.isDelivery)).dualSidePosition
			if (!posMode.isHedge) posMode.BUY = posMode.SELL = leverage
		} catch (ex) {
			this.debug(`couldn't get position mode! (Error: ${ex.message})`)
		}
		return posMode
	}

	async trade(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.symbol
		let isMulti = !market.isDelivery && (this.getBalanceRaw()[this.meta.multiAsset] || {}).isMulti
		let currency = cmd.currency = (market.isDelivery && market.marginAsset) || (isMulti && this.meta.multiAsset) || market.quoteAsset || this.meta.defaultAsset
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.symbol)) || (await this.symbolTicker(cmd.up, market.symbol, cmd.up || ["mark", "index"].includes(cmd.pr)))
		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
			}
		}

		let balance = {available: 0, balance: 0}
		if ((cmd.y !== "possize" && cmd.q.wasPercent()) || cmd.ub) {
			balance = cmd.bc && !cmd.ub && this.getBalance(currency)
			const balances = !balance && (await this.account(currency))
			if (!market.isDelivery && balances?.[this.meta.multiAsset]?.isMulti !== undefined && balances?.[this.meta.multiAsset]?.isMulti !== isMulti) {
				isMulti = balances[this.meta.multiAsset].isMulti
				currency = cmd.currency = (market.isDelivery && market.marginAsset) || (isMulti && this.meta.multiAsset) || market.quoteAsset || this.meta.defaultAsset
			}
			balance = balance || balances[currency]
			this.checkBalance(cmd, balance, currency)
			if (!cmd.bc || cmd.ub) {
				this.updateBalance(balances, currency)
				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 side = cmd.isBid ? "BUY" : "SELL"
		const posMode = await this.cacheState(market, this.getPosMode)
		if (isMulti) posMode.isCross = true
		const leverage = cmd._lev = cmd.l * (this.ohlc(cmd, ohlc, 'lr') || 1) || posMode[side] || 1
		const available = cmd.y === "equity" ? balance.balance : balance.available
		const contracts = ohlc[cmd.y] || available * (cmd.lc || leverage) * (market.isDelivery ? price / market.contractSize : 1 / price)
		const hedge = cmd.pm ? cmd.pm === "hedge" : posMode.isHedge
		const cross = cmd.mt ? cmd.mt === "crossed" : posMode.isCross || cmd.l === 0
		const doMode = hedge != posMode.isHedge
		const doType = cross != posMode.isCross
		const doLev = cmd.l !== undefined && leverage != posMode[side]

		let order = {
			symbol: market.symbol, side,
			type: cmd.t === "market" ? "MARKET" : "LIMIT",
			timeInForce: ({fok: "FOK", ioc: "IOC", post: "GTX"})[cmd.t] || "GTC",
			quantity: cmd.q._().mul(!cmd.q.wasPercent() ? (cmd.lc || leverage) / (cmd.u === "currency" ? (market.isDelivery ? market.contractSize : price) : 1) : 1)
				.reference(contracts).mul(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc)).minmax(cmd.minq, cmd.maxq).resolve(market.quantityPrecision),
			positionSide: !hedge ? "BOTH" : cmd.isBid ? (!cmd.r ? "LONG" : "SHORT") : (!cmd.r ? "SHORT" : "LONG"),
			newOrderRespType: "RESULT"
		}
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			order.quantity = ohlc.left.toFixed(market.quantityPrecision)
		}
		if (market.minNotional && order.quantity * price < market.minNotional) {
			this.warn(`order value below instrument minimum notional of ${market.minNotional} – use minq= to set minimum!`)
		} else if (market.minQty && order.quantity < market.minQty) {
			this.warn(`order quantity below instrument minimum of ${market.minQty} – use minq= to set minimum!`)
		} else if (market.maxQty && order.quantity > market.maxQty) {
			this.warn(`order quantity above instrument maximum of ${market.maxQty} – use maxq= to set minimum!`)
		} else if (order.quantity <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated, use minq= to set minimum)")
		}
		if (cmd.t !== "market") {
			order.price = price
		} else {
			delete order.timeInForce
		}
		order.newClientOrderId = this.prefixID("x-AnnFpdrg")+this.uniqueIDA(26, cmd.id)

		let isAlgo = false
		if (cmd.ts) {
			const param = cmd.so && 'soref' || 'tsref'
			const stop = this.ohlc(cmd, ohlc, param, ticker, price) || first
			order.type = "TRAILING_STOP_MARKET"
			if (cmd.so || cmd.cbrate) {
				order.activationPrice = (cmd.so || cmd.ts)._().relative(stop).resolve(market.pricePrecision)
			}
			order.callbackRate = cmd.cbrate ? minmax(cmd.cbrate || 0.1, 0.1, 5) : cmd.ts._().reference(stop).div(stop / 10).minmax(0.1, 5).toDecimal()
			delete order.timeInForce
			if (cmd.sr !== "last") order.workingType = cmd.sr.toUpperCase() + "_PRICE"
			isAlgo = !market.isDelivery
		} else if (cmd.tp) {
			order.type = cmd.t === "market" ? "TAKE_PROFIT_MARKET" : "TAKE_PROFIT"
			order.stopPrice = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.pricePrecision)
			if (cmd.sr !== "last") order.workingType = cmd.sr.toUpperCase() + "_PRICE"
			if (cmd.pp) order.priceProtect = "TRUE"
			isAlgo = !market.isDelivery
		} else if (cmd.sl || cmd.so) {
			order.type = cmd.t === "market" ? "STOP_MARKET" : "STOP"
			order.stopPrice = (cmd.sl || cmd.so)._().relative(this.ohlc(cmd, ohlc, cmd.sl && 'slref' || 'soref', ticker, price) || first).resolve(market.pricePrecision)
			if (cmd.sr !== "last") order.workingType = cmd.sr.toUpperCase() + "_PRICE"
			if (cmd.pp) order.priceProtect = "TRUE"
			isAlgo = !market.isDelivery
		}
		if (cmd.r && order.positionSide === "BOTH") {
			if ((cmd.sl || cmd.tp) && cmd.t === "market" && cmd.q.wasPercent(true)) {
				delete order.quantity
				order.closePosition = true
			} else {
				order.reduceOnly = true
			}
		}
		if (isAlgo) {
			order.algoType = "CONDITIONAL"
			order.clientAlgoId = order.newClientOrderId
			delete order.newClientOrderId
			if (order.stopPrice !== undefined) {
				order.triggerPrice = order.stopPrice
				delete order.stopPrice
			}
		}

		this.info(`placing ${currency} order @ ${leverage}x ${cross ? 'cross' : 'isolated'} ${hedge ? 'hedged' : 'one-way'}${isMulti?" (Multi Asset)":''}: `+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}

		if (doMode) {
			this.info(`setting position mode to ${hedge ? 'hedged' : 'one-way'}...`)
			try {
				await this.req('POST', "v1/positionSide/dual", market.isDelivery, {dualSidePosition: hedge})
				if (!hedge) {
					this.clearState(market)
				} else {
					posMode.isHedge = hedge
					this.updateState(market, posMode)
				}
			} catch(ex) {
				const err = ex.error || ex.msg || ex.message
				this.warn(`couldn't change position mode, please change manually! (Error: ${err})`)
			}
		}
		if (!isMulti && doType) {
			this.info(`setting contract margin type to ${cross ? 'cross' : 'isolated'}...`)
			try {
				await this.req('POST', "v1/marginType", market.isDelivery, {symbol: market.symbol, marginType: cross?'CROSSED':'ISOLATED'})
				posMode.isCross = cross
				this.updateState(market, posMode)
			} catch(ex) {
				const err = ex.error || ex.msg || ex.message
				(!err || !err.startsWith("No need to change")) && this.warn(`couldn't change margin type, please change manually! (Error: ${err})`)
			}
		}
		if (doLev) {
			this.info(`setting contract leverage to ${leverage}x...`)
			try {
				await this.req('POST', "v1/leverage", market.isDelivery, {symbol: market.symbol, leverage})
				if (hedge) {
					posMode[side] = leverage
				} else {
					posMode.BUY = posMode.SELL = leverage
				}
				this.updateState(market, posMode)
			} catch(ex) {
				const err = ex.error || ex.msg || ex.message
				this.warn(`couldn't set leverage, please set manually! (Error: ${err})`)
			}
		}

		let result = null
		this.checkOrder(order, 107115019, 10)
		if (market.maxQty && order.quantity > market.maxQty) {
			let leftQty = order.quantity, i = 1
			do {
				if (i > 1 && opt.splitDelay) {
					this.info(`waiting for ${opt.splitDelay}sec (split order delay)...`)
					await sleep(opt.splitDelay)
				}
				order.quantity = new NumberObject(Math.min(leftQty, market.maxQty)).resolve(market.quantityPrecision)
				order.newClientOrderId = this.prefixID("x-AnnFpdrg")+this.uniqueIDA(26, cmd.id, i)
				this.info(`posting split order #${i++} of size ${order.quantity}...`)
				result = await this.req('POST', isAlgo ? "v1/algoOrder" : "v1/order", market.isDelivery, order)
				leftQty -= order.quantity
			} while(leftQty > 0)
		} else {
			result = await this.req('POST', isAlgo ? "v1/algoOrder" : "v1/order", market.isDelivery, order)
		}
		if (result && result.price === "0") delete result.price
		if (result && result.stopPrice === "0") delete result.stopPrice
		return result
	}

	async ordersCancel(cmd, ohlc = {}) {
		const market = await this.symbolInfo(cmd.s)
		cmd._sym = market.symbol
		if (!cmd.ch && !cmd.d && !cmd.b && !cmd.fp && !cmd.tp && !cmd.sl && !cmd.so && !cmd.ts && (!cmd.has('t') || (!market.isDelivery && (cmd.t === "open" || cmd.t === "close"))) && 
			cmd.r === undefined && !cmd.id && cmd.cm.wasPercent(true) && !cmd.gt && !cmd.gte && !cmd.lt && !cmd.lte && !cmd.sv) {
			let success = true
			if (market.isDelivery || cmd.t !== "close") {
				this.info(`canceling all ${market.symbol} orders in bulk!`)
				const result = await this.req('DELETE', "v1/allOpenOrders", market.isDelivery, {symbol: market.symbol})
				if (result?.code !== 200) success = false
			}
			if (!market.isDelivery && cmd.t !== "open") {
				this.info(`canceling all ${market.symbol} algo orders in bulk!`)
				const result = await this.req('DELETE', "v1/algoOpenOrders", false, {symbol: market.symbol})
				if (result?.code !== 200) success = false
			}
			return success
		}

		let orders = []
		if (market.isDelivery || cmd.t !== "close") {
			orders = await this.req('GET*', "v1/openOrders", market.isDelivery, {symbol: market.symbol})
			if (!orders || !orders.filter) {
				this.msgOrderErr()
				return false
			}
		}
		if (!market.isDelivery && cmd.t !== "open") {
			const algos = await this.req('GET*', "v1/openAlgoOrders", false, {symbol: market.symbol})
			if (!algos || !algos.filter) {
				this.msgOrderErr()
				return false
			}
			orders.push(...algos)
		}
		orders = orders.filter(order => order.status !== "FILLED")
		if (!orders.length) {
			this.msgOrdersNone()
			return false
		}

		const ids = (cmd.id || "").split(',').map(e => alnum(e))
		const hasType = cmd.has('t') && (cmd.t === "limit" || cmd.t === "market")
		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
			}
			const type = order.type || order.orderType || ""
			if ((cmd.tp && !type.startsWith("TAKE_PROFIT")) || ((cmd.sl || cmd.so) && !type.startsWith("STOP")) || (cmd.ts && !type.startsWith("TRAILING_STOP"))) {
				return !match
			}
			if (hasType && ((cmd.t === "limit" && !order.type.includes("LIMIT")) || (cmd.t === "market" && order.type.includes("LIMIT")))) {
				return !match
			}
			if ((cmd.t === "open" && Number(order.stopPrice || order.triggerPrice)) || (cmd.t === "close" && !Number(order.stopPrice || order.triggerPrice))) {
				return !match
			}
			if (cmd.r !== undefined && !cmd.r != !order.reduceOnly) {
				return !match
			}
			const id = order.clientOrderId || order.clientAlgoId
			if (cmd.id && (!id || !id.substr(10).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": 	["updateTime", true],
				"oldest": 	["updateTime", false],
				"highest": 	["price", true],
				"lowest": 	["price", false],
				"biggest": 	["origQty", true],
				"smallest": ["origQty", 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
		}

		const orderIds = orders.map(order => order.orderId).filter(Boolean)
		// $$$ max batch length 10 !!!
		const result = !orderIds.length ? [] : await this.req('DELETE', "v1/batchOrders", market.isDelivery, {symbol: market.symbol, orderIdList: `[${orderIds.join(',')}]`})
		if (!market.isDelivery) {
			for (const order of orders) {
				if (!order.algoId) continue
				const res = await this.req('DELETE', "v1/algoOrder", false, {algoid: order.algoId})
				if (Number(res?.code) === 200) result.push(order)
			}
 		}
		return result
	}

	async positionsClose(cmd, pos, i, ohlc = {}, market) {
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.symbol)) || (await this.symbolTicker(cmd.up, market.symbol, cmd.up || ["mark", "index"].includes(cmd.pr)))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.symbol} is not available!`)
		}
		ticker.pos = pos.entryPrice
		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 ? '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 leverage = cmd.l || pos.leverage || 1
		let order = {
			symbol: market.symbol,
			side: pos.positionAmt > 0 ? 'SELL' : 'BUY',
			type: cmd.t === "market" ? "MARKET" : "LIMIT",
			timeInForce: ({fok: "FOK", ioc: "IOC", post: "GTX"})[cmd.t] || "GTC",
			quantity: cmd.q._().mul(!cmd.q.wasPercent() ? (cmd.lc || leverage) / (cmd.u === "currency" ? price : 1) : 1)
				.reference(pos.quantity).mul(this.ohlc(cmd, ohlc, 'qr') || 1).add(this.addpos(cmd, ohlc, pos)).minmax(cmd.minq, cmd.maxq).resolve(market.quantityPrecision),
			positionSide: pos.positionSide,
			newOrderRespType: "RESULT"
		}
		if (cmd.t !== "market") order.price = price
		else delete order.timeInForce
		order.newClientOrderId = this.prefixID("x-AnnFpdrg")+this.uniqueIDA(26, cmd.id, i)
		if (order.positionSide === "BOTH") {
			if ((cmd.tp || cmd.sl) && cmd.t === "market" && cmd.q.wasPercent(true)) {
				delete order.quantity
				order.closePosition = true
			} else {
				order.reduceOnly = cmd.r === undefined ? true : cmd.r
			}
		}
		if (market.maxQty && order.quantity > market.maxQty) {
			this.warn(`order quantity above instrument maximum of ${market.maxQty} – use maxq= to set minimum!`)
		} else if (order.quantity <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated, use minq= to set minimum)")
		}

		let isAlgo = false
		if (cmd.ts) {
			const hasStop = cmd.so && !cmd.cbrate
			const param = hasStop && 'soref' || 'tsref'
			const stop = this.ohlc(cmd, ohlc, param, ticker, price) || first
			order.type = "TRAILING_STOP_MARKET"
			order.activationPrice = (hasStop ? cmd.so : cmd.ts)._().relative(stop).resolve(market.pricePrecision)
			order.callbackRate = hasStop ? cmd.ts._().reference(stop).div(stop / 10).minmax(0.1, 5).toDecimal() : minmax(cmd.cbrate || 0.1, 0.1, 5)
			delete order.timeInForce
			if (cmd.sr !== "last") order.workingType = cmd.sr.toUpperCase() + "_PRICE"
			isAlgo = !market.isDelivery
		} else if (cmd.tp) {
			order.type = cmd.t === "market" ? "TAKE_PROFIT_MARKET" : "TAKE_PROFIT"
			order.stopPrice = cmd.tp._().relative(this.ohlc(cmd, ohlc, 'tpref', ticker, price) || first).resolve(market.pricePrecision)
			if (cmd.sr !== "last") order.workingType = cmd.sr.toUpperCase() + "_PRICE"
			if (cmd.pp) order.priceProtect = "TRUE"
			isAlgo = !market.isDelivery
		} else if (cmd.sl || cmd.so) {
			order.type = cmd.t === "market" ? "STOP_MARKET" : "STOP"
			order.stopPrice = (cmd.sl || cmd.so)._().relative(this.ohlc(cmd, ohlc, cmd.sl && 'slref' || 'soref', ticker, price) || first).resolve(market.pricePrecision)
			if (cmd.sr !== "last") order.workingType = cmd.sr.toUpperCase() + "_PRICE"
			if (cmd.pp) order.priceProtect = "TRUE"
			isAlgo = !market.isDelivery
		}
		if (isAlgo) {
			order.algoType = "CONDITIONAL"
			order.clientAlgoId = order.newClientOrderId
			delete order.newClientOrderId
			if (order.stopPrice !== undefined) {
				order.triggerPrice = order.stopPrice
				delete order.stopPrice
			}
		}

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

		let result = null
		this.checkOrder(order, 107115019, 10)
		if (market.maxQty && order.quantity > market.maxQty) {
			let leftQty = order.quantity, i = 1
			do {
				if (i > 1 && opt.splitDelay) {
					this.info(`waiting for ${opt.splitDelay}sec (split order delay)...`)
					await sleep(opt.splitDelay)
				}
				order.quantity = Math.min(leftQty, market.maxQty)
				order.newClientOrderId = this.prefixID("x-AnnFpdrg")+this.uniqueIDA(26, cmd.id, i)
				this.info(`posting split order #${i++} of size ${order.quantity}...`)
				result = await this.req('POST', isAlgo ? "v1/algoOrder" : "v1/order", market.isDelivery, order)
				leftQty -= order.quantity
			} while(leftQty > 0)
		} else {
			result = await this.req('POST', isAlgo ? "v1/algoOrder" : "v1/order", market.isDelivery, order)
		}
		if (result && result.price === "0") delete result.price
		if (result && result.stopPrice === "0") delete result.stopPrice
		return result
	}

	async positionsCloseAll(cmd, ohlc, positions) {
		const symbols = await this.symbolInfo()
		if (!positions) {
			const sym = cmd.s !== '*' && cmd.s
			const market = sym && symbols[this.sym(sym)]
			if (sym && !market) {
				throw new Error(`Unknown market symbol: ${sym} – Use the 'Symbols' button in the exchange settings or alert editor to browse for valid symbols!`)
			}
			const isDelivery = market && market.isDelivery
			const futures = !sym || !isDelivery ? (await this.req('GET', "v3/positionRisk", false, sym && {symbol: market.symbol})) : []
			if (!futures || !futures.filter) {
				this.msgPosErr("Futures / USDⓈ-M")
				return false
			}
			const delivery = !sym || isDelivery ? (await this.req('GET', "v1/positionRisk", true, sym && {pair: market.pair})) : []
			if (!delivery || !delivery.filter) {
				this.msgPosErr("Delivery / COIN-M")
				return false
			}
			positions = [...futures, ...delivery].filter(pos => Number(pos.positionAmt) && (!sym || pos.symbol == market.symbol))
			if (!positions.length) {
				this.msgPosNone()
				return false
			}
		}

		const isMulti = (this.getBalanceRaw()[this.meta.multiAsset] || {}).isMulti
		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.positionAmt < 0) || (cmd.isAsk && pos.positionAmt > 0)) {
				return false
			}
			if (cmd.pm === "hedge" && pos.positionSide === "BOTH") {
				return !match
			}
			if (cmd.fp && cmd.fp.compare(pos.entryPrice)) {
				return !match
			}
			pos.leverage = Number(pos.leverage) || Math.abs(Math.round(pos.notional / pos.positionInitialMargin))
			if (cmd.l && pos.leverage !== cmd.l) {
				return !match
			}
			pos.marginType ||= isMulti || pos.isolatedMargin == 0 ? 'cross' : 'isolated'
			const market = symbols[this.sym(pos.symbol)] || {}
			cmd._sym = market.symbol
			pos.currency = cmd.currency = (market.isDelivery && market.marginAsset) || (isMulti && this.meta.multiAsset) || market.quoteAsset || this.meta.defaultAsset
			pos.side = pos.positionAmt < 0 ? 'short' : 'long'
			pos.quantity = Math.abs(pos.positionAmt)
			pos._sizeCoin = pos.quantity / pos.leverage / (market.isDelivery ? pos.entryPrice / market.contractSize : 1)
			pos._sizeUSD = pos._sizeCoin * pos.markPrice
			pos.pnl = pos.unRealizedProfit / (pos._sizeCoin * pos.leverage * (market.isDelivery ? 1 : pos.entryPrice))
			pos.markPrice = toPrecision(pos.markPrice, market.pricePrecision)
			pos.liquidationPrice = toPrecision(pos.liquidationPrice, market.pricePrecision)
			pos.unRealizedProfit = toPrecision(pos.unRealizedProfit, market.quantityPrecision)
			pos.avgEntryPrice = toPrecision(pos.entryPrice || 0, market.pricePrecision)
			if (!this.filterPos(cmd, ohlc, tickers, pos, balance => balance * pos.leverage * (market.isDelivery ? pos.entryPrice / market.contractSize : 1 / pos.entryPrice), market.quantityPrecision)) {
				return !match
			}
			return match
		})

		if (cmd.cm._().reference(positions.length).getMax() < positions.length) {
			const sort = {
				"newest": 	["updateTime", true],
				"oldest": 	["updateTime", false],
				"highest": 	["entryPrice", true],
				"lowest": 	["entryPrice", 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, symbols[this.sym(pos.symbol)])
			if (order) {
				closeOrders.push(order)
				Object.assign(closeOrders.last(), Object.without(pos, ["quantity", "side"]))
			}
		}
		return closeOrders
	}

/*
	async transfer(cmd, ohlc) {
	}
*/
}

class BinanceFutures extends BinanceFuturesTestnet {
	static meta = Object.assign({}, super.meta, {
		aliases: [
			"BINANCEFUTURES",
			"BINANCEFT",
		],
		endpoint: "https://fapi.binance.com/fapi/",
		endpoint2: "https://dapi.binance.com/dapi/",
		name: "Binance Futures",
		permissions: {
			origins: [
				"https://fapi.binance.com/fapi/*",
				"https://dapi.binance.com/dapi/*",
			],
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"binft1",
				"binft2",
			],
		},
		website: "https://www.binance.com/?ref=AOBJU8Z4",
	})
	constructor(meta = BinanceFutures.meta) {
		super(meta)
	}
}

Broker.add(new BinanceFutures())
Broker.add(new BinanceFuturesTestnet())
