From fe5e088d0aa84c09d0f1baa379ea56742e39aae7 Mon Sep 17 00:00:00 2001 From: Morty Space Date: Mon, 27 Sep 2021 01:15:37 +0300 Subject: [PATCH] Fixed tests, updated transaction structs --- azure-pipelines.yml | 2 +- docs/source/conf.py | 2 +- src/cryptocom/exchange/__init__.py | 6 +- src/cryptocom/exchange/api.py | 2 +- src/cryptocom/exchange/market.py | 7 +-- src/cryptocom/exchange/private.py | 50 ++++++++------- src/cryptocom/exchange/structs.py | 97 ++++++++++++++++++++---------- tests/test_api.py | 8 +++ tests/test_market.py | 6 +- tests/test_private.py | 30 +++++++++ 10 files changed, 144 insertions(+), 66 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f34bc04..2af9585 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -18,7 +18,7 @@ variables: value: 16bcfb0958d99f11456f8d80aeb5800d567724471e151fe6e74a4b329b45dcb6 pool: - vmImage: 'ubuntu-latest' + vmImage: 'debian-latest' strategy: maxParallel: 1 diff --git a/docs/source/conf.py b/docs/source/conf.py index 4430a52..4d6487f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,7 +18,7 @@ from cryptocom.exchange import __version__ # -- Project information ----------------------------------------------------- project = 'cryptocom-exchange' -copyright = '2020, goincrypto.com' +copyright = '2020-2021' author = 'Yaroslav Rudenok [MortySpace]' # The short X.Y version diff --git a/src/cryptocom/exchange/__init__.py b/src/cryptocom/exchange/__init__.py index 089153b..e297527 100644 --- a/src/cryptocom/exchange/__init__.py +++ b/src/cryptocom/exchange/__init__.py @@ -1,6 +1,7 @@ from .structs import ( Order, OrderSide, OrderStatus, OrderType, Pair, Period, Candle, - MarketTrade, Coin, PrivateTrade + MarketTrade, Coin, PrivateTrade, Timeframe, DepositStatus, Deposit, + WithdrawalStatus, Withdrawal ) from .market import Exchange from .private import Account @@ -11,8 +12,9 @@ __all__ = [ 'Order', 'OrderSide', 'OrderStatus', 'OrderType', 'pairs', 'Pair', 'coins', 'Coin', 'Period', 'Candle', 'MarketTrade', 'PrivateTrade', + 'Timeframe', 'Deposit', 'Withdrawal', 'DepositStatus', 'WithdrawalStatus', 'Exchange', 'Account', 'ApiError', 'ApiProvider' ] -__version__ = '0.8.1' +__version__ = '0.9' diff --git a/src/cryptocom/exchange/api.py b/src/cryptocom/exchange/api.py index 2471b8b..a11d63b 100644 --- a/src/cryptocom/exchange/api.py +++ b/src/cryptocom/exchange/api.py @@ -33,7 +33,7 @@ class ApiProvider: self.retries = retries # NOTE: do not change this, due to crypto.com rate-limits - self.semaphore = asyncio.Semaphore(40) + self.semaphore = asyncio.Semaphore(20) if not auth_required: return diff --git a/src/cryptocom/exchange/market.py b/src/cryptocom/exchange/market.py index 427029d..99bcc0b 100644 --- a/src/cryptocom/exchange/market.py +++ b/src/cryptocom/exchange/market.py @@ -58,10 +58,10 @@ class Exchange: 'public/get-trades', {'instrument_name': pair.name}) return [MarketTrade.from_api(pair, trade) for trade in reversed(data)] - async def get_orderbook(self, pair: Pair, depth: int = 150) -> OrderBook: - """Get the order book for a particular market.""" + async def get_orderbook(self, pair: Pair) -> OrderBook: + """Get the order book for a particular market, depth always 150.""" data = await self.api.get('public/get-book', { - 'instrument_name': pair.name, 'depth': depth}) + 'instrument_name': pair.name}) buys = [ OrderInBook(*order, pair, OrderSide.BUY) for order in data[0]['bids'] @@ -75,7 +75,6 @@ class Exchange: async def get_candles(self, pair: Pair, period: Period) -> List[Candle]: data = await self.api.get('public/get-candlestick', { 'instrument_name': pair.name, 'timeframe': period.value}) - return [Candle.from_api(pair, candle) for candle in data] async def listen_candles( diff --git a/src/cryptocom/exchange/private.py b/src/cryptocom/exchange/private.py index 8817fcd..7328902 100644 --- a/src/cryptocom/exchange/private.py +++ b/src/cryptocom/exchange/private.py @@ -5,9 +5,9 @@ from typing import List, Dict from .api import ApiProvider, ApiError from .market import Exchange from .structs import ( - Pair, OrderSide, OrderStatus, OrderType, Order, Coin, Balance, - OrderExecType, OrderForceType, PrivateTrade, Interest, Withdrawal, - Deposit + Deposit, DepositStatus, Pair, OrderSide, OrderStatus, OrderType, Order, + Coin, Balance, OrderExecType, OrderForceType, PrivateTrade, Interest, + Withdrawal, WithdrawalStatus ) @@ -38,57 +38,60 @@ class Account: } async def get_deposit_history( - self, coin: Coin, start_ts: int, end_ts: int, status: int, page: int = 0, page_size: int = 20)\ - -> List[Deposit]: + self, coin: Coin, start_ts: int = None, end_ts: int = None, + status: DepositStatus = None, page: int = 0, page_size: int = 20 + ) -> List[Deposit]: """Return all history withdrawals.""" params = {'page_size': page_size, 'page': page} if coin: params['currency'] = coin.name if start_ts: - params['start_ts'] = start_ts + params['start_ts'] = int(start_ts) * 1000 if end_ts: - params['end_ts'] = end_ts + params['end_ts'] = int(end_ts) * 1000 if status: - params['status'] = str(status) + params['status'] = status data = await self.api.post( 'private/get-deposit-history', {'params': params}) or {} return [ - Deposit.create_from_api(deposit) - for deposit in data.get('deposit_list') or [] + Deposit.create_from_api(trx) + for trx in data.get('deposit_list') or [] ] async def get_withdrawal_history( - self, coin: Coin, start_ts: int, end_ts: int, status: int, page: int = 0, page_size: int = 20)\ - -> List[Withdrawal]: - """Return all history withdrawals.""" + self, coin: Coin, start_ts: int = None, end_ts: int = None, + status: WithdrawalStatus = None, page: int = 0, page_size: int = 20 + ) -> List[Withdrawal]: + """Return all history for withdrawal transactions.""" params = {'page_size': page_size, 'page': page} if coin: params['currency'] = coin.name if start_ts: - params['start_ts'] = start_ts + params['start_ts'] = int(start_ts) * 1000 if end_ts: - params['end_ts'] = end_ts + params['end_ts'] = int(end_ts) * 1000 if status: - params['status'] = str(status) + params['status'] = status data = await self.api.post( 'private/get-withdrawal-history', {'params': params}) or {} return [ - Withdrawal.create_from_api(withdrawal) - for withdrawal in data.get('withdrawal_list') or [] + Withdrawal.create_from_api(trx) + for trx in data.get('withdrawal_list') or [] ] async def get_interest_history( - self, coin: Coin, start_ts: int, end_ts: int, page: int = 0, page_size: int = 20) -> List[Interest]: + self, coin: Coin, start_ts: int = None, end_ts: int = None, + page: int = 0, page_size: int = 20) -> List[Interest]: """Return all history interest.""" params = {'page_size': page_size, 'page': page} if coin: params['currency'] = coin.name if start_ts: - params['start_ts'] = start_ts + params['start_ts'] = int(start_ts) * 1000 if end_ts: - params['end_ts'] = end_ts + params['end_ts'] = int(end_ts) * 1000 data = await self.api.post( 'private/margin/get-order-history', {'params': params}) or {} @@ -105,9 +108,10 @@ class Account: if pair: params['instrument_name'] = pair.name if start_ts: - params['start_ts'] = start_ts + params['start_ts'] = int(start_ts) * 1000 if end_ts: - params['end_ts'] = end_ts + params['end_ts'] = int(end_ts) * 1000 + data = await self.api.post( 'private/get-order-history', {'params': params}) or {} return [ diff --git a/src/cryptocom/exchange/structs.py b/src/cryptocom/exchange/structs.py index bd9c5d9..705dd4e 100644 --- a/src/cryptocom/exchange/structs.py +++ b/src/cryptocom/exchange/structs.py @@ -1,8 +1,9 @@ import time -from enum import Enum +from enum import Enum, IntEnum from typing import List, Dict from dataclasses import dataclass +from datetime import datetime from cached_property import cached_property @@ -368,53 +369,87 @@ class Interest: ) +class WithdrawalStatus(str, Enum): + PENDING = '0' + PROCESSING = '1' + REJECTED = '2' + PAYMENT_IN_PROGRESS = '3' + PAYMENT_FAILED = '4' + COMPLETED = '5' + CANCELLED = '6' + + +class DepositStatus(str, Enum): + NOT_ARRIVED = '0' + ARRIVED = '1' + FAILED = '2' + PENDING = '3' + + +class TransactionType(IntEnum): + WITHDRAWAL = 0 + DEPOSIT = 1 + + @dataclass -class Withdrawal: +class Transaction: coin: Coin - client_wid: str fee: float create_time: int id: str update_time: int amount: float address: str - status: str - @classmethod - def create_from_api(cls, data: Dict) -> 'Withdrawal': - return cls( + @staticmethod + def _prepare(data): + return dict( + id=data['id'], coin=Coin(data['currency']), - client_wid=data['client_wid'], fee=float(data['fee']), - create_time=data['create_time'], - id=data['id'], - update_time=data['update_time'], + create_time=datetime.fromtimestamp( + int(data['create_time']) / 1000), + update_time=datetime.fromtimestamp( + int(data['update_time']) / 1000), amount=float(data['amount']), address=data['address'], - status=data['status'] ) @dataclass -class Deposit: - coin: Coin - fee: float - create_time: int - id: str - update_time: int - amount: float - address: str - status: str +class Deposit(Transaction): + status: DepositStatus @classmethod def create_from_api(cls, data: Dict) -> 'Deposit': - return cls( - coin=Coin(data['currency']), - fee=float(data['fee']), - create_time=data['create_time'], - id=data['id'], - update_time=data['update_time'], - amount=float(data['amount']), - address=data['address'], - status=data['status'] - ) + params = cls._prepare(data) + params['status'] = DepositStatus(data['status']) + return cls(**params) + + +@dataclass +class Withdrawal(Transaction): + client_wid: str + status: WithdrawalStatus + txid: str + + @classmethod + def create_from_api(cls, data: Dict) -> 'Withdrawal': + params = cls._prepare(data) + params['client_wid'] = data.get('client_wid', '') + params['status'] = WithdrawalStatus(data['status']) + params['txid'] = data['txid'] + return cls(**params) + + +class Timeframe(IntEnum): + NOW = 0 + MINUTES = 60 + HOURS = 60 * MINUTES + DAYS = 24 * HOURS + WEEKS = 7 * DAYS + MONTHS = 30 * DAYS + + @classmethod + def resolve(cls, seconds: int) -> int: + return seconds + int(time.time()) diff --git a/tests/test_api.py b/tests/test_api.py index 05ec53a..541cc4e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,9 +1,17 @@ import os +import time import pytest import cryptocom.exchange as cro +def test_timeframe(): + days_5 = cro.Timeframe.DAYS * 5 + result = cro.Timeframe.resolve(days_5) + assert result - int(time.time()) == days_5 + assert cro.Timeframe.resolve(cro.Timeframe.NOW) == int(time.time()) + + def test_api_args(monkeypatch): with pytest.raises(ValueError): cro.Account() diff --git a/tests/test_market.py b/tests/test_market.py index 3df0902..6bd6029 100644 --- a/tests/test_market.py +++ b/tests/test_market.py @@ -41,8 +41,8 @@ async def test_get_price(exchange: cro.Exchange): @pytest.mark.asyncio async def test_get_orderbook(exchange: cro.Exchange): - depth = 50 - book = await exchange.get_orderbook(cro.pairs.CRO_USDT, depth=depth) + depth = 150 + book = await exchange.get_orderbook(cro.pairs.CRO_USDT) assert book.buys and book.sells assert book.sells[0].price > book.buys[0].price assert book.spread > 0 @@ -62,7 +62,7 @@ async def test_listen_candles(exchange: cro.Exchange): candles = [] pairs = (cro.pairs.CRO_USDC, cro.pairs.USDC_USDT, cro.pairs.BTC_USDT) count = 0 - default_count = 600 + default_count = 2 async for candle in exchange.listen_candles(cro.Period.MINS, *pairs): candles.append(candle) diff --git a/tests/test_private.py b/tests/test_private.py index 4060971..7738e11 100644 --- a/tests/test_private.py +++ b/tests/test_private.py @@ -1,8 +1,12 @@ +import time import asyncio import pytest import cryptocom.exchange as cro +from cryptocom.exchange.structs import ( + DepositStatus, Timeframe, WithdrawalStatus +) @pytest.mark.asyncio @@ -23,6 +27,32 @@ async def test_account_get_balance(account: cro.Account): assert coin in local_coins +@pytest.mark.asyncio +async def test_deposit_withdrawal_history( + exchange: cro.Exchange, account: cro.Account): + transactions = await account.get_withdrawal_history(cro.coins.CRO) + assert transactions + assert transactions[0].status == cro.WithdrawalStatus.COMPLETED + + transactions = await account.get_deposit_history(cro.coins.CRO) + assert transactions + assert transactions[0].status == cro.DepositStatus.ARRIVED + + transactions = await account.get_deposit_history( + cro.coins.CRO, status=DepositStatus.NOT_ARRIVED) + assert not transactions + + transactions = await account.get_withdrawal_history( + cro.coins.CRO, status=WithdrawalStatus.CANCELLED) + assert not transactions + + transactions = await account.get_deposit_history( + cro.coins.CRO, start_ts=time.time() - Timeframe.DAYS * 5, + end_ts=Timeframe.resolve(Timeframe.NOW) + ) + assert not transactions + + @pytest.mark.asyncio async def test_no_duplicate_mass_limit_orders( exchange: cro.Exchange, account: cro.Account):