Skip to content

Commit bf54c3c

Browse files
authored
AccountStats Plugin - daily notification (#349)
* Adding tools dir and a simple script to analyse lending history * Implement AccountStats plugin - WORK IN PROGRESS - Created Plugin system, allowing user to define which Plugins are loaded from config file - Started implementation of AccountStats (fetching history) * Fetch all lending history from Poloniex Split Plugin events to before and after lending * Fix fetch all lending history from Poloniex Added notify event (every cycle at the moment) * Notify every 24Hrs * Updated documentation * Removed tools * Strip plugin names from config
1 parent da4afd6 commit bf54c3c

File tree

8 files changed

+263
-6
lines changed

8 files changed

+263
-6
lines changed

docs/configuration.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,26 @@ Advanced logging and Web Display
295295
- Acceptable values: BTC, USDT, Any coin with a direct Poloniex BTC trading pair (ex. DOGE, MAID, ETH), Currencies that have a BTC exchange rate on blockchain.info (i.e. EUR, USD)
296296
- Will be a close estimate, due to unexpected market fluctuations, trade fees, and other unforseeable factors.
297297

298+
Plugins
299+
-------
300+
301+
Plugins allow extending Bot functionality with extra features.
302+
To enable/disable a plugin add/remove it to the ``plugins`` list config option, example::
303+
304+
plugins = Plugin1, Plugin2, etc...
305+
306+
AccountStats Plugin
307+
~~~~~~~~~~~~~~~~~~~
308+
309+
The AccountStats plugin fetches all your loan history and provides statistics based on it.
310+
Current implementation sends a earnings summary Notification (see Notifications sections) every 24hr.
311+
312+
To enable the plugin add ``AccountStats`` to the ``plugins`` config options, example::
313+
314+
plugins = AccountStats
315+
316+
Be aware that first initialization might take longer as the bot will fetch all the history.
317+
298318
lendingbot.html options
299319
-----------------------
300320

lendingbot.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@
44
import sys
55
import time
66
import traceback
7+
from decimal import Decimal
78
from httplib import BadStatusLine
89
from urllib2 import URLError
910

10-
from decimal import Decimal
11-
12-
from modules.Logger import Logger
13-
from modules.Poloniex import Poloniex, PoloniexApiError
1411
import modules.Configuration as Config
15-
import modules.MaxToLend as MaxToLend
1612
import modules.Data as Data
1713
import modules.Lending as Lending
14+
import modules.MaxToLend as MaxToLend
15+
from modules.Logger import Logger
16+
from modules.Poloniex import Poloniex, PoloniexApiError
17+
import modules.PluginsManager as PluginsManager
18+
1819

1920
try:
2021
open('lendingbot.py', 'r')
@@ -57,6 +58,8 @@
5758
analysis = None
5859
Lending.init(Config, api, log, Data, MaxToLend, dry_run, analysis, notify_conf)
5960

61+
# load plugins
62+
PluginsManager.init(Config, api, log, notify_conf)
6063

6164
print 'Welcome to Poloniex Lending Bot'
6265
# Configure web server
@@ -70,9 +73,11 @@
7073
while True:
7174
try:
7275
Data.update_conversion_rates(output_currency, json_output_enabled)
76+
PluginsManager.before_lending()
7377
Lending.transfer_balances()
7478
Lending.cancel_all()
7579
Lending.lend_all()
80+
PluginsManager.after_lending()
7681
log.refreshStatus(Data.stringify_total_lent(*Data.get_total_lent()),
7782
Data.get_max_duration(end_date, "status"))
7883
log.persistStatus()
@@ -116,6 +121,7 @@
116121
except KeyboardInterrupt:
117122
if web_server_enabled:
118123
WebServer.stop_web_server()
124+
PluginsManager.on_bot_exit()
119125
log.log('bye')
120126
print 'bye'
121127
os._exit(0) # Ad-hoc solution in place of 'exit(0)' TODO: Find out why non-daemon thread(s) are hanging on exit

modules/Configuration.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,10 @@ def get_notification_config():
158158
notify_conf[conf] = get('notifications', conf)
159159

160160
return notify_conf
161+
162+
163+
def get_plugins_config():
164+
active_plugins = []
165+
if config.has_option("BOT", "plugins"):
166+
active_plugins = map(str.strip, config.get("BOT", "plugins").split(','))
167+
return active_plugins

modules/PluginsManager.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from plugins import *
2+
import plugins.Plugin as Plugin
3+
4+
config = None
5+
api = None
6+
log = None
7+
notify_conf = None
8+
plugins = []
9+
10+
11+
def init_plugin(plugin_name):
12+
"""
13+
:return: instance of requested class
14+
:rtype: Plugin
15+
"""
16+
klass = globals()[plugin_name] # type: Plugin
17+
instance = klass(config, api, log, notify_conf)
18+
instance.on_bot_init()
19+
return instance
20+
21+
22+
def init(cfg, api1, log1, notify_conf1):
23+
"""
24+
@type cfg1: modules.Configuration
25+
@type api1: modules.Poloniex.Poloniex
26+
@type log1: modules.Logger.Logger
27+
"""
28+
global config, api, log, notify_conf
29+
config = cfg
30+
api = api1
31+
log = log1
32+
notify_conf = notify_conf1
33+
34+
plugin_names = config.get_plugins_config()
35+
for plugin_name in plugin_names:
36+
plugins.append(init_plugin(plugin_name))
37+
38+
39+
def after_lending():
40+
for plugin in plugins:
41+
plugin.after_lending()
42+
43+
44+
def before_lending():
45+
for plugin in plugins:
46+
plugin.before_lending()
47+
48+
49+
def on_bot_exit():
50+
for plugin in plugins:
51+
plugin.on_bot_exit()

modules/Poloniex.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import urllib
88
import urllib2
99
import threading
10+
import calendar
11+
1012
from modules.RingBuffer import RingBuffer
1113

1214

@@ -15,7 +17,7 @@ class PoloniexApiError(Exception):
1517

1618

1719
def create_time_stamp(datestr, formatting="%Y-%m-%d %H:%M:%S"):
18-
return time.mktime(time.strptime(datestr, formatting))
20+
return calendar.timegm(time.strptime(datestr, formatting))
1921

2022

2123
def post_process(before):
@@ -175,6 +177,9 @@ def return_open_loan_offers(self):
175177
def return_active_loans(self):
176178
return self.api_query('returnActiveLoans')
177179

180+
def return_lending_history(self, start, stop, limit=500):
181+
return self.api_query('returnLendingHistory', {'start': start, 'end': stop, 'limit': limit})
182+
178183
# Returns your trade history for a given market, specified by the "currencyPair" POST parameter
179184
# Inputs:
180185
# currencyPair The currency pair e.g. "BTC_XCP"

plugins/AccountStats.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# coding=utf-8
2+
from plugins.Plugin import Plugin
3+
import modules.Poloniex as Poloniex
4+
import sqlite3
5+
6+
BITCOIN_GENESIS_BLOCK_DATE = "2009-01-03 18:15:05"
7+
DAY_IN_SEC = 86400
8+
DB_DROP = "DROP TABLE IF EXISTS history"
9+
DB_CREATE = "CREATE TABLE IF NOT EXISTS history(" \
10+
"id INTEGER UNIQUE, open TIMESTAMP, close TIMESTAMP," \
11+
" duration NUMBER, interest NUMBER, rate NUMBER," \
12+
" currency TEXT, amount NUMBER, earned NUMBER, fee NUMBER )"
13+
DB_INSERT = "INSERT OR REPLACE INTO 'history'" \
14+
"('id','open','close','duration','interest','rate','currency','amount','earned','fee')" \
15+
" VALUES (?,?,?,?,?,?,?,?,?,?);"
16+
DB_GET_LAST_TIMESTAMP = "SELECT max(close) as last_timestamp FROM 'history'"
17+
DB_GET_FIRST_TIMESTAMP = "SELECT min(close) as first_timestamp FROM 'history'"
18+
DB_GET_TOTAL_EARNED = "SELECT sum(earned) as total_earned, currency FROM 'history' GROUP BY currency"
19+
DB_GET_24HR_EARNED = "SELECT sum(earned) as total_earned, currency FROM 'history' " \
20+
"WHERE close BETWEEN datetime('now','-1 day') AND datetime('now') GROUP BY currency"
21+
22+
23+
class AccountStats(Plugin):
24+
last_notification = 0
25+
26+
def on_bot_init(self):
27+
super(AccountStats, self).on_bot_init()
28+
self.init_db()
29+
30+
def after_lending(self):
31+
self.update_history()
32+
self.notify_daily()
33+
34+
# noinspection PyAttributeOutsideInit
35+
def init_db(self):
36+
self.db = sqlite3.connect(r'market_data\loan_history.sqlite3')
37+
self.db.execute(DB_CREATE)
38+
self.db.commit()
39+
40+
def update_history(self):
41+
# timestamps are in UTC
42+
last_time_stamp = self.get_last_timestamp()
43+
44+
if last_time_stamp is None:
45+
# no entries means db is empty and needs initialization
46+
last_time_stamp = BITCOIN_GENESIS_BLOCK_DATE
47+
self.db.execute("PRAGMA user_version = 0")
48+
49+
self.fetch_history(Poloniex.create_time_stamp(last_time_stamp), sqlite3.time.time())
50+
51+
# As Poloniex API return a unspecified number of recent loans, but not all so we need to loop back.
52+
if (self.get_db_version() == 0) and (self.get_first_timestamp() is not None):
53+
last_time_stamp = BITCOIN_GENESIS_BLOCK_DATE
54+
loop = True
55+
while loop:
56+
sqlite3.time.sleep(10) # delay a bit, try not to annoy poloniex
57+
first_time_stamp = self.get_first_timestamp()
58+
count = self.fetch_history(Poloniex.create_time_stamp(last_time_stamp, )
59+
, Poloniex.create_time_stamp(first_time_stamp))
60+
loop = count != 0
61+
# if we reached here without errors means we managed to fetch all the history, db is ready.
62+
self.set_db_version(1)
63+
64+
def set_db_version(self, version):
65+
self.db.execute("PRAGMA user_version = " + str(version))
66+
67+
def get_db_version(self):
68+
return self.db.execute("PRAGMA user_version").fetchone()[0]
69+
70+
def fetch_history(self, first_time_stamp, last_time_stamp):
71+
history = self.api.return_lending_history(first_time_stamp, last_time_stamp - 1, 50000)
72+
loans = []
73+
for loan in reversed(history):
74+
loans.append(
75+
[loan['id'], loan['open'], loan['close'], loan['duration'], loan['interest'],
76+
loan['rate'], loan['currency'], loan['amount'], loan['earned'], loan['fee']])
77+
self.db.executemany(DB_INSERT, loans)
78+
self.db.commit()
79+
count = len(loans)
80+
self.log.log('Downloaded ' + str(count) + ' loans history '
81+
+ sqlite3.datetime.datetime.utcfromtimestamp(first_time_stamp).strftime('%Y-%m-%d %H:%M:%S')
82+
+ ' to ' + sqlite3.datetime.datetime.utcfromtimestamp(last_time_stamp - 1).strftime(
83+
'%Y-%m-%d %H:%M:%S'))
84+
if count > 0:
85+
self.log.log('Last: ' + history[0]['close'] + ' First:' + history[count - 1]['close'])
86+
return count
87+
88+
def get_last_timestamp(self):
89+
cursor = self.db.execute(DB_GET_LAST_TIMESTAMP)
90+
row = cursor.fetchone()
91+
cursor.close()
92+
return row[0]
93+
94+
def get_first_timestamp(self):
95+
cursor = self.db.execute(DB_GET_FIRST_TIMESTAMP)
96+
row = cursor.fetchone()
97+
cursor.close()
98+
return row[0]
99+
100+
def notify_daily(self):
101+
if self.get_db_version() == 0:
102+
self.log.log_error('AccountStats DB isn\'t ready.')
103+
return
104+
105+
if self.last_notification != 0 and self.last_notification + DAY_IN_SEC > sqlite3.time.time():
106+
return
107+
108+
cursor = self.db.execute(DB_GET_24HR_EARNED)
109+
output = ''
110+
for row in cursor:
111+
output += str(row[0]) + ' ' + str(row[1]) + ' in last 24hrs\n'
112+
cursor.close()
113+
114+
cursor = self.db.execute(DB_GET_TOTAL_EARNED)
115+
for row in cursor:
116+
output += str(row[0]) + ' ' + str(row[1]) + ' in total\n'
117+
cursor.close()
118+
if output != '':
119+
self.last_notification = sqlite3.time.time()
120+
output = 'Earnings:\n----------\n' + output
121+
self.log.notify(output, self.notify_config)
122+
self.log.log(output)

plugins/Plugin.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# coding=utf-8
2+
3+
4+
class Plugin(object):
5+
"""
6+
@type cfg1: modules.Configuration
7+
@type api1: modules.Poloniex.Poloniex
8+
@type log1: modules.Logger.Logger
9+
"""
10+
def __init__(self, cfg1, api1, log1, notify_config1):
11+
self.api = api1
12+
self.config = cfg1
13+
self.notify_config = notify_config1
14+
self.log = log1
15+
16+
# override this to run plugin init code
17+
def on_bot_init(self):
18+
self.log.log(self.__class__.__name__ + ' plugin started.')
19+
20+
# override this to run plugin loop code before lending
21+
def before_lending(self):
22+
pass
23+
24+
# override this to run plugin loop code after lending
25+
def after_lending(self):
26+
pass
27+
28+
# override this to run plugin stop code
29+
# since the bot can be killed, there is not guarantee this will be called.
30+
def on_bot_stop(self):
31+
pass

plugins/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# coding=utf-8
2+
__all__ = []
3+
4+
import pkgutil
5+
import inspect
6+
7+
for loader, name, is_pkg in pkgutil.walk_packages(__path__):
8+
module = loader.find_module(name).load_module(name)
9+
10+
for name, value in inspect.getmembers(module):
11+
if name.startswith('__'):
12+
continue
13+
14+
globals()[name] = value
15+
__all__.append(name)

0 commit comments

Comments
 (0)