Merge branch 'master' of Stocks

This commit is contained in:
oddluck 2019-12-05 13:40:27 +00:00
commit 06d9737068
6 changed files with 663 additions and 0 deletions

2
Stocks/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.bak
__pycache__/

1
Stocks/README.md Normal file
View File

@ -0,0 +1 @@
Plugin to fetch various market information

71
Stocks/__init__.py Normal file
View File

@ -0,0 +1,71 @@
###
# Copyright (c) 2018, cottongin
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Stocks: Plugin to fetch various market information
"""
import supybot
import supybot.world as world
# Use this for the version of this plugin. You may wish to put a CVS keyword
# in here if you're keeping the plugin in CVS or some similar system.
__version__ = ""
# XXX Replace this with an appropriate author or supybot.Author instance.
__author__ = supybot.Author('cottongin', 'cottongin',
'cottongin@cottongin.club')
__maintainer__ = getattr(supybot.authors, 'oddluck',
supybot.Author('oddluck', 'oddluck', 'oddluck@riseup.net'))
# This is a dictionary mapping supybot.Author instances to lists of
# contributions.
__contributors__ = {}
# This is a url where the most recent plugin package can be downloaded.
__url__ = 'https://github.com/oddluck/limnoria-plugins/'
from . import config
from . import plugin
from imp import reload
# In case we're being reloaded.
reload(config)
reload(plugin)
# Add more reloads here if you add third-party modules and want them to be
# reloaded when this plugin is reloaded. Don't forget to import them as well!
if world.testing:
from . import test
Class = plugin.Class
configure = config.configure
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

60
Stocks/config.py Normal file
View File

@ -0,0 +1,60 @@
###
# Copyright (c) 2018, cottongin
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import supybot.conf as conf
import supybot.registry as registry
try:
from supybot.i18n import PluginInternationalization
_ = PluginInternationalization('Stocks')
except:
# Placeholder that allows to run the plugin on a bot
# without the i18n module
_ = lambda x: x
def configure(advanced):
# This will be called by supybot to configure this module. advanced is
# a bool that specifies whether the user identified themself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from supybot.questions import expect, anything, something, yn
conf.registerPlugin('Stocks', True)
Stocks = conf.registerPlugin('Stocks')
# This is where your configuration variables (if any) should go. For example:
# conf.registerGlobalValue(Stocks, 'someConfigVariableName',
# registry.Boolean(False, _("""Help for someConfigVariableName.""")))
#conf.registerGlobalValue(Predictit, 'GoogleAPIKey',
# registry.String('', """Google API key.""", private=True))
conf.registerGlobalValue(Stocks, 'StocksAPIKey',
registry.String('', """Stock API key.""", private=True))
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

491
Stocks/plugin.py Normal file
View File

@ -0,0 +1,491 @@
###
# Copyright (c) 2018, cottongin
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import requests
import datetime
import pendulum
import collections
import supybot.utils as utils
from supybot.commands import *
import supybot.plugins as plugins
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
try:
from supybot.i18n import PluginInternationalization
_ = PluginInternationalization('Stocks')
except ImportError:
# Placeholder that allows to run the plugin on a bot
# without the i18n module
_ = lambda x: x
class Stocks(callbacks.Plugin):
"""Plugin to fetch various market information"""
threaded = True
def __init__(self, irc):
self.__parent = super(Stocks, self)
self.__parent.__init__(irc)
# response headers from Postman go inside {} as 'key': 'value' format
self.HEADERS = {
}
def die(self):
self.__parent.die()
def _parseDelta(self, value):
"""parses delta and returns formatted string"""
if value > 0:
value = '{:.2f}'.format(value)
value = ircutils.mircColor(value, 'green')
elif value < 0:
value = '{:.2f}'.format(value)
value = ircutils.mircColor(value, 'red')
else:
value = '---'
return value
def _parsePct(self, value):
"""parses delta and returns formatted string"""
if value > 0:
value = '{:.2f}%'.format(value)
value = ircutils.mircColor(value, 'green')
elif value < 0:
value = '{:.2f}%'.format(value)
value = ircutils.mircColor(value, 'red')
else:
value = '---'
return value
def _parseCoins(self, data, optmarket=None):
ticker = []
def _humifyCap(cap):
if not cap:
return cap
if cap > 1000000000000:
cap = cap / 1000000000000
cap = '${:.2f}T'.format(cap)
return cap
elif cap > 1000000000:
cap = cap / 1000000000
cap = '${:.2f}B'.format(cap)
elif cap > 1000000:
cap = cap / 1000000
cap = '${:.2f}M'.format(cap)
else:
cap = '${:.2f}'.format(cap)
return cap
return cap
for symbol in data:
name = symbol
name = ircutils.bold(name)
symbol = data[symbol]['USD']
current_price = symbol['PRICE']
change = symbol['CHANGEDAY']
pct_change = symbol['CHANGEPCTDAY']
high24 = '${:g}'.format(symbol['HIGH24HOUR'])
low24 = '${:g}'.format(symbol['LOW24HOUR'])
mcap = _humifyCap(symbol['MKTCAP'])
if 0 < pct_change < 0.5:
change = ircutils.mircColor('+{:g}'.format(change), 'yellow')
pct_change = ircutils.mircColor('+{:.2g}%'.format(pct_change), 'yellow')
elif pct_change >= 0.5:
change = ircutils.mircColor('+{:g}'.format(change), 'green')
pct_change = ircutils.mircColor('+{:.2g}%'.format(pct_change), 'green')
elif 0 > pct_change > -0.5:
change = ircutils.mircColor('{:g}'.format(change), 'orange')
pct_change = ircutils.mircColor('{:.2g}%'.format(pct_change), 'orange')
elif pct_change <= -0.5:
change = ircutils.mircColor('{:g}'.format(change), 'red')
pct_change = ircutils.mircColor('{:.2g}%'.format(pct_change), 'red')
else:
change = '{:g}'.format(change)
pct_change = '{:g}%'.format(pct_change)
string = '{} ${:g} {} ({})'.format(name, current_price, change, pct_change)
if optmarket:
if optmarket.lower() in name.lower():
string += ' :: \x02Market Cap:\x02 {} | \x0224hr High:\x02 {} | \x0224hr Low:\x02 {}'.format(
mcap, ircutils.mircColor(high24, 'green'), ircutils.mircColor(low24, 'red'))
ticker.append(string)
else:
ticker.append(string)
return ticker
def _parseTickerPrices(self, data, optmarket):
ticker = []
def _humifyCap(cap):
if not cap:
return cap
if cap > 1000000000000:
cap = cap / 1000000000000
cap = '${:.2f}T'.format(cap)
return cap
elif cap > 1000000000:
cap = cap / 1000000000
cap = '${:.2f}B'.format(cap)
elif cap > 1000000:
cap = cap / 1000000
cap = '${:.2f}M'.format(cap)
else:
cap = '${:.2f}'.format(cap)
return cap
return cap
googs = ["GOOGL:US", "GOOG:US"]
for symbol in data:
if symbol['id'] in googs:
name = symbol['shortName'].title()
else:
name = symbol['longName']
name = ircutils.bold(name)
current_price = symbol['price']
change = symbol['priceChange1Day']
pct_change = symbol['percentChange1Day']
prv_close = symbol['previousClosingPriceOneTradingDayAgo']
market_cap = _humifyCap(symbol.get('marketCap'))
highYear = '${:.2f}'.format(symbol['highPrice52Week'])
lowYear = '${:.2f}'.format(symbol['lowPrice52Week'])
pe_ratio = symbol.get('priceEarningsRatio')
ytd = symbol.get('totalReturnYtd')
oneyr = symbol.get('totalReturn1Year')
if pe_ratio:
pe_ratio = ' | \x02P/E Ratio:\x02 ${:.2f}'.format(pe_ratio)
else:
pe_ratio = ''
if market_cap:
market_cap = ' | \x02Market Cap:\x02 {} | '.format(market_cap)
else:
market_cap = ' | '
if 0 < pct_change < 0.5:
change = ircutils.mircColor('+{:.2f}'.format(change), 'yellow')
pct_change = ircutils.mircColor('+{:.2f}%'.format(pct_change), 'yellow')
elif pct_change >= 0.5:
change = ircutils.mircColor('+{:.2f}'.format(change), 'green')
pct_change = ircutils.mircColor('+{:.2f}%'.format(pct_change), 'green')
elif 0 > pct_change > -0.5:
change = ircutils.mircColor('{:.2f}'.format(change), 'orange')
pct_change = ircutils.mircColor('{:.2f}%'.format(pct_change), 'orange')
elif pct_change <= -0.5:
change = ircutils.mircColor('{:.2f}'.format(change), 'red')
pct_change = ircutils.mircColor('{:.2f}%'.format(pct_change), 'red')
else:
change = '{:.2f}'.format(change)
pct_change = '{:.2f}%'.format(pct_change)
string = ('{} ${:.2f} {} ({}) :: \x02Previous Close:\x02 ${:.2f}'
'{}\x0252wk High:\x02 {} | \x0252wk Low:\x02 {}'
'{}').format(
name, current_price, change, pct_change, prv_close, market_cap,
ircutils.mircColor(highYear, 'green'), ircutils.mircColor(lowYear, 'red'), pe_ratio)
if ytd:
if ytd > 0:
ytd = ircutils.mircColor('{:+.2f}%'.format(ytd), 'green')
elif ytd < 0:
ytd = ircutils.mircColor('{:.2f}%'.format(ytd), 'red')
else:
ytd = '{:.2f}%'.format(ytd)
string += ' | \x02YTD Return:\x02 {}'.format(ytd)
if oneyr:
if oneyr > 0:
oneyr = ircutils.mircColor('{:+.2f}%'.format(oneyr), 'green')
elif oneyr < 0:
oneyr = ircutils.mircColor('{:.2f}%'.format(oneyr), 'red')
else:
oneyr = '{:.2f}%'.format(oneyr)
string += ' | \x021Yr Return:\x02 {}'.format(oneyr)
ticker.append(string)
return ticker
def _parsePrices(self, data, optmarket=None):
ticker = []
for symbol in data:
name = symbol['shortName']
name = name.replace('COMPOSITE INDEX', '').strip()
if name == "Generic 1st 'SI' Future":
name = 'Silver'
elif name == "Generic 1st 'NG' Future":
name = 'Natural Gas'
elif 'WTI Crude' in name:
name = 'Oil'
elif 'S&P/TSX' in name:
name = 'TSX'
elif 'HANG SENG' in name:
name = 'SEHK'
elif 'NYSE' in name:
name = 'NYSE'
else:
name = symbol['shortName']
name = ircutils.bold(name)
current_price = symbol['price']
change = symbol['priceChange1Day']
pct_change = symbol['percentChange1Day']
prv_close = symbol['previousClosingPriceOneTradingDayAgo']
ytd = symbol.get('totalReturnYtd')
oneyr = symbol.get('totalReturn1Year')
if 0 < pct_change < 0.5:
change = ircutils.mircColor('+{:.2f}'.format(change), 'yellow')
pct_change = ircutils.mircColor('+{:.2f}%'.format(pct_change), 'yellow')
elif pct_change >= 0.5:
change = ircutils.mircColor('+{:.2f}'.format(change), 'green')
pct_change = ircutils.mircColor('+{:.2f}%'.format(pct_change), 'green')
elif 0 > pct_change > -0.5:
change = ircutils.mircColor('{:.2f}'.format(change), 'orange')
pct_change = ircutils.mircColor('{:.2f}%'.format(pct_change), 'orange')
elif pct_change <= -0.5:
change = ircutils.mircColor('{:.2f}'.format(change), 'red')
pct_change = ircutils.mircColor('{:.2f}%'.format(pct_change), 'red')
else:
change = '{:.2f}'.format(change)
pct_change = '{:.2f}%'.format(pct_change)
string = '{} ${:.2f} {} ({})'.format(name, current_price, change, pct_change)
if optmarket:
if optmarket.lower() in symbol['shortName'].lower():
string += ' :: Previous Close: {:.2f}'.format(prv_close)
if ytd:
if ytd > 0:
ytd = ircutils.mircColor('{:+.2f}%'.format(ytd), 'green')
elif ytd < 0:
ytd = ircutils.mircColor('{:.2f}%'.format(ytd), 'red')
else:
ytd = '{:.2f}%'.format(ytd)
string += ' :: YTD Return: {}'.format(ytd)
if oneyr:
if oneyr > 0:
oneyr = ircutils.mircColor('{:+.2f}%'.format(oneyr), 'green')
elif oneyr < 0:
oneyr = ircutils.mircColor('{:.2f}%'.format(oneyr), 'red')
else:
oneyr = '{:.2f}%'.format(oneyr)
string += ' :: 1Yr Return: {}'.format(oneyr)
ticker.append(string)
else:
ticker.append(string)
return ticker
@wrap(['somethingWithoutSpaces'])
def coin(self, irc, msg, args, optcoin):
"""Fetches current values for a given coin"""
coin_url = 'https://min-api.cryptocompare.com/data/pricemultifull?fsyms={coins}&tsyms=USD'
coins = []
coins.append(optcoin)
coins_str = ','.join(c.upper() for c in coins)
coin_data = requests.get(coin_url.format(coins=coins_str))
coin_data = coin_data.json()
if 'RAW' not in coin_data:
irc.reply('ERROR: no coin found for {}'.format(optcoin))
return
output = []
tmp = {}
data = coin_data['RAW']
data2 = collections.OrderedDict.fromkeys(sorted(data))
for k,v in data.items():
data2.update({k: v})
output = self._parseCoins(data2, optcoin)
irc.reply(' | '.join(t for t in output))
return
@wrap([optional('somethingWithoutSpaces')])
def coins(self, irc, msg, args, optcoin):
"""Fetches current values for top 10 coins (+ dogecoin) trading by volume"""
volm_url = 'https://min-api.cryptocompare.com/data/top/totalvol?limit=10&tsym=USD'
coin_url = 'https://min-api.cryptocompare.com/data/pricemultifull?fsyms={coins}&tsyms=USD'
volm_data = requests.get(volm_url).json()
coins = []
for thing in volm_data['Data']:
name = thing['CoinInfo']['Name']
coins.append(name)
coins.append('DOGE')
coins_str = ','.join(c for c in coins)
coin_data = requests.get(coin_url.format(coins=coins_str))
coin_data = coin_data.json()
output = []
tmp = {}
data = coin_data['RAW']
tmp['BTC'] = data.pop('BTC')
data2 = collections.OrderedDict.fromkeys(sorted(data))
for k,v in data.items():
data2.update({k: v})
data2.update(tmp)
data2.move_to_end('BTC', last=False)
output = self._parseCoins(data2, optcoin)
irc.reply(' | '.join(t for t in output))
return
@wrap([optional('somethingWithoutSpaces')])
def markets(self, irc, msg, args, optmarket):
"""Fetches current values for several markets"""
url = ('https://www.bloomberg.com/markets2/api/datastrip/'
'{markets_to_fetch}'
'?locale=en&customTickerList=true')
m = 'INDU:IND,SPX:IND,CCMP:IND,NYA:IND,UKX:IND,NKY:IND,SPTSX:IND,HSI:IND'
url = url.format(markets_to_fetch=m)
try:
data = requests.get(url, headers=self.HEADERS).json()
except:
irc.reply('Something went wrong fetching data')
return
ticker = self._parsePrices(data, optmarket)
irc.reply(' | '.join(t for t in ticker))
return
@wrap([optional('somethingWithoutSpaces')])
def commodities(self, irc, msg, args, optmarket):
"""Fetches current values for several commodities"""
url = ('https://www.bloomberg.com/markets2/api/datastrip/'
'{markets_to_fetch}'
'?locale=en&customTickerList=true')
m = 'CL1:COM,NG1:COM,GC1:COM,SI1:COM'
url = url.format(markets_to_fetch=m)
try:
data = requests.get(url, headers=self.HEADERS).json()
except:
irc.reply('Something went wrong fetching data')
return
ticker = self._parsePrices(data, optmarket)
irc.reply(' | '.join(t for t in ticker))
return
@wrap([commalist('text')])
def ticker(self, irc, msg, args, optmarket):
"""Fetches current values for several commodities"""
url = ('https://www.bloomberg.com/markets2/api/datastrip/'
'{markets_to_fetch}'
'?locale=en&customTickerList=true')
search_url = ('https://search.bloomberg.com/lookup.json?'
'types=Company_Public,Index,Fund,Currency,'
'Commodity,Bond&exclude_subtypes=label:editorial'
'&group_size=3,3,3,3,3,3'
'&fields=name,slug,ticker_symbol,organization,title,primary_site'
'&query={query}')
for idx,v in enumerate(optmarket):
if v == 'google':
optmarket[idx] = 'googl'
optmarket.append('goog')
mkts = []
for m in optmarket:
try:
tmp_rank = 0
tmp_symbol = ''
search_data = requests.get(search_url.format(query=m.lower()), headers=self.HEADERS).json()
for group in search_data:
if group['total_results'] == 0:
continue
for result in group['results']:
if result['score'] > tmp_rank:
tmp_symbol = result['ticker_symbol']
tmp_rank = result['score']
if not tmp_symbol:
tmp_symbol = m.upper() + ":US"
mkts.append(tmp_symbol)
except:
mkts.append(m.upper() + ':US')
m = '{}'.format(','.join(i.upper() for i in mkts[:5]))
url = url.format(markets_to_fetch=m)
try:
data = requests.get(url, headers=self.HEADERS).json()
except:
irc.reply('Something went wrong fetching data, or couldn\'t find provided symbol')
return
ticker = self._parseTickerPrices(data, optmarket)
for symbol in ticker:
irc.reply(symbol)
return
Class = Stocks
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:

38
Stocks/test.py Normal file
View File

@ -0,0 +1,38 @@
###
# Copyright (c) 2018, cottongin
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
from supybot.test import *
class StocksTestCase(PluginTestCase):
plugins = ('Stocks',)
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: