Skip to content

Commit

Permalink
.
Browse files Browse the repository at this point in the history
  • Loading branch information
nochiel committed Apr 16, 2022
1 parent d11dd5a commit ac1dddf
Showing 1 changed file with 72 additions and 95 deletions.
167 changes: 72 additions & 95 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
class ServerErrors: # TODO(nochiel) Replace these with HTTPException
NO_DATA = 'Spotbit did not find any data.'
EXCHANGE_NOT_SUPPORTED = 'Spotbit is not configured to support the exchange.'
BAD_DATE_FORMAT = 'Please use dates in YYYY-MM-DDTHH:mm:ss ISO8601 format or unix timestamps.'

class Error(BaseModel):
code : int
Expand Down Expand Up @@ -53,7 +54,7 @@ class Config:
# we can determine which ones shouldn't be supported i.e. populate
# this list with exchanges that fail tests.
_unsupported_exchanges = []
_supported_exchanges: dict[str, ccxt.Exchange] = {} # TODO(nochiel) Load these when self.exchanges is loaded.
_supported_exchanges: dict[str, ccxt.Exchange] = {}

def get_logger():

Expand Down Expand Up @@ -108,7 +109,7 @@ def get_logger():

assert _supported_exchanges

ExchangeName = Enum('ExchangeName', [(exchange.title(), exchange) for exchange in _supported_exchanges])
ExchangeName = Enum('ExchangeName', [(id.upper(), id) for id in _supported_exchanges])

# Exchange data is sometimes returned as epoch milliseconds.
def is_ms(timestamp): return timestamp % 1e3 == 0
Expand Down Expand Up @@ -137,13 +138,14 @@ def request_single(exchange: ccxt.Exchange, currency: CurrencyName) -> Candle |
Make a single request, without having to loop through all exchanges and currency pairs.
'''
assert exchange and isinstance(exchange, ccxt.Exchange)
assert str
assert currency

result: Candle = None
latest_candle = None
ticker = f'BTC/{currency}'
ticker = f'BTC/{currency.value}'
dt = None

exchange.load_markets()
if ticker not in exchange.markets:
return None

Expand Down Expand Up @@ -401,31 +403,31 @@ def get_candle(exchange: ccxt.Exchange, currency: CurrencyName) -> tuple[ccxt.Ex

return result

@app.get('/api/now/{currency}/{exchange: str}')
@app.get('/api/now/{currency}/{exchange}', response_model = Candle)
def now(currency: CurrencyName, exchange: ExchangeName):
'''
parameters:
exchange(required): an exchange to use.
currency(required): the symbol for the base currency to use e.g. USD, GBP, UST.
exchange: an exchange to use.
currency: the symbol for the base currency to use e.g. USD, GBP, UST.
'''

if exchange not in _supported_exchanges:
if exchange.value not in _supported_exchanges:
raise HTTPException(
status_code = HTTPStatus.INTERNAL_SERVER_ERROR,
detail = f'Spotbit is not configured to use {exchange} exchange.')
detail = f'Spotbit is not configured to use {exchange.value} exchange.')

result = None

exchange = _supported_exchanges[exchange]
assert exchange
exchange.load_markets()
ccxtExchange = _supported_exchanges[exchange.value]
assert ccxtExchange
ccxtExchange.load_markets()

if currency.value not in exchange.currencies:
if currency.value not in ccxtExchange.currencies:
raise HTTPException(
status_code = HTTPStatus.INTERNAL_SERVER_ERROR,
detail = f'Spotbit does not support {currency.value} on {exchange}.' )
detail = f'Spotbit does not support {currency.value} on {ccxtExchange}.' )

result = request_single(exchange, currency)
result = request_single(ccxtExchange, currency)
if not result:
raise HTTPException(
status_code = HTTPStatus.INTERNAL_SERVER_ERROR,
Expand All @@ -448,76 +450,61 @@ class OHLCV(IntEnum):

# FIXME(nochiel) Use query parameters.
# - exchange, currency, date_end are optional.
# FIXME(nochiel) Add parameter validation.
# FIXME(nochiel) Add date parameter validation.
# TODO(nochiel) Write tests
@app.get('/api/history/{currency}/{exchange}/{date_start}/{date_end}')
async def get_candles_in_range(currency: CurrencyName, exchange: ExchangeName, date_start, date_end):
@app.get('/api/history/{currency}/{exchange}', response_model = list[Candle])
async def get_candles_in_range(
currency: CurrencyName,
exchange: ExchangeName,
start: datetime,
end: datetime = datetime.now()):
'''
parameters:
exchange(required): an exchange to use.
currency(required): the symbol for the base currency to use e.g. USD, GBP, UST.
date_start, date_end(required): datetime formatted as ISO8601 "YYYY-MM-DDTHH:mm:SS".
start, end(required): datetime formatted as ISO8601 "YYYY-MM-DDTHH:mm:SS" or unix timestamp.
'''

if exchange not in _supported_exchanges:
flask.abort(flask.Response(response = f'Spotbit is not configured to use {exchange} exchange.',
status = HTTPStatus.INTERNAL_SERVER_ERROR))
ccxtExchange = _supported_exchanges[exchange.value]
ccxtExchange.load_markets()
assert ccxtExchange.currencies
assert ccxtExchange.markets

exchange = _supported_exchanges[exchange]
exchange.load_markets()
assert exchange.currencies
assert exchange.markets
currency = currency.upper()
if currency not in exchange.currencies:
flask.abort(flask.Response(response = f'Spotbit does not support the {currency} on {exchange}',
status = HTTPStatus.INTERNAL_SERVER_ERROR))

pair = f'BTC/{currency}'
if exchange.id == 'bitmex':
pair = f'BTC/{currency.value}'
if ccxtExchange.id == 'bitmex':
pair = f'BTC/{currency}:{currency}'
if pair not in exchange.markets:
flask.abort(flask.Response(response = f'Spotbit does not support the {pair} pair on {exchange}',
status = HTTPStatus.INTERNAL_SERVER_ERROR))
if pair not in ccxtExchange.markets:
HTTPException(
detail = f'Spotbit does not support the {pair} pair on {ccxtExchange}',
status_code = HTTPStatus.INTERNAL_SERVER_ERROR)

result = None
start, end = date_start, date_end
try:
try:
start = datetime.fromisoformat(date_start)
except ValueError:
start = datetime.fromtimestamp(int(date_start))

try:
end = datetime.fromisoformat(date_end)
except ValueError:
end = datetime.fromtimestamp(int(date_end))
except Exception as e:
flask.abort(flask.Response(
response = f'Error: {e}. Please use dates in YYYY-MM-DDTHH:mm:ss ISO8601 format.',
status = HTTPStatus.BAD_REQUEST))
start = start.astimezone(start.tzinfo)
end = end.astimezone(end.tzinfo)

(start, end) = (end, start) if end < start else (start, end)
logger.debug(f'start: {start}, end: {end}')

limit = 100

candles = None
periods = []

# Consider enabling a backup historical candle feed e.g. coinmarketcap, coingecko, cryptowa.ch, graph or a dex?
# Especially if there's a feed that allows you to filter by an exchange.
if exchange.has['fetchOHLCV'] is not True:
flask.abort(flask.Response(
response = f'{exchange} does not support pagination of historical candles. Please try to use a different exchange.',
status = HTTPStatus.BAD_REQUEST))
if ccxtExchange.has['fetchOHLCV'] is not True:
HTTPException(
detail = f'{ccxtExchange} does not support pagination of historical candles. Please try to use a different exchange.',
status_code = HTTPStatus.BAD_REQUEST)

dt = timedelta(0)
params = None
timeframe = ''
if '1h' in exchange.timeframes:
if '1h' in ccxtExchange.timeframes:
timeframe = '1h'
dt = timedelta(hours = 1)

elif '30m' in exchange.timeframes:
elif '30m' in ccxtExchange.timeframes:
timeframe = '30m'
dt = timedelta(minutes = 30)

Expand All @@ -534,7 +521,7 @@ async def get_candles_in_range(currency: CurrencyName, exchange: ExchangeName, d
logger.debug(f'requesting periods with {limit} limit: {periods}')

def get_history(*,
exchange: ccxt.Exchange = exchange,
exchange: ccxt.Exchange = ccxtExchange,
since: datetime,
limit: int = limit,
timeframe: str = timeframe) -> list:
Expand Down Expand Up @@ -563,7 +550,7 @@ def get_history(*,

wait = 0

except ccxt.base.errors.RateLimitExceeded as e:
except ccxt.errors.RateLimitExceeded as e:
logger.debug(f'{e}. Rate limit for {exchange} is {rateLimit}')
time.sleep(wait)
wait *= 2
Expand All @@ -576,7 +563,7 @@ def get_history(*,
tasks = []
for p in periods:
task = asyncio.to_thread(get_history,
exchange = exchange,
exchange = ccxtExchange,
since = p)
tasks.append(task)

Expand Down Expand Up @@ -611,16 +598,17 @@ def get_history(*,
expected_number_of_candles = (n_periods * limit) + remaining_frames
received_number_of_candles = len(candles)
if received_number_of_candles < expected_number_of_candles:
logger.info(f'{exchange} does not have data for the entire period. Expected {expected_number_of_candles} candles. Got {received_number_of_candles} candles')
logger.info(f'{ccxtExchange} does not have data for the entire period. Expected {expected_number_of_candles} candles. Got {received_number_of_candles} candles')

if candles is None or len(candles) == 0:
flask.abort(flask.Response(response = f'Spotbit did not receive any candle history for the period {start} - {end} from {exchange}',
status = HTTPStatus.INTERNAL_SERVER_ERROR))
HTTPException(
detail = f'Spotbit did not receive any candle history for the period {start} - {end} from {ccxtExchange}',
status_code = HTTPStatus.INTERNAL_SERVER_ERROR)

logger.debug(f'got: {len(candles)} candles')
logger.debug(f'candles: {candles[:10]} ... {candles[-10:]}')
result = candles

result = flask.jsonify([candle.dict() for candle in candles])
return result

tests_for_get_candles_at_dates = [
Expand All @@ -638,36 +626,9 @@ async def get_candles_at_dates(currency: CurrencyName, exchange: ExchangeName):
logger.debug(f'{request.get_data()}')

if exchange not in _supported_exchanges:
flask.abort(flask.Response(
response = ServerErrors.EXCHANGE_NOT_SUPPORTED,
status = HTTPStatus.INTERNAL_SERVER_ERROR
))
exchange = _supported_exchanges[exchange]

if exchange.has['fetchOHLCV'] is not True:
flask.abort(flask.Response(
response = f'{exchange} does not support pagination of historical candles. Please try to use a different exchange.',
status = HTTPStatus.BAD_REQUEST))

dates = None
try:
dates = request.get_json()
# TODO(nochiel) Check if isoformat or timestamps. Raise HTTPException if dates don't parse.
dates = [datetime.fromisoformat(date) for date in dates] if dates else []
if len(dates) == 0:
raise Exception(f'received: ${dates}')
except Exception as e:
flask.abort(flask.Response(
response = f'error processing dates: {e}.\n' +
'Dates should be provided in the body of the request as a json array of dates formatted as ISO8601 "YYYY-MM-DDTHH:mm:SS".',
status = HTTPStatus.INTERNAL_SERVER_ERROR))

results = None
exchange.load_markets()
pair = f'BTC/{currency.upper()}'
if pair not in exchange.markets:
flask.abort(flask.Response(response = f'Spotbit does not support the {pair} pair on {exchange}',
status = HTTPStatus.INTERNAL_SERVER_ERROR))
HTTPException(
detail = f'Spotbit does not support the {pair} pair on {exchange}',
status_code = HTTPStatus.INTERNAL_SERVER_ERROR)

limit = 1
timeframe = '1h'
Expand Down Expand Up @@ -727,11 +688,27 @@ def get_candle(*,

return result

def tests():
# Placeholder

import requests
response = requests.get("http://[::1]:5000/api/now/FOOBAR")
response = requests.get("http://[::1]:5000/api/now/usd")
response = requests.get("http://[::1]:5000/api/now/USD")
response = requests.get("http://[::1]:5000/api/now/JPY")


response = requests.get("http://[::1]:5000/api/now/USD/Bitstamp")
response = requests.get("http://[::1]:5000/api/now/USD/bitstamp")

response = requests.get(
"http://[::1]:5000/api/history/USD/bitstamp?start=2019-01-01T0000&end=1522641600"
)

if __name__ == '__main__':
import uvicorn

assert logger
logger.setLevel(logging.DEBUG) # FIXME(nochiel) This won't actually change logging level.
logger.debug('Running in debug mode')
logger.debug(f'app.debug: {app.debug}')
uvicorn.run('app:app',
Expand Down

0 comments on commit ac1dddf

Please sign in to comment.