forked from LBRYCommunity/lbry-sdk
Merge pull request #501 from lbryio/fix_exchange_rate_manager
Improvements to exchange rate manager
This commit is contained in:
commit
9350b9e127
7 changed files with 142 additions and 69 deletions
|
@ -19,7 +19,7 @@ at anytime.
|
||||||
*
|
*
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
*
|
* Fixed ExchangeRateManager freezing the app
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,13 @@ class KeyFeeAboveMaxAllowed(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidExchangeRateResponse(Exception):
|
||||||
|
def __init__(self, source, reason):
|
||||||
|
Exception.__init__(self, 'Failed to get exchange rate from {}:{}'.format(source, reason))
|
||||||
|
self.source = source
|
||||||
|
self.reason = reason
|
||||||
|
|
||||||
|
|
||||||
class UnknownNameError(Exception):
|
class UnknownNameError(Exception):
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
Exception.__init__(self, 'Name {} is unknown'.format(name))
|
Exception.__init__(self, 'Name {} is unknown'.format(name))
|
||||||
|
|
|
@ -2,13 +2,12 @@ import time
|
||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import googlefinance
|
from twisted.internet import defer, threads, reactor
|
||||||
from twisted.internet import defer, reactor
|
|
||||||
from twisted.internet.task import LoopingCall
|
from twisted.internet.task import LoopingCall
|
||||||
|
|
||||||
from lbrynet import conf
|
from lbrynet import conf
|
||||||
from lbrynet.metadata.Fee import FeeValidator
|
from lbrynet.metadata.Fee import FeeValidator
|
||||||
|
from lbrynet.core.Error import InvalidExchangeRateResponse
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -24,11 +23,18 @@ class ExchangeRate(object):
|
||||||
self.spot = spot
|
self.spot = spot
|
||||||
self.ts = ts
|
self.ts = ts
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
out = "Currency pair:{}, spot:{}, ts:{}".format(
|
||||||
|
self.currency_pair, self.spot, self.ts)
|
||||||
|
return out
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
return {'spot': self.spot, 'ts': self.ts}
|
return {'spot': self.spot, 'ts': self.ts}
|
||||||
|
|
||||||
|
|
||||||
class MarketFeed(object):
|
class MarketFeed(object):
|
||||||
|
REQUESTS_TIMEOUT = 20
|
||||||
|
EXCHANGE_RATE_UPDATE_RATE_SEC = 300
|
||||||
def __init__(self, market, name, url, params, fee):
|
def __init__(self, market, name, url, params, fee):
|
||||||
self.market = market
|
self.market = market
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -38,13 +44,13 @@ class MarketFeed(object):
|
||||||
self.rate = None
|
self.rate = None
|
||||||
self._updater = LoopingCall(self._update_price)
|
self._updater = LoopingCall(self._update_price)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rate_is_initialized(self):
|
||||||
|
return self.rate is not None
|
||||||
|
|
||||||
def _make_request(self):
|
def _make_request(self):
|
||||||
try:
|
r = requests.get(self.url, self.params, timeout=self.REQUESTS_TIMEOUT)
|
||||||
r = requests.get(self.url, self.params)
|
return r.text
|
||||||
return defer.succeed(r.text)
|
|
||||||
except Exception as err:
|
|
||||||
log.error(err)
|
|
||||||
return defer.fail(err)
|
|
||||||
|
|
||||||
def _handle_response(self, response):
|
def _handle_response(self, response):
|
||||||
return NotImplementedError
|
return NotImplementedError
|
||||||
|
@ -64,15 +70,16 @@ class MarketFeed(object):
|
||||||
self.market, self.name)
|
self.market, self.name)
|
||||||
|
|
||||||
def _update_price(self):
|
def _update_price(self):
|
||||||
d = self._make_request()
|
d = threads.deferToThread(self._make_request)
|
||||||
d.addCallback(self._handle_response)
|
d.addCallback(self._handle_response)
|
||||||
d.addCallback(self._subtract_fee)
|
d.addCallback(self._subtract_fee)
|
||||||
d.addCallback(self._save_price)
|
d.addCallback(self._save_price)
|
||||||
d.addErrback(self._log_error)
|
d.addErrback(self._log_error)
|
||||||
|
return d
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if not self._updater.running:
|
if not self._updater.running:
|
||||||
self._updater.start(300)
|
self._updater.start(self.EXCHANGE_RATE_UPDATE_RATE_SEC)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
if self._updater.running:
|
if self._updater.running:
|
||||||
|
@ -91,8 +98,17 @@ class BittrexFeed(MarketFeed):
|
||||||
)
|
)
|
||||||
|
|
||||||
def _handle_response(self, response):
|
def _handle_response(self, response):
|
||||||
trades = json.loads(response)['result']
|
json_response = json.loads(response)
|
||||||
vwap = sum([i['Total'] for i in trades]) / sum([i['Quantity'] for i in trades])
|
if 'result' not in json_response:
|
||||||
|
raise InvalidExchangeRateResponse(self.name, 'result not found')
|
||||||
|
trades = json_response['result']
|
||||||
|
if len(trades) == 0:
|
||||||
|
raise InvalidExchangeRateResponse(self.market, 'trades not found')
|
||||||
|
totals = sum([i['Total'] for i in trades])
|
||||||
|
qtys = sum([i['Quantity'] for i in trades])
|
||||||
|
if totals <= 0 or qtys <= 0:
|
||||||
|
raise InvalidExchangeRateResponse(self.market, 'quantities were not positive')
|
||||||
|
vwap = totals/qtys
|
||||||
return defer.succeed(float(1.0 / vwap))
|
return defer.succeed(float(1.0 / vwap))
|
||||||
|
|
||||||
|
|
||||||
|
@ -102,20 +118,20 @@ class GoogleBTCFeed(MarketFeed):
|
||||||
self,
|
self,
|
||||||
"USDBTC",
|
"USDBTC",
|
||||||
"Coinbase via Google finance",
|
"Coinbase via Google finance",
|
||||||
None,
|
'http://finance.google.com/finance/info',
|
||||||
None,
|
{'client':'ig', 'q':'CURRENCY:USDBTC'},
|
||||||
COINBASE_FEE
|
COINBASE_FEE
|
||||||
)
|
)
|
||||||
|
|
||||||
def _make_request(self):
|
|
||||||
try:
|
|
||||||
r = googlefinance.getQuotes('CURRENCY:USDBTC')[0]
|
|
||||||
return defer.succeed(r)
|
|
||||||
except Exception as err:
|
|
||||||
return defer.fail(err)
|
|
||||||
|
|
||||||
def _handle_response(self, response):
|
def _handle_response(self, response):
|
||||||
return float(response['LastTradePrice'])
|
response = response[3:] # response starts with "// "
|
||||||
|
json_response = json.loads(response)[0]
|
||||||
|
if 'l' not in json_response:
|
||||||
|
raise InvalidExchangeRateResponse(self.name, 'last trade not found')
|
||||||
|
last_trade_price = float(json_response['l'])
|
||||||
|
if last_trade_price <= 0:
|
||||||
|
raise InvalidExchangeRateResponse(self.name, 'trade price was not positive')
|
||||||
|
return defer.succeed(last_trade_price)
|
||||||
|
|
||||||
|
|
||||||
def get_default_market_feed(currency_pair):
|
def get_default_market_feed(currency_pair):
|
||||||
|
@ -149,14 +165,17 @@ class ExchangeRateManager(object):
|
||||||
source.stop()
|
source.stop()
|
||||||
|
|
||||||
def convert_currency(self, from_currency, to_currency, amount):
|
def convert_currency(self, from_currency, to_currency, amount):
|
||||||
log.info("Converting %f %s to %s" % (amount, from_currency, to_currency))
|
rates = [market.rate for market in self.market_feeds]
|
||||||
|
log.info("Converting %f %s to %s, rates: %s" % (amount, from_currency, to_currency, rates))
|
||||||
if from_currency == to_currency:
|
if from_currency == to_currency:
|
||||||
return amount
|
return amount
|
||||||
for market in self.market_feeds:
|
for market in self.market_feeds:
|
||||||
if market.rate.currency_pair == (from_currency, to_currency):
|
if (market.rate_is_initialized and
|
||||||
|
market.rate.currency_pair == (from_currency, to_currency)):
|
||||||
return amount * market.rate.spot
|
return amount * market.rate.spot
|
||||||
for market in self.market_feeds:
|
for market in self.market_feeds:
|
||||||
if market.rate.currency_pair[0] == from_currency:
|
if (market.rate_is_initialized and
|
||||||
|
market.rate.currency_pair[0] == from_currency):
|
||||||
return self.convert_currency(
|
return self.convert_currency(
|
||||||
market.rate.currency_pair[1], to_currency, amount * market.rate.spot)
|
market.rate.currency_pair[1], to_currency, amount * market.rate.spot)
|
||||||
raise Exception(
|
raise Exception(
|
||||||
|
@ -215,10 +234,12 @@ class DummyExchangeRateManager(object):
|
||||||
def convert_currency(self, from_currency, to_currency, amount):
|
def convert_currency(self, from_currency, to_currency, amount):
|
||||||
log.debug("Converting %f %s to %s" % (amount, from_currency, to_currency))
|
log.debug("Converting %f %s to %s" % (amount, from_currency, to_currency))
|
||||||
for market in self.market_feeds:
|
for market in self.market_feeds:
|
||||||
if market.rate.currency_pair == (from_currency, to_currency):
|
if (market.rate_is_initialized and
|
||||||
|
market.rate.currency_pair == (from_currency, to_currency)):
|
||||||
return amount * market.rate.spot
|
return amount * market.rate.spot
|
||||||
for market in self.market_feeds:
|
for market in self.market_feeds:
|
||||||
if market.rate.currency_pair[0] == from_currency:
|
if (market.rate_is_initialized and
|
||||||
|
market.rate.currency_pair[0] == from_currency):
|
||||||
return self.convert_currency(
|
return self.convert_currency(
|
||||||
market.rate.currency_pair[1], to_currency, amount * market.rate.spot)
|
market.rate.currency_pair[1], to_currency, amount * market.rate.spot)
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ dnspython==1.12.0
|
||||||
ecdsa==0.13
|
ecdsa==0.13
|
||||||
envparse==0.2.0
|
envparse==0.2.0
|
||||||
gmpy==1.17
|
gmpy==1.17
|
||||||
googlefinance==0.7
|
|
||||||
jsonrpc==1.2
|
jsonrpc==1.2
|
||||||
jsonrpclib==0.1.7
|
jsonrpclib==0.1.7
|
||||||
jsonschema==2.5.1
|
jsonschema==2.5.1
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -30,7 +30,6 @@ requires = [
|
||||||
'appdirs',
|
'appdirs',
|
||||||
'base58',
|
'base58',
|
||||||
'envparse',
|
'envparse',
|
||||||
'googlefinance',
|
|
||||||
'jsonrpc',
|
'jsonrpc',
|
||||||
'jsonschema',
|
'jsonschema',
|
||||||
'lbryum>=2.7.6',
|
'lbryum>=2.7.6',
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
from lbrynet.metadata import Fee
|
|
||||||
from lbrynet.lbrynet_daemon import ExchangeRateManager
|
|
||||||
|
|
||||||
from twisted.trial import unittest
|
|
||||||
|
|
||||||
from tests import util
|
|
||||||
|
|
||||||
|
|
||||||
class FeeFormatTest(unittest.TestCase):
|
|
||||||
def test_fee_created_with_correct_inputs(self):
|
|
||||||
fee_dict = {
|
|
||||||
'USD': {
|
|
||||||
'amount': 10.0,
|
|
||||||
'address': "bRcHraa8bYJZL7vkh5sNmGwPDERFUjGPP9"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fee = Fee.FeeValidator(fee_dict)
|
|
||||||
self.assertEqual(10.0, fee['USD']['amount'])
|
|
||||||
|
|
||||||
|
|
||||||
class FeeTest(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
util.resetTime(self)
|
|
||||||
|
|
||||||
def test_fee_converts_to_lbc(self):
|
|
||||||
fee_dict = {
|
|
||||||
'USD': {
|
|
||||||
'amount': 10.0,
|
|
||||||
'address': "bRcHraa8bYJZL7vkh5sNmGwPDERFUjGPP9"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rates = {
|
|
||||||
'BTCLBC': {'spot': 3.0, 'ts': util.DEFAULT_ISO_TIME + 1},
|
|
||||||
'USDBTC': {'spot': 2.0, 'ts': util.DEFAULT_ISO_TIME + 2}
|
|
||||||
}
|
|
||||||
manager = ExchangeRateManager.DummyExchangeRateManager(rates)
|
|
||||||
result = manager.to_lbc(fee_dict).amount
|
|
||||||
self.assertEqual(60.0, result)
|
|
85
tests/unit/lbrynet_daemon/test_ExchangeRateManager.py
Normal file
85
tests/unit/lbrynet_daemon/test_ExchangeRateManager.py
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
from lbrynet.metadata import Fee
|
||||||
|
from lbrynet.lbrynet_daemon import ExchangeRateManager
|
||||||
|
from lbrynet import conf
|
||||||
|
from lbrynet.core.Error import InvalidExchangeRateResponse
|
||||||
|
|
||||||
|
from twisted.trial import unittest
|
||||||
|
from twisted.internet import defer
|
||||||
|
from tests import util
|
||||||
|
|
||||||
|
|
||||||
|
class FeeFormatTest(unittest.TestCase):
|
||||||
|
def test_fee_created_with_correct_inputs(self):
|
||||||
|
fee_dict = {
|
||||||
|
'USD': {
|
||||||
|
'amount': 10.0,
|
||||||
|
'address': "bRcHraa8bYJZL7vkh5sNmGwPDERFUjGPP9"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fee = Fee.FeeValidator(fee_dict)
|
||||||
|
self.assertEqual(10.0, fee['USD']['amount'])
|
||||||
|
|
||||||
|
|
||||||
|
class FeeTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
util.resetTime(self)
|
||||||
|
|
||||||
|
def test_fee_converts_to_lbc(self):
|
||||||
|
fee_dict = {
|
||||||
|
'USD': {
|
||||||
|
'amount': 10.0,
|
||||||
|
'address': "bRcHraa8bYJZL7vkh5sNmGwPDERFUjGPP9"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rates = {
|
||||||
|
'BTCLBC': {'spot': 3.0, 'ts': util.DEFAULT_ISO_TIME + 1},
|
||||||
|
'USDBTC': {'spot': 2.0, 'ts': util.DEFAULT_ISO_TIME + 2}
|
||||||
|
}
|
||||||
|
manager = ExchangeRateManager.DummyExchangeRateManager(rates)
|
||||||
|
result = manager.to_lbc(fee_dict).amount
|
||||||
|
self.assertEqual(60.0, result)
|
||||||
|
|
||||||
|
class GoogleBTCFeedTest(unittest.TestCase):
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_handle_response(self):
|
||||||
|
feed = ExchangeRateManager.GoogleBTCFeed()
|
||||||
|
|
||||||
|
response = '// [ { "id": "-2001" ,"t" : "USDBTC" ,"e" : "CURRENCY" ,"l" : "0.0008" ,"l_fix" : "" ,"l_cur" : "" ,"s": "0" ,"ltt":"" ,"lt" : "Feb 27, 10:21PM GMT" ,"lt_dts" : "2017-02-27T22:21:39Z" ,"c" : "-0.00001" ,"c_fix" : "" ,"cp" : "-0.917" ,"cp_fix" : "" ,"ccol" : "chr" ,"pcls_fix" : "" } ]'
|
||||||
|
out = yield feed._handle_response(response)
|
||||||
|
self.assertEqual(0.0008, out)
|
||||||
|
|
||||||
|
# check negative trade price throws exception
|
||||||
|
response = '// [ { "id": "-2001" ,"t" : "USDBTC" ,"e" : "CURRENCY" ,"l" : "-0.0008" ,"l_fix" : "" ,"l_cur" : "" ,"s": "0" ,"ltt":"" ,"lt" : "Feb 27, 10:21PM GMT" ,"lt_dts" : "2017-02-27T22:21:39Z" ,"c" : "-0.00001" ,"c_fix" : "" ,"cp" : "-0.917" ,"cp_fix" : "" ,"ccol" : "chr" ,"pcls_fix" : "" } ]'
|
||||||
|
with self.assertRaises(InvalidExchangeRateResponse):
|
||||||
|
out = yield feed._handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class BittrexFeedTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
conf.initialize_settings()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
conf.settings = None
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_handle_response(self):
|
||||||
|
feed = ExchangeRateManager.BittrexFeed()
|
||||||
|
|
||||||
|
response ='{"success":true,"message":"","result":[{"Id":6902471,"TimeStamp":"2017-02-27T23:41:52.213","Quantity":56.12611239,"Price":0.00001621,"Total":0.00090980,"FillType":"PARTIAL_FILL","OrderType":"SELL"},{"Id":6902403,"TimeStamp":"2017-02-27T23:31:40.463","Quantity":430.99988180,"Price":0.00001592,"Total":0.00686151,"FillType":"PARTIAL_FILL","OrderType":"SELL"}]}'
|
||||||
|
out = yield feed._handle_response(response)
|
||||||
|
expected= 1.0 / ((0.00090980+0.00686151) / (56.12611239+430.99988180))
|
||||||
|
self.assertEqual(expected, out)
|
||||||
|
|
||||||
|
response='{}'
|
||||||
|
with self.assertRaises(InvalidExchangeRateResponse):
|
||||||
|
out = yield feed._handle_response(response)
|
||||||
|
|
||||||
|
response='{"success":true,"result":[]}'
|
||||||
|
with self.assertRaises(InvalidExchangeRateResponse):
|
||||||
|
out = yield feed._handle_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue