"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",
		d: "Disabled",
		disable: "Disable Alert",
		delay: "Delay",
		e: "Exchange",
		echo: "Print to log",
		eid: "Error Handling Cancel/Close ID",
		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",
		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",
		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",
		maxpl: "Max Profit Loss",
		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",
		mslr: "Max Slippage Reference",
		mt: "Margin Type",
		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",
		set: "Set OHLC variable",
		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",
		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",
		up: "Update Price",
		update: "Update Price / Balance",
		wf: "Wait For", // $$$
		wid: "Wrong Order ID",
		withdraw: "Withdraw Balance", // $$$
		ws: "Wrong Side",
		y: "Yield",
	}
	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",
		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",
		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",
		maxpnl: "maxpl",
		minquantity: "minq",
		maxquantity: "maxq",
		minsize: "mins",
		minsizeref: "minsr",
		maxsize: "maxs",
		maxsizeref: "maxsr",
		minspr: "minspref",
		maxspr: "maxspref",
		marketclose: "mc",
		closemarket: "mc",
		marketcloseall: "mca",
		maxslip: "msl",
		maxslipref: "mslr",
		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",
		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",
		updateprice: "up",
		updateticker: "up",
		wait: "delay",
		waitfor: "wf",
		wrongid: "wid",
		wrongside: "ws",
		yield: "y",
		equity: "y",
	}

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

	static prefs = ['ask', 'bid', 'mid', 'last', 'pos', 'even', 'liq', 'mark', 'index', 'open', 'high', 'low', 'close', 'vol', 'oc', 'hl', 'ohlc', 'price', 
				'filled', 'filled2', 'filled3', 'left', 'left2', 'left3', 'lastqty', 'lastqty2', 'lastqty3', 'lastprice', 'lastprice2', 'lastprice3', 'laststop', 'laststop2', 'laststop3', 'lasttp', 'lasttp2', 'lasttp3', 
				'lastsl', 'lastsl2', 'lastsl3', 'lastid', 'lastid2', 'lastid3', 'lastside', 'lastside2', 'lastside3', 'lasttype', 'lasttype2', 'lasttype3', 'laststatus', 'laststatus2', 'laststatus3', 
				'lastavg', 'lastavg2', 'lastavg3', 'lastqtysum', 'lastqtysum2', 'lastqtysum3', 'lastpnl', 'lastpnl2', 'lastpnl3', 'lastpnltotal', 'lastpnltotal2', 'lastpnltotal3', 'lastpnlsum', 'lastpnlsum2', 'lastpnlsum3', 
				'lastask', 'lastask2', 'lastask3', 'lastbid', 'lastbid2', 'lastbid3', 'lastmid', 'lastmid2', 'lastmid3']
	static jumpKeys = ['fb', 'err', 'ifd', 'ife', 'ifbg', 'ifl', 'ifn', 'ifnp', 'ifnnp', 'ifo', 'ifp', 'ifr', 'ifs', 'ifsl', 'ifslp', 'ifslnp', 'ifsm', 'ift', 'iftf', 'iftnf', 'iftp', 'iftnp', 'ifminb', 'ifmaxb', 'ifminsp', 'ifmaxsp', 'nr', 'skip', 'wid', 'ws']
	static custom = /^(_[\w]+|custom\d+)$/
	static customStart = /^(_[\w]+|custom\d+)/
	static customStartEx = /^([+\-])?(_[\w]+|custom\d+)(%|)(.*)?/

	static err = {
		cancel: -7,
		cancelall: -6,
		cancelside: -5,
		closemarket: -4,
		closeall: -3,
		closeside: -2,
		abort: -1,
		back: -0.5,
		this: -0.2,
		retry: 0,
		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]/

	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 = 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); 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)
				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 "minsr":
			case "maxsr":
			case "mslr":
			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.includes(value) && !Command.custom.test(value)) {
					warn(`Invalid ${({"lr": 'leverage', "mslr": 'slippage', "spa": 'spread ask', "spb": 'spread bid', "qr": 'quantity', "minsr": 'min size', "maxsr": 'max size'})[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 "set":
				// $$$$$$$$$$$$
				// set=var1:val1,var2:(calc),...
				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 "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); this.set("bc", value, warn, false, origKey);
				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); 
				this.up = true; this.#own.push('up', 'sv'); break
			case "savebalance":
				this.set("sv", value, warn, false, origKey); 
				this.ub = true; this.#own.push('ub', 'sv'); break
			case "saveorder":
				this.set("sv", value, warn, false, origKey); 
				this.ch = "order"; this.#own.push('ch', 'sv'); break
			case "savepos":
				this.set("sv", value, warn, false, origKey); 
				this.ch = "position"; this.#own.push('ch', 'sv'); break

			case "e":
				if (value.includes && value.includes(',')) {
					const list = value.split(',').map(e => broker && broker.checkAlias(e.toUpperCase()))
					if (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":
				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 "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":
				if (value.includes && value.includes(',')) {
					const list = value.split(',').map(e => e.toUpperCase())
					if (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 "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) || (value.startsWithAny && value.startsWithAny(Command.prefs))) {
					const params = value.split(/[*/]/)
					this.set('lr', params[0], warn, false, origKey)
					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 && 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)
					}
				}
			case "p":
			case "sl":
			case "tp":
			case "ts":
			case "so":
			case "pb":
			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 "minpl":
			case "maxpl":
			case "msl":
				const custom = value && value.match && value.match(Command.customStartEx)
				if (custom || (value.startsWithAny && value.startsWithAny(Command.prefs))) {
					if (key === 'q' || key === 'mins' || key === 'maxs') {	// handle quantity options separately... mult / div only
						const list = value.split(',')
						let isPercent = value = ""
						for (let i = 0; i < list.length; ++i) {
							const params = list[i].match(/(\w*)(@|)(%|)([*/]|)(.*|)/)
							// params[1] = _var
							// params[2] = @ (levcalc 1)
							// params[3] = % (isPercent)
							// params[4] = *,/ (calc)
							// params[5] = val (calc value)
							if (!i) {
								this.set(key+'r', params[1], warn, false, origKey)
								isPercent = params[3]
							}
							if ((params[5] || i) && !params[4]) warn(`Only multiplication (*) and division (/) supported as variable operations for ${origKey||key}=! (Line ${this._line})`)
							if (params[5] && params[5].endsWith('%') && isPercent) params[5] = params[5].slice(0, -1)
							let part = params[5] || '1'
							if (params[4] === '/' && !isNaN(part)) part = (1 / part).toString()
							if (params[2] === '@') this.lc = 1
							value += (i > 0 ? ',' : '') + part + isPercent
						}
					} else {
						const isPercent = custom && custom[3]
						if (isPercent || 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 = ohlc && ohlc[custom[2]]
							let num = val || 0
							if (custom[4]) {	// [+-*/]1234
								const exp = custom[4][0]
								const op = custom[4].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[1] || ''}${num}${isPercent}`
							if (!checkOnly) {
								if (val === undefined) {
									warn(`Unkown preprocessor variable "${custom[2]}" specified! (Line ${this._line})`)
								}/* else {
									warn(`Warning: Replacing variable "${custom[2]}"" with value ${value}! (Line ${this._line})`)
								}*/
							}
						} else {	// handle non-percent separately... plus / minus only
							if (custom && custom[1]) {
								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) {
								const params = list[i].match(/(\w*)([+-]|)(.*|)/)
								// params[1] = _var
								// params[2] = +,- (calc)
								// params[3] = val (calc value)
								if (!i) {
									this.set(key+(key === 'p' ? 'r' : 'ref'), params[1], warn, false, origKey)
								}
								if ((params[3] || i) && !params[2]) warn(`Only addition (+) and subtraction (-) supported as variable operations for ${origKey||key}=! (Line ${this._line})`)
								let part = params[3] || '0'
								if (params[2] === '-' && part[0] !== '-') part = '-'+part
								value += (i > 0 ? ',' : '') + part
							}
						}
					}
				}
				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 "jd":
			case "time":
				// $$$$$ add rd? check '-' upfront...
				if (["delay", "from", "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":
				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"].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 existing 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 existing 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.includes(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})`)
				}
				if (value.includes && value.includes(',')) {
					let list = value.split(',')
					if (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
						const defEx = (this._e && this._e[0]) || this.e || opt.defExchange
						const defSym = (this._s && 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
			default:
				this[key] = value
		}
	}

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

	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-existing 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(/([\w\.]+)\s*(?:([^\w\s"']*)\s*(.*))?/))) {
			return warn(`Syntax error in preprocessor comparison!`)
		}
		if (exp[2] === '=*') { exp[2] = '='; exp[3] = '*'+exp[3] }
		const v = exp[1]
		const op = exp[2]
		const c = exp[3] && exp[3].trim()

		let val = vars.hasOwnProperty(v) ? vars[v] : vars2 && vars2[v && v.toLowerCase()]
		if (val === undefined) {
			val = v
			if (isNaN(v)) {
				if (checkOnly && (Command.custom.test(v) || Command.prefs.includes(v))) val = 1
				else return warn(`Unkown preprocessor variable "${v}" specified!`)
			}
		}
		if (c === undefined || c === '') {
			if (!op) return val != 0
			return warn(`Missing right-hand argument in preprocessor comparison for "${v}"!`)
		} else if (isNaN(val) || op.includes('*') || c.includes('"') || c.includes(',')/* || c.includes('*')*/) {
			if (!['=', '==', '*=', '!=', '!*=', '<>'].includes(op)) {
				return warn((op ? `Unkown preprocessor comparison "${op}"` : "Missing preprocessor comparison")+` for string comparison! (use only =, ==, *=, !=, !*= or <> for strings)`)
			}
			const list = c.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 {
			const comp = c === 'NaN' || c === 'nan' || c === 'na' ? NaN : Command.solveExp(c, vars, vars2, checkOnly, warn)
			switch (op) {
				case  '<': return val  < comp
				case '<=': return val <= comp
				case  '=':
				case '==': return val == comp
				case '<>':
				case '!=': return val != comp
				case '>=': return val >= comp
				case '>':  return val  > comp
			}
			return warn((op ? `Unkown preprocessor comparison "${op}" specified!` : "Missing preprocessor comparison!")+` (use any of <, <=, =, ==, <>, !=, >=, >)`)
		}
	}

	static solveExp(exp, vars, vars2, checkOnly, warn = log.warn) {
		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) {
					result.push(str.substring(part, i).trim())
					part = i + 1
				}
			}
			result.push(str.substring(part).trim())
			parentheses && (result._parentheses = parentheses)
			return result
		}
		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)

		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
			}

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

			ops._prev()
			return nums.reduce(f[op], init === null ? nums.shift() : init) || 0
		}
		const result = calc(exp)
		return result === Infinity ? 0 : result
	}

	merge(params, set) {
		for (const key in params) {
			const value = params[key]
			if (value !== undefined && !this.has(key)) {
				if (set) {
					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, flat) {
		let result = 0
		for (const key of params) {
			if (key && (this.has(key) || (override && override[key] !== undefined))) {
				const arr = this['_'+key]
				dest[key] = !flat && 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]
			arr && arr._next && (this[param] = arr._next())
		})
	}
	listPrev() {
		Command.listParams.forEach(param => {
			const arr = this['_'+param]
			arr && arr._prev && arr._prev()
		})
	}
	listGoto(idx) {
		let i = -1
		Command.listParams.forEach(param => {
			if (++i) {
				const arr = this['_'+param]
				arr && Array.isArray(arr) && (this[param] = arr[idx])
			}
		})
	}
	listReset() {
		Command.listParams.forEach(param => {
			const arr = this['_'+param]
			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(':')
		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 = msg.desc || ""
		checkOnly = checkOnly || msg.checkOnly
		pvid = 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 && 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 && broker.getAliases()
				const ex = aliases && aliases.includesAny(parts)
				let exx = overrides && overrides.e || ex && parts[ex-1] || defaults.e || opt.defExchange || 'BYBIT-TESTNET'
				let syy = overrides && overrides.s || ex && parts[ex  ] || defaults.s || opt.defSymbol   || 'BTCUSDT'
				let acc = overrides && overrides.a || ex && parts[ex+1] || defaults.a || opt.defAccount  || '*'
				Array.isArray(exx) && (exx = exx[isChild])
				Array.isArray(syy) && (syy = syy[isChild])
				Array.isArray(acc) && (acc = acc[isChild])
				const data = {
					"ex": exx, "sym": syy,
					"acc": acc, "ACC": acc.toUpperCase(),
					"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,
					"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,
					"telebot": msg.telebot || 0,
					// barpos/time $$$$$
				}

				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.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 (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 && ex[1] && 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 (exp.length > 2 && /[+\-*/%^]/.test(exp)) {
						return Command.solveExp(exp, data, msg.ohlc, checkOnly, text => warn(text, idx, raw))
					} else if (Command.custom.test(exp) || Command.prefs.includes(exp)) {
						return toDecimal(msg.ohlc && msg.ohlc[exp.toLowerCase()] || 0, 10)
					} else if (data.hasOwnProperty(exp)) {
						return data[exp] || 0
					}
					warn(`Unkown preprocessor directive "${exp}" specified!`, idx, raw)
					return match

				}).replace(/\[if ([^\[\]]+)\](.*?)(?:\[(?:else)?\](.*?))?\[end(?:if)?\]/gis, (match, exp, yes, no, idx, raw) => {
					let i = idx, inBlock = 0, inParam = 0
					while (i > 0 && raw[--i] !== '\n') {
						if (raw[i] === ']') ++inBlock
						if (raw[i] === '[') --inBlock
						if (!inBlock && !inParam) {
							if (raw[i] === '=') ++inParam
							if (raw[i] === ' ' || raw[i] === '\t') --inParam
						}
						if (raw[i] === '#') return match
					}
					no = no || ""
					const not = exp.startsWith("not ") || exp.startsWith("NOT ")
					if (not) exp = exp.substr(4)

					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) {
							if (!Command.evalExp(andExp, data, msg.ohlc, checkOnly, text => warn(text, idx, raw))) {
								and = false
								// $$$ what if msg.checkOnly (= opt.legacyIgnErr)
								if (!checkOnly) break
							}
						}
						if (and) {
							result = true
							// $$$ what if msg.checkOnly (= opt.legacyIgnErr)
							if (!checkOnly) break
						}
					}
					// $$$ if inParam => add ` {param}={xxx}`+no
//					return checkOnly ? yes+(no ? (inParam ? ',' : ' ')+no : '') : result ? (not ? no : yes) : (not ? yes : no)
					return checkOnly ? yes+(no ? (inParam > 0 ? '' : ' '+no) : '') : result ? (not ? no : yes) : (not ? yes : no)
					// $$$ what if msg.checkOnly (= opt.legacyIgnErr)
				})
			} 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: name, side: side
	}, {
		a: opt.defAccount, e: opt.defExchange, s: opt.defSymbol
	}, true, true).getWarn()
}
