"use strict"

const timeStart = Date.now()

log.upgrade()
opt.clearLog && log.getStore().clear()
log.setLevel(opt.debugLog ? log.level.DEBUG : log.level.INFO)

const pvName = chrome.runtime.getManifest().name
const pvVersion = chrome.runtime.getManifest().version
const pvBuilt = chrome.runtime.getManifest().version_name.substr(-11, 10)
const pvShortU = pvName.replace(/[^A-Z]/g, '')
const pvShortL = pvShortU.toLowerCase()
const pvCode = pvShortU.charCodeAt(0)-0x41
const ls = localStorage
const bg = window

bg.GUID = 0
bg.UUID = 0
bg.UUIDD = 0
bg.broker = null

bg.tvTabs = 0
bg.tvLastPing = 0
bg.tvLastPing2 = 0
bg.tvLastReload = 0
bg.tvLastEvent = 0
bg.tvEvents = {}
bg.windowCount = 1
bg.autoLastEvent = 0
bg.lastTimeSync = 0

bg.statRunning = 0
bg.pauseAll = false
bg.exitAll = false
bg.inScheduler = false
bg.restartDelayed = false
bg.positions = {}
bg.accountNum = 0
bg.exNum = 0

bg.TRADINGVIEW_STREAM_CLOSED = 0
bg.TRADINGVIEW_STREAM_CONNECTING = 1
bg.TRADINGVIEW_STREAM_OPEN = 2
bg.TRADINGVIEW_STREAM_RECEIVING = 3

bg.tvStreamTime = 0
bg.tvStreamSource = null
bg.tvStreamStatus = TRADINGVIEW_STREAM_CLOSED
bg.tvStreamStalled = 0
bg.tvStreamToken = 0
bg.ID = 0
bg.state = 1

bg.telebotId = 0
bg.telebotLast = 0
bg.telebotLastSender = 0
bg.telebotErr = 0
bg.telebotErrLast = 0

const s2ms = 1000
const m2ms = 60 * s2ms
const h2ms = 60 * m2ms
const d2ms = 24 * h2ms

const heartBeatInterval = 3 * s2ms
const storageSaveInterval = 5 * s2ms
const schedulerInterval = 1 * m2ms
const accountInterval = 5 * m2ms
const pingTimeout = 30 * s2ms
const telebotTimeout = 3 * m2ms
const reloadSafetyTimeout = 7 * s2ms
const streamRetries = 9
const streamRetriesToken = 5
const streamRetryTimeout = 5 * s2ms
const streamConnectWindow = 3 * s2ms
const addonIgnoreWindow = 3 * s2ms
const pollSleep = 1 //s
const forkSleep = 10 //ms
const childSleep = 20 / 1000

const newChecks = [errOrderID, errSlippage, errMinSpread, errMaxSpread, errIsRejected]
const newCheckParams = ['wid', 'ifsl', 'ifminsp', 'ifmaxsp', 'ifr']
const errSlippageCheck = 2

const defaultParams = ['e', 's', 'd', 'l', 'lr', 'p', 'pr', 'q', 'qr', 'retries', 'retryall', 'timeout', 'minb', 'maxb', 'minp', 'maxp', 'minq', 'maxq', /*'time',*/ 'bc', 'pc', 'jd', 'err', 'sv', 'y', 'pm', 'since', 'iso', 'mt', 'ismargin']
const cmdParamsInt = ['n', 'enable', 'disable', 'toggle', 'interval', 'setvar', 'unset', 'skip']
const cmdParamsEx = ['b', 'c', 'ch', 'ub', 'up', 'tb', 'loan', 'repay', 'convert', 'query']
const cmdParams = [...cmdParamsEx, 'lock', 'lockcheck', 'time', 'max', 'minhvd', 'maxhvd', 'minhvw', 'maxhvw', 'minhvm', 'maxhvm', 'min1h', 'max1h', 'min24h', 'max24h', 'min7d', 'max7d', 'trigger', 'triggeren', 'run', 'runen', 'exitall', 'pauseall', 'resumeall', 'restart']
const sourceMap = [
	[/BINANCE:(.*)(PERP|\.P)$/, "BINANCEFT:$1"],
	[/BINANCE:(.*\d\d\d\d)$/, "BINANCEFT:$1"],
	[/BITMEX:(.*)(PERP|\.P)$/, "BITMEX:$1"],
	[/BITFINEX:(.*)(UST|USDT)\.P$/, "BITFINEX:$1F0$2F0"],
	[/BYBIT:(.*)(PERP|\.P)$/, "BYBIT:$1"],
	[/BYBIT:(.*)\d\d(\d\d)$/, "BYBIT:$1$2"],
	[/BYBIT:(.*)/, "BYBIT:$1S"],
	[/DERIBIT:(.*)USD\.P$/, "$1-PERPETUAL"],
	[/DERIBIT:(.*)USD(.)\.P$/, "$1_USD$2-PERPETUAL"],
	[/DERIBIT:(.*)(USDT|BTC)$/, "$1_$2"],
	[/KRAKEN:BTCUSD(|.)$/, "KRAKEN:XBTUSD$1"],
	[/KRAKEN:BTCUSD\.PM$/, "KRAKENFT:PF_XBTUSD"],
	[/KRAKEN:BTCUSD(PERP|\.P)$/, "KRAKENFT:PI_XBTUSD"],
	[/KRAKEN:(.*)\.PM$/, "KRAKENFT:PF_$1"],
	[/KRAKEN:(.*)(PERP|\.P)$/, "KRAKENFT:PI_$1"],
	[/KRAKEN:BTCUSD(.*\d\d\d\d)$/, "KRAKENFT:XBTUSD$1"],
	[/KRAKEN:(.*\d\d\d\d)$/, "KRAKENFT:$1"],
	[/OKX:(.*)\.P$/, "OKX:$1PERP"],
	[/PHEMEX:BTCUSD\.P$/, "PHEMEX:UBTCUSD"],
	[/PHEMEX:ETHUSD\.PI$/, "PHEMEX:CETHUSD"],
	[/PHEMEX:(.*)(PERP|\.P|\.PI)$/, "PHEMEX:$1"],
	[/PHEMEX:(.*)/, "PHEMEX:S$1"],
]

chrome.runtime.onInstalled.addListener(run.bind(window, onInstall))
chrome.runtime.onMessage.addListener(run.bind(window, executeMessage))

function updateWindowCount() {
	chrome.windows.getAll({}, win => bg.windowCount = win.length)
}
updateWindowCount()
chrome.windows.onCreated.addListener(updateWindowCount)
chrome.windows.onRemoved.addListener(() => --bg.windowCount)

let isReady = false
let inShutdown = false
function onShutdown() {
	inShutdown = true
	log.notice(`*** ${pvName} ${pvVersion}${opt.instName?` (${opt.instName})`:''} shutting down ***`)
}
window.onbeforeunload = onShutdown

run(init)

function* onInstall(event) {
	void chrome.runtime.lastError
	const manifest = chrome.runtime.getManifest()
	log.notice("Runtime Event => "+event.reason)
	if (event.reason === 'install') {
		log.notice(`Installed ${manifest.name} ${manifest.version}`)
		chrome.tabs.create({url: chrome.runtime.getURL("options.html")})
		return yield* reloadTradingViewCheck(true)
	}

	if (['chrome_update', 'update'].includes(event.reason)) {
		log.notice(`Updated to ${manifest.name} ${manifest.version}`)
		ls.statUpdates = Number(ls.statUpdates || 0) + 1
		ls.statPrevUpdate = ls.statLastUpdate || 0
		ls.statLastUpdate = Date.now()
		ls.statPrevVersion = ls.statLastVersion || ""
		ls.statLastVersion = manifest.version
		opt.reloadTV && (yield* reloadTradingViewCheck(true))
	}
}

function* getChange(symbol = "BTC") {
	bg.CMC_CACHE = bg.CMC_CACHE || {}
	const expired = Date.now() - opt.cmcExpire * 60000
	symbol = symbol.toUpperCase()

	if (!bg.CMC_CACHE[symbol] || bg.CMC_CACHE[symbol].updated < expired) {
		if (!opt.cmcAPI) throw new ReferenceError("CoinMarketCap API key missing – please go to 'General Options' and enter it!")
		let ticker = null
		const headers = {
			"X-CMC_PRO_API_KEY": opt.cmcAPI,
			"Accept": "application/json"
		}
		try {
			ticker = yield request.bind(this, 'GET', "https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest", {convert: 'USD', symbol: symbol}, headers)
			ticker = ticker && ticker.data && ticker.data[symbol] && ticker.data[symbol].quote && ticker.data[symbol].quote.USD
		} catch (e) {
			throw new Error(`Couldn't retrieve CoinMarketCap ticker – ${(e.status && e.status.error_message) || e.message || e}!`)
		}
		if (!ticker) {
			throw new Error("Couldn't retrieve CoinMarketCap ticker!")
		}

		log.info(`Current CoinMarketCap ticker for ${symbol}: `+stringify(ticker))
		ticker.updated = Date.now()
		bg.CMC_CACHE[symbol] = ticker
	}
	return bg.CMC_CACHE[symbol]
}

function* getVolatility(days) {
	const expired = Date.now() - (days > 7 ? 24*60 : 5) * 60000
	const cache = "BVOL"+days

	if (!window[cache] || window[cache].updated < expired) {
		let data = null
		try {
			data = yield request.bind(this, 'GET', "https://www.bitmex.com/api/v1/trade", {
				symbol: ".BVOL" + (days <= 1 ? "24H" : days <= 7 ? "7D":''),
				columns: "price",
				count: 1,
				reverse: true
			}, null)
			data = data && data[0] && data[0].price
		} catch(e) {}

		if (!data) {
			throw new Error(`Couldn't retrieve volatility index information – make sure you grant ${pvShortU} permission to access the BitMEX API!`)
		}
		log.info(`Current ${days > 7 ? 'monthly' : days > 1 ? 'weekly' : 'daily'} volatility index value: `+data)
		window[cache] = {
			value: data,
			updated: Date.now()
		}
	}
	return window[cache].value
}

function lockSymbol(alert, symbol, res) {
	const lastAlert = localStore.get('lastAlert')
	lastAlert.lock = lastAlert.lock || {}
	lastAlert.lock[symbol] = {id: alert.id, time: new Date().toISOString(), res: res || opt.defaultRes}

	if (alert) {
		alert.locks = (alert.locks || 0) + 1
	}
	localStore.updated()
	log.warn(`Manually locked symbol ${symbol} (${formatTF(res)})!`)
}

function unlockSymbol(alert) {
	let keys = []
	Object.each(localStore.get('lastAlert', 'lock'), (key, entry) => {
		if (!alert || entry.id == alert.id) {
			localStore.remove('lastAlert', 'lock', key)
			keys.push(key+` (${formatTF(entry.res)})`)
		}
	})

	if (alert) {
		delete alert.locks
	} else {
		getAlerts().forEach(alert => {delete alert.locks})
	}
	log.warn(`Manually removed symbol locks: ${keys.join(', ')}`)
}

function parseOHLC(vals, ohlc) {
	vals = vals && (vals[0] === '#' || vals[0] === '(') && /[\w]/.test(vals[1]) && vals.slice(1, vals[0] === '(' ? vals.indexOf(')') : undefined).split(/[,;](?=(?:(?:[^"]*"){2})*[^"]*$)/).numbers(true)
	if (!vals) return null

	const addedVals = ohlc !== undefined && {}
	ohlc = ohlc || {}

	function add(name, val) {
		if (Array.isArray(name)) {
			const tmp = name.shift(); val = name.length > 1 ? name : name[0]; name = tmp
		}
		const force = name[0] === '!'
		name = name.substr(force ? 1 : 0).toLowerCase()
		if (name[0] !== '_' && !Command.prefs[name] && !['e', 'ex', 's', 'sym', 'a', 'acc', 'ap', 'par'].includes(name)) {
			name = '_'+name
		}
		if (!addedVals) {
			ohlc[name] = val
		} else if (force || ohlc[name] === undefined) {
			addedVals[name] = ohlc[name] = val
		}
		return val
	}

	if (vals.length >= 4 && isNumbers(vals, 5)) {
		add('open', vals.shift())
		add('high', vals.shift())
		add('low', vals.shift())
		add('close', vals.shift())
		add('vol', vals.shift())
		add('oc', (ohlc.open + ohlc.close) / 2)
		add('hl', (ohlc.high + ohlc.low) / 2)
		add('ohlc', (ohlc.open + ohlc.high + ohlc.low + ohlc.close) / 4)
	} else if (addedVals && !(vals[0].length > 1)) {
		return null
	}

	let i = 1
	for (let val of vals) {
		val.length > 1 && (val = add(val))
		!addedVals && (ohlc['custom'+i++] = val)
	}
	if (ohlc._side !== undefined && !['buy', 'sell', 'long', 'short'].includes(ohlc._side)) {
		ohlc._side = ohlc._side === '1:0' || ohlc._side > 0 ? 'long' : ohlc._side === '0:1' || ohlc._side < 0 ? 'short' : 'auto'
	}
	return addedVals || ohlc
}

function* runCommandsCheck(id, ev, disabled, alertId = 0, startPos = 1, overrides = {}, isChild = 0) {
	try {
		yield* runCommands(id, ev, disabled, alertId, startPos, overrides, isChild)
	} catch(e) {
		log.error(`[${id}] Unhandled exception: ${e.message} (please report!) <pre>${e.stack}</pre>`)
	}
}

function* runCommands(id, ev, disabled, alertId = 0, startPos = 1, overrides = {}, isChild = 0, parallel) {
	if (inShutdown) return
	if (isChild) id = `${id}-${isChild+1}`
	let pre = `[${id}] `
	if (!ev.isManual && !ev.isFork && opt.maxParallel && statRunning >= opt.maxParallel) {
		log.warn(pre+`Alert skipped because of too many alerts already running! (${statRunning})`)
		incStat('MaxPar'); return
	}
	const logLevel = ev.log !== undefined ? ev.log : getAlerts(alertId, {log: Command.log.all}).log
	const logSyntax = !(logLevel & 16)
	let cmdLog, logAll, logCond, logWarn, logErr
	function setLog(level) {
		cmdLog = Math.abs(level)
		logAll = cmdLog >= 14
		logCond = cmdLog & 8
		logWarn = cmdLog & 4
		logErr = cmdLog & 2
	}
	setLog(logLevel)

	let def = {}
	const isManual = ev.isManual && !ev.isInternal
	if ((opt.defAccountUse || isManual) && opt.defAccount) def.a = opt.defAccount
	if ((opt.defExchangeUse || isManual) && opt.defExchange) def.e = opt.defExchange
	if ((opt.defSymbolUse || isManual) && opt.defSymbol) def.s = opt.defSymbol

	if (ev.side === 'auto' && !isChild) {
		const exSym = (ev.sym || ':').split(':')
		const lines = ev.desc.replace(/[#;].*/g, '').split('\n')
		let ex = (findSyntax("e|ex|exchange", lines) || def.e || exSym[0] || opt.defExchange || '').split(',', 1)[0].toUpperCase()
		let sym = (findSyntax("s|sym|symbol", lines) || def.s || exSym[1] || opt.defSymbol || '').split(',', 1)[0].toUpperCase()
		let acc = (findSyntax("a|acc|account|ap|par|parallel", lines) || def.a || overrides.a || opt.defAccount || '*').split(',', 1)[0]
		if (acc.includes(':')) {
			acc = acc.split(':')
			ex = acc[0] || ex; sym = acc[1] || sym; acc = acc[2] || '*'
		}
		logAll && log.info(pre+`Trying to auto detect position side for ${ex}:${sym} and account '${acc}'...`)

		const exchange = broker.get(id, ex, sym, acc)
		const pos = exchange && (yield* exchange.getPositions(acc, sym, 1))[acc].list
		const side = Exchange.findSide(pos)

		if (!pos) {
			logWarn && log.warn(pre+"Encountered an error when trying to detect position side!")
		} else if (pos._error) {
			logWarn && log.warn(pre+"Error when trying to detect position side: "+pos._error)
		} else if (!pos.length) {
			logAll && log.info(pre+"No open position found – using only name for unified placeholders!")
		}
		if (side) {
			logAll && log.info(pre+`Open position found – auto detected side for unified placeholders as ${side.toUpperCase()}!`)
			ev.side = side
		} else {
			delete ev.side
		}
	}
	let syntax = {}
	let ohlc = ev.ohlc = ev.ohlc || {}
	if (ev.side) ohlc._side = ev.side
	const added = isChild < 1 && parseOHLC(ev.desc.split('\n', 1)[0], ohlc)
	ev.caller = ev.caller || alertId
	ev.alert = alertId

	try {
		// $$$$$ todo: TEST [placeholders] per child AND MAKE SURE NO [if acc=xxx] BEFORE AP= LINE !!!
		// fix/improve => "ap=ex1:sym1:acc1,ex2,sym2,acc2" use first ex/sym for "parent" placeholders!
		syntax = new Alert(ev, def, alertId /*&& (isChild < 1 || opt.childReplace)*/, false, overrides, id, isChild)
	} catch(e) {
		syntax.error = e.message
	}
	if (isChild < 1) {
		if (ev.isManual) {
			logWarn && logSyntax && log.warn(`Manually ${disabled ? 'testing':'running'} ${id}! => Commands to run${alertId?` (#${alertId})`:''}${ev.side?' – Side '+ev.side.toUpperCase():''}: {\n${lineNumbers(ev.desc)}\n}`)
			if (!disabled) {
				let alert = getAlerts(alertId, {})
				alert.manualNum = (alert.manualNum || 0) + 1
			}
		} else {
			logAll && logSyntax && log.notice(pre+`Commands to run${alertId?` (#${alertId})`:''}${ev.side?' – Side '+ev.side.toUpperCase():''}: {\n${lineNumbers(ev.desc)}\n}`)
		}
		!Object.isEmpty(added) && logAll && log.notice(pre+"Default OHLC data found:"+stringify(added, true))
	}

	if (syntax.error) {
		logErr && log.error(pre+"Syntax error: "+syntax.error)
		ev.telebot && telebotSend(ev.telebot, pre+"Syntax error: "+syntax.error)
		broker.free(id)
		return 0
	}
	const cmds = syntax.commands
	cmds.pos = startPos
	if (isChild < 1) {
		if (!ev.isManual && (!ev.isAuto || opt.badgeAuto) && !ev.isInternal && cmds.length > 0) {
			ls.badgeRun = Number(ls.badgeRun || 0) + 1
		}
		if (def.a || def.e || def.s) {
			logAll && log.notice(pre+(ev.isManual ? "Using configured defaults" : "Overriding alert defaults with")+": "+stringify(def))
		}
		if (ev.telebot && cmds.warnings && cmds.warnings.length) {
			cmds.warnings.forEach(warn => telebotSend(ev.telebot, pre+warn.text+` (Line ${warn.row+1})`))
		}
	}

	let isParent = false
	const res = ev.res || opt.defaultRes
	const startTime = Date.now()
	const fireTime = ev.fire_time ? ev.fire_time.getTime() : startTime
	const barTime = ev.bar_time ? ev.bar_time.getTime() : startTime
	const cbarTime = new Date(Math.floor(startTime / 1000 / 60 / res) * res * 60 * 1000).getTime()
	let cmdNum = 0

	if (!ev.isFork) {
		// $$$$ needs fix: if code below fails without decrementing statRunning it gets stuck!
		++statRunning
		ls.statTotalPar = Math.max(ls.statTotalPar || 0, statRunning)
		ls.statSessionPar = Math.max(ls.statSessionPar || 0, statRunning)
	}
	let jumpStack = []

	function handleParallel(accs, off = 0) {
		isParent = true
		overrides.a = accs.shift()
		let child = 0
		for (const acc of accs) {
			setTimeout(run.bind(this, runCommands, id, Object.assignDeep(ev), disabled, alertId, cmds.pos+off, Object.assignDeep(overrides, {a: acc}), ++child), forkSleep)
		}
		child && (id = `${id}-1`)
	}
	parallel && handleParallel(parallel)

	for (; cmds.pos <= cmds.length; ++cmds.pos) {
		const cmd = cmds[cmds.pos-1]
		if (!cmd._a || (cmd._a.isStart && cmd._a.isStart())) {
			cmd.merge(overrides)
			if (!cmd._res) {
				cmd._res = res, cmd._firetime = fireTime, cmd._bartime = barTime, cmd._cbartime = cbarTime
			}
			cmd._count = (cmd._count || 0) + 1
			cmd._time = Date.now()
			delete cmd._errCount
		}
		pre = `[${id}:${cmd._line}${cmd._count > 1 ? ':'+cmd._count:''}${cmd.a !== '*' ? ' @ '+cmd.a:''}] `
		setLog(cmd.log < logLevel ? cmd.log : logLevel)

		for (let pass=0; pass < 2; ++pass) {
			if (bg.pauseAll && !bg.exitAll) {
				log.notice(pre+" Alert manually paused!")
				while (bg.pauseAll && !bg.exitAll) {
					yield* sleep(pollSleep)
				}
				!bg.exitAll && log.notice(pre+" Alert manually unpaused (resuming)...")
			}
			if (bg.exitAll) {
/*				// $$$$$ add exitAllStart timestamp - if(older than 3*pollSleep) {
					bg.statRunning = 0, bg.exitAll = false
					log.notice("All alerts terminated!")
				} else
*/
				log.warn(pre+" Alert manually terminated!")
				break
			}

			if (!pass) {
				const delay = cmd.retry ? Number(cmd.timeout) : 
							cmd.delay ? Number(cmd.delay._().reference(res * 60).resolve()) : 0
				if (delay > 0) {
					logAll && log.info(pre+formatDur(delay, -2).replace('.00', '')+" delay")
					let till = Date.now() / 1000 + delay, slice = Math.min(delay, pollSleep), paused = false
					do {
						while((yield* sleep(slice)) == null);
						if (bg.pauseAll && !bg.exitAll) {
							if (!paused) {
								paused = true
								log.notice(pre+" Alert manually paused! (while in delay command)")
							}
							till += slice = pollSleep
						} else {
							if (paused) {
								paused = false
								!bg.exitAll && log.notice(pre+` Alert manually unpaused (resuming)... ${formatDur(till - Date.now() / 1000, -2).replace('.00', '')} delay left!`)
							}
							slice = Math.min(till - Date.now() / 1000, pollSleep)
						}
					} while (slice > 0 && !bg.exitAll)
				}
			}
		}
		if (bg.exitAll) break

		let isConditional = false
		let successful = false
		let exchange = null

		function* handleJump(type, doJump = true) {
			const nc = !isConditional
			const level = (nc && ({err: 'warn', skip: 'info'})[type]) || 'notice'
			const doLog = ({notice: logCond, warn: logWarn})[level] || logAll
			const jpre = pre + (nc && ({err: "Error handling", skip: "Parameter"})[type] || `Conditional (${type}${type === 'err' || type === 'skip' ? " & ret":''})`)
			let val = cmd[type]

			if (val < Command.err.abort && exchange) {
				doLog && log[level](jpre+` set to cancel orders${val >= Command.err.closemarket ? ' and close positions':''} (${type}=${val})...`)
				try {
					yield* exchange.executeCommand(new Command({
						a: cmd.a, e: cmd.e, s: cmd.s, d: cmd.d, cm: '100%', id: val < Command.err.closemarket && cmd[type === 'err' ? 'eid' : 'cid'] || undefined, 
						c: 'order', b: val == Command.err.closeside || val == Command.err.cancelside ? cmd.b : undefined
					}), pre.slice(1, -2), ohlc, cmdLog)

					if (val >= Command.err.closemarket) {
						yield* exchange.executeCommand(new Command({
							a: cmd.a, e: cmd.e, s: cmd.s, d: cmd.d, cm: '100%', id: val > Command.err.closemarket && cmd[type === 'err' ? 'eid' : 'cid'] || undefined, 
							c: 'position', b: val == Command.err.closeside ? cmd.b : undefined, 
							t: cmd.ep && val != Command.err.closemarket ? 'limit':'market', p: cmd.ep || '0', q: '100%'
						}), pre.slice(1, -2), ohlc, cmdLog)
					}
				} catch(e) {
					logErr && log.error(pre+`Error while trying to close/cancel everything: ${getError(e, false, false)}`)
				}
			}
			if (!doJump) return false

			if (val != Command.err.cancel) {
				if (val <= Command.err.abort) {
					doLog && log[level](jpre+` set to abort alert (${type} <= -1)...`)
					cmds.pos = cmds.length
				} else if (type != 'err' || val != Command.errIgnore) {
					let jumpBack = false, msg
					const pos = cmds.labels && cmds.labels[val]
					if (pos) {
						if (!jumpStack.length || jumpStack.last()[1] !== val) {
							jumpStack.push([cmds.pos, val, Date.now(), 0])
						} else if (jumpStack.length) {
							jumpStack.last()[3]++
						}
						if (cmd.max && jumpStack.last()[3]+1 >= cmd.max) {
							doLog && log[level](jpre+` set to ${val == Command.doRepeat ? 'repeat' : val == Command.doLoop ? 'loop' : 'jump'} but maximum iterations of ${cmd.max} reached!`)
							return false
						}
						jumpBack = pos <= cmds.pos
						cmds.pos = pos-1
						// $$$$ jumpStack.last()[3]+1 ??? check!
						msg = `jump to label '${val}' ↻${jumpStack.last()[3]} (command #${pos} @ line ${(cmds[cmds.pos] && cmds[cmds.pos]._line) || 'EOF'})`
					} else if (val == Command.doRepeat) {
						if (cmd.max && cmd._count >= cmd.max) {
							doLog && log[level](jpre+` set to ${val == Command.doRepeat ? 'repeat' : val == Command.doLoop ? 'loop' : 'jump'} but maximum iterations of ${cmd.max} reached!`)
							return false
						}
						--cmds.pos
						jumpBack = true
						msg = `repeat this command (${type}=${val})`
					} else if (val == Command.doLoop || val == Command.goBack) {
						let closest = null;
						if (!jumpStack.length) {
							if (val == Command.doLoop) {
								closest = Object.entries(cmds.labels).filter(pair => pair[1] <= cmds.pos).sort(([,a],[,b]) => b-a)[0]
								if (!closest) {
									log[level](jpre+` set to loop previous jump mark but none found above!`)
									return false
								}
								jumpStack.push([closest[1], closest[0], Date.now(), 0])
							} else {
								log[level](jpre+` set to ${val == Command.doLoop ? "loop last jump" : "resume after original caller"} but no previous jump occured!`)
								return false
							}
						}
						if (cmd.max && jumpStack.last()[3]+1 >= cmd.max) {
							doLog && log[level](jpre+` set to ${val == Command.doRepeat ? 'repeat' : val == Command.doLoop ? 'loop' : 'jump'} but maximum iterations of ${cmd.max} reached!`)
							return false
						}
						const retPos = val == Command.doLoop ? jumpStack.last()[0] : jumpStack.pop()[0]+1
						val == Command.doLoop && jumpStack.last()[3]++
						jumpBack = retPos <= cmds.pos
						cmds.pos = retPos-1
						msg = `${val == Command.doLoop ? (closest ? "loop previous jump mark" : "loop last jump")+` ↻${jumpStack.last()[3]}` : "resume after caller"} (command #${retPos} @ line ${(cmds[retPos-1] && cmds[retPos-1]._line) || 'EOF'})`
					} else {
						if (cmd.max && cmd._count >= cmd.max) {
							doLog && log[level](jpre+` set to ${val == Command.doRepeat ? 'repeat' : val == Command.doLoop ? 'loop' : 'jump'} but maximum iterations of ${cmd.max} reached!`)
							return false
						}
						cmds.pos += val
						msg = `skip next ${val > 1 ? val+' commands':'command'} (${type}=${val})`
					}

					let jd = cmds[cmds.pos] && cmd.jd && Number(cmd.jd._().reference(res * 60).resolve())
					if ((jd > 0 || jumpBack) && (jd = Math.abs(jd)) > 0) {
						doLog && log[level](jpre+` set to ${msg}... (+${jd}s delay on ${jumpBack?'back ':''}jump)`)
						while((yield* sleep(jd)) == null);
					} else {
						doLog && log[level](jpre+` set to ${msg}...`)
					}
				}
			}
			if (cmd._a && cmd._a.reset) {
				cmd.listReset()
			}
			return true
		}

		function handleToggle(type) {
			cmd[type].forEach(name => {
				if (name === pvShortU || name === pvShortL || name === 'APP' || name === 'app') {
					loadOptions()
					opt.disableAll = type === 'enable' ? false : type === 'disable' ? true : !opt.disableAll
					logAll && log.info(pre+`Parameter set to ${type} running of ALL new alerts!`)
					saveOptions(), disableAllChanged()

				} else getAlertsByName(name || alertId).forEach(alert => {
					alert.enabled = type === 'enable' ? true : type === 'disable' ? false : !alert.enabled
					logAll && log.info(pre+`Parameter set to ${type} alert #${alert.id} – "${alert.names.join(', ')}"!`)
					chrome.runtime.sendMessage(null, {method: 'alert.toggle', value: alert.id})
				})
			})
		}

		function handleInterval() {
			const args = cmd.interval
			getAlertsByName(args[0] || alertId).forEach(alert => {
				alert.auto = args[1] !== "" && args[1] != 0
				alert.autoInt = (args[1] != 0 && args[1]) || "*"
				if (alert.autoInt[0] === '+') {
					const ts = new Date(Date.now() + getTF(alert.autoInt, 1) * m2ms)
					alert.autoInt = ts.getMinutes() + ' ' + ts.getHours() + ' ' + ts.getDate() + ' ' + (ts.getMonth() + 1)
				}
				logAll && log.info(pre+`Parameter set to ${alert.auto?'configure interval '+alert.autoInt:'disable interval'} for alert #${alert.id} – "${alert.names.join(', ')}"!`)
				chrome.runtime.sendMessage(null, {method: 'alert.interval', value: alert.id})
			})
		}

		function handleLockCheck() {
			const symbol = cmd.lockcheck[1] || (cmd.e + ':' + cmd.s)
			const tf = getTF(cmd.lockcheck[2], res)
			const lastAlert = localStore.get('lastAlert')
			const lock = lastAlert.lock[symbol]
			if (lock) {
				const key = getAlerts(lock.id)
				const info = `Lockcheck – Symbol ${symbol} (${formatTF(lock.res)}) locked @ ${formatDateS(lock.time)} by '${(key||{names:[lock.id?'Unknown':'legacy alert']}).names.join(', ')}'`

				if (lock.exp && timeAdd(lock.time, lock.exp * 60) < new Date()) {
					logWarn && log.warn(pre+info+` => Expired after ${lock.exp}m! Unlocking...`)
					delete lastAlert.lock[symbol]
					localStore.updated()
					key && (key.locks = Math.max(key.locks || 1, 1) - 1)
				} else if (!key && lock.id) {
					logWarn && log.warn(pre+info+" => Dormant! (locking alert not found) Unlocking...")
					delete lastAlert.lock[symbol]
					localStore.updated()
					key && (key.locks = Math.max(key.locks || 1, 1) - 1)
				} else if (cmd.lockcheck[0] == 1 && lock.res != tf) {
					logWarn && log.warn(pre+info+` => Timeframe mismatch! (${formatTF(tf)})`)
				} else {
					throw new Error(info+"!")
				}
			}
		}

		function handleLock() {
			const symbol = cmd.lock[1] || (cmd.e + ':' + cmd.s)
			const tf = Number(cmd.lock[2] || res)
			const lastAlert = localStore.get('lastAlert')
			lastAlert.lock = lastAlert.lock || {}
			const lock = lastAlert.lock[symbol]
			const key = lock && getAlerts(lock.id)

			if (cmd.lock[0] == 0) {
				if (lock) {
					const info = `Symbol ${symbol} (${formatTF(lock.res)}) locked @ ${formatDateS(lock.time)} by '${(key||{names:[lock.id?'Unknown':'legacy alert']}).names.join(', ')}'`

					logWarn && log.warn(pre+info+` => ${key||!lock.id?'':'Dormant! (locking alert not found) '}Unlocking${key?' #'+(key.locks || 1):''}...`)
					delete lastAlert.lock[symbol]
					localStore.updated()
					key && (key.locks = Math.max(key.locks || 1, 1) - 1)
				} else {
					logAll && log.info(pre+"No symbol lock found, nothing to unlock...")
				}
			} else {
				key && (key.locks = Math.max(key.locks || 1, 1) - 1)
				const exp = Math.min(getTF(cmd.lock[0]), 0)
				lastAlert.lock[symbol] = {id: alertId, time: new Date().toISOString(), res: tf, exp: -exp}
				localStore.updated()

				const alert = getAlerts(alertId, {})
				alert.locks = (alert.locks || 0) + 1
				logWarn && log.warn(pre+`Locking ${symbol} (${formatTF(tf)}) #${alert.locks}...`+(exp?` (expiring in ${-exp}m!)`:''))
			}
		}

		function* handleRun(type = "run") {
			for (const name of cmd[type]) {
				const alert = getAlertsByName(name)[0]
				if (!alert) {
					logWarn && log.warn(pre+`Couldn't find any alert with name/id "${name}"!`)
					return
				} else if (!alert.enabled && type.endsWith("en")) {
					logAll && log.info(pre+`Alert "${name}" is disabled – not running!`)
					return
				}
				const evID = pre.slice(1, -2)+" => "+alert.names.join(', ')
				const evCopy = Object.assignDeep(ev, {
					sym: cmd.e+':'+cmd.s,
					desc: alert.commands.join('\n'), descRaw: null,
					// $$$$$$ check alert name as fallback?
					side: (cmd.has('b') && cmd.b) || ev.side || ohlc._side || (alert.detect && 'auto'),
					isInternal: true,
					caller: alertId
					// $$$ log: cmd.log ???
				})

				let defs = {}
				cmd.extract(defs, defaultParams, overrides, true)
				delete defs.e, delete defs.s, defs.a = cmd.a

				logAll && log.info(pre+`Parameter set to ${type} alert #${alert.id} – "${alert.names.join(', ')}"!`)
				alert.fired = new Date().toISOString()
				alert.firedNum = (alert.firedNum || 0) + 1
				alert.intNum = (alert.intNum || 0) + 1

				// $$$$ implement params=var1:val1,var2:val2,...

				if (type.startsWith("trigger")) {
					setTimeout(run.bind(this, runCommands, evID, evCopy, disabled, alert.id, undefined, Object.assignDeep(defs)), forkSleep)
				} else {
					evCopy.isFork = true
					yield* runCommands(evID, evCopy, disabled, alert.id, undefined, defs)
				}
			}
		}

		function handleBrake() {
			// $$$$ DON'T PAUSE OURSELFES!
			if (bg.statRunning <= 1) {
				logAll && log.info(pre+"No other alert is currently running!")
				return
			}
			if (bg.exitAll) {
				logAll && log.info(pre+"Waiting for all other alerts to terminate...")
				return
			}
			if (cmd.exitall) {
				if (bg.statRunning > 1) {
					bg.exitAll = true; bg.pauseAll = false
				}
				logAll && log.info(pre+`Stopping ${bg.statRunning} running alert${bg.statRunning>1?'s':''} immediately!`)
			} else {
				const doPause = cmd.pauseAll
				if (doPause && bg.pauseAll) {
					logAll && log.info(pre+`Running alerts are already paused! (${bg.statRunning} paused)`)
				} else if (!doPause && !bg.pauseAll) {
					logAll && log.info(pre+`Paused alerts are already resumed! (${bg.statRunning} running)`)
				} else {
					logAll && log.info(pre+`${doPause?'Pausing':'Resuming'} ${bg.statRunning} running alert${bg.statRunning>1?'s':''} immediately!`)
				}
				bg.pauseAll = doPause
			}
		}

		function handleSet() {
			const combo = Object.assign({}, cmd, ohlc)
			for (const pair of cmd.setvar) {
				const key = pair[0], value = pair[1]
				if (key && Command.legalVarGlobal.test(key)) {
					const data = value[0] === '"' ? mustacheFields(combo, unquote(value)) : Command.solveExp(value, cmd, ohlc, false, msg => logWarn && log.warn(pre+msg))
					const isGlobal = key[0] === '$'
					logAll && log.info(pre+`Setting ${isGlobal ? 'global' : 'OHLC'} variable "${key}" to ${typeof data === 'string' ? `"${data}"` : data}`)
					if (isGlobal) {
						globalVars.set(key, data, ev, cmd)
					} else {
						ohlc[key] = data
					}
				}
			}
		}

		function handleUnset() {
			const [globals, locals] = cmd.unset.part(key => key[0] === '$')
			// $$$$$$$ add info output about non-existing vars!
			if (globals.length) {
				logAll && log.info(pre+`Removing global variable${globals.length > 1?'s':''} "${globals.join('", "')}"`)
				globalVars.remove(globals)
			}
			if (locals.length) {
				logAll && log.info(pre+`Removing OHLC variable${locals.length > 1?'s':''} "${locals.join('", "')}"`)
				locals.forEach(key => delete ohlc[key])
			}
		}

		if (cmd.cha) {
			let found = 0, enabled = 0
			getAlertsByName(cmd.cha[0]).forEach(alert => ++found && alert.enabled && ++enabled)
			const msg = 
				enabled < found ? `At least one alert DISABLED! (${found} found / ${enabled} enabled – cha=${cmd.cha[0]} specified)` : 
						enabled ? `All alerts ENABLED! (${found} found / ${enabled} enabled – cha=${cmd.cha[0]} specified)` : 
							`No matching alert found! (cha=${cmd.cha[0]} specified)`

			if (!(cmd.ife || cmd.ifd)) {
				if (enabled < found || !found) {
					logCond && log.notice(pre+msg+", SKIPPING command...")
					++cmdNum; continue
				}
			} else {
				if (found) {
					const type = enabled < found ? 'ifd' : enabled ? 'ife' : null
					if (type && cmd[type]) {
						logCond && log.notice(pre+msg)
						incStat('Cond'); isConditional = true
						if (yield* handleJump(type)) continue
					}
				} else {
					logWarn && log.warn(pre+msg)
				}
			}
		} else if (cmd.if) {
			Exchange.loadData(cmd, ohlc)
			if (!Command.evalIf(cmd.if, cmd, ohlc, false, msg => logWarn && log.warn(pre+msg))) {
				const msg = `Expression evaluated to FALSE (if="${cmd.if}" specified)`
				if (cmd.else) {
					logCond && log.notice(pre+msg)
					incStat('Cond'); isConditional = true
					if (yield* handleJump('else')) continue
				} else {
					logCond && log.notice(pre+msg+", SKIPPING command...")
					++cmdNum; continue
				}
			} else {
				logCond && log.notice(pre+`Expression evaluated to TRUE (if="${cmd.if}" specified), executing command...`)
			}
		}

		if (!cmd.hasAny(cmdParams)) {
			if (!cmd.hasAny(cmdParamsInt) && cmd.extract(overrides, [isParent || isChild > 0 ? null : 'a', ...defaultParams])) {
				logAll && log.info(pre+"Alert defaults set to: "+stringifyList(overrides))
			}
			if (cmd.ap) {
				if (cmd.has('a')) {
					logErr && log.error(pre+"Parallel accounts (ap=) and normal accounts option (a=) cannot be combined!")
				} else if (isParent || isChild > 0) {
					logErr && log.error(pre+"Parallel accounts option (ap=) can only be used ONCE per alert!")
				} else {
					handleParallel(cmd._ap || [cmd.ap], 1)
				}
			}
			if (isChild > 0) {
				cmd.listGoto(isChild)
			}
			cmd.has('n') && Exchange.loadData(cmd, ohlc) && handleNotifications(logAll && pre, ev, true, cmd)
			cmd.enable && handleToggle('enable')
			cmd.disable && handleToggle('disable')
			cmd.toggle && handleToggle('toggle')
			cmd.interval && handleInterval()
			cmd.setvar && handleSet()
			cmd.unset && handleUnset()
			cmd.skip && (yield* handleJump('skip'))
			++cmdNum; continue
		}
		if (cmd.ap) {
			logErr && log.error(pre+"Parallel accounts option (ap=) can only be used in a line by itself or with setting defaults!")
		}

		if (cmd._a && cmd._a.remain && cmd._a.remain()) {
			cmd.listNext()
		} else if (isChild > 0) {
			cmd.listGoto(isChild)
		}
		pre = `[${id}:${cmd._line}${cmd._count > 1 ? ':'+cmd._count:''}${cmd.a !== '*' ? ' @ '+cmd.a:''}] `

		if (disabled) cmd.d = 1
		try {
			if (!cmd.skip && cmd.max && cmd._count > cmd.max) {
				throw new Error(`Maximum iterations of ${cmd.max} reached!`)
			}
			cmd.lockcheck && handleLockCheck()

			if (cmd.hasAny(['minhvd', 'maxhvd'])) {
				const vol = yield* getVolatility(1)
				ohlc.hvd = vol
				if (cmd.minhvd && vol < cmd.minhvd) throw new Error(`Daily volatility (${vol}) too low! (minhvd=${cmd.minhvd} was specified)`)
				if (cmd.maxhvd && vol > cmd.maxhvd) throw new Error(`Daily volatility (${vol}) too high! (maxhvd=${cmd.maxhvd} was specified)`)
			}
			if (cmd.hasAny(['minhvw', 'maxhvw'])) {
				const vol = yield* getVolatility(7)
				ohlc.hvw = vol
				if (cmd.minhvw && vol < cmd.minhvw) throw new Error(`Weekly volatility (${vol}) too low! (minhvw=${cmd.minhvw} was specified)`)
				if (cmd.maxhvw && vol > cmd.maxhvw) throw new Error(`Weekly volatility (${vol}) too high! (maxhvw=${cmd.maxhvw} was specified)`)
			}
			if (cmd.hasAny(['minhvm', 'maxhvm'])) {
				const vol = yield* getVolatility(30)
				ohlc.hvm = vol
				if (cmd.minhvm && vol < cmd.minhvm) throw new Error(`Monthly volatility (${vol}) too low! (minhvm=${cmd.minhvm} was specified)`)
				if (cmd.maxhvm && vol > cmd.maxhvm) throw new Error(`Monthly volatility (${vol}) too high! (maxhvm=${cmd.maxhvm} was specified)`)
			}

			if (cmd.hasAny(['min1h', 'max1h', 'min24h', 'max24h', 'min7d', 'max7d'])) {
				const change = yield* getChange(cmd.cmcid)
				//change.cmc_price = change.price
				//delete change.price
				// $$$$ Object.without(change, ['price', 'tvl'])
				Object.assign(ohlc, change)

				const c1h = change.percent_change_1h
				if (cmd.has('min1h') && c1h < cmd.min1h) throw new Error(`1h price change (${c1h}%) too low! (min1h=${cmd.min1h} was specified)`)
				if (cmd.has('max1h') && c1h > cmd.max1h) throw new Error(`1h price change (${c1h}%) too high! (max1h=${cmd.max1h} was specified)`)

				const c24h = change.percent_change_24h
				if (cmd.has('min24h') && c24h < cmd.min24h) throw new Error(`24h price change (${c24h}%) too low! (min24h=${cmd.min24h} was specified)`)
				if (cmd.has('max24h') && c24h > cmd.max24h) throw new Error(`24h price change (${c24h}%) too high! (max24h=${cmd.max24h} was specified)`)

				const c7d = change.percent_change_7d
				if (cmd.has('min7d') && c7d < cmd.min7d) throw new Error(`7d price change (${c7d}%) too low! (min7d=${cmd.min7d} was specified)`)
				if (cmd.has('max7d') && c7d > cmd.max7d) throw new Error(`7d price change (${c7d}%) too high! (max7d=${cmd.max7d} was specified)`)
			}

			if (!cmd.hasAny(cmdParamsEx) || cmd.trigger || cmd.triggeren || cmd.run || cmd.runen) {
				if (cmd.time && cmd.time.reference) {
					const elapsed = Date.now() - (cmd.since === "cbar" ? cbarTime : cmd.since === "bar" ? barTime : fireTime)
					const max = cmd.time._().reference(res * 60).resolve() * 1000
					if (elapsed > max) {
						const msg = `Too much time elapsed! (${(elapsed / 1000).toFixed(2)}s since ${cmd.since||'fired'} – maximum set to ${(max / 1000).toFixed(2)}s)`
						if (cmd.ift) {
							logCond && log.notice(pre+msg)
							incStat('Cond'); isConditional = true
							if (yield* handleJump('ift', (!cmd._a || (cmd._a.remain && !cmd._a.remain())))) continue
						} else {
							throw new Error(msg)
						}
					}
				}
				if (cmd.trigger || cmd.triggeren || cmd.run || cmd.runen) {
					cmd.trigger && (yield* handleRun("trigger"))
					cmd.triggeren && (yield* handleRun("triggeren"))
					cmd.run && (yield* handleRun("run"))
					cmd.runen && (yield* handleRun("runen"))
				} else if (cmd.has('p') || cmd.has('q')) {
					throw new SyntaxError("Side of the market (b= or side=) not specified!")
				}
				cmd.has('n') && Exchange.loadData(cmd, ohlc) && handleNotifications(logAll && pre, ev, true, cmd)
				++cmdNum
				ls.statLastCmd = Date.now()

			} else if ((exchange = broker.get(id, cmd.e, cmd.s, cmd.a))) {
				if (cmd.time && cmd.time.reference) {
					const elapsed = Date.now() - (cmd.since === "cbar" ? cbarTime : cmd.since === "bar" ? barTime : fireTime)
					const max = cmd.time._().reference(res * 60).resolve() * 1000
					if (elapsed > max) {
						const msg = `Too much time elapsed! (${(elapsed / 1000).toFixed(2)}s since ${cmd.since||'fired'} – maximum set to ${(max / 1000).toFixed(2)}s)`
						if (cmd.iftf || cmd.iftnf) {
							const ords = (yield* exchange.getOrders(cmd.a, cmd.s, 1, cmd.id, cmd.isMargin))[cmd.a]		// $$$ loglevel?
							const ord = ords && ords[0] // search for match (type etc.) $$$
							if (ord && ord.getAny) {
								const type = 'ift' + (exchange.isPartial(ord) ? 'f' : 'nf')
								if (type && cmd[type]) {
/*
		iftf: "If Timeout And Fill (Partial)",
		iftnf: "If Timeout And No Fill",
*/
									logCond && log.notice(pre+msg) // $$$$$$$ add some message about iftf/iftnf !!!
									incStat('Cond'); isConditional = true
									if (yield* handleJump(type, (!cmd._a || (cmd._a.remain && !cmd._a.remain())))) continue
								}
							}
						}
						if (cmd.iftp || cmd.iftnp) {
							const pos = (yield* exchange.getPositions(cmd.a, cmd.s, 1))[cmd.a].list		// $$$ loglevel?
							// $$$$$ todo: check side ?!
							const type = 'ift' + (pos && pos.length ? 'p' : 'np')
							if (type && cmd[type]) {
/*
		iftp: "If Timeout And Position",
		iftnp: "If Timeout And No Position",
*/
								logCond && log.notice(pre+msg) // $$$$$$$ add some message about iftp/iftnp !!!
								incStat('Cond'); isConditional = true
								if (yield* handleJump(type, (!cmd._a || (cmd._a.remain && !cmd._a.remain())))) continue
							}
						}
						if (cmd.ift) {
							logCond && log.notice(pre+msg)
							incStat('Cond'); isConditional = true
							if (yield* handleJump('ift', (!cmd._a || (cmd._a.remain && !cmd._a.remain())))) continue
						} else {
							throw new Error(msg)
						}
					}
				}
				if (!cmd.s) {
					throw new Error("Couldn't determine symbol! (on manual run set s= in syntax or define a default under 'General Options')")
				}

				// $$$ api-fy
				const exOpt = syncStore.get("options_"+exchange.getAlias().toLowerCase()) || defExchangeOpt
				if ((!cmd.c && exOpt.retryorder) || (cmd.c && exOpt.retrycancel)) {
					if (cmd.retries === undefined) cmd.retries = exOpt.retries
					if (cmd.timeout === undefined) cmd.timeout = exOpt.timeout
				}
				if (cmd.retries && cmd.retry === undefined) cmd.retry = 0

				++cmdNum
				ls.statLastCmd = Date.now()
				const result = yield* exchange.executeCommand(cmd, pre.slice(1, -2), ohlc, cmdLog, (opt.telebotEcho > 1 || (opt.telebotEcho > 0 && !alertId)) && ev.telebot)
				if (isParent || isChild) {
					while((yield* sleep(childSleep)) == null);
				}

				if(result) {
					// $$$ orderReports = Binance; still needed?
					let data = result.orderReports || result
					try {
						if (Array.isArray(data)) {
							if (data.length < 1) throw false
							data = data.last()
						}
						if (!cmd.ch && !cmd.d && !cmd.ub && (!cmd.up || cmd.b) && !cmd.query) {
							ls.lastSuccess = JSON.stringify({date: new Date().toISOString(), command: cmd, order: data, alert: alertId, name: ev.name, num: cmds.pos, res: res, side: ev.side})

							let info = pre + exchange.getName() + ' ' + (result && result.length > 1 ? result.length+' ':'') + ucfirst((cmd.b ? cmd.b + ' ':'') + (
									cmd.c === 'order' ? `order${result && result.length > 1 ? 's':''} canceled!` : 
								 	cmd.c === 'position' ? `position close order${result && result.length > 1 ? 's':''} placed!` : 
								 	//cmd.query ? cmd.query+' queried!' : 
								 	(cmd.tb ? "transfer " : cmd.loan ? "loan " : cmd.repay ? "repay ":'')+"order placed!"
								)) + (cmd.id ? ` (${cmd.id})`:'')

							if (data && typeof data === 'object') {
								info += (" {"+formatFields(data, /*cmd.query ? fieldPNLInfo :*/ fieldAll)+"\n}").replace(" {\n}", "")
							}
							logAll && log.success(info)
						}
						if ((!cmd.ch && !cmd.d) || cmd.has('n')) {
							handleNotifications(logAll && pre, ev, true, cmd, data, result.length, result.orderReports || result)
						}
					} catch(e){}

					if (result.length) {
						if (cmd.ifl || cmd.ifs) {
							const side = Exchange.findSide(result, cmd.ifl !== undefined, cmd.ifs !== undefined)
							if (side) {
								incStat('Cond'); isConditional = true
								if (yield* handleJump(side === 'long' ? 'ifl' : 'ifs', (!cmd._a || (cmd._a.remain && !cmd._a.remain())))) continue
							}
						}
						if ((cmd.ifbg || cmd.ifsm) && data && data.getAny) {
							const qty = Number(data.getAny(fieldQuantity))
							const prevqty = cmd.sv ? ohlc.lastqty2 : ohlc.lastqty
							let type = 'ifbg'
							if ((cmd.ifbg && qty > prevqty) || (cmd.ifsm && qty < prevqty && (type = 'ifsm'))) {
								incStat('Cond'); isConditional = true
								if (yield* handleJump(type, (!cmd._a || (cmd._a.remain && !cmd._a.remain())))) continue
							}
						}
						if (cmd.ifp && data && data.getAny && exchange.isPartial(data)) {
							incStat('Cond'); isConditional = true
							if (yield* handleJump('ifp', (!cmd._a || (cmd._a.remain && !cmd._a.remain())))) continue
						}
					} else if (cmd.ws) {
						if (!cmd.b) {
							logWarn && log.warn(pre+`Option ws= (wrongside) specified on command without b= (side) parameter – please see the description of ws= under "Setup: Commands"!`)
						} else {
							incStat('Cond'); isConditional = true
							if (yield* handleJump('ws', (!cmd._a || (cmd._a.remain && !cmd._a.remain())))) continue
						}
					}
				} else if (result === false && cmd.nr) {
					incStat('Cond'); isConditional = true
					if (yield* handleJump('nr', (!cmd._a || (cmd._a.remain && !cmd._a.remain())))) continue
				}

				if (cmd.ifn || cmd.ifo || cmd.ret) {
					const min = (cmd.ifn || cmd.ifo) && cmd.retries && cmd.has('retries') ? 1 : (cmd.ret || -1)
					if (!result || result.length < Math.abs(min)) {
						if (min < 0 && cmd.retries) {
							logAll && cmd.has('retries') && log.info(pre+`Skipping retries for not enough ${cmd.ch||cmd.c||'item'}s returned condition (${cmd.ifn?'ifn':cmd.ifo?'ifo':'ret < 0'} specified)...`)
							delete cmd.retries
						}
						isConditional = true
						throw new Error(`Not enough ${cmd.ch||cmd.c||'item'}s returned! (${cmd.ifn?'ifn':cmd.ifo?'ifo':'ret='+min} specified, not enough ${cmd.ch||cmd.c||'item'}s matched)`)
					}
				} else if (cmd.has('ret') && result && result.length) {
					delete cmd.retries
					isConditional = true
					throw new Error(`Too many ${cmd.ch||cmd.c||'item'}s returned! (ret=0 specified)`)
				}
			} else {
				throw new Error("Couldn't determine exchange! (on manual run set e= in syntax or define a default under 'General Options')")
			}
			successful = true
			cmd.enable && handleToggle('enable')
			cmd.disable && handleToggle('disable')
			cmd.toggle && handleToggle('toggle')
			cmd.interval && handleInterval()
			cmd.setvar && handleSet()
			cmd.unset && handleUnset()
			cmd.lock && handleLock()
			;(cmd.exitall || cmd.pauseall || cmd.resumeall) && handleBrake()
			cmd.restart && restart(true)

		} catch (ex) {
			let err = getError(ex, "Not configured – check if exchange and account are specified correctly!")
			const line = `command #${cmds.pos}`+(cmd._line && cmd._line != cmds.pos ? ` (line ${cmd._line})`:'')

			const timeSync = opt.timeSync && exchange && exchange.checkTime && err.includesNoCase('timestamp') && Date.now() - lastTimeSync > streamRetryTimeout
			if (timeSync) {
				logWarn && log.warn(pre+`Time sync issue detected, trying automatic time sync...`)
				lastTimeSync = Date.now()
				yield* exchange.checkTime()
			}

			const isNewCheck = err.startsWithAny(newChecks)
			if (isNewCheck) {
				delete cmd.retries
				isConditional = cmd.has(newCheckParams[isNewCheck-1])
			}

			if (exchange && ((isNewCheck === errSlippageCheck && (cmd.ifslp || cmd.ifslnp)) || 
				(isConditional && err.startsWith('Not enough ') && (cmd.ifnp || cmd.ifnnp)))) {
				const pos = (yield* exchange.getPositions(cmd.a, cmd.s, 1))[cmd.a].list		// $$$ loglevel?
				// $$$$$ todo: check side ?!
				const type = (isNewCheck === errSlippageCheck ? 'ifsl' : 'ifn') + (pos && pos.length ? 'p' : 'np')
				if (type && cmd[type]) {
/*
		=> ifslp= specified
		ifslp: "If Slippage And Position",
		ifslnp: "If Slippage And No Position",
		ifnp: "If No Match And Position",
		ifnnp: "If No Match And No Position",
*/
					logCond && log.notice(pre+err) // $$$$$$$ add some message about ifslp/ifslnp/ifnp/ifnnp !!!
					incStat('Cond'); isConditional = true
					if (yield* handleJump(type, (!cmd._a || (cmd._a.remain && !cmd._a.remain())))) continue
				}
			}

			let isErr = false
			if (isConditional) {
				incStat('Cond')
				logCond && log.notice(pre+`Conditional in ${line}: ${err}`)
				if (isNewCheck && (yield* handleJump(newCheckParams[isNewCheck-1], (!cmd._a || (cmd._a.remain && !cmd._a.remain()))))) continue

			} else if (errIsWarning.test(err) || (opt.ignPerm && err.includes('Please grant')) || (opt.ignPerm && (err.includes('No account') || err.includes('Non-existent account')))) {
				incStat('Warn')
				logWarn && log.warn(pre+`Warning in ${line}: ${err}`)
			} else {
				isErr = true, incStat('Err')
				ls.badgeErr = Number(ls.badgeErr || 0) + 1
				ls.lastError = JSON.stringify({date: new Date().toISOString(), command: cmd, error: err, alert: alertId, name: ev.name, num: cmds.pos, res: res, side: ev.side})
				logErr && log.error(pre+`Error in ${line}: ${err} – Syntax: "${syntax.raw.split('\n')[cmd._line-1]}" – Command: ${stringify(cmd)}(calculated values)`)
				ev.telebot && telebotSend(ev.telebot, pre+`Error in ${line}: ${err} – Syntax: "${syntax.raw.split('\n')[cmd._line-1]}" – Command: ${stringify(cmd)}(calculated values)`)

				handleNotifications(logAll && pre, ev, false, cmd, err)

				const alert = getAlerts(alertId, {})
				alert.errNum = (alert.errNum || 0) + 1
			}

			if (!timeSync && cmd.retries && !cmd.retryall && errIsNoRetry.test(err)) {
				if (!opt.retryBal || !errIsBalance.test(err)) {
					isErr && logWarn && log.warn(pre+"Skipping retries for this error type...")
					delete cmd.retries
				}
			}
		}

		if (!successful) {
			if (cmd.err != Command.errIgnore && cmd.retries && exchange) {
				if (cmd.retry < cmd.retries) {
					++cmd.retry, --cmds.pos
					if (cmd._a && cmd._a._prev) {
						cmd.listPrev()
					}
					logWarn && log.warn(pre+`Retrying in ${cmd.timeout} seconds (${cmd.retry} of ${cmd.retries} retries)...`)
					incStat('Retries'); continue
				} else {
					logWarn && log.warn(pre+`Maximum retries reached (${cmd.retry} of ${cmd.retries}) – giving up!`)
					incStat('RetriesMax')
					cmd.retry = 0
				}
			}
			cmd._errCount = (cmd._errCount || 0) + 1

			if (isConditional && cmd.has('ifn') ? cmd.ifn : cmd.err) {
				if (yield* handleJump(isConditional && cmd.has('ifn') ? 'ifn' : 'err', !cmd._a || cmd._errCount >= cmd._a.length)) continue
			}
		} else if (cmd.ifo || cmd.skip) {
			if (yield* handleJump(cmd.ifo ? 'ifo' : 'skip', (!cmd._a || (cmd._a.remain && !cmd._a.remain())) && !cmd._errCount)) continue
		}

		if (cmd._a && cmd._a.remain) {
			if (cmd._a.remain()) {
				--cmds.pos
			} else {
				cmd.listReset()
			}
		}
	}

	const totalTime = (Date.now() - startTime) / 1000
	;(cmdNum || ev.isManual) && logAll && log.info(pre+`Executed ${cmdNum} command${cmdNum > 1 ? 's':''} in ${formatDur(totalTime, 2)} – DONE!`)
	ev.telebot && opt.telebotSum && telebotSend(ev.telebot, pre+`Executed ${cmdNum} command${cmdNum > 1 ? 's':''} in ${formatDur(totalTime, 2)} – DONE!`)
	if (!ev.isFork) {
		--statRunning && logAll && log.info(`Still running ${statRunning} alert${statRunning > 1 ? 's':''}...`)
		if (!statRunning && bg.exitAll) {
			bg.exitAll = false
			log.notice("All alerts terminated!")
		}

		if (cmdNum && totalTime && (!ev.isManual || opt.statManual)) {
			let alert = getAlerts(alertId, {})
			alert.time = totalTime
			incStat('Commands', cmdNum)
			incStat('Time', totalTime)

			if (totalTime >= opt.statMinTime) {
				alert.timeAvg = ((alert.timeAvg || totalTime) + totalTime) / 2
				alert.timeMax = Math.max(alert.timeMax || 0, totalTime)
				alert.timeMin = Math.min(alert.timeMin || Number.MAX_VALUE, totalTime)
				minMaxStat('Time', totalTime)
				autoLastEvent = Date.now()
			}
		}
		incStat('Alerts')
		if (!cmdNum || totalTime < opt.statMinTime) incStat('AlertsEmpty')
		if (ev.telebot) incStat('TeleRun')
	}
	if (isChild > 0) incStat('Children')
	else if (isManual) incStat('Manual')
	else if (ev.isFork) incStat('Forked')
	else if (ev.isInternal) incStat('Trigger')
	isManual && (autoLastEvent = Date.now())

	broker.free(id)
	return cmdNum
}

const handlers = {
	"connect": function* (msg) {
		const tabs = yield* getTradingViewTabs()
		tvTabs = tabs.length
		log.info(`${pvName} TV Addon loading – tab opened / loaded! (${tvTabs} total)`)
	},
	"disconnect": function* (msg) {
		--tvTabs
		log.info(`${pvName} TV Addon unloaded – tab closed / unloaded! (${tvTabs} total)`)

		if (tvTabs < 1 && Date.now() - tvLastReload >= reloadSafetyTimeout) {
			yield* sleep(1)
			if (tvTabs < 1 && Date.now() - tvLastReload >= reloadSafetyTimeout) {
				!opt.disableAll && (!opt.onlyStream || tvStreamStatus < TRADINGVIEW_STREAM_OPEN) && log.warn("NOTE: NO TRADINGVIEW TAB LEFT OPEN!")
				tvLastReload = Date.now()
				yield* forceTradingViewCheck()
			}
		}
	},
	"screener": function* (ev, sender) {
		if (inShutdown) return
		if (opt.onlyStream && sender && tvStreamStatus >= TRADINGVIEW_STREAM_OPEN && tvLastEvent > Date.now() - addonIgnoreWindow) {
			console.log("Event Stream only mode: Ignoring TV Addon Screener event!")
			return
		}
		if (sender) incStat('ScreenerEvents')
		else incStat('ScreenerStreamEvents')

		let i = 0
		for (const sym of ev.d) {
			const evCopy = Object.assignDeep(ev)
			evCopy.id = evCopy.id+'-'+(++i)
			evCopy.screener = i
			evCopy.sym = sym
			evCopy.res = opt.screenRes || opt.defaultRes
			yield* handlers.event(evCopy, sender)

			if (opt.screenFirst) break
		}
	},
	"event": function* (ev, sender) {
		if (inShutdown) return
		if (opt.onlyStream && sender && tvStreamStatus >= TRADINGVIEW_STREAM_OPEN && tvLastEvent > Date.now() - addonIgnoreWindow) {
			console.log("Event Stream only mode: Ignoring TV Addon event!")
			return
		}
		let eventID = ev.id

		if (ev.telebot || !bg.tvEvents[eventID]) {
			if (!ev.telebot) {
				bg.tvEvents[eventID] = Date.now()
				log.debug(`TradingView ${sender?'TV Addon':'Stream'} <= received event id: ${eventID}`)
				tvLastEvent = Date.now()
				if (opt.prefixTV && !ev.desc.startsWith(opt.prefixTVmsg)) {
					log.notice(`[${eventID}] Received alert message does NOT start with configured prefix "${opt.prefixTVmsg}"! (see 'General Options / TradingView Connection') {\n${lineNumbers(ev.desc)}\n}`)
					return
				}
			}
			if (!ev.screener || ev.screener === 1) {
				ls.badgeRcv = Number(ls.badgeRcv || 0) + 1
				incStat(ev.telebot ? 'Telebot' : sender ? 'Events' : 'StreamEvents')
			}

			for (const entry of sourceMap) {
				if (entry[0].test(ev.sym)) {
					ev.sym = ev.sym.replace(entry[0], entry[1]); break
				}
			}
			ev.bar_time = new Date(ev.bar_time * 1000)
			ev.fire_time = new Date(ev.fire_time * 1000)
			ev.res = getTF(ev.res)
			ev.bar_pos = (ev.fire_time - ev.bar_time) / 1000 / 60 / ev.res * 100

			const fireOffset = Math.max((Date.now() - ev.fire_time) / 1000, 0)
			if(!ev.telebot) if (!ev.screener) {
				log.notice(`[${eventID}] Received Alert (${sender?'A':'S'}) for ${ev.sym} ${formatTF(ev.res)} @ ${formatDateS(ev.bar_time)} / fired ${formatDateS(ev.fire_time)} (${ev.bar_pos.toFixed(1).replace('.0', '')}% bar time, `+
					`${fireOffset.toFixed(2).replace('.00', '')}s slippage) => Commands to run: {\n${lineNumbers(ev.desc)}\n}`)
			} else {
				log.notice(`[${eventID}] Received Screener Alert (${sender?'A':'S'}) '${ev.title}' for ${ev.sym} @ ${formatDateS(ev.bar_time)} (${fireOffset.toFixed(2).replace('.00', '')}s slippage) => Commands to run: {\n${lineNumbers(ev.desc)}\n}`)
			}

			minMaxStat('Offset', fireOffset)
			if (opt.maxTime && fireOffset > opt.maxTime) {
				log.warn(`[${eventID}] Too much time between firing and running of alert! (${fireOffset.toFixed(2).replace('.00', '')}s – 'Maximum Time since Alert fired' set to ${opt.maxTime}s under 'General Options')`)
				incStat('MaxTime'); return
			}

			const alerts = getAlerts().sort(alertSort)
			const lastAlert = localStore.get('lastAlert')
			lastAlert.local = lastAlert.local || {}				// local[sym]		=> last alert ID
			lastAlert.localLast = lastAlert.localLast || {}		// localLast[sym]	=> last alert bar time
			lastAlert.localName = lastAlert.localName || {}		// localName[sym]	=> last alert name
			lastAlert.lock = lastAlert.lock || {}
			const lastPulse = localStore.get('lastPulse')
			if (ev.desc.includes('[')) {
				new Alert(ev, undefined, -1, false, undefined, eventID)
			}
			let lines = ev.desc.trim().split('\n'), warn

			const ohlc = parseOHLC(lines.last())
			if (ohlc) {
				ev.ohlc = ohlc
				log.info(`[${eventID}] Attached OHLC data found:`+stringifyList(ohlc, true))
			}

			if (!alerts.length) {
				log.info(`[${eventID}] No ${pvName} alerts defined, only running legacy alerts...`)
			} else if (!ev.noAlerts) {
				let matched = 0, foundAlerts

				do {
					do {
						ev.name = lines[0].trimSep()
						if (ev.name.length && ev.name[0] !== '#') break
						lines.shift()
					} while(lines.length)
					if (!lines.length) break

					let params = ev.name.indexOf('(')
					if (params > 0) {
						const data = ev.name.substr(params)
						ev.name = ev.name.substr(0, params).trimSep()
						params = parseOHLC(data)
					} else {
						params = null
					}

					foundAlerts = 0
					let disabledAlerts = 0, repeatAlerts = 0, repeatFired = 0, soloFired = 0

					for (const alert of alerts) {
						if (alert.names && alert.names.includesNoCase(ev.name, opt.wildName)) {
							++matched
							++foundAlerts
							alert.recv = opt.recvNow ? new Date().toISOString() : ev.fire_time.toISOString()
							alert.recvNum = (alert.recvNum || 0) + 1
							eventID = `${ev.id}-${matched} => ${ev.name}`
							const info = ` (#${alert.id} – "${alert.names.join(', ')}")`

							if (!alert.enabled && (ev.allow || 0) < 2) {
								++disabledAlerts
								log.warn((warn = `[${eventID}] FILTER: Alert not enabled!`+info))
								ev.telebot && telebotSend(ev.telebot, warn)
								incStat('FilterEn'); continue
							}
							if (!ev.noFilter) {
								if (alert.sched && !intervalMatch(alert.autoInt, new Date())) {
									++disabledAlerts
									log.warn((warn = `[${eventID}] FILTER: Alert schedule not matched!`+info))
									ev.telebot && telebotSend(ev.telebot, warn)
									incStat('FilterSch'); continue
								}

								const idx = ev.sym+'|'+ev.res+'|'+alert.id
								const pulse = (lastPulse[idx] || '|').split('|')
								if (alert.barAmount) {
									if (ev.bar_time > new Date(pulse[0] || 0)) {
										pulse[1] = 0
									}
									pulse[0] = ev.fire_time.toISOString()
									++pulse[1]
									lastPulse[idx] = pulse.join('|')
									pulse[1] /= ev.res / 100
								}

								if (ev.bar_pos <=  3 && !alert.barOpen) {
									log.warn((warn = `[${eventID}] FILTER: Bar time/position 'Bar Open' not allowed!`+info)); incStat('Filter'); ev.telebot && telebotSend(ev.telebot, warn); continue
								}
								if (ev.bar_pos >   3 && ev.bar_pos <= 25 && !alert.barStart) {
									log.warn((warn = `[${eventID}] FILTER: Bar time/position 'Bar Start' not allowed!`+info)); incStat('Filter'); ev.telebot && telebotSend(ev.telebot, warn); continue
								}
								if (ev.bar_pos >  25 && ev.bar_pos <  50 && !alert.barMid1) {
									log.warn((warn = `[${eventID}] FILTER: Bar time/position 'Bar Mid 1' not allowed!`+info)); incStat('Filter'); ev.telebot && telebotSend(ev.telebot, warn); continue
								}
								if (ev.bar_pos >= 50 && ev.bar_pos <  75 && !alert.barMid2) {
									log.warn((warn = `[${eventID}] FILTER: Bar time/position 'Bar Mid 2' not allowed!`+info)); incStat('Filter'); ev.telebot && telebotSend(ev.telebot, warn); continue
								}
								if (ev.bar_pos >= 75 && ev.bar_pos < 96.5 && !alert.barEnd) {
									log.warn((warn = `[${eventID}] FILTER: Bar time/position 'Bar End' not allowed!`+info)); incStat('Filter'); ev.telebot && telebotSend(ev.telebot, warn); continue
								}
								if (ev.bar_pos >= 96.5 && !alert.barClose) {
									log.warn((warn = `[${eventID}] FILTER: Bar time/position 'Bar Close' not allowed!`+info)); incStat('Filter'); ev.telebot && telebotSend(ev.telebot, warn); continue
								}
								if (alert.barSolo && !soloFired && new Date(lastAlert.localLast[ev.sym] || 0) >= ev.bar_time) {
									log.warn((warn = `[${eventID}] FILTER: Option 'Solo per Bar' set and some alert (#${lastAlert.local[ev.sym]}) already fired in bar at ${formatDateS(lastAlert.localLast[ev.sym])}!`+info)); incStat('FilterOnce')
									ev.telebot && telebotSend(ev.telebot, warn); continue
								}
								if (alert.barOnce && new Date(alert.fired || 0) >= ev.bar_time && new Date(lastAlert.localLast[ev.sym] || 0) >= ev.bar_time) {	// $$$ need to keep track of alert.fired per ex:sym $$$$$$$ check res too!? (if lockTF=true?)
									log.warn((warn = `[${eventID}] FILTER: Option 'Once per Bar' set and alert already fired in bar at ${formatDateS(alert.fired)}!`+info)); incStat('FilterOnce')
									ev.telebot && telebotSend(ev.telebot, warn); continue
								}
								if (lastAlert.local[ev.sym] === alert.id) {
									repeatFired = alert.fired || lastAlert.localLast[ev.sym]
									++repeatAlerts
								}
								if (alert.noRepeat && repeatAlerts) {
									if (alert.repName && lastAlert.localName[ev.sym] !== ev.name.toUpperCase()) {
										log.warn(`[${eventID}] FILTER: Option 'No Repeat' set, last alert for symbol repeated but with different name ("${lastAlert.localName[ev.sym]}")! ('Check Name' set)`+info)
										incStat('FilterRepName')
									} else if (alert.repReset && new Date(repeatFired) <= Date.now() - alert.repResetMin*60000) {
										log.warn(`[${eventID}] FILTER: Option 'No Repeat' set, last alert for symbol repeated but expired at ${formatDateS(repeatFired)}! ('Repeat Timeout' set to ${alert.repResetMin}m)`+info)
										incStat('FilterRepRes')
									} else {
										const note = lastAlert.local[ev.sym] !== alert.id ? " (from a different alert sharing the same name)":''
										log.warn((warn = `[${eventID}] FILTER: Option 'No Repeat' set and last alert for symbol at ${formatDateS(repeatFired)+note} is repeated!`+info))
										incStat('FilterRep'); ev.telebot && telebotSend(ev.telebot, warn); continue
									}
								}
								if (alert.repReset && !alert.noRepeat) {
									const last = getAlerts(lastAlert.local[ev.sym], {}).fired || lastAlert.localLast[ev.sym]
									if (new Date(last) > Date.now() - alert.repResetMin*60000) {
										log.warn((warn = `[${eventID}] FILTER: Option 'Timeout' set and some alert (#${lastAlert.local[ev.sym]}) was fired within timeout window at ${formatDateS(last)}! ('Repeat Timeout' set to ${alert.repResetMin}m)`+info))
										incStat('FilterTime'); ev.telebot && telebotSend(ev.telebot, warn); continue
									}
								}

								if (alert.barPA && alert.barPos && alert.barAmount) {
									if (ev.bar_pos < alert.barPos && pulse[1] < alert.barAmount) {
										log.warn((warn = `[${eventID}] FILTER: Option 'Pos %' set to ${alert.barPos}% (alert fired at ${ev.bar_pos.toFixed(1).replace('.0', '')}%) and option 'Dur %' set to ${alert.barAmount}% (alert fired for ${pulse[1].toFixed(1)}%) – neither reached! (OR)`+info))
										incStat('Filter'); ev.telebot && telebotSend(ev.telebot, warn); continue
									}
								} else {
									if (alert.barPos && ev.bar_pos < alert.barPos) {
										log.warn((warn = `[${eventID}] FILTER: Option 'Pos %' set to ${alert.barPos}% and alert only fired at ${ev.bar_pos.toFixed(1).replace('.0', '')}%!`+info))
										incStat('Filter'); ev.telebot && telebotSend(ev.telebot, warn); continue
									}
									if (alert.barAmount && pulse[1] < alert.barAmount) {
										log.warn((warn = `[${eventID}] FILTER: Option 'Dur %' set to ${alert.barAmount}% and alert only fired for ${pulse[1].toFixed(1)}% yet!`+info))
										incStat('Filter'); ev.telebot && telebotSend(ev.telebot, warn); continue
									}
								}

								let lock = lastAlert.lock[ev.sym]
								const key = lock && getAlerts(lock.id)
								if (lock) {
									const lockinfo = `[${eventID}] FILTER: Symbol ${ev.sym} (${formatTF(lock.res)}) locked @ ${formatDateS(lock.time)} by '${(key||{names:[lock.id?'Unknown':'legacy alert']}).names.join(', ')}'`

									if (lock.exp && timeAdd(lock.time, lock.exp * 60) < new Date()) {
										log.warn(lockinfo+` => Expired after ${lock.exp}m! Unlocking${key ? ' #'+(key.locks || 1):''}...`+info)
										delete lastAlert.lock[ev.sym]
										localStore.updated()
										key && (key.locks = Math.max(key.locks || 1, 1) - 1)
										lock = null
									} else {
										if (alert.lockOnly) {
											if (alert.lockTF && lock.res != ev.res) {
												log.warn((warn = lockinfo+` => 'Lock Required' and 'Timeframe Check' enabled, timeframe mismatch! (${formatTF(ev.res)})`+info))
												incStat('FilterLock'); ev.telebot && telebotSend(ev.telebot, warn); continue
											}
											log.warn(lockinfo+` => 'Lock Required' enabled...`)
										} else if (alert.ignLock) {
											log.warn(lockinfo+` => 'Ignore Lock' enabled...`)
										} else if (!key || alert.clearLock) {
											if (key && alert.lockTF && lock.res != ev.res) {
												log.warn((warn = lockinfo+` => 'Timeframe Check' enabled, can't unlock due to timeframe mismatch! (${formatTF(ev.res)})`+info))
												incStat('FilterLock'); ev.telebot && telebotSend(ev.telebot, warn); continue
											}
											if (key && alert.setLock) {
												log.warn(lockinfo+` => Updating...`+info)
											} else if (lock.id) {
												log.warn(lockinfo+` => ${key ? '':'Dormant! (locking alert not found) '}Unlocking${key ? ` #${key.locks||1}`:''}...`+info)
												delete lastAlert.lock[ev.sym]
												localStore.updated()
												key && (key.locks = Math.max(key.locks || 1, 1) - 1)
											}
										} else {
											log.warn((warn = lockinfo+'!'+info)); incStat('FilterLock'); ev.telebot && telebotSend(ev.telebot, warn); continue
										}
									}
								}
								if (!lock && alert.lockOnly) {
									log.warn((warn = `FILTER: No lock found and 'Lock Required' enabled!`+info)); incStat('FilterLock'); ev.telebot && telebotSend(ev.telebot, warn); continue
								}
								if (alert.setLock) {
									key && (key.locks = Math.max(key.locks || 1, 1) - 1)
									lastAlert.lock[ev.sym] = {id: alert.id, time: alert.recv, res: ev.res, exp: alert.lockExp}
									localStore.updated()
									alert.locks = (alert.locks || 0) + 1
									log.warn(`[${eventID}] FILTER: Locking ${ev.sym} (${formatTF(ev.res)}) #${alert.locks}...${alert.lockExp ? ` (expiring in ${alert.lockExp}m!)`:''}`+info)
								}
							}
							if (foundAlerts - disabledAlerts < 2) {
								lastAlert.local[ev.sym] = alert.id
								lastAlert.localLast[ev.sym] = ev.bar_time.toISOString()
								lastAlert.localName[ev.sym] = ev.name.toUpperCase()
								localStore.updated()
								alert.barSolo && ++soloFired
							}

							const evCopy = Object.assignDeep(ev)
							evCopy.desc = alert.commands.join('\n')
							if (params) {
								evCopy.ohlc = evCopy.ohlc ? Object.assign(evCopy.ohlc, params) : params
								log.info(`[${eventID}] Attached parameters found:`+stringifyList(params, true))
							}
							evCopy.side = (evCopy.ohlc && evCopy.ohlc._side) || (alert.detect && 'auto')
							const defs = {}
							let parallel = null
							if (evCopy.ohlc) {
								const o = evCopy.ohlc
								let e = o.e || o.ex, s = o.s || o.sym, a = o.a || o.acc, ap = o.ap || o.par
								if (ap && (!Array.isArray(ap) || ap.length < 2)) {
									a = ap; ap = null
								}
								if (e || s || a || ap) {
									delete o.e; delete o.ex; delete o.s; delete o.sym; delete o.a; delete o.acc; delete o.ap; delete o.par
									const ua = (arr) => Array.isArray(arr) ? arr[0] : arr
									const aa = (arr) => Array.isArray(arr) ? arr : arr && [arr]

									// $$$ api-fy ex:sym:acc
									const exSym = (evCopy.sym || ':').split(':')
									let ex = e || exSym[0], sym = s || exSym[1], acc = a || '*'
									let list = aa(a || ap)
									if (list && list.contains(':')) {
										const defEx = ua(ex) || opt.defExchange
										const defSym = ua(sym) || opt.defSymbol
										ex = []; sym = []
										list = list.map(a => {
											a = a.split(':')
											let alias = (a.length > 1 && a[0] || defEx).toUpperCase()
											const s = (a.length > 2 && a[1] || defSym).toUpperCase()
											a = a.last() || '*'
											if (broker) {
												const e = broker.getByAlias(alias)
												if (!e) {
													log.warn(`[${eventID}] Invalid exchange alias "${alias}" specified – expecting a list of account names or "ex:sym:acc"!`)
												} else {
													alias = e.getAlias()
													if (!e.hasAccount(a)) {
														log.warn(`[${eventID}] Non-existent account "${a}" for ${e.getName()} specified – expecting a list of account names or "ex:sym:acc"!`)
													}
												}
											}
											ex.push(alias), sym.push(s)
											return a
										})
										if (list.length < 2) {
											list = list[0]; ex = ex[0]; sym = sym[0]
										}
										if (ap) ap = list
										else acc = list
									}
									if (ap) {
										parallel = ap
										acc = ua(ap)
									}

									if (ua(ex) != exSym[0] || ua(sym) != exSym[1]) {
										evCopy.sym = (ua(ex)+':'+ua(sym)).toUpperCase()
										log.info(`[${eventID}] Overriding symbol default: `+evCopy.sym)
									}
									if (Array.isArray(ex)) defs.e = ex
									if (Array.isArray(sym)) defs.s = sym
									if (acc != '*') {
										defs.a = acc
										log.info(`[${eventID}] Overriding account default: `+defs.a)
									}
								}
							}
							const badge = evCopy.name+(evCopy.side ? ' '+evCopy.side.toUpperCase():'')+`\n(${evCopy.sym} ${formatTF(evCopy.res)} @ ${formatDateS(alert.recv)})`
							if (badge != ls.badgeEvt) {
								ls.badgeEvt && (ls.badgeEvtLast = ls.badgeEvt)
								ls.badgeEvt = badge
							}

							if (opt.disableAll && !alert.allow && !ev.allow) {
								log.warn((warn = `[${eventID}] Running of new alerts currently disabled!`))
								ev.telebot && telebotSend(ev.telebot, warn)
							} else {
								alert.fired = alert.recv
								alert.firedNum = (alert.firedNum || 0) + 1
								alert.slip = fireOffset

								if (alert.parallel) {
									setTimeout(run.bind(this, runCommands, eventID, evCopy, false, alert.id, 1, defs, 0, parallel), forkSleep)
								} else {
									yield* runCommands(eventID, evCopy, false, alert.id, 1, defs, 0, parallel)
								}
							}
						}
					}
					foundAlerts && lines.shift()
				} while(foundAlerts && lines.length)

				if (lines.length < 1 || (lines.length === 1 && lines[0][0] === '#')) return
				if (ev.noLegacy || (!ev.telebot && opt.legacyIgn)) {
					log.info(`[${eventID}] Running of legacy alerts disabled!`)
					return
				}
				if (matched) {
					ev.desc = lines.join('\n')
					eventID = ev.id
					log.info(`[${eventID}] Found ${matched} ${pvName} alert${matched>1?'s':''} in message, trying to run the rest as legacy...`)
				} else {
					log.info(`[${eventID}] No matching ${pvName} alert found, trying to run as legacy only...`)
				}
				delete ev.name
			}

			const id = ev.id || md5(ev.desc)
			lastAlert.legacy = lastAlert.legacy || {}				// legacy[sym]		=> last alert id (id || md5)
			lastAlert.legacyLast = lastAlert.legacyLast || {}		// legacyLast[sym]	=> last alert bar time
			lastAlert.legacyFired = lastAlert.legacyFired || {}		// legacyFired[id]	=> last alert fire time

			if (!ev.noFilter) {
				const idx = ev.sym+'|'+ev.res+'|L'+id
				const pulse = (lastPulse[idx] || '|').split('|')
				if (opt.legacyBarAmount) {
					if (ev.bar_time > new Date(pulse[0] || 0)) {
						pulse[1] = 0
					}
					pulse[0] = ev.fire_time.toISOString()
					++pulse[1]
					lastPulse[idx] = pulse.join('|')
					pulse[1] /= ev.res / 100
				}

				if (ev.bar_pos <=  3 && !opt.legacyBarOpen) {
					log.warn(`[${eventID}] LEGACY FILTER: Bar time/position 'Bar Open' not allowed!`); incStat('FilterLegacy'); return
				}
				if (ev.bar_pos >   3 && ev.bar_pos <= 25 && !opt.legacyBarStart) {
					log.warn(`[${eventID}] LEGACY FILTER: Bar time/position 'Bar Start' not allowed!`); incStat('FilterLegacy'); return
				}
				if (ev.bar_pos >  25 && ev.bar_pos <  50 && !opt.legacyBarMid1) {
					log.warn(`[${eventID}] LEGACY FILTER: Bar time/position 'Bar Mid 1' not allowed!`); incStat('FilterLegacy'); return
				}
				if (ev.bar_pos >= 50 && ev.bar_pos <  75 && !opt.legacyBarMid2) {
					log.warn(`[${eventID}] LEGACY FILTER: Bar time/position 'Bar Mid 2' not allowed!`); incStat('FilterLegacy'); return
				}
				if (ev.bar_pos >= 75 && ev.bar_pos < 96.5 && !opt.legacyBarEnd) {
					log.warn(`[${eventID}] LEGACY FILTER: Bar time/position 'Bar End' not allowed!`); incStat('FilterLegacy'); return
				}
				if (ev.bar_pos >= 96.5 && !opt.legacyBarClose) {
					log.warn(`[${eventID}] LEGACY FILTER: Bar time/position 'Bar Close' not allowed!`); incStat('FilterLegacy'); return
				}

				if (opt.legacyBarSolo && new Date(lastAlert.legacyLast[ev.sym] || 0) >= ev.bar_time) {
					log.warn(`[${eventID}] LEGACY FILTER: Option 'Solo per Bar' set and some alert already fired in bar at ${formatDateS(lastAlert.legacyLast[ev.sym])}!`); incStat('FilterLegacyOnce'); return
				}
				if (opt.legacyBarOnce && new Date(lastAlert.legacyFired[id] || 0) >= ev.bar_time && new Date(lastAlert.legacyLast[ev.sym] || 0) >= ev.bar_time) {
					log.warn(`[${eventID}] LEGACY FILTER: Option 'Once per Bar' set and alert already fired in bar at ${formatDateS(lastAlert.legacyFired[id])}!`); incStat('FilterLegacyOnce'); return
				}
				if (opt.legacyNoRepeat && lastAlert.legacy[ev.sym] === id) {
					if (opt.legacyRepReset && new Date(lastAlert.legacyFired[id] || 0) <= Date.now() - opt.legacyRepResetMin*60000) {
						log.warn(`[${eventID}] LEGACY FILTER: Option 'No Repeat' set, last alert for symbol repeated but expired at ${formatDateS(lastAlert.legacyFired[id])}! ('Repeat Timeout' set to ${opt.legacyRepResetMin}m)`)
					} else {
						log.warn(`[${eventID}] LEGACY FILTER: Option 'No Repeat' set and last alert for symbol is repeated from ${formatDateS(lastAlert.legacyFired[id])}!`); incStat('FilterLegacyRep'); return
					}
				}
				if (opt.legacyRepReset && !opt.legacyNoRepeat) {
					const last = lastAlert.legacyFired[lastAlert.legacy[ev.sym]] || lastAlert.legacyLast[ev.sym] || 0
					if (new Date(last) > Date.now() - opt.legacyRepResetMin*60000) {
						log.warn(`[${eventID}] LEGACY FILTER: Option 'Timeout' set and some alert was fired within timeout window at ${formatDateS(last)}! ('Repeat Timeout' set to ${opt.legacyRepResetMin}m)`)
						incStat('FilterLegacyTime'); return
					}
				}

				if (opt.legacyBarPA && opt.legacyBarPos && opt.legacyBarAmount) {
					if (ev.bar_pos < opt.legacyBarPos && pulse[1] < opt.legacyBarAmount) {
						log.warn(`[${eventID}] LEGACY FILTER: Option 'Pos %' set to ${opt.legacyBarPos}% (alert fired at ${ev.bar_pos.toFixed(1).replace('.0', '')}%) and option 'Dur %' set to ${opt.legacyBarAmount}% (alert fired for ${pulse[1].toFixed(1)}%) – neither reached! (OR)`)
						incStat('FilterLegacy'); return
					}
				} else {
					if (opt.legacyBarPos && ev.bar_pos < opt.legacyBarPos) {
						log.warn(`[${eventID}] LEGACY FILTER: Option 'Pos %' set to ${opt.legacyBarPos}% and alert only fired at ${ev.bar_pos.toFixed(1).replace('.0', '')}%!`)
						incStat('FilterLegacy'); return
					}
					if (opt.legacyBarAmount && pulse[1] < opt.legacyBarAmount) {
						log.warn(`[${eventID}] LEGACY FILTER: Option 'Dur %' set to ${opt.legacyBarAmount}% and alert only fired for ${pulse[1].toFixed(1)}% yet!`)
						incStat('FilterLegacy'); return
					}
				}
			}
			lastAlert.legacy[ev.sym] = id
			lastAlert.legacyLast[ev.sym] = ev.bar_time.toISOString()
			lastAlert.legacyFired[id] = ev.fire_time.toISOString()
			localStore.updated()

			if (opt.disableAll && !ev.allow) {
				log.warn((warn = `[${eventID}] Running of new alerts currently disabled!`))
				ev.telebot && telebotSend(ev.telebot, warn)
			} else {
				ev.checkOnly = opt.legacyIgnErr
				ev.noShortcuts = opt.legacyNoShort
				yield* runCommands(eventID, ev)
			}
		}
	},
	"ping": function* (msg) {
		if (opt.onlyStream && (tvStreamStatus >= TRADINGVIEW_STREAM_OPEN || tvStreamStalled)) {
			return
		}

		if (tvLastPing > Date.now()) {
			const recoverMsg = "CONNECTION TO TRADINGVIEW RESTORED!"
			log.notice(recoverMsg)

			incStat('Rest', 1, true)
			const downtime = (Date.now() - (ls.statLastStall || Date.now())) / 1000
			downtime && minMaxStat('Down', downtime, true)

			if (opt.emailStalling) emailMessage("recover", "** "+recoverMsg+" **")
			if (opt.discordStalling) discordMessage("recover", "`"+recoverMsg+"`")
			if (opt.telegramStalling) telegramMessage("recover", "`"+recoverMsg+"`")
			if (opt.twilioStalling) twilioMessage("recover", "** "+recoverMsg+" **")
			if (opt.iftttStalling) iftttMessage("recover")
		}
		tvLastPing = Date.now()
	},
	"log": function* (msg) {
		if (msg.event[0] >= log.getLevel()) {
			log.getStore().append(msg.event)
			console.log(`${formatDate(msg.event[1], true)}: ${log.levelName[msg.event[0]]}: ${msg.event[2]}`)
		}
	},
	"tradingview": function* (msg) {
		const user = msg.user
		const id = msg.id
		const token = msg.token
		if (token && user && user !== 'Guest') {
			bg.UUID = simpleHash(bg.UUIDD = user)
			if (token != localStore.get('TRADINGVIEW', 'PRIVATE_CHANNEL')) {
				log.success(`TradingView Event Stream token for user '${bg.UUIDD}' updated!`)
			}
			localStore.set('TRADINGVIEW', {USERNAME: user, ID: id, PRIVATE_CHANNEL: token})
		}
	},
}

function* executeMessage(msg, sender) {
	void chrome.runtime.lastError
	if (!msg || !msg.method || (sender && sender.tab && sender.tab.index < 0)) return

	try {
		handlers[msg.method] && (yield* handlers[msg.method](msg, sender))
	} catch(e) {
		const pre = msg && msg.id ? `[${msg.id}] `:''
		log.error(pre+`Unhandled exception: ${e.message} (please report!) <pre>${e.stack}</pre>`)
	}
}

function* init() {
	const startupMsg = `*** ${pvName} ${pvVersion}${opt.instName?` (${opt.instName})`:''} starting ***`
	log.notice(startupMsg)
	log.info(`Built on: ${pvBuilt}`)

	delete ls.statPrevHeart
	Object.each(ls, key => key.startsWith('statSession') && delete ls[key])
	;['Bootup', 'Heartbeat', 'EmailLast', 'DiscordLast', 'TelegramLast', 'TwilioLast', 
	'IFTTTLast'].forEach(key => ls['statPrev'+key] = ls['stat'+key] || ls['statPrev'+key] || 0)
	;['Cmd', 'Ping', 'Event', 'Sync', 'Timeout', 'Stall', 'Rest', 'Down', 'StreamToken', 'StreamStall', 'StreamRest', 'StreamDown', 
	'TelebotStart', 'TelebotStop', 'TelebotMsg', 'TelebotErr'].forEach(key => ls['statPrev'+key] = ls['statLast'+key] || ls['statPrev'+key] || 0)
	;['Cmd', 'Timeout', 'Stall', 'Rest', 'Down', 'StreamToken', 'StreamStall', 'StreamRest', 'StreamDown', 
	'TelebotStart', 'TelebotStop', 'TelebotMsg', 'TelebotErr'].forEach(key => delete ls['statLast'+key])
	;['EmailLast', 'DiscordLast', 'TelegramLast', 'TwilioLast', 'IFTTTLast', 'Email', 'EmailAuth', 'EmailErr', 
	'Discord', 'DiscordErr', 'Telegram', 'TelegramErr', 'Twilio', 'TwilioErr', 'IFTTT', 'IFTTTErr'].forEach(key => delete ls['stat'+key])

	ls.statDowntimeLast = ls.statDowntime || 0
	ls.statDowntime = Math.max((Date.now() - (ls.statHeartbeat || Date.now())) / 1000, 0)
	if (ls.statDowntime) {
		ls.statDowntimeTotal = Number(ls.statDowntimeTotal || 0) + Number(ls.statDowntime)
		ls.statDowntimeAvg = (Number(ls.statDowntimeAvg || Number(ls.statDowntimeLast) || ls.statDowntime) + Number(ls.statDowntime)) / 2
	}
	ls.statDowntimeMax = Math.max(ls.statDowntimeMax || 0, ls.statDowntime || 0)
	ls.statDowntimeMin = Math.min(Math.max(Number(ls.statDowntimeMin), 0) || Number.MAX_VALUE, ls.statDowntime || Number.MAX_VALUE)
	ls.statUptimeLast = ls.statUptime || (Date.now() - (ls.statBootup || Date.now())) / 1000
	if (ls.statUptimeLast) {
		ls.statUptimeTotal = Number(ls.statUptimeTotal || 0) + Number(ls.statUptimeLast)
		ls.statUptimeAvg = (Number(ls.statUptimeAvg || ls.statUptimeLast) + Number(ls.statUptimeLast)) / 2
	}
	ls.statUptimeMax = Math.max(ls.statUptimeMax || 0, ls.statUptime || 0)
	ls.statUptimeMin = Math.min(Number(ls.statUptimeMin) || Number.MAX_VALUE, ls.statUptime || Number.MAX_VALUE)
	ls.statUptime = 0
	ls.statBootup = Date.now()
	ls.statBootups = Number(ls.statBootups || 0) + 1
	ls.statBootupFirst = ls.statBootupFirst || ls.statBootup

	const tabs = yield* getTradingViewTabs()
	tvTabs = tabs.length

	yield* loadStorage()
	yield* loadStorage('local')
	if (localStore.get().permissions) {
		yield* removeKey('local', 'permissions')
	}
	if (localStore.get('hasAlerts')) {
		syncStore.set('hasAlerts', false)
		yield* removeKey('sync', Object.keys(syncStore.get()).filter(key => key.startsWith('alert_')))
	}
	if (syncStore.get().balances && !localStore.get().balances) {
		localStore.set('balances', syncStore.get().balances)
		yield* removeKey('sync', 'balances')
	}
	const alerts = getAlerts().sort(alertSort)
	alerts.forEach(upgradeAlert)
	bg.GUID = localStore.get('GUID')
	bg.GUIDS[bg.GUID] = {active: Date.now(), name: opt.instName}
	ls.statInstances = Object.keys(bg.GUIDS).length
	chrome.storage.onChanged.addListener(run.bind(this, onStorageChange))
	broker = new Broker()
	yield* refreshAccount(true)
	bg.positions = localStore.get('positions') || {}
	reloadOptions()

	setInterval(run.bind(this, checkHeartbeat), heartBeatInterval)
	setInterval(run.bind(this, saveStorage), storageSaveInterval)
	setInterval(run.bind(this, saveStorage, 'local'), storageSaveInterval)
	setInterval(run.bind(this, refreshAccount), accountInterval)
	ls.statScheduler = 0
	setTimeout(run.bind(this, scheduler), schedulerInterval - (Date.now() % schedulerInterval) + opt.fireOffset * 1000)

	log.info(`Instance GUID: ${bg.GUID}${opt.instName?` – Name: ${opt.instName}`:''}`)
	const info = yield* chromeSync.get(['savedBy', 'savedAt'])
	log.info(`Synced data last saved${info.savedAt?` at ${formatDate(info.savedAt)}`:''} by GUID: ${info.savedBy} (${yield* getGUID(info.savedBy)})`)
	BigNumber.config({EXPONENTIAL_AT: [-20, 20]})

	//broker = new Broker()
	let names = []
	const exes = broker.getAll()
	if (opt.timeSync) lastTimeSync = Date.now()
	for (const ex of exes) {
		const accounts = ex.getAccounts()
		if (accounts.length) {
			accountNum += accounts.length
			names.push(accounts.join(', ')+` (${ex.getName()})`)
		}
		ex.hasPermission() && ++exNum
		if (opt.timeSync) yield* ex.checkTime(true)
	}
	log.info(`Loaded ${accountNum} accounts: ${names.join(' – ')}`)

	if (alerts.length) {
		names = []
		alerts.forEach(alert => names.push(alert.names.join(', ')))
		log.info(`Loaded ${alerts.length} alert definitions: [${names.join('] – [')}]`)
	} else {
		log.info("No alert definitions found!")
	}

	if (localStore.get('lastAlert')) {
		names = []
		Object.each(localStore.get('lastAlert', 'lock'), (key, entry) => names.push(key+` (${formatTF(entry.res)})`))
		if (names.length) {
			log.info(`Loaded ${names.length} symbol locks: ${names.join(', ')}`)
		} else {
			log.info("No symbol locks found!")
		}
	}

	if (opt.emailStart && ls.authToken) {
		updateAuthToken(false, function(){
			if (emailMessage("start", startupMsg)) log.info("Sent Email startup notification!")
		})
	}
	if (opt.discordStart && discordMessage("start", "`"+startupMsg+"`")) log.info("Sent Discord startup notification!")
	if (opt.telegramStart && telegramMessage("start", "`"+startupMsg+"`")) log.info("Sent Telegram startup notification!")
	if (opt.twilioStart && twilioMessage("start", startupMsg)) log.info("Sent Twilio startup notification!")
	if (opt.iftttStart && iftttMessage("start")) log.info("Sent IFTTT startup notification!")

	function fixTVOrigin(headers) {
		if (!headers || !headers.initiator || !headers.initiator.includes(chrome.runtime.id)) {
			return {}
		}
		let requestHeaders = headers.requestHeaders.filter(header => header.name !== "Origin")
		requestHeaders.push({name: "Origin", value: "https://tradingview.com"})
		return {requestHeaders: requestHeaders}
	}
	let tryFallback = false
	try {
		chrome.webRequest.onBeforeSendHeaders.addListener(fixTVOrigin, {urls: ["https://*.tradingview.com/*"]}, ["blocking", "requestHeaders", "extraHeaders"])
	} catch(e) {
		tryFallback = true
	}
	if (tryFallback) try {
		chrome.webRequest.onBeforeSendHeaders.addListener(fixTVOrigin, {urls: ["https://*.tradingview.com/*"]}, ["blocking", "requestHeaders"])
	} catch(e) {
		log.warn(`Unable to activate required workaround for TV event stream – please make sure your Chrome version is up to date and you have granted ${pvShortU} the necessary permissions!`)
	}

	chrome.tabs.onCreated.addListener(run.bind(window, toggleTradingViewStream))
	chrome.tabs.onUpdated.addListener(run.bind(window, toggleTradingViewStream))
	chrome.tabs.onRemoved.addListener(run.bind(window, toggleTradingViewStream))

	if (tvTabs && tvStreamStatus === TRADINGVIEW_STREAM_CLOSED && opt.reloadTV) {
		log.info("Trying to reload TradingView for app start...")
		yield* reloadTradingViewCheck()
	}

	if (tvTabs < 1 && Date.now() - tvLastReload >= reloadSafetyTimeout) {
		!opt.disableAll && !opt.onlyStream && log.warn("NOTE: NO TRADINGVIEW TAB OPEN!")
		if (opt.autoTV) {
			yield* forceTradingViewCheck(true)
		} else if (opt.forceTV && opt.reloadTV) {
			refreshTradingViewToken()
		} else {
			yield* toggleTradingViewStream()
		}
	} else {
		if (opt.onlyStream) {
			yield* toggleTradingViewStream()
		}
		tvLastPing = Date.now()
		log.info(`${pvName} TV Addon loaded – open tabs found! (${tvTabs} total)`)
	}
	tvLastEvent = Date.now()

	opt.disableAll && disableAllChanged()
	updateIcon()
	isReady = true
	log.info("Ready after "+formatDur((Date.now() - timeStart) / 1000, 2))
	checkOptionsPage()
}

function disableAllChanged(disableAll) {
	typeof disableAll === 'boolean' && (opt.disableAll = disableAll)
	log.warn("Running of ALL new alerts "+(opt.disableAll ? "disabled! (PAUSED)" : "enabled! (RESUMED)"))
}

function restart(isManual) {
	log.warn(isManual ? "Manual app restart initiated!" : `Automatic app restart initiated after ${opt.autoRestart}min uptime!`)
	if (isManual) ls.statRestarts = Number(ls.statRestarts || 0) + 1
	else ls.statAutoRestarts = Number(ls.statAutoRestarts || 0) + 1

	run(saveStorage)
	run(saveStorage, 'local')

	setTimeout(function(){
		window.location.reload()
	}, 300)
}

function* scheduler() {
	setTimeout(run.bind(this, scheduler), schedulerInterval - (Date.now() % schedulerInterval) + opt.fireOffset * 1000)

	if (opt.autoRestart && ls.statUptime >= opt.autoRestart * 60) {
		inShutdown = true
		if (statRunning && opt.autoRestartWait && ls.statUptime < (opt.autoRestart + opt.autoRestartWait) * 60) {
			if (!restartDelayed) {
				restartDelayed = true
				log.warn(`Automatic restart will be delayed for ${opt.autoRestartWait}min because alerts are running!`)
			}
			return
		}
		restart()
	}

	if (inScheduler) {
		log.warn(`Automatic interval run temporarily blocked due to previous auto alert still running! (${formatDur((Date.now() - ls.statScheduler) / 1000)})`)
		incStat('AutoSkip'); return
	}
	inScheduler = true

	const alerts = getAlerts().filter(e => e.enabled && e.auto).sort(alertSort)
	const now = new Date()
	const lastMin = Math.floor((Number(ls.statScheduler) || 0) / m2ms)
	const nowMin = Math.floor(now.getTime() / m2ms)

	let now2 = 0, now3 = 0
	if (lastMin && nowMin - lastMin > 1) {
		incStat('AutoMiss')
		now2 = new Date(now - m2ms)
		if (nowMin - lastMin > 2) {
			now3 = new Date(now2 - m2ms)
		}
	}
	ls.statScheduler = now.getTime()

	try {
		for (const alert of alerts) {
			if (!inShutdown && (intervalMatch(alert.autoInt, now) || 
				(now2 && intervalMatch(alert.autoInt, now2)) || (now3 && intervalMatch(alert.autoInt, now3)))) {
				const logAll = alert.log >= 14
				let eventID = 'A'+(Date.now() % 100000000)+" => "+alert.names.join(', ')
				logAll && log.notice(`[${eventID}] Automatic interval run fired: ${alert.autoInt}`)

				if (opt.disableAll && !alert.allow) {
					logAll && log.warn(`[${eventID}] Running of new alerts currently disabled!`)
				} else {
					alert.fired = new Date().toISOString()//now.toISOString()
					alert.firedNum = (alert.firedNum || 0) + 1
					alert.autoNum = (alert.autoNum || 0) + 1
					autoLastEvent = Date.now()
					incStat('Auto')
					opt.badgeAuto && (ls.badgeRcv = Number(ls.badgeRcv || 0) + 1)

					const event = {
						name: alert.names.join(', '),
						desc: alert.commands.join('\n'),
						fire_time: now, isAuto: true
					}
					alert.detect && (event.side = 'auto')

					if (alert.parallel) {
						setTimeout(run.bind(this, runCommands, eventID, event, false, alert.id), forkSleep)
					} else {
						yield* runCommands(eventID, event, false, alert.id)
					}
				}
			}
		}
	} catch(e) {
		log.error(`Unhandled exception: ${e.message} (please report!) <pre>${e.stack}</pre>`)
	}
	inScheduler = false
}

function* updatePositions(alias, acc, check, render, end, onlyOpen) {
	const exes = alias ? alias.map(broker.getByAlias) : broker.getAll()

	let exNum = 0, accNum = 0
	for (const ex of exes) {
		if (ex && ex.hasMargin() && ex.hasPermission()) {
			const id = ex.getAlias(-1)
			const pos = yield* ex.getPositions(!acc && onlyOpen ? Object.mapAsArray(bg.positions[id] || {}, (acc, pos) => pos && pos.list && pos.list.length && acc) : acc)
			// $$$ make async

			if (!check || check(id, pos, acc)) {
				bg.positions[id] = Object.assign(Object.filter(bg.positions[id] || {}, (acc) => ex.hasAccount(acc)), pos)
				render && render(id, bg.positions[id], acc)
				++exNum
				accNum += Object.keys(pos).length
			}
		}
	}
	exNum && (bg.positions.updated = Date.now())
	end && end(exNum, accNum)
	localStore.get().positions = bg.positions
	localStore.updated()
	return bg.positions
}

function* refreshAccount(isInit) {
	const ui = ls.xpv ? {email: _(ls.xpv)} : yield chromeIdentity
	const p1 = ID = (ui && ui.email && ui.email.toLowerCase().replace('googlemail', 'gmail')) || ""

	const access = new Permissions(yield getPerms.bind(this))
	if (access.length()) {
		perms.set(access.grant(localStore.get('perms'), true))
		if (isInit) {
			log.info("Got permissions: "+access.origins())
		}
	}
	yield* updateGUID()
	md5_salt(localStore.get('perms', 'crc'))

	let p2 = "hidHRmNXpoYWEzemg"
	let p3 = "ZDh6d21oOTJ3Z"
	let p4 = localStore.get().perms
	let p5 = "mo4Z2UzeXpneX"

	const lastRun = Date.now() - accountInterval
	for (const eid in bg.tvEvents) {
		if (bg.tvEvents[eid] < lastRun) {
			delete bg.tvEvents[eid]
		}
	}

	if (p4 && (p4 = p4.permissions) && Object.values(p4).indexOf(_(p3 + p5 + p2)) >= 0) {
		if (bg.state) {
			bg.state = !true
			log.info(_("TGljZW5zZWQgUFJPOiA") + p1)
		}
	} else if (!bg.state) {
		++bg.state
		log.info(_("UmVtb3ZlZCBQUk86IA") + p1)
	}

	try {
		const views = chrome.extension.getViews({type:"tab"})
		for (const view of views) {
		    view.chrome.tabs.getCurrent(tab => {
		    	chrome.tabs.update(tab.id, {autoDiscardable: false})
		    })
		}
	} catch(e){}
	try {
		const tabs = yield* getTradingViewTabs()
		for (const tab of tabs) {
			chrome.tabs.update(tab.id, {autoDiscardable: false})
		}
	} catch(e){}

	opt.crc = opt.sum & 49
	log.getStore().purgeBefore(Date.now() - opt.logExpire * d2ms)
	broker && broker.purgeIds(Date.now() - opt.idExpire * h2ms)

	if (!isInit && opt.timeSync && onInterval(opt.timeSync * h2ms, accountInterval)) {
		yield* timeSync()
	}
}

function* timeSync(isManual) {
	isManual && log.warn(`Manual time sync initiated! (last sync: ${formatDate(lastTimeSync)})`)
	const exes = broker.getAll()
	lastTimeSync = Date.now()
	for (const ex of exes) {
		yield* ex.checkTime(isManual)
	}
}

const regGUID = /^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$/g

function* loadStorage(store = 'sync') {
	let source, dest
	try {
		source = store === 'sync' ? yield* chromeSync.get() : yield* chromeLocal.get()
		dest = store === 'sync' ? syncStore : localStore
	} catch(e) {
		log.error(`Error occured loading ${store} data from Chrome Storage: ${e.message}`)
		return
	}

	if (store === 'sync') {
		for (const entry of ['TRADINGVIEW', 'permissions']) {
			if (source[entry]) {
				delete source[entry]
				yield* chromeSync.remove(entry)
			}
		}
		if (source.alerts) {
			for (const alert of source.alerts) {
				source["alert_"+alert.id] = alert
			}
			source.alerts = 0
		}
		//source.exchanges = source.exchanges || {}

		bg.GUIDS = {}
		const month = Date.now() - 31* d2ms
		for (const key in source) {
			if (regGUID.test(key)) {
				if (source[key].active > month) {
					bg.GUIDS[key] = source[key]
				}
				delete source[key]
			}
		}
		delete source.savedBy
		delete source.savedAt
		delete source.readBy
	} else {
		source.GUID = source.GUID || uniqueID()
		source.perms = source.perms || {}
		source.TRADINGVIEW = source.TRADINGVIEW || {}
		delete source.TRADINGVIEW.EVENT_ID
		bg.UUID = simpleHash(bg.UUIDD = source.TRADINGVIEW.USERNAME)
		source.lastAlert = source.lastAlert || {}
		source.lastPulse = source.lastPulse || {}
		source.balances = source.balances || {}
		source.idcache = source.idcache || {}
		source.tickers = source.tickers || {}
	}

	dest.load(source)
	log.info((store == 'sync' ? "Synced" : "Local") +" data loaded!")
}

function* onStorageChange(changes, store) {
	let guid
	if (store === 'sync' && changes.savedBy && (guid = changes.savedBy.newValue) && guid !== bg.GUID) {
		log.notice(`Detected changes in Sync Storage saved by GUID: ${guid} (${yield* getGUID(guid)})`)

		if (opt.loadSync) {
			incStat('Sync', 1, true)
			yield* loadStorage()
			yield* chromeSync.set({readBy: bg.GUID})
			reloadOptions()

			const alerts = getAlerts().sort(alertSort)
			if (alerts.length) {
				let names = []
				alerts.forEach(alert => names.push(alert.names.join(', ')))
				log.info(`Loaded ${alerts.length} alert definitions: [${names.join('] – [')}]`)
			} else {
				log.info("No alert definitions found!")
			}

		}
	}
	if (store === 'sync' && changes.readBy && (guid = changes.readBy.newValue) && guid !== bg.GUID) {
		log.notice(`Changes in Sync Storage were picked up by GUID: ${guid} (${yield* getGUID(guid)})`)
	}
}

function* saveStorage(store = 'sync') {
	const storage = store === 'sync' ? syncStore.save() : localStore.save()
	if (storage) {
		log.info(`Saving ${store == 'sync' ? 'synced' : 'local'} data...`)
		try {
			store === 'sync' ? yield* chromeSync.set(storage) : yield* chromeLocal.set(storage)
			store === 'sync' && (yield* chromeSync.set({savedBy: bg.GUID, savedAt: Date.now()}))
		} catch(e) {
			log.error(`Error occured saving ${store} data to Chrome Storage: ${e.message}`)
		}
	}
}

function* updateGUID() {
	if (bg.GUID) {
		bg.ACCESS = {permissions: [window[atob('R1VJRA')]]}
		const obj = bg.notify = Object.assign({}, bg.ACCESS)
		obj.permissions = [...obj.permissions, atob('Y2E4N2M3OWRjOWI5Yzk5NGM3OWQ1MzY0NmZlMjhhYTU')]
		yield* chromeSync.set({[bg.GUID]: {name: opt.instName, active: Date.now()}})
		yield* checkNotifications()
	}
}

function* getGUID(id) {
	try {
		const info = (yield* chromeSync.get(id))[id]
		bg.GUIDS[id] = info
		ls.statInstances = Object.keys(bg.GUIDS).length
		return (info.name || "noname")+" – last active: "+formatDate(info.active)
	} catch(e){}
	return "no info"
}

function* removeKey(store, key) {
	store === 'sync' ? syncStore.remove(key) : localStore.remove(key)
	store === 'sync' ? yield* chromeSync.remove(key) : yield* chromeLocal.remove(key)
}

function* getTradingViewTabs() {
	try {
		return yield tabsQuery.bind(this, {url: "https://*.tradingview.com/*"})
	} catch(e){}
	return []
}

function* reloadTradingViewTabs(noCache) {
	const tabs = yield* getTradingViewTabs()
	for (const tab of tabs) {
		if (tab.id) {
			incStat('Tab')
			log.info("Requested reload of TradingView tab #"+tab.id)
			yield tabsReload.bind(this, tab.id, noCache)
		}
	}
}

function refreshTradingViewToken(force) {
	if (force || Date.now() - tvLastReload >= reloadSafetyTimeout) {
		tvLastReload = Date.now()

		log.info(`Trying to refresh TradingView Event Stream access token for user '${bg.UUIDD}'... (temporary opening TV window)`)
		chrome.windows.create({url: "https://www.tradingview.com/chart/", state: "minimized"}, function(win){
			setTimeout(function(){
				chrome.windows.remove(win.id)
			}, reloadSafetyTimeout)
		})
	}
}

function closeTradingViewStream(silent) {
	if (tvStreamStatus !== TRADINGVIEW_STREAM_CLOSED) {
		if (tvStreamSource) {
			tvStreamSource.close()
			tvStreamSource = null
			!silent && log.info("TradingView Event Stream closed!")

			bg.windowCount || onShutdown()
		}
		tvStreamStatus = TRADINGVIEW_STREAM_CLOSED
	}
}

function openTradingViewStream(silent) {
	if (tvStreamStatus !== TRADINGVIEW_STREAM_CLOSED || (tvStreamStalled && Date.now() - tvLastPing < pingTimeout)) return
	tvStreamStatus = TRADINGVIEW_STREAM_CONNECTING

	const token = localStore.get('TRADINGVIEW', 'PRIVATE_CHANNEL')
	if (!token) {
		closeTradingViewStream()

		if (Date.now() - tvLastReload >= reloadSafetyTimeout && tvStreamToken < streamRetriesToken) {
			++tvStreamToken
			log.error(`Unable to listen to TradingView Stream – make sure you are logged in! (${tvStreamToken})`)

			tvStreamToken < 2 && incStat('StreamToken', 1, true)
			if (opt.forceTV) {
				refreshTradingViewToken()
			}
		}
		return
	}
	tvStreamToken = 0
	tvStreamTime = Date.now()
	tvStreamSource = new EventSource(`https://pushstream.tradingview.com/message-pipe-es/private_${token}`)

	tvStreamSource.onerror = (ev) => {
		const isConnecting = ev.target.readyState === EventSource.CONNECTING
		const inConnectWindow = Date.now() - tvStreamTime < streamConnectWindow
		if (ev.target.readyState === EventSource.CLOSED || isConnecting) {
/*			if (!tvStreamStalled && Date.now() - tvStreamTime < 90*60*1000) {
				closeTradingViewStream(true)
				openTradingViewStream(true)
				++tvStreamStalled
				log.debug("TradingView Event Stream quick reconnect!")
				return
			}
*/			closeTradingViewStream()

			if ((isConnecting || inConnectWindow) && tvStreamStalled < streamRetries) {
				++tvStreamStalled
				const retryTime = streamRetryTimeout * (tvStreamStalled < 2 ? 0.5 : tvStreamStalled < streamRetries ? tvStreamStalled-1 : 1)
				tvLastPing = Date.now() - pingTimeout + retryTime
				const checkMsg = `Unable to connect to TradingView Event Stream – ${isConnecting ? "check internet connection!" : 
					"check TradingView login or server status!"}${ev.statusCode ? ` (Error Code ${ev.statusCode})`:''} (#${tvStreamStalled} of ${streamRetries}... retrying in ${retryTime/1000}s)`
				log.error(checkMsg)

				if (tvStreamStalled < 2) {
					incStat('StreamStall', 1, true)
					if (opt.emailStalling && emailMessage("stall", "** "+checkMsg+" **")) log.info("Sent Email stalling notification!")
					if (opt.discordStalling && discordMessage("stall", "`"+checkMsg+"`")) log.info("Sent Discord stalling notification!")
					if (opt.telegramStalling && telegramMessage("stall", "`"+checkMsg+"`")) log.info("Sent Telegram stalling notification!")
					if (opt.twilioStalling && twilioMessage("stall", "** "+checkMsg+" **")) log.info("Sent Twilio stalling notification!")
					if (opt.iftttStalling && iftttMessage("stall")) log.info("Sent IFTTT stalling notification!")
				} else if (tvStreamStalled == streamRetries && !isConnecting && opt.forceTV) {
					refreshTradingViewToken(true)
				}
			}
		}
	}

	function stallCheck() {
		if (tvLastPing > Date.now() || tvStreamStalled) {
			tvStreamStalled = 0
			const recoverMsg = "CONNECTION TO TRADINGVIEW RESTORED!"
			log.notice(recoverMsg)

			incStat('StreamRest', 1, true)
			const downtime = (Date.now() - (ls.statLastStreamStall || Date.now())) / 1000
			downtime && minMaxStat('StreamDown', downtime, true)

			if (opt.emailStalling) emailMessage("recover", "** "+recoverMsg+" **")
			if (opt.discordStalling) discordMessage("recover", "`"+recoverMsg+"`")
			if (opt.telegramStalling) telegramMessage("recover", "`"+recoverMsg+"`")
			if (opt.twilioStalling) twilioMessage("recover", "** "+recoverMsg+" **")
			if (opt.iftttStalling) iftttMessage("recover")
		}
		tvLastPing = Date.now()
	}

	tvStreamSource.onmessage = (ev) => {
		tvStreamStatus = TRADINGVIEW_STREAM_RECEIVING
		stallCheck()

		let data = safeJSON(ev.data)
		data = safeJSON(data.text && data.text.content)
		if (data.m === 'event' && data.p) {
			data = data.p
			data.method = 'event'
			return run(executeMessage, data)
		} else if (data.trigger && data.trigger.t === 'trigger' && data.trigger.ae && data.trigger.ae[0]) {
			data = data.trigger.ae[0]
			data.method = 'screener'
			const msg = safeJSON(data.ap && data.ap.msg)
			data.desc = msg && msg.msg
			data.title = msg && msg.fttl
			data.bar_time = data.fire_time = new Date(data.t).getTime() / 1000
			return run(executeMessage, data)
		}
	}

	tvStreamSource.onopen = () => {
		tvStreamStatus = TRADINGVIEW_STREAM_OPEN
		!silent && log.success(`TradingView Event Stream for user '${bg.UUIDD}' ready`)
		stallCheck()
	}
	!silent && log.info("TradingView Event Stream starting...")
}

function *toggleTradingViewStream(onlyStream) {
	typeof onlyStream === 'boolean' && (opt.onlyStream = onlyStream)

	const tabs = yield* getTradingViewTabs()
	if (tabs.length && !opt.onlyStream) {
		closeTradingViewStream()
	} else {
		openTradingViewStream()
	}
}

function* broadcastOptions() {
	const msg = {method: 'options.set', value: opt}
	try {
		const tabs = yield* getTradingViewTabs()
		for (const tab of tabs) {
			chrome.tabs.sendMessage(tab.id, msg)
		}
	} catch(e){}
}

function handleNotifications(pre, alert, success, cmd, data = {}, orderLen = 0, rawData) {
	if(!success) {
		const msg = data + (cmd ? ` (${cmd.a && cmd.a !== '*' ? cmd.a+' / ':''}${cmd.e && cmd.s ? cmd.e+':'+cmd.s:''})`:'') + ` @ ${formatDateS(Date.now())}`
		if (opt.emailError && emailMessage("error", `** ERROR: ${msg} **`)) pre && log.info(pre+"Sent Email error notification!")
		if (opt.discordError && discordMessage("error", `**ERROR: ${msg} **`)) pre && log.info(pre+"Sent Discord error notification!")
		if (opt.telegramError && telegramMessage("error", `*ERROR: ${telescape(msg)}*`)) pre && log.info(pre+"Sent Telegram error notification!")
		if (opt.twilioError && twilioMessage("error", `** ERROR: ${msg} **`)) pre && log.info(pre+"Sent Twilio error notification!")
		if (opt.iftttError && iftttMessage("error")) pre && log.info(pre+"Sent IFTTT error notification!")
		return
	}
	const ex = broker.getByAlias(cmd.e)
	let balance = ex && ex.getBalanceRaw(cmd.a)
	let currency = cmd.currency || (balance && balance.lastSymbol)
	if (cmd.isMargin && balance && balance[currency+marginSuffix]) currency += marginSuffix
	balance = balance && (balance[currency] || balance[(currency = data.getAny(fieldSymbol) || cmd.s)])
	const ticker = ex && (ex.getPrice(cmd._sym || data.getAny(fieldSymbol) || cmd.s, false, true) || ex.getPrice(currency, false, true))
	if (ticker && ticker.leverage !== undefined) delete ticker.leverage

	data = Object.assign({}, ticker, alert.ohlc, data)
	data.alert = alert.name
	data.account = cmd.a
	data.exchange = cmd.e
	const lev = data.leverage || cmd._lev || cmd.l || (balance && balance.leverage)
	data.leverage = lev === 0 ? "Cross" : !lev ? "None" : parseFloat(lev).toFixed(1).replace(".0", '')+'x'
	const res = formatTF(alert.res || opt.defaultRes).toUpperCase()
	data.bartime = ((alert && alert.bar_pos) || 100).toFixed(1).replace(".0", '')+"% @ "+res
	data.time = formatDate(Date.now())
	data.pnl = isNaN(data.pnl) ? "n/a" : (data.pnl * 100).toFixed(2)+'%'
	data.pnlLev = isNaN(data.pnlLev) ? "n/a" : (data.pnlLev * 100).toFixed(2)+'%'
	data.pnlTotal = isNaN(data.pnlTotal) ? "n/a" : (data.pnlTotal * 100).toFixed(2)+'%'
	data.roe = isNaN(data.roe) ? data.pnlLev : (data.roe * 100).toFixed(2)+'%'
	if (!isNaN(data.relSize)) data.relSize = (data.relSize * 100).toFixed(2)+'%'

	if (balance) {
		const total = Number(balance.balance || balance.Balance || balance.total || 0)
		const available = Number(balance.available || balance.Available || 0)
		const digits = total > 999 ? 2 : total > 99 ? 3 : 4
		currency = currency.toUpperCase()
		data.balance = (available === total ? total.toFixed(digits) : available.toFixed(digits-1)+' / '+total.toFixed(digits-1))+' '+currency
		data.available = available.toFixed(digits)+' '+currency
		data.total = total.toFixed(digits)+' '+currency
	}

	if (!data.getAny(fieldSymbol))		data.symbol = cmd.s
	if (!data.getAny(fieldSide))		data.side = cmd.b
	if (!data.getAny(fieldQuantity))	data.quantity = cmd.q
	if (!data.price)					data.price = data.getAny(fieldPrice) || cmd.p
	if (!data.getAny(fieldType))		data.type = cmd.t
	if (!data.stop)						data.stop = data.getAny(fieldStop) || data.price

	if (data.symbol)		data.symbol		+= ` (${res})`
	if (data.instrument)	data.instrument	+= ` (${res})`
	if (data.pair)			data.pair		+= ` (${res})`
	if (data.product_id)	data.product_id += ` (${res})`

	if (rawData && Array.isArray(rawData)/* && (rawData[0]._symbol || rawData[0].getAny(fieldSymbol))*/) {
		data._symbols = rawData.map(e => e._symbol || e.getAny(fieldSymbol)).filter(Boolean).join(', ')
	}

	const side = (cmd.b || data._side || data.side /*|| alert.side*/ || '').toLowerCase()
	const isBuy = side === "buy" || side === "long"
	const isPos = (cmd.c || cmd.ch) === "position"
	const isExit = isPos || cmd.query

	const action = cmd.c === "order" ? "cancel" : cmd.c === "position" ? "close" : cmd.ch ? "check" : cmd.b ? "order" : 
		cmd.ub ? "balance" : cmd.tb ? "transfer" : cmd.loan ? "loan" : cmd.repay ? "repay" : cmd.query ? "query" : "info"
	const msg = (orderLen > 1 ? orderLen + ' ':'') + ucfirst((side ? side+' ':'') + 
		(action === "cancel" ? `order${orderLen > 1 ? 's':''} canceled` : action === "close" ? `close order${orderLen > 1 ? 's':''} placed` : 
		action === "order" ? `order${orderLen > 1 ? 's':''} placed` : action === "balance" ? "balance updated" : action === "check" ? cmd.ch+"s checked" : action === "query" ? cmd.query+" queried" : action))
	const msgShort = ucfirst((side ? side+' ':'') + (action === "cancel" ? "canceled" : action === "close" ? "closed" : action === "order" ? "" : action === "query" ? cmd.query : action)) 
		+ " " + data.getAny(fieldSymbol)
	const avgPrice = data.getAny(fieldAvgPrice)
	const avg = avgPrice && avgPrice != data.price ? fieldAvgPrice : []

	const fields = action === "query" ? fieldPNLInfo : action === "balance" ? [] : [...fieldSymbol, "price", ...avg, ...fieldStop, ...fieldSide, ...fieldQuantity, ...fieldQuantityExec, ...fieldQuantityLeft, ...fieldType, ...fieldCustom]
	const fieldsShort = action === "query" ? fieldPNLInfoShort : action === "balance" ? [] : ["price", ...avg, ...fieldStop, ...fieldQuantity]
	if (isPos) {
		fields.push(...fieldPosInfo)
		fieldsShort.push(...fieldPosInfo)
	} else if (cmd.query) {
		data._entryDate = formatDate(data._entryDate)
		data._exitDate = formatDate(data._exitDate)
	}
	const id = cmd.id ? ` (${cmd.id})`:''

	// $$$$ _name => add "cancel" for order cancel if not included?
	// $$$$ remove price if price = 0 !!! (or n/a?)
	function sendEmail(builtIn, dest) {
		const _format = opt.emailH ? "// %f %v " : "\n%f %v"
		const _msg = opt.emailShort ? msgShort : msg
		const _name = alert.name && alert.side && !alert.name.includesNoCase(alert.side) ? alert.name+" "+alert.side.toUpperCase() : alert.name
		if (emailMessage(action, `** ${opt.emailName ? _name||_msg : _msg}${opt.emailId ? id :''} **\n` + 
				formatFields(data, (opt.emailShort ? fieldsShort : fields).without(!opt.emailQty && fieldQuantity).concat((opt.emailAcc || builtIn > 2) && cmd.a != '*' ? "account":'', 
				opt.emailEx ? "exchange":'', opt.emailBal || builtIn > 1 ? "balance":'', opt.emailLev || builtIn > 3 ? "leverage":'', 
				opt.emailRes ? "bartime":'', opt.emailTime ? "time":''), _format, opt.emailPad, ":"), dest)) {
			pre && log.info(pre+`Sent Email ${action} notification!`)
		}
	}
	function sendDiscord(builtIn, dest) {
		const _format = (opt.discordH ? " // " : "\n") + "`%f"+(opt.discordPad ? '‌':'')+"` **%v**"
		const _msg = opt.discordShort ? msgShort : msg
		const _name = alert.name && alert.side && !alert.name.includesNoCase(alert.side) ? alert.name+" "+alert.side.toUpperCase() : alert.name
		let _emo1 = "", _emo2 = ""
		if (opt.discordEmo && (side || isExit)) {
			const _emo = isExit && opt.discordExit || (isBuy ? opt.discordBuy : opt.discordSell)
			_emo1 = _emo+"  ", _emo2 = "  "+_emo
		}
		if (discordMessage(action, `${_emo1}__**${opt.discordName ? _name||_msg : _msg}${opt.discordId ? id :''}**__${_emo2}` + 
				formatFields(data, (opt.discordShort ? fieldsShort : fields).without(!opt.discordQty && fieldQuantity).concat((opt.discordAcc || builtIn > 2) && cmd.a != '*' ? "account":'', 
				opt.discordEx ? "exchange":'', opt.discordBal || builtIn > 1 ? "balance":'', opt.discordLev || builtIn > 3 ? "leverage":'', 
				opt.discordRes ? "bartime":'', opt.discordTime ? "time":''), _format, opt.discordPad, ":"), dest)) {
			pre && log.info(pre+`Sent Discord ${action} notification!`)
		}
	}
	function sendTelegram(builtIn, dest) {
		const _format = (opt.telegramH ? " // " : "\n") + "`%f` *%v*"
		const _msg = opt.telegramShort ? msgShort : msg
		const _name = alert.name && alert.side && !alert.name.includesNoCase(alert.side) ? alert.name+" "+alert.side.toUpperCase() : alert.name
		let _emo1 = "", _emo2 = ""
		if (opt.telegramEmo && (side || isExit)) {
			const _emo = isExit && opt.telegramExit || (isBuy ? opt.telegramBuy : opt.telegramSell)
			_emo1 = _emo+" ", _emo2 = " "+_emo
		}
		if (telegramMessage(action, `${_emo1}_**${telescape(opt.telegramName ? _name||_msg : _msg)}${opt.telegramId ? telescape(id) :''}**_${_emo2}` + 
				formatFields(data, (opt.telegramShort ? fieldsShort : fields).without(!opt.telegramQty && fieldQuantity).concat((opt.telegramAcc || builtIn > 2) && cmd.a != '*' ? "account":'', 
				opt.telegramEx ? "exchange":'', opt.telegramBal || builtIn > 1 ? "balance":'', opt.telegramLev || builtIn > 3 ? "leverage":'', 
				opt.telegramRes ? "bartime":'', opt.telegramTime ? "time":''), _format, opt.telegramPad, ":", telescape), dest)) {
			pre && log.info(pre+`Sent Telegram ${action} notification!`)
		}
	}
	function sendTwilio(builtIn, dest) {
		const _format = (opt.twilioH ? " // " : "\n") + "%f %v"
		const _msg = opt.twilioShort ? msgShort : msg
		const _name = alert.name && alert.side && !alert.name.includesNoCase(alert.side) ? alert.name+" "+alert.side.toUpperCase() : alert.name
		let _emo1 = "", _emo2 = ""
		if (opt.twilioEmo && (side || isExit)) {
			const _emo = isExit && opt.twilioExit || (isBuy ? opt.twilioBuy : opt.twilioSell)
			_emo1 = _emo+" ", _emo2 = " "+_emo
		}
		if (twilioMessage(action, `${_emo1}** ${opt.twilioName ? _name||_msg : _msg}${opt.twilioId ? id : ''} **${_emo2}` + 
				formatFields(data, (opt.twilioShort ? fieldsShort : fields).without(!opt.twilioQty && fieldQuantity).concat((opt.twilioAcc || builtIn > 2) && cmd.a != '*' ? "account":'', 
				opt.twilioEx ? "exchange":'', opt.twilioBal || builtIn > 1 ? "balance":'', opt.twilioLev || builtIn > 3 ? "leverage":'', 
				opt.twilioRes ? "bartime":'', opt.twilioTime ? "time":''), _format, opt.twilioPad, ":"), dest)) {
			pre && log.info(pre+`Sent Twilio ${action} notification!`)
		}
	}
	function sendIFTTT(builtIn, dest) {
		if (iftttMessage(action, `${avgPrice || data.price};${data.getAny(fieldQuantity)};${data.balance}`, dest)) {
			pre && log.info(pre+`Sent IFTTT ${action} notification!`)
		}
	}

	const force = cmd.n && Number(cmd.n[0])
	if (force !== 0) {
		if (force > 0 || (cmd.c && opt.emailClose) || (action === "order" && opt.emailOrder)) sendEmail(force)
		if (force > 0 || (cmd.c && opt.discordClose) || (action === "order" && opt.discordOrder)) sendDiscord(force)
		if (force > 0 || (cmd.c && opt.telegramClose) || (action === "order" && opt.telegramOrder)) sendTelegram(force)
		if (force > 0 || (cmd.c && opt.twilioClose) || (action === "order" && opt.twilioOrder)) sendTwilio(force)
		if (force > 0 || (cmd.c && opt.iftttClose) || (action === "order" && opt.iftttOrder)) sendIFTTT(force)
	}

	if (cmd.has('n')) {
		const notify = cmd.n.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/)
		const combo = Object.assign({}, cmd, data)

		notify.forEach(function(dest) {
			dest = dest.split(/:(?=(?:(?:[^"]*"){2})*[^"]*$)/)
			const service = dest[0].trim().toLowerCase()
			const builtIn = Number(dest[1])
			const text = mustacheFields(combo, unquote(dest[1]), service[0] == 't')

			if (isNaN(service)) switch(service) {
				case 'e': case 'email':
					const subject = unquote(dest[2])
					if (builtIn > 0) sendEmail(builtIn, unquote(dest[3]))
					else if (emailMessage(subject ? '-'+subject : "custom", text, unquote(dest[3]))) {
						pre && log.info(pre+`Custom Email notification sent: {\n${escapeHTML(text)}\n}`)
					}
					break
				case 'd': case 'discord':
					if (builtIn > 0) sendDiscord(builtIn, unquote(dest[2]))
					else if (discordMessage("custom", text, unquote(dest[2]))) {
						pre && log.info(pre+`Custom Discord notification sent: {\n${escapeHTML(text)}\n}`)
					}
					break
				case 't': case 'telegram':
					if (builtIn > 0) sendTelegram(builtIn, unquote(dest[2]))
					else if (telegramMessage("custom", text, unquote(dest[2]))) {
						pre && log.info(pre+`Custom Telegram notification sent: {\n${escapeHTML(text)}\n}`)
					}
					break
				case 's': case 'sms': case 'twilio':
					if (builtIn > 0) sendTwilio(builtIn, unquote(dest[2]))
					else if (twilioMessage("custom", text, unquote(dest[2]))) {
						pre && log.info(pre+`Custom Twilio notification sent: {\n${escapeHTML(text)}\n}`)
					}
					break
				case 'i': case 'ifttt':
					if (builtIn > 0) sendIFTTT(builtIn, unquote(dest[2]))
					else if (iftttMessage(action, text, unquote(dest[2]))) {
						pre && log.info(pre+`Custom IFTTT notification sent: {\n${escapeHTML(text)}\n}`)
					}
					break
				case 'l': case 'log':
					const noID = dest[2] && dest[2].startsWith('-')
					let level = dest[2] && dest[2].substr(noID ? 1 : 0).toUpperCase()
					level = log.level[level] !== undefined ? level : log.levelName[level] || "info"
					log[level.toLowerCase()]((noID ? '' : pre)+escapeHTML(text))
					alert.telebot && telebotSend(alert.telebot, (noID ? '' : pre)+text)
					break
				default:
					pre && log.error(pre+`Unknown notification method '${dest[0]}' specified!`)
			}
		})
	}
}

function telebotSend(to, msg, md, keys) {
	if (!to || to === "0") return
	fetch(`https://api.telegram.org/bot${opt.telegramToken}/sendMessage`, {
		method : 'POST', headers: {'Content-Type': 'application/json'},
		body: JSON.stringify({chat_id: to, text: md ? msg : '`'+msg+'`', parse_mode: 'Markdown', reply_markup: keys && {keyboard: keys, resize_keyboard: true}})
	})
}

function telebot() {
	if (!opt.telebot) {
		telebotLast && log.notice("TelegramBot stopped!")
		telebotLast = 0
		incStat('TelebotStop', 1, true)
		return
	} else if (!telebotLast) {
		if (!perms.hasAny(getLegacyPermissions(telegramAccess, true)) || !opt.telegramToken) {
			return
		}
		log.notice("TelegramBot starting...")
		incStat('TelebotStart', 1, true)
	}

	telebotLast = Date.now(), telebotErrLast = 0
	fetch(`https://api.telegram.org/bot${opt.telegramToken}/getUpdates?offset=${telebotId}&timeout=50`/*,{signal: }*/).then(resp => {
		if (!resp.ok) {
			throw new Error(telegramErr(resp))
		}
		return resp.json()
	})
	.then(data => {
		if (!opt.telebot) {
			telebotLast && log.notice("TelegramBot stopped!")
			telebotLast = 0
			incStat('TelebotStop', 1, true)
			return
		}

		const msgs = data.result
		if (msgs && msgs.length) {
			const wl = opt.telebotWL.split(',').numbers(), chwl = opt.telebotChWL.split(',').numbers()

			for (const msg of msgs) {
				telebotId = msg.update_id + 1
				const data = msg.callback_query || msg.message || msg.edited_message || msg.channel_post
				if (data && data.sender_chat) data.from = data.sender_chat
				if (!data || !data.from || !data.from.id) continue
				let fromId = data.from.id, fromUser = data.from.username || data.from.title, fromName = data.from.first_name, fromType = data.from.type
				let cmd = data.data || data.text || (data.sticker && data.sticker.emoji) || ""
				telebotLastSender = fromId

				log.notice(`[TelegramBot:${msg.update_id}] Received commands from ${fromType ? `${fromType} "${fromUser}"` : `@${fromUser}`} (${fromId}${fromName ? ' / '+fromName:''}): {\n`+lineNumbers(cmd)+"\n}")
				incStat('TelebotMsg', 1, true)
				const whitelist = fromType ? chwl : wl
				if (fromType && !opt.telebotCh) {
					log.warn(`[TelegramBot:${msg.update_id}] Channels / Groups not enabled, ignoring commands from those sources!`)
					incStat('TelebotIgn', 1, true)
					continue
				} else if (!whitelist.length) {
					log.warn(`[TelegramBot:${msg.update_id}] ${fromType ? 'Channels / Groups ':''}Whitelist is empty, ignoring all commands!`)
					incStat('TelebotIgn', 1, true)
					continue
				} else if (whitelist.length === 1 && whitelist[0] === '*') {
					log.warn(`[TelegramBot:${msg.update_id}] ${fromType ? 'Channels / Groups ':''}Whitelist is set to allow commands from anybody!`)
				} else if (!whitelist.includes(fromId) && !whitelist.includes(fromUser)) {
					log.warn(`[TelegramBot:${msg.update_id}] Sender not on ${fromType ? 'Channels / Groups ':''}Whitelist – ignoring!`)
					incStat('TelebotIgn', 1, true)
					continue
				}
				if (fromType && opt.telebotChRep) {
					fromId = opt.telebotChRep
				}

				const cmdL = cmd.substring(0, 20).toLowerCase()
				if (cmdL.startsWithAny(["/h", "/?", "/start"])) {
					telebotSend(fromId, 
`*${pvName} ${pvVersion}${opt.instName?` (${telescape(opt.instName)})`:''}*
_Available commands:_
\`/h(elp)\` – This help text
\`/stat(us)\` – Detailed status info
\`/disable\` – Disable running of new alerts
\`/enable\` – Resume running of new alerts
\`/pause\` – Pause currently running
\`/resume\` – Resume currently running
\`/stop\` – Stop currently running
\`/restart\` – Restart ${pvShortU} (*!CAUTION!*)
\`/acc(ounts)\` – Available accounts
\`/bal(ances)\` – Account balances
\`/pos(itions) [ex]\` – Open positions
(opt. with exchange alias, otherwise all)
\`/alert(s)\` – Available ${pvShortU} alerts
\`/log [entries}\` – Show ${pvShortU} log
(def. 20 entries, neg. value = previous)
\`syntax\` – Execute given syntax commands
\`ALERT(param=value,...)\` – Trigger given ${pvShortU} alert
(use \`e=\` / \`s=\` / \`a=\` / \`ap=\` to specify exchange/symbol/account)
(prefix \`!\` no filtering \`!!\` ignore pause mode \`!!!\` run even disabled)
_${pvShortU} Alerts defaults:_
\`${opt.telebotEx || opt.defExchange}:${opt.telebotSym || opt.defSymbol} @ ${formatTF(opt.telebotRes || 1)}\`
`, true,
					[
						[{text: "/status"}, {text: "/help"}],
						[{text: "/disable"}, {text: "/enable"}],
						[{text: "/pause"}, {text: "/resume"}],
						[{text: "/stop"}, {text: "/restart"}],
						[{text: "/accounts"}, {text: "/balances"}],
						[{text: "/positions"}, {text: "/alerts"}],
						[{text: "/log"}]
					])

				} else if (cmdL.startsWith("/stat")) {
					if (fromType && !opt.telebotChStat) continue
					if (!opt.telebotStat) { telebotSend(fromId, `*NOT ENABLED!*`, true); continue }
					const hasAddon = bg.tvTabs && bg.tvLastPing && bg.tvLastPing < Date.now()
					telebotSend(fromId, 
`*${pvName} ${pvVersion}${opt.instName?` (${telescape(opt.instName)})`:''}*
\`     Status:\` ${opt.disableAll?'*Paused 🚫*':'Live ✅'}
\` Connection:\` ${bg.tvStreamStatus?'Eventstream ✅':hasAddon?'Addon ✔':'*Disconnected! 🚫*'}
\`     ⤷ Loss:\` ⇄ \`${ls.statSessionStreamStall||0}\` │ \`${formatDur(ls.statLastStreamDown, -2, 0)} @ ${formatDateS(ls.statLastStreamStall)}\` ¬
\`      (prev)\` \`${formatDur(ls.statPrevStreamDown, -2, 0)} @ ${formatDateS(ls.statPrevStreamStall)}\`
\`     Uptime:\` \`${formatDur(ls.statUptime, -2)}\` │ \`${formatDur(ls.statUptimeLast, -2)}\` │ \`${formatDur(ls.statUptimeTotal, -2, 0)}\`
\`   Downtime:\` \`${formatDur(ls.statDowntime, -2)}\` │ \`${formatDur(ls.statDowntimeLast, -2)}\` │ \`${formatDur(ls.statDowntimeTotal, -2, 0)}\`
\`        New:\` ⇄ \`${ls.badgeRcv||0}\` │ ⇛ \`${ls.badgeRun||0}\` │ 🚫 \`${ls.badgeErr||0}\`
\`   Last Run:\` \`${ls.badgeEvt||ls.badgeEvtLast||'—'}\`
\`   Prev Run:\` \`${ls.badgeEvt?(ls.badgeEvtLast||'—'):'—'}\`
\`   Ev.Addon:\` \`${ls.statSessionEvents||0}\` │ \`${ls.statTotalEvents||0}\`
\`  Ev.Stream:\` \`${ls.statSessionStreamEvents||0}\` │ \`${ls.statTotalStreamEvents||0}\`
\`Ev.Interval:\` \`${ls.statSessionAuto||0}\` │ \`${ls.statTotalAuto||0}\`
\`Ev.Telegram:\` \`${ls.statSessionTelebot||0}\` │ \`${ls.statTotalTelebot||0}\`
\`    Running:\` \`${bg.statRunning}\`
\`   Last Cmd:\` \`${formatDateS(ls.statLastCmd)}\` │ \`${formatDateS(ls.statPrevCmd)}\`
\` Run Alerts:\` \`${ls.statSessionAlerts||0}\` │ \`${ls.statTotalAlerts||0}\`
\`       ⤷ TV:\` \`${(ls.statSessionAlerts||0)-(ls.statSessionChildren||0)-(ls.statSessionManual||0)-(ls.statSessionAuto||0)-(ls.statSessionTeleRun||0)-(ls.statSessionTrigger||0)}\` │ \`${(ls.statTotalAlerts||0)-(ls.statTotalChildren||0)-(ls.statTotalManual||0)-(ls.statTotalAuto||0)-(ls.statTotalTeleRun||0)-(ls.statTotalTrigger||0)}\`
\`    ⤷ Child:\` \`${ls.statSessionChildren||0}\` │ \`${ls.statTotalChildren||0}\`
\`   ⤷ Manual:\` \`${ls.statSessionManual||0}\` │ \`${ls.statTotalManual||0}\`
\` ⤷ Interval:\` \`${ls.statSessionAuto||0}\` │ \`${ls.statTotalAuto||0}\`
\` ⤷ Telegram:\` \`${ls.statSessionTeleRun||0}\` │ \`${ls.statTotalTeleRun||0}\`
\`  ⤷ Trigger:\` \`${ls.statSessionTrigger||0}\` │ \`${ls.statTotalTrigger||0}\`
\`    ⤷ Empty:\` \`${ls.statSessionAlertsEmpty||0}\` │ \`${ls.statTotalAlertsEmpty||0}\`
\`     Forked:\` \`${ls.statSessionForked||0}\` │ \`${ls.statTotalForked||0}\`
\`     Errors:\` \`${ls.statSessionErr||0}\` │ \`${ls.statTotalErr||0}\`
\`   Warnings:\` \`${ls.statSessionWarn||0}\` │ \`${ls.statTotalWarn||0}\`
\`    Retries:\` \`${ls.statSessionRetries||0}\` │ \`${ls.statTotalRetries||0}\`
\`Max.Retries:\` \`${ls.statSessionRetriesMax||0}\` │ \`${ls.statTotalRetriesMax||0}\`
`, true)

				} else if (cmdL.startsWithAny(["/disable", "/enable"])) {
					if (fromType && !opt.telebotChAppCtrl) continue
					if (!opt.telebotAppCtrl) { telebotSend(fromId, `*NOT ENABLED!*`, true); continue }
					loadOptions()
					opt.disableAll = cmdL.startsWith("/disable")
					saveOptions(), disableAllChanged()
					telebotSend(fromId, `Running of ALL new alerts ${opt.disableAll ? '*disabled*! (PAUSED)' : '*enabled*! (RESUMED)'}`, true)

				} else if (cmdL.startsWithAny(["/pause", "/resume", "/stop"])) {
					if (fromType && !opt.telebotAlrtCtrl) continue
					if (!opt.telebotAlrtCtrl) { telebotSend(fromId, `*NOT ENABLED!*`, true); continue }
					if (!bg.statRunning) { telebotSend(fromId, `*No alert* is currently running!`, true); continue }
					if (bg.exitAll) { telebotSend(fromId, `*Please wait* for all alerts to terminate! (${bg.statRunning} still running)`, true); continue }

					if (cmdL.startsWith("/stop")) {
						if (bg.statRunning) {
							bg.exitAll = true; bg.pauseAll = false
						}
						telebotSend(fromId, `*Stopping* ${bg.statRunning} running alert${bg.statRunning>1?'s':''} immediately!`, true)
					} else {
						const doPause = cmdL.startsWith("/pause")
						if (doPause && bg.pauseAll) {
							telebotSend(fromId, `Running alerts are *already paused*! (${bg.statRunning} paused)`, true)
						} else if (!doPause && !bg.pauseAll) {
							telebotSend(fromId, `Paused alerts are *already resumed*! (${bg.statRunning} running)`, true)
						} else {
							telebotSend(fromId, `*${doPause?'Pausing':'Resuming'}* ${bg.statRunning} running alert${bg.statRunning>1?'s':''} immediately!`, true)
						}
						bg.pauseAll = doPause
					}

				} else if (cmdL.startsWith("/restart")) {
					if (fromType && !opt.telebotChAppRest) continue
					if (!opt.telebotAppRest) { telebotSend(fromId, `*NOT ENABLED!*`, true); continue }
					telebotSend(fromId, `Initiating manual restart *now*!`, true)
					restart(true)

				} else if (cmdL.startsWith("/acc")) {
					if (fromType && !opt.telebotChAcc) continue
					if (!opt.telebotAcc) { telebotSend(fromId, `*NOT ENABLED!*`, true); continue }
					let output = "", i = 0
					const exes = broker.getAll()
					for (const ex of exes) {
						if (output.length >= 3840) {
							telebotSend(fromId, output, true), output = ""
						}
						const accounts = ex.getAccounts()
						if (accounts.length) {
							output += `${ex.getName()}: \`${accounts.join("\`, \`")}\`\n`
							i += accounts.length
						}
					}
					output += `\n${i} account${i>1?'s':''} found!`
					telebotSend(fromId, output, true)

				} else if (cmdL.startsWith("/bal")) {
					// $$$$$$$ /balances ex | ex:acc | acc
					if (fromType && !opt.telebotChAcc) continue
					if (!opt.telebotAcc) { telebotSend(fromId, `*NOT ENABLED!*`, true); continue }
					let output = ""
					const list = broker.getBalances()
					for(const entry of list) {
						if (output.length >= 3840) {
							telebotSend(fromId, output, true), output = ""
						}
						const digits = entry.total > 999 ? 1 : entry.total > 99 ? 2 : entry.total > 9 ? 3 : 4
						const amount = entry.avail.toFixed(digits) === entry.total.toFixed(digits) ? entry.total.toFixed(digits) : entry.avail.toFixed(digits-1)+' / '+entry.total.toFixed(digits-1)
						output += `${entry.name}${entry.acc != '*' ? ` / *${telescape(entry.acc)}*` : ''} – \`${amount} ${entry.ccy}\`\n_${formatDateS(entry.update, false, '—')}_\n`
					}
					output += `\n${list.length} saved balance${list.length>1?'s':''} found!`
//					if (list.length < 1) {}
					telebotSend(fromId, output, true)

				} else if (cmdL.startsWith("/pos")) {
					if (fromType && !opt.telebotChPos) continue
					if (!opt.telebotPos) { telebotSend(fromId, `*NOT ENABLED!*`, true); continue }
					const where = (cmd.split(' ')[1] || '').toUpperCase()
					if (where && !broker.checkAlias(where)) { telebotSend(fromId, `Invalid exchange alias *${where}* specified!`, true); continue }
					telebotSend(fromId, where ? `Querying positions on *${where}*...` : "Querying *all* positions...", true)
					let posNum = 0
					setTimeout(run.bind(this, updatePositions, where && [where], undefined, undefined, (alias, exPos) => {
						const ex = broker.getByAlias(alias)
						const name = ex && ex.getName()
						Object.each(exPos, (acc, pos) => {
							acc = acc != '*' ? ` / *${telescape(acc)}*` : ''
							if (pos.error) {
								const err = getError(pos.error, undefined, false)
								telebotSend(fromId, `${name}${acc} – \`ERROR: ${err}\``, true)
								return
							}
							if (pos.list && pos.list.length) {
								pos.list.forEach(p => {
									const sizeUSD = p._sizeUSD ? p._sizeUSD.toFixed(2) : '–'
									const relSize = (p.relSize * 100).toFixed(2)
									const lev = Number(p.leverage || 1).toFixed(2).replace('.00', '')
									const pnl = (p.pnl * 100).toFixed(2)
									const pnlLev = (p.pnlLev * 100).toFixed(2)
									const pnlTotal = (p.pnlTotal * 100).toFixed(2)
//									isProfit: p._profit > 0,
//									_date: p._date ? formatDate(p._date).substr(5) : "–",
//									age: p._date ? formatDur((Date.now() - p._date) / 1000) : "–",
									telebotSend(fromId, `${name}${acc} – \`${p._symbol}\` ${p._side} @ \`${p._entry}\` │ size: \`${p._size} / ${sizeUSD} USD\` (\`${relSize}%\`) @ \`${lev}x\` │ PnL: \`${toDecimal(p._profit)}\` (*${pnl}%* / \`${pnlLev}%\` / \`${pnlTotal}%\`)`, true)
								})
								posNum += pos.list.length
							}
						})
					}, (exNum, accNum) => {
						telebotSend(fromId, posNum ? `${posNum} open position${posNum>1?'s':''} found on ${accNum} account${accNum>1?'s':''} / ${exNum} exchange${exNum>1?'s':''}!` : `No open positions found!`, true)
					}), forkSleep)

				} else if (cmdL.startsWith("/alert")) {
					// $$$$$$$ /alerts enabled|disabled
					// /alert(s) name => details + content
					if (fromType && !opt.telebotChAlrt) continue
					if (!opt.telebotAlrt) { telebotSend(fromId, `*NOT ENABLED!*`, true); continue }
					let output = "", i = 0
					const alerts = getAlerts().sort(alertSort)
					alerts.forEach(alert => {
						if (output.length >= 3840) {
							telebotSend(fromId, output, true), output = ""
						}
						output += 
`${alert.enabled?'✅':'🚫'} \`[${alert.names.join(', ')}]\` #${alert.id}${alert.auto||alert.sched?`
  ⏱ \`${alert.autoInt}\``:''}
  ⇄ ${formatDateS(alert.recv, false, '—')} (${alert.recvNum||0})
  ⇛ ${formatDateS(alert.fired, false, '—')} (${alert.firedNum||0})

`						// $$$ print lock
						++i
					})
					output += `${i} ${pvShortU} alert${i>1?'s':''} found!`
					telebotSend(fromId, output, true)

				} else if (cmdL.startsWith("/log")) {
					if (fromType && !opt.telebotChLog) continue
					if (!opt.telebotLog) { telebotSend(fromId, `*NOT ENABLED!*`, true); continue }
					let len = Number(cmd.split(' ')[1]) || 20
					let back = Math.abs(len)
					if (len < 0) len = 20

					const logStore = log.getStore()
					logStore.getEventsCB(function(events){
						let output = ""
						for (let i = 0; i < events.length; ++i) {
							const ev = events[i]
							if (!ev.d || !ev.m || ev.l < 1) {
								continue
							}
							const line = formatDate(ev.d)+' '+ev.m.replace('{\n','\n').replace('\n}','').replace(/,\n/g, '\n')+'\n'
							if (output.length + line.length >= 4094) {
								telebotSend(fromId, output), output = ""
							}
							output += line
						}
						output.length && telebotSend(fromId, output)
					}, logStore.getNext() - back, len, true)

				} else {
					if (fromType && !opt.telebotChRunCmd && !opt.telebotChRunAlrt) continue
					if (!opt.telebotRunCmd && !opt.telebotRunAlrt) { telebotSend(fromId, `*NOT ENABLED!*`, true); continue }
					if (cmd.startsWith('/')) cmd = cmd.slice(1)
					const res = cmd.match(/[\s(,]res=([\d.shdwm]+)/i)
					const time = data.date || Date.now() / 1000
					const ev = {
						id: 'T'+msg.update_id,
						sym: (opt.telebotEx || opt.defExchange)+':'+(opt.telebotSym || opt.defSymbol),
						res: getTF(res && res[1], opt.telebotRes || opt.defaultRes),
						bar_time: time,
						fire_time: time,
						desc: cmd,
						telebot: fromId,
						noLegacy: fromType ? !opt.telebotChRunCmd : !opt.telebotRunCmd,
						noAlerts: fromType ? !opt.telebotChRunAlrt : !opt.telebotRunAlrt
					}
					if (cmd.startsWith('!') && (fromType ? opt.telebotChOver : opt.telebotOver)) {
						ev.noFilter = true
						ev.allow = cmd.startsWith('!!!') ? 2 : cmd.startsWith('!!') ? 1 : 0
						ev.desc = cmd.slice(1 + ev.allow)
					}
					setTimeout(run.bind(this, handlers.event, ev), forkSleep)
				}
			}
		}
		setTimeout(telebot, forkSleep)
	})
	.catch(ex => {
		telebotErr = (ex.message || "Unknown – Please report").replace("Failed to fetch", "Can't connect")
		telebotErrLast = Date.now()
		incStat('TelebotErr', 1, true, telebotErr)
		log.error(`TelegramBot Error: ${telebotErr}!`)
	})
}

function* checkHeartbeat() {
	loadOptions()
	yield* broadcastOptions()
	checkOptionsPage()
	log.setLevel(opt.debugLog ? log.level.DEBUG : log.level.INFO)
	ls.statUptime = ((ls.statHeartbeat = Date.now()) - ls.statBootup) / 1000
	ls.statLastPing = tvLastPing
	ls.statLastEvent = tvLastEvent
	let oldPing = 0

	const lastEventMin = (Date.now() - tvLastEvent) / m2ms
	if (tvLastEvent && lastEventMin >= opt.timeoutTVmin) {
		tvLastEvent = Date.now()
		const timeoutMsg = `WARNING: NO ALERT RECEIVED FOR ${Math.floor(lastEventMin)} MINUTES! (${opt.timeoutTV ? (!tvTabs || opt.onlyStream ? "reconnecting TV stream... " : "reloading TV tabs... "):''}checking again in ${opt.timeoutTVmin} min)`

		if (opt.timeoutTV) {
			log.warn(timeoutMsg)
			if (!tvTabs || opt.onlyStream) {
				closeTradingViewStream()
				openTradingViewStream()
			}
			if (opt.autoTV || (tvTabs && !opt.onlyStream)) {
				yield* forceTradingViewCheck(true)
			}
		}

		incStat('Timeout', 1, true)
		if (opt.emailTimeout && emailMessage("timeout", "** "+timeoutMsg+" **")) log.info("Sent Email timeout notification!")
		if (opt.discordTimeout && discordMessage("timeout", "`"+timeoutMsg+"`")) log.info("Sent Discord timeout notification!")
		if (opt.telegramTimeout && telegramMessage("timeout", "`"+timeoutMsg+"`")) log.info("Sent Telegram timeout notification!")
		if (opt.twilioTimeout && twilioMessage("timeout", "** "+timeoutMsg+" **")) log.info("Sent Twilio timeout notification!")
		if (opt.iftttTimeout && iftttMessage("timeout")) log.info("Sent IFTTT timeout notification!")
		if (opt.timeoutTV) return
	}

	const newPing = Date.now()
	if (!tvLastPing2) {
		debugger
	}
	if ((oldPing = Date.now()) && tvLastPing && oldPing - tvLastPing >= pingTimeout && tvStreamStatus < TRADINGVIEW_STREAM_OPEN) {
		if ((tvTabs < 1 || opt.onlyStream) && Date.now() - tvLastReload >= reloadSafetyTimeout) {
			tvLastReload = Date.now()
			yield* toggleTradingViewStream()
		}
		yield* sleep(reloadSafetyTimeout / 1000)
		if (Date.now() - tvLastPing >= pingTimeout) {
			tvLastPing = Date.now() + opt.forceTVmin * m2ms - pingTimeout
			const checkMsg = `ERROR: NO CONNECTION TO TRADINGVIEW! (checking again in ${opt.forceTVmin} min)`

			log.error(checkMsg)
			yield* forceTradingViewCheck()

			incStat('Stall', 1, true)
			if (opt.emailStalling && emailMessage("stall", "** "+checkMsg+" **")) log.info("Sent Email stalling notification!")
			if (opt.discordStalling && discordMessage("stall", "`"+checkMsg+"`")) log.info("Sent Discord stalling notification!")
			if (opt.telegramStalling && telegramMessage("stall", "`"+checkMsg+"`")) log.info("Sent Telegram stalling notification!")
			if (opt.twilioStalling && twilioMessage("stall", "** "+checkMsg+" **")) log.info("Sent Twilio stalling notification!")
			if (opt.iftttStalling && iftttMessage("stall")) log.info("Sent IFTTT stalling notification!")

			if (tvStreamStalled >= streamRetries && opt.forceTV) tvStreamStalled = 0
		}
	}
	if (oldPing - newPing > 50) tvLastPing2 = oldPing - newPing

	if (opt.telebot && ((telebotErrLast > telebotLast && telebotErr && !telebotErr.startsWith('Conflict')) || Date.now() - telebotLast > telebotTimeout)) {
		telebot()
	}
}

function* reloadTradingViewCheck(force) {
	if (force || Date.now() - tvLastReload >= reloadSafetyTimeout) {
		tvLastReload = Date.now()
		yield* reloadTradingViewTabs(force)
	}
}

function* forceTradingViewCheck(force) {
	if (!force && !opt.forceTV) return
	const tabs = yield* getTradingViewTabs()
	tvTabs = tabs.length

	if (tvTabs > 0) {
		log.notice("Trying to reload TradingView...")
		chrome.tabs.update(tabs.last().id, {selected: true})
//		chrome.windows.update(tabs.last().windowId, {focused: true})
		yield* reloadTradingViewCheck(true)
	} else if (opt.autoTV) {
		log.notice("Trying to open TradingView...")
		chrome.tabs.create({url: 'https://www.tradingview.com/chart/'})
	}
}

function checkOptionsPage() {
	if (/*isReady &&*/ opt.keepApp && !chrome.extension.getViews({type:"tab"}).filter(view => view.location.pathname === '/options.html').length) {
		chrome.tabs.create({url: chrome.runtime.getURL("options.html#/log")})
	}
}

function reloadOptions() {
	const options = chrome.extension.getViews({type:"tab"}).filter(view => view.location.pathname === '/options.html')[0]
	if (options) {
		options.location.reload()
	}
}
