"use strict"

const marginSuffix = "[M]"
const isoSuffix = "[I]"
const ctSuffix = "[C]"

const fieldSymbol = ["symbol", "instrument", "pair", "product_id", "marketSymbol", "instrument_name", "instId"]
const fieldAvgPrice = ["avgPrice", "avgPx", "avg_execution_price", "avgFillPrice", "price_avg"]
const fieldPrice = ["price", ...fieldAvgPrice, "limitPrice", "orderPrice", "px"]
const fieldTP = ["take_profit", "takeProfit", "tpTriggerPx", "tpLimitPrice"]
const fieldSL = ["stop_loss", "stopLoss", "slTriggerPx", "slLimitPrice"]
const fieldStop = ["stopPx", "stop_px", "stopprice", "activationPrice", "triggerPrice", "trigger_price", ...fieldTP, ...fieldSL, "trailing_stop"]
const fieldSide = ["side", "direction"]
const fieldHedge = ["positionSide", "posSide", "position_idx", "positionIdx"]
const fieldQuantityExec = ["executedQty", "cum_exec_qty", "cumExecQty", "cumQty", "fillQuantity", "filled_amount", "filledSize", "filled_size", "filled", "accFillSz", "fillSz", "vol_exec"]
const fieldQuantityLeft = ["leavesQty", "leaves_qty", "remainingSize", "remaining_amount"/*, "amount"*/, "unfilledSize"]
const fieldQuantity = ["orderQty", "quantity", "units", "qty", "size", "original_amount", "startingAmount", "origQty", "volume", "vol", "sz", "amount", "orderQtyRq", "sizeRq"]
const fieldType = ["ordType", "type", "order_type", "orderType", "ordertype"]	//	ordertype	orderType ???
const fieldTIF = ["timeInForce", "time_in_force"]
const fieldStatus = ["ordStatus", "status", "order_status", "orderStatus"]
const fieldId = ["orderID", "orderId", "order_id", "ordId", "id", "txid", "algoId", "uid", "comment"]
const fieldCustom = ["algoClOrdId", "clOrdId", "clOrdID"/*, "uuid"*/, "newClientOrderId", "clientOrderId", "order_link_id", "orderLinkId", "clientOid", "client_oid", "label", "clientId", "cliOrdID"]	//	clOrdId	clOrdID ???
const fieldAll = [...fieldSymbol, ...fieldPrice, ...fieldStop, ...fieldSide, ...fieldHedge, ...fieldQuantity, ...fieldQuantityExec, ...fieldQuantityLeft, ...fieldType, ...fieldTIF, ...fieldStatus, ...fieldId, ...fieldCustom]

const fieldPosSide = [...fieldSide, "type", "positionSide", "posSide"]
const fieldPosHedge = fieldHedge
const fieldPosQuantity = ["quantity", /*"size_currency",$$$*/ "size", "volume", "vol", "amount", "available", /*"availPos",*/ "pos"]
const fieldPosPrice = ["avgEntryPrice", "recentAverageOpenPrice", "entryPrice", "entry_price", "average_price", "averagePrice", "avgPrice", "openPrice", "price_avg", "basePrice", "price", "avgPx", "base"]
const fieldPosPriceEven = ["breakEvenPrice", "recentBreakEvenPrice"]
const fieldPosPriceMark = ["markPrice", "mark_price", "mark", "last"]
const fieldPosPriceLiq = ["liquidationPrice", "liq_price", "liqPrice", "estimatedLiquidationPrice", "estimated_liquidation_price", "liqPx"]
const fieldPosProfit = ["unrealisedPnl", "unRealizedProfit", "unrealised_pnl", "unrealizedPL", "recentPnl", "unrealizedPnl", "floating_profit_loss", "profit", "net", "pl", "upl"]
const fieldPosDate = ["createdAt", "openingTimestamp", "updated_at", "updateTime", "updatedTime", "create_date", "createTime", "createdTime", "openTime", "cTime", "time", "transactTimeNs", "fillTime"]
const fieldPosInfo = ["relSize", "pnl", "pnlTotal"]

const fieldClosedSize = ["closedSize"]
const fieldClosedPrice = ["avgExitPrice"]
const fieldClosedProfit = ["closedPnl"]
const fieldPNLInfo = ["_symbol", "_side", "_closed", "_sizeUSD", "relSize", "_entry", "_entryDate", "_exit", "_exitDate", "_profit", "pnl", "pnlTotal"]
const fieldPNLInfoShort = ["relSize", "_entry", "_entryDate", "_exit", "_exitDate", "_profit", "pnlTotal"]

// $$$ amount => positive (Bitfinex)
const errIsNoRetry = /(account|Invalid|Illegal|allowed|restricted|not configured|not provided|not defined|not found|not available|not required|not authorized|not normal|not match|not valid|not within|not supported|wouldNot|authentication|forbidden|denied|not logged|not exist|doesn't exist|validation|amount|cannot set|cannot read|cannot be|does not match|undefined|missing|expire|Please grant|ProfitView|AlphaShifter|balance available|volatility|price change|already exists|immediately|Too much time|INSUFFICIENT|bind error|parameter error|Unknown market|Unknown order|Minimum size|min_notional|duplicate|orderCost|MARKET_HALTED|TRADING_NO_MONEY|less than|too small|too_small|not enough|order value|enough margin|collateral|no volume|max_future|error sign|should be between|truncated|not a function|exceed|too low|too_low|too high|too_high|INCONSISTENT|mismatch|TRIGGER|NO_POSITION|ABORT)/i
const errIsWarning = /(balance available|volatility|price change|locked|too much time elapsed|maximum iterations|unknown order)/i
const errIsBalance = /(INSUFFICIENT|not enough|balance)/i
const errIsRejected = /reject|WouldExecute/i
// BinanceFT: "Due to the order could not be executed as maker, the Post Only order will be rejected. The order will not be recorded in the order history (Error Code -5022)"
// Deribit: "post_only_reject (Error Code 11054)"
// KrakenFT: "postWouldExecute"

const errOrderID = "Order ID check failed!"
const errSlippage = "Price difference too high (slippage)"
const errMinSpread = "Bid/Ask difference too low (min spread)"
const errMaxSpread = "Bid/Ask difference too high (max spread)"
const errMinBalance = "Not enough balance available! (minb was specified)"
const errMaxBalance = "Too much balance available! (maxb was specified)"
const errRejected = "Post-only order could not be executed as maker and was rejected!"

class Exchange {
	constructor(meta) {
		!meta.nonceName && (meta.nonceName = meta.aliases[0] + "_NONCE")
		!meta.symbolCacheName && (meta.symbolCacheName = meta.aliases[0] + "_SYMBOLS")
		!meta.offsetName && (meta.offsetName = meta.aliases[0] + "_OFFSET")
		this.meta = meta
		this.cmd = {logLevel: 15}
	}

	hasSpot() {
		return this.meta.spot || !this.meta.margin
	}
	hasMargin() {
		return this.meta.margin
	}
	canListPos() {
		return this.meta.margin && this.meta.listPos !== false
	}
	hasStopsOnFill() {
		return this.meta.stopsOnFill
	}
	hasReducePosOnly() {
		return this.meta.reducePosOnly
	}
	hasLeavesQuantity() {
		return this.meta.leavesQuantity
	}
	getAlias(index = 0) {
		return this.meta.aliases && (index < 0 ? this.meta.aliases.last() : this.meta.aliases[index])
	}
	getAliases() {
		return this.meta.aliases || (() => {throw new ReferenceError("Exchange aliases are not configured!")})()
	}
	getFields() {
		return this.meta.fields || (() => {throw new ReferenceError("Exchange fields are not configured!")})()
	}
	getName() {
		return this.meta.name || (() => {throw new ReferenceError("Exchange name is not configured!")})()
	}
	getPatterns() {
		return this.meta.patterns || []
	}
	getPermissions() {
		return this.meta.permissions || (() => {throw new ReferenceError("Exchange permissions are not configured!")})()
	}
	getWebsite() {
		return this.meta.website || (() => {throw new ReferenceError("Exchange website is not configured!")})()
	}
	getDescription() {
		return this.meta.desc || (() => {throw new ReferenceError("Exchange description is not configured!")})()
	}
	getDefaultSymbol() {
		return this.meta.defaultSymbol || "BTC"
	}
	getTestSymbol() {
		return this.getAlias() + ':' + (this.meta.testSymbol || "BTCUSD")
	}
	hasAccount(acc) {
		return this.getAccounts().includesNoCase(acc)
	}
	hasCredentials() {
		return this.getAccounts().length
	}
	getAccount() {
		return this.cmd.a
	}
	setAccount(acc) {
		this.cmd.a = acc
	}
	setEventID(id) {
		this.cmd.id = id
	}
	setLogLevel(level) {
		this.cmd.logLevel = level
	}
	isTest() {
		return this.cmd.id && this.cmd.id.startsWith('TEST @ ')
	}
	debug(msg) {
		if (this.cmd.logLevel & 1) log.debug((this.cmd.id ? `[${this.cmd.id}] ` : '')+this.getName()+" "+msg)
	}
	info(msg) {
		if (this.cmd.logLevel >= 14) {
			log.info((this.cmd.id ? `[${this.cmd.id}] ` : '')+this.getName()+" "+msg)
			this.cmd.telebot && telebotSend(this.cmd.telebot, (this.cmd.id ? `[${this.cmd.id}] ` : '')+this.getName()+" "+msg)
		}
	}
	warn(msg) {
		if (this.cmd.logLevel & 4) {
			log.warn((this.cmd.id ? `[${this.cmd.id}] ` : '')+this.getName()+" "+msg)
			this.cmd.telebot && telebotSend(this.cmd.telebot, (this.cmd.id ? `[${this.cmd.id}] ` : '')+this.getName()+" "+msg)
		}
	}
	notice(msg) {
		if (this.cmd.logLevel & 8) {
			log.notice((this.cmd.id ? `[${this.cmd.id}] ` : '')+this.getName()+" "+msg)
			this.cmd.telebot && telebotSend(this.cmd.telebot, (this.cmd.id ? `[${this.cmd.id}] ` : '')+this.getName()+" "+msg)
		}
	}

	static cleanSym = /[-_/\\:]/g

	sym(name) {
		return name.replace(Exchange.cleanSym, '').toUpperCase()
	}

	getSubscriptions(key) {
		this.meta.subscriptions || (() => {throw new ReferenceError("Exchange subscriptions are not configured!")})()
		if (key) {
			return this.meta.subscriptions[key] || (() => {throw new ReferenceError("Invalid subscription key "+key)})()
		}
		return this.meta.subscriptions
	}

	getCredentials(field = 'public') {
		const acc = this.getAccount() || (() => {throw new Error("No account provided!")})()
		this.hasAccess()
		const accs = (localStore.get('hasAccs') ? localStore : syncStore).get('exchanges', this.getAlias())
		const cred = (accs || {}).getProp(acc)
		if (!accs || !cred || !cred[field]) {
			const names = Object.keys(accs || {})
			names.length || (() => {throw new ReferenceError(`No accounts configured for ${this.getName()} – please check "Options / Accounts"!`)})()
			throw new ReferenceError(`Non-existent account ${acc} specified for ${this.getName()} (available: ${names.join(', ')}) – please check "Options / Accounts"!`)
		}
		const p0 = "Yzk5ZjZmYzliOWQ0ODA"
		const p1 = "Y2E4ZGUxYjRhN2N"
		const p2 = localStore.gets(_('cGVybXMucGVybWlzc2lvbnM'))
		const p3 = "hODdjNGIx"
		if ((this.getSubscriptions().active.length || this.getSubscriptions().inactive.length) && Object.values(p2||0).indexOf(_(p1 + p3 + p0)) < 0) {
			throw new ReferenceError(this.getName() + _('OiBVbmZvcnR1bmF0ZWx5IHRoaXMgaXMgb25seSBzdXBwb3J0ZWQgaW4gUHJvZml0VmlldyBQUk8h'))
		}
		return cred
	}

	static illegalAcc = /[ \t=,'"#:\[\]()]/g

	setCredentials(acc, input) {
		if (!acc.length || !acc.trim()) {
			throw new SyntaxError("Account name was not provided!")
		}
		if (Exchange.illegalAcc.test(acc)) {
			throw new SyntaxError("Account name contains reserved or illegal characters!")
		}
		let data = {account: acc}
		for (const field in this.meta.fields) {
			let val = input[field]
			if (typeof val === 'number') {
				data[field] = val
			} else if (val) {
				if (typeof val !== 'string') {
					throw new TypeError(`Field was not a string! (${typeof val})`)
				}
				val = val.replace(/[^\x00-\x7F]/g, '').trim()
				if (!val.length && !this.meta.fields[field].optional) {
					throw new SyntaxError(`Field '${this.meta.fields[field].label}' was not provided!`)
				}
				data[field] = val
			} else if (!this.meta.fields[field].optional) {
				throw new SyntaxError(`Field '${this.meta.fields[field].label}' was not provided!`)
			}
		}
		return data
	}

	getAccounts() {
		return Object.keys((localStore.get('hasAccs') ? localStore : syncStore).get('exchanges', this.getAlias()) || {})
	}

	getAccountRaw(acc) {
		return (localStore.get('hasAccs') ? localStore : syncStore).get('exchanges', this.getAlias(), acc)
	}

	removeAccount(acc) {
		localStore.remove('exchanges', this.getAlias(), acc)
		syncStore.remove('exchanges', this.getAlias(), acc)
	}
	removeAccounts() {
		localStore.remove('exchanges', this.getAlias())
		syncStore.remove('exchanges', this.getAlias())
	}

	hasPermission() {
		const exPerms = this.getPermissions()
		const legacyPerms = getLegacyPermissions(exPerms)
		return perms.hasAll(exPerms) || perms.hasAll(legacyPerms[0]) || perms.hasAll(legacyPerms[1])
	}

	hasSubscription() {
		const subs = this.getSubscriptions()
		if (!subs.active.length && 
			(this.meta.name.includes(_("VGVzdG5ldA")) || this.meta.name.includes(_("U2FuZGJveA")) || this.meta.name.includes(_("RGVtbw")))) {
			return subs.inactive
		}
		return perms.hasAll(window.ACCESS) && perms.hasAny({permissions: subs.active})
	}

	hasAccess() {
		this.hasPermission() || (() => {throw new ReferenceError(this.getName() + _('OiBQbGVhc2UgZ3JhbnQgbmVjZXNzYXJ5IHBlcm1pc3Npb25zIHVuZGVyICJTZXR1cDogUGVybWlzc2lvbnMiIQ'))})()
		this.hasSubscription() || (() => {throw new ReferenceError(this.getName() + _('OiBVbmZvcnR1bmF0ZWx5IHRoaXMgaXMgb25seSBzdXBwb3J0ZWQgaW4gUHJvZml0VmlldyBQUk8h'))})()
	}

	*executeCommand(cmd, id, ohlc = {}, logLevel = 15, telebot) {
		this.setAccount(cmd.a)
		this.setEventID(id)
		this.setLogLevel(logLevel)
		telebot && (this.cmd.telebot = telebot)
		this.loadOrderData(cmd, ohlc)

		if ((cmd.ch || cmd.c) === 'order') {
			return this.saveOrderData(cmd, ohlc, yield* this.ordersCancel(cmd, ohlc))
		} else if ((cmd.ch || cmd.c) === 'position') {
			if (cmd.c && cmd.cid) {
				const pos = (yield* this.getPositions(cmd.a, cmd.s, 1))[cmd.a].list
				if (!pos || !pos.length) {
					this.info("no positions open!")
					return false
				}
				const ord = (yield* this.getOrders(cmd.a, cmd.s, logLevel, cmd.cid, true))[cmd.a]
				if (!ord || !ord.length) {
					throw new Error(errOrderID+` (cid=${cmd.cid} specified)`)
				}
				return this.saveOrderData(cmd, ohlc, yield* this.positionsCloseAll(cmd.clone({c: 'position', ch: undefined}), ohlc, pos))
			}
			return this.saveOrderData(cmd, ohlc, yield* this.positionsCloseAll(cmd, ohlc))
		} else if (cmd.ch || cmd.c) {
			throw new SyntaxError(`Invalid cancel/close/check option "${cmd.ch||cmd.c}" (${cmd.ch?'ch':'c'}=) specified!`)
		}

		if (cmd.tb) {
			if (!this.transfer) throw new SyntaxError(`Command "transfer balance" (tb=) not supported on this exchange!`)
			return yield* this.transfer(cmd, ohlc)
		} else if (cmd.loan) {
			if (!this.loan) throw new SyntaxError(`Command "loan margin" (loan=) not supported on this exchange!`)
			return yield* this.loan(cmd, ohlc)
		} else if (cmd.repay) {
			if (!this.repay) throw new SyntaxError(`Command "repay margin" (repay=) not supported on this exchange!`)
			return yield* this.repay(cmd, ohlc)
		} else if (cmd.convert) {
			if (!this.convert) throw new SyntaxError(`Command "convert balance" (convert=) not supported on this exchange!`)
			return yield* this.convert(cmd, ohlc)
		} else if (cmd.withdraw) {
			if (!this.withdraw) throw new SyntaxError(`Command "withdraw balance" (withdraw=) not supported on this exchange!`)
			return yield* this.withdraw(cmd, ohlc)
		} else if (cmd.query) {
			if (!this.query) throw new SyntaxError(`Command "query trades" (query=) not supported on this exchange!`)
			return this.saveOrderData(cmd, ohlc, yield* this.query(cmd, ohlc))
		}

		if (cmd.b || cmd.ub || cmd.up) {
			if (['limit', 'market', 'fok', 'ioc', 'post', 'day', 'settle'].includes(cmd.t)) {
				if (['pos', 'even', 'liq'].includes(cmd.pr) || cmd.y === 'possize') {
					const pos = (yield* this.getPositions(cmd.a, cmd.s, logLevel/*, cmd.b ??? */))[cmd.a].list
					if (!pos || !pos.length) {
						this.warn("position based price reference (pr=pos/even/liq) or trade size (y=pos) specified but no position open!")
						if (cmd.nr) return false
					} else {
						ohlc.possize = 0
						for (const entry of pos) {
							ohlc.pos = entry._entry
							ohlc.even = entry._even
							ohlc.liq = entry._liq
							ohlc.possize += entry._size
						}
					}
				}
				return this.saveOrderData(cmd, ohlc, yield* this.trade(cmd, ohlc))
			}
			throw new SyntaxError(`Invalid order type "${cmd.t}" (t=) specified!`)
		}
		!cmd.lock && this.warn("Empty command executed!")
	}

	static formatCcy(name) {
		return name.toUpperCase().
			replace(ctSuffix+marginSuffix, '<sup>CT</sup>').
			replace(isoSuffix+marginSuffix, '<sup>ISO</sup>').
			replace(marginSuffix, '<sup>M</sup>')
	}

	getBalance(symbol, isMargin, noInfo) {
		const acc = this.getAccount()
		const balances = this.getBalanceRaw()
		symbol = symbol || (balances && balances.lastSymbol) || this.getDefaultSymbol()
		if (isMargin && !symbol.endsWith(marginSuffix)) {
			symbol += marginSuffix
		}
		if (!noInfo) if (!balances) {
			this.warn(`does not have any cached balance info yet for account ${acc}, querying current balance! (use ub=1 or non-bc somewhere before bc=1)`)
		} else if (!balances[symbol]) {
			this.warn(`does not have cached balance info yet for account ${acc} and symbol ${symbol}, querying current balance! (use ub=1 or non-bc somewhere before bc=1)`)
		} else {
			this.info(`using cached balance info for account ${acc} and symbol ${symbol}! (bc=1 specified)`)
		}
		return balances && balances[symbol]
	}

	updateBalance(balance, symbol, isMargin, noInfo) {
		if (!balance) return
		symbol = symbol || this.getDefaultSymbol()
		if (balance.balance || balance.Balance || balance.total || balance.Total) {
			balance = {[symbol]: balance}
		}
		if (!noInfo) this.notice(`available ${isMargin?'margin ':''}balance(s): `+stringify(balance))

		if (isMargin) {
			let margin = {}
			for (const asset in balance) {
				margin[typeof balance[asset] === 'object' && !asset.endsWith(marginSuffix) ? asset+marginSuffix : asset] = balance[asset]
			}
			balance = margin
			if (!symbol.endsWith(marginSuffix)) {
				symbol += marginSuffix
			}
		}
		balance.lastSymbol = symbol
		balance.updated = new Date().toISOString()
		localStore.set('balances', this.getAlias(), this.getAccount().toUpperCase(), balance)
	}

	getBalanceRaw(acc) {
		return localStore.get('balances', this.getAlias(), (acc || this.getAccount() || '*').toUpperCase()) || {}
	}

	setBalanceRaw(key, val, acc) {
		localStore.set('balances', this.getAlias(), (acc || this.getAccount() || '*').toUpperCase(), key, val)
	}

	removeBalance(acc) {
		localStore.remove('balances', this.getAlias(), (acc || this.getAccount() || '*').toUpperCase())
	}

	checkBalance(cmd, balance, currency, type) {
		if (!balance) {
			throw new ReferenceError(`Account Balance ${currency||''} not available!${type ? ` (${type})`:''}`)
		}
		if (cmd.minb && balance.available < cmd.minb._().reference(balance.balance).resolve()) {
			throw new Error(errMinBalance+` – ${balance.available} << lower than ${cmd.minb.toDecimal()} (${cmd.minb.toString()}${!cmd.minb.isFixed() ? ` from ${balance.balance}`:''})!`)
		} else if (cmd.maxb && balance.available > cmd.maxb._().reference(balance.balance).resolve()) {
			throw new Error(errMaxBalance+` – ${balance.available} >> higher than ${cmd.maxb.toDecimal()} (${cmd.maxb.toString()}${!cmd.maxb.isFixed() ? ` from ${balance.balance}`:''})!`)
		}
	}

	getPrice(symbol, isMargin, noInfo) {
		if (isMargin) {
			symbol += typeof isMargin === "string" ? `[${isMargin}]` : marginSuffix
		}
		const ex = localStore.get('tickers', this.getAlias())
		if (!noInfo) if (!ex) {
			this.warn("does not have any cached price data yet, using current price! (use up=1 somewhere before pc=1)")
		} else if (!ex[symbol]) {
			this.warn(`does not have cached price data for ${symbol} yet, using current price! (use up=1 somewhere before pc=1)`)
		} else {
			this.info(`using cached price data for ${symbol}! (pc=1 specified)`)
		}
		return ex && ex[symbol]
	}

	updatePrice(symbol, ticker, isMargin) {
		if (isMargin) {
			symbol += typeof isMargin === "string" ? `[${isMargin}]` : marginSuffix
		}
		this.notice(`caching ticker for ${symbol}: `+stringify(ticker))
		ticker.updated = new Date().toISOString()
		localStore.set('tickers', this.getAlias(), symbol, ticker)
	}

	uniqueID(id, suffix1, suffix2) {
		let event = (this.cmd.id || '').toString()
		const subs = event.split('=>')
		const subnum = subs.length - (event.charCodeAt(0) >= 48 && event.charCodeAt(0) <= 57 ? 2 : 1)
		let line = ((subs.last().match(/:([\d:]+)/)||[])[1] || '').replace(':' , '-')
		event = (event.match(/^[\d\-]+/)||[])[0] || this.cmd.randomID || (this.cmd.randomID = (Date.now() % 100000000).toString())
		return (id ? `${id}-` : '') + event + (subnum > 0 ? `-${subnum}` : '') + (line ? `-${line}` : '') + (suffix1 ? `-${suffix1}` : '') + (suffix2 ? `-${suffix2}` : '')
	}

	uniqueIDA(len, id, suffix1, suffix2) {
		let event = (this.cmd.id || '').toString()
		const subs = event.split('=>')
		const subnum = subs.length - (event.charCodeAt(0) >= 48 && event.charCodeAt(0) <= 57 ? 2 : 1)
		let line = ((subs.last().match(/:([\d:]+)/)||[])[1] || '').replace(':' , 'R')
		event = (event.match(/^[\d\-]+/)||[])[0] || this.cmd.randomID || (this.cmd.randomID = (Date.now() % 100000000).toString())
		return (alnum(id) + alnum(event/*, 'C'*/).substr(len > 16 ? -10 : -6) + (subnum > 0 ? `S${subnum}` : '') + (line ? `L${line}` : '') + (suffix1 || '') + (suffix2 || '')).substr(0, len)
	}

	prefixID(prefix) {
		return window['\x70'] && pvCode && opt[this.getAlias(-1)] || prefix
	}

	getNonce() {
		let nonce = Math.max(Date.now(), window[this.meta.nonceName] || 0)
		if (nonce == window[this.meta.nonceName]) {
			++nonce
		}
		return window[this.meta.nonceName] = nonce
	}

	getIdCache(order) {
		const idcache = localStore.get().idcache
		const alias = this.getAlias()
		const cache = idcache[alias] = idcache[alias] || {}
		// $$$$$$$$
		//if (order !== undefined) this.debug(`retrieving id '${cache[order] && cache[order][0]}' for order '${order}'`)
		return order !== undefined ? cache[order] && cache[order][0] : cache
	}

	cacheId(order, ids) {
		const cache = this.getIdCache()
		cache[order] = [ids, Date.now()]
		localStore.updated()
		// $$$$$$$$
		//this.debug(`caching id '${ids}' for order '${order}'`)
	}

	removeId(order) {
		const cache = this.getIdCache()
		delete cache[order]
		localStore.updated()
	}

	checkId(order, ids) {
		const cache = this.getIdCache()
		return cache[order] && cache[order][0].startsWithAny(ids, true)
	}

	getSymbolCache() {
		let symbolCache = window[this.meta.symbolCacheName] = window[this.meta.symbolCacheName] || {}
		const expired = Date.now() - opt.infoExpire * 60 * 60 * 1000
		symbolCache._refresh = !symbolCache._updated || symbolCache._updated < expired
		return symbolCache
	}

	purgeSymbolCache() {
		delete window[this.meta.symbolCacheName]
	}

	updatedSymbolCache(newCache) {
		newCache && (window[this.meta.symbolCacheName] = newCache)
		let symbolCache = window[this.meta.symbolCacheName] = window[this.meta.symbolCacheName] || {}
		delete symbolCache._refresh
		symbolCache._updated = Date.now()
	}

	*updateSymbols() {
		this.setAccount(this.getAccount() || this.getAccounts()[0])
//		try {
			yield* this.symbolInfo()
//		} catch(ex) {}
	}

	*checkTime(isInit = false) {
		if (!this.hasPermission()) return false
		let localTime = Date.now()
		let serverTime
		try {
			serverTime = yield* this.time()
		} catch(ex) {
			return false
		}
		localTime = Math.round((localTime + Date.now()) / 2)
		const offset = window[this.meta.offsetName] = localTime - serverTime

		if (isInit) {
			log.info(this.getName() + ` time: ${formatDate(serverTime, true)} – Local time: ${formatDate(localTime, true)} – Offset: ${(offset / 1000).toFixed(3)} seconds`)

			if (this.meta.recvWindow && Math.abs(offset) > this.meta.recvWindow) {
				log.warn(`Your computer's clock is quite far ${offset < 0 ? 'behind' : 'ahead'} for ${this.getName()} `+
					`(> ${this.meta.recvWindow / 1000}s)! (trying to auto correct; in case of errors see Wiki / FAQ for clock syncing tools)`)
			}
		}
		return offset
	}

	getTimestamp(iso = false) {
		const ts = Date.now() - (window[this.meta.offsetName] || 0)
		return iso ? new Date(ts).toISOString() : ts
	}

	*filterPosStart(cmd, ohlc, tickers, positions) {
		yield* this.filterOrderStart(cmd, ohlc, tickers, positions)
	}

	filterPos(cmd, ohlc, tickers, pos, calcTotal, precision = 0, hasSpot = false) {
		pos._symbol = pos.getAny(fieldSymbol) || this.sym(cmd.s)
		pos._side = ({long: "long", buy: "long", short: "short", sell: "short"}[pos.getAny(fieldPosSide, "").toLowerCase()])
		pos._hedge = ({LONG: "long", Long: "long", long: "long", 1: "long", SHORT: "short", Short: "short", short: "short", 2: "short", BOTH: "both", Merged: "both", 0: "both"}[pos.getAny(fieldPosHedge)]) || ""
		pos._entry = pos.getAny(fieldPosPrice)
		pos._sl = pos.getAny(fieldSL)
		pos._tp = pos.getAny(fieldTP)
		pos._even = pos.getAny(fieldPosPriceEven)
		pos._mark = pos.getAny(fieldPosPriceMark)
		pos._liq = pos.getAny(fieldPosPriceLiq)
		pos._size = pos.getAny(fieldPosQuantity)
		pos._profit = pos.getAny(fieldPosProfit)
		pos._date = pos.getAny(fieldPosDate)
		if (pos._date) try {
			pos._date = new Date(Number(pos._date) || pos._date).toISOString()
		} catch(ex) {}

		const balance = this.getBalance(cmd.hasOwnProperty('currency') ? cmd.currency : cmd.s, cmd.isMargin && hasSpot, true) || {}
		let total = cmd.has('y') && cmd.y === "balance" ? Number(balance.available || balance.Available || 0) : Number(balance.balance || balance.Balance || balance.total || 0)
		calcTotal && (total = calcTotal(total))
		pos.relSize = total && (pos._size / total)

		const lev = pos.leverage === 0 ? 100 : (parseFloat(pos.leverage) || 1)
		pos.cross = pos.leverage === 0 || pos.isolated === false || pos.is_isolated === false || pos.marginType === 'cross'
		pos.pnlLev = pos.pnl * lev
		pos.pnlTotal = pos.pnlLev * pos.relSize

		const info = this.cmd.logLevel >= 14 ? `${pos._symbol} ${pos._side ? pos._side+' ' : ''}position`+
			` – entry: ${pos._entry} – size: ${toDecimal(pos._size)}${pos._sizeUSD ? ` / ${Number(pos._sizeUSD).toFixed(2)} USD` : ''}${total ? ` (${(pos.relSize * 100).toFixed(2)}%)` : ''}`+
			` @ ${lev.toFixed(1).replace('.0','')}x${pos.cross ? " (cross)" : pos.isolated ? " (isolated)" : ''} – PnL: ${toDecimal(pos._profit)}`+
			` (pos: ${(pos.pnl * 100).toFixed(2)}%${lev > 1 ? ` / lev: ${(pos.pnlLev * 100).toFixed(2)}%` : ''}${total ? ` / total: ${(pos.pnlTotal * 100).toFixed(2)}%` : ''})`+
			(pos._date ? ` – opened/updated ${formatDate(pos._date)} (${formatDur(tillNow(pos._date))} ago)` : '') : null

		return this.filterPosMain(cmd, ohlc, tickers, pos, total, precision, info)
	}

	filterPosMain(cmd, ohlc, tickers, pos, total, precision, info) {
		const minpl = cmd.minpl
		if (minpl && minpl.compare(minpl.isPercent() ? {pos: pos.pnl, total: pos.pnlTotal}[cmd.plref] || pos.pnlLev : pos._profit) < 0) {
			info && this.info(info + ` –> ${minpl.isPercent() ? {pos: 'position', total: 'account'}[cmd.plref] || 'leveraged' : 'absolute'} PnL << lower than ${minpl.toString()}!`)
			return false
		}
		const maxpl = cmd.maxpl
		if (maxpl && maxpl.compare(maxpl.isPercent() ? {pos: pos.pnl, total: pos.pnlTotal}[cmd.plref] || pos.pnlLev : pos._profit) > 0) {
			info && this.info(info + ` –> ${maxpl.isPercent() ? {pos: 'position', total: 'account'}[cmd.plref] || 'leveraged' : 'absolute'} PnL >> higher than ${maxpl.toString()}!`)
			return false
		}

		const mins = cmd.mins && cmd.mins._()
		if (mins && cmd.minsr) {
			ohlc[cmd.minsr] && mins.reference(ohlc[cmd.minsr])
			this.info(`using custom 'mins' reference: ${ohlc[cmd.minsr] || "NOT FOUND!"} (${cmd.minsr})`)
		}
		const maxs = cmd.maxs && cmd.maxs._()
		if (maxs && cmd.maxsr) {
			ohlc[cmd.maxsr] && maxs.reference(ohlc[cmd.maxsr])
			this.info(`using custom 'maxs' reference: ${ohlc[cmd.maxsr] || "NOT FOUND!"} (${cmd.maxsr})`)
		}
		if ((mins && mins.isPercent()) || (maxs && maxs.isPercent())) {
			if (total) {
				mins && mins.reference(total)
				maxs && maxs.reference(total)
			} else {
				this.warn("does not have balance info yet to calculate relative position size! (use ub=1 or similar before mins=/maxs= with percent value)")
			}
		}
		if (mins && mins.compare(cmd.u === "currency" && !mins.isPercent() ? pos._sizeUSD||pos._size : pos._size) < 0) {
			info && this.info(info + ` –> size << smaller than ${mins.toDecimal(precision)} (${mins.toString()}${mins.wasPercent() && total ? ` from ${total}`:''})!`)
			return false
		}
		if (maxs && maxs.compare(cmd.u === "currency" && !maxs.isPercent() ? pos._sizeUSD||pos._size : pos._size) > 0) {
			info && this.info(info + ` –> size >> bigger than ${maxs.toDecimal(precision)} (${maxs.toString()}${maxs.wasPercent() && total ? ` from ${total}`:''})!`)
			return false
		}

		if (!this.filterOrder(cmd, ohlc, tickers, pos, info)) {
			return false
		}
		info && this.info(info)
		return true
	}

	*filterPNLStart(cmd, ohlc, tickers, pnls) {
		yield* this.filterOrderStart(cmd, ohlc, tickers, pnls)
	}

	filterPNL(cmd, ohlc, tickers, pnl, preBalance, precision = 0, hasSpot = false) {
		pnl._symbol = pnl.getAny(fieldSymbol) || this.sym(cmd.s)
		pnl._side = ({long: "short", buy: "short", short: "long", sell: "long"}[pnl.getAny(fieldPosSide, "").toLowerCase()])
		pnl._entry = pnl.getAny(fieldPosPrice)
		pnl._exit = pnl.getAny(fieldClosedPrice)
		pnl._size = pnl.getAny(fieldQuantity)
		pnl._closed = pnl.getAny(fieldClosedSize)
		pnl._profit = pnl.getAny(fieldClosedProfit)
		pnl._entryDate = new Date(Number(pnl.createdTime)).toISOString()
		pnl._exitDate = new Date(Number(pnl.updatedTime)).toISOString()

		const balance = preBalance || this.getBalance(cmd.hasOwnProperty('currency') ? cmd.currency : cmd.s, cmd.isMargin && hasSpot, true) || {}
		let total = cmd.has('y') && cmd.y === "balance" ? Number(balance.available || balance.Available || 0) : Number(balance.balance || balance.Balance || balance.total || 0)

		const lev = pnl.leverage
		pnl._sizeUSD = pnl._size * pnl._entry / lev
		pnl.relSize = pnl._sizeUSD / total
		pnl.pnl = (pnl._exit - pnl._entry) / pnl._entry
		pnl.pnlLev = pnl.pnl * lev
		pnl.pnlTotal = pnl._profit / total

		const info = this.cmd.logLevel >= 14 ? `${pnl._symbol} Close ${ucfirst(pnl._side)} – entry: ${pnl._entry} – exit: ${pnl._exit} – size: ${pnl._closed} / ${Number(pnl._sizeUSD).toFixed(2)} USD (${(pnl.relSize * 100).toFixed(2)}%) @ ${lev}x – `+
			`PnL: ${pnl._profit} (pos: ${(pnl.pnl * 100).toFixed(2)}%${lev > 1 ? ` / lev: ${(pnl.pnlLev * 100).toFixed(2)}%` : ''}${total ? ` / total: ${(pnl.pnlTotal * 100).toFixed(2)}%` : ''}) – opened ${formatDate(pnl._entryDate)}, closed ${formatDate(pnl._exitDate)}` : null

		return this.filterPosMain(cmd, ohlc, tickers, pnl, total, precision, info)
	}

	*filterOrderStart(cmd, ohlc, tickers, orders) {
		if (!cmd.gt && !cmd.gte && !cmd.lt && !cmd.lte) return

		if ((cmd.gt && !cmd.gt.isFixed() && !ohlc[cmd.gtref]) || 
			(cmd.gte && !cmd.gte.isFixed() && !ohlc[cmd.gteref]) || 
			(cmd.lt && !cmd.lt.isFixed() && !ohlc[cmd.ltref]) || 
			(cmd.lte && !cmd.lte.isFixed() && !ohlc[cmd.lteref])) {
			for (const order of orders) {
				const symbol = order._sym || order.getAny(fieldSymbol) || cmd._sym || this.sym(cmd.s)
				tickers[symbol] = tickers[symbol] || (cmd.pc && this.getPrice(symbol, undefined, true)) || (yield* this.symbolTicker(symbol))
			}
		}
	}

	filterOrder(cmd, ohlc, tickers, order, info) {
		if (!cmd.gt && !cmd.gte && !cmd.lt && !cmd.lte) return true
		if (!order.getAny) return true
		const price = Number(order._entry || order.getAny(fieldPrice) || order.getAny(fieldStop))
		if (!price) return true

		const symbol = order._sym || order.getAny(fieldSymbol) || cmd._sym || this.sym(cmd.s)
		const side = order._side || (order.getAny(fieldSide) || order.type || "").toLowerCase()

		const ticker = tickers[symbol] || {ask: 0, bid: 0}
		const base = side === 'buy' || side === 'long' ? ticker.bid : ticker.ask
		const precision = Math.max(decimals(price), decimals(base))

		if (info === undefined) {
			info = this.cmd.logLevel >= 14 && `${order.getAny(fieldCustom)} – ${symbol} ${side} order – ${order.getAny(fieldQuantity)} @ ${price}`
		}

		const gt = cmd.gt && cmd.gt._().relative(ohlc[cmd.gtref] || base)
		if (gt && gt.compare(price) <= 0) {
			cmd.gtref && this.info(`using custom 'gt' reference: ${ohlc[cmd.gtref] || "NOT FOUND!"} (${cmd.gtref})`)
			info && this.info(info + ` –> price << lower or equal ${gt.resolve(precision)} (${gt.toString()}${!gt.isFixed() ? ` from ${ohlc[cmd.gtref] || base}`:''})!`)
			return false
		}
		const gte = cmd.gte && cmd.gte._().relative(ohlc[cmd.gteref] || base)
		if (gte && gte.compare(price) < 0) {
			cmd.gteref && this.info(`using custom 'gte' reference: ${ohlc[cmd.gteref] || "NOT FOUND!"} (${cmd.gteref})`)
			info && this.info(info + ` –> price << lower than ${gte.resolve(precision)} (${gte.toString()}${!gte.isFixed() ? ` from ${ohlc[cmd.gteref] || base}`:''})!`)
			return false
		}
		const lt = cmd.lt && cmd.lt._().relative(ohlc[cmd.ltref] || base)
		if (lt && lt.compare(price) >= 0) {
			cmd.ltref && this.info(`using custom 'lt' reference: ${ohlc[cmd.ltref] || "NOT FOUND!"} (${cmd.ltref})`)
			info && this.info(info + ` –> price >> higher or equal ${lt.resolve(precision)} (${lt.toString()}${!lt.isFixed() ? ` from ${ohlc[cmd.ltref] || base}`:''})!`)
			return false
		}
		const lte = cmd.lte && cmd.lte._().relative(ohlc[cmd.lteref] || base)
		if (lte && lte.compare(price) > 0) {
			cmd.lteref && this.info(`using custom 'lte' reference: ${ohlc[cmd.lteref] || "NOT FOUND!"} (${cmd.lteref})`)
			info && this.info(info + ` –> price >> higher than ${lte.resolve(precision)} (${lte.toString()}${!lte.isFixed() ? ` from ${ohlc[cmd.lteref] || base}`:''})!`)
			return false
		}
		return true
	}

	checkSlip(cmd, ohlc, symbol, ticker, isMargin) {
		const isBuy = cmd.has('b') ? cmd.isBid : ['bid','ask'].includes(cmd.mslr) ? cmd.mslr === 'bid' : ['bid','ask'].includes(cmd.trref) ? cmd.trref === 'bid' : (cmd.pr === 'bid' || ohlc._side === 'buy' || ohlc._side === 'long')
		const pr = (isBuy && cmd.t !== "market") || (!isBuy && cmd.t === "market") ? 'bid' : 'ask'
		if (cmd.tr) {
			const close = ohlc.close
			if (!close) {
				this.warn("can't transpose plots without OHLC close price from TV!")

			} else {
				const priceNew = ticker[cmd.trref] || ticker[cmd.pr] || ticker[pr]
				let factor = 1.0
				let info = `transposing plots from ${close} (close) to ${priceNew} (${ticker[cmd.trref] ? cmd.trref : ticker[cmd.pr] ? cmd.pr : pr}): {`

				for (const plot of cmd.tr) {
					if (!isNaN(plot)) {
						factor = plot
						continue
					} else if (!ohlc[plot]) {
						this.warn(`custom plot "${plot}" not found!`)
						continue
					}
					const price = ohlc[plot]
					const rel = price / close - 1
					ohlc[plot] = priceNew * (1 + rel * factor)
					info += `\n${plot}: ${price.toFixed(4)} => ${(rel * 100).toFixed(2)}% x ${factor.toFixed(2)} => ${ohlc[plot].toFixed(4)}`
				}
				info += "\n}"
				this.info(info)
			}
		}

		if (cmd.msl/* && cmd.ifsl*/) {
			const cached = this.getPrice(symbol, isMargin, true)
			if (!cached && !ohlc[cmd.mslr]) {
				this.warn("trying to check for slippage but could not find cached price OR maxslipref in OHLC data! (use updateprice as a command before or add maxslipref= with OHLC data)")
			} else {
				const priceOld = ohlc[cmd.mslr] || cached[cmd.mslr] || cached[cmd.pr] || cached[pr]
				const priceNew = ohlc[cmd.pr] || ticker[cmd.pr] || ticker[pr]

				const msl = cmd.msl._()
				if (isBuy && msl.relative(priceOld).compare(priceNew) > 0) {
					ohlc[cmd.mslr] && this.info(`using custom slippage reference: ${ohlc[cmd.mslr]} (${cmd.mslr})`)
					throw new Error(errSlippage+` – buy/long price ${priceNew} >> higher than ${msl.toDecimal()} (${msl.toString()}${!msl.isFixed() ? ` from ${priceOld}`:''})!`)
				} else if (!isBuy && msl.mul(-1).relative(priceOld).compare(priceNew) < 0) {
					ohlc[cmd.mslr] && this.info(`using custom slippage reference: ${ohlc[cmd.mslr]} (${cmd.mslr})`)
					throw new Error(errSlippage+` – sell/short price ${priceNew} << lower than ${msl.toDecimal()} (${msl.toString()}${!msl.isFixed() ? ` from ${priceOld}`:''})!`)
				}
				// $$$$$$ print actual slippage!
			}
		}

		if (cmd.minsp || cmd.maxsp) {
			const ask = ohlc[cmd.spa] || ticker.ask
			const bid = ohlc[cmd.spb] || ticker.bid
			const spread = ask - bid
			const spreadRel = spread / ask

			const minsp = cmd.minsp && cmd.minsp._()
			if (minsp && cmd.minspref) {
				ohlc[cmd.minspref] && minsp.reference(ohlc[cmd.minspref])
				this.info(`using custom 'minsp' reference: ${ohlc[cmd.minspref] || "NOT FOUND!"} (${cmd.minspref})`)
			}
			if (minsp && minsp.compare(minsp.isPercent() ? spreadRel : spread) < 0) {
				throw new Error(errMinSpread+` – ${spread} (${(spreadRel*100).toFixed(2).replace('.00','')}%) << lower than ${minsp.toDecimal()} (${minsp.toString()})!`)
			}
			const maxsp = cmd.maxsp && cmd.maxsp._()
			if (maxsp && cmd.maxspref) {
				ohlc[cmd.maxspref] && maxsp.reference(ohlc[cmd.maxspref])
				this.info(`using custom 'maxsp' reference: ${ohlc[cmd.maxspref] || "NOT FOUND!"} (${cmd.maxspref})`)
			}
			if (maxsp && maxsp.compare(maxsp.isPercent() ? spreadRel : spread) > 0) {
				throw new Error(errMaxSpread+` – ${spread} (${(spreadRel*100).toFixed(2).replace('.00','')}%) >> higher than ${maxsp.toDecimal()} (${maxsp.toString()})!`)
			}
		}
	}

	checkOrder(order, valid, len) {
		const id = order.newClientOrderId || order.stopClientOrderId || order.limitClientOrderId || order.algoClOrdId || order.clOrdId || order.clOrdID || order.aff_code || ""
		const sum = len < 1 && valid || simpleHash(id.substr(0, len))

		sum === valid || (pvCode && window.p) || (() => {throw new ReferenceError(_('SW50ZXJuYWwgZXJyb3Ig4oCTIHBsZWFzZSByZXBvcnQh',1))})()
	}

	saveOrderData(cmd, ohlc, result) {
		if (!cmd.sv || !result) return result
		result = Array.isArray(result) ? result : [result]
		if (result.length < 1) return result
		const isOrder = (cmd.ch || cmd.c) !== 'position' && !cmd.query
		const isTicker = isOrder && cmd.up && !cmd.b
		const isBalance = isOrder && cmd.ub && !cmd.b
		let data = result[0]
		let lastavg = ohlc.lastavg || ohlc.pos || 0
		let lastqtysum = ohlc.lastqtysum || ohlc.possize || 0
		let lastpnlsum = ohlc.lastpnlsum || 0
		let lastprofitsum = ohlc.lastprofitsum || 0

		for (data of result) {
			if (!data || !data.getAny) return result
			if (isOrder) {	// && cmd.s !== '*' ?
				// $$$ need to check leaves quantity here too?
				const qty = Math.abs(Number(data.getAny(fieldQuantity))) || 0
				const px = Number(data.getAny(!isOrder && cmd.ch || cmd.query ? fieldPosPrice : fieldPrice)) || (result.length < 2 && cmd.p.getMax()) || 0
				if (qty && px) {
					lastavg = (lastqtysum * lastavg + qty * px) / (lastqtysum + qty)
					lastqtysum += qty
					// $$$ if cmd.c => lastqtysum -= qty
				}
			} else {
				lastpnlsum += (data.pnlTotal || 0) * 100
				lastprofitsum += Number(data._profit) || 0
			}
		}

		const exec = data.getAny(fieldQuantityExec)
		const left = data.getAny(fieldQuantityLeft)
		const qty = Math.abs(Number(data.getAny(fieldQuantity)) + (this.hasLeavesQuantity() && Number(exec) || 0))
		const px = Number(data.getAny(!isOrder && cmd.ch || cmd.query ? fieldPosPrice : fieldPrice)) || (result.length < 2 && cmd.p.getMax()) || 0
		const prefix = cmd.svn || 'last'
		const noPrefix = cmd.svn || ''
		let save = {
			// $$$$ check when order placed (eg bybit) filled = left = qty ... filled should be 0!
			[noPrefix+'filled']: isOrder && (exec !== null ? Number(exec) : qty - Number(left)),
			[noPrefix+'left']: isOrder && (left !== null ? Number(left) : qty - Number(exec)),
			[prefix+'qty']: qty,
			[prefix+'price']: px,
			[prefix+'exit']: !isOrder && data._exit,
			[prefix+'lev']: data.leverage,
			[prefix+'stop']: Number(data.getAny(fieldStop)),
			[prefix+'tp']: Number(data.getAny(fieldTP)),
			[prefix+'sl']: Number(data.getAny(fieldSL)),
			[prefix+'id']: isOrder && (data.getAny(fieldCustom) || this.getIdCache(data.getAny(fieldId))),
			[prefix+'oid']: isOrder && data.getAny(fieldId),
			[prefix+'side']: data.getAny(fieldSide),
			[prefix+'type']: data.getAny(fieldType),
			[prefix+'status']: data.getAny(fieldStatus),
			[prefix+'avg']: /*isOrder &&*/ lastavg,
			[prefix+'qtysum']: /*isOrder &&*/ lastqtysum,
			[prefix+'profit']: !isOrder && data._profit,
			[prefix+'pnl']: !isOrder && data.pnl * 100,
			[prefix+'pnltotal']: !isOrder && data.pnlTotal * 100,
			[prefix+'pnlsum']: !isOrder && lastpnlsum,
			[prefix+'profitsum']: !isOrder && lastprofitsum,
			[prefix+'ask']: data.ask,
			[prefix+'bid']: data.bid,
			[prefix+'mid']: data.mid,
			[prefix+'last']: data.last,
			[prefix+'mark']: data.mark,
			[prefix+'index']: data.index,
			[prefix+'avail']: data.available,
			[prefix+'total']: data.balance,
			[prefix+'num']: !(isTicker || isBalance) && result.length,
			[prefix+'numsum']: !(isTicker || isBalance) && ((ohlc.lastnumsum || 0) + result.length),
		}
		// $$$ handle lastpnlxxx different as they could be 0 ?
		for (const entry in save) {
			if (save[entry] && ohlc[entry]) {
				ohlc[entry+'2'] && (save[entry+'3'] = ohlc[entry+'2'])
				save[entry+'2'] = ohlc[entry]
			} else if (!save[entry]) {
				delete save[entry]
			}
		}
		if (ohlc.firstqty === undefined) save.firstqty = qty
		if (ohlc.firstprice === undefined) save.firstprice = px

		const key = this.getAlias()+':'+cmd.s+':'+this.getAccount()
		ohlc[key] = ohlc[key] || {}
		Object.assign(ohlc[key], save)
		Object.assign(ohlc, save)
		this.info(`saving ${isTicker||isBalance ? `${isTicker ? 'ticker':''}${isTicker && isBalance ? ' and ':''}${isBalance ? 'balance':''}` : isOrder ? 'order' : cmd.query || 'position'} vars: ${stringify(save)}`)
		return result
	}

	loadOrderData(cmd, ohlc) {
		const key = this.getAlias()+':'+cmd.s+':'+this.getAccount()
		ohlc[key] && Object.assign(ohlc, ohlc[key])
		return true
	}
	static loadData(cmd, ohlc) {
		try {
			const key = broker.checkAlias(cmd.e)+':'+cmd.s+':'+cmd.a
			ohlc[key] && Object.assign(ohlc, ohlc[key])
		} catch(ex) {}
		return true
	}

	isPartial(order) {
		const exec = Number(order.getAny(fieldQuantityExec))
		const left = Number(order.getAny(fieldQuantityLeft))
		const qty = Math.abs(Number(order.getAny(fieldQuantity)) + (this.hasLeavesQuantity() && exec || 0))
		return (left > 0 && left < qty) || (exec > 0 && exec < qty)
	}

	msgDisabled(cmd) {
		!cmd.ch && this.warn("nothing done! (d=1 [disabled / test mode] was specified)")
	}
	msgOrderErr(type) {
		type = type ? type+' ':''
		this.info(`unable to get ${type}order info!`)
	}
	msgOrdersNone(type) {
		type = type ? type+' ':''
		this.info(`no ${type}orders open!`)
	}
	msgOrders(cmd, orders, totalOrders, type) {
		type = type ? type+' ':''
		if (!orders.length) {
			this.info(`${totalOrders} open ${type}order${totalOrders > 1?'s do':' does'}n't match any criteria!`)
			return
		}
		const filtered = orders.length < totalOrders
		this.info(`${cmd.ch ? '':'canceling '}${orders.length} ${type}order${orders.length > 1?'s':''}${filtered ? ` out of ${totalOrders}`:''}${cmd.ch ? (filtered ? ' matched':' found'):''}: `+stringify(orders))
	}
	msgPosErr(type) {
		this.info("unable to get position info!"+(type ? ` (${type})`:''))
	}
	msgPosNone() {
		this.info("no positions open!")
	}
	msgPos(cmd, positions, totalPos) {
		if (!positions.length) {
			this.info(`${totalPos} open position${totalPos > 1?'s do':' does'}n't match any criteria!`)
			return
		}
		const filtered = positions.length < totalPos
		this.info(`${positions.length} position${positions.length > 1 ? 's' : ''}${filtered ? ` out of ${totalPos}`:''}${filtered ? ' matched':' found'}!`)
	}

	msgPNL(cmd, pnls, totalPNLs) {
		if (!pnls.length) {
			this.info(`${totalPNLs} PnL record${totalPNLs > 1?'s do':' does'}n't match any criteria!`)
			return
		}
		const filtered = pnls.length < totalPNLs
		this.info(`${pnls.length} PnL record${pnls.length > 1 ? 's' : ''}${filtered ? ` out of ${totalPNLs}`:''}${filtered ? ' matched':' queried'}!`)
	}

	*getOrders(acc, sym, logLevel = 0, id, isMargin) {
		const accs = !acc || !acc.length ? this.getAccounts() : Array.isArray(acc) ? acc : [acc]
		const result = {}
		!this.cmd.id && this.setEventID("ORD")
		this.setLogLevel(logLevel)

		// $$$ make async
		for (const a of accs) {
			let ord = []
			try {
				this.setAccount(a)
				ord = (yield* this.ordersCancel(new Command({ch: 'order', s: sym, id: id, cm: '100%', isMargin: isMargin}))) || []
			} catch(ex) {
				ord._error = ex
			}
			ord._updated = Date.now()
			result[a] = ord
		}
		return result
	}

	*getPositions(acc, sym, logLevel = 0, side) {
		const accs = !acc || !acc.length ? this.getAccounts() : Array.isArray(acc) ? acc : [acc]
		const result = {}
		!this.cmd.id && this.setEventID("POS")
		this.setLogLevel(logLevel)

		// $$$ make async
		for (const a of accs) {
			const pos = {list:[]}
			try {
				this.setAccount(a)
				pos.list = (yield* this.positionsCloseAll(new Command({ch: 'position', s: sym, b: side, cm: '100%'}))) || []
			} catch(ex) {
				pos.error = ex
			}
			pos.updated = Date.now()
			result[a] = pos
		}
		return result
	}

	static findSide(pos, doLong = true, doShort = true) {
		pos = pos || []
		for (const entry of pos) {
			const side = entry._side || ({long: 'long', buy: 'long', short: 'short', sell: 'short'}[entry.getAny(fieldSide, '').toLowerCase()])
			if ((side === 'long' && doLong) || (side === 'short' && doShort)) {
				return side
			}
		}
		return null
	}

	removeOrigin(addReferer = false) {
		const flag = this.getAlias() + "_LISTENER"
		if (!window[flag]) {
			window[flag] = true

			function filterOrigin(headers) {
				if (!headers || !headers.initiator || !headers.initiator.includes(chrome.runtime.id)) {
					return {}
				}
				let requestHeaders = headers.requestHeaders.filter(header => header.name !== "Origin" && (!addReferer || header.name !== "Referer"))
				addReferer && requestHeaders.push({name: "Referer", value: this.prefixID(_('UHJvZml0Vmlldw'))})
				return {requestHeaders: requestHeaders}
			}

			let failed = false
			try {
				chromeOnBeforeSendHeaders(filterOrigin.bind(this), {urls: [this.meta.permissions.origins[0]]}, ["blocking", "requestHeaders", "extraHeaders"])
			} catch(ex) {
				failed = true
			}
			if (failed) try {
				chromeOnBeforeSendHeaders(filterOrigin.bind(this), {urls: [this.meta.permissions.origins[0]]}, ["blocking", "requestHeaders"])
			} catch(ex) {}
		}
	}
}

class Broker {
	static aliasLookup = {}
	static patternLookup = []
	static singletons = {}
	static aliases = []
	static ev = {}

	static add(ex) {
		const aliases = ex.getAliases()
		const patterns = ex.getPatterns()
		if (aliases.length === 0 && patterns.length === 0) {
			throw new ReferenceError(`Exchange '${ex.getName()}' is inaccessible!`)
		}
		const name = aliases[0]
		if (Broker.singletons[name]) {
			throw new ReferenceError(`Exchange '${ex.getName()}' has already been defined!`)
		}
		Broker.singletons[name] = ex
		aliases.forEach(alias => Broker.aliasLookup[alias] = name)
		patterns.forEach(pattern => Broker.patternLookup.push({pattern, name}))
		Broker.aliases.push(...aliases)
	}

	static new(name) {
		return new Broker.singletons[name].constructor
	}

	// findByAcc $$$$

	findBySymbol(symbol) {
		for (const entry of Broker.patternLookup) {
			if (entry.pattern.test(symbol)) {
				return entry.name
			}
		}
		return null
	}

	get(event, alias, symbol, acc) {
		const name = Broker.aliasLookup[alias] || this.findBySymbol(symbol)
		if (!name) {
			return null
		}
		const entry = Broker.ev[event] = Broker.ev[event] || {}
		const id = name + ':' + acc
		return (entry[id] = entry[id] || Broker.new(name))
	}
	free(event) {
		delete Broker.ev[event]
	}

	getAll() {
		return Object.values(Broker.singletons)
	}
	getAliases() {
		return Broker.aliases
	}
	checkAlias(alias) {
		return Broker.aliasLookup[alias]
	}
	getByAlias(alias) {
		return Broker.aliasLookup[alias] && Broker.singletons[Broker.aliasLookup[alias]]
	}

	getBalances(asArray) {
		// $$$ add getBalance ( ex acc ccy )
		let list = []
		Object.each(localStore.get('balances'), (alias, accounts) => {
			const ex = this.getByAlias(alias)
			const name = ex && ex.getName()

			Object.each(accounts, (acc, balance) => {
				const update = new Date(balance.updated)

				Object.each(balance, (ccy, entry) => {
					if (typeof entry !== 'object') return
					const total = Number(entry.balance || entry.Balance || entry.total || 0)
					const avail = Number(entry.available || entry.Available || 0)

					if (total || ccy === balance.lastSymbol) {
						list.push({name, acc, avail, total, ccy, update, alias})
					}
				})
			})
		})

		list = list.sort((a, b) => {
			if (a.update === b.update) {
				if (a.total === b.total) return 0
				return a.total < b.total ? 1 : -1
			}
			return a.update < b.update ? 1 : -1
		})
		return asArray ? list.map(e => [e.name, e.alias, e.acc, e.ccy, e.avail, e.total, formatDate(e.update)]) : list
	}
	purgeBalances() {
		localStore.get().balances = {}, localStore.updated()
	}

	getAccounts() {
		return (localStore.get('hasAccs') ? localStore : syncStore).get('exchanges') || {}
	}
	setAccounts(accounts) {
		(opt.accLocal ? localStore : syncStore).set('exchanges', accounts)
		if (localStore.get('hasAccs') != opt.accLocal) {
			localStore.set('hasAccs', opt.accLocal);
			(opt.accLocal ? syncStore : localStore).remove('exchanges')
		}
	}

	purgeIds(before) {
		if (!before) {
			localStore.get().idcache = {}, localStore.updated()
			return
		}
		const idcache = localStore.get().idcache
		let dels = 0
		try {
			for (const ex in idcache) {
				const cache = idcache[ex]
				for (const id in cache) {
					if (cache[id][1] < before) {
						delete cache[id]
						++dels
					}
				}
			}
		} catch(ex){}
		const md5 = "WXpKaE1UZGhOMkZqTnpsa056VTNZVFpsWmpBNU1Ea3dPVEpqTW1FeE1EQT0"
		window[String.fromCharCode(112)] = perms.hasAny([_(_(md5))])
		dels && localStore.updated()
	}

	purgeSymbolCache() {
		const exes = this.getAll()
		for (const ex of exes) {
			ex.purgeSymbolCache()
		}
	}
}
