Better message priority internals
This commit is contained in:
parent
ef2abe3622
commit
85166fb692
|
@ -1,5 +1,5 @@
|
||||||
:mod:`StockPlay` --- Simulated stock trading game
|
:mod:`StockPlay` --- Stock-like trading game
|
||||||
=================================================
|
============================================
|
||||||
|
|
||||||
This module provides a simulated stock trading game. Requires and api key from
|
This module provides a simulated stock trading game. Requires and api key from
|
||||||
https://www.alphavantage.co/ to fetch stock quotes.
|
https://www.alphavantage.co/ to fetch stock quotes.
|
||||||
|
@ -7,12 +7,21 @@ https://www.alphavantage.co/ to fetch stock quotes.
|
||||||
Most commands require that the player login as described in the NickUser module.
|
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
|
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 reporting reasons we need to keep the prices of all
|
maximum of 5 requests per minute and 500 requests per day. For reasonable trading - that is, executing trades at the
|
||||||
traded symbols reasonably up-to-date (see *bginterval*). This happens at some interval.
|
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:
|
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
|
`(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 leaving no room for normal trades.
|
completely consume the daily limit.
|
||||||
|
|
||||||
|
When trading, the price of the traded symbol is allowed to be *tcachesecs* 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. The
|
||||||
|
*rcachesecs* setting controls the maximum price age before the API is hit.
|
||||||
|
|
||||||
|
|
||||||
Commands
|
Commands
|
||||||
|
@ -20,24 +29,18 @@ Commands
|
||||||
|
|
||||||
.. cmdoption:: .buy <amount> <symbol>
|
.. cmdoption:: .buy <amount> <symbol>
|
||||||
|
|
||||||
Buy some number of the specified stock symbol such as ".buy 10 amd"
|
Buy some number of the specified symbol such as ".buy 10 amd"
|
||||||
|
|
||||||
.. cmdoption:: .sell <amount> <symbol>
|
.. cmdoption:: .sell <amount> <symbol>
|
||||||
|
|
||||||
Sell similar to .buy
|
Sell similar to .buy
|
||||||
|
|
||||||
.. cmdoption:: .bal
|
.. cmdoption:: .port [<player>] [<full>]
|
||||||
|
|
||||||
Show a summary report on the value of the player's cash + stock holdings
|
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
|
||||||
.. cmdoption:: .cash
|
full listing of the player's holdings. Per the above, values based on symbol prices may be delayed based on the
|
||||||
|
*rcachesecs* config setting.
|
||||||
Show the player's cash balance
|
|
||||||
|
|
||||||
.. cmdoption:: .port
|
|
||||||
|
|
||||||
Get a report on the player's portfolio. Value based on stocks may be delayed based on the *rcachesecs*
|
|
||||||
config setting.
|
|
||||||
|
|
||||||
|
|
||||||
Config
|
Config
|
||||||
|
@ -72,13 +75,13 @@ Config
|
||||||
|
|
||||||
.. cmdoption:: tcachesecs
|
.. cmdoption:: tcachesecs
|
||||||
|
|
||||||
When performing a trade, how old of a cached stock value is permitted before fetching from API.
|
When performing a trade, how old of a cached symbol price is permitted before fetching from API.
|
||||||
|
|
||||||
Recommended ~30 minutes (1800)
|
Recommended ~30 minutes (1800)
|
||||||
|
|
||||||
.. cmdoption:: rcachesecs
|
.. cmdoption:: rcachesecs
|
||||||
|
|
||||||
When calculating a portfolio report, how old of a cached stock value is permitted before fetching from API.
|
When calculating a portfolio report, how old of a cached symbol price is permitted before fetching from API.
|
||||||
|
|
||||||
Recommended ~4 hours (14400)
|
Recommended ~4 hours (14400)
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,8 @@ from inspect import getargspec
|
||||||
from pyircbot.common import burstbucket, parse_irc_line
|
from pyircbot.common import burstbucket, parse_irc_line
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
|
||||||
IRCEvent = namedtuple("IRCEvent", "command args prefix trailing")
|
IRCEvent = namedtuple("IRCEvent", "command args prefix trailing")
|
||||||
UserPrefix = namedtuple("UserPrefix", "nick username hostname")
|
UserPrefix = namedtuple("UserPrefix", "nick username hostname")
|
||||||
|
@ -110,8 +112,8 @@ class IRCCore(object):
|
||||||
async def outputqueue(self):
|
async def outputqueue(self):
|
||||||
self.bucket = burstbucket(self.rate_max, self.rate_int)
|
self.bucket = burstbucket(self.rate_max, self.rate_int)
|
||||||
while True:
|
while True:
|
||||||
prio, line = await self.outputq.get()
|
|
||||||
# sleep until the bucket allows us to send
|
# 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:
|
if self.rate_limit:
|
||||||
while True:
|
while True:
|
||||||
s = self.bucket.get()
|
s = self.bucket.get()
|
||||||
|
@ -119,6 +121,7 @@ class IRCCore(object):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
await asyncio.sleep(s, loop=self._loop)
|
await asyncio.sleep(s, loop=self._loop)
|
||||||
|
prio, _, line = await self.outputq.get()
|
||||||
self.fire_hook('_SEND', args=None, prefix=None, trailing=None)
|
self.fire_hook('_SEND', args=None, prefix=None, trailing=None)
|
||||||
self.log.debug(">>> {}".format(repr(line)))
|
self.log.debug(">>> {}".format(repr(line)))
|
||||||
self.outputq.task_done()
|
self.outputq.task_done()
|
||||||
|
@ -152,7 +155,7 @@ class IRCCore(object):
|
||||||
if priority is None:
|
if priority is None:
|
||||||
self.outseq += 1
|
self.outseq += 1
|
||||||
priority = self.outseq
|
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 "
|
" Module related code "
|
||||||
def initHooks(self):
|
def initHooks(self):
|
||||||
|
@ -313,12 +316,12 @@ class IRCCore(object):
|
||||||
return self.nick
|
return self.nick
|
||||||
|
|
||||||
" Action Methods "
|
" Action Methods "
|
||||||
def act_PONG(self, data):
|
def act_PONG(self, data, priority=1):
|
||||||
"""Use the `/pong` command - respond to server pings
|
"""Use the `/pong` command - respond to server pings
|
||||||
|
|
||||||
:param data: the string or number the server sent with it's ping
|
:param data: the string or number the server sent with it's ping
|
||||||
:type data: str"""
|
:type data: str"""
|
||||||
self.sendRaw("PONG :%s" % data)
|
self.sendRaw("PONG :%s" % data, priority)
|
||||||
|
|
||||||
def act_USER(self, username, hostname, realname, priority=2):
|
def act_USER(self, username, hostname, realname, priority=2):
|
||||||
"""Use the USER protocol command. Used during connection
|
"""Use the USER protocol command. Used during connection
|
||||||
|
@ -344,18 +347,18 @@ class IRCCore(object):
|
||||||
|
|
||||||
:param channel: the channel to attempt to join
|
:param channel: the channel to attempt to join
|
||||||
:type channel: str"""
|
: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
|
"""Use the `/msg` command
|
||||||
|
|
||||||
:param towho: the target #channel or user's name
|
:param towho: the target #channel or user's name
|
||||||
:type towho: str
|
:type towho: str
|
||||||
:param message: the message to send
|
:param message: the message to send
|
||||||
:type message: str"""
|
: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
|
"""Use the `/mode` command
|
||||||
|
|
||||||
:param channel: the channel this mode is for
|
:param channel: the channel this mode is for
|
||||||
|
@ -365,20 +368,20 @@ class IRCCore(object):
|
||||||
:param extra: additional argument if the mode needs it. Example: user@*!*
|
:param extra: additional argument if the mode needs it. Example: user@*!*
|
||||||
:type extra: str"""
|
:type extra: str"""
|
||||||
if extra is not None:
|
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:
|
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
|
"""Use the `/me <action>` command
|
||||||
|
|
||||||
:param channel: the channel name or target's name the message is sent to
|
:param channel: the channel name or target's name the message is sent to
|
||||||
:type channel: str
|
:type channel: str
|
||||||
:param action: the text to send
|
:param action: the text to send
|
||||||
:type action: str"""
|
: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
|
"""Use the `/kick <user> <message>` command
|
||||||
|
|
||||||
:param channel: the channel from which the user will be kicked
|
:param channel: the channel from which the user will be kicked
|
||||||
|
@ -387,7 +390,7 @@ class IRCCore(object):
|
||||||
:type action: str
|
:type action: str
|
||||||
:param comment: the kick message
|
:param comment: the kick message
|
||||||
:type comment: str"""
|
: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):
|
def act_QUIT(self, message, priority=2):
|
||||||
"""Use the `/quit` command
|
"""Use the `/quit` command
|
||||||
|
@ -396,8 +399,8 @@ class IRCCore(object):
|
||||||
:type message: str"""
|
:type message: str"""
|
||||||
self.sendRaw("QUIT :%s" % message, priority)
|
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
|
Send server password, for use on connection
|
||||||
"""
|
"""
|
||||||
self.sendRaw("PASS %s" % password)
|
self.sendRaw("PASS %s" % password, priority)
|
||||||
|
|
|
@ -21,6 +21,22 @@ DUSTACCT = "#dust"
|
||||||
Trade = namedtuple("Trade", "nick buy symbol amount replyto")
|
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):
|
def format_price(cents, prefix="$", plus=False):
|
||||||
"""
|
"""
|
||||||
Formats cents as a dollar value
|
Formats cents as a dollar value
|
||||||
|
@ -36,6 +52,35 @@ def format_decimal(decm, prefix="$", plus=False):
|
||||||
return "{}{}{:,.2f}".format(prefix, "+" if plus and decm >= 0 else "", decm)
|
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):
|
class StockPlay(ModuleBase):
|
||||||
def __init__(self, bot, moduleName):
|
def __init__(self, bot, moduleName):
|
||||||
ModuleBase.__init__(self, bot, moduleName)
|
ModuleBase.__init__(self, bot, moduleName)
|
||||||
|
@ -82,6 +127,12 @@ class StockPlay(ModuleBase):
|
||||||
`cents` integer,
|
`cents` integer,
|
||||||
PRIMARY KEY(nick, day)
|
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
|
||||||
|
# );""")
|
||||||
|
|
||||||
# Last time the interval tasks were executed
|
# Last time the interval tasks were executed
|
||||||
self.task_time = 0
|
self.task_time = 0
|
||||||
|
@ -96,6 +147,31 @@ class StockPlay(ModuleBase):
|
||||||
self.pricer = Thread(target=self.price_updater)
|
self.pricer = Thread(target=self.price_updater)
|
||||||
self.pricer.start()
|
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
|
||||||
|
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():
|
||||||
|
count += row["count"] * (1 if row["type"] == "buy" else -1)
|
||||||
|
spent += row["price"] * (1 if row["type"] == "buy" else -1)
|
||||||
|
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(count)
|
||||||
|
|
||||||
def price_updater(self):
|
def price_updater(self):
|
||||||
"""
|
"""
|
||||||
Perform quote cache updating task
|
Perform quote cache updating task
|
||||||
|
@ -120,11 +196,6 @@ class StockPlay(ModuleBase):
|
||||||
delay -= 1
|
delay -= 1
|
||||||
sleep(1)
|
sleep(1)
|
||||||
|
|
||||||
def ondisable(self):
|
|
||||||
self.running = False
|
|
||||||
self.trader.join()
|
|
||||||
self.pricer.join()
|
|
||||||
|
|
||||||
def trader_background(self):
|
def trader_background(self):
|
||||||
"""
|
"""
|
||||||
Perform trading, reporting and other background tasks
|
Perform trading, reporting and other background tasks
|
||||||
|
@ -150,6 +221,9 @@ class StockPlay(ModuleBase):
|
||||||
def do_trade(self, trade):
|
def do_trade(self, trade):
|
||||||
"""
|
"""
|
||||||
Perform a queued trade
|
Perform a queued trade
|
||||||
|
|
||||||
|
:param trade: trade struct to perform
|
||||||
|
:type trade: Trade
|
||||||
"""
|
"""
|
||||||
self.log.warning("{} wants to {} {} of {}".format(trade.nick,
|
self.log.warning("{} wants to {} {} of {}".format(trade.nick,
|
||||||
"buy" if trade.buy else "sell",
|
"buy" if trade.buy else "sell",
|
||||||
|
@ -221,42 +295,42 @@ class StockPlay(ModuleBase):
|
||||||
"""
|
"""
|
||||||
Generate a text report of the nick's portfolio ::
|
Generate a text report of the nick's portfolio ::
|
||||||
|
|
||||||
<@player> .port
|
<player> .port profit full
|
||||||
<bot> player: cash: $2,501.73 stock value: ~$7,498.27 total: ~$10,000.00
|
<bloomberg_terminal> player: profit has cash: $491.02 stock value: ~$11,137.32 total: ~$11,628.34 (24h +1,504.37 (14.86%)⬆)
|
||||||
<bot> player: 122xAMD=$2,812.10, 10xFB=$1,673.30, 10xJNUG=$108.80, 5xINTC=$244.20, ...
|
<bloomberg_terminal> player: 1 AAPL bought at average $170.41 +3.92 (2.30%)⬆ now $174.33
|
||||||
<bot> player: 1xJD=$23.99, 1xMFGP=$19.78, 1xNOK=$6.16, 1xNVDA=$148.17, 1xTWTR=$30.01
|
<bloomberg_terminal> player: 14 AMD bought at average $23.05 +1.16 (5.03%)⬆ now $24.21
|
||||||
|
<bloomberg_terminal> player: 25 DBX bought at average $25.42 -1.08 (-4.25%)⬇ now $24.34
|
||||||
|
<bloomberg_terminal> player: 10 DENN bought at average $17.94 -0.27 (-1.51%)⬇ now $17.67
|
||||||
|
<bloomberg_terminal> player: 18 EA bought at average $99.77 -1.27 (-1.28%)⬇ now $98.50
|
||||||
|
<bloomberg_terminal> player: 10 INTC bought at average $53.23 +0.00 (0.00%)⬆ now $53.23
|
||||||
|
<bloomberg_terminal> player: 160 KPTI bought at average $4.88 +0.00 (0.00%)⬆ now $4.88
|
||||||
"""
|
"""
|
||||||
data = self.build_report(lookup)
|
data = self.build_report(lookup)
|
||||||
dest = sender if full else replyto
|
dest = sender if full else replyto
|
||||||
|
|
||||||
# Format holdings as "{symbol}x{price}={total_value}"
|
self.bot.act_PRIVMSG(dest, "{}: {} cash: {} stock value: ~{} total: ~{} (24h {})"
|
||||||
sym_x_count = []
|
|
||||||
for symbol, count, symprice in data["holdings"]:
|
|
||||||
sym_x_count.append("{}x{}={}".format(count, symbol, format_decimal(symprice * count)))
|
|
||||||
|
|
||||||
profit = data["24hgain"] >= 0
|
|
||||||
gainloss = "(24h {}{} ({:,.2f}%){}\x0f)" \
|
|
||||||
.format("\x0303" if profit else "\x0304", # green or red
|
|
||||||
format_decimal(data["24hgain"], prefix="", plus=True),
|
|
||||||
data["24hpct"] * 100,
|
|
||||||
"⬆" if profit else "⬇")
|
|
||||||
|
|
||||||
self.bot.act_PRIVMSG(dest, "{}: {} cash: {} stock value: ~{} total: ~{} {}"
|
|
||||||
.format(sender,
|
.format(sender,
|
||||||
"you have" if lookup == sender else "{} has".format(lookup),
|
"you have" if lookup == sender else "{} has".format(lookup),
|
||||||
format_decimal(data["cash"]),
|
format_decimal(data["cash"]),
|
||||||
format_decimal(data["holding_value"]),
|
format_decimal(data["holding_value"]),
|
||||||
format_decimal(data["cash"] + data["holding_value"]),
|
format_decimal(data["cash"] + data["holding_value"]),
|
||||||
gainloss))
|
format_gainloss(data["24hgain"], data["24hpct"])))
|
||||||
|
|
||||||
# print each symbol_count/total value with a max of 10 symbols per line
|
if not full:
|
||||||
while full and sym_x_count:
|
return
|
||||||
message_segment = []
|
|
||||||
for i in range(min(len(sym_x_count), 10)): # show up to 10 "SYMx$d0llar, " strings per message
|
rows = []
|
||||||
message_segment.append(sym_x_count.pop(0))
|
for symbol, count, symprice, avgbuy, buychange in data["holdings"]:
|
||||||
if sym_x_count: # if there's more to print, append an ellipsis to indicate a forthcoming message
|
rows.append([count,
|
||||||
message_segment.append("...")
|
symbol,
|
||||||
self.bot.act_PRIVMSG(dest, "{}: {}".format(sender, ", ".join(message_segment)))
|
"bought at average",
|
||||||
|
format_decimal(avgbuy),
|
||||||
|
*format_gainloss_inner(symprice - avgbuy, buychange),
|
||||||
|
"now",
|
||||||
|
format_decimal(symprice)])
|
||||||
|
|
||||||
|
for line in tabulate(rows, justify=[False, True, True, False, False, False, True, False]):
|
||||||
|
self.bot.act_PRIVMSG(dest, "{}: {}".format(sender, line), priority=5)
|
||||||
|
|
||||||
def build_report(self, nick):
|
def build_report(self, nick):
|
||||||
"""
|
"""
|
||||||
|
@ -264,7 +338,7 @@ class StockPlay(ModuleBase):
|
||||||
"""
|
"""
|
||||||
cash = Decimal(self.get_bal(nick)) / 100
|
cash = Decimal(self.get_bal(nick)) / 100
|
||||||
|
|
||||||
# generate a list of (symbol, count, price) tuples of the player's holdings
|
# generate a list of (symbol, count, price, avgbuy, %change_on_avgbuy) tuples of the player's holdings
|
||||||
symbol_count = []
|
symbol_count = []
|
||||||
holding_value = Decimal(0)
|
holding_value = Decimal(0)
|
||||||
with closing(self.sql.getCursor()) as c:
|
with closing(self.sql.getCursor()) as c:
|
||||||
|
@ -276,18 +350,26 @@ class StockPlay(ModuleBase):
|
||||||
# is 86400 (1 day)
|
# is 86400 (1 day)
|
||||||
symprice = Decimal(self.get_price(row["symbol"], self.config["rcachesecs"]))
|
symprice = Decimal(self.get_price(row["symbol"], self.config["rcachesecs"]))
|
||||||
holding_value += symprice * row["count"]
|
holding_value += symprice * row["count"]
|
||||||
symbol_count.append((row["symbol"], row["count"], symprice))
|
avgbuy = self.calc_user_avgbuy(nick, row["symbol"])
|
||||||
|
symbol_count.append((row["symbol"],
|
||||||
|
row["count"],
|
||||||
|
symprice,
|
||||||
|
avgbuy,
|
||||||
|
calc_gain(avgbuy, symprice)))
|
||||||
|
|
||||||
|
symbol_count.sort(key=lambda x: x[0]) # sort by symbol name
|
||||||
|
|
||||||
# calculate gain/loss percent
|
# calculate gain/loss percent
|
||||||
# TODO 1 week / 2 week / 1 month averages
|
# TODO 1 week / 2 week / 1 month averages
|
||||||
day_start_bal = self.get_latest_hist_bal(nick)
|
day_start_bal = self.get_latest_hist_bal(nick)
|
||||||
gain_value = Decimal(0)
|
gain_value = Decimal(0)
|
||||||
gain_pct = Decimal(0)
|
gain_pct = Decimal(0)
|
||||||
|
|
||||||
if day_start_bal:
|
if day_start_bal:
|
||||||
newbal = cash + holding_value
|
newbal = cash + holding_value
|
||||||
startbal = Decimal(day_start_bal["cents"]) / 100
|
startbal = Decimal(day_start_bal["cents"]) / 100
|
||||||
gain_value = newbal - startbal
|
gain_value = newbal - startbal
|
||||||
gain_pct = gain_value / startbal
|
gain_pct = calc_gain(Decimal(day_start_bal["cents"]) / 100, cash + holding_value)
|
||||||
|
|
||||||
return {"cash": cash,
|
return {"cash": cash,
|
||||||
"holdings": symbol_count,
|
"holdings": symbol_count,
|
||||||
|
@ -472,6 +554,7 @@ class StockPlay(ModuleBase):
|
||||||
def set_bal(self, nick, amount):
|
def set_bal(self, nick, amount):
|
||||||
"""
|
"""
|
||||||
Set a player's balance
|
Set a player's balance
|
||||||
|
|
||||||
:param amount: new balance in cents
|
:param amount: new balance in cents
|
||||||
"""
|
"""
|
||||||
with closing(self.sql.getCursor()) as c:
|
with closing(self.sql.getCursor()) as c:
|
||||||
|
|
Loading…
Reference in New Issue