Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions additional_tests/exchanges_tests/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ PHEMEX_SANDBOXED=true
POLYMARKET_KEY=
POLYMARKET_SECRET=
POLYMARKET_PASSWORD=

COINRABBIT_KEY=
COINRABBIT_SECRET=
COINRABBIT_JWT= # plain JWT for x-user-token
COINRABBIT_X_API_KEY= # plain OctoBot settings API key for x-api-key header
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@
import octobot_trading.exchanges as trading_exchanges
import octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants
import octobot_trading.exchanges.connectors.ccxt.ccxt_client_util as ccxt_client_util
import octobot_protocol.models as protocol_models
import octobot_trading.personal_data as personal_data
import octobot_trading.personal_data.orders as personal_data_orders
import octobot_trading.personal_data.transactions.protocol as transactions_protocol
import octobot_trading.util.test_tools.exchanges_test_tools as exchanges_test_tools
import octobot_trading.exchanges.util.exchange_data as exchange_data_import
import octobot_tentacles_manager.api as tentacles_manager_api
Expand Down Expand Up @@ -128,6 +130,8 @@ class AbstractAuthenticatedExchangeTester:
# dedicated api and have to be checked by sending an order operation
EXPECTED_INVALID_ORDERS_QUANTITY = [] # orders with known invalid quantity exchange order id (usually legacy)
IS_READ_ONLY_EXCHANGE = False # set True when the exchange is read only and should not be able to fetch portfolio, create or cancel orders
REQUIRES_CURRENCIES_FOR_TRANSACTIONS = False # True when get_deposits/get_withdrawals need portfolio currencies (e.g. Coinbase)
ALLOW_NO_DEPOSIT_HISTORY = False # True when empty deposit history is valid (e.g. MEXC 7-day window)
CHECK_EMPTY_ACCOUNT = False # set True when the account to check has no funds. Warning: does not check order
# parse/create/fill/cancel or portfolio & trades parsing
IS_BROKER_ENABLED_ACCOUNT = True # set False when this test account can't generate broker fees
Expand Down Expand Up @@ -776,6 +780,115 @@ async def inner_test_get_my_recent_trades(self):
assert trades
self.check_raw_trades(trades)

def _exhaust_history_symbol_kwargs(self) -> dict:
if self._get_option_value(
trading_enums.ExchangeClientOptions.MY_TRADES_SYMBOL_FILTER_IS_CLIENT_SIDE
):
return {"symbol": None}
return {"symbol": self.SYMBOL}

def _expects_my_trades_pagination(self) -> bool:
if self._get_option_value(
trading_enums.ExchangeClientOptions.MY_TRADES_FETCH_PAGINATION_OFFSET
):
return True
if self._get_option_value(
trading_enums.ExchangeClientOptions.MY_TRADES_FETCH_USE_CCXT_PAGINATE
):
return True
if (
self._get_option_value(
trading_enums.ExchangeClientOptions.REQUIRE_RECENT_TRADES_FROM_CLOSED_ORDERS
)
and self._get_option_value(
trading_enums.ExchangeClientOptions.CLOSED_ORDERS_FETCH_USE_CCXT_PAGINATE
)
):
return True
return False

async def test_get_my_recent_trades_exhaust_history(self):
async with self.local_exchange_manager():
await self.inner_test_get_my_recent_trades_exhaust_history()

@expect_not_supported_on_read_only("get_my_recent_trades")
async def inner_test_get_my_recent_trades_exhaust_history(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if self.CHECK_EMPTY_ACCOUNT or self.EXPECT_EMPTY_RECENT_TRADES:
return
symbol_kwargs = self._exhaust_history_symbol_kwargs()
single = await self.exchange_manager.exchange.get_my_recent_trades(
**symbol_kwargs, exhaust_history=False,
)
exhaust = await self.exchange_manager.exchange.get_my_recent_trades(
**symbol_kwargs, exhaust_history=True,
)
if self._expects_my_trades_pagination():
assert len(exhaust) >= len(single)
if exhaust:
self.check_raw_trades(exhaust)
else:
assert len(exhaust) == len(single)
if exhaust:
self.check_raw_trades(exhaust)

async def test_get_deposits(self):
async with self.local_exchange_manager():
await self.inner_test_get_deposits()

@expect_not_supported_on_read_only("get_deposits")
async def inner_test_get_deposits(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if self.CHECK_EMPTY_ACCOUNT:
deposits = await self.get_deposits()
assert deposits == [], f"{deposits} != []"
return
if not self.exchange_manager.exchange.connector.client.has["fetchDeposits"]:
with pytest.raises(trading_errors.NotSupported):
await self.get_deposits()
return
deposits_kwargs = {}
if self.REQUIRES_CURRENCIES_FOR_TRANSACTIONS:
portfolio = await self.get_portfolio()
currencies = self._portfolio_currencies_for_transactions(portfolio)
if not currencies:
return
deposits_kwargs = {"currencies": currencies}
deposits = await self.get_deposits(**deposits_kwargs)
assert isinstance(deposits, list)
if not deposits and self.ALLOW_NO_DEPOSIT_HISTORY:
return
assert deposits
self.check_raw_transactions(
deposits, trading_enums.TransactionType.BLOCKCHAIN_DEPOSIT
)

async def test_get_withdrawals(self):
async with self.local_exchange_manager():
await self.inner_test_get_withdrawals()

@expect_not_supported_on_read_only("get_withdrawals")
async def inner_test_get_withdrawals(self):
if self.CHECK_EMPTY_ACCOUNT:
withdrawals = await self.get_withdrawals()
assert withdrawals == [], f"{withdrawals} != []"
return
if not self.exchange_manager.exchange.connector.client.has["fetchWithdrawals"]:
with pytest.raises(trading_errors.NotSupported):
await self.get_withdrawals()
return
withdrawals_kwargs = {}
if self.REQUIRES_CURRENCIES_FOR_TRANSACTIONS:
portfolio = await self.get_portfolio()
currencies = self._portfolio_currencies_for_transactions(portfolio)
if not currencies:
return
withdrawals_kwargs = {"currencies": currencies}
withdrawals = await self.get_withdrawals(**withdrawals_kwargs)
assert isinstance(withdrawals, list)
if withdrawals:
self.check_raw_transactions(
withdrawals, trading_enums.TransactionType.BLOCKCHAIN_WITHDRAWAL
)

async def test_get_closed_orders(self):
async with self.local_exchange_manager():
await self.inner_test_get_closed_orders()
Expand Down Expand Up @@ -1050,6 +1163,20 @@ async def get_my_recent_trades(self, exchange_data=None):
exchange_data = exchange_data or self.get_exchange_data()
return await exchanges_test_tools.get_trades(self.exchange_manager, exchange_data)

async def get_deposits(self, **kwargs):
return await self.exchange_manager.exchange.get_deposits(**kwargs)

async def get_withdrawals(self, **kwargs):
return await self.exchange_manager.exchange.get_withdrawals(**kwargs)

def _portfolio_currencies_for_transactions(self, portfolio, max_count=3):
min_holdings_threshold = decimal.Decimal("1e-8")
funded_currencies = sorted([
asset for asset, values in portfolio.items()
if values[trading_constants.CONFIG_PORTFOLIO_TOTAL] > min_holdings_threshold
])
return funded_currencies[:max_count]

async def get_closed_orders(self, symbol=None):
return await self.exchange_manager.exchange.get_closed_orders(symbol or self.SYMBOL)

Expand Down Expand Up @@ -1205,6 +1332,65 @@ def check_parsed_trade(self, trade: personal_data.Trade):
f"is FALSE: Trade: {trade.to_dict()}"
)

def check_duplicate_transactions(self, transactions):
transaction_columns = trading_enums.ExchangeConstantsTransactionColumns
unique_transaction_keys = {
f"{transaction.get(transaction_columns.ID.value)}"
f"{transaction.get(transaction_columns.TXID.value)}"
f"{transaction[transaction_columns.TIMESTAMP.value]}"
f"{transaction[transaction_columns.AMOUNT.value]}"
f"{transaction[transaction_columns.CURRENCY.value]}"
for transaction in transactions
}
assert len(unique_transaction_keys) == len(transactions)

def check_raw_transactions(self, transactions, expected_type: trading_enums.TransactionType):
self.check_duplicate_transactions(transactions)
for transaction in transactions:
self.check_parsed_transaction(transaction, expected_type)

def check_parsed_transaction(
self, transaction: dict, expected_type: trading_enums.TransactionType
):
transaction_columns = trading_enums.ExchangeConstantsTransactionColumns
assert transaction[transaction_columns.TYPE.value] == expected_type.value
currency = transaction[transaction_columns.CURRENCY.value]
assert currency
assert isinstance(currency, str)
amount = transaction[transaction_columns.AMOUNT.value]
assert isinstance(amount, decimal.Decimal)
assert amount > trading_constants.ZERO
timestamp = transaction[transaction_columns.TIMESTAMP.value]
assert timestamp is not None
assert int(timestamp) > 0
transaction_id = transaction.get(transaction_columns.ID.value)
transaction_txid = transaction.get(transaction_columns.TXID.value)
assert transaction_id or transaction_txid
status = transaction.get(transaction_columns.STATUS.value)
if status is not None:
assert status
assert isinstance(status, str)
protocol_raw = transaction
if not transaction_txid and transaction_id:
protocol_raw = dict(transaction)
protocol_raw[transaction_columns.TXID.value] = transaction_id
protocol_transaction = transactions_protocol.to_protocol_transaction(protocol_raw)
assert protocol_transaction.asset == currency
assert protocol_transaction.amount == float(amount)
expected_protocol_type = {
trading_enums.TransactionType.BLOCKCHAIN_DEPOSIT: (
protocol_models.TransactionType.BLOCKCHAIN_DEPOSIT
),
trading_enums.TransactionType.BLOCKCHAIN_WITHDRAWAL: (
protocol_models.TransactionType.BLOCKCHAIN_WITHDRAWAL
),
}[expected_type]
assert protocol_transaction.type == expected_protocol_type
if transaction_txid:
assert protocol_transaction.id == transaction_txid
else:
assert protocol_transaction.id == transaction_id

def check_theoretical_cost(self, symbol, quantity, price, cost):
theoretical_cost = quantity * price
assert theoretical_cost * decimal.Decimal("0.8") <= cost <= theoretical_cost * decimal.Decimal("1.2")
Expand Down
11 changes: 11 additions & 0 deletions additional_tests/exchanges_tests/test_binance.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ async def test_create_and_fill_market_orders(self):
async def test_get_my_recent_trades(self):
await super().test_get_my_recent_trades()

async def test_get_my_recent_trades_exhaust_history(self):
await super().test_get_my_recent_trades_exhaust_history()

async def test_get_deposits(self):
# untested yet
await super().test_get_deposits()

async def test_get_withdrawals(self):
# untested yet
await super().test_get_withdrawals()

async def test_get_closed_orders(self):
await super().test_get_closed_orders()

Expand Down
11 changes: 11 additions & 0 deletions additional_tests/exchanges_tests/test_binance_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,17 @@ async def test_create_and_fill_market_orders(self):
async def test_get_my_recent_trades(self):
await super().test_get_my_recent_trades()

async def test_get_my_recent_trades_exhaust_history(self):
await super().test_get_my_recent_trades_exhaust_history()

async def test_get_deposits(self):
# untested yet
await super().test_get_deposits()

async def test_get_withdrawals(self):
# untested yet
await super().test_get_withdrawals()

async def test_get_closed_orders(self):
await super().test_get_closed_orders()

Expand Down
9 changes: 9 additions & 0 deletions additional_tests/exchanges_tests/test_bingx.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ async def test_create_and_fill_market_orders(self):
async def test_get_my_recent_trades(self):
await super().test_get_my_recent_trades()

async def test_get_my_recent_trades_exhaust_history(self):
await super().test_get_my_recent_trades_exhaust_history()

async def test_get_deposits(self):
await super().test_get_deposits()

async def test_get_withdrawals(self):
await super().test_get_withdrawals()

async def test_get_closed_orders(self):
await super().test_get_closed_orders()

Expand Down
11 changes: 11 additions & 0 deletions additional_tests/exchanges_tests/test_bitget.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ async def test_create_and_fill_market_orders(self):
async def test_get_my_recent_trades(self):
await super().test_get_my_recent_trades()

async def test_get_my_recent_trades_exhaust_history(self):
await super().test_get_my_recent_trades_exhaust_history()

async def test_get_deposits(self):
# untested yet
await super().test_get_deposits()

async def test_get_withdrawals(self):
# untested yet
await super().test_get_withdrawals()

async def test_get_closed_orders(self):
await super().test_get_closed_orders()

Expand Down
9 changes: 9 additions & 0 deletions additional_tests/exchanges_tests/test_bitmart.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ async def test_create_and_fill_market_orders(self):
async def test_get_my_recent_trades(self):
await super().test_get_my_recent_trades()

async def test_get_my_recent_trades_exhaust_history(self):
await super().test_get_my_recent_trades_exhaust_history()

async def test_get_deposits(self):
await super().test_get_deposits()

async def test_get_withdrawals(self):
await super().test_get_withdrawals()

async def test_get_closed_orders(self):
await super().test_get_closed_orders()

Expand Down
11 changes: 11 additions & 0 deletions additional_tests/exchanges_tests/test_bybit.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ async def test_create_and_fill_market_orders(self):
async def test_get_my_recent_trades(self):
await super().test_get_my_recent_trades()

async def test_get_my_recent_trades_exhaust_history(self):
await super().test_get_my_recent_trades_exhaust_history()

async def test_get_deposits(self):
# untested yet
await super().test_get_deposits()

async def test_get_withdrawals(self):
# untested yet
await super().test_get_withdrawals()

async def test_get_closed_orders(self):
await super().test_get_closed_orders()

Expand Down
11 changes: 11 additions & 0 deletions additional_tests/exchanges_tests/test_bybit_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ async def test_create_and_fill_market_orders(self):
async def test_get_my_recent_trades(self):
await super().test_get_my_recent_trades()

async def test_get_my_recent_trades_exhaust_history(self):
await super().test_get_my_recent_trades_exhaust_history()

async def test_get_deposits(self):
# untested yet
await super().test_get_deposits()

async def test_get_withdrawals(self):
# untested yet
await super().test_get_withdrawals()

async def test_get_closed_orders(self):
await super().test_get_closed_orders()

Expand Down
12 changes: 11 additions & 1 deletion additional_tests/exchanges_tests/test_coinbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ class TestCoinbaseAuthenticatedExchange(
SETTLEMENT_CURRENCY = "USDC"
SYMBOL = f"{ORDER_CURRENCY}/{SETTLEMENT_CURRENCY}"
ORDER_SIZE = 70 # % of portfolio to include in test orders
MIN_TRADE_USD_VALUE = decimal.Decimal("0.0004")
MIN_TRADE_USD_VALUE = decimal.Decimal("0.000003")
CONVERTS_ORDER_SIZE_BEFORE_PUSHING_TO_EXCHANGES = True
VALID_ORDER_ID = "8bb80a81-27f7-4415-aa50-911ea46d841c"
USE_ORDER_OPERATION_TO_CHECK_API_KEY_RIGHTS = True # set True when api key rights can't be checked using a
EXPECT_MISSING_FEE_IN_CANCELLED_ORDERS = False
IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE = True # set True when is_authenticated_request is implemented
REQUIRES_CURRENCIES_FOR_TRANSACTIONS = True
SLEEP_SECONDS_BEFORE_CHECKING_PORTFOLIO = 8

SPECIAL_ORDER_TYPES_BY_EXCHANGE_ID: dict[
Expand Down Expand Up @@ -128,6 +129,15 @@ async def test_create_and_fill_market_orders(self):
async def test_get_my_recent_trades(self):
await super().test_get_my_recent_trades()

async def test_get_my_recent_trades_exhaust_history(self):
await super().test_get_my_recent_trades_exhaust_history()

async def test_get_deposits(self):
await super().test_get_deposits()

async def test_get_withdrawals(self):
await super().test_get_withdrawals()

async def test_get_closed_orders(self):
await super().test_get_closed_orders()

Expand Down
Loading
Loading