Merge pull request #1794 from lbryio/async-exchange-rate-manager

refactor exchange rate manager to use asyncio
This commit is contained in:
Jack Robison 2019-01-22 12:05:07 -05:00 committed by GitHub
commit 6fe9c5bf00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 60 additions and 73 deletions

View file

@ -22,7 +22,7 @@ from lbrynet.blob.client.EncryptedFileDownloader import EncryptedFileSaverFactor
from lbrynet.blob.client.EncryptedFileOptions import add_lbry_file_to_sd_identifier from lbrynet.blob.client.EncryptedFileOptions import add_lbry_file_to_sd_identifier
from lbrynet.dht.node import Node from lbrynet.dht.node import Node
from lbrynet.extras.daemon.Component import Component from lbrynet.extras.daemon.Component import Component
from lbrynet.extras.daemon.ExchangeRateManager import ExchangeRateManager from lbrynet.extras.daemon.exchange_rate_manager import ExchangeRateManager
from lbrynet.extras.daemon.storage import SQLiteStorage from lbrynet.extras.daemon.storage import SQLiteStorage
from lbrynet.extras.daemon.HashAnnouncer import DHTHashAnnouncer from lbrynet.extras.daemon.HashAnnouncer import DHTHashAnnouncer
from lbrynet.extras.reflector.server.server import ReflectorServerFactory from lbrynet.extras.reflector.server.server import ReflectorServerFactory

View file

@ -1,11 +1,9 @@
import asyncio
import aiohttp
import time import time
import logging import logging
import json import json
import treq
from twisted.internet import defer
from twisted.internet.task import LoopingCall
from lbrynet.p2p.Error import InvalidExchangeRateResponse, CurrencyConversionError from lbrynet.p2p.Error import InvalidExchangeRateResponse, CurrencyConversionError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -38,14 +36,14 @@ class MarketFeed:
REQUESTS_TIMEOUT = 20 REQUESTS_TIMEOUT = 20
EXCHANGE_RATE_UPDATE_RATE_SEC = 300 EXCHANGE_RATE_UPDATE_RATE_SEC = 300
def __init__(self, market, name, url, params, fee): def __init__(self, market: str, name: str, url: str, params, fee):
self.market = market self.market = market
self.name = name self.name = name
self.url = url self.url = url
self.params = params self.params = params
self.fee = fee self.fee = fee
self.rate = None self.rate = None
self._updater = LoopingCall(self._update_price) self._task: asyncio.Task = None
self._online = True self._online = True
def rate_is_initialized(self): def rate_is_initialized(self):
@ -54,17 +52,16 @@ class MarketFeed:
def is_online(self): def is_online(self):
return self._online return self._online
@defer.inlineCallbacks async def _make_request(self):
def _make_request(self): async with aiohttp.request('get', self.url, params=self.params) as response:
response = yield treq.get(self.url, params=self.params, timeout=self.REQUESTS_TIMEOUT) return (await response.read()).decode()
defer.returnValue((yield response.content()))
def _handle_response(self, response): def _handle_response(self, response):
return NotImplementedError raise NotImplementedError()
def _subtract_fee(self, from_amount): def _subtract_fee(self, from_amount):
# increase amount to account for market fees # increase amount to account for market fees
return defer.succeed(from_amount / (1.0 - self.fee)) return from_amount / (1.0 - self.fee)
def _save_price(self, price): def _save_price(self, price):
log.debug("Saving price update %f for %s from %s" % (price, self.market, self.name)) log.debug("Saving price update %f for %s from %s" % (price, self.market, self.name))
@ -77,21 +74,23 @@ class MarketFeed:
log.debug("Exchange rate error (%s from %s): %s", self.market, self.name, err) log.debug("Exchange rate error (%s from %s): %s", self.market, self.name, err)
self._online = False self._online = False
def _update_price(self): async def _update_price(self):
d = self._make_request() while True:
d.addCallback(self._handle_response) try:
d.addCallback(self._subtract_fee) response = await asyncio.wait_for(self._make_request(), self.REQUESTS_TIMEOUT)
d.addCallback(self._save_price) self._save_price(self._subtract_fee(self._handle_response(response)))
d.addErrback(self._on_error) except (asyncio.TimeoutError, InvalidExchangeRateResponse) as err:
return d self._on_error(err)
await asyncio.sleep(self.EXCHANGE_RATE_UPDATE_RATE_SEC)
def start(self): def start(self):
if not self._updater.running: if not self._task:
self._updater.start(self.EXCHANGE_RATE_UPDATE_RATE_SEC) self._task = asyncio.create_task(self._update_price())
def stop(self): def stop(self):
if self._updater.running: if self._task and not self._task.done():
self._updater.stop() self._task.cancel()
self._task = None
class BittrexFeed(MarketFeed): class BittrexFeed(MarketFeed):
@ -116,7 +115,7 @@ class BittrexFeed(MarketFeed):
if totals <= 0 or qtys <= 0: if totals <= 0 or qtys <= 0:
raise InvalidExchangeRateResponse(self.market, 'quantities were not positive') raise InvalidExchangeRateResponse(self.market, 'quantities were not positive')
vwap = totals / qtys vwap = totals / qtys
return defer.succeed(float(1.0 / vwap)) return float(1.0 / vwap)
class LBRYioFeed(MarketFeed): class LBRYioFeed(MarketFeed):
@ -133,7 +132,7 @@ class LBRYioFeed(MarketFeed):
json_response = json.loads(response) json_response = json.loads(response)
if 'data' not in json_response: if 'data' not in json_response:
raise InvalidExchangeRateResponse(self.name, 'result not found') raise InvalidExchangeRateResponse(self.name, 'result not found')
return defer.succeed(1.0 / json_response['data']['lbc_btc']) return 1.0 / json_response['data']['lbc_btc']
class LBRYioBTCFeed(MarketFeed): class LBRYioBTCFeed(MarketFeed):
@ -153,7 +152,7 @@ class LBRYioBTCFeed(MarketFeed):
raise InvalidExchangeRateResponse(self.name, "invalid rate response : %s" % response) raise InvalidExchangeRateResponse(self.name, "invalid rate response : %s" % response)
if 'data' not in json_response: if 'data' not in json_response:
raise InvalidExchangeRateResponse(self.name, 'result not found') raise InvalidExchangeRateResponse(self.name, 'result not found')
return defer.succeed(1.0 / json_response['data']['btc_usd']) return 1.0 / json_response['data']['btc_usd']
class CryptonatorBTCFeed(MarketFeed): class CryptonatorBTCFeed(MarketFeed):
@ -174,7 +173,7 @@ class CryptonatorBTCFeed(MarketFeed):
if 'ticker' not in json_response or len(json_response['ticker']) == 0 or \ if 'ticker' not in json_response or len(json_response['ticker']) == 0 or \
'success' not in json_response or json_response['success'] is not True: 'success' not in json_response or json_response['success'] is not True:
raise InvalidExchangeRateResponse(self.name, 'result not found') raise InvalidExchangeRateResponse(self.name, 'result not found')
return defer.succeed(float(json_response['ticker']['price'])) return float(json_response['ticker']['price'])
class CryptonatorFeed(MarketFeed): class CryptonatorFeed(MarketFeed):
@ -195,7 +194,7 @@ class CryptonatorFeed(MarketFeed):
if 'ticker' not in json_response or len(json_response['ticker']) == 0 or \ if 'ticker' not in json_response or len(json_response['ticker']) == 0 or \
'success' not in json_response or json_response['success'] is not True: 'success' not in json_response or json_response['success'] is not True:
raise InvalidExchangeRateResponse(self.name, 'result not found') raise InvalidExchangeRateResponse(self.name, 'result not found')
return defer.succeed(float(json_response['ticker']['price'])) return float(json_response['ticker']['price'])
class ExchangeRateManager: class ExchangeRateManager:

View file

@ -14,7 +14,7 @@ from lbrynet.p2p.Error import RequestCanceledError
from lbrynet.p2p import BlobAvailability from lbrynet.p2p import BlobAvailability
from lbrynet.blob.EncryptedFileManager import EncryptedFileManager from lbrynet.blob.EncryptedFileManager import EncryptedFileManager
from lbrynet.dht.node import Node as RealNode from lbrynet.dht.node import Node as RealNode
from lbrynet.extras.daemon import ExchangeRateManager as ERM from lbrynet.extras.daemon import exchange_rate_manager as ERM
KB = 2**10 KB = 2**10
PUBLIC_EXPONENT = 65537 # http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html PUBLIC_EXPONENT = 65537 # http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html

View file

@ -11,7 +11,7 @@ from lbrynet.p2p.BlobManager import DiskBlobManager
from lbrynet.p2p.RateLimiter import DummyRateLimiter from lbrynet.p2p.RateLimiter import DummyRateLimiter
from lbrynet.p2p.client.DownloadManager import DownloadManager from lbrynet.p2p.client.DownloadManager import DownloadManager
from lbrynet.extras.daemon import Downloader from lbrynet.extras.daemon import Downloader
from lbrynet.extras.daemon import ExchangeRateManager from lbrynet.extras.daemon.exchange_rate_manager import ExchangeRateManager
from lbrynet.extras.daemon.storage import SQLiteStorage from lbrynet.extras.daemon.storage import SQLiteStorage
from lbrynet.extras.daemon.PeerFinder import DummyPeerFinder from lbrynet.extras.daemon.PeerFinder import DummyPeerFinder
from lbrynet.blob.EncryptedFileStatusReport import EncryptedFileStatusReport from lbrynet.blob.EncryptedFileStatusReport import EncryptedFileStatusReport

View file

@ -1,8 +1,7 @@
import unittest
from lbrynet.schema.fee import Fee from lbrynet.schema.fee import Fee
from lbrynet.extras.daemon import ExchangeRateManager from lbrynet.extras.daemon import exchange_rate_manager
from lbrynet.p2p.Error import InvalidExchangeRateResponse from lbrynet.p2p.Error import InvalidExchangeRateResponse
from twisted.trial import unittest
from twisted.internet import defer
from tests import test_utils from tests import test_utils
from tests.mocks import ExchangeRateManager as DummyExchangeRateManager from tests.mocks import ExchangeRateManager as DummyExchangeRateManager
from tests.mocks import BTCLBCFeed, USDBTCFeed from tests.mocks import BTCLBCFeed, USDBTCFeed
@ -36,9 +35,9 @@ class ExchangeRateTest(unittest.TestCase):
def test_invalid_rates(self): def test_invalid_rates(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
ExchangeRateManager.ExchangeRate('USDBTC', 0, test_utils.DEFAULT_ISO_TIME) exchange_rate_manager.ExchangeRate('USDBTC', 0, test_utils.DEFAULT_ISO_TIME)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
ExchangeRateManager.ExchangeRate('USDBTC', -1, test_utils.DEFAULT_ISO_TIME) exchange_rate_manager.ExchangeRate('USDBTC', -1, test_utils.DEFAULT_ISO_TIME)
class FeeTest(unittest.TestCase): class FeeTest(unittest.TestCase):
@ -65,7 +64,7 @@ class FeeTest(unittest.TestCase):
def test_missing_feed(self): def test_missing_feed(self):
# test when a feed is missing for conversion # test when a feed is missing for conversion
fee = Fee({ fee = Fee({
'currency':'USD', 'currency': 'USD',
'amount': 1.0, 'amount': 1.0,
'address': "bRcHraa8bYJZL7vkh5sNmGwPDERFUjGPP9" 'address': "bRcHraa8bYJZL7vkh5sNmGwPDERFUjGPP9"
}) })
@ -80,107 +79,96 @@ class FeeTest(unittest.TestCase):
class LBRYioFeedTest(unittest.TestCase): class LBRYioFeedTest(unittest.TestCase):
@defer.inlineCallbacks
def test_handle_response(self): def test_handle_response(self):
feed = ExchangeRateManager.LBRYioFeed() feed = exchange_rate_manager.LBRYioFeed()
response = '{\"data\": {\"fresh\": 0, \"lbc_usd\": 0.05863062523378918, ' \ response = '{\"data\": {\"fresh\": 0, \"lbc_usd\": 0.05863062523378918, ' \
'\"lbc_btc\": 5.065289549855739e-05, \"btc_usd\": 1157.498}, ' \ '\"lbc_btc\": 5.065289549855739e-05, \"btc_usd\": 1157.498}, ' \
'\"success\": true, \"error\": null}' '\"success\": true, \"error\": null}'
out = yield feed._handle_response(response) out = feed._handle_response(response)
expected = 1.0 / 5.065289549855739e-05 expected = 1.0 / 5.065289549855739e-05
self.assertEqual(expected, out) self.assertEqual(expected, out)
response = '{}' response = '{}'
with self.assertRaises(InvalidExchangeRateResponse): with self.assertRaises(InvalidExchangeRateResponse):
out = yield feed._handle_response(response) out = feed._handle_response(response)
response = '{"success":true,"result":[]}' response = '{"success":true,"result":[]}'
with self.assertRaises(InvalidExchangeRateResponse): with self.assertRaises(InvalidExchangeRateResponse):
out = yield feed._handle_response(response) out = feed._handle_response(response)
class LBRYioBTCFeedTest(unittest.TestCase): class TestExchangeRateFeeds(unittest.TestCase):
@defer.inlineCallbacks def test_handle_lbryio_btc_response(self):
def test_handle_response(self): feed = exchange_rate_manager.LBRYioBTCFeed()
feed = ExchangeRateManager.LBRYioBTCFeed()
response = '{\"data\": {\"fresh\": 0, \"lbc_usd\": 0.05863062523378918, ' \ response = '{\"data\": {\"fresh\": 0, \"lbc_usd\": 0.05863062523378918, ' \
'\"lbc_btc\": 5.065289549855739e-05, \"btc_usd\": 1157.498}, ' \ '\"lbc_btc\": 5.065289549855739e-05, \"btc_usd\": 1157.498}, ' \
'\"success\": true, \"error\": null}' '\"success\": true, \"error\": null}'
out = yield feed._handle_response(response) out = feed._handle_response(response)
expected = 1.0 / 1157.498 expected = 1.0 / 1157.498
self.assertEqual(expected, out) self.assertEqual(expected, out)
response = '{}' response = '{}'
with self.assertRaises(InvalidExchangeRateResponse): with self.assertRaises(InvalidExchangeRateResponse):
out = yield feed._handle_response(response) out = feed._handle_response(response)
response = '{"success":true,"result":[]}' response = '{"success":true,"result":[]}'
with self.assertRaises(InvalidExchangeRateResponse): with self.assertRaises(InvalidExchangeRateResponse):
out = yield feed._handle_response(response) out = feed._handle_response(response)
class CryptonatorFeedTest(unittest.TestCase): def test_handle_cryptonator_lbc_response(self):
@defer.inlineCallbacks feed = exchange_rate_manager.CryptonatorFeed()
def test_handle_response(self):
feed = ExchangeRateManager.CryptonatorFeed()
response = '{\"ticker\":{\"base\":\"BTC\",\"target\":\"LBC\",\"price\":\"23657.44026496\"' \ response = '{\"ticker\":{\"base\":\"BTC\",\"target\":\"LBC\",\"price\":\"23657.44026496\"' \
',\"volume\":\"\",\"change\":\"-5.59806916\"},\"timestamp\":1507470422' \ ',\"volume\":\"\",\"change\":\"-5.59806916\"},\"timestamp\":1507470422' \
',\"success\":true,\"error\":\"\"}' ',\"success\":true,\"error\":\"\"}'
out = yield feed._handle_response(response) out = feed._handle_response(response)
expected = 23657.44026496 expected = 23657.44026496
self.assertEqual(expected, out) self.assertEqual(expected, out)
response = '{}' response = '{}'
with self.assertRaises(InvalidExchangeRateResponse): with self.assertRaises(InvalidExchangeRateResponse):
out = yield feed._handle_response(response) out = feed._handle_response(response)
response = '{"success":true,"ticker":{}}' response = '{"success":true,"ticker":{}}'
with self.assertRaises(InvalidExchangeRateResponse): with self.assertRaises(InvalidExchangeRateResponse):
out = yield feed._handle_response(response) out = feed._handle_response(response)
class CryptonatorBTCFeedTest(unittest.TestCase): def test_handle_cryptonator_btc_response(self):
@defer.inlineCallbacks feed = exchange_rate_manager.CryptonatorBTCFeed()
def test_handle_response(self):
feed = ExchangeRateManager.CryptonatorBTCFeed()
response = '{\"ticker\":{\"base\":\"USD\",\"target\":\"BTC\",\"price\":\"0.00022123\",' \ response = '{\"ticker\":{\"base\":\"USD\",\"target\":\"BTC\",\"price\":\"0.00022123\",' \
'\"volume\":\"\",\"change\":\"-0.00000259\"},\"timestamp\":1507471141,' \ '\"volume\":\"\",\"change\":\"-0.00000259\"},\"timestamp\":1507471141,' \
'\"success\":true,\"error\":\"\"}' '\"success\":true,\"error\":\"\"}'
out = yield feed._handle_response(response) out = feed._handle_response(response)
expected = 0.00022123 expected = 0.00022123
self.assertEqual(expected, out) self.assertEqual(expected, out)
response = '{}' response = '{}'
with self.assertRaises(InvalidExchangeRateResponse): with self.assertRaises(InvalidExchangeRateResponse):
out = yield feed._handle_response(response) out = feed._handle_response(response)
response = '{"success":true,"ticker":{}}' response = '{"success":true,"ticker":{}}'
with self.assertRaises(InvalidExchangeRateResponse): with self.assertRaises(InvalidExchangeRateResponse):
out = yield feed._handle_response(response) out = feed._handle_response(response)
def test_handle_bittrex_response(self):
class BittrexFeedTest(unittest.TestCase): feed = exchange_rate_manager.BittrexFeed()
@defer.inlineCallbacks
def test_handle_response(self):
feed = ExchangeRateManager.BittrexFeed()
response = '{"success":true,"message":"","result":[{"Id":6902471,"TimeStamp":"2017-02-2'\ response = '{"success":true,"message":"","result":[{"Id":6902471,"TimeStamp":"2017-02-2'\
'7T23:41:52.213","Quantity":56.12611239,"Price":0.00001621,"Total":0.00090980,"FillType":"'\ '7T23: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","Qu'\ 'PARTIAL_FILL","OrderType":"SELL"},{"Id":6902403,"TimeStamp":"2017-02-27T23:31:40.463","Qu'\
'antity":430.99988180,"Price":0.00001592,"Total":0.00686151,"FillType":"PARTIAL_FILL","Ord'\ 'antity":430.99988180,"Price":0.00001592,"Total":0.00686151,"FillType":"PARTIAL_FILL","Ord'\
'erType":"SELL"}]}' 'erType":"SELL"}]}'
out = yield feed._handle_response(response) out = feed._handle_response(response)
expected = 1.0 / ((0.00090980+0.00686151) / (56.12611239+430.99988180)) expected = 1.0 / ((0.00090980+0.00686151) / (56.12611239+430.99988180))
self.assertEqual(expected, out) self.assertEqual(expected, out)
response = '{}' response = '{}'
with self.assertRaises(InvalidExchangeRateResponse): with self.assertRaises(InvalidExchangeRateResponse):
out = yield feed._handle_response(response) out = feed._handle_response(response)
response = '{"success":true,"result":[]}' response = '{"success":true,"result":[]}'
with self.assertRaises(InvalidExchangeRateResponse): with self.assertRaises(InvalidExchangeRateResponse):
out = yield feed._handle_response(response) out = feed._handle_response(response)