←︎ iex :: f9ba8a9


1
commit f9ba8a9e357fb8c983162528d631089689e20699 (HEAD -> master)
2
Author: acidvegas <acid.vegas@acid.vegas>
3
Date:   Mon Jun 24 22:59:16 2019 -0400
4
5
    Initial commit
6
---
7
 LICENSE    |  15 +++
8
 README.md  |  19 +++
9
 iex/iex.py | 413 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10
 3 files changed, 447 insertions(+)
11
12
diff --git a/LICENSE b/LICENSE
13
new file mode 100644
14
index 0000000..69997e8
15
--- /dev/null
16
+++ b/LICENSE
17
@@ -0,0 +1,15 @@
18
+ISC License
19
+
20
+Copyright (c) 2019, acidvegas <acid.vegas@acid.vegas>
21
+
22
+Permission to use, copy, modify, and/or distribute this software for any
23
+purpose with or without fee is hereby granted, provided that the above
24
+copyright notice and this permission notice appear in all copies.
25
+
26
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
27
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
28
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
29
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
30
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
31
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
32
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
33
diff --git a/README.md b/README.md
34
new file mode 100644
35
index 0000000..19f5c66
36
--- /dev/null
37
+++ b/README.md
38
@@ -0,0 +1,19 @@
39
+###### Requirments
40
+* [Python](https://www.python.org/downloads/) *(**Note:** This script was developed to be used with the latest version of Python.)*
41
+* [PySocks](https://pypi.python.org/pypi/PySocks) *(**Optional:** For using the `proxy` setting.)*
42
+
43
+###### Commands
44
+| Command | Description |
45
+| --- | --- |
46
+| @iex | Information about the bot. |
47
+| !stock \<symbol> | Return information for the \<symbol> stock. *(\<symbol> can be a comma seperated list)* |
48
+| !stock company \<symbol> | Return information for the \<symbol> company. |
49
+| !stock list \<active/gainers/losers/volume/percent> | Return lists based on \<active/gainers/losers/volume/percent>. |
50
+| !stock news \<symbol> | Get the latest news about the \<symbol> stock. |
51
+| !stock search \<query> | Search for a company name containing \<query>. |
52
+
53
+###### Mirrors
54
+- [acid.vegas](https://acid.vegas/iex) *(main)*
55
+- [SuperNETs](https://git.supernets.org/pumpcoin/iex)
56
+- [GitHub](https://github.com/pumpcoin/iex)
57
+- [GitLab](https://gitlab.com/pumpcoin/iex)
58
diff --git a/iex/iex.py b/iex/iex.py
59
new file mode 100644
60
index 0000000..41bb604
61
--- /dev/null
62
+++ b/iex/iex.py
63
@@ -0,0 +1,413 @@
64
+#!/usr/bin/env python
65
+# IExTrading IRC Bot - Developed by acidvegas in Python (https://acid.vegas/iex)
66
+
67
+import http.client
68
+import json
69
+import random
70
+import socket
71
+import time
72
+
73
+# Connection
74
+server     = 'irc.server.com'
75
+port	   = 6667
76
+proxy      = None # Proxy should be a Socks5 in IP:PORT format.
77
+use_ipv6   = False
78
+use_ssl    = False
79
+ssl_verify = False
80
+vhost      = None
81
+channel    = '#stocks'
82
+key        = None
83
+
84
+# Certificate
85
+cert_key  = None
86
+cert_file = None
87
+cert_pass = None
88
+
89
+# Identity
90
+nickname = 'StockMarket'
91
+username = 'iex'
92
+realname = 'acid.vegas/iex'
93
+
94
+# Login
95
+nickserv_password = None
96
+network_password  = None
97
+operator_password = None
98
+
99
+# Settings
100
+throttle_cmd = 3
101
+throttle_msg = 0.5
102
+user_modes   = None
103
+
104
+# Formatting Control Characters / Color Codes
105
+bold        = '\x02'
106
+italic      = '\x1D'
107
+underline   = '\x1F'
108
+reverse     = '\x16'
109
+reset       = '\x0f'
110
+white       = '00'
111
+black       = '01'
112
+blue        = '02'
113
+green       = '03'
114
+red         = '04'
115
+brown       = '05'
116
+purple      = '06'
117
+orange      = '07'
118
+yellow      = '08'
119
+light_green = '09'
120
+cyan        = '10'
121
+light_cyan  = '11'
122
+light_blue  = '12'
123
+pink        = '13'
124
+grey        = '14'
125
+light_grey  = '15'
126
+
127
+def condense_value(value):
128
+	value = float(value)
129
+	if value < 0.01:
130
+		return '${0:,.8f}'.format(value)
131
+	elif value < 24.99:
132
+		return '${0:,.2f}'.format(value)
133
+	else:
134
+		return '${:,}'.format(int(value))
135
+
136
+def debug(msg):
137
+	print(f'{get_time()} | [~] - {msg}')
138
+
139
+def error(msg, reason=None):
140
+	if reason:
141
+		print(f'{get_time()} | [!] - {msg} ({reason})')
142
+	else:
143
+		print(f'{get_time()} | [!] - {msg}')
144
+
145
+def error_exit(msg):
146
+	raise SystemExit(f'{get_time()} | [!] - {msg}')
147
+
148
+def get_float(data):
149
+	try:
150
+		float(data)
151
+		return True
152
+	except ValueError:
153
+		return False
154
+
155
+def get_time():
156
+	return time.strftime('%I:%M:%S')
157
+
158
+def percent_color(percent):
159
+	percent = float(percent)
160
+	if percent == 0.0:
161
+		return grey
162
+	elif percent < 0.0:
163
+		if percent > -10.0:
164
+			return brown
165
+		else:
166
+			return red
167
+	else:
168
+		if percent < 10.0:
169
+			return green
170
+		else:
171
+			return light_green
172
+
173
+def random_int(min, max):
174
+	return random.randint(min, max)
175
+
176
+class IEX:
177
+	def api(api_data):
178
+		conn = http.client.HTTPSConnection('api.iextrading.com', timeout=15)
179
+		conn.request('GET', '/1.0/' + api_data)
180
+		response = conn.getresponse().read().decode('utf-8')
181
+		data = json.loads(response)
182
+		conn.close()
183
+		return data
184
+
185
+	def company(symbol):
186
+		return IEX.api(f'stock/{symbol}/company')
187
+
188
+	def lists(list_type):
189
+		return IEX.api('stock/market/list/' + list_type)
190
+
191
+	def news(symbol):
192
+		return IEX.api(f'stock/{symbol}/news')
193
+
194
+	def quote(symbols):
195
+		data = IEX.api(f'stock/market/batch?symbols={symbols}&types=quote')
196
+		if len(data) == 1:
197
+			return data[next(iter(data))]['quote']
198
+		else:
199
+			return [data[item]['quote'] for item in data]
200
+
201
+	def stats(symbol):
202
+		return IEX.api(f'stock/{symbol}/stats')
203
+
204
+	def symbols():
205
+		return IEX.api('ref-data/symbols')
206
+
207
+class IRC(object):
208
+	def __init__(self):
209
+		self.last = 0
210
+		self.slow = False
211
+		self.sock = None
212
+
213
+	def stock_info(self, data):
214
+		sep      = self.color('|', grey)
215
+		sep2     = self.color('/', grey)
216
+		name     = '{0} ({1})'.format(self.color(data['companyName'], white), data['symbol'])
217
+		value    = condense_value(data['latestPrice'])
218
+		percent  = self.color('{:,.2f}%'.format(float(data['change'])), percent_color(data['change']))
219
+		volume   = '{0} {1}'.format(self.color('Volume:', white), '${:,}'.format(data['avgTotalVolume']))
220
+		cap      = '{0} {1}'.format(self.color('Market Cap:', white), '${:,}'.format(data['marketCap']))
221
+		return f'{name} {sep} {value} ({percent}) {sep} {volume} {sep} {cap}'
222
+
223
+	def stock_matrix(self, data): # very retarded way of calculating the longest strings per-column
224
+		results = {'symbol':list(),'value':list(),'percent':list(),'volume':list(),'cap':list()}
225
+		for item in data:
226
+			results['symbol'].append(item['symbol'])
227
+			results['value'].append(condense_value(item['latestPrice']))
228
+			results['percent'].append('{:,.2f}%'.format(float(item['change'])))
229
+			results['volume'].append('${:,}'.format(item['avgTotalVolume']))
230
+			results['cap'].append('${:,}'.format(item['marketCap']))
231
+		for item in results:
232
+			results[item] = len(max(results[item], key=len))
233
+		if results['symbol'] < len('Symbol'):
234
+			results['symbol'] = len('Symbol')
235
+		if results['value'] < len('Value'):
236
+			results['value'] = len('Value')
237
+		if results['percent'] < len('Change'):
238
+			results['percent'] = len('Change')
239
+		if results['volume'] < len('Volume'):
240
+			results['volume'] = len('Volume')
241
+		if results['cap'] < len('Market Cap'):
242
+			results['cap'] = len('Market Cap')
243
+		return results
244
+
245
+	def stock_table(self, data):
246
+		matrix = self.stock_matrix(data)
247
+		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)
248
+		lines  = [header,]
249
+		for item in data:
250
+			symbol   = item['symbol'].ljust(matrix['symbol'])
251
+			value    = condense_value(item['latestPrice']).rjust(matrix['value'])
252
+			percent  = self.color('{:,.2f}%'.format(float(item['change'])).rjust(matrix['percent']), percent_color(item['change']))
253
+			volume   = '${:,}'.format(item['avgTotalVolume']).rjust(matrix['volume'])
254
+			cap      = '${:,}'.format(item['marketCap']).rjust(matrix['cap'])
255
+			lines.append(' {0} | {1} | {2} | {3} | {4} '.format(symbol,value,percent,volume,cap))
256
+		return lines
257
+
258
+	def color(self, msg, foreground, background=None):
259
+		if background:
260
+			return f'\x03{foreground},{background}{msg}{reset}'
261
+		else:
262
+			return f'\x03{foreground}{msg}{reset}'
263
+
264
+	def connect(self):
265
+		try:
266
+			self.create_socket()
267
+			self.sock.connect((server, port))
268
+			self.register()
269
+		except socket.error as ex:
270
+			error('Failed to connect to IRC server.', ex)
271
+			self.event_disconnect()
272
+		else:
273
+			self.listen()
274
+
275
+	def create_socket(self):
276
+		family = socket.AF_INET6 if use_ipv6 else socket.AF_INET
277
+		if proxy:
278
+			proxy_server, proxy_port = proxy.split(':')
279
+			self.sock = socks.socksocket(family, socket.SOCK_STREAM)
280
+			self.sock.setblocking(0)
281
+			self.sock.settimeout(15)
282
+			self.sock.setproxy(socks.PROXY_TYPE_SOCKS5, proxy_server, int(proxy_port))
283
+		else:
284
+			self.sock = socket.socket(family, socket.SOCK_STREAM)
285
+		if vhost:
286
+			self.sock.bind((vhost, 0))
287
+		if use_ssl:
288
+			ctx = ssl.SSLContext()
289
+			if cert_file:
290
+				ctx.load_cert_chain(cert_file, cert_key, cert_pass)
291
+			if ssl_verify:
292
+				ctx.verify_mode = ssl.CERT_REQUIRED
293
+				ctx.load_default_certs()
294
+			else:
295
+				ctx.check_hostname = False
296
+				ctx.verify_mode = ssl.CERT_NONE
297
+			self.sock = ctx.wrap_socket(self.sock)
298
+
299
+	def error(self, chan, msg, reason=None):
300
+		if reason:
301
+			self.sendmsg(chan, '[{0}] {1} {2}'.format(self.color('!', red), msg, self.color('({0})'.format(reason), grey)))
302
+		else:
303
+			self.sendmsg(chan, '[{0}] {1}'.format(self.color('!', red), msg))
304
+
305
+	def event_connect(self):
306
+		if user_modes:
307
+			self.mode(nickname, '+' + user_modes)
308
+		if nickserv_password:
309
+			self.identify(nickname, nickserv_password)
310
+		if operator_password:
311
+			self.oper(username, operator_password)
312
+		self.join_channel(channel, key)
313
+
314
+	def event_disconnect(self):
315
+		self.sock.close()
316
+		time.sleep(10)
317
+		self.connect()
318
+
319
+	def event_kick(self, chan, kicked):
320
+		if chan == channel and kicked == nickname:
321
+			time.sleep(3)
322
+			self.join_channel(channel, key)
323
+
324
+	def event_message(self, nick, chan, msg):
325
+		try:
326
+			if msg[:1] in '@!':
327
+				if time.time() - self.last < throttle_cmd:
328
+					if not self.slow:
329
+						self.error(chan, 'Slow down nerd!')
330
+						self.slow = True
331
+				else:
332
+					args = msg.split()
333
+					if msg == '@iex':
334
+						self.sendmsg(chan, bold + 'IExTrading IRC Bot - Developed by acidvegas in Python - https://acid.vegas/iex')
335
+					elif args[0] == '!stock':
336
+						if len(args) == 2:
337
+							symbols = args[1].upper()
338
+							if ',' in symbols:
339
+								symbols = ','.join(list(symbols.split(','))[:10])
340
+								data    = IEX.quote(symbols)
341
+								if type(data) == dict:
342
+									self.sendmsg(chan, self.stock_info(data))
343
+								elif type(data) == list:
344
+									for line in self.stock_table(data):
345
+										self.sendmsg(chan, line)
346
+										time.sleep(throttle_msg)
347
+								else:
348
+									self.error(chan, 'Invalid stock names!')
349
+							else:
350
+								symbol = args[1]
351
+								data = IEX.quote(symbol)
352
+								if data:
353
+									self.sendmsg(chan, self.stock_info(data))
354
+								else:
355
+									self.error(chan, 'Invalid stock name!')
356
+						elif len(args) == 3:
357
+							if args[1] == 'company':
358
+								symbol = args[2]
359
+								data = IEX.company(symbol)
360
+								if data:
361
+									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']))
362
+									self.sendmsg(chan, '{0} {1}'.format(self.color('Description:', white), data['description']))
363
+								else:
364
+									self.error('Invalid stock name!')
365
+							elif args[1] == 'search':
366
+								query = args[2].lower()
367
+								data = [{'symbol':item['symbol'],'name':item['name']} for item in IEX.symbols() if query in item['name'].lower()]
368
+								if data:
369
+									count = 1
370
+									max_length = len(max([item['name'] for item in data], key=len))
371
+									for item in data[:10]:
372
+										self.sendmsg(chan, '[{0}] {1} {2} {3}'.format(self.color(str(count), pink), item['name'].ljust(max_length), self.color('|', grey), item['symbol']))
373
+										count += 1
374
+										time.sleep(throttle_msg)
375
+								else:
376
+									self.error(chan, 'No results found.')
377
+							elif args[1] == 'list':
378
+								options = {'active':'mostactive','gainers':'gainers','losers':'losers','volume':'iexvolume','percent':'iexpercent'}
379
+								option = args[2]
380
+								try:
381
+									option = options[option]
382
+								except KeyError:
383
+									self.error(chan, 'Invalid option!', 'Valid options are active, gainers, losers, volume, & percent')
384
+								else:
385
+									data = IEX.lists(option)
386
+									for line in self.stock_table(data):
387
+										self.sendmsg(chan, line)
388
+										time.sleep(throttle_msg)
389
+							elif args[1] == 'news':
390
+								symbol = args[2]
391
+								data   = IEX.news(symbol)
392
+								if data:
393
+									count  = 1
394
+									for item in data:
395
+										self.sendmsg(chan, '[{0}] {1}'.format(self.color(str(count), pink), item['headline']))
396
+										self.sendmsg(chan, ' - ' + self.color(item['url'], grey))
397
+										count += 1
398
+										time.sleep(throttle_msg)
399
+								else:
400
+									self.error(chan, 'Invalid stock name!')
401
+				self.last = time.time()
402
+		except Exception as ex:
403
+			self.error(chan, 'Unknown error occured!', ex)
404
+
405
+	def event_nick_in_use(self):
406
+		self.nick('IEX_' + str(random_int(10,99)))
407
+
408
+	def handle_events(self, data):
409
+		args = data.split()
410
+		if data.startswith('ERROR :Closing Link:'):
411
+			raise Exception('Connection has closed.')
412
+		elif args[0] == 'PING':
413
+			self.raw('PONG ' + args[1][1:])
414
+		elif args[1] == '001':
415
+			self.event_connect()
416
+		elif args[1] == '433':
417
+			self.event_nick_in_use()
418
+		elif args[1] == 'KICK':
419
+			chan   = args[2]
420
+			kicked = args[3]
421
+			self.event_kick(nick, chan, kicked)
422
+		elif args[1] == 'PRIVMSG':
423
+			nick = args[0].split('!')[0][1:]
424
+			chan = args[2]
425
+			msg  = ' '.join(args[3:])[1:]
426
+			if chan == channel:
427
+				self.event_message(nick, chan, msg)
428
+
429
+	def identify(self, nick, passwd):
430
+		self.sendmsg('nickserv', f'identify {nick} {passwd}')
431
+
432
+	def join_channel(self, chan, key=None):
433
+		self.raw(f'JOIN {chan} {key}') if key else self.raw('JOIN ' + chan)
434
+
435
+	def listen(self):
436
+		while True:
437
+			try:
438
+				data = self.sock.recv(1024).decode('utf-8')
439
+				for line in (line for line in data.split('\r\n') if line):
440
+					debug(line)
441
+					if len(line.split()) >= 2:
442
+						self.handle_events(line)
443
+			except (UnicodeDecodeError,UnicodeEncodeError):
444
+				pass
445
+			except Exception as ex:
446
+				error('Unexpected error occured.', ex)
447
+				break
448
+		self.event_disconnect()
449
+
450
+	def mode(self, target, mode):
451
+		self.raw(f'MODE {target} {mode}')
452
+
453
+	def nick(self, nick):
454
+		self.raw('NICK ' + nick)
455
+
456
+	def raw(self, msg):
457
+		self.sock.send(bytes(msg + '\r\n', 'utf-8'))
458
+
459
+	def register(self):
460
+		if network_password:
461
+			self.raw('PASS ' + network_password)
462
+		self.raw(f'USER {username} 0 * :{realname}')
463
+		self.nick(nickname)
464
+
465
+	def sendmsg(self, target, msg):
466
+		self.raw(f'PRIVMSG {target} :{msg}')
467
+
468
+# Main
469
+if proxy:
470
+	try:
471
+		import socks
472
+	except ImportError:
473
+		error_exit('Missing PySocks module! (https://pypi.python.org/pypi/PySocks)')
474
+if use_ssl:
475
+	import ssl
476
+IRC().connect()