"use strict"

class Command {
	constructor(defaults) {
		this.isAsk = false
		this.isBid = false
		this.isMargin = false
		defaults && this.merge(defaults, true)
	}
	#own = []

	static #cmds = {
		a: "Account",
		ap: "Account Parallel",
		b: "Book",
		bc: "Balance Cached",
		buy: "New Buy Order",
		c: "Cancel / Close",
		cached: "Price / Balance Cached",
		cancelall: "Cancel All Orders",
		cbrate: "Callback Rate",
		ch: "Check",
		cha: "Check Alert",
		chb: "Check Balance", // $$$
		cid: "Check Order ID (or Cancel/CLose ID)",
		closeall: "Close All Positions",
		cm: "Cancel / Close Maximum",
		cmo: "Cancel / Close Maximum Order",
		cmcid: "CMC Ticker ID",
		convert: "Convert Balance",
		cr: "Cancel / Close Reverse Match",
		crossed: "Margin Type Crossed",
		d: "Disabled",
		disable: "Disable Alert",
		div: "Divide Variable",
		dec: "Decrement Variable",
		delay: "Delay",
		e: "Exchange",
		echo: "Print to log",
		eid: "Error Handling Cancel/Close ID",
		else: "If Expression False Handling",
		enable: "Enable Alert",
		end: "Jump Back",
		err: "Error Handling",
		ep: "Error Handling Price",
		exit: "Stop Alert",
		exitall: "Stop Running Alerts",
		fb: "Fallback Action",
		fid: "Filter Trades Order ID",
		fifo: "FIFO",
		fp: "Fixed Price",
		from: "Query Starting Time",
		ft: "Filter Trades",
		gsl: "Guaranteed Stop-Loss",
		gt: "Price Greater Than",
		gte: "Price Greater Than Or Equal",
		h: "Hidden/Iceberg",
		id: "Order ID",
		interval: "Set Alert Interval",
		inc: "Increment Variable",
		if: "If Expression Evaluates True-ish",
		ifd: "If Disabled",
		ife: "If Enabled",
		ifbg: "If Bigger",
		ifl: "If Long",
		ifn: "If None",
		ifnp: "If No Match And Position",
		ifnnp: "If No Match And No Position",
		ifo: "If Open",
		iforder: "If Order Matches",
		ifnoorder: "If No Order Matches",
		ifp: "If Partial",
		ifpos: "If Position Matches",
		ifnopos: "If No Position Matches",
		ifr: "If Rejected",
		ifs: "If Short",
		ifsl: "If Slippage",
		ifslp: "If Slippage And Position",
		ifslnp: "If Slippage And No Position",
		ifsm: "If Smaller",
		ift: "If Timeout (Maxtime)",
		iftf: "If Timeout And Fill (Partial)",
		iftnf: "If Timeout And No Fill",
		iftp: "If Timeout And Position",
		iftnp: "If Timeout And No Position",
		ifminb: "If Balance Under", // $$$
		ifmaxb: "If Balance Over", // $$$
		ifminsp: "If Spread Too Low",
		ifmaxsp: "If Spread Too High",
		ismargin: "Is Margin Trade",
		iso: "Isolated",
		jd: "Jump Delay",
		l: "Leverage",
		lc: "Leverage for Calculation",
		limit: "Order Type Limit",
		log: "Loglevel",
		logvars: "List all OHLC variables",
		loan: "Loan Margin",
		long: "New Long Order",
		lock: "Lock / Unlock Symbol",
		lockcheck: "Check Symbol Lock",
		lq: "Leftover Quantity",
		lr: "Leverage Reference",
		lt: "Price Less Than",
		lte: "Price Less Than Or Equal",
		market: "Order Type Market",
		marketbuy: "New Buy Order (Order Type Market)",
		marketsell: "New Sell Order (Order Type Market)",
		marketlong: "New Long Order (Order Type Market)",
		marketshort: "New Short Order (Order Type Market)",
		max: "Maximum Count",
		minb: "Min Balance",
		maxb: "Max Balance",
		minhvd: "Min Daily Volatility",
		maxhvd: "Max Daily Volatility",
		minhvw: "Min Weekly Volatility",
		maxhvw: "Max Weekly Volatility",
		minhvm: "Min Monthly Volatility",
		maxhvm: "Max Monthly Volatility",
		minp: "Min Price",
		maxp: "Max Price",
		minpl: "Min Profit Loss",
		minplr: "Min Profit Loss Reference",
		maxpl: "Max Profit Loss",
		maxplr: "Max Profit Loss Reference",
		minq: "Min Quantity",
		maxq: "Max Quantity",
		mins: "Min Position Size",
		minsr: "Min Position Size Reference",
		maxs: "Max Position Size",
		maxsr: "Max Position Size Reference",
		minsp: "Min Spread",
		minspref: "Min Spread Reference",
		maxsp: "Max Spread",
		maxspref: "Max Spread Reference",
		min1h: "Minimum 1h Price Change",
		max1h: "Maximum 1h Price Change",
		min24h: "Minimum 24h Price Change",
		max24h: "Maximum 24h Price Change",
		min7d: "Minimum 7d Price Change",
		max7d: "Maximum 7d Price Change",
		mc: "Market Close Positions",
		mca: "Market Close All Positions",
		msl: "Max Slippage",
		msle: "Max Slippage Exchange",
		mslr: "Max Slippage Reference",
		mslv: "Max Slippage Variable",
		mt: "Margin Type",
		mul: "Multiply Variable",
		n: "Notify",
		nr: "No Result",
		p: "Price",
		pause: "Pause Alert", // $$$
		pauseall: "Pause Running Alerts",
		pb: "Price Bound",
		pc: "Price Cached",
		plref: "Profit Loss Reference",
		pm: "Position Mode",
		post: "Order Type Post",
		pr: "Price Reference",
		pp: "Price Protect",
		q: "Quantity",
		qr: "Quantity Reference",
		query: "Query Past Trades or PnL",
		r: "Reduce",
		rd: "Retry (Back Jump) Delay",
		repay: "Repay Margin",
		restart: "Restart App",
		resume: "Resume Alert",	// $$$
		resumeall: "Resume All Alerts",
		ret: "Return Expected",
		retries: "Retries",
		retryall: "Retry All Errors",
		rf: "Retry Until Filled",
		run: "Run Alert",
		runen: "Run Alert If Enabled",
		s: "Symbol",
		saveprice: "Update and Save Price",
		savebalance: "Update and Save Balance",
		saveorder: "Query and Save Orders",
		savepos: "Query and Save Positions",
		sell: "New Sell Order",
		setvar: "Set OHLC variables",
		short: "New Short Order",
		since: "Time/Count Reference",
		skip: "Skip Commands",
		sl: "Stop Loss",
		slref: "Stop Loss Price Reference",
		so: "Stop Order",
		soref: "Stop order Price Reference",
		spa: "Spread Check Ask",
		spb: "Spread Check Bid",
		sr: "Stop Reference",
		sv: "Save Order Data",
		svn: "Save Order Data Name",
		t: "Order Type",
		tb: "Transfer Balance",
		time: "Maximum Time",
		timeout: "Timeout",
		toggle: "Toggle Alert",
		to: "Query Ending Time",
		tp: "Take Profit",
		tpref: "Take Profit Price Reference",
		tr: "Transpose Plots",
		trref: "Transpose Price Reference",
		trigger: "Trigger Alert",
		triggeren: "Trigger Alert If Enabled",
		ts: "Trailing Stop",
		tsref: "Trailing Stop Price Reference",
		u: "Unit",
		ub: "Update Balance",
		unset: "Delete OHLC variables",
		up: "Update Price",
		update: "Update Price / Balance",
		wf: "Wait For", // $$$
		wid: "Wrong Order ID",
		withdraw: "Withdraw Balance", // $$$
		ws: "Wrong Side",
		y: "Yield",
		yp: "Yield / Add Position",
	}
	static #alias = {
		acc: "a",
		account: "a",
		par: "ap",
		parallel: "ap",
		book: "b",
		side: "b",
		cb: "bc",
		cachedbalance: "bc",
		close: "c",
		closepos: "c",
		cancel: "c",
		check: "ch",
		checkpos: "ch",
		checkorder: "ch",
		checkalert: "cha",
		alertcheck: "cha",
		checkbalance: "chb",
		balancecheck: "chb",
		chid: "cid",
		checkid: "cid",
		cancelid: "cid",
		closeid: "cid",
		maximum: "cm",
		order: "cmo",
		nonmatch: "cr",
		reverse: "cr",
		cross: "crossed",
		debug: "d",
		disabled: "d",
		ex: "e",
		exchange: "e",
		errid: "eid",
		errorid: "eid",
		back: "end",
		error: "err",
		iferror: "err",
		onerror: "err",
		errorp: "ep",
		errorprice: "ep",
		abort: "exit",
		quit: "exit",
		fallback: "fb",
		filterid: "fid",
		fixed: "fp",
		fixedp: "fp",
		eq: "fp",
		filtertrades: "ft",
		guaranteed: "gsl",
		above: "gt",
		aboveeq: "gte",
		hidden: "h",
		iceberg: "h",
		custom: "id",
		name: "id",
		ifdisabled: "ifd",
		ifenabled: "ife",
		ifbigger: "ifbg",
		iflong: "ifl",
		ifbuy: "ifl",
		ifnot: "ifn",
		ifnone: "ifn",
		ifnonepos: "ifnp",
		ifnonenopos: "ifnnp",
		ifnomatch: "ifn",
		ifnomatchpos: "ifnp",
		ifnomatchnopos: "ifnnp",
		ifopen: "ifo",
		iffound: "ifo",
		ifmatch: "ifo",
		ifpartial: "ifp",
		ifreject: "ifr",
		ifrejected: "ifr",
		ifshort: "ifs",
		ifsell: "ifs",
		ifslip: "ifsl",
		ifslippos: "ifslp",
		ifslipnopos: "ifslnp",
		ifsmaller: "ifsm",
		iftime: "ift",
		iftimeout: "ift",
		iftimefill: "iftf",
		iftimenofill: "iftnf",
		iftimepos: "iftp",
		iftimenopos: "iftnp",
		ifminbalance: "ifminb",
		ifbalanceunder: "ifminb",
		ifmaxbalance: "ifmaxb",
		ifbalanceover: "ifmaxb",
		ifminspread: "ifminsp",
		ifmaxspread: "ifmaxsp",
		margin: "ismargin",
		isolated: "iso",
		jumpdelay: "jd",
		retrydelay: "rd",
		leftover: "lq",
		lev: "l",
		leverage: "l",
		levcalc: "lc",
		loglevel: "log",
		dumpvars: "logvars",
		checklock: "lockcheck",
		lref: "lr",
		below: "lt",
		beloweq: "lte",
		maxcount: "max",
		minbalance: "minb",
		maxbalance: "maxb",
		mindailyvola: "minhvd",
		maxdailyvola: "maxhvd",
		minweeklyvola: "minhvw",
		maxweeklyvola: "maxhvw",
		minmonthlyvola: "minhvm",
		maxmonthlyvola: "maxhvm",
		minprice: "minp",
		maxprice: "maxp",
		minpnl: "minpl",
		minpnlref: "minplr",
		maxpnl: "maxpl",
		maxpnlref: "maxplr",
		minquantity: "minq",
		maxquantity: "maxq",
		minsize: "mins",
		minsizeref: "minsr",
		maxsize: "maxs",
		maxsizeref: "maxsr",
		minspr: "minspref",
		maxspr: "maxspref",
		marketclose: "mc",
		closemarket: "mc",
		marketcloseall: "mca",
		maxslip: "msl",
		maxslipex: "msle",
		maxslipref: "mslr",
		maxslipvar: "mslv",
		minspread: "minsp",
		maxspread: "maxsp",
		margintype: "mt",
		notify: "n",
		noorder: "nr",
		nopos: "nr",
		noresult: "nr",
		price: "p",
		bound: "pb",
		cp: "pc",
		cachedprice: "pc",
		pnlref: "plref",
		posmode: "pm",
		priceref: "pr",
		protect: "pp",
		amount: "q",
		quantity: "q",
		size: "q",
		qref: "qr",
		sizeref: "qr",
		reduce: "r",
		return: "ret",
		expect: "ret",
		fill: "rf",
		sym: "s",
		symbol: "s",
		set: "setvar",
		do: "skip",
		go: "skip",
		goto: "skip",
		jump: "skip",
		loop: "skip",
		sleep: "delay",
		stoploss: "sl",
		stop: "so",
		stopref: "sr",
		spreadask: "spa",
		spreadbid: "spb",
		save: "sv",
		savename: "svn",
		getprice: "saveprice",
		getbalance: "savebalance",
		getorder: "saveorder",
		getpos: "savepos",
		type: "t",
		transfer: "tb",
		maxtime: "time",
		takeprofit: "tp",
		transpose: "tr",
		transref: "trref",
		trailing: "ts",
		unit: "u",
		updatebalance: "ub",
		unsetvar: "unset",
		updateprice: "up",
		updateticker: "up",
		wait: "delay",
		waitfor: "wf",
		wrongid: "wid",
		wrongside: "ws",
		equity: "y",
		yield: "y",
		addpos: "yp",
		usepos: "yp",
		yieldpos: "yp",
	}

	static listParams = ['a', 'e', 'ep', 'fp', 'gt', 'gte', 'l', 'lt', 'lte', 'maxb', 'maxp', 'maxpl', 'maxq', 'maxs', 'maxsp', 'minb', 'minp', 'minpl', 'minq', 'mins', 'minsp', 'msl', 'p', 'q', 's', 'sl', 'so', 'tp', 'ts']
	static noRefParams = ['ep', 'fp', 'maxb', 'minb']
	static relRefParams = ['q', 'mins', 'maxs', 'minpl', 'maxpl', 'msl']

	static prefs = {'acc': true, 'ACC': true, 'alert': true, 'ask': true, 'auto': true, 'barpos': true, 'bartime': true, 'bid': true, 'caller': true, 'close': true, 'date': true, 'day': true, 'disabled': true, 'even': true, 'ex': true, 'filled': true, 'filled2': true, 'filled3': true, 
		'firstprice': true, 'firstqty': true, 'fork': true, 'fully_diluted_market_cap': true, 'high': true, 'hl': true, 'hour': true, 'hvd': true, 'hvm': true, 'hvw': true, 'id': true, 'ID': true, 'index': true, 'inst': true, 'internal': true, 'last': true, 'lastask': true, 'lastask2': true, 
		'lastask3': true, 'lastavail': true, 'lastavail2': true, 'lastavail3': true, 'lastavg': true, 'lastavg2': true, 'lastavg3': true, 'lastbid': true, 'lastbid2': true, 'lastbid3': true, 'lastexit': true, 'lastexit2': true, 'lastexit3': true, 'lastid': true, 'lastid2': true, 
		'lastid3': true, 'lastindex': true, 'lastindex2': true, 'lastindex3': true, 'lastlast': true, 'lastlast2': true, 'lastlast3': true, 'lastlev': true, 'lastlev2': true, 'lastlev3': true, 'lastmark': true, 'lastmark2': true, 'lastmark3': true, 'lastmid': true, 'lastmid2': true, 
		'lastmid3': true, 'lastnum': true, 'lastnum2': true, 'lastnum3': true, 'lastnumsum': true, 'lastnumsum2': true, 'lastnumsum3': true, 'lastoid': true, 'lastoid2': true, 'lastoid3': true, 'lastpnl': true, 'lastpnl2': true, 'lastpnl3': true, 'lastpnlsum': true, 'lastpnlsum2': true, 
		'lastpnlsum3': true, 'lastpnltotal': true, 'lastpnltotal2': true, 'lastpnltotal3': true, 'lastprice': true, 'lastprice2': true, 'lastprice3': true, 'lastprofit': true, 'lastprofit2': true, 'lastprofit3': true, 'lastprofitsum': true, 'lastprofitsum2': true, 'lastprofitsum3': true,
		'lastrelsize': true, 'lastrelsize2': true, 'lastrelsize3': true, 'lastrelsizesum': true, 'lastrelsizesum2': true, 'lastrelsizesum3': true, 
		'lastqty': true, 'lastqty2': true, 'lastqty3': true, 'lastqtysum': true, 'lastqtysum2': true, 'lastqtysum3': true, 'lastside': true, 'lastside2': true, 'lastside3': true, 'lastsl': true, 'lastsl2': true, 'lastsl3': true, 'laststatus': true, 'laststatus2': true, 'laststatus3': true, 
		'laststop': true, 'laststop2': true, 'laststop3': true, 'lasttotal': true, 'lasttotal2': true, 'lasttotal3': true, 'lasttp': true, 'lasttp2': true, 'lasttp3': true, 'lasttype': true, 'lasttype2': true, 'lasttype3': true, 'left': true, 'left2': true, 'left3': true, 'liq': true, 
		'long': true, 'low': true, 'manual': true, 'mark': true, 'market_cap': true, 'market_cap_dominance': true, 'mid': true, 'minute': true, 'mode': true, 'month': true, 'name': true, 'NAME': true, 'oc': true, 'ohlc': true, 'open': true, 'percent_change_1h': true, 'percent_change_24h': true, 
		'percent_change_30d': true, 'percent_change_60d': true, 'percent_change_7d': true, 'percent_change_90d': true, 'pos': true, 'price': true, 'res': true, 'screener': true, 'short': true, 'slip': true, 'sym': true, 'telebot': true, 'tf': true, 'this': true, 'time': true, 'vol': true, 
		'volume_24h': true, 'volume_change_24h': true, 'weekday': true, 'weekend': true}

	static jumpKeys = ['else', 'err', 'fb', 'ifbg', 'ifd', 'ife', 'ifl', 'ifmaxb', 'ifmaxsp', 'ifminb', 'ifminsp', 'ifn', 'ifnnp', 'ifnp', 'ifo', 'ifp', 'ifr', 'ifs', 'ifsl', 'ifslnp', 'ifslp', 'ifsm', 'ift', 'iftf', 'iftnf', 'iftnp', 'iftp', 'nr', 'skip', 'wid', 'ws']
	static custom = /^([_$][\w{}]+|custom\d+)$/
	static customStart = /^([_$][\w]+|custom\d+)/
	static customStartEx = /^(?<sign>[+\-])?(?<var>[_$][\w]+|custom\d+)(?<perc>%|)(?<calc>.*)?/

	static err = {
		cancel: -7,
		cancelall: -6,
		cancelside: -5,
		closemarket: -4,
		closeall: -3,
		closeside: -2,
		abort: -1,
		back: -0.5,
		this: -0.2,
		retry: 0,
		do: 0.2,
		continue: 0.5,
		repeat: 0.5,
		skip: 1,
	}
	static errDefault = Command.err.retry
	static errIgnore = Command.err.continue
	static goBack = Command.err.back
	static doLoop = Command.err.this
	static doRepeat = Command.err.continue
	static log = {
		all: 15,
		nosyntax: 31,
		nodebug: 14,
		cond: 10,
		warn: 6,
		err: 2,
		debug: 1,
		none: 0
	}
	static illegalId = /[^\w\-,*]/
	static illegalVar = /[\W]/
	static legalVarGlobal = /^\$?[\w]+/

	static defaultWarn(msg) {
		if (!msg.startsWith("Warning")) throw new SyntaxError(msg)
		log.warn(msg)
	}
	static ignoreWarn(msg) {}

	set(key, value, warn = Command.defaultWarn, noShortcuts, origKey, checkOnly, ohlc) {
		switch (key) {
			case "marketbuy":
			case "marketsell":
			case "marketlong":
			case "marketshort":
				this.t = "market"; this.#own.push('t');
				key = key.slice(6)
			case "buy":
			case "sell":
			case "long":
			case "short":
				if (noShortcuts) break
				if (value) {
					this.set("q", value, warn, false, origKey, checkOnly, ohlc); this.#own.push('q')
				}
				value = key
				this.#own.push((key = "b"))
			case "b":
				this.isAsk = false
				this.isBid = false
				this.isMargin = false
				switch (this[key] = value.toLowerCase()) {
					case "long":
						this.isMargin = true
					case "bid":
					case "buy":
						this.isBid = true
						break
					case "short":
						this.isMargin = true
					case "ask":
					case "sell":
						this.isAsk = true
						break
					default:
						warn(`Invalid book option "${value}" (${origKey||key}=) specified – expecting either "long"/"bid"/"buy", "short"/"ask"/"sell"! (Line ${this._line})`)
				}
				break

			case "closeall":
//				if (noShortcuts) break
			case "mca":
				this.s = '*'; this.#own.push('s');
			case "mc":
				if (key !== "closeall") {
					this.t = "market"; this.#own.push('t');
				}
				value ||= "position"
				this.#own.push((key = "c"))
			case "c":
			case "ch":
				// $$$$ add warning for cancel=pos!
				if (value && (value.endsWith && value.endsWith('%') || !isNaN(value))) {
					// $$$$ checkpos=123% => ??? (minsize or similar?)
					this.set("q", value, warn, false, origKey, checkOnly, ohlc); this.#own.push('q')
					this[key] = "position"; break
				}
				if (origKey === "closepos" || origKey === "checkpos") {
					if (value) {
						this.set("b", value, warn, false, origKey, checkOnly, ohlc); this.#own.push('b')
					}
					this[key] = "position"; break
				}
				value = value.toLowerCase && value.toLowerCase() || value || (origKey === "close" ? "position" : "order")
				const alias = ["takeprofit"/*0*/, "tp"/*1*/, "stoploss"/*2*/, "sl"/*3*/, "ts"/*4*/, "so"/*5*/, "open"/*6*/, "limit"/*7*/, "close"/*8*/, "stops"/*9*/, "long"/*10*/, "short"/*11*/, "buy"/*12*/, "sell"/*13*/].indexOf(value)
				if (!["order", "position", "pos"].includes(value) && alias < 0) {
					warn(`Invalid cancel/close/check option "${value}" (${origKey||key}=) specified – expecting either "order", "position"/"pos", "long"/"short", "buy"/"sell", "takeprofit"/"tp", "stoploss"/"sl", "ts", "so", "open"/"limit" or "close"/"stops"! (Line ${this._line})`)
				}
				if (alias >= 0) {
					if (alias > 9) {
						this.set("b", value, warn, false, origKey, checkOnly, ohlc); this.#own.push('b')
						value = alias > 11 || origKey === "cancel" ? "order" : "position"
					} else {
						value = "order"
						switch (alias) {
							case 0: case 1: this.tp = new NumberObject(1); this.#own.push('tp'); break
							case 2: case 3: this.sl = new NumberObject(1); this.#own.push('sl'); break
							case 4: 		this.ts = new NumberObject(1); this.#own.push('ts'); break
							case 5: 		this.so = new NumberObject(1); this.#own.push('so'); break
							case 6: case 7: this.t = "open"; this.#own.push('t'); break
							case 8: case 9: this.t = "close"; this.#own.push('t'); break
						}
					}
				}
				this[key] = value === "pos" ? "position" : value; break

			case "cancelall":
//				if (noShortcuts) break
				this.c = "order"; this.s = '*'; this.#own.push('c', 's'); break

			case "ifpos":
			case "ifnopos":
			case "iforder":
			case "ifnoorder":
				this.ch = key.endsWith("pos") ? "position" : "order"
				this.set((key = key.startsWith("ifn") ? "ifn" : "ifo"), value, warn, false, origKey, checkOnly, ohlc)
				this.#own.push('ch', key); break

			case "cmo":
				value = value.toLowerCase ? value.toLowerCase() : value
				if (!["newest", "oldest", "lowest", "highest", "smallest", "biggest", "random"].includes(value)) {
					warn(`Invalid cancel/close order option "${value}" (${origKey||key}=) specified – expecting either "newest", "oldest", "lowest", "highest", "smallest", "biggest", "random"! (Line ${this._line})`)
				}
				this[key] = value; break

			case "pr":
			case "slref":
			case "tpref":
			case "tsref":
			case "soref":
			case "trref":
			case "qr":
			case "lr":
			case "minplr":
			case "maxplr":
			case "minsr":
			case "maxsr":
			case "mslr":
			case "mslv":
			case "minspref":
			case "maxspref":
			case "spa":
			case "spb":
			case "gtref":
			case "gteref":
			case "ltref":
			case "lteref":
				value = value.toLowerCase ? value.toLowerCase() : value
				if (!Command.prefs[value] && !Command.custom.test(value)) {
					warn(`Invalid ${({"lr": 'leverage', "mslr": 'slippage', "mslv": 'slippage variable', "spa": 'spread ask', "spb": 'spread bid', "qr": 'quantity', 
						"minsr": 'min size', "maxsr": 'max size', "minplr": 'min pnl', "maxplr": 'max pnl'})[key] || 'price'} reference option "${value}" (${origKey||key}=) specified! (Line ${this._line})`)
				}
				this[key] = value; break

			case "plref":
				value = value.toLowerCase ? value.toLowerCase() : value
				if (!["pos", "lev", "total"].includes(value)) {
					warn(`Invalid profit loss reference option "${value}" (${origKey||key}=) specified – expecting either "pos", "lev", "total"! (Line ${this._line})`)
				}
				this[key] = value; break

			case "pm":
				value = value.toLowerCase ? value.toLowerCase() : value
				if (!["normal", "oneway", "hedge", "hedged", "auto"].includes(value)) {
					warn(`Invalid position mode option "${value}" (${origKey||key}=) specified – expecting either "normal"/"oneway", "hedge"/"hedged", "auto"! (Line ${this._line})`)
				}
				this[key] = ({"oneway": "normal", "hedged": "hedge"})[value] || value; break

			case "sr":
				value = value.toLowerCase ? value.toLowerCase() : value
				if (!["last", "mark", "index", "contract"].includes(value)) {
					warn(`Invalid stop reference option "${value}" (${origKey||key}=) specified – expecting either "last", "mark", "index", "contract"! (Line ${this._line})`)
				}
				this[key] = value; break

			case "if":
				if (value?.[0] === '(' && value[value.length-1] === ')') {
					value = value.slice(1, -1)
				}
				value && Command.evalIf(value, null, ohlc, true, msg => warn(msg+` (Line ${this._line})`))
				this[key] = value; break

			case "unset":
				value = value?.split(',')
				for (const name of value) {
					if (!Command.legalVarGlobal.test(name)) {
						warn(`Invalid variable name "${name}" (${origKey||key}=) specified – expecting a list of only alphanumeric / underscore names! (Line ${this._line})`)
					}
				}
				this[key] = value; break

			case "setvar":
				value = value?.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/).map(set => set.split(/:(.*)/))
				for (const pair of value) {
					if (!Command.legalVarGlobal.test(pair[0])) {
						warn(`Invalid variable name "${pair[0]}" (${origKey||key}=) specified – expecting a list of only alphanumeric / underscore names! (Line ${this._line})`)
					}
					if (pair[1][0] !== '"' && Command.isExp(pair[1])) {
						Command.solveExp(pair[1], null, ohlc, true, msg => warn(msg+` (Line ${this._line})`))
					}
				}
				this[key] = value; break

			case "inc":
			case "dec":
			case "mul":
			case "div":
				value = value?.split(',').map(set => set.split(':', 2)).map(set => `${set[0]}:(${set[0]}${({"inc": '+', "dec": '-', "mul": '*', "div": '/'})[key]}${set[1]||'1'})`).join()
				this.set("setvar", value, warn, false, origKey, checkOnly, ohlc); this.#own.push('setvar'); break

			case "limit":
			case "market":
			case "post":
				value = key
				this.#own.push((key = "t"))
			case "t":
				value = value.toLowerCase ? value.toLowerCase() : value
				if (!["limit", "market", "fok", "ioc", "post", "close", "open", "settle", "day"].includes(value)) {
					warn(`Invalid type option "${value}" (${origKey||key}=) specified – expecting either "limit", "market", "fok", "ioc", "post", "close", "open", "settle", "day"! (Line ${this._line})`)
				}
				this[key] = value; break

			case "u":
				value = value.toLowerCase ? value.toLowerCase() : value
				if (!["contracts", "ct", "currency", "ccy"].includes(value)) {
					warn(`Invalid unit option "${value}" (${origKey||key}=) specified – expecting either "contracts"/"ct" or "currency"/"ccy"! (Line ${this._line})`)
				}
				this[key] = ({"ct": "contracts", "ccy": "currency"})[value] || value; break

			case "y":
				value = !value && origKey === "equity" ? origKey : value.toLowerCase ? value.toLowerCase() : value
				const isTransfer = this.has('tb')
				if (!isTransfer && !["free", "balance", "total", "equity", "pos", "possize"].includes(value)) {
					warn(`Invalid yield option "${value}" (${origKey||key}=) specified – expecting either "free"/"balance", "total"/"equity" or "pos"/"possize"! (Line ${this._line})`)
				}
				this[key] = !isTransfer && ({"free": "balance", "total": "equity", "pos": "possize"})[value] || value; break

			case "ismargin":
				key = "isMargin"
			case "isMargin":
			case "bc":
			case "cr":
			case "d":
			case "exitall":
			case "fifo":
			case "ft":
			case "gsl":
			case "iso":
			case "logvars":
			case "lq":
			case "pauseall":
			case "pc":
			case "pp":
			case "r":
			case "restart":
			case "resumeall":
			case "retryall":
			case "rf":
			case "sv":
			case "ub":
			case "up":
				value = value.toLowerCase ? value.toLowerCase() : value
				this[key] = ({"1": true, "true": true, "on": true, "": true, "0": false, "false": false, "off": false})[value]
				if (key !== 'sv' || this[key] !== undefined) {
					if (this[key] === undefined) {
						warn(`Invalid option "${value}" (${origKey||key}=) specified – expecting a boolean! (Line ${this._line})`)
					}
					break
				}
				this[key] = true
				this.#own.push((key = "svn"))
			case "svn":
				if (!value) {
					warn(`No variable prefix (${origKey||key}=) specified! (Line ${this._line})`)
				} else if (Command.illegalVar.test(value)) {
					warn(`Invalid variable prefix (${origKey||key}=) specified – expecting only alphanumeric / underscore! (Line ${this._line})`)
				}
				this[key] = value.toLowerCase ? value.toLowerCase() : value; break

			case "cached":
				this.set("pc", value, warn, false, origKey, checkOnly, ohlc); this.set("bc", value, warn, false, origKey, checkOnly, ohlc);
				this.#own.push('pc', 'bc'); break
			case "update":
				this.up = this.ub = true; this.#own.push('up', 'ub'); break
			case "saveprice":
				this.set("sv", value, warn, false, origKey, checkOnly, ohlc); 
				this.up = true; this.#own.push('up', 'sv'); break
			case "savebalance":
				this.set("sv", value, warn, false, origKey, checkOnly, ohlc); 
				this.ub = true; this.#own.push('ub', 'sv'); break
			case "saveorder":
				this.set("sv", value, warn, false, origKey, checkOnly, ohlc); 
				this.ch = "order"; this.#own.push('ch', 'sv'); break
			case "savepos":
				this.set("sv", value, warn, false, origKey, checkOnly, ohlc); 
				this.ch = "position"; this.#own.push('ch', 'sv'); break

			case "e":
				value = unquote(value)
				if (value.includes && value.includes(',')) {
					const list = value.split(',').filter(Boolean).map(e => broker && broker.checkAlias(e.toUpperCase()))
					if (!list.length || list.includes(undefined)) {
						warn(`Invalid options "${value}" (${origKey||key}=) specified – expecting a list of exchange aliases! (Line ${this._line})`)
					}
					this['_'+key] = list, this[key] = list[0]
					break
				}
				if ((value || this.has(key)) && broker && !broker.checkAlias(value.toUpperCase())) {
					warn((value ? `Invalid exchange alias "${value}"` : "No exchange alias")+` (${origKey||key}=) specified – please use the "Symbol Browser" or check "Options / Accounts"! (Line ${this._line})`)
				}
				this[key] = broker && broker.checkAlias(value.toUpperCase()); break

			case "id":
			case "cid":
			case "eid":
			case "fid":
				value = unquote(value)
				if (!value) {
					warn(`No custom id (${origKey||key}=) specified! (Line ${this._line})`)
				} else if (Command.illegalId.test(value)) {
					warn(`Invalid custom id (${origKey||key}=) specified – expecting only alphanumeric / underscore / dash / asterisk! (Line ${this._line})`)
				}
				this[key] = value.toUpperCase ? value.toUpperCase() : value; break

			case "crossed":
				value = key
				this.#own.push((key = "mt"))
			case "mt":
				value = value.toLowerCase ? value.toLowerCase() : value
				if (!["isolated", "iso", "crossed", "cross", "borrow", "repay"].includes(value)) {
					warn(`Invalid margin type option "${value}" (${origKey||key}=) specified – expecting either "isolated"/"iso", "crossed"/"cross", "borrow", "repay"! (Line ${this._line})`)
				}
				this[key] = ({"iso": "isolated", "cross": "crossed"})[value] || value; break

			case "s":
				value = unquote(value)
				if (value.includes && value.includes(',')) {
					const list = value.split(',').filter(Boolean).map(e => e.toUpperCase())
					if (!list.length || list.includes("")) {
						warn(`Invalid options "${value}" (${origKey||key}=) specified – expecting a list of symbol names! (Line ${this._line})`)
					}
					this['_'+key] = list, this[key] = list[0]; break
				}
				if (!value && this.has(key)) {
					warn(`No symbol name (${origKey||key}=) specified! (Line ${this._line})`)
				}
				this[key] = value.toUpperCase ? value.toUpperCase() : value; break

			case "cmcid":
				if (!value) {
					warn(`No CMC ticker ID (${origKey||key}=) specified! (Line ${this._line})`)
				}
				this[key] = value.toLowerCase ? value.toLowerCase() : value; break

			case "skip":
				if (!value && origKey === "loop") value = Command.doLoop
			case "else":
			case "err":
			case "fb":
			case "ifd":
			case "ife":
			case "ifbg":
			case "ifl":
			case "ifn":
			case "ifnp":
			case "ifnnp":
			case "ifo":
			case "ifp":
			case "ifr":
			case "ifs":
			case "ifsl":
			case "ifslp":
			case "ifslnp":
			case "ifsm":
			case "ift":
			case "iftf":
			case "iftnf":
			case "iftp":
			case "iftnp":
			case "ifminb":
			case "ifmaxb":
			case "ifminsp":
			case "ifmaxsp":
			case "nr":
			case "wid":
			case "ws":
				value = value.toLowerCase ? value.toLowerCase() : value
				if (key !== "err") { // switched defaults for non-err!
					value = ({retry: "continue", continue: "retry"})[value] || value
				}
				this[key] = Command.err[value] === undefined ? (Number(value) || value) : Command.err[value]
				break

			case "end":
				this.skip = Command.goBack; this.#own.push('skip'); break

			case "exit":
				this.skip = Command.err.abort; this.#own.push('skip'); break

			case "log":
				value = value.toLowerCase ? value.toLowerCase() : value
				const force = value.startsWith && value.startsWith('!')
				force && (value = value.substr(1))
				this[key] = Command.log[value] === undefined ? parseInt(value) : Command.log[value]
				if (isNaN(this[key]) || this[key] < 0 || this[key] > 31) {
						warn(`Invalid log option "${value}" (${origKey||key}=) specified! (Line ${this._line})`)
				}
				force && (this[key] = -this[key])
				break;

			case "l":
				if (Command.customStart.test(value) || Command.prefs[alnumStart(value)]) {
					const params = value.split(/[*/]/)
					this.set('lr', params[0], warn, false, origKey, checkOnly, ohlc)
					value = params[1] || '1'
					value.includes('/') && (value = (1 / value).toString())
				}
			case "minp":
			case "maxp":
			case "minq":
			case "maxq":
				if (value.includes && value.includes(',')) {
					const list = value.split(',').map(e => Number(e || NaN))
					if (list.includes(NaN)) {
						warn(`Invalid options "${value}" (${origKey||key}=) specified – expecting a list of numbers! (Line ${this._line})`)
					}
					this['_'+key] = list, this[key] = list[0]; break
				}
			case "cbrate":
			case "lc":
			case "max":
			case "minhvd":
			case "maxhvd":
			case "minhvw":
			case "maxhvw":
			case "minhvm":
			case "maxhvm":
			case "min1h":
			case "max1h":
			case "min24h":
			case "max24h":
			case "min7d":
			case "max7d":
			case "ret":
			case "retries":
			case "timeout":
				this[key] = value !== "" ? Number(value) : key === "lc" || key === "ret" ? 1 : NaN
				if (isNaN(this[key])) {
					warn(`${value !== "" ? `Invalid option "${value}"` : "No value"} (${origKey||key}=) specified – expecting a number! (Line ${this._line})`)
				}
				break

			case "q":
				if (value?.endsWith) {
					if (value.endsWith("%p")) {
						this.y = "possize"; this.#own.push('y')
						value = value.slice(0, -1)
					} else if (value.endsWith("%e")) {
						this.y = "equity"; this.#own.push('y')
						value = value.slice(0, -1)
					} else if (value.endsWith("$")) {
						this.u = "currency"; this.#own.push('u')
						value = value.slice(0, -1)
					} else if (value.endsWith("%l")) {
						this.qr = "lastqty"; this.#own.push('qr'); this.lc = 1
						value = value.slice(0, -1)
					}
				}
			case "p":
				if (value?.endsWith && value.endsWith("%l")) {
					this.pr = "lastprice"; this.#own.push('pr')
					value = value.slice(0, -1)
				} /* $$$$$ add %p for pos support as well? */ 
			case "sl":
			case "tp":
			case "ts":
			case "so":
			case "pb":
			case "minpl":
			case "maxpl":
			case "mins":
			case "maxs":
			case "minsp":
			case "maxsp":
			case "gt":
			case "gte":
			case "lt":
			case "lte":
				// multi-account support but NO xxxref option...
			case "ep":
			case "fp":
			case "minb":
			case "maxb":
			case "msl":
			case "msle":
				// $$$$$ handle q=@lastqty*123% by saving & removing '@' here
				const custom = value?.match && value.match(Command.customStartEx)?.groups
				if (custom || Command.prefs[alnumStart(value)]) {
					if (Command.relRefParams.includes(key)) {	// handle quantity options separately... mult / div only
						const list = value.split(',')
						let isPercent = value = ""
						for (let i = 0; i < list.length; ++i) {
							let {groups: {ref, abs, perc, calc, val}} = list[i].match(/(?<ref>[$\w]*)(?<abs>@|)(?<perc>%|)(?<calc>[*/]|)(?<val>.*|)/)
							// ref  = _var $var
							// abs  = @ (levcalc 1)
							// perc = % (isPercent)
							// calc = * / (calc)
							// val  = val (calc value)
							if (!i) {
								this.set(key+(key === 'msl' ? 'v' : 'r'), ref, warn, false, origKey, checkOnly, ohlc)
								isPercent = perc
							}
							if ((val || i) && !calc) {
								warn(`Only multiplication (*) and division (/) supported as variable operations for ${origKey||key}=! (Line ${this._line})`)
							}
							if (val.endsWith('%')) val = parseFloat(val) / 100
							val ||= 1
							if (calc === '/' && !isNaN(val)) val = (1 / val).toString()
							if (abs) this.lc = 1
							value += (i > 0 ? ',':'') + val + isPercent
						}
					} else {
						if (custom?.perc || Command.noRefParams.includes(key)) {	// isPercent OR no ref option support (hardcoded values)
							if(value.includes(',')) warn(`Warning: Cannot specify values for multiple accounts in this shortcut style! (Line ${this._line})`)
							const val = globalVars.ohlc(custom?.var, null, ohlc)
							let num = val || 0
							if (custom?.calc) {	// [+-*/]1234
								const exp = custom.calc[0]
								// $$$$$$$$$$$ exp === '@' ???
								const op = custom.calc.slice(1)
								const n = Number(op || NaN) // $$$$ ohlc && ohlc[op] ?
								if (isNaN(n)) {
									warn(`${op !== "" ? `Invalid option "${op}"` : "No value"} (${origKey||key}=) for right-hand operand specified – expecting a number! (Line ${this._line})`)
								}
								if (!"+-*/".includes(exp)) {
									warn(`Only addition (+), subtraction (-), multiplication (*) and division (/) supported as % variable operations for ${origKey||key}=! (Line ${this._line})`)
								} else {
									num = exp === '+' ? num+(n||0) : exp === '-' ? num-(n||0) : exp === '*' ? num*(n||1) : num/(n||1)
								}
							}
							value = `${custom?.sign||''}${num}${custom?.perc||''}`
							if (!checkOnly) {
								if (val === undefined) {
									warn(`Unkown preprocessor variable "${custom?.var}" specified! (Line ${this._line})`)
								}/* else {
									warn(`Warning: Replacing variable "${custom?.var}"" with value ${value}! (Line ${this._line})`)
								}*/
							}
						} else {	// handle non-percent separately... plus / minus only
							if (custom?.sign) {
								warn(`Warning: Cannot use +/- in front of variable here – always absolute or % (Line ${this._line})`)
								value = value.slice(1)
							}
							const list = value.split(',')
							value = ""
							for (let i = 0; i < list.length; ++i) {
								let {groups: {ref, calc, val}} = list[i].match(/(?<ref>[$\w]*)(?<calc>[+-]|)(?<val>.*|)/)
								// ref  = _var
								// calc = + - (calc)
								// val  = val (calc value)
								if (!i) {
									this.set(key+(key === 'p' ? 'r' : 'ref'), ref, warn, false, origKey, checkOnly, ohlc)
								}
								if ((val || i) && !calc) {
									warn(`Only addition (+) and subtraction (-) supported as variable operations for ${origKey||key}=! (Line ${this._line})`)
								}
								value += (i > 0 ? ',':'') + (calc === '-' && val[0] !== '-' ? '-':'') + (val || '0')
							}
						}
					}
				}
				if (value.includes && value.includes(',')) {
					let isValid = true
					const list = value.split(',').map(e => new NumberObject(e))
					list.forEach(e => isValid = isValid && e && e.isValid())
					if (!isValid) {
						warn(`Invalid options "${value}" (${origKey||key}=) specified – expecting a list of numbers, ranges or percentages! (Line ${this._line})`)
					}
					this['_'+key] = list, this[key] = list[0]; break
				}
			case "rd":
				if (key === 'rd') {
					this.#own.push((key = "jd"))
					if (value.startsWith && !value.startsWith('-')) {
						value = '-'+value
					}
				}
			case "delay":
			case "from":
			case "to":
			case "jd":
			case "time":
				// $$$$$ add rd? check '-' upfront...
				if (["delay", "from", "to", "jd", "time"].includes(key) && value.slice && "shdwm".includes(value.slice(-1).toLowerCase())) {
					const mult = getTF(value.slice(-1), 1) * 60
					value = value.slice(0, -1).split('-').map(val => val === "" ? val : val * mult).join('-')
				}
			case "cm":
			case "h":
			case "yp":
				this[key] = new NumberObject(value)
				if (!this[key].isValid()) {
					warn(`${value !== "" ? `Invalid option "${value}"` : "No value"} (${origKey||key}=) specified – expecting a number, range or percentage! (Line ${this._line})`)
				} else if (key === 'q' && this[key].isFixed()) {
					this.lc = 1
				}
				break

			case "since":
				value = value.toLowerCase ? value.toLowerCase() : value
				if (!["fired", "bar", "candle", "cbar", "jump", "loop", "eod", "eodutc"].includes(value)) {
					warn(`Invalid "since" (max time/count) option "${value}" (${origKey||key}=) specified! (Line ${this._line})`)
				}
				this[key] = ({"candle": "bar", "loop": "jump"})[value] || value; break

			case "tb":
			case "loan":
			case "repay":
			case "convert":
			case "withdraw":
			case "lock":
			case "lockcheck":
			case "query":
				this[key] = value.split(',').map(e => e.trim().toUpperCase())
				if (!this[key].length || this[key].includes("")) {
					warn(`Invalid options "${value}" (${origKey||key}=) specified – expecting a parameter or list of parameters! (Line ${this._line})`)
				}
				if (key === 'query' && this[key].without(["TRADES", "TRANSFER", "PNL"]).length) {
					warn(`Invalid query account history option "${value}" (${origKey||key}=) specified – expecting either "trades", "transfer" or "pnl"! (Line ${this._line})`)
				}
				break

			case "cha":
			case "enable":
			case "disable":
			case "toggle":
			case "run":
			case "runen":
			case "trigger":
			case "triggeren":
				value = value.replace(/["']/g, '').split(',').numbers()
				if (value.length > 1 || value[0]) {
					for (const name of value) {
						if (!getAlertsByName(name).length && ![pvShortU, pvShortL, 'APP', 'app'].includes(name)) {
							warn(`Unknown alert "${name}" (${origKey||key}=) specified – expecting name or id of existent alert or '${pvShortU}' / 'APP' for global! (Line ${this._line})`)
						}
					}
				}
				this[key] = value; break

			case "interval":
				value = value.replace(/["']/g, '').split(',').map(e => (e[0] !== '+' && Number(e)) || e.trim())
				if (value.length < 2) value.unshift("")
				value[1] = value[1].toString()
				let check
				if (value.length != 2) {
					warn(`Wrong number of options (${origKey||key}=[<alert>,]<interval>) specified – expecting either alert id and interval/offset or only interval/offset! (Line ${this._line})`)
				} else if (value[0].length && !getAlertsByName(value[0]).length) {
					warn(`Unknown alert "${value[0]}" (${origKey||key}=[<alert>,]<interval>) specified – expecting name or id of existent alert! (Line ${this._line})`)
				} else if (value[1].length && (check = intervalMatch(value[1]))) {
					if (value[1][0] === '+' && (!isNaN(value[1]) || "shdwm".includes(value[1].slice(-1).toLowerCase()))) {
					} else {
						warn(check+` (${origKey||key}=[<alert>,]<interval>) – expecting either alert id and interval/offset or only interval/offset! (Line ${this._line})`)
					}
				}
				this[key] = value; break

			case "tr":
				const list = value.toLowerCase ? value.toLowerCase().split(',').map(e => !Command.prefs[e] && !Command.custom.test(e) ? Number(e) || NaN : e) : []
				if (list.includes(NaN)) {
					warn(`Invalid options "${value}" (${origKey||key}=) specified – expecting a list of OHLC vars or custom plots! (Line ${this._line})`)
				}
				this[key] = list; break

			case "a":
			case "ap":
				if (!value && this.has(key)) {
					warn(`No account name (${origKey||key}=) specified! (Line ${this._line})`)
				}
				value = unquote(value)
				if (value.includes && value.includes(',')) {
					let list = value.split(',').filter(Boolean)
					if (!list.length || list.includes("")) {
						warn(`Invalid options "${value}" (${origKey||key}=) specified – expecting a list of account names or "ex:sym:acc"! (Line ${this._line})`)
					}
					if (value.includes(':')) {
						// $$$ api-fy ex:sym:acc
						// $$$$$ check if ex= has been specified as new default before???
						const defEx = this._e?.[0] || this.e || opt.defExchange
						const defSym = this._s?.[0] || this.s || opt.defSymbol
						let exes = [], syms = []
						list = list.map(acc => {
							acc = acc.split(':')
							if (acc.length > 3) warn(`Warning: Too many parts (${origKey||key}=) specified – expecting "ex:sym:acc" format! (Line ${this._line})`)
							let alias = (acc.length > 1 && acc[0] || defEx).toUpperCase()
							const sym = (acc.length > 2 && acc[1] || defSym).toUpperCase()
							acc = acc.last() || '*'
							if (broker) {
								const ex = broker.getByAlias(alias)
								if (!ex) {
									warn(`Invalid exchange alias "${alias}" (${origKey||key}=) specified – expecting a list of account names or "ex:sym:acc"! (Line ${this._line})`)
								} else {
									alias = ex.getAlias()
									if (!ex.hasAccount(acc)) {
										warn(`Non-existent account "${acc}" for ${ex.getName()} (${origKey||key}=) specified (available: ${ex.getAccounts().join(', ')}) – expecting a list of account names or "ex:sym:acc"! (Line ${this._line})`)
									}
								}
							}
							exes.push(alias), syms.push(sym)
							return acc
						})
						this._e = exes, this.e = exes[0], this.#own.push('e')
						this._s = syms, this.s = syms[0], this.#own.push('s')
					}
					// $$$$ check account validity if e=/s= are known!
					this['_'+key] = list
				} else if (value.includes && value.includes(':')) {
					// $$$ api-fy ex:sym:acc
					value = value.split(':')
					if (value.length > 3) warn(`Warning: Too many parts (${origKey||key}=) specified – expecting "ex:sym:acc" format! (Line ${this._line})`)
					if (value[0]) {
						this.e = value[0].toUpperCase()
						if (broker) {
							const alias = broker && broker.checkAlias(this.e)
							if (!alias) {
								warn(`Invalid exchange alias "${this.e}" (${origKey||key}=) specified – expecting a list of account names or "ex:sym:acc"! (Line ${this._line})`)
							} else {
								this.e = alias, this.#own.push('e')
							}
						}
					}
					value.length > 2 && value[1] && (this.s = value[1].toUpperCase()) && this.#own.push('s')
					const ex = broker && broker.getByAlias(this.e || opt.defExchange)
					const acc = value.last() || '*'
					if (ex && !ex.hasAccount(acc)) {
						warn(`Non-existent account "${acc}" for ${ex.getName()} (${origKey||key}=) specified (available: ${ex.getAccounts().join(', ')}) – expecting a list of account names or "ex:sym:acc"! (Line ${this._line})`)
					}
					value = acc
				}
				this[key] = value; break

			case "echo":
				this.#own.push((key = 'n')); value = "log:"+value
			case "n":
				// $$$ check format!
			default:
				this[key] = value
		}
	}

	parseLine(syntax, defaults, line = 1, label, warn = Command.defaultWarn, noShortcuts, checkOnly, ohlc) {
		syntax = syntax.replace(/[#;](?:(?:[^"]*"){2})*[^"]*$/, '')
		this._line = line
		label && (this._label = label)

		const keyValMatch = /([^=\s]+)(=|)((?:\".*?\"|\(.*?\)|[^=\s]+?)*)/g
		if (!keyValMatch.test(syntax)) {
			return false
		}
		keyValMatch.lastIndex = 0

		let params = {}
		let keyVal
		while (keyVal = keyValMatch.exec(syntax)) {
			const origKey = keyVal[1] && keyVal[1].toLowerCase()
			const key = Command.#alias[origKey] || origKey
			params[key] = [keyVal[3].trim(), origKey]
			this.#own.push(key)
		}
		for (const key in defaults) {
			params[key] === undefined && this.set(key, defaults[key], Command.ignoreWarn, noShortcuts, undefined, checkOnly, ohlc)
		}
		for (const key in params) {
			if (key[0] === '[') {
				warn(`Invalid or unclosed (missing [end]) preprocessor directive encountered! (Line ${line})`)
				continue
			}
			if (!Command.#cmds[key]) {
				warn(`Warning: Unknown parameter "${key}" used in syntax! (Line ${line})`)
				continue
			}
			this.set(key, params[key][0], warn, noShortcuts, params[key][1], checkOnly, ohlc)
		}

		if ((this.ifbg || this.ifsm || this.ifl || this.ifs) && !(this.c || this.ch)) {
			// $$$$$ check this.ifbuy this.ifsell => "order"?
			this.ch = "position"; this.#own.push('ch')
		} else if (this.ifp && !(this.c || this.ch)) {
			this.ch = "order"; this.#own.push('ch')
		}
		return true
	}

	static parseSyntax(syntax, defaults, checkOnly, noShortcuts, id, ohlc) {
		const lines = syntax.split(/[\n|]/)
		let cmds = []

		const labelMatch = /^:([\w,]*)\s*(.*)/g
		let labels = []
		cmds.labels = {}

		cmds.warnings = []
		cmds.warnings.errors = 0

		function warn(msg) {
			const isErr = !msg.startsWith("Warning")
			if (!checkOnly) {
				if (isErr) throw new SyntaxError(msg)
				log.warn((id?`[${id}] `:'')+msg)
			}
			msg = msg.split(" (Line")
			cmds.warnings.push({
				row: parseInt(msg[1])-1 || 0,
				text: msg[0],
				type: isErr ? "error" : "warning"
			})
			isErr && cmds.warnings.errors++
		}

		for (let i = 1; i <= lines.length; ++i) {
			const line = lines[i-1]
			labelMatch.lastIndex = 0

			const isLabel = labelMatch.exec(line)
			if (isLabel) {
				const label = isLabel[1].toLowerCase()
				if (isLabel[2] && !(isLabel[2][0] === '#' || isLabel[2][0] === ';')) {
					warn(`Labels have to be specified in a line by themselves (use comma for multiple) and can't contain spaces! (Line ${i})`)
				} else if (!label) {
					warn(`No name for label specified! (Line ${i})`)
				} else if (Command.err.hasOwnProperty(label)) {
					warn(`Label "${label}" using reserved name! (Line ${i})`)
				} else if (cmds.labels[label]) {
					warn(`Duplicate label "${label}" specified! (Line ${i})`)
				} else {
					// $$$$ move split up to fix name checks
					labels.push(...label.split(','))
				}
				continue
			}

			const cmd = new Command()
			// $$$$ implement concat multi-line syntax (via '\'+br)
			if (cmd.parseLine(line, defaults, i, labels.last(), warn, noShortcuts, checkOnly, ohlc)) {
				cmds.push(cmd)
				if (labels.length) {
					labels.forEach(label => cmds.labels[label] = cmds.length)
					labels = []
				}
			}
		}
		labels.forEach(label => cmds.labels[label] = cmds.length+1)

		for (const cmd of cmds) {
			Command.jumpKeys.forEach(key => {
				const label = cmd[key]
				if (label && isNaN(label) && !cmds.labels[label]) {
					if (key !== 'fb' && cmd.has('fb')) {
						cmd[key] = cmd.fb
					} else {
						warn(`Non-existent label "${label}" specified for parameter "${key}"! (Line ${cmd._line})`)
					}
				}
			})
		}
		return cmds
	}

	static evalExp(exp, vars, vars2, checkOnly, warn = log.warn) {
		// $$$$ add support for [if high/low-1 > 0.5]
		if (!(exp = exp.match(/(?<lhs>[$\w\.]+)\s*(?:(?<op>[^\w\s"'{}]*)\s*(?<rhs>.*))?/)?.groups)) {
			return warn(`Syntax error in preprocessor comparison!`)
		}
		let {lhs, op, rhs} = exp
		rhs = rhs?.trim()
		if (op === '=*') { op = '='; rhs = '*'+rhs }

		let val = globalVars.ohlc(lhs, vars, vars2)
		if (val === undefined) {
			val = lhs
			if (isNaN(lhs)) {
				if (checkOnly && (Command.custom.test(lhs) || Command.prefs[lhs])) val = 1
				else return warn(`Unkown preprocessor variable "${lhs}" specified!`)
			}
		}
		if (rhs === undefined || rhs === '') {
			if (!op) return val != 0
			return warn(`Missing right-hand argument in preprocessor comparison for "${lhs}"!`)
		}
		rhs = mustacheFields(vars2, rhs)	// $$$$$$$ add warning if undefined global var used!!!
		if (isNaN(val) || op.includes('*') || rhs.includes('"') || rhs.includes(',')/* || rhs.includes('*')*/) {
			// $$$$ support variable on other side (rhs) !
			if (!['=', '==', '*=', '!=', '!*=', '<>'].includes(op)) {
				return warn((op ? `Unkown preprocessor comparison "${op}"` : "Missing preprocessor comparison")+` for string comparison! (use only =, ==, *=, !=, !*= or <> for strings)`)
			}
			const list = rhs.split(',')
			const result = op.includes('*') ? val.toString().startsWithAny(list.map(unquote), true) : list.includesNoCase(val.toString(), 2)
			return op[0] === '=' || op[0] === '*' ? result : !result
		} else {
			rhs = rhs === 'NaN' || rhs === 'nan' || rhs === 'na' ? NaN : Command.solveExp(rhs, vars, vars2, checkOnly, warn)
			switch (op) {
				case  '<': return val  < rhs
				case '<=': return val <= rhs
				case  '=':
				case '==': return val == rhs
				case '<>':
				case '!=': return val != rhs
				case '>=': return val >= rhs
				case '>':  return val  > rhs
			}
			return warn((op ? `Unkown preprocessor comparison "${op}" specified!` : "Missing preprocessor comparison!")+` (use any of <, <=, =, ==, <>, !=, >=, >)`)
		}
	}

	static evalIf(exp, data, ohlc, checkOnly, warn = log.warn) {
		let result = false
		exp = exp.split(/ or /i)
		for (let orExp of exp) {
			orExp = orExp.split(/ and /i)
			let and = true
			for (let andExp of orExp) {
				const not = andExp.startsWith("not ") || andExp.startsWith("NOT ")
				if (not) andExp = andExp.substr(4)
				let evalRes = Command.evalExp(andExp, data, ohlc, checkOnly, warn)
				if (not ? evalRes : !evalRes) {
					and = false
					// $$$ what if msg.checkOnly (= opt.legacyIgnErr)
					if (!checkOnly) break
				}
			}
			if (and) {
				result = true
				// $$$ what if msg.checkOnly (= opt.legacyIgnErr)
				if (!checkOnly) break
			}
		}
		return result
	}

	static isExp(exp) {
		return exp?.length > 2 && /[+\-*/%^]/.test(exp)
	}

	static solveExp(exp, vars, vars2, checkOnly, warn = log.warn) {
		const add = (l, r) => l + r
		const sub = (l, r) => l - r
		const mul = (l, r) => l * r
		const div = (l, r) => l / r
		const mod = (l, r) => l % r
		const pow = (l, r) => Math.pow(l, r)
		const f = {'+': add, '-': sub, '*': mul, '/': div, '%': mod, '^': pow}
		const fkeys = Object.keys(f).filter(op => exp.includes(op))
		if (!fkeys.length) fkeys.push('+')

		const split = (str, div) => {
			const result = []
			let parentheses = 0, part = 0
			for (let i = 0; i < str.length; ++i) {
				const ch = str[i]
				if (ch === '(') ++parentheses
				else if (ch === ')') --parentheses
				else if (ch === div && !parentheses) {
					const sub = str.substring(part, i).trim()
					if ((div === '+' || div === '-') && (sub === "" || fkeys.includes(sub.slice(-1)))) continue
					result.push(sub)
					part = i + 1
				}
			}
			result.push(str.substring(part).trim())
			if (parentheses) result._parentheses = parentheses
			return result
		}

		const calc = (subexp, ops = [...fkeys]) => {
			const op = ops._next()
			const init = op === '+' ? 0.0 : op === '*' ? 1.0 : null
			const parts = split(subexp, op)
			if (parts._parentheses) {
				warn(`${Math.abs(parts._parentheses)} ${parts._parentheses < 0 ? 'stray closing' : 'unclosed'} parentheses found!`)
				return 0
			}

			const nums = ops.isEnd() ? parts.map(part => {
				let sign = 1
				while (part[0] === '+' || part[0] === '-') {
					if (part[0] === '-') sign *= -1
					part = part.slice(1)
				}
				if (part[0] === '(') {
					return calc(part.slice(1, -1))
				}
				if (isNaN(part) || part === "") {
					let val = globalVars.ohlc(part, vars, vars2)
					if (isNaN(val)) {
						if (checkOnly && (Command.custom.test(part) || Command.prefs[part])) {
							return 1 //ok
						} else if (part === "" || (val === undefined && fkeys.includes(part))) {
							warn(`Missing operand(s) in expression "${exp}"!`)
						} else if (val === undefined || val === null) {
							warn(`Unkown variable "${part}" specified in expression "${exp}"!`)
						} else {
							warn(`Variable "${part}" in expression "${exp}" is not a number!`)
						}
						return (parseFloat(val) || 0) * sign
					}
					return +val * sign
				}
				return +part * sign
			}) : parts.map(part => calc(part, ops))

			ops._prev()
			return nums.reduce(f[op], init === null ? nums.shift() : init) || 0
		}
		const result = calc(exp)
		if (result === Infinity) {
			!checkOnly && warn(`Division by zero in expression "${exp}"!`)
			return 0
		}
		return result
	}

	merge(params, doSet) {
		for (const key in params) {
			const value = params[key]
			if (value !== undefined && !this.has(key)) {
				if (doSet) {
					this.set(key, /*Array.isArray(value) ? value.join(',') : */value, Command.ignoreWarn)
				} else if (Array.isArray(value)) {
					this['_'+key] = value
					this[key] = value[0]
				} else {
					this[key] = value
				}
			}
		}
	}

	extract(dest, params, override, doFlat) {
		let result = 0
		for (const key of params) {
			if (key && (this.has(key) || (override && override[key] !== undefined))) {
				const arr = this['_'+key]
				dest[key] = !doFlat && arr && Array.isArray(arr) ? arr : this[key]
				++result
			}
		}
		return result
	}

	has(key) {
		return this.#own.includes(key)
	}

	hasAny(keys) {
		return keys.without(this.#own).length != keys.length
	}

	clone(overwrite) {
		return Object.assign(new Command(), this, overwrite)
	}

	listNext() {
		Command.listParams.forEach(param => {
			const arr = this['_'+param]
			if (arr && arr._next) this[param] = arr._next()
		})
	}
	listPrev() {
		Command.listParams.forEach(param => {
			const arr = this['_'+param]
			if (arr && arr._prev) arr._prev()
		})
	}
	listGoto(idx) {
		let i = -1
		Command.listParams.forEach(param => {
			if (++i) {
				const arr = this['_'+param]
				if (arr && Array.isArray(arr)) this[param] = arr[idx]
			}
		})
	}
	listReset() {
		Command.listParams.forEach(param => {
			const arr = this['_'+param]
			if (arr && arr.reset) arr.reset()
		})
	}

	toJSON() {
		let result = {}
		for (const key in this) {
			if (this[key] !== null && typeof this[key] === 'object' && this[key].toJSON) {
				result[key] = this[key].toJSON()
				continue
			}
			result[key] = this[key]
		}
		return result
	}
}

class Alert {
	constructor(msg, defaults, doReplace, checkOnly, overrides, pvid, isChild = 0) {
//		const t1 = performance.now()
		const sym = (msg.sym || ':').split(':')
		if (sym.length < 2) sym.unshift(opt.defExchange || 'BYBIT-TESTNET')
		defaults = Object.assign({
			a: "*",
			cm: "100%",
			cmo: "oldest",
			e: sym[0],
			p: 0,
			q: "100%",
			s: sym[1],
			t: "limit",
			u: "contracts",
			y: "balance",
			sr: "last",
			jd: "-3"
		}, defaults || {})

		msg.desc ||= ""
		checkOnly ||= msg.checkOnly
		pvid ||= msg.id

		let preWarn = []
		if (doReplace) {
			try {
				// $$$$$$$$ add warning if [side] or similar used but not clearly defined side info available!!!
				const fired = msg.fire_time || new Date()
				const name = msg.name?.split(',')[0] || ""
				const nameLC = name.toLowerCase()
				const parts = name.toUpperCase().split(/[\W_]/).filter(Boolean)
				const id = parts[0]
				const side = msg.side || (nameLC.includes("short") ? "short" : nameLC.includes("long") ? "long" : nameLC.includes("sell") ? "sell" : nameLC.includes("buy") ? "buy" : 
					nameLC.includes("s") ? "short" : nameLC.includes("l") ? "long" : nameLC.includes("b") ? "buy" : "")
				const inv = side == "short" ? "long" : side == "long" ? "short" : side == "sell" ? "buy" : side == "buy" ? "sell" : ""
				const plus = side == "short" || side == "sell" ? '-' : '+'
				const minus = side == "short" || side == "sell" ? '+' : '-'
				const aliases = broker?.getAliases()
				const ex = aliases?.includesAny(parts)
				let exx = overrides?.e || ex && parts[ex-1] || defaults.e || opt.defExchange || 'BYBIT-TESTNET'
				let syy = overrides?.s || ex && parts[ex  ] || defaults.s || opt.defSymbol   || 'BTCUSDT'
				let acc = overrides?.a || ex && parts[ex+1] || defaults.a || opt.defAccount  || '*'
				if (Array.isArray(exx)) exx = exx[isChild]
				if (Array.isArray(syy)) syy = syy[isChild]
				if (Array.isArray(acc)) acc = acc[isChild]
				const data = {
					"ex": exx, "sym": syy,
					"acc": acc, "ACC": acc.toUpperCase(),
					"accx": acc === '*' ? '' : acc,
					"name": name, "NAME": name.toUpperCase(),
					"id": id, "ID": id,
					"res": msg.res || opt.defaultRes,
					"tf": formatTF(msg.res || opt.defaultRes),
					"side": side, "!side": inv,
					"SIDE": side.toUpperCase(), "!SIDE": inv.toUpperCase(),
					"+": plus, "-": minus,
					"long" : plus === '+' ? 1 : 0,
					"short": plus === '-' ? 1 : 0,
					"alert": msg.screener || msg.isManual || msg.isAuto ? 0 : 1, "screener": msg.screener ? 1 : 0, "manual": msg.isManual ? 1 : 0,
					"auto": msg.isAuto ? 1 : 0, "internal": msg.isInternal ? 1 : 0, "fork": msg.isFork ? 1 : 0, "telebot": msg.telebot || 0,
					"mode": msg.isFork ? "fork" : msg.isInternal ? "internal" : msg.isAuto ? "auto" : msg.isManual ? "manual" : msg.screener ? "screener" : "alert",
					"caller": msg.caller || "",
					"this": msg.alert || "",
					"disabled": opt.disableAll,
					"inst": opt.instName || pvShortU,
					"weekday": fired.getDay(),		// 0-6; 0 = sunday
					"month": fired.getMonth()+1,	// 1-12
					"day": fired.getDate(),			// 1-31
					"date": (fired.getMonth()+1)*100+fired.getDate(),	// 0101 - 1231
					"hour": fired.getHours(),		// 0-23
					"minute": fired.getMinutes(),	// 0-59
					"time": fired.getHours()*100+fired.getMinutes(),	// 0000 - 2359
					"weekend": fired.getDay() < 1 || fired.getDay() > 5 ? 1 : 0,
					"slip": msg.slip, "barpos": msg.bar_pos, "bartime": (msg.fire_time - msg.bar_time) / 1000,
				}

				const isParam = (i, raw) => {
					let inBlock = 0, inParam = 0
					while (i > 0 && raw[--i] !== '\n') {
						if (raw[i] === ']') ++inBlock
						else if (raw[i] === '[') --inBlock
						if (!inBlock && !inParam) {
							if (raw[i] === '=') ++inParam
							else if (raw[i] === ' ' || raw[i] === '\t') --inParam
						}
						if (raw[i] === '#') return -1
					}
					return inParam
				}
				const warn = (text, idx, raw) => {
					const row = raw.count('\n', 0, idx)
					if (checkOnly) preWarn.push({row: row, text: text, type: "warning"})
					else log.warn((pvid?`[${pvid}] `:'') + text + ` (Line ${row})`)
					return false
				}
				msg.descRaw ||= msg.desc
				msg.desc = msg.descRaw.replace(/\[([^\[\]]+)\]/g, (match, exp, idx, raw) => {
					if (["if", "else", "end", "endif"].includes(exp.split(' ', 1)[0].toLowerCase())) {
						return match
					} else if (data.hasOwnProperty(exp)) {
						return data[exp] || 0
					} else if (exp.startsWith("accs")) {
						if (!broker) return checkOnly ? '*' : ''
						let name = (exp.split(/\s+/, 2)[1] || '').split(':', 3)
						function closestEx() {
							const ex = getLine(raw, idx).match(/[(,;\s](?:e|ex|exchange)=([\w\d\-_]+)/i)
							return ex?.[1]?.toUpperCase()
						}
						let alias = name.length > 1 && name.shift().toUpperCase() || closestEx() || defaults.e || (ex && parts[ex-1]) || opt.defExchange || 'BYBIT-TESTNET'
						const sym = name.length > 1 && name.shift()
						name = name[0]
						const e = broker.getByAlias(alias)
						if (!e) {
							warn(`Unkown exchange alias "${alias}" specified!`, idx, raw)
							return checkOnly ? '*' : ''
						}
						alias = e.getAlias()
						// $$$ handle alias = *
						const prefix = sym !== false ? `${alias}:${sym}:` : ''
						let accs = e.getAccounts()
						// $$$$ handle alert(ap=[accs]) without ""
						// ==> check if enclosed in () and no "" then add
						if (!name || name === '*') {
							!accs.length && warn(`No accounts configured for ${e.getName()}! – please check "Options / Accounts"!`, idx, raw)
							return accs.map(acc => prefix+acc).toString() || (checkOnly ? '*' : '')
						}
						const wild = wildcardExp(mustacheFields(data, name))
						accs = accs.filter(acc => wild.test(acc)).map(acc => prefix+acc).toString()
						!accs.length && warn(`No accounts match "${name}" for ${e.getName()}! (available: ${e.getAccounts().join(', ')})`, idx, raw)
						return accs || (checkOnly ? '*' : '')
					} else if (Command.isExp(exp)) {
						return Command.solveExp(exp, data, msg.ohlc, checkOnly, text => warn(text, idx, raw))
					} else if (Command.custom.test(exp) || Command.prefs[exp]) {
						const isGlobal = exp[0] === '$'
						const val = globalVars.ohlc(mustacheFields(msg.ohlc, exp), null, msg.ohlc)
						if (val === undefined && isGlobal && isParam(idx, raw) >= 0) {
							warn(`Non-existent global variable "${exp}" specified!`, idx, raw)
						}
						return val === "" && isGlobal ? val : toDecimal(val || 0, 10)
					}
					warn(`Unkown preprocessor directive "${exp}" specified!`, idx, raw)
					return match

				}).replace(/\[if ([^\[\]]+)\](.*?)(?:\[(?:else)?\](.*?))?\[end(?:if)?\]/gis, (match, exp, yes, no, idx, raw) => {
					const inParam = isParam(idx, raw)
					if (inParam < 0) return match
					no ||= ""
					const result = Command.evalIf(exp, data, msg.ohlc, checkOnly, text => warn(text, idx, raw))
					// $$$ if inParam => add ` {param}={xxx}`+no
//					return checkOnly ? yes+(no ? (inParam ? ',' : ' ')+no : '') : result ? yes : no
					return checkOnly ? yes+(no ? (inParam > 0 ? '' : ' '+no) : '') : result ? yes : no
				})

				;[/*"side", */"!side", /*"SIDE", */"!SIDE", "+", "-"].forEach(key => delete data[key])
				Object.assign(msg.ohlc, data)

			} catch(ex){}
			if (doReplace < 0) {
				delete msg.descRaw
				return
			}
		}

		this.raw = msg.desc
		this.commands = Command.parseSyntax(msg.desc, defaults, checkOnly, msg.noShortcuts, pvid, msg.ohlc)
		preWarn.length && this.commands.warnings.unshift(...preWarn)
		this.eid = (msg.aid && msg.id) || 0
		this.id = msg.aid || msg.id || 0
//		console.log(`${performance.now()-t1}ms`)
	}

	getWarn() {
		return this.commands && this.commands.warnings
	}
}

function syntaxCheck(syntax, name, side = "short") {
	return new Alert({desc: syntax, name, side}, {a: opt.defAccount, e: opt.defExchange, s: opt.defSymbol}, true, true).getWarn()
}
