Compare commits
37 Commits
Author | SHA1 | Date |
---|---|---|
|
13ea0fe52f | |
|
fb6a5766c7 | |
|
0db7e23383 | |
|
d183172ca7 | |
|
9e3a7b68fe | |
|
235921fbab | |
|
f2c6668e18 | |
|
f6d1e0ba16 | |
|
2bdece8b9e | |
|
166807e181 | |
|
55212a5977 | |
|
998d3a5f55 | |
|
46c1ef3562 | |
|
e2c412fdef | |
|
4161be93e4 | |
|
a986639588 | |
|
bd208a13df | |
|
eccea93a6d | |
|
525e87803f | |
|
b4b6d72607 | |
|
f9927e9acb | |
|
f6b6c13c12 | |
|
bdfadb0c11 | |
|
ca3b4f903f | |
|
2533c5872a | |
|
6404adb773 | |
|
6b9fd384ec | |
|
85166fb692 | |
|
ef2abe3622 | |
|
f31c307ee4 | |
|
01efeed44f | |
|
97941aa529 | |
|
7546b05191 | |
|
ade7fed3a8 | |
|
a00a5d9f9c | |
|
29f5fa9e0f | |
|
50357b91de |
|
@ -0,0 +1,6 @@
|
|||
/testenv/*
|
||||
/share/*
|
||||
/build/*
|
||||
/dist/*
|
||||
/*.egg-info
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
FROM ubuntu:bionic
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y python3-pip git && \
|
||||
useradd --home-dir /srv/bot bot && \
|
||||
mkdir -p /srv/bot && \
|
||||
chown bot /srv/bot
|
||||
|
||||
COPY ./requirements.txt /requirements.txt
|
||||
|
||||
RUN pip3 install -r /requirements.txt
|
||||
|
||||
COPY ./ /tmp/pyircbot
|
||||
RUN cd /tmp/pyircbot && \
|
||||
python3 setup.py install
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/pyircbot"]
|
||||
WORKDIR /srv/bot/
|
||||
CMD ["-c", "config.json"]
|
||||
USER bot
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
===================================================
|
||||
|
||||
A module providing a simple login/logout account service. "Trust" is based upon
|
||||
hostname - logging in autorizes your current hostname for your account data,
|
||||
hostname - logging in autorizes your current hostname for your account data,
|
||||
which is tied to your nick.
|
||||
|
||||
Commands
|
||||
|
@ -21,6 +21,24 @@ Commands
|
|||
|
||||
Log out of account (deauthorize your current hostname)
|
||||
|
||||
Utilities
|
||||
---------
|
||||
|
||||
NickUser provides a decorator that can be used to lock module commands methods
|
||||
behind a login:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pyircbot.modulebase import ModuleBase, command
|
||||
from pyircbot.modules.NickUser import protected
|
||||
|
||||
class MyModule(ModuleBase):
|
||||
|
||||
@command("foo", allow_private=True)
|
||||
@protected()
|
||||
def cmd_foo(self, message, command):
|
||||
print(message.prefix.nick, "called foo whiled logged in!")
|
||||
|
||||
Class Reference
|
||||
---------------
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
:mod:`StockIndex` --- DJIA and NASDAQ Quotes
|
||||
============================================
|
||||
|
||||
This module provides quotes for the DJIA and NASDAQ indexes. It requires a free API key from
|
||||
https://financialmodelingprep.com/
|
||||
|
||||
|
||||
Commands
|
||||
--------
|
||||
|
||||
.. cmdoption:: .djia
|
||||
|
||||
Display the DJIA index
|
||||
|
||||
.. cmdoption:: .nasdaq
|
||||
|
||||
Display the NASDAQ index
|
||||
|
||||
|
||||
Config
|
||||
------
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"apikey": "xxxxxxxxxxxxx",
|
||||
"cache_update_interval": 600,
|
||||
"cache_update_timeout": 10,
|
||||
"warning_thresh": 1800
|
||||
}
|
||||
|
||||
.. cmdoption:: apikey
|
||||
|
||||
API ley obtained from https://financialmodelingprep.com/
|
||||
|
||||
.. cmdoption:: cache_update_interval
|
||||
|
||||
How many seconds between fetching new index quotes from the API.
|
||||
|
||||
.. cmdoption:: cache_update_timeout
|
||||
|
||||
Maximum seconds to wait on the HTTP request sent to the API
|
||||
|
||||
.. cmdoption:: warning_thresh
|
||||
|
||||
A warning will be shown that the quote is out-of-date if the last successful fetch was longer ago than this
|
||||
setting's number of seconds.
|
||||
|
||||
|
||||
Class Reference
|
||||
---------------
|
||||
|
||||
.. automodule:: pyircbot.modules.StockIndex
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
|
@ -0,0 +1,134 @@
|
|||
:mod:`StockPlay` --- Stock-like trading game
|
||||
============================================
|
||||
|
||||
This module provides a simulated stock trading game. Requires and api key from
|
||||
https://www.alphavantage.co/ to fetch stock quotes.
|
||||
|
||||
Most commands require that the player login as described in the NickUser module.
|
||||
|
||||
Note that it is important to configure api limitations when configuring this module. The alphavantage.co api allows a
|
||||
maximum of 5 requests per minute and 500 requests per day. For reasonable trading - that is, executing trades at the
|
||||
current market price - we need to be able to lookup the price of any symbol at any time. Likewise, to generate reports
|
||||
we need to keep the prices of all symbols somewhat up to date. This happens at some interval - see *bginterval*.
|
||||
|
||||
Considering the daily limit means, when evenly spread, we can sent a request *no more often* than 173 seconds:
|
||||
`(24 * 60 * 60 / 500)` - and therefore, the value of *bginterval* must be some value larger than 173, as this value will
|
||||
completely consume the daily limit.
|
||||
|
||||
When trading, the price of the traded symbol is allowed to be *trade_cache_seconds* seconds old before the API will be
|
||||
used to fetch a more recent price. This value must be balanced against *bginterval* depending on your trade frequency
|
||||
and variety.
|
||||
|
||||
Background or batch-style tasks that rely on symbol prices run afoul with the above constraints - but in a
|
||||
magnified way as they rely on api-provided data to calculate player stats across many players at a time.
|
||||
|
||||
|
||||
Commands
|
||||
--------
|
||||
|
||||
.. cmdoption:: .buy <amount> <symbol>
|
||||
|
||||
Buy some number of the specified symbol such as ".buy 10 amd"
|
||||
|
||||
.. cmdoption:: .sell <amount> <symbol>
|
||||
|
||||
Sell similar to .buy
|
||||
|
||||
.. cmdoption:: .port [<player>] [<full>]
|
||||
|
||||
Get a report on the calling player's portfolio. Another player's name can be passed as an argument to retrieve
|
||||
information about a player other than the calling player. Finally, the 'full' argument can be added to retrieve a
|
||||
full listing of the player's holdings.
|
||||
|
||||
|
||||
Config
|
||||
------
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"startbalance": 10000,
|
||||
"tradedelay": 0,
|
||||
"trade_cache_seconds": 300,
|
||||
"bginterval": 300,
|
||||
"announce_trades": false,
|
||||
"announce_channel": "#trades",
|
||||
"providers": [
|
||||
{
|
||||
"provider": "iexcloud",
|
||||
"apikey": "xxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"provider": "alphavantage",
|
||||
"apikey": "xxxxxxxxxxxxxxx"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. cmdoption:: startbalance
|
||||
|
||||
Number of dollars that players start with
|
||||
|
||||
.. cmdoption:: tradedelay
|
||||
|
||||
Delay in seconds between differing trades of the same symbol. Multiple buys OR multiple sells are allowed, but
|
||||
not a mix.
|
||||
|
||||
NOT IMPLEMENTED
|
||||
|
||||
.. cmdoption:: providers
|
||||
|
||||
A list of providers to fetch stock data from
|
||||
|
||||
Supported providers:
|
||||
|
||||
* https://www.alphavantage.co/
|
||||
* https://iexcloud.io/
|
||||
|
||||
.. cmdoption:: trade_cache_seconds
|
||||
|
||||
When performing a trade, how old of a cached symbol price is permitted before fetching from API.
|
||||
|
||||
Recommended ~30 minutes (1800)
|
||||
|
||||
.. cmdoption:: bginterval
|
||||
|
||||
Symbol prices are updated in the background. This is necessary because fetching a portfolio report may require
|
||||
fetching many symbol prices. The alphavantage.co api allows only 5 calls per minute. Because of this limitation,
|
||||
fetching a report would take multiple minutes with more than 5 symbols, which would not work.
|
||||
|
||||
For this reason, we update symbols at a low interval in the background. Every *bginterval* seconds, a task will be
|
||||
started that updates the price of the oldest symbol.
|
||||
|
||||
Estimated 5 minute (300), but likely will need tuning depending on playerbase
|
||||
|
||||
.. cmdoption:: midnight_offset
|
||||
|
||||
Number of seconds **added** to the clock when calculating midnight.
|
||||
|
||||
At midnight, the bot logs all player balances for use in gain/loss over time calculations later on. If you want this
|
||||
to happen at midnight system time, leave this at 0. Otherwise, it can be set to some number of seconds to e.g. to
|
||||
compensate for time zones.
|
||||
|
||||
Default: 0
|
||||
|
||||
.. cmdoption:: announce_trades
|
||||
|
||||
Boolean option to announce all trades in a specific channel.
|
||||
|
||||
Default: false
|
||||
|
||||
.. cmdoption:: announce_channel
|
||||
|
||||
Channel name to announce all trades in.
|
||||
|
||||
Default: not set
|
||||
|
||||
|
||||
Class Reference
|
||||
---------------
|
||||
|
||||
.. automodule:: pyircbot.modules.StockPlay
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
|
@ -1,13 +1,14 @@
|
|||
FROM ubuntu:artful
|
||||
FROM ubuntu:bionic
|
||||
|
||||
ADD ./ /tmp/pyircbot/
|
||||
|
||||
RUN apt-get update ; \
|
||||
RUN apt-get update && \
|
||||
export DEBIAN_FRONTEND=noninteractive && \
|
||||
apt-get install -y python3 python3-sphinx python3-setuptools python3-dev python3-pip make wget unzip git
|
||||
|
||||
RUN cd /tmp/pyircbot/ && pip3 install -r requirements.txt
|
||||
COPY ./docs/builder/start /start
|
||||
|
||||
COPY docs/builder/start /start
|
||||
COPY ./ /tmp/pyircbot/
|
||||
|
||||
RUN cd /tmp/pyircbot/ && pip3 install -r requirements-test.txt
|
||||
|
||||
RUN chmod +x /start ; \
|
||||
mkdir /tmp/docs
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh -ex
|
||||
|
||||
sudo docker run -it --rm -v $PWD/:/tmp/pyircbot/ pybdocbuilder bash
|
||||
docker run -it --rm -v $PWD/:/tmp/pyircbot/ pybdocbuilder bash
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#!/bin/sh -ex
|
||||
|
||||
docker build -t pybdocbuilder -f docs/builder/Dockerfile .
|
||||
|
||||
|
|
|
@ -2,8 +2,19 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
* :release:`4.1.0 <2017-03-28>`
|
||||
* :feature:`-` Added StockPlay module
|
||||
* :feature:`-` Added `@protected` decorator
|
||||
|
||||
* :release:`4.1.0 <2019-02-10>`
|
||||
* :support:`-` First documented release in awhile. Many new modules and tests have been added. See the git log if you so desire.
|
||||
* :feature:`-` Upgraded docker base image to ubuntu:bionic
|
||||
* :feature:`-` Misc macOs related fixes
|
||||
* :feature:`-` Misc python 3.7 related fixes
|
||||
* :feature:`-` Misc fixes preventing doc building
|
||||
|
||||
* :release:`4.0.0 <2017-03-28>`
|
||||
* :support:`-` Added a changelog
|
||||
* :feature:`-` Dropped use of deprecated asynchat in favor of asyncio - Python 3.5+ now required.
|
||||
* :support:`-` Minor docs cleanup
|
||||
* :feature:`-` Upgraded docker base image to ubuntu:xenial
|
||||
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
Contributing
|
||||
============
|
||||
|
||||
The pull requests, bug reports, feature suggestions or even demo use cases contributed by the pyircbot community are all
|
||||
highly appreciated and a valuable asset to the project.
|
||||
|
||||
This document outlines best practices and conditions for contributing changes pyircbot.
|
||||
|
||||
|
||||
Sending a Pull Request
|
||||
----------------------
|
||||
|
||||
The pyircbot project does have a couple rules for pull requests:
|
||||
|
||||
|
||||
Where
|
||||
While pyircbot's main git repository is hosted on a private server, PRs are welcome on the
|
||||
`github mirror <https://github.com/dpedu/pyircbot>`_ of the project.
|
||||
|
||||
|
||||
Linting
|
||||
Pyircbot's codebase is linted with flake8. The configuration arguments for flake8 are: ``--max-line-length 120
|
||||
--ignore E402,E712,E731,E211 --select E,F``. TODO add a flake8 config file to the repo so these args are codified.
|
||||
|
||||
|
||||
Python versions
|
||||
Pyircbot supports the two latest versions of python 3. At present, this is Python 3.6 and 3.7. All changes must be
|
||||
compatible with these versions. Using compatibility modules or checking the version at runtime is discouraged; the
|
||||
code should be written in a fashion that is interoperable.
|
||||
|
||||
|
||||
Dependencies
|
||||
The core of pyircbot - that is, the bot runtime itself and the basic modules needed to operate it - `PingResponder`
|
||||
and `Services` - have no dependencies outside python's standard library of modules. All changes must not change this
|
||||
condition. The use of 3rd party dependencies in pyircbot modules is allowed, provided the versions used do not
|
||||
conflict with modules already in use.
|
||||
|
||||
All 3rd party dependencies must be installable via Pip with no extra setup required (e.g. OS-level packages), with
|
||||
some exceptions:
|
||||
|
||||
- Requiring ``git`` for pip links directly to repositories is permitted.
|
||||
|
||||
- Requiring a C/C++ compiler available on the system as well as python headers which are typically required for
|
||||
libraries with C/C++ or other native language extensions is permitted. However, requiring additional headers or
|
||||
shared libraries is prohibited, unless:
|
||||
|
||||
- Many operating systems provide a ``python3-pip`` or similar package that depends on many other packages that violate
|
||||
the above rules, such as openssl headers or sqlite shared libraries. Pyircbot's preferred platform is Ubuntu Bionic,
|
||||
and any dependencies of such a package are permitted.
|
||||
|
||||
- These rules apply to code dependencies only, and do not apply to external *services* such as MySQL.
|
||||
|
||||
Modules that require setup or dependencies beyond these rules will not be accepted into the core module set. The
|
||||
recommended approach to use such a module is via the ``usermodules`` config directive.
|
||||
|
||||
No set of rules will handle all cases, and discussion or defending a change that violates these rules is encouraged.
|
||||
|
||||
|
||||
Tests
|
||||
Please consider the test suite when submitting changes affecting the core runtime and existing modules. Reduction of
|
||||
coverage is acceptable, but breaking or removing tests is not. Changes submitted with updated or new tests will
|
||||
be fast-tracked.
|
||||
|
||||
|
||||
Contributors to Pyircbot
|
||||
------------------------
|
||||
|
||||
Any change submitted to pyircbot is highly appreciated.
|
||||
|
||||
Our contributors, in no particular order:
|
||||
|
||||
- `@ollien <https://github.com/ollien>`_ ❤️
|
||||
- `@medmr1 <https://github.com/medmr1>`_ ❤️
|
||||
- `@dpedu <https://github.com/dpedu>`_ ❤️
|
|
@ -15,6 +15,7 @@ Contents:
|
|||
module_guide/_module_guide.rst
|
||||
module_guide/_pubsub_mode.rst
|
||||
changelog.rst
|
||||
contributing.rst
|
||||
|
||||
More Information
|
||||
================
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"startbalance": 10000,
|
||||
"tradedelay": 0,
|
||||
"apikey": "",
|
||||
"tcachesecs": 300,
|
||||
"rcachesecs": 14400,
|
||||
"bginterval": 300,
|
||||
"midnight_offset": 0,
|
||||
"announce_trades": true,
|
||||
"announce_channel": "#trades"
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
FROM ubuntu:artful
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/pyircbot"]
|
||||
WORKDIR /srv/bot/
|
||||
CMD ["-c", "config.json"]
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y python3 python3-setuptools python3-requests curl unzip sqlite3 && \
|
||||
easy_install3 pip && \
|
||||
pip3 install praw==5.0.1 pytz cherrypy twilio==6.9.0 && \
|
||||
cd /tmp && \
|
||||
curl -o msgbus.tar.gz 'http://gitlab.davepedu.com/dave/pymsgbus/repository/archive.tar.gz?ref=master' && \
|
||||
mkdir pymsgbus && tar zxvf msgbus.tar.gz --strip-components 1 -C pymsgbus/ &&\
|
||||
cd pymsgbus && \
|
||||
pip3 install -r requirements.txt && \
|
||||
python3 setup.py install && \
|
||||
cd /tmp && \
|
||||
curl -o bitcoinrpc.tar.gz https://codeload.github.com/dpedu/python-bitcoinrpc/tar.gz/master && \
|
||||
tar zxvf bitcoinrpc.tar.gz && \
|
||||
cd python-bitcoinrpc-master && \
|
||||
python3 setup.py install && \
|
||||
useradd --home-dir /srv/bot bot && \
|
||||
chown bot /srv/bot && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/bitcoinrpc.tar.gz /tmp/python-bitcoinrpc-master
|
||||
|
||||
COPY . /tmp/pyircbot/
|
||||
|
||||
RUN cd /tmp/pyircbot/ && \
|
||||
python3 setup.py install && \
|
||||
su -c "cp -r /tmp/pyircbot/examples/config.json /tmp/pyircbot/examples/data/ /srv/bot/" bot && \
|
||||
cd / && \
|
||||
rm -rf /tmp/pyircbot
|
||||
|
||||
USER bot
|
|
@ -3,12 +3,12 @@ import sys
|
|||
import logging
|
||||
import signal
|
||||
from argparse import ArgumentParser
|
||||
from pyircbot.common import load
|
||||
from pyircbot.common import load, sentry_sdk
|
||||
from pyircbot import PyIRCBot
|
||||
from json import loads
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def main():
|
||||
" logging level and facility "
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s")
|
||||
|
@ -33,6 +33,9 @@ if __name__ == "__main__":
|
|||
|
||||
botconfig = loads(sys.stdin.read()) if args.config == "-" else load(args.config)
|
||||
|
||||
if sentry_sdk and "dsn" in botconfig["bot"]:
|
||||
sentry_sdk.init(botconfig["bot"]["dsn"])
|
||||
|
||||
log.debug(botconfig)
|
||||
|
||||
bot = PyIRCBot(botconfig)
|
||||
|
@ -45,3 +48,7 @@ if __name__ == "__main__":
|
|||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
bot.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -9,7 +9,7 @@ from msgbus.client import MsgbusSubClient
|
|||
import pyircbot
|
||||
import traceback
|
||||
from pyircbot.pyircbot import PrimitiveBot
|
||||
from pyircbot.irccore import IRCEvent, UserPrefix
|
||||
from pyircbot.irccore import IRCEvent, UserPrefix, IRCCore
|
||||
from pyircbot.common import TouchReload
|
||||
from json import dumps
|
||||
|
||||
|
@ -57,12 +57,10 @@ class PyIRCBotSub(PrimitiveBot):
|
|||
args, sender, trailing, extras = loads(rest)
|
||||
nick, username, hostname = extras["prefix"]
|
||||
|
||||
msg = IRCEvent(command.upper(),
|
||||
args,
|
||||
UserPrefix(nick,
|
||||
username,
|
||||
hostname),
|
||||
trailing)
|
||||
msg = IRCCore.packetAsObject(command.upper(),
|
||||
args,
|
||||
f"{nick}!{username}@{hostname}", # hack
|
||||
trailing)
|
||||
|
||||
for module_name, module in self.moduleInstances.items():
|
||||
for hook in module.irchooks:
|
||||
|
@ -112,7 +110,7 @@ class PyIRCBotSub(PrimitiveBot):
|
|||
return self.meta.get("nick", None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def main():
|
||||
logging.basicConfig(level=logging.WARNING,
|
||||
format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s")
|
||||
log = logging.getLogger('main')
|
||||
|
@ -160,3 +158,6 @@ if __name__ == "__main__":
|
|||
|
||||
bot.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -5,11 +5,20 @@ from collections import namedtuple
|
|||
from time import sleep
|
||||
import os
|
||||
from threading import Thread
|
||||
try:
|
||||
import sentry_sdk
|
||||
except ImportError:
|
||||
sentry_sdk = None
|
||||
|
||||
|
||||
ParsedCommand = namedtuple("ParsedCommand", "command args args_str message")
|
||||
|
||||
|
||||
def report(exception):
|
||||
if sentry_sdk:
|
||||
sentry_sdk.capture_exception(exception)
|
||||
|
||||
|
||||
class burstbucket(object):
|
||||
def __init__(self, maximum, interval):
|
||||
"""
|
||||
|
|
|
@ -12,11 +12,13 @@ import logging
|
|||
import traceback
|
||||
import sys
|
||||
from inspect import getargspec
|
||||
from pyircbot.common import burstbucket, parse_irc_line
|
||||
from pyircbot.common import burstbucket, parse_irc_line, report
|
||||
from collections import namedtuple
|
||||
from io import StringIO
|
||||
from time import time
|
||||
|
||||
IRCEvent = namedtuple("IRCEvent", "command args prefix trailing")
|
||||
|
||||
IRCEvent = namedtuple("IRCEvent", "command args prefix trailing replyto")
|
||||
UserPrefix = namedtuple("UserPrefix", "nick username hostname")
|
||||
ServerPrefix = namedtuple("ServerPrefix", "hostname")
|
||||
|
||||
|
@ -78,9 +80,10 @@ class IRCCore(object):
|
|||
family=self.connection_family,
|
||||
local_addr=self.bind_addr)
|
||||
self.fire_hook("_CONNECT")
|
||||
except (socket.gaierror, ConnectionRefusedError):
|
||||
traceback.print_exc()
|
||||
except (socket.gaierror, ConnectionRefusedError, OSError) as e:
|
||||
logging.warning("Non-fatal connect error, trying next server...")
|
||||
self.trace()
|
||||
report(e)
|
||||
self.server = (self.server + 1) % len(self.servers)
|
||||
await asyncio.sleep(1, loop=loop)
|
||||
continue
|
||||
|
@ -95,11 +98,13 @@ class IRCCore(object):
|
|||
.format(command, prefix, args, trailing))
|
||||
else:
|
||||
self.fire_hook(command, args=args, prefix=prefix, trailing=trailing)
|
||||
except (ConnectionResetError, asyncio.streams.IncompleteReadError):
|
||||
traceback.print_exc()
|
||||
except (ConnectionResetError, asyncio.streams.IncompleteReadError) as e:
|
||||
self.trace()
|
||||
report(e)
|
||||
break
|
||||
except (UnicodeDecodeError, ):
|
||||
traceback.print_exc()
|
||||
except (UnicodeDecodeError, ) as e:
|
||||
self.trace()
|
||||
report(e)
|
||||
self.fire_hook("_DISCONNECT")
|
||||
self.writer.close()
|
||||
if self.alive:
|
||||
|
@ -110,8 +115,8 @@ class IRCCore(object):
|
|||
async def outputqueue(self):
|
||||
self.bucket = burstbucket(self.rate_max, self.rate_int)
|
||||
while True:
|
||||
prio, line = await self.outputq.get()
|
||||
# sleep until the bucket allows us to send
|
||||
# TODO warn/drop option if age (the _ above is older than some threshold)
|
||||
if self.rate_limit:
|
||||
while True:
|
||||
s = self.bucket.get()
|
||||
|
@ -119,14 +124,15 @@ class IRCCore(object):
|
|||
break
|
||||
else:
|
||||
await asyncio.sleep(s, loop=self._loop)
|
||||
prio, _, line = await self.outputq.get()
|
||||
self.fire_hook('_SEND', args=None, prefix=None, trailing=None)
|
||||
self.log.debug(">>> {}".format(repr(line)))
|
||||
self.outputq.task_done()
|
||||
try:
|
||||
self.writer.write((line + "\r\n").encode("UTF-8"))
|
||||
except Exception as e: # Probably fine if we drop messages while offline
|
||||
print(e)
|
||||
print(self.trace())
|
||||
self.trace()
|
||||
report(e)
|
||||
|
||||
async def kill(self, message="Help! Another thread is killing me :(", forever=True):
|
||||
"""Send quit message, flush queue, and close the socket
|
||||
|
@ -152,7 +158,7 @@ class IRCCore(object):
|
|||
if priority is None:
|
||||
self.outseq += 1
|
||||
priority = self.outseq
|
||||
asyncio.run_coroutine_threadsafe(self.outputq.put((priority, data, )), self._loop)
|
||||
asyncio.run_coroutine_threadsafe(self.outputq.put((priority, time(), data, )), self._loop)
|
||||
|
||||
" Module related code "
|
||||
def initHooks(self):
|
||||
|
@ -218,8 +224,9 @@ class IRCCore(object):
|
|||
else:
|
||||
hook(args, prefix, trailing)
|
||||
|
||||
except:
|
||||
except Exception as e:
|
||||
self.log.warning("Error processing hook: \n%s" % self.trace())
|
||||
report(e)
|
||||
|
||||
def addHook(self, command, method):
|
||||
"""**Internal.** Enable (connect) a single hook of a module
|
||||
|
@ -251,6 +258,7 @@ class IRCCore(object):
|
|||
self.log.warning("Invalid hook - %s" % command)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def packetAsObject(command, args, prefix, trailing):
|
||||
"""Given an irc message's args, prefix, and trailing data return an object with these properties
|
||||
|
||||
|
@ -262,9 +270,15 @@ class IRCCore(object):
|
|||
:type trailing: str
|
||||
:returns: object -- a IRCEvent object with the ``args``, ``prefix``, ``trailing``"""
|
||||
|
||||
return IRCEvent(command, args,
|
||||
IRCCore.decodePrefix(prefix) if prefix else None,
|
||||
trailing)
|
||||
prefix = IRCCore.decodePrefix(prefix) if prefix else None
|
||||
|
||||
replyto = None
|
||||
if command == "PRIVMSG":
|
||||
# prefix will always be set for PRIVMSG
|
||||
# TODO server side fuzzing
|
||||
replyto = args[0] if args[0].startswith("#") else prefix.nick
|
||||
|
||||
return IRCEvent(command, args, prefix, trailing, replyto)
|
||||
|
||||
" Utility methods "
|
||||
@staticmethod
|
||||
|
@ -313,12 +327,12 @@ class IRCCore(object):
|
|||
return self.nick
|
||||
|
||||
" Action Methods "
|
||||
def act_PONG(self, data):
|
||||
def act_PONG(self, data, priority=1):
|
||||
"""Use the `/pong` command - respond to server pings
|
||||
|
||||
:param data: the string or number the server sent with it's ping
|
||||
:type data: str"""
|
||||
self.sendRaw("PONG :%s" % data)
|
||||
self.sendRaw("PONG :%s" % data, priority)
|
||||
|
||||
def act_USER(self, username, hostname, realname, priority=2):
|
||||
"""Use the USER protocol command. Used during connection
|
||||
|
@ -344,18 +358,18 @@ class IRCCore(object):
|
|||
|
||||
:param channel: the channel to attempt to join
|
||||
:type channel: str"""
|
||||
self.sendRaw("JOIN %s" % channel, priority=3)
|
||||
self.sendRaw("JOIN %s" % channel, priority)
|
||||
|
||||
def act_PRIVMSG(self, towho, message):
|
||||
def act_PRIVMSG(self, towho, message, priority=3):
|
||||
"""Use the `/msg` command
|
||||
|
||||
:param towho: the target #channel or user's name
|
||||
:type towho: str
|
||||
:param message: the message to send
|
||||
:type message: str"""
|
||||
self.sendRaw("PRIVMSG %s :%s" % (towho, message))
|
||||
self.sendRaw("PRIVMSG %s :%s" % (towho, message), priority)
|
||||
|
||||
def act_MODE(self, channel, mode, extra=None):
|
||||
def act_MODE(self, channel, mode, extra=None, priority=2):
|
||||
"""Use the `/mode` command
|
||||
|
||||
:param channel: the channel this mode is for
|
||||
|
@ -365,20 +379,20 @@ class IRCCore(object):
|
|||
:param extra: additional argument if the mode needs it. Example: user@*!*
|
||||
:type extra: str"""
|
||||
if extra is not None:
|
||||
self.sendRaw("MODE %s %s %s" % (channel, mode, extra))
|
||||
self.sendRaw("MODE %s %s %s" % (channel, mode, extra), priority)
|
||||
else:
|
||||
self.sendRaw("MODE %s %s" % (channel, mode))
|
||||
self.sendRaw("MODE %s %s" % (channel, mode), priority)
|
||||
|
||||
def act_ACTION(self, channel, action):
|
||||
def act_ACTION(self, channel, action, priority=2):
|
||||
"""Use the `/me <action>` command
|
||||
|
||||
:param channel: the channel name or target's name the message is sent to
|
||||
:type channel: str
|
||||
:param action: the text to send
|
||||
:type action: str"""
|
||||
self.sendRaw("PRIVMSG %s :\x01ACTION %s" % (channel, action))
|
||||
self.sendRaw("PRIVMSG %s :\x01ACTION %s" % (channel, action), priority)
|
||||
|
||||
def act_KICK(self, channel, who, comment=""):
|
||||
def act_KICK(self, channel, who, comment="", priority=2):
|
||||
"""Use the `/kick <user> <message>` command
|
||||
|
||||
:param channel: the channel from which the user will be kicked
|
||||
|
@ -387,7 +401,7 @@ class IRCCore(object):
|
|||
:type action: str
|
||||
:param comment: the kick message
|
||||
:type comment: str"""
|
||||
self.sendRaw("KICK %s %s :%s" % (channel, who, comment))
|
||||
self.sendRaw("KICK %s %s :%s" % (channel, who, comment), priority)
|
||||
|
||||
def act_QUIT(self, message, priority=2):
|
||||
"""Use the `/quit` command
|
||||
|
@ -396,8 +410,8 @@ class IRCCore(object):
|
|||
:type message: str"""
|
||||
self.sendRaw("QUIT :%s" % message, priority)
|
||||
|
||||
def act_PASS(self, password):
|
||||
def act_PASS(self, password, priority=1):
|
||||
"""
|
||||
Send server password, for use on connection
|
||||
"""
|
||||
self.sendRaw("PASS %s" % password)
|
||||
self.sendRaw("PASS %s" % password, priority)
|
||||
|
|
|
@ -96,9 +96,8 @@ class RecieveGenerator(object):
|
|||
print("total", total, "expected", self.length)
|
||||
if total != self.length:
|
||||
raise TransferFailedException("Transfer failed: expected {} bytes but got {}".format(self.length, total))
|
||||
raise StopIteration()
|
||||
finally:
|
||||
self.sock.shutdown(socket.SHUT_RDWR)
|
||||
# self.sock.shutdown(socket.SHUT_RDWR)
|
||||
self.sock.close()
|
||||
|
||||
|
||||
|
@ -140,8 +139,11 @@ class OfferThread(Thread):
|
|||
clientsocket.shutdown(socket.SHUT_RDWR)
|
||||
clientsocket.close()
|
||||
finally:
|
||||
self.listener.shutdown(socket.SHUT_RDWR)
|
||||
# try:
|
||||
# self.listener.shutdown(socket.SHUT_RDWR)
|
||||
self.listener.close()
|
||||
# except Exception:
|
||||
# pass
|
||||
|
||||
def abort(self):
|
||||
"""
|
||||
|
|
|
@ -7,23 +7,16 @@
|
|||
|
||||
"""
|
||||
|
||||
from pyircbot.modulebase import ModuleBase, ModuleHook
|
||||
from pyircbot.modulebase import ModuleBase, hook
|
||||
|
||||
|
||||
class Error(ModuleBase):
|
||||
def __init__(self, bot, moduleName):
|
||||
ModuleBase.__init__(self, bot, moduleName)
|
||||
self.hooks = [ModuleHook("PRIVMSG", self.error)]
|
||||
|
||||
def error(self, args, prefix, trailing):
|
||||
"""If the message recieved from IRC has the string "error" in it, cause a ZeroDivisionError
|
||||
|
||||
:param args: IRC args received
|
||||
:type args: list
|
||||
:param prefix: IRC prefix of sender
|
||||
:type prefix: str
|
||||
:param trailing: IRC message body
|
||||
:type trailing: str"""
|
||||
if "error" in trailing:
|
||||
@hook("PRIVMSG")
|
||||
def error(self, message, command):
|
||||
"""
|
||||
If the message recieved from IRC has the string "error" in it, cause a ZeroDivisionError
|
||||
"""
|
||||
if "error" in message.trailing:
|
||||
print(10 / 0)
|
||||
|
||||
|
|
|
@ -72,7 +72,6 @@ class ModInfo(ModuleBase):
|
|||
# Find widest value per col
|
||||
for row in rows:
|
||||
for col, value in enumerate(row):
|
||||
print(col, value)
|
||||
vlen = len(value)
|
||||
if vlen > widths[col]:
|
||||
widths[col] = vlen
|
||||
|
@ -109,4 +108,3 @@ class ModInfo(ModuleBase):
|
|||
if callable(attr) and hasattr(attr, "irchelp"):
|
||||
for cmdinfo in attr.irchelp:
|
||||
yield (modname, module, cmdinfo.cmdspec, cmdinfo.docstring, cmdinfo.aliases)
|
||||
raise StopIteration()
|
||||
|
|
|
@ -50,13 +50,16 @@ class NickUser(ModuleBase):
|
|||
oldpass = attr.getKey(prefix.nick, "password")
|
||||
if oldpass is None:
|
||||
attr.setKey(prefix.nick, "password", cmd.args[0])
|
||||
self.bot.act_PRIVMSG(prefix.nick, ".setpass: Your password has been set to \"%s\"." % cmd.args[0])
|
||||
attr.setKey(prefix.nick, "loggedinfrom", prefix.hostname)
|
||||
self.bot.act_PRIVMSG(prefix.nick, ".setpass: You've been logged in and "
|
||||
"your password has been set to \"%s\"." % cmd.args[0])
|
||||
else:
|
||||
if len(cmd.args) == 2:
|
||||
if cmd.args[0] == oldpass:
|
||||
attr.setKey(prefix.nick, "password", cmd.args[1])
|
||||
self.bot.act_PRIVMSG(prefix.nick, ".setpass: Your password has been set to \"%s\"." %
|
||||
cmd.args[1])
|
||||
self.bot.act_PRIVMSG(prefix.nick,
|
||||
".setpass: Your password has been set to \"%s\"." % cmd.args[1])
|
||||
attr.setKey(prefix.nick, "loggedinfrom", prefix.hostname)
|
||||
else:
|
||||
self.bot.act_PRIVMSG(prefix.nick, ".setpass: Old password incorrect.")
|
||||
else:
|
||||
|
@ -72,10 +75,8 @@ class NickUser(ModuleBase):
|
|||
else:
|
||||
if len(cmd.args) == 1:
|
||||
if userpw == cmd.args[0]:
|
||||
#################
|
||||
attr.setKey(prefix.nick, "loggedinfrom", prefix.hostname)
|
||||
self.bot.act_PRIVMSG(prefix.nick, ".login: You have been logged in from: %s" % prefix.hostname)
|
||||
#################
|
||||
else:
|
||||
self.bot.act_PRIVMSG(prefix.nick, ".login: incorrect password.")
|
||||
else:
|
||||
|
@ -89,3 +90,23 @@ class NickUser(ModuleBase):
|
|||
else:
|
||||
attr.setKey(prefix.nick, "loggedinfrom", None)
|
||||
self.bot.act_PRIVMSG(prefix.nick, ".logout: You have been logged out.")
|
||||
|
||||
|
||||
# Decorator for methods that require login
|
||||
# Assumes your args matches the same format that @command(...) expects
|
||||
class protected(object):
|
||||
def __init__(self, message=None):
|
||||
self.message = message or "{}: you need to .login to do that"
|
||||
|
||||
def __call__(self, func):
|
||||
def wrapper(*args, **kwargs):
|
||||
module, message, command = args
|
||||
login = module.bot.getBestModuleForService("login")
|
||||
|
||||
if not login.check(message.prefix.nick, message.prefix.hostname):
|
||||
module.bot.act_PRIVMSG(message.args[0] if message.args[0].startswith("#") else message.prefix.nick,
|
||||
self.message.format(message.prefix.nick))
|
||||
return
|
||||
|
||||
func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
|
|
@ -22,7 +22,7 @@ class PingResponder(ModuleBase):
|
|||
"""Respond to the PING command"""
|
||||
# got a ping? send it right back
|
||||
self.bot.act_PONG(msg.trailing)
|
||||
self.log.info("%s Responded to a ping: %s" % (self.bot.get_nick(), msg.trailing))
|
||||
self.log.debug("%s Responded to a ping: %s" % (self.bot.get_nick(), msg.trailing))
|
||||
|
||||
@hook("_RECV", "_SEND")
|
||||
def resettimer(self, msg, cmd):
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
from pyircbot.modulebase import ModuleBase, command
|
||||
from pyircbot.modules.ModInfo import info
|
||||
from threading import Thread
|
||||
from time import sleep, time
|
||||
import requests
|
||||
import traceback
|
||||
|
||||
|
||||
API_URL = "https://financialmodelingprep.com/api/v3/quotes/index?apikey={apikey}"
|
||||
|
||||
|
||||
def bits(is_gain):
|
||||
if is_gain:
|
||||
return ("\x0303", "⬆", "+", )
|
||||
return ("\x0304", "⬇", "-", )
|
||||
|
||||
|
||||
class StockIndex(ModuleBase):
|
||||
def __init__(self, bot, moduleName):
|
||||
super().__init__(bot, moduleName)
|
||||
self.session = requests.session()
|
||||
self.updater = None
|
||||
self.running = True
|
||||
self.last_update = 0
|
||||
self.start_cache_updater()
|
||||
|
||||
def start_cache_updater(self):
|
||||
self.updater = Thread(target=self.cache_updater, daemon=True)
|
||||
self.updater.start()
|
||||
|
||||
def ondisable(self):
|
||||
self.running = False
|
||||
self.updater.join()
|
||||
|
||||
def cache_updater(self):
|
||||
while self.running:
|
||||
try:
|
||||
self.update_cache()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
delay = self.config.get("cache_update_interval", 600)
|
||||
while self.running and delay > 0:
|
||||
delay -= 1
|
||||
sleep(1)
|
||||
|
||||
def update_cache(self):
|
||||
data = self.session.get(API_URL.format(**self.config),
|
||||
timeout=self.config.get("cache_update_timeout", 10)).json()
|
||||
self.cache = {item["symbol"]: item for item in data}
|
||||
self.last_update = time()
|
||||
|
||||
@info("djia", "get the current value of the DJIA", cmds=["djia"])
|
||||
@command("djia", allow_private=True)
|
||||
def cmd_djia(self, message, command):
|
||||
self.send_quote("^DJI", "DJIA", message.replyto)
|
||||
|
||||
@info("nasdaq", "get the current value of the NASDAQ composite index", cmds=["nasdaq"])
|
||||
@command("nasdaq", allow_private=True)
|
||||
def cmd_nasdaq(self, message, command):
|
||||
self.send_quote("^IXIC", "NASDAQ", message.replyto)
|
||||
|
||||
@info("vix", "get the current value of the vix/fear index", cmds=["vix"])
|
||||
@command("vix", allow_private=True)
|
||||
def cmd_vix(self, message, command):
|
||||
self.send_quote("^VIX", "VIX", message.replyto)
|
||||
|
||||
def send_quote(self, key, symbol, to):
|
||||
index = self.cache[key]
|
||||
is_gain = index["price"] >= index["previousClose"]
|
||||
color, arrow, plusmin = bits(is_gain)
|
||||
|
||||
change = float(index["price"]) - float(index["previousClose"])
|
||||
percentchange = float(change) / float(index["previousClose"]) * 100
|
||||
|
||||
warn_thresh = self.config.get("warning_thresh", 1800)
|
||||
|
||||
warning = "" if time() - self.last_update < warn_thresh else " \x030(quote is out-of-date)"
|
||||
|
||||
self.bot.act_PRIVMSG(to, "{} ${:,.2f} {}{}{:,.2f} ({:.2f}%){}{}".format(symbol,
|
||||
index["price"],
|
||||
color,
|
||||
plusmin,
|
||||
change,
|
||||
percentchange,
|
||||
arrow,
|
||||
warning))
|
|
@ -0,0 +1,816 @@
|
|||
from pyircbot.modulebase import ModuleBase, MissingDependancyException, command
|
||||
from pyircbot.modules.ModInfo import info
|
||||
from pyircbot.modules.NickUser import protected
|
||||
from contextlib import closing
|
||||
from decimal import Decimal
|
||||
from time import sleep, time
|
||||
from queue import Queue, Empty
|
||||
from threading import Thread
|
||||
from requests import get
|
||||
from collections import namedtuple
|
||||
from math import ceil, floor
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
import json
|
||||
import traceback
|
||||
|
||||
|
||||
RE_SYMBOL = re.compile(r'^([A-Z\-]+)$')
|
||||
DUSTACCT = "#dust"
|
||||
|
||||
Trade = namedtuple("Trade", "nick buy symbol amount replyto")
|
||||
|
||||
|
||||
def tabulate(rows, justify=None):
|
||||
"""
|
||||
:param rows: list of lists making up the table data
|
||||
:param justify: array of True/False to enable left justification of text
|
||||
"""
|
||||
colwidths = [0] * len(rows[0])
|
||||
justify = justify or [False] * len(rows[0])
|
||||
for row in rows:
|
||||
for col, value in enumerate(row):
|
||||
colwidths[col] = max(colwidths[col], len(str(value)))
|
||||
|
||||
for row in rows:
|
||||
yield " ".join([("{: <{}}" if justify[coli] else "{: >{}}")
|
||||
.format(value, colwidths[coli]) for coli, value in enumerate(row)])
|
||||
|
||||
|
||||
def format_price(cents, prefix="$", plus=False):
|
||||
"""
|
||||
Formats cents as a dollar value
|
||||
"""
|
||||
return format_decimal((Decimal(cents) / 100) if cents > 0 else 0, # avoids "-0.00" output
|
||||
prefix, plus)
|
||||
|
||||
|
||||
def format_decimal(decm, prefix="$", plus=False):
|
||||
"""
|
||||
Formats a decimal as a dollar value
|
||||
"""
|
||||
return "{}{}{:,.2f}".format(prefix, "+" if plus and decm >= 0 else "", decm)
|
||||
|
||||
|
||||
def format_gainloss(diff, pct):
|
||||
"""
|
||||
Formats a difference and percent change as "+0.00 (0.52%)⬆" with appropriate IRC colors
|
||||
"""
|
||||
return ' '.join(format_gainloss_inner(diff, pct))
|
||||
|
||||
|
||||
def format_gainloss_inner(diff, pct):
|
||||
"""
|
||||
Formats a difference and percent change as "+0.00 (0.52%)⬆" with appropriate IRC colors
|
||||
"""
|
||||
profit = diff >= 0
|
||||
return "{}{}".format("\x0303" if profit else "\x0304", # green or red
|
||||
format_decimal(diff, prefix="", plus=True)), \
|
||||
"({:,.2f}%){}\x0f".format(pct * 100,
|
||||
"⬆" if profit else "⬇")
|
||||
|
||||
|
||||
def calc_gain(start, end):
|
||||
"""
|
||||
Calculate the +/- gain percent given start/end values
|
||||
:return: Decimal
|
||||
"""
|
||||
if not start:
|
||||
return Decimal(0)
|
||||
gain_value = end - start
|
||||
return Decimal(gain_value) / Decimal(start)
|
||||
|
||||
|
||||
class StockPlay(ModuleBase):
|
||||
def __init__(self, bot, moduleName):
|
||||
ModuleBase.__init__(self, bot, moduleName)
|
||||
self.sqlite = self.bot.getBestModuleForService("sqlite")
|
||||
|
||||
if self.sqlite is None:
|
||||
raise MissingDependancyException("StockPlay: SQLIite service is required.")
|
||||
|
||||
self.sql = self.sqlite.opendb("stockplay.db")
|
||||
|
||||
with closing(self.sql.getCursor()) as c:
|
||||
if not self.sql.tableExists("stockplay_balances"):
|
||||
c.execute("""CREATE TABLE `stockplay_balances` (
|
||||
`nick` varchar(64) PRIMARY KEY,
|
||||
`cents` integer
|
||||
);""")
|
||||
c.execute("""INSERT INTO `stockplay_balances` VALUES (?, ?)""", (DUSTACCT, 0))
|
||||
if not self.sql.tableExists("stockplay_holdings"):
|
||||
c.execute("""CREATE TABLE `stockplay_holdings` (
|
||||
`nick` varchar(64),
|
||||
`symbol` varchar(12),
|
||||
`count` integer,
|
||||
PRIMARY KEY (nick, symbol)
|
||||
);""")
|
||||
if not self.sql.tableExists("stockplay_trades"):
|
||||
c.execute("""CREATE TABLE `stockplay_trades` (
|
||||
`nick` varchar(64),
|
||||
`time` integer,
|
||||
`type` varchar(8),
|
||||
`symbol` varchar(12),
|
||||
`count` integer,
|
||||
`price` integer,
|
||||
`quoteprice` varchar(12)
|
||||
);""")
|
||||
if not self.sql.tableExists("stockplay_prices"):
|
||||
c.execute("""CREATE TABLE `stockplay_prices` (
|
||||
`symbol` varchar(12) PRIMARY KEY,
|
||||
`time` integer,
|
||||
`attempt_time` integer,
|
||||
`data` text
|
||||
);""")
|
||||
if not self.sql.tableExists("stockplay_balance_history"):
|
||||
c.execute("""CREATE TABLE `stockplay_balance_history` (
|
||||
`nick` varchar(64),
|
||||
`day` text,
|
||||
`cents` integer,
|
||||
PRIMARY KEY(nick, day)
|
||||
);""")
|
||||
# if not self.sql.tableExists("stockplay_report_cache"):
|
||||
# c.execute("""CREATE TABLE `stockplay_report_cache` (
|
||||
# `nick` varchar(64) PRIMARY KEY,
|
||||
# `time` integer,
|
||||
# `data` text
|
||||
# );""")
|
||||
|
||||
self.cache = PriceCache(self)
|
||||
|
||||
# Last time the interval tasks were executed
|
||||
self.task_time = 0
|
||||
|
||||
# background work executor thread
|
||||
self.asyncq = Queue()
|
||||
self.running = True
|
||||
|
||||
self.trader = Thread(target=self.trader_background)
|
||||
self.trader.start()
|
||||
|
||||
# quote updater thread
|
||||
self.pricer = Thread(target=self.price_updater)
|
||||
self.pricer.start()
|
||||
|
||||
def ondisable(self):
|
||||
self.running = False
|
||||
self.trader.join()
|
||||
self.pricer.join()
|
||||
|
||||
def calc_user_avgbuy(self, nick, symbol):
|
||||
"""
|
||||
Calculate the average buy price of a user's stock. This is generated by backtracking through their
|
||||
buy/sell history
|
||||
:return: price, in cents
|
||||
"""
|
||||
target_count = self.get_holding(nick, symbol) # backtrack until we hit this many shares
|
||||
spent = 0
|
||||
count = 0
|
||||
buys = 0
|
||||
with closing(self.sql.getCursor()) as c:
|
||||
for row in c.execute("SELECT * FROM stockplay_trades WHERE nick=? AND symbol=? ORDER BY time DESC",
|
||||
(nick, symbol)).fetchall():
|
||||
if row["type"] == "buy":
|
||||
count += row["count"]
|
||||
spent += row["price"]
|
||||
buys += row["count"]
|
||||
else:
|
||||
count -= row["count"]
|
||||
|
||||
if count == target_count: # at this point in history the user held 0 of the symbol, stop backtracking
|
||||
break
|
||||
if not count:
|
||||
return Decimal(0)
|
||||
return Decimal(spent) / 100 / Decimal(buys)
|
||||
|
||||
def price_updater(self):
|
||||
"""
|
||||
Perform quote cache updating task
|
||||
"""
|
||||
while self.running:
|
||||
self.log.info("price_updater")
|
||||
try:
|
||||
updatesym = None
|
||||
with closing(self.sql.getCursor()) as c:
|
||||
row = c.execute("""SELECT * FROM stockplay_prices
|
||||
WHERE symbol in (SELECT symbol FROM stockplay_holdings WHERE count>0)
|
||||
ORDER BY attempt_time ASC LIMIT 1""").fetchone()
|
||||
updatesym = row["symbol"] if row else None
|
||||
c.execute("UPDATE stockplay_prices SET attempt_time=? WHERE symbol=?;", (time(), updatesym))
|
||||
|
||||
if updatesym:
|
||||
self.cache.get_price(updatesym, 0)
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
delay = self.config["bginterval"]
|
||||
while self.running and delay > 0:
|
||||
delay -= 1
|
||||
sleep(1)
|
||||
|
||||
def trader_background(self):
|
||||
"""
|
||||
Perform trading, reporting and other background tasks
|
||||
"""
|
||||
while self.running:
|
||||
try:
|
||||
queued = None
|
||||
try:
|
||||
queued = self.asyncq.get(block=True, timeout=1)
|
||||
except Empty:
|
||||
self.do_tasks()
|
||||
continue
|
||||
if queued:
|
||||
action, data = queued
|
||||
if action == "trade":
|
||||
self.do_trade(data)
|
||||
elif action == "portreport":
|
||||
self.do_report(*data)
|
||||
elif action == "topten":
|
||||
self.do_topten(*data)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
def do_topten(self, nick, replyto):
|
||||
"""
|
||||
Do lookup of highest valued portfolios
|
||||
"""
|
||||
self.log.warning("{} wants top 10 sent to {}".format(nick, replyto))
|
||||
rows = []
|
||||
|
||||
with closing(self.sql.getCursor()) as c:
|
||||
for num, row in enumerate(c.execute("""SELECT h1.nick as nick, h1.cents as cents
|
||||
FROM stockplay_balance_history h1
|
||||
INNER JOIN (SELECT nick, max(day) as MaxDate FROM stockplay_balance_history
|
||||
WHERE nick != ? GROUP BY nick) h2
|
||||
ON h1.nick = h2.nick AND h1.day = h2.MaxDate
|
||||
ORDER BY cents DESC LIMIT 10""", (DUSTACCT, )).fetchall(), start=1):
|
||||
total = Decimal(row["cents"]) / 100
|
||||
rows.append(("#{}".format(num), row["nick"], "with total:", "~{}".format(format_decimal(total)), ))
|
||||
|
||||
for line in tabulate(rows, justify=[False, True, False, False]):
|
||||
self.bot.act_PRIVMSG(replyto, line, priority=5)
|
||||
|
||||
def do_trade(self, trade):
|
||||
"""
|
||||
Perform a queued trade
|
||||
|
||||
:param trade: trade struct to perform
|
||||
:type trade: Trade
|
||||
"""
|
||||
self.log.warning("{} wants to {} {} of {}".format(trade.nick,
|
||||
"buy" if trade.buy else "sell",
|
||||
trade.amount,
|
||||
trade.symbol))
|
||||
# Update quote price
|
||||
try:
|
||||
price = self.cache.get_price(trade.symbol, self.config["trade_cache_seconds"])
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
self.bot.act_PRIVMSG(trade.replyto, "{}: invalid symbol or api failure, trade aborted!"
|
||||
.format(trade.nick))
|
||||
return
|
||||
if price is None:
|
||||
self.bot.act_PRIVMSG(trade.replyto,
|
||||
"{}: invalid symbol '{}'".format(trade.nick, trade.symbol) |