🢀︎ iex :: f9ba8a9


commit f9ba8a9e357fb8c983162528d631089689e20699
Author: acidvegas <acid.vegas@acid.vegas>
Date:   Mon Jun 24 22:59:16 2019 -0400

    Initial commit

diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..69997e8
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2019, acidvegas <acid.vegas@acid.vegas>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..19f5c66
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+###### Requirments
+* [Python](https://www.python.org/downloads/) *(**Note:** This script was developed to be used with the latest version of Python.)*
+* [PySocks](https://pypi.python.org/pypi/PySocks) *(**Optional:** For using the `proxy` setting.)*
+
+###### Commands
+| Command | Description |
+| --- | --- |
+| @iex | Information about the bot. |
+| !stock \<symbol> | Return information for the \<symbol> stock. *(\<symbol> can be a comma seperated list)* |
+| !stock company \<symbol> | Return information for the \<symbol> company. |
+| !stock list \<active/gainers/losers/volume/percent> | Return lists based on \<active/gainers/losers/volume/percent>. |
+| !stock news \<symbol> | Get the latest news about the \<symbol> stock. |
+| !stock search \<query> | Search for a company name containing \<query>. |
+
+###### Mirrors
+- [acid.vegas](https://acid.vegas/iex) *(main)*
+- [SuperNETs](https://git.supernets.org/pumpcoin/iex)
+- [GitHub](https://github.com/pumpcoin/iex)
+- [GitLab](https://gitlab.com/pumpcoin/iex)
diff --git a/iex/iex.py b/iex/iex.py
new file mode 100644
index 0000000..41bb604
--- /dev/null
+++ b/iex/iex.py
@@ -0,0 +1,413 @@
+#!/usr/bin/env python
+# IExTrading IRC Bot - Developed by acidvegas in Python (https://acid.vegas/iex)
+
+import http.client
+import json
+import random
+import socket
+import time
+
+# Connection
+server     = 'irc.server.com'
+port	   = 6667
+proxy      = None # Proxy should be a Socks5 in IP:PORT format.
+use_ipv6   = False
+use_ssl    = False
+ssl_verify = False
+vhost      = None
+channel    = '#stocks'
+key        = None
+
+# Certificate
+cert_key  = None
+cert_file = None
+cert_pass = None
+
+# Identity
+nickname = 'StockMarket'
+username = 'iex'
+realname = 'acid.vegas/iex'
+
+# Login
+nickserv_password = None
+network_password  = None
+operator_password = None
+
+# Settings
+throttle_cmd = 3
+throttle_msg = 0.5
+user_modes   = None
+
+# Formatting Control Characters / Color Codes
+bold        = '\x02'
+italic      = '\x1D'
+underline   = '\x1F'
+reverse     = '\x16'
+reset       = '\x0f'
+white       = '00'
+black       = '01'
+blue        = '02'
+green       = '03'
+red         = '04'
+brown       = '05'
+purple      = '06'
+orange      = '07'
+yellow      = '08'
+light_green = '09'
+cyan        = '10'
+light_cyan  = '11'
+light_blue  = '12'
+pink        = '13'
+grey        = '14'
+light_grey  = '15'
+
+def condense_value(value):
+	value = float(value)
+	if value < 0.01:
+		return '${0:,.8f}'.format(value)
+	elif value < 24.99:
+		return '${0:,.2f}'.format(value)
+	else:
+		return '${:,}'.format(int(value))
+
+def debug(msg):
+	print(f'{get_time()} | [~] - {msg}')
+
+def error(msg, reason=None):
+	if reason:
+		print(f'{get_time()} | [!] - {msg} ({reason})')
+	else:
+		print(f'{get_time()} | [!] - {msg}')
+
+def error_exit(msg):
+	raise SystemExit(f'{get_time()} | [!] - {msg}')
+
+def get_float(data):
+	try:
+		float(data)
+		return True
+	except ValueError:
+		return False
+
+def get_time():
+	return time.strftime('%I:%M:%S')
+
+def percent_color(percent):
+	percent = float(percent)
+	if percent == 0.0:
+		return grey
+	elif percent < 0.0:
+		if percent > -10.0:
+			return brown
+		else:
+			return red
+	else:
+		if percent < 10.0:
+			return green
+		else:
+			return light_green
+
+def random_int(min, max):
+	return random.randint(min, max)
+
+class IEX:
+	def api(api_data):
+		conn = http.client.HTTPSConnection('api.iextrading.com', timeout=15)
+		conn.request('GET', '/1.0/' + api_data)
+		response = conn.getresponse().read().decode('utf-8')
+		data = json.loads(response)
+		conn.close()
+		return data
+
+	def company(symbol):
+		return IEX.api(f'stock/{symbol}/company')
+
+	def lists(list_type):
+		return IEX.api('stock/market/list/' + list_type)
+
+	def news(symbol):
+		return IEX.api(f'stock/{symbol}/news')
+
+	def quote(symbols):
+		data = IEX.api(f'stock/market/batch?symbols={symbols}&types=quote')
+		if len(data) == 1:
+			return data[next(iter(data))]['quote']
+		else:
+			return [data[item]['quote'] for item in data]
+
+	def stats(symbol):
+		return IEX.api(f'stock/{symbol}/stats')
+
+	def symbols():
+		return IEX.api('ref-data/symbols')
+
+class IRC(object):
+	def __init__(self):
+		self.last = 0
+		self.slow = False
+		self.sock = None
+
+	def stock_info(self, data):
+		sep      = self.color('|', grey)
+		sep2     = self.color('/', grey)
+		name     = '{0} ({1})'.format(self.color(data['companyName'], white), data['symbol'])
+		value    = condense_value(data['latestPrice'])
+		percent  = self.color('{:,.2f}%'.format(float(data['change'])), percent_color(data['change']))
+		volume   = '{0} {1}'.format(self.color('Volume:', white), '${:,}'.format(data['avgTotalVolume']))
+		cap      = '{0} {1}'.format(self.color('Market Cap:', white), '${:,}'.format(data['marketCap']))
+		return f'{name} {sep} {value} ({percent}) {sep} {volume} {sep} {cap}'
+
+	def stock_matrix(self, data): # very retarded way of calculating the longest strings per-column
+		results = {'symbol':list(),'value':list(),'percent':list(),'volume':list(),'cap':list()}
+		for item in data:
+			results['symbol'].append(item['symbol'])
+			results['value'].append(condense_value(item['latestPrice']))
+			results['percent'].append('{:,.2f}%'.format(float(item['change'])))
+			results['volume'].append('${:,}'.format(item['avgTotalVolume']))
+			results['cap'].append('${:,}'.format(item['marketCap']))
+		for item in results:
+			results[item] = len(max(results[item], key=len))
+		if results['symbol'] < len('Symbol'):
+			results['symbol'] = len('Symbol')
+		if results['value'] < len('Value'):
+			results['value'] = len('Value')
+		if results['percent'] < len('Change'):
+			results['percent'] = len('Change')
+		if results['volume'] < len('Volume'):
+			results['volume'] = len('Volume')
+		if results['cap'] < len('Market Cap'):
+			results['cap'] = len('Market Cap')
+		return results
+
+	def stock_table(self, data):
+		matrix = self.stock_matrix(data)
+		header = self.color(' {0}   {1}   {2}   {3}   {4}'.format('Symbol'.center(matrix['symbol']), 'Value'.center(matrix['value']), 'Percent'.center(matrix['percent']),  'Volume'.center(matrix['volume']), 'Market Cap'.center(matrix['cap'])), black, light_grey)
+		lines  = [header,]
+		for item in data:
+			symbol   = item['symbol'].ljust(matrix['symbol'])
+			value    = condense_value(item['latestPrice']).rjust(matrix['value'])
+			percent  = self.color('{:,.2f}%'.format(float(item['change'])).rjust(matrix['percent']), percent_color(item['change']))
+			volume   = '${:,}'.format(item['avgTotalVolume']).rjust(matrix['volume'])
+			cap      = '${:,}'.format(item['marketCap']).rjust(matrix['cap'])
+			lines.append(' {0} | {1} | {2} | {3} | {4} '.format(symbol,value,percent,volume,cap))
+		return lines
+
+	def color(self, msg, foreground, background=None):
+		if background:
+			return f'\x03{foreground},{background}{msg}{reset}'
+		else:
+			return f'\x03{foreground}{msg}{reset}'
+
+	def connect(self):
+		try:
+			self.create_socket()
+			self.sock.connect((server, port))
+			self.register()
+		except socket.error as ex:
+			error('Failed to connect to IRC server.', ex)
+			self.event_disconnect()
+		else:
+			self.listen()
+
+	def create_socket(self):
+		family = socket.AF_INET6 if use_ipv6 else socket.AF_INET
+		if proxy:
+			proxy_server, proxy_port = proxy.split(':')
+			self.sock = socks.socksocket(family, socket.SOCK_STREAM)
+			self.sock.setblocking(0)
+			self.sock.settimeout(15)
+			self.sock.setproxy(socks.PROXY_TYPE_SOCKS5, proxy_server, int(proxy_port))
+		else:
+			self.sock = socket.socket(family, socket.SOCK_STREAM)
+		if vhost:
+			self.sock.bind((vhost, 0))
+		if use_ssl:
+			ctx = ssl.SSLContext()
+			if cert_file:
+				ctx.load_cert_chain(cert_file, cert_key, cert_pass)
+			if ssl_verify:
+				ctx.verify_mode = ssl.CERT_REQUIRED
+				ctx.load_default_certs()
+			else:
+				ctx.check_hostname = False
+				ctx.verify_mode = ssl.CERT_NONE
+			self.sock = ctx.wrap_socket(self.sock)
+
+	def error(self, chan, msg, reason=None):
+		if reason:
+			self.sendmsg(chan, '[{0}] {1} {2}'.format(self.color('!', red), msg, self.color('({0})'.format(reason), grey)))
+		else:
+			self.sendmsg(chan, '[{0}] {1}'.format(self.color('!', red), msg))
+
+	def event_connect(self):
+		if user_modes:
+			self.mode(nickname, '+' + user_modes)
+		if nickserv_password:
+			self.identify(nickname, nickserv_password)
+		if operator_password:
+			self.oper(username, operator_password)
+		self.join_channel(channel, key)
+
+	def event_disconnect(self):
+		self.sock.close()
+		time.sleep(10)
+		self.connect()
+
+	def event_kick(self, chan, kicked):
+		if chan == channel and kicked == nickname:
+			time.sleep(3)
+			self.join_channel(channel, key)
+
+	def event_message(self, nick, chan, msg):
+		try:
+			if msg[:1] in '@!':
+				if time.time() - self.last < throttle_cmd:
+					if not self.slow:
+						self.error(chan, 'Slow down nerd!')
+						self.slow = True
+				else:
+					args = msg.split()
+					if msg == '@iex':
+						self.sendmsg(chan, bold + 'IExTrading IRC Bot - Developed by acidvegas in Python - https://acid.vegas/iex')
+					elif args[0] == '!stock':
+						if len(args) == 2:
+							symbols = args[1].upper()
+							if ',' in symbols:
+								symbols = ','.join(list(symbols.split(','))[:10])
+								data    = IEX.quote(symbols)
+								if type(data) == dict:
+									self.sendmsg(chan, self.stock_info(data))
+								elif type(data) == list:
+									for line in self.stock_table(data):
+										self.sendmsg(chan, line)
+										time.sleep(throttle_msg)
+								else:
+									self.error(chan, 'Invalid stock names!')
+							else:
+								symbol = args[1]
+								data = IEX.quote(symbol)
+								if data:
+									self.sendmsg(chan, self.stock_info(data))
+								else:
+									self.error(chan, 'Invalid stock name!')
+						elif len(args) == 3:
+							if args[1] == 'company':
+								symbol = args[2]
+								data = IEX.company(symbol)
+								if data:
+									self.sendmsg(chan, '{0} {1} ({2}) {3} {4} {5} {6} {7} {8}'.format(self.color('Company:', white), data['companyName'], data['symbol'], self.color('|', grey), data['website'], self.color('|', grey), data['industry'], self.color('|', grey), data['CEO']))
+									self.sendmsg(chan, '{0} {1}'.format(self.color('Description:', white), data['description']))
+								else:
+									self.error('Invalid stock name!')
+							elif args[1] == 'search':
+								query = args[2].lower()
+								data = [{'symbol':item['symbol'],'name':item['name']} for item in IEX.symbols() if query in item['name'].lower()]
+								if data:
+									count = 1
+									max_length = len(max([item['name'] for item in data], key=len))
+									for item in data[:10]:
+										self.sendmsg(chan, '[{0}] {1} {2} {3}'.format(self.color(str(count), pink), item['name'].ljust(max_length), self.color('|', grey), item['symbol']))
+										count += 1
+										time.sleep(throttle_msg)
+								else:
+									self.error(chan, 'No results found.')
+							elif args[1] == 'list':
+								options = {'active':'mostactive','gainers':'gainers','losers':'losers','volume':'iexvolume','percent':'iexpercent'}
+								option = args[2]
+								try:
+									option = options[option]
+								except KeyError:
+									self.error(chan, 'Invalid option!', 'Valid options are active, gainers, losers, volume, & percent')
+								else:
+									data = IEX.lists(option)
+									for line in self.stock_table(data):
+										self.sendmsg(chan, line)
+										time.sleep(throttle_msg)
+							elif args[1] == 'news':
+								symbol = args[2]
+								data   = IEX.news(symbol)
+								if data:
+									count  = 1
+									for item in data:
+										self.sendmsg(chan, '[{0}] {1}'.format(self.color(str(count), pink), item['headline']))
+										self.sendmsg(chan, ' - ' + self.color(item['url'], grey))
+										count += 1
+										time.sleep(throttle_msg)
+								else:
+									self.error(chan, 'Invalid stock name!')
+				self.last = time.time()
+		except Exception as ex:
+			self.error(chan, 'Unknown error occured!', ex)
+
+	def event_nick_in_use(self):
+		self.nick('IEX_' + str(random_int(10,99)))
+
+	def handle_events(self, data):
+		args = data.split()
+		if data.startswith('ERROR :Closing Link:'):
+			raise Exception('Connection has closed.')
+		elif args[0] == 'PING':
+			self.raw('PONG ' + args[1][1:])
+		elif args[1] == '001':
+			self.event_connect()
+		elif args[1] == '433':
+			self.event_nick_in_use()
+		elif args[1] == 'KICK':
+			chan   = args[2]
+			kicked = args[3]
+			self.event_kick(nick, chan, kicked)
+		elif args[1] == 'PRIVMSG':
+			nick = args[0].split('!')[0][1:]
+			chan = args[2]
+			msg  = ' '.join(args[3:])[1:]
+			if chan == channel:
+				self.event_message(nick, chan, msg)
+
+	def identify(self, nick, passwd):
+		self.sendmsg('nickserv', f'identify {nick} {passwd}')
+
+	def join_channel(self, chan, key=None):
+		self.raw(f'JOIN {chan} {key}') if key else self.raw('JOIN ' + chan)
+
+	def listen(self):
+		while True:
+			try:
+				data = self.sock.recv(1024).decode('utf-8')
+				for line in (line for line in data.split('\r\n') if line):
+					debug(line)
+					if len(line.split()) >= 2:
+						self.handle_events(line)
+			except (UnicodeDecodeError,UnicodeEncodeError):
+				pass
+			except Exception as ex:
+				error('Unexpected error occured.', ex)
+				break
+		self.event_disconnect()
+
+	def mode(self, target, mode):
+		self.raw(f'MODE {target} {mode}')
+
+	def nick(self, nick):
+		self.raw('NICK ' + nick)
+
+	def raw(self, msg):
+		self.sock.send(bytes(msg + '\r\n', 'utf-8'))
+
+	def register(self):
+		if network_password:
+			self.raw('PASS ' + network_password)
+		self.raw(f'USER {username} 0 * :{realname}')
+		self.nick(nickname)
+
+	def sendmsg(self, target, msg):
+		self.raw(f'PRIVMSG {target} :{msg}')
+
+# Main
+if proxy:
+	try:
+		import socks
+	except ImportError:
+		error_exit('Missing PySocks module! (https://pypi.python.org/pypi/PySocks)')
+if use_ssl:
+	import ssl
+IRC().connect()