"use strict"

class Kraken extends Exchange {
	static meta = {
		aliases: [
			"KRAKEN",
		],
		endpoint: "https://api.kraken.com",
		fields: {
			public: {
				label: "API Key",
			},
			private: {
				label: "Private Key",
			}
		},
		name: "Kraken",
		patterns: [],
		permissions: {
			origins: [
				"https://api.kraken.com/*"
			],
			permissions: [],
		},
		subscriptions: {
			active: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"4db6261b067dabf8ffb0924c18d95d06",
			],
			inactive: [
				"krake1",
				"krake2",
			],
		},
		version: "0",
		website: "https://www.kraken.com",
		desc: "Buy, sell and margin trade Bitcoin (BTC) and Etherum (ETH) in exchange with EUR, USD, CAD, GBP, and JPY. Leveraged trading on US based Bitcoin and Ethereum exchange.",
		defaultSymbol: "XXBT",
		testSymbol: "XBTUSD",
		margin: true, spot: true, leavesQuantity: true
	}
	constructor() {
		super(Kraken.meta)
	}

	*req(method, resource, params = {}) {
		resource = `/${this.meta.version}/${method === 'GET' ? 'public' : 'private'}`+resource

		let headers = {}
		if (method === 'GET') {
			this.hasAccess()
			resource += '?' + serialize(params).replace(/%20/g, '+')//.replace(/\+/g, '%2B')
			params = null
		} else {
			const crypt = (path, nonce, data, key) => {
				const sha256 = new jsSHA("SHA-256", "TEXT")
				sha256.update(nonce + data)
				const sha512 = new jsSHA("SHA-512", "BYTES")
				sha512.setHMACKey(key, "B64")
				sha512.update(path + sha256.getHash("BYTES"))
				return sha512.getHMAC("B64")
			}
			const credentials = this.getCredentials("private")
			const nonce = this.getNonce()
			params.nonce = nonce
			params = serialize(params)
			headers["api-key"] = credentials.public
			headers["api-sign"] = crypt(resource, nonce, params, credentials.private)
		}

		const response = yield request.bind(this, method, this.meta.endpoint + resource, params, headers)
		if (response.error && response.error.length) {
			const error = response.error[0]
			if (error[0] === 'E') {
				throw new Error(error.substr(1))
			}
			this.warn(error.substr(1))
		}
		return response.result
	}

	*time() {
		const result = yield* this.req('GET', "/Time")
		return result.unixtime * 1000
	}

	*account(currency, isMargin) {
		const isTest = this.isTest()
		let balances = {}
		if (isMargin) {
			const data = yield* this.req('POST', "/TradeBalance", {asset: currency})
			if (data) {
				balances[currency] = {
					available: data.mf,
					balance: data.e
				}
			}
		} else {
			const data = yield* this.req('POST', "/BalanceEx")
			Object.each(data, (asset, info) => {
				const amount = Number(info.balance)
				const avail = amount - Number(info.hold_trade)
				if (amount || asset == currency) {
					balances[asset] = {
						available: avail,
						balance: amount
					}
				}
			})
		}
		isTest && currency && (balances[currency] = balances[currency] || {available: 0, balance: 0})
		return balances
	}

	*symbolInfo(symbol) {
		const symbolCache = this.getSymbolCache()
		if (symbolCache._refresh || !symbolCache[symbol]) {
			const response = yield* this.req('GET', "/AssetPairs", symbol && {pair: symbol})

			for (const symbol in response) {
				const info = response[symbol]
				symbolCache[(info.symbol = symbol.toUpperCase())] = info
				symbolCache[info.altname.toUpperCase()] = info
			}
			this.updatedSymbolCache()
		}
		if (!symbol) return symbolCache

		if (!symbolCache[symbol]) {
			throw new Error(`Unknown market symbol: ${symbol} – Use the 'Symbols' button in the exchange settings or alert editor to browse for valid symbols!`)
		}
		return symbolCache[symbol]
	}

	*symbolTicker(symbol) {
		let ticker = yield* this.req('GET', "/Ticker", {pair: symbol})
		ticker = ticker && ticker[symbol]

		return ticker && {
			ask: Number(ticker.a[0]),
			bid: Number(ticker.b[0]),
			mid: (Number(ticker.a[0]) + Number(ticker.b[0])) / 2,
			last: Number(ticker.c[0])
		}
	}

	*trade(cmd, ohlc = {}) {
		const market = yield* this.symbolInfo(cmd.s)
		cmd._sym = market.symbol
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.symbol)) || (yield* this.symbolTicker(market.symbol))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.symbol} is not available!`)
		}
		this.checkSlip(cmd, ohlc, market.symbol, ticker)
		if (cmd.up) {
			this.updatePrice(market.symbol, ticker)
			if (!cmd.b && !cmd.ub) {
				return ticker
			}
		}

		const currency = cmd.currency = cmd.isBid ? market.quote : market.base
		if (cmd.isMargin) cmd.currency += marginSuffix
		let balance = {available: 0, balance: 0}
		if ((cmd.y !== "possize" && cmd.q.wasPercent()) || cmd.ub) {
			balance = cmd.bc && !cmd.ub && this.getBalance(currency, cmd.isMargin)
			const balances = !balance && (yield* this.account(currency, cmd.isMargin))
			balance = balance || balances[currency]
			this.checkBalance(cmd, balance, currency)
			if (!cmd.bc || cmd.ub) {
				this.updateBalance(balances, currency, cmd.isMargin)
				if (cmd.ub) {
					return Object.assign(ticker, balance)
				}
			}
		}

		const first = ticker[cmd.pr] || ohlc[cmd.pr] || ((cmd.isBid && cmd.t !== "market") || (cmd.isAsk && cmd.t === "market") ? ticker.bid : ticker.ask)
		let price = cmd.fp ? cmd.fp.resolve(market.pair_decimals) : cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp).resolve(market.pair_decimals)
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated)")
		}

		cmd.isMargin && ohlc[cmd.lr] && this.info(`using custom leverage multiplier: ${ohlc[cmd.lr]} (${cmd.lr})`)
		let leverage = cmd.isMargin ? (cmd.l * (ohlc[cmd.lr] || 1)) || 2 : 1
		let available = ohlc[cmd.y] || (cmd.y === "equity" ? balance.balance : balance.available) * (cmd.lc || leverage)
		if (cmd.y !== "possize" && cmd.isBid) {
			available /= price
		}

		ohlc[cmd.qr] && this.info(`using custom quantity multiplier: ${ohlc[cmd.qr]} (${cmd.qr})`)
		let order = {
			pair: market.symbol,
			ordertype: cmd.t === "market" ? "market" : "limit",
			type: cmd.isBid ? "buy" : "sell",
			volume: cmd.q._().div(cmd.u === "currency" && !cmd.q.wasPercent() ? price : 1)
				.reference(available).mul(ohlc[cmd.qr] || 1).minmax(cmd.minq, cmd.maxq).resolve(market.lot_decimals),
			trading_agreement: "agree"
		}
		if (cmd.lq && ohlc.left) {
			this.info("using leftover quantity from last order: "+ohlc.left)
			order.volume = ohlc.left.toFixed(market.lot_decimals)
		}
		if (order.volume <= 0) this.warn("your q parameter might be incorrect! (quantity of 0 calculated)")
		if (cmd.t !== "market") order.price = price
		if (cmd.t === "post") order.oflags = "post"
		if (cmd.r) order.reduce_only = true
		if (leverage > 1) cmd._lev = order.leverage = leverage

		if (cmd.sl && cmd.tp) {
			cmd.slref && cmd.slref !== "price" && this.info(`using custom SL reference: ${ohlc[cmd.slref] || ticker[cmd.slref] || "NOT FOUND!"} (${cmd.slref})`)
			const stop1 = cmd.slref === "price" ? price : ((cmd.slref && (ohlc[cmd.slref] || ticker[cmd.slref])) || first)
			cmd.tpref && cmd.tpref !== "price" && this.info(`using custom TP reference: ${ohlc[cmd.tpref] || ticker[cmd.tpref] || "NOT FOUND!"} (${cmd.tpref})`)
			const stop2 = cmd.tpref === "price" ? price : ((cmd.tpref && (ohlc[cmd.tpref] || ticker[cmd.tpref])) || first)
			order.ordertype = "stop-loss-profit" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.sl._().relative(stop1).resolve(market.pair_decimals)
			order.price2 = cmd.tp._().relative(stop2).resolve(market.pair_decimals)
		} else if (cmd.sl) {
			cmd.slref && cmd.slref !== "price" && this.info(`using custom SL reference: ${ohlc[cmd.slref] || ticker[cmd.slref] || "NOT FOUND!"} (${cmd.slref})`)
			const stop = cmd.slref === "price" ? price : ((cmd.slref && (ohlc[cmd.slref] || ticker[cmd.slref])) || first)
			order.ordertype = "stop-loss" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.sl._().relative(stop).resolve(market.pair_decimals)
 			cmd.t !== "market" && (order.price2 = price)
		} else if (cmd.tp) {
			cmd.tpref && cmd.tpref !== "price" && this.info(`using custom TP reference: ${ohlc[cmd.tpref] || ticker[cmd.tpref] || "NOT FOUND!"} (${cmd.tpref})`)
			const stop = cmd.tpref === "price" ? price : ((cmd.tpref && (ohlc[cmd.tpref] || ticker[cmd.tpref])) || first)
			order.ordertype = "take-profit" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.tp._().relative(stop).resolve(market.pair_decimals)
 			cmd.t !== "market" && (order.price2 = price)
		} else if (cmd.ts) {
			cmd.tsref && cmd.tsref !== "price" && this.info(`using custom TS reference: ${ohlc[cmd.tsref] || ticker[cmd.tsref] || "NOT FOUND!"} (${cmd.tsref})`)
			const stop = cmd.tsref === "price" ? price : ((cmd.tsref && (ohlc[cmd.tsref] || ticker[cmd.tsref])) || first)
			order.ordertype = "trailing-stop" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.ts._().relative(stop).resolve(market.pair_decimals)
 			cmd.t !== "market" && (order.price2 = price)
		}

		this.info(`placing order${leverage > 1?` @ ${leverage}x`:''}: `+stringify(order))
		if (cmd.d) {
			this.msgDisabled(cmd)
			return order
		}
		const result = yield* this.req('POST', "/AddOrder", order)
		cmd.id && result && result.txid && result.txid.forEach(txid => this.cacheId(txid, cmd.id))
		return result
	}

	*ordersCancel(cmd, ohlc = {}) {
		const market = yield* this.symbolInfo(cmd.s)
		cmd._sym = market.symbol

		let orders = yield* this.req('POST', "/OpenOrders")
		if (!orders || !orders.open) {
			this.msgOrderErr()
			return false
		}
		orders = Object.filterAsArray(orders.open, (txid, order) => {
			if (order.status !== "open" && order.status !== "pending") {
				return false
			}
			if (order.descr.pair !== market.altname) {
				return false
			}
			order = Object.assign(order, order.descr)
			delete order.descr
			order.txid = txid
			order.leverage = parseInt(order.leverage) || 0
			return true
		})
		if (!orders.length) {
			this.msgOrdersNone()
			return false
		}

		const ids = (cmd.id || "").split(',')
		const match = cmd.cr ? false : true
		const tickers = {}
		yield* this.filterOrderStart(cmd, ohlc, tickers, orders)
		const totalOrders = orders.length
		orders = orders.filter(order => {
			if (cmd.b) {
				if (order.type !== (cmd.isBid ? 'buy' : 'sell')) {
					return false
				}
				if (cmd.isMargin) {
					if (!order.leverage) {
						return false
					}
				} else if (order.leverage) {
					return false
				}
			}
			if (cmd.fp && cmd.fp.compare(order.price)) {
				return !match
			}
			if (cmd.l && order.leverage != cmd.l) {
				return !match
			}
			if (cmd.r !== undefined && !cmd.r != !order.reduceOnly) {
				return !match
			}
			if (cmd.id && !this.checkId(order.txid, ids)) {
				return !match
			}
			if (!this.filterOrder(cmd, ohlc, tickers, order)) {
				return !match
			}
			return match
		})

		if (cmd.cm._().reference(orders.length).getMax() < orders.length) {
			const sort = {
				"newest": 	["opentm", true],
				"oldest": 	["opentm", false],
				"highest": 	[["descr", "price"], true],
				"lowest": 	[["descr", "price"], false],
				"biggest": 	["vol", true],
				"smallest": ["vol", false]
			}
			cmd.cmo === 'random' ? shuffle(orders) : sortByIndex(orders, sort[cmd.cmo][0], sort[cmd.cmo][1])
			orders = orders.slice(0, cmd.cm.resolve(0))
		}

		this.msgOrders(cmd, orders, totalOrders)
		if (cmd.ch || cmd.d) {
			orders.length && this.msgDisabled(cmd)
			return orders
		}

		let cancelOrders = []
		for (const order of orders) {
			const response = yield* this.req('POST', "/CancelOrder", {txid: order.txid})
			if (response && response.count) {
				cancelOrders.push(order)
				this.removeId(order.txid)
			}
		}
		return cancelOrders
	}

	*positionsClose(cmd, pos, i, ohlc = {}, market) {
		const ticker = (cmd.pc && !cmd.up && this.getPrice(market.symbol)) || (yield* this.symbolTicker(market.symbol))
		if (!ticker) {
			throw new ReferenceError(`Ticker ${market.symbol} is not available!`)
		}
		ticker.pos = pos.price
		this.checkSlip(cmd, ohlc, market.symbol, ticker)
		cmd.up && this.updatePrice(market.symbol, ticker)

		const first = ticker[cmd.sl || cmd.tp || cmd.ts ? 'pos' : cmd.pr] || ohlc[cmd.pr] || ((cmd.isBid && cmd.t !== "market") || (cmd.isAsk && cmd.t === "market") ? ticker.bid : ticker.ask)
		let price = cmd.p._().relative(first).minmax(cmd.minp, cmd.maxp).resolve(market.pair_decimals)
		if (cmd.fp) {
			price = cmd.fp.resolve(market.pair_decimals)
		}
		if (price <= 0) {
			this.warn("your p parameter might be incorrect! (price of 0 calculated)")
		}

		ohlc[cmd.qr] && this.info(`using custom quantity multiplier: ${ohlc[cmd.qr]} (${cmd.qr})`)
		let order = {
			pair: pos.pair,
			type: pos.type === "buy" ? "sell" : "buy",
			ordertype: cmd.t === "market" ? "market" : "limit",
			volume: cmd.q._().div(cmd.u === "currency" && !cmd.q.wasPercent() ? price : 1)
				.reference(pos.quantity).mul(ohlc[cmd.qr] || 1).minmax(cmd.minq, cmd.maxq).resolve(market.lot_decimals),
			leverage: cmd.l || pos.leverage || 2,
			trading_agreement: "agree",
			reduce_only: cmd.r === undefined ? true : cmd.r
		}
		if (cmd.t !== "market") order.price = price
		if (cmd.t === "post") order.oflags = "post"

		if (cmd.sl && cmd.tp) {
			cmd.slref && cmd.slref !== "price" && this.info(`using custom SL reference: ${ohlc[cmd.slref] || ticker[cmd.slref] || "NOT FOUND!"} (${cmd.slref})`)
			const stop1 = cmd.slref === "price" ? price : ((cmd.slref && (ohlc[cmd.slref] || ticker[cmd.slref])) || first)
			cmd.tpref && cmd.tpref !== "price" && this.info(`using custom TP reference: ${ohlc[cmd.tpref] || ticker[cmd.tpref] || "NOT FOUND!"} (${cmd.tpref})`)
			const stop2 = cmd.tpref === "price" ? price : ((cmd.tpref && (ohlc[cmd.tpref] || ticker[cmd.tpref])) || first)
			order.ordertype = "stop-loss-profit" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.sl._().relative(stop1).resolve(market.pair_decimals)
			order.price2 = cmd.tp._().relative(stop2).resolve(market.pair_decimals)
		} else if (cmd.sl) {
			cmd.slref && cmd.slref !== "price" && this.info(`using custom SL reference: ${ohlc[cmd.slref] || ticker[cmd.slref] || "NOT FOUND!"} (${cmd.slref})`)
			const stop = cmd.slref === "price" ? price : ((cmd.slref && (ohlc[cmd.slref] || ticker[cmd.slref])) || first)
			order.ordertype = "stop-loss" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.sl._().relative(stop).resolve(market.pair_decimals)
			cmd.t !== "market" && (order.price2 = price)
 		} else if (cmd.tp) {
			cmd.tpref && cmd.tpref !== "price" && this.info(`using custom TP reference: ${ohlc[cmd.tpref] || ticker[cmd.tpref] || "NOT FOUND!"} (${cmd.tpref})`)
			const stop = cmd.tpref === "price" ? price : ((cmd.tpref && (ohlc[cmd.tpref] || ticker[cmd.tpref])) || first)
			order.ordertype = "take-profit" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.tp._().relative(stop).resolve(market.pair_decimals)
			cmd.t !== "market" && (order.price2 = price)
		} else if (cmd.ts) {
			cmd.tsref && cmd.tsref !== "price" && this.info(`using custom TS reference: ${ohlc[cmd.tsref] || ticker[cmd.tsref] || "NOT FOUND!"} (${cmd.tsref})`)
			const stop = cmd.tsref === "price" ? price : ((cmd.tsref && (ohlc[cmd.tsref] || ticker[cmd.tsref])) || first)
			order.ordertype = "trailing-stop" + (cmd.t === "market" ? "" : "-limit")
			order.price = cmd.ts._().relative(stop).resolve(market.pair_decimals)
			cmd.t !== "market" && (order.price2 = price)
		}

		if (cmd.t === "settle") {
			order.ordertype = "settle-position"
			order.type = pos.type
			order.volume = 0
		} else if (order.volume <= 0) {
			this.warn("your q parameter might be incorrect! (quantity of 0 calculated)")
		}

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

		const result = yield* this.req('POST', "/AddOrder", order)
		cmd.id && result && result.txid && result.txid.forEach(txid => this.cacheId(txid, cmd.id))
		return result
	}

	*positionsCloseAll(cmd, ohlc, positions) {
		const symbols = yield* this.symbolInfo()
		if (!positions) {
			positions = yield* this.req('POST', "/OpenPositions", {docalcs: true})
			if (!positions) {
				this.msgPosErr()
				return false
			}
			const symbol = cmd.s && cmd.s !== '*' && symbols[cmd.s] && symbols[cmd.s].symbol
			positions = Object.filterAsArray(positions, (txid, pos) => {
				if (symbol && pos.pair !== symbol) {
					return false
				}
				pos.txid = txid
				pos.price = toPrecision(pos.cost / pos.vol, symbols[pos.pair] && symbols[pos.pair].pair_decimals || 4)
				pos.quantity = pos.vol - pos.vol_closed
				pos.leverage = Math.round(pos.cost / pos.margin)
				pos.pnl = pos.net / pos.margin / pos.leverage
				pos.time *= 1000
				return true
			})
			if (!positions.length) {
				this.msgPosNone()
				return false
			}
		}

		const match = cmd.cr ? false : true
		const tickers = {}
		yield* this.filterPosStart(cmd, ohlc, tickers, positions)
		const totalPos = positions.length
		positions = positions.filter(pos => {
			if ((cmd.isBid && pos.type !== 'buy') || (cmd.isAsk && pos.type !== 'sell')) {
				return false
			}
			if (cmd.fp && cmd.fp.compare(pos.price)) {
				return !match
			}
			if (cmd.l && pos.leverage !== cmd.l) {
				return !match
			}
			const market = symbols[pos.pair]
			cmd._sym = market.symbol
			pos.currency = cmd.currency = market.quote + marginSuffix
			pos._sizeUSD = Number(pos.margin)
			pos._sizeCoin = pos._sizeUSD / pos.price
			if (!this.filterPos(cmd, ohlc, tickers, pos, total => total * pos.leverage / pos.price, market.lot_decimals)) {
				return !match
			}
			return match
		})

		if (cmd.cm._().reference(positions.length).getMax() < positions.length) {
			const sort = {
				"newest": 	["time", true],
				"oldest": 	["time", false],
				"highest": 	["price", true],
				"lowest": 	["price", false],
				"biggest": 	["volume", true],
				"smallest": ["volume", false]
			}
			cmd.cmo === 'random' ? shuffle(positions) : sortByIndex(positions, sort[cmd.cmo][0], sort[cmd.cmo][1])
			positions = positions.slice(0, cmd.cm.resolve(0))
		}
		if (!positions.length || cmd.ch) {
			this.info(`${positions.length} position${positions.length > 1 ?'s':''} found!`)
			return positions
		}

		let closeOrders = [], i = 0
		for (const pos of positions) {
			const order = yield* this.positionsClose(cmd, pos, i++, ohlc, symbols[pos.pair])
			if (order) {
				closeOrders.push(order)
				Object.assign(closeOrders[closeOrders.length-1], pos)
			}
		}
		return closeOrders
	}
}

Broker.add(new Kraken())
