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() |