lbry-sdk/lbrynet/lbrynet_daemon/ExchangeRateManager.py
2017-03-03 12:50:47 -05:00

260 lines
8.3 KiB
Python

import time
import requests
import logging
import json
from twisted.internet import defer, threads, reactor
from twisted.internet.task import LoopingCall
from lbrynet import conf
from lbrynet.metadata.Fee import FeeValidator
from lbrynet.core.Error import InvalidExchangeRateResponse
log = logging.getLogger(__name__)
CURRENCY_PAIRS = ["USDBTC", "BTCLBC"]
BITTREX_FEE = 0.0025
COINBASE_FEE = 0.0 #add fee
class ExchangeRate(object):
def __init__(self, market, spot, ts):
assert int(time.time()) - ts < 600
self.currency_pair = (market[0:3], market[3:6])
self.spot = spot
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):
return {'spot': self.spot, 'ts': self.ts}
class MarketFeed(object):
REQUESTS_TIMEOUT = 20
EXCHANGE_RATE_UPDATE_RATE_SEC = 300
def __init__(self, market, name, url, params, fee):
self.market = market
self.name = name
self.url = url
self.params = params
self.fee = fee
self.rate = None
self._updater = LoopingCall(self._update_price)
@property
def rate_is_initialized(self):
return self.rate is not None
def _make_request(self):
r = requests.get(self.url, self.params, timeout=self.REQUESTS_TIMEOUT)
return r.text
def _handle_response(self, response):
return NotImplementedError
def _subtract_fee(self, from_amount):
# increase amount to account for market fees
return defer.succeed(from_amount / (1.0 - self.fee))
def _save_price(self, price):
log.debug("Saving price update %f for %s" % (price, self.market))
self.rate = ExchangeRate(self.market, price, int(time.time()))
def _log_error(self, err):
log.error(err)
log.warning(
"There was a problem updating %s exchange rate information from %s",
self.market, self.name)
def _update_price(self):
d = threads.deferToThread(self._make_request)
d.addCallback(self._handle_response)
d.addCallback(self._subtract_fee)
d.addCallback(self._save_price)
d.addErrback(self._log_error)
return d
def start(self):
if not self._updater.running:
self._updater.start(self.EXCHANGE_RATE_UPDATE_RATE_SEC)
def stop(self):
if self._updater.running:
self._updater.stop()
class BittrexFeed(MarketFeed):
def __init__(self):
MarketFeed.__init__(
self,
"BTCLBC",
"Bittrex",
conf.settings['bittrex_feed'],
{'market': 'BTC-LBC', 'count': 50},
BITTREX_FEE
)
def _handle_response(self, response):
json_response = json.loads(response)
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))
class GoogleBTCFeed(MarketFeed):
def __init__(self):
MarketFeed.__init__(
self,
"USDBTC",
"Coinbase via Google finance",
'http://finance.google.com/finance/info',
{'client':'ig', 'q':'CURRENCY:USDBTC'},
COINBASE_FEE
)
def _handle_response(self, response):
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):
currencies = None
if isinstance(currency_pair, str):
currencies = (currency_pair[0:3], currency_pair[3:6])
elif isinstance(currency_pair, tuple):
currencies = currency_pair
assert currencies is not None
if currencies == ("USD", "BTC"):
return GoogleBTCFeed()
elif currencies == ("BTC", "LBC"):
return BittrexFeed()
class ExchangeRateManager(object):
def __init__(self):
reactor.addSystemEventTrigger('before', 'shutdown', self.stop)
self.market_feeds = [
get_default_market_feed(currency_pair) for currency_pair in CURRENCY_PAIRS]
def start(self):
log.info("Starting exchange rate manager")
for feed in self.market_feeds:
feed.start()
def stop(self):
log.info("Stopping exchange rate manager")
for source in self.market_feeds:
source.stop()
def convert_currency(self, from_currency, to_currency, amount):
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:
return amount
for market in self.market_feeds:
if (market.rate_is_initialized and
market.rate.currency_pair == (from_currency, to_currency)):
return amount * market.rate.spot
for market in self.market_feeds:
if (market.rate_is_initialized and
market.rate.currency_pair[0] == from_currency):
return self.convert_currency(
market.rate.currency_pair[1], to_currency, amount * market.rate.spot)
raise Exception(
'Unable to convert {} from {} to {}'.format(amount, from_currency, to_currency))
def fee_dict(self):
return {market: market.rate.as_dict() for market in self.market_feeds}
def to_lbc(self, fee):
if fee is None:
return None
if not isinstance(fee, FeeValidator):
fee_in = FeeValidator(fee)
else:
fee_in = fee
return FeeValidator({
fee_in.currency_symbol: {
'amount': self.convert_currency(fee_in.currency_symbol, "LBC", fee_in.amount),
'address': fee_in.address
}
})
class DummyBTCLBCFeed(MarketFeed):
def __init__(self):
MarketFeed.__init__(
self,
"BTCLBC",
"market name",
"derp.com",
None,
0.0
)
class DummyUSDBTCFeed(MarketFeed):
def __init__(self):
MarketFeed.__init__(
self,
"USDBTC",
"market name",
"derp.com",
None,
0.0
)
class DummyExchangeRateManager(object):
def __init__(self, rates):
self.market_feeds = [DummyBTCLBCFeed(), DummyUSDBTCFeed()]
for feed in self.market_feeds:
feed.rate = ExchangeRate(
feed.market, rates[feed.market]['spot'], rates[feed.market]['ts'])
def convert_currency(self, from_currency, to_currency, amount):
log.debug("Converting %f %s to %s" % (amount, from_currency, to_currency))
for market in self.market_feeds:
if (market.rate_is_initialized and
market.rate.currency_pair == (from_currency, to_currency)):
return amount * market.rate.spot
for market in self.market_feeds:
if (market.rate_is_initialized and
market.rate.currency_pair[0] == from_currency):
return self.convert_currency(
market.rate.currency_pair[1], to_currency, amount * market.rate.spot)
def to_lbc(self, fee):
if fee is None:
return None
if not isinstance(fee, FeeValidator):
fee_in = FeeValidator(fee)
else:
fee_in = fee
return FeeValidator({
fee_in.currency_symbol: {
'amount': self.convert_currency(fee_in.currency_symbol, "LBC", fee_in.amount),
'address': fee_in.address
}
})