-
-
Notifications
You must be signed in to change notification settings - Fork 119
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* New exchange rates system * Fix formatting issues * Adaptive rates fetching * Fix worker * Try fixing * Fix database access * Final fix * Fix tests * Fix tests * Fix final test * More fixes * Fix all tests * Fixes * Fix flaky test * Fixes * Finalize tests * Finalize
- Loading branch information
1 parent
89d8b43
commit e53ad38
Showing
41 changed files
with
777 additions
and
225 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import asyncio | ||
import time | ||
from abc import ABCMeta, abstractmethod | ||
from decimal import Decimal | ||
from typing import Dict, List | ||
|
||
from bitcart.coin import Coin | ||
|
||
from api.ext.fxrate import ExchangePair | ||
from api.logger import get_exception_message, get_logger | ||
|
||
logger = get_logger(__name__) | ||
|
||
REFRESH_TIME = 150 | ||
EXCHANGE_ACTIVE_TIME = 12 * 60 * 60 | ||
|
||
# Adaptive system: avoid refresh on call except for first time, then refresh in background | ||
# If exchange wasn't used for 12 hours, stop refreshing in background | ||
|
||
|
||
def get_inverse_dict(d): | ||
return {str(ExchangePair(k).inverse()): 1 / v for k, v in d.items()} | ||
|
||
|
||
class BaseExchange(metaclass=ABCMeta): | ||
def __init__(self, coins: List[Coin], contracts: Dict[str, list]): | ||
self.coins = coins | ||
self.contracts = contracts | ||
self.quotes = {} | ||
self.last_refresh = 0 | ||
self.last_called = 0 | ||
self.lock = asyncio.Lock() | ||
asyncio.create_task(self.refresh_task()) | ||
|
||
async def _check_fresh(self, called=False): | ||
async with self.lock: | ||
cur_time = time.time() | ||
if (called and (self.last_refresh == 0 or cur_time - self.last_called > EXCHANGE_ACTIVE_TIME)) or ( | ||
not called and cur_time - self.last_refresh > REFRESH_TIME | ||
): | ||
try: | ||
await self.refresh() | ||
self.quotes.update(get_inverse_dict(self.quotes)) | ||
except Exception as e: | ||
logger.error(f"Failed refreshing exchange rates:\n{get_exception_message(e)}") | ||
self.last_refresh = cur_time | ||
if called: | ||
self.last_called = cur_time | ||
|
||
async def get_rate(self, pair=None): | ||
await self._check_fresh(True) | ||
if pair is None: | ||
return self.quotes | ||
return self.quotes.get(pair, Decimal("NaN")) | ||
|
||
async def get_fiat_currencies(self): | ||
await self._check_fresh(True) | ||
return list(map(lambda x: x.split("_")[1], self.quotes)) | ||
|
||
@abstractmethod | ||
async def refresh(self): | ||
pass | ||
|
||
async def refresh_task(self): | ||
while True: | ||
if time.time() - self.last_called <= EXCHANGE_ACTIVE_TIME: | ||
await self._check_fresh() | ||
await asyncio.sleep(REFRESH_TIME + 1) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import asyncio | ||
import json | ||
|
||
from api import settings, utils | ||
from api.ext.exchanges.base import BaseExchange | ||
|
||
|
||
async def fetch_delayed(*args, delay=1, **kwargs): | ||
resp, data = await utils.common.send_request(*args, return_json=False) | ||
if resp.status == 429: | ||
if delay < 60: | ||
await asyncio.sleep(delay) | ||
return await fetch_delayed(*args, **kwargs, delay=delay * 2) | ||
resp.raise_for_status() | ||
data = json.loads(data) | ||
if kwargs.get("return_json", True): | ||
return data | ||
return resp, data | ||
|
||
|
||
def find_by_coin(all_coins, coin): | ||
coingecko_id = settings.settings.exchange_rates.coingecko_ids.get(coin.coin_name.lower()) | ||
if coingecko_id: | ||
for currency in all_coins: | ||
if currency.get("id", "").lower() == coingecko_id.lower(): | ||
return currency | ||
for currency in all_coins: | ||
if currency.get("name", "").lower() == coin.friendly_name.lower(): | ||
return currency | ||
for currency in all_coins: | ||
if currency.get("symbol", "").lower() == coin.coin_name.lower(): | ||
return currency | ||
|
||
|
||
def find_by_contract(all_coins, contract): | ||
for currency in all_coins: | ||
if contract in currency.get("platforms", {}).values(): | ||
return currency | ||
|
||
|
||
def find_id(all_coins, x): | ||
for coin in all_coins: | ||
if coin["id"] == x: | ||
return coin["symbol"] | ||
|
||
|
||
class CoingeckoExchange(BaseExchange): | ||
def __init__(self, coins, contracts): | ||
super().__init__(coins, contracts) | ||
self.coins_cache = {} | ||
|
||
async def refresh(self): | ||
vs_currencies = await fetch_delayed("GET", "https://api.coingecko.com/api/v3/simple/supported_vs_currencies") | ||
if not self.coins_cache: | ||
self.coins_cache = await fetch_delayed("GET", "https://api.coingecko.com/api/v3/coins/list?include_platform=true") | ||
coins = [] | ||
for coin in self.coins.copy(): | ||
currency = find_by_coin(self.coins_cache, coin) | ||
if currency: | ||
coins.append(currency["id"]) | ||
for contracts in self.contracts.copy().values(): | ||
for contract in contracts: | ||
currency = find_by_contract(self.coins_cache, contract) | ||
if currency: | ||
coins.append(currency["id"]) | ||
data = await fetch_delayed( | ||
"GET", | ||
( | ||
f"https://api.coingecko.com/api/v3/simple/price?ids={','.join(coins)}" | ||
f"&vs_currencies={','.join(vs_currencies)}&precision=full" | ||
), | ||
) | ||
self.quotes = { | ||
f"{find_id(self.coins_cache, k).upper()}_{k2.upper()}": utils.common.precise_decimal(v2) | ||
for k, v in data.items() | ||
for k2, v2 in v.items() | ||
} | ||
|
||
|
||
def coingecko_based_exchange(name): | ||
class CoingeckoBasedExchange(BaseExchange): | ||
def __init__(self, coins, contracts): | ||
super().__init__(coins, contracts) | ||
self.coins_cache = {} | ||
|
||
async def refresh(self): | ||
if not self.coins_cache: | ||
self.coins_cache = await fetch_delayed("GET", "https://api.coingecko.com/api/v3/coins/list") | ||
coins = [] | ||
for coin in self.coins.copy(): | ||
currency = find_by_coin(self.coins_cache, coin) | ||
if currency: | ||
coins.append(currency["id"]) | ||
self.quotes = await self.fetch_rates(coins) | ||
|
||
async def fetch_rates(self, coins, page=1): | ||
base_url = f"https://api.coingecko.com/api/v3/exchanges/{name}/tickers" | ||
page = 1 | ||
resp, data = await fetch_delayed("GET", f"{base_url}?page={page}&coin_ids={','.join(coins)}", return_json=False) | ||
result = {f"{x['base']}_{x['target']}": utils.common.precise_decimal(x["last"]) for x in data["tickers"]} | ||
total = resp.headers.get("total") | ||
per_page = resp.headers.get("per-page") | ||
if page == 1 and total and per_page: | ||
total = int(total) | ||
per_page = int(per_page) | ||
total_pages = total // per_page | ||
if total % per_page != 0: | ||
total_pages += 1 | ||
for page in range(2, total_pages + 1): | ||
result.update(await self.fetch_rates(page=page)) | ||
return result | ||
|
||
return CoingeckoBasedExchange |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
class BTC: | ||
coingecko_id = "bitcoin" | ||
|
||
|
||
class BCH: | ||
coingecko_id = "bitcoin-cash" | ||
|
||
|
||
class LTC: | ||
coingecko_id = "litecoin" | ||
|
||
|
||
class BSTY: | ||
coingecko_id = "globalboost" | ||
|
||
|
||
class XRG: | ||
coingecko_id = "bitcoin" # not working | ||
|
||
|
||
class ETH: | ||
coingecko_id = "ethereum" | ||
|
||
|
||
class BNB: | ||
coingecko_id = "binancecoin" | ||
|
||
|
||
class SBCH: | ||
default_rule = "SBCH_X = BCH_X" | ||
|
||
|
||
class MATIC: | ||
coingecko_id = "matic-network" | ||
|
||
|
||
class TRX: | ||
coingecko_id = "tron" | ||
|
||
|
||
class GRS: | ||
coingecko_id = "groestlcoin" | ||
|
||
|
||
class XMR: | ||
coingecko_id = "monero" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from api import utils | ||
from api.ext.exchanges.base import BaseExchange | ||
|
||
|
||
class FiatExchange(BaseExchange): | ||
async def refresh(self): | ||
result = await utils.common.send_request( | ||
"GET", "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/usd.json" | ||
) | ||
self.quotes = {f"USD_{k.upper()}": utils.common.precise_decimal(v) for k, v in result["usd"].items()} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from api import utils | ||
from api.ext.exchanges.base import BaseExchange | ||
|
||
|
||
class Kraken(BaseExchange): | ||
async def refresh(self): | ||
ccys = ["EUR", "USD", "CAD", "GBP", "JPY"] | ||
pairs = ["XBT%s" % c for c in ccys] | ||
json = await utils.common.send_request("GET", "https://api.kraken.com/0/public/Ticker?pair=%s" % ",".join(pairs)) | ||
self.quotes = {f"BTC_{k[-3:]}": utils.common.precise_decimal(str(v["c"][0])) for k, v in json["result"].items()} |
Oops, something went wrong.