|
|
|
@ -9,6 +9,7 @@ from threading import Thread
|
|
|
|
|
from requests import get
|
|
|
|
|
from collections import namedtuple
|
|
|
|
|
from math import ceil
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
import re
|
|
|
|
|
import json
|
|
|
|
|
import traceback
|
|
|
|
@ -20,8 +21,19 @@ DUSTACCT = "#dust"
|
|
|
|
|
Trade = namedtuple("Trade", "nick buy symbol amount replyto")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_price(cents):
|
|
|
|
|
return "${:,.2f}".format(Decimal(cents) / 100)
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StockPlay(ModuleBase):
|
|
|
|
@ -63,13 +75,24 @@ class StockPlay(ModuleBase):
|
|
|
|
|
`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)
|
|
|
|
|
);""")
|
|
|
|
|
|
|
|
|
|
# trade executor thread
|
|
|
|
|
# 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()
|
|
|
|
|
|
|
|
|
@ -104,144 +127,183 @@ class StockPlay(ModuleBase):
|
|
|
|
|
|
|
|
|
|
def trader_background(self):
|
|
|
|
|
"""
|
|
|
|
|
Perform trading and reporting tasks
|
|
|
|
|
Perform trading, reporting and other background tasks
|
|
|
|
|
"""
|
|
|
|
|
while self.running:
|
|
|
|
|
try:
|
|
|
|
|
self.do_background()
|
|
|
|
|
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)
|
|
|
|
|
except Exception:
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
def do_background(self):
|
|
|
|
|
queued = None
|
|
|
|
|
def do_trade(self, trade):
|
|
|
|
|
"""
|
|
|
|
|
Perform a queued trade
|
|
|
|
|
"""
|
|
|
|
|
self.log.warning("{} wants to {} {} of {}".format(trade.nick,
|
|
|
|
|
"buy" if trade.buy else "sell",
|
|
|
|
|
trade.amount,
|
|
|
|
|
trade.symbol))
|
|
|
|
|
# Update quote price
|
|
|
|
|
try:
|
|
|
|
|
queued = self.asyncq.get(block=True, timeout=1)
|
|
|
|
|
except Empty:
|
|
|
|
|
return
|
|
|
|
|
if not queued:
|
|
|
|
|
symprice = self.get_price(trade.symbol, self.config["tcachesecs"])
|
|
|
|
|
except Exception:
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
self.bot.act_PRIVMSG(trade.replyto, "{}: invalid symbol or api failure, trade aborted!"
|
|
|
|
|
.format(trade.nick))
|
|
|
|
|
return
|
|
|
|
|
if symprice is None:
|
|
|
|
|
self.bot.act_PRIVMSG(trade.replyto,
|
|
|
|
|
"{}: invalid symbol '{}'".format(trade.nick, trade.symbol))
|
|
|
|
|
return # invalid stock
|
|
|
|
|
|
|
|
|
|
# calculate various prices needed
|
|
|
|
|
# symprice -= Decimal("0.0001") # for testing dust collection
|
|
|
|
|
dprice = symprice * trade.amount
|
|
|
|
|
price_rounded = int(ceil(dprice * 100)) # now in cents
|
|
|
|
|
dust = abs((dprice * 100) - price_rounded) # cent fractions that we rounded out
|
|
|
|
|
self.log.info("our price: {}".format(price_rounded))
|
|
|
|
|
self.log.info("dust: {}".format(dust))
|
|
|
|
|
|
|
|
|
|
# fetch existing user balances
|
|
|
|
|
nickbal = self.get_bal(trade.nick)
|
|
|
|
|
count = self.get_holding(trade.nick, trade.symbol)
|
|
|
|
|
|
|
|
|
|
# check if trade is legal
|
|
|
|
|
if trade.buy and nickbal < price_rounded:
|
|
|
|
|
self.bot.act_PRIVMSG(trade.replyto, "{}: you can't afford {}."
|
|
|
|
|
.format(trade.nick, format_price(price_rounded)))
|
|
|
|
|
return # can't afford trade
|
|
|
|
|
if not trade.buy and trade.amount > count:
|
|
|
|
|
self.bot.act_PRIVMSG(trade.replyto, "{}: you don't have that many.".format(trade.nick))
|
|
|
|
|
return # asked to sell more shares than they have
|
|
|
|
|
|
|
|
|
|
# perform trade calculations
|
|
|
|
|
if trade.buy:
|
|
|
|
|
nickbal -= price_rounded
|
|
|
|
|
count += trade.amount
|
|
|
|
|
else:
|
|
|
|
|
nickbal += price_rounded
|
|
|
|
|
count -= trade.amount
|
|
|
|
|
|
|
|
|
|
# commit the trade
|
|
|
|
|
self.set_bal(trade.nick, nickbal)
|
|
|
|
|
self.set_holding(trade.nick, trade.symbol, count)
|
|
|
|
|
|
|
|
|
|
# save dust
|
|
|
|
|
dustbal = self.get_bal(DUSTACCT)
|
|
|
|
|
self.set_bal(DUSTACCT, dustbal + int(dust * 100))
|
|
|
|
|
|
|
|
|
|
# notify user
|
|
|
|
|
self.bot.act_PRIVMSG(trade.replyto,
|
|
|
|
|
"{}: {} {} {} for {}. cash: {}".format(trade.nick,
|
|
|
|
|
"bought" if trade.buy else "sold",
|
|
|
|
|
trade.amount,
|
|
|
|
|
trade.symbol,
|
|
|
|
|
format_price(price_rounded),
|
|
|
|
|
format_price(nickbal)))
|
|
|
|
|
|
|
|
|
|
self.log_trade(trade.nick, time(), "buy" if trade.buy else "sell",
|
|
|
|
|
trade.symbol, trade.amount, price_rounded)
|
|
|
|
|
|
|
|
|
|
def do_report(self, lookup, sender, replyto, full):
|
|
|
|
|
"""
|
|
|
|
|
Generate a text report of the nick's portfolio ::
|
|
|
|
|
|
|
|
|
|
action, data = queued
|
|
|
|
|
|
|
|
|
|
if action == "trade":
|
|
|
|
|
# Perform a stock trade
|
|
|
|
|
trade = data
|
|
|
|
|
self.log.warning("{} wants to {} {} of {}".format(trade.nick,
|
|
|
|
|
"buy" if trade.buy else "sell",
|
|
|
|
|
trade.amount,
|
|
|
|
|
trade.symbol))
|
|
|
|
|
# Update quote price
|
|
|
|
|
try:
|
|
|
|
|
symprice = self.get_price(trade.symbol, self.config["tcachesecs"])
|
|
|
|
|
except Exception:
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
self.bot.act_PRIVMSG(trade.replyto, "{}: invalid symbol or api failure, trade aborted!"
|
|
|
|
|
.format(trade.nick))
|
|
|
|
|
return
|
|
|
|
|
if symprice is None:
|
|
|
|
|
self.bot.act_PRIVMSG(trade.replyto,
|
|
|
|
|
"{}: invalid symbol '{}'".format(trade.nick, trade.symbol))
|
|
|
|
|
return # invalid stock
|
|
|
|
|
|
|
|
|
|
# calculate various prices needed
|
|
|
|
|
# symprice -= Decimal("0.0001") # for testing dust collection
|
|
|
|
|
dprice = symprice * trade.amount
|
|
|
|
|
# print("that would cost ", repr(dprice))
|
|
|
|
|
price_rounded = int(ceil(dprice * 100)) # now in cents
|
|
|
|
|
dust = abs((dprice * 100) - price_rounded) # cent fractions that we rounded out
|
|
|
|
|
self.log.info("our price: {}".format(price_rounded))
|
|
|
|
|
self.log.info("dust: {}".format(dust))
|
|
|
|
|
|
|
|
|
|
# fetch existing user balances
|
|
|
|
|
nickbal = self.get_bal(trade.nick)
|
|
|
|
|
count = self.get_holding(trade.nick, trade.symbol)
|
|
|
|
|
|
|
|
|
|
# check if trade is legal
|
|
|
|
|
if trade.buy and nickbal < price_rounded:
|
|
|
|
|
self.bot.act_PRIVMSG(trade.replyto, "{}: you can't afford {}."
|
|
|
|
|
.format(trade.nick, format_price(price_rounded)))
|
|
|
|
|
return # can't afford trade
|
|
|
|
|
if not trade.buy and trade.amount > count:
|
|
|
|
|
self.bot.act_PRIVMSG(trade.replyto, "{}: you don't have that many.".format(trade.nick))
|
|
|
|
|
return # asked to sell more shares than they have
|
|
|
|
|
|
|
|
|
|
# perform trade calculations
|
|
|
|
|
if trade.buy:
|
|
|
|
|
nickbal -= price_rounded
|
|
|
|
|
count += trade.amount
|
|
|
|
|
else:
|
|
|
|
|
nickbal += price_rounded
|
|
|
|
|
count -= trade.amount
|
|
|
|
|
|
|
|
|
|
# commit the trade
|
|
|
|
|
self.set_bal(trade.nick, nickbal)
|
|
|
|
|
self.set_holding(trade.nick, trade.symbol, count)
|
|
|
|
|
|
|
|
|
|
# save dust
|
|
|
|
|
dustbal = self.get_bal(DUSTACCT)
|
|
|
|
|
self.set_bal(DUSTACCT, dustbal + int(dust * 100))
|
|
|
|
|
<@player> .port
|
|
|
|
|
<bot> player: cash: $2,501.73 stock value: ~$7,498.27 total: ~$10,000.00
|
|
|
|
|
<bot> player: 122xAMD=$2,812.10, 10xFB=$1,673.30, 10xJNUG=$108.80, 5xINTC=$244.20, ...
|
|
|
|
|
<bot> player: 1xJD=$23.99, 1xMFGP=$19.78, 1xNOK=$6.16, 1xNVDA=$148.17, 1xTWTR=$30.01
|
|
|
|
|
"""
|
|
|
|
|
data = self.build_report(lookup)
|
|
|
|
|
dest = sender if full else replyto
|
|
|
|
|
|
|
|
|
|
# Format holdings as "{symbol}x{price}={total_value}"
|
|
|
|
|
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,
|
|
|
|
|
"you have" if lookup == sender else "{} has".format(lookup),
|
|
|
|
|
format_decimal(data["cash"]),
|
|
|
|
|
format_decimal(data["holding_value"]),
|
|
|
|
|
format_decimal(data["cash"] + data["holding_value"]),
|
|
|
|
|
gainloss))
|
|
|
|
|
|
|
|
|
|
# print each symbol_count/total value with a max of 10 symbols per line
|
|
|
|
|
while full and sym_x_count:
|
|
|
|
|
message_segment = []
|
|
|
|
|
for i in range(min(len(sym_x_count), 10)): # show up to 10 "SYMx$d0llar, " strings per message
|
|
|
|
|
message_segment.append(sym_x_count.pop(0))
|
|
|
|
|
if sym_x_count: # if there's more to print, append an ellipsis to indicate a forthcoming message
|
|
|
|
|
message_segment.append("...")
|
|
|
|
|
self.bot.act_PRIVMSG(dest, "{}: {}".format(sender, ", ".join(message_segment)))
|
|
|
|
|
|
|
|
|
|
def build_report(self, nick):
|
|
|
|
|
"""
|
|
|
|
|
Return a dict containing the player's cash, stock value, holdings listing, and 24 hour statistics.
|
|
|
|
|
"""
|
|
|
|
|
cash = Decimal(self.get_bal(nick)) / 100
|
|
|
|
|
|
|
|
|
|
# notify user
|
|
|
|
|
self.bot.act_PRIVMSG(trade.replyto,
|
|
|
|
|
"{}: {} {} {} for {}. cash: {}".format(trade.nick,
|
|
|
|
|
"bought" if trade.buy else "sold",
|
|
|
|
|
trade.amount,
|
|
|
|
|
trade.symbol,
|
|
|
|
|
format_price(price_rounded),
|
|
|
|
|
format_price(nickbal)))
|
|
|
|
|
|
|
|
|
|
self.log_trade(trade.nick, time(), "buy" if trade.buy else "sell",
|
|
|
|
|
trade.symbol, trade.amount, price_rounded)
|
|
|
|
|
|
|
|
|
|
elif action == "portreport":
|
|
|
|
|
# Generate a text report of the nick's portfolio
|
|
|
|
|
# <@player> .port
|
|
|
|
|
# <bot> player: cash: $2,501.73 stock value: ~$7,498.27 total: ~$10,000.00
|
|
|
|
|
# <bot> player: 122xAMD=$2,812.10, 10xFB=$1,673.30, 10xJNUG=$108.80, 5xINTC=$244.20, ...
|
|
|
|
|
# <bot> player: 1xJD=$23.99, 1xMFGP=$19.78, 1xNOK=$6.16, 1xNVDA=$148.17, 1xTWTR=$30.01
|
|
|
|
|
lookup, sender, replyto, full = data
|
|
|
|
|
cash = self.get_bal(lookup)
|
|
|
|
|
# when $full is true we PM the user instead
|
|
|
|
|
# when $full is false we just say their total value
|
|
|
|
|
|
|
|
|
|
# generate a list of (symbol, count, ) tuples of the player's symbol holdings
|
|
|
|
|
symbol_count = []
|
|
|
|
|
with closing(self.sql.getCursor()) as c:
|
|
|
|
|
for row in c.execute("SELECT * FROM stockplay_holdings WHERE count>0 AND nick=? ORDER BY count DESC",
|
|
|
|
|
(lookup, )).fetchall():
|
|
|
|
|
symbol_count.append((row["symbol"], row["count"], ))
|
|
|
|
|
|
|
|
|
|
# calculate the cash sum of the player's symbol holdings (while also formatting text representations)
|
|
|
|
|
sym_x_count = []
|
|
|
|
|
stock_value = Decimal(0)
|
|
|
|
|
for symbol, count in symbol_count:
|
|
|
|
|
# generate a list of (symbol, count, price) tuples of the player's holdings
|
|
|
|
|
symbol_count = []
|
|
|
|
|
holding_value = Decimal(0)
|
|
|
|
|
with closing(self.sql.getCursor()) as c:
|
|
|
|
|
for row in c.execute("SELECT * FROM stockplay_holdings WHERE count>0 AND nick=? ORDER BY count DESC",
|
|
|
|
|
(nick, )).fetchall():
|
|
|
|
|
# the API limits us to 5 requests per minute or 500 requests per day or about 1 request every 173s
|
|
|
|
|
# The background thread updates the oldest price every 5 minutes. Here, we allow even very stale quotes
|
|
|
|
|
# because it's simply impossible to request fresh data for every stock right now. Recommended rcachesecs
|
|
|
|
|
# is 86400 (1 day)
|
|
|
|
|
symprice = self.get_price(symbol, self.config["rcachesecs"])
|
|
|
|
|
dprice = Decimal(symprice * count) * 100
|
|
|
|
|
stock_value += dprice
|
|
|
|
|
sym_x_count.append("{}x{}={}".format(count, symbol, format_price(dprice)))
|
|
|
|
|
|
|
|
|
|
dest = sender if full else replyto
|
|
|
|
|
|
|
|
|
|
self.bot.act_PRIVMSG(dest, "{}: {} cash: {} stock value: ~{} total: ~{}"
|
|
|
|
|
.format(sender,
|
|
|
|
|
"you have" if lookup == sender else "{} has".format(lookup),
|
|
|
|
|
format_price(cash),
|
|
|
|
|
format_price(stock_value),
|
|
|
|
|
format_price(cash + stock_value)))
|
|
|
|
|
if full:
|
|
|
|
|
# print symbol_count with a max of 10 symbols per line
|
|
|
|
|
while sym_x_count:
|
|
|
|
|
message_segment = []
|
|
|
|
|
for i in range(min(len(sym_x_count), 10)): # show up to 10 "SYMx$d0llar, " strings per message
|
|
|
|
|
message_segment.append(sym_x_count.pop(0))
|
|
|
|
|
if sym_x_count: # if there's more to print, append an ellipsis to indicate a forthcoming message
|
|
|
|
|
message_segment.append("...")
|
|
|
|
|
self.bot.act_PRIVMSG(dest, "{}: {}".format(sender, ", ".join(message_segment)))
|
|
|
|
|
symprice = Decimal(self.get_price(row["symbol"], self.config["rcachesecs"]))
|
|
|
|
|
holding_value += symprice * row["count"]
|
|
|
|
|
symbol_count.append((row["symbol"], row["count"], symprice))
|
|
|
|
|
|
|
|
|
|
# calculate gain/loss percent
|
|
|
|
|
# TODO 1 week / 2 week / 1 month averages
|
|
|
|
|
day_start_bal = self.get_latest_hist_bal(nick)
|
|
|
|
|
gain_value = Decimal(0)
|
|
|
|
|
gain_pct = Decimal(0)
|
|
|
|
|
if day_start_bal:
|
|
|
|
|
newbal = cash + holding_value
|
|
|
|
|
startbal = Decimal(day_start_bal["cents"]) / 100
|
|
|
|
|
gain_value = newbal - startbal
|
|
|
|
|
gain_pct = gain_value / startbal
|
|
|
|
|
|
|
|
|
|
return {"cash": cash,
|
|
|
|
|
"holdings": symbol_count,
|
|
|
|
|
"holding_value": holding_value,
|
|
|
|
|
"24hgain": gain_value,
|
|
|
|
|
"24hpct": gain_pct}
|
|
|
|
|
|
|
|
|
|
def do_tasks(self):
|
|
|
|
|
"""
|
|
|
|
|
Do interval tasks such as recording nightly balances
|
|
|
|
|
"""
|
|
|
|
|
now = time()
|
|
|
|
|
if now - 60 < self.task_time:
|
|
|
|
|
return
|
|
|
|
|
self.task_time = now
|
|
|
|
|
self.record_nightly_balances()
|
|
|
|
|
|
|
|
|
|
def get_price(self, symbol, thresh=None):
|
|
|
|
|
"""
|
|
|
|
@ -280,7 +342,8 @@ class StockPlay(ModuleBase):
|
|
|
|
|
|
|
|
|
|
def fetch_priceinfo(self, symbol):
|
|
|
|
|
"""
|
|
|
|
|
API provides
|
|
|
|
|
Request a stock quote from the API. The API provides the format::
|
|
|
|
|
|
|
|
|
|
{'Global Quote': {
|
|
|
|
|
{'01. symbol': 'MSFT',
|
|
|
|
|
'02. open': '104.3900',
|
|
|
|
@ -292,7 +355,9 @@ class StockPlay(ModuleBase):
|
|
|
|
|
'08. previous close':'105.2700',
|
|
|
|
|
'09. change': '0.4000',
|
|
|
|
|
'10. change percent': '0.3800%'}}
|
|
|
|
|
Reformat as:
|
|
|
|
|
|
|
|
|
|
Reformat as::
|
|
|
|
|
|
|
|
|
|
{'symbol': 'AMD',
|
|
|
|
|
'open': '22.3300',
|
|
|
|
|
'high': '23.2750',
|
|
|
|
@ -318,6 +383,9 @@ class StockPlay(ModuleBase):
|
|
|
|
|
return {k[4:]: v for k, v in data.items() if k[4:] in keys}
|
|
|
|
|
|
|
|
|
|
def checksym(self, s):
|
|
|
|
|
"""
|
|
|
|
|
Validate that a string looks like a stock symbol
|
|
|
|
|
"""
|
|
|
|
|
if len(s) > 12:
|
|
|
|
|
return
|
|
|
|
|
s = s.upper()
|
|
|
|
@ -329,6 +397,9 @@ class StockPlay(ModuleBase):
|
|
|
|
|
@command("buy", require_args=True, allow_private=True)
|
|
|
|
|
@protected()
|
|
|
|
|
def cmd_buy(self, message, command):
|
|
|
|
|
"""
|
|
|
|
|
Command to buy stocks
|
|
|
|
|
"""
|
|
|
|
|
self.check_nick(message.prefix.nick)
|
|
|
|
|
amount = int(command.args[0])
|
|
|
|
|
symbol = self.checksym(command.args[1])
|
|
|
|
@ -344,6 +415,9 @@ class StockPlay(ModuleBase):
|
|
|
|
|
@command("sell", require_args=True, allow_private=True)
|
|
|
|
|
@protected()
|
|
|
|
|
def cmd_sell(self, message, command):
|
|
|
|
|
"""
|
|
|
|
|
Command to sell stocks
|
|
|
|
|
"""
|
|
|
|
|
self.check_nick(message.prefix.nick)
|
|
|
|
|
amount = int(command.args[0])
|
|
|
|
|
symbol = self.checksym(command.args[1])
|
|
|
|
@ -359,6 +433,9 @@ class StockPlay(ModuleBase):
|
|
|
|
|
@command("port", "portfolio", allow_private=True)
|
|
|
|
|
@protected()
|
|
|
|
|
def cmd_port(self, message, command):
|
|
|
|
|
"""
|
|
|
|
|
Portfolio report command
|
|
|
|
|
"""
|
|
|
|
|
full = False
|
|
|
|
|
lookup = message.prefix.nick
|
|
|
|
|
if command.args:
|
|
|
|
@ -376,27 +453,44 @@ class StockPlay(ModuleBase):
|
|
|
|
|
full)))
|
|
|
|
|
|
|
|
|
|
def check_nick(self, nick):
|
|
|
|
|
"""
|
|
|
|
|
Set up a user's account by setting the initial balance
|
|
|
|
|
"""
|
|
|
|
|
if not self.nick_exists(nick):
|
|
|
|
|
self.set_bal(nick, self.config["startbalance"] * 100) # initial balance for user
|
|
|
|
|
# TODO welcome message
|
|
|
|
|
# TODO maybe even some random free shares for funzies
|
|
|
|
|
|
|
|
|
|
def nick_exists(self, name):
|
|
|
|
|
"""
|
|
|
|
|
Check whether a nick has a record
|
|
|
|
|
"""
|
|
|
|
|
with closing(self.sql.getCursor()) as c:
|
|
|
|
|
return c.execute("SELECT COUNT(*) as num FROM stockplay_balances WHERE nick=?",
|
|
|
|
|
(name, )).fetchone()["num"] and True
|
|
|
|
|
|
|
|
|
|
def set_bal(self, nick, amount):
|
|
|
|
|
"""
|
|
|
|
|
Set a player's balance
|
|
|
|
|
:param amount: new balance in cents
|
|
|
|
|
"""
|
|
|
|
|
with closing(self.sql.getCursor()) as c:
|
|
|
|
|
c.execute("REPLACE INTO stockplay_balances VALUES (?, ?)",
|
|
|
|
|
(nick, amount, ))
|
|
|
|
|
|
|
|
|
|
def get_bal(self, nick):
|
|
|
|
|
"""
|
|
|
|
|
Get player's balance
|
|
|
|
|
:return: balance in cents
|
|
|
|
|
"""
|
|
|
|
|
with closing(self.sql.getCursor()) as c:
|
|
|
|
|
return c.execute("SELECT * FROM stockplay_balances WHERE nick=?",
|
|
|
|
|
(nick, )).fetchone()["cents"]
|
|
|
|
|
|
|
|
|
|
def get_holding(self, nick, symbol):
|
|
|
|
|
"""
|
|
|
|
|
Return the number of stocks of a certain symbol a player has
|
|
|
|
|
"""
|
|
|
|
|
assert symbol == symbol.upper()
|
|
|
|
|
with closing(self.sql.getCursor()) as c:
|
|
|
|
|
r = c.execute("SELECT * FROM stockplay_holdings WHERE nick=? AND symbol=?",
|
|
|
|
@ -404,11 +498,38 @@ class StockPlay(ModuleBase):
|
|
|
|
|
return r["count"] if r else 0
|
|
|
|
|
|
|
|
|
|
def set_holding(self, nick, symbol, count):
|
|
|
|
|
"""
|
|
|
|
|
Set the number of stocks of a certain symbol a player that
|
|
|
|
|
"""
|
|
|
|
|
with closing(self.sql.getCursor()) as c:
|
|
|
|
|
c.execute("REPLACE INTO stockplay_holdings VALUES (?, ?, ?)",
|
|
|
|
|
(nick, symbol, count, ))
|
|
|
|
|
|
|
|
|
|
def log_trade(self, nick, time, type, symbol, count, price):
|
|
|
|
|
"""
|
|
|
|
|
Append a record of a trade to the database log
|
|
|
|
|
"""
|
|
|
|
|
with closing(self.sql.getCursor()) as c:
|
|
|
|
|
c.execute("INSERT INTO stockplay_trades VALUES (?, ?, ?, ?, ?, ?)",
|
|
|
|
|
(nick, time, type, symbol, count, price, ))
|
|
|
|
|
|
|
|
|
|
def get_latest_hist_bal(self, nick):
|
|
|
|
|
"""
|
|
|
|
|
Return the most recent historical balance of a player. Aka their "opening" value.
|
|
|
|
|
"""
|
|
|
|
|
with closing(self.sql.getCursor()) as c:
|
|
|
|
|
return c.execute("SELECT * FROM stockplay_balance_history WHERE nick=? ORDER BY DAY DESC LIMIT 1",
|
|
|
|
|
(nick, )).fetchone()
|
|
|
|
|
|
|
|
|
|
def record_nightly_balances(self):
|
|
|
|
|
"""
|
|
|
|
|
Create a record for each user's balance at the start of each day.
|
|
|
|
|
"""
|
|
|
|
|
now = (datetime.now() + timedelta(seconds=self.config.get("midnight_offset", 0))).strftime("%Y-%m-%d")
|
|
|
|
|
with closing(self.sql.getCursor()) as c:
|
|
|
|
|
for row in c.execute("""SELECT * FROM stockplay_balances WHERE nick NOT IN
|
|
|
|
|
(SELECT nick FROM stockplay_balance_history WHERE day=?)""", (now, )).fetchall():
|
|
|
|
|
data = self.build_report(row["nick"])
|
|
|
|
|
total = int((data["cash"] + data["holding_value"]) * 100)
|
|
|
|
|
self.log.info("Recording {} daily balance for {}".format(now, row["nick"]))
|
|
|
|
|
c.execute("INSERT INTO stockplay_balance_history VALUES (?, ?, ?)", (row["nick"], now, total))
|
|
|
|
|