def test_get_trade_with_1_token_pool( rotkehlchen_api_server, ethereum_accounts, # pylint: disable=unused-argument rotki_premium_credentials, # pylint: disable=unused-argument start_with_valid_premium, # pylint: disable=unused-argument ): """ Test the special case of a swap within an 1 token pool. This can probably happen if the controller has since removed tokens from the pool. """ rotki = rotkehlchen_api_server.rest_api.rotkehlchen setup = setup_balances( rotki, ethereum_accounts=ethereum_accounts, btc_accounts=None, original_queries=['zerion', 'logs', 'blocknobytime'], ) with ExitStack() as stack: # patch ethereum/etherscan to not autodetect tokens setup.enter_ethereum_patches(stack) response = requests.get( api_url_for(rotkehlchen_api_server, 'balancertradeshistoryresource'), json={ 'from_timestamp': 1621358338, 'to_timestamp': 1621358340, }, ) result = assert_proper_response_with_result(response) db_trades = rotki.data.db.get_amm_swaps() assert len(db_trades) == 29 address_trades = result[BALANCER_TEST_ADDR4] assert len(address_trades) == 1 assert address_trades[0] == AMMTrade( trade_type=TradeType.BUY, base_asset=A_WETH, quote_asset=A_WBTC, amount=AssetAmount(FVal('0.205421420618533148')), rate=Price(FVal('0.07606382992071615428519015532')), trade_index=0, swaps=[ AMMSwap( tx_hash='0x4f9e0d8aa660a5d3db276a1ade038f7027f29838dd22d5276571d2e4ea7131ae', # noqa: E501 log_index=84, address=string_to_ethereum_address(BALANCER_TEST_ADDR4), # noqa: E501 from_address=string_to_ethereum_address('0xFD3dFB524B2dA40c8a6D703c62BE36b5D8540626'), # noqa: E501 to_address=string_to_ethereum_address('0x582818356331877553F3E9Cf9557b48e5DdbD54a'), # noqa: E501 timestamp=Timestamp(1621358339), location=Location.BALANCER, token0=A_WBTC, token1=A_WETH, amount0_in=AssetAmount(FVal('0.01562514')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('0.205421420618533148')), ), ], ).serialize()
def add_trades_from_swaps( swaps: List[AMMSwap], trades: List[AMMTrade], both_in: bool, quote_assets: Sequence[Tuple[Any, ...]], token_amount: AssetAmount, token: EthereumToken, trade_index: int, ) -> List[AMMTrade]: bought_amount = AssetAmount(token_amount / 2) if both_in else token_amount for entry in quote_assets: quote_asset = entry[0] sold_amount = entry[1] rate = sold_amount / bought_amount trade = AMMTrade( trade_type=TradeType.BUY, base_asset=token, quote_asset=quote_asset, amount=bought_amount, rate=rate, swaps=swaps, trade_index=trade_index, ) trades.append(trade) trade_index += 1 return trades
def test_deserialize_asset_movement_withdrawal(mock_kucoin): raw_result = { 'id': '5c2dc64e03aa675aa263f1ac', 'address': '0x5bedb060b8eb8d823e2414d82acce78d38be7fe9', 'memo': '', 'currency': 'ETH', 'amount': 1, 'fee': 0.01, 'walletTxId': '3e2414d82acce78d38be7fe9', 'isInner': False, 'status': 'SUCCESS', 'remark': 'test', 'createdAt': 1612556794259, 'updatedAt': 1612556795000, } expected_asset_movement = AssetMovement( timestamp=Timestamp(1612556794), location=Location.KUCOIN, category=AssetMovementCategory.WITHDRAWAL, address='0x5bedb060b8eb8d823e2414d82acce78d38be7fe9', transaction_id='3e2414d82acce78d38be7fe9', asset=A_ETH, amount=AssetAmount(FVal('1')), fee_asset=A_ETH, fee=Fee(FVal('0.01')), link='5c2dc64e03aa675aa263f1ac', ) asset_movement = mock_kucoin._deserialize_asset_movement( raw_result=raw_result, case=KucoinCase.WITHDRAWALS, ) assert asset_movement == expected_asset_movement
def _trade_from_independentreserve(raw_trade: Dict) -> Trade: """Convert IndependentReserve raw data to a trade https://www.independentreserve.com/products/api#GetClosedFilledOrders May raise: - DeserializationError - UnknownAsset - KeyError """ log.debug(f'Processing raw IndependentReserve trade: {raw_trade}') trade_type = TradeType.BUY if 'Bid' in raw_trade[ 'OrderType'] else TradeType.SELL base_asset = independentreserve_asset(raw_trade['PrimaryCurrencyCode']) quote_asset = independentreserve_asset(raw_trade['SecondaryCurrencyCode']) amount = FVal(raw_trade['Volume']) - FVal(raw_trade['Outstanding']) timestamp = deserialize_timestamp_from_date( date=raw_trade['CreatedTimestampUtc'], formatstr='iso8601', location='IndependentReserve', ) rate = Price(FVal(raw_trade['AvgPrice'])) fee_amount = FVal(raw_trade['FeePercent']) * amount fee_asset = base_asset return Trade( timestamp=timestamp, location=Location.INDEPENDENTRESERVE, base_asset=base_asset, quote_asset=quote_asset, trade_type=trade_type, amount=AssetAmount(amount), rate=rate, fee=Fee(fee_amount), fee_currency=fee_asset, link=str(raw_trade['OrderGuid']), )
def test_get_associated_locations( rotkehlchen_api_server_with_exchanges, added_exchanges, ethereum_accounts, # pylint: disable=unused-argument start_with_valid_premium, # pylint: disable=unused-argument ): rotki = rotkehlchen_api_server_with_exchanges.rest_api.rotkehlchen mock_exchange_data_in_db(added_exchanges, rotki) db = rotki.data.db db.add_trades([ Trade( timestamp=Timestamp(1595833195), location=Location.NEXO, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=AssetAmount(FVal('1.0')), rate=Price(FVal('281.14')), fee=Fee(ZERO), fee_currency=A_EUR, link='', notes='', ) ]) # get locations response = requests.get( api_url_for( rotkehlchen_api_server_with_exchanges, 'associatedlocations', ), ) result = assert_proper_response_with_result(response) assert set(result) == {'nexo', 'binance', 'poloniex'}
def process_entry( self, db: DBHandler, db_ledger: DBLedgerActions, timestamp: Timestamp, data: BinanceCsvRow, ) -> None: amount = data['Change'] asset = data['Coin'] category = AssetMovementCategory.DEPOSIT if data['Operation'] == 'Deposit' else AssetMovementCategory.WITHDRAWAL # noqa: E501 if category == AssetMovementCategory.WITHDRAWAL: amount = -amount asset_movement = AssetMovement( location=Location.BINANCE, category=category, address=None, transaction_id=None, timestamp=timestamp, asset=asset, amount=AssetAmount(amount), fee=Fee(ZERO), fee_asset=A_USD, link=f'Imported from binance CSV file. Binance operation: {data["Operation"]}', ) db.add_asset_movements([asset_movement])
def test_deserialize_trade_sell(mock_bitfinex): mock_bitfinex.currency_map = {'UST': 'USDt'} mock_bitfinex.pair_bfx_symbols_map = {'ETHUST': ('ETH', 'UST')} raw_result = [ 399251013, 'tETHUST', 1573485493000, 33963608932, -0.26334268, 187.37, 'LIMIT', None, -1, -0.09868591, 'USD', ] expected_trade = Trade( timestamp=Timestamp(1573485493), location=Location.BITFINEX, base_asset=A_ETH, quote_asset=A_USDT, trade_type=TradeType.SELL, amount=AssetAmount(FVal('0.26334268')), rate=Price(FVal('187.37')), fee=Fee(FVal('0.09868591')), fee_currency=A_USD, link='399251013', notes='', ) trade = mock_bitfinex._deserialize_trade(raw_result=raw_result) assert trade == expected_trade
def test_deserialize_v1_trade(mock_kucoin): raw_result = { 'id': 'xxxx', 'symbol': 'NANO-ETH', 'dealPrice': '0.015743', 'dealValue': '0.00003441', 'amount': '0.002186', 'fee': '0.00000003', 'side': 'sell', 'createdAt': 1520471876, } expected_trade = Trade( timestamp=Timestamp(1520471876), location=Location.KUCOIN, base_asset=A_NANO, quote_asset=A_ETH, trade_type=TradeType.SELL, amount=AssetAmount(FVal('0.002186')), rate=Price(FVal('0.015743')), fee=Fee(FVal('0.00000003')), fee_currency=A_ETH, link='xxxx', notes='', ) trade = mock_kucoin._deserialize_trade( raw_result=raw_result, case=KucoinCase.OLD_TRADES, ) assert trade == expected_trade
def mock_exchange_data_in_db(exchange_locations, rotki) -> None: db = rotki.data.db for exchange_location in exchange_locations: db.add_trades([ Trade( timestamp=Timestamp(1), location=exchange_location, base_asset=A_BTC, quote_asset=A_ETH, trade_type=TradeType.BUY, amount=AssetAmount(FVal(1)), rate=Price(FVal(1)), fee=Fee(FVal('0.1')), fee_currency=A_ETH, link='foo', notes='boo', ) ]) db.update_used_query_range( name=f'{str(exchange_location)}_trades_{str(exchange_location)}', start_ts=0, end_ts=9999) # noqa: E501 db.update_used_query_range( name=f'{str(exchange_location)}_margins_{str(exchange_location)}', start_ts=0, end_ts=9999) # noqa: E501 db.update_used_query_range( name= f'{str(exchange_location)}_asset_movements_{str(exchange_location)}', start_ts=0, end_ts=9999) # noqa: E501
def deserialize_asset_amount(amount: AcceptableFValInitInput) -> AssetAmount: try: result = AssetAmount(FVal(amount)) except ValueError as e: raise DeserializationError( f'Failed to deserialize an amount entry: {str(e)}') from e return result
def deserialize_asset_amount_force_positive( amount: AcceptableFValInitInput) -> AssetAmount: """Acts exactly like deserialize_asset_amount but also forces the number to be positive Is needed for some places like some exchanges that list the withdrawal amounts as negative numbers because it's a withdrawal""" result = deserialize_asset_amount(amount) if result < ZERO: result = AssetAmount(abs(result)) return result
def _deserialize_trade(self, raw_result: List[Any]) -> Trade: """Process a trade result from Bitfinex and deserialize it. The base and quote assets are instantiated using the `fee_currency_symbol` (from raw_result[10]) over the pair (from raw_result[1]). Known pairs format: 'tETHUST', 'tETH:UST'. Can raise: - DeserializationError. - UnknownAsset - UnsupportedAsset Schema reference in: https://docs.bitfinex.com/reference#rest-auth-trades """ amount = deserialize_asset_amount(raw_result[4]) trade_type = TradeType.BUY if amount >= ZERO else TradeType.SELL bfx_pair = self._process_bfx_pair(raw_result[1]) if bfx_pair not in self.pair_bfx_symbols_map: raise DeserializationError( f'Could not deserialize bitfinex trade pair {raw_result[1]}. ' f'Raw trade: {raw_result}', ) bfx_base_asset_symbol, bfx_quote_asset_symbol = self.pair_bfx_symbols_map[ bfx_pair] base_asset = asset_from_bitfinex( bitfinex_name=bfx_base_asset_symbol, currency_map=self.currency_map, ) quote_asset = asset_from_bitfinex( bitfinex_name=bfx_quote_asset_symbol, currency_map=self.currency_map, ) fee_asset = asset_from_bitfinex( bitfinex_name=raw_result[10], currency_map=self.currency_map, ) trade = Trade( timestamp=Timestamp(int(raw_result[2] / 1000)), location=Location.BITFINEX, base_asset=base_asset, quote_asset=quote_asset, trade_type=trade_type, amount=AssetAmount(abs(amount)), rate=deserialize_price(raw_result[5]), fee=Fee(abs(deserialize_fee(raw_result[9]))), fee_currency=fee_asset, link=str(raw_result[0]), notes='', ) return trade
def _deserialize_trade( self, raw_trade: Dict[str, Any], ) -> Trade: """Process a trade user transaction from Bitstamp and deserialize it. Can raise DeserializationError. """ timestamp = deserialize_timestamp_from_bitstamp_date( raw_trade['datetime']) trade_pair_data = self._get_trade_pair_data_from_transaction(raw_trade) base_asset_amount = deserialize_asset_amount( raw_trade[trade_pair_data.base_asset_symbol], ) quote_asset_amount = deserialize_asset_amount( raw_trade[trade_pair_data.quote_asset_symbol], ) rate = deserialize_price(raw_trade[trade_pair_data.pair]) fee_currency = trade_pair_data.quote_asset if base_asset_amount >= ZERO: trade_type = TradeType.BUY else: if quote_asset_amount < 0: raise DeserializationError( f'Unexpected bitstamp trade format. Both base and quote ' f'amounts are negative: {raw_trade}', ) trade_type = TradeType.SELL trade = Trade( timestamp=timestamp, location=Location.BITSTAMP, base_asset=trade_pair_data.base_asset, quote_asset=trade_pair_data.quote_asset, trade_type=trade_type, amount=AssetAmount(abs(base_asset_amount)), rate=rate, fee=deserialize_fee(raw_trade['fee']), fee_currency=fee_currency, link=str(raw_trade['id']), notes='', ) return trade
def test_deserialize_v2_trade_sell(mock_kucoin): raw_result = { 'symbol': 'BCHSV-USDT', 'tradeId': '601da995e0ee8b00063a075c', 'orderId': '601da9950c92050006bd45c5', 'counterOrderId': '601da9950c92050006bd457d', 'side': 'sell', 'liquidity': 'taker', 'forceTaker': True, 'price': '37624.4', 'size': '0.0013', 'funds': '48.91172', 'fee': '0.034238204', 'feeRate': '0.0007', 'feeCurrency': 'USDT', 'stop': '', 'tradeType': 'TRADE', 'type': 'market', 'createdAt': 1612556794259, } expected_trade = Trade( timestamp=Timestamp(1612556794), location=Location.KUCOIN, base_asset=A_BSV, quote_asset=A_USDT, trade_type=TradeType.SELL, amount=AssetAmount(FVal('0.0013')), rate=Price(FVal('37624.4')), fee=Fee(FVal('0.034238204')), fee_currency=A_USDT, link='601da995e0ee8b00063a075c', notes='', ) trade = mock_kucoin._deserialize_trade( raw_result=raw_result, case=KucoinCase.TRADES, ) assert trade == expected_trade
def test_deserialize_v2_trade_buy(mock_kucoin): raw_result = { 'symbol': 'KCS-USDT', 'tradeId': '601da9faf1297d0007efd712', 'orderId': '601da9fa0c92050006bd83be', 'counterOrderId': '601bad620c9205000642300f', 'side': 'buy', 'liquidity': 'taker', 'forceTaker': True, 'price': 1000, 'size': '0.2', 'funds': 200, 'fee': '0.14', 'feeRate': '0.0007', 'feeCurrency': 'USDT', 'stop': '', 'tradeType': 'TRADE', 'type': 'market', 'createdAt': 1612556794259, } expected_trade = Trade( timestamp=Timestamp(1612556794), location=Location.KUCOIN, base_asset=A_KCS, quote_asset=A_USDT, trade_type=TradeType.BUY, amount=AssetAmount(FVal('0.2')), rate=Price(FVal('1000')), fee=Fee(FVal('0.14')), fee_currency=A_USDT, link='601da9faf1297d0007efd712', notes='', ) trade = mock_kucoin._deserialize_trade( raw_result=raw_result, case=KucoinCase.TRADES, ) assert trade == expected_trade
def trade_from_bitmex(bitmex_trade: Dict) -> MarginPosition: """Turn a bitmex trade returned from bitmex trade history to our common trade history format. This only returns margin positions as bitmex only deals in margin trading. May raise: - KeyError - DeserializationError """ close_time = iso8601ts_to_timestamp(bitmex_trade['transactTime']) profit_loss = AssetAmount( satoshis_to_btc(deserialize_asset_amount(bitmex_trade['amount']))) currency = bitmex_to_world(bitmex_trade['currency']) fee = deserialize_fee(bitmex_trade['fee']) notes = bitmex_trade['address'] assert currency == A_BTC, 'Bitmex trade should only deal in BTC' log.debug( 'Processing Bitmex Trade', timestamp=close_time, profit_loss=profit_loss, currency=currency, fee=fee, notes=notes, ) return MarginPosition( location=Location.BITMEX, open_time=None, close_time=close_time, profit_loss=profit_loss, pl_currency=currency, fee=fee, fee_currency=A_BTC, notes=notes, link=str(bitmex_trade['transactID']), )
from rotkehlchen.utils.version_check import get_current_version if TYPE_CHECKING: from rotkehlchen.accounting.accountant import Accountant from rotkehlchen.db.dbhandler import DBHandler from rotkehlchen.rotkehlchen import Rotkehlchen from rotkehlchen.tests.fixtures.google import GoogleService history1 = [ Trade( timestamp=Timestamp(1446979735), location=Location.EXTERNAL, base_asset=A_BTC, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=AssetAmount(FVal(82)), rate=Price(FVal('268.678317859')), fee=None, fee_currency=None, link=None, ), Trade( timestamp=Timestamp(1446979735), location=Location.EXTERNAL, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=AssetAmount(FVal(1450)), rate=Price(FVal('0.2315893')), fee=None, fee_currency=None,
def process_trades( db: DBHandler, timestamp: Timestamp, data: List[BinanceCsvRow], ) -> List[Trade]: """Processes multiple rows data and stores it into rotki's trades Each row has format: {'Operation': ..., 'Change': ..., 'Coin': ...} Change is amount, Coin is asset If amount is negative then this asset is sold, otherwise it's bought """ # Because we can get mixed data (e.g. multiple Buys or Sells on a single timestamp) we need # to group it somehow. We are doing it by grouping the highest bought with the highest # sold value. We query usd equivalent for each amount because different Sells / Buys # may use different assets. # If we query price for the first time it can take long, so we would like to avoid it, # and therefore we check if all Buys / Sells use the same asset. # If so, we can group by original amount. # Checking assets same_assets = True assets: Dict[str, Optional[Asset]] = defaultdict(lambda: None) for row in data: if row['Operation'] == 'Fee': cur_operation = 'Fee' elif row['Change'] < 0: cur_operation = 'Sold' else: cur_operation = 'Bought' assets[cur_operation] = assets[cur_operation] or row['Coin'] if assets[cur_operation] != row['Coin']: same_assets = False break # Querying usd value if needed if same_assets is False: for row in data: try: price = PriceHistorian.query_historical_price( from_asset=row['Coin'], to_asset=A_USD, timestamp=timestamp, ) except NoPriceForGivenTimestamp: # If we can't find price we can't group, so we quit the method log.warning(f'Couldn\'t find price of {row["Coin"]} on {timestamp}') return [] row['usd_value'] = row['Change'] * price # Group rows depending on whether they are fee or not and then sort them by amount rows_grouped_by_fee: Dict[bool, List[BinanceCsvRow]] = defaultdict(list) for row in data: is_fee = row['Operation'] == 'Fee' rows_grouped_by_fee[is_fee].append(row) for rows_group in rows_grouped_by_fee.values(): rows_group.sort(key=lambda x: x['Change'] if same_assets else x['usd_value'], reverse=True) # noqa: E501 # Grouping by combining the highest sold with the highest bought and the highest fee # Using fee only we were provided with fee (checking by "True in rows_by_operation") grouped_trade_rows = [] while len(rows_grouped_by_fee[False]) > 0: cur_batch = [rows_grouped_by_fee[False].pop(), rows_grouped_by_fee[False].pop(0)] if True in rows_grouped_by_fee: cur_batch.append(rows_grouped_by_fee[True].pop()) grouped_trade_rows.append(cur_batch) # Creating trades structures based on grouped rows data raw_trades: List[Trade] = [] for trade_rows in grouped_trade_rows: to_asset: Optional[Asset] = None to_amount: Optional[AssetAmount] = None from_asset: Optional[Asset] = None from_amount: Optional[AssetAmount] = None fee_asset: Optional[Asset] = None fee_amount: Optional[Fee] = None trade_type: Optional[TradeType] = None for row in trade_rows: cur_asset = row['Coin'] amount = row['Change'] if row['Operation'] == 'Fee': fee_asset = cur_asset fee_amount = Fee(amount) else: trade_type = TradeType.SELL if row['Operation'] == 'Sell' else TradeType.BUY # noqa: E501 if amount < 0: from_asset = cur_asset from_amount = AssetAmount(-amount) else: to_asset = cur_asset to_amount = amount # Validate that we have received proper assets and amounts. # There can be no fee, so we don't validate it if ( to_asset is None or from_asset is None or trade_type is None or to_amount is None or to_amount == ZERO or from_amount is None or from_amount == ZERO ): log.warning( f'Skipped binance rows {data} because ' f'it didn\'t have enough data', ) db.msg_aggregator.add_warning('Skipped some rows because couldn\'t find amounts or it was zero') # noqa: E501 continue rate = to_amount / from_amount trade = Trade( timestamp=timestamp, location=Location.BINANCE, trade_type=trade_type, base_asset=to_asset, quote_asset=from_asset, amount=to_amount, rate=Price(rate), fee_currency=fee_asset, fee=fee_amount, link='', notes='Imported from binance CSV file. Binance operation: Buy / Sell', ) raw_trades.append(trade) # Sometimes we can get absolutely identical trades (including timestamp) but the database # allows us to add only one of them. So we combine these trades into a huge single trade # First step: group trades grouped_trades: Dict[TradeID, List[Trade]] = defaultdict(list) for trade in raw_trades: grouped_trades[trade.identifier].append(trade) # Second step: combine them unique_trades = [] for trades_group in grouped_trades.values(): result_trade = trades_group[0] for trade in trades_group[1:]: result_trade.amount = AssetAmount(result_trade.amount + trade.amount) # noqa: E501 if result_trade.fee is not None and trade.fee is not None: result_trade.fee = Fee(result_trade.fee + trade.fee) unique_trades.append(result_trade) return unique_trades
def test_associated_locations(database): """Test that locations imported in different places are correctly stored in database""" # Add trades from different locations trades = [Trade( timestamp=Timestamp(1595833195), location=Location.CRYPTOCOM, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=AssetAmount(FVal('1.0')), rate=Price(FVal('281.14')), fee=Fee(ZERO), fee_currency=A_USD, link='', notes='', ), Trade( timestamp=Timestamp(1587825824), location=Location.CRYPTOCOM, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=AssetAmount(FVal('50.0')), rate=Price(FVal('3.521')), fee=Fee(ZERO), fee_currency=A_USD, link='', notes='', ), Trade( timestamp=Timestamp(1596014214), location=Location.BLOCKFI, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=AssetAmount(FVal('50.0')), rate=Price(FVal('3.521')), fee=Fee(ZERO), fee_currency=A_USD, link='', notes='', ), Trade( timestamp=Timestamp(1565888464), location=Location.NEXO, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=AssetAmount(FVal('50.0')), rate=Price(FVal('3.521')), fee=Fee(ZERO), fee_currency=A_USD, link='', notes='', ), Trade( timestamp=Timestamp(1596014214), location=Location.NEXO, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=AssetAmount(FVal('50.0')), rate=Price(FVal('3.521')), fee=Fee(ZERO), fee_currency=A_USD, link='', notes='', ), Trade( timestamp=Timestamp(1612051199), location=Location.BLOCKFI, base_asset=symbol_to_asset_or_token('USDC'), quote_asset=symbol_to_asset_or_token('LTC'), trade_type=TradeType.BUY, amount=AssetAmount(FVal('6404.6')), rate=Price(FVal('151.6283999982779809352223797')), fee=None, fee_currency=None, link='', notes='One Time', ), Trade( timestamp=Timestamp(1595833195), location=Location.POLONIEX, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=AssetAmount(FVal('1.0')), rate=Price(FVal('281.14')), fee=Fee(ZERO), fee_currency=A_USD, link='', notes='', ), Trade( timestamp=Timestamp(1596429934), location=Location.COINBASE, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=AssetAmount(FVal('0.00061475')), rate=Price(FVal('309.0687271248474989833265555')), fee=Fee(ZERO), fee_currency=A_USD, link='', notes='', ), Trade( timestamp=Timestamp(1596429934), location=Location.EXTERNAL, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=AssetAmount(FVal('1')), rate=Price(FVal('320')), fee=Fee(ZERO), fee_currency=A_USD, link='', notes='', )] # Add multiple entries for same exchange + connected exchange database.add_trades(trades) kraken_api_key1 = ApiKey('kraken_api_key') kraken_api_secret1 = ApiSecret(b'kraken_api_secret') kraken_api_key2 = ApiKey('kraken_api_key2') kraken_api_secret2 = ApiSecret(b'kraken_api_secret2') binance_api_key = ApiKey('binance_api_key') binance_api_secret = ApiSecret(b'binance_api_secret') # add mock kraken and binance database.add_exchange('kraken1', Location.KRAKEN, kraken_api_key1, kraken_api_secret1) database.add_exchange('kraken2', Location.KRAKEN, kraken_api_key2, kraken_api_secret2) database.add_exchange('binance', Location.BINANCE, binance_api_key, binance_api_secret) # Add uniswap and sushiswap events database.add_amm_events([ LiquidityPoolEvent( tx_hash='0x47ea26957ce09e84a51b51dfdab6a4ac1c3672a372eef77b15ef7677174ac847', log_index=23, address=ChecksumEthAddress('0x3163Bb273E8D9960Ce003fD542bF26b4C529f515'), timestamp=Timestamp(1590011534), event_type=EventType.MINT_SUSHISWAP, pool_address=ChecksumEthAddress('0xa2107FA5B38d9bbd2C461D6EDf11B11A50F6b974'), token0=EthereumToken('0x514910771AF9Ca656af840dff83E8264EcF986CA'), token1=EthereumToken('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'), amount0=FVal('3.313676003468974932'), amount1=FVal('0.064189269269768657'), usd_price=FVal('26.94433946158740371839009166230438'), lp_amount=FVal('0.460858304063739927'), ), ]) database.add_amm_swaps([ AMMSwap( tx_hash='0xa54bf4c68d435e3c8f432fd7e62b7f8aca497a831a3d3fca305a954484ddd7b2', log_index=208, address=ChecksumEthAddress('0xa2107FA5B38d9bbd2C461D6EDf11B11A50F6b974'), from_address=string_to_ethereum_address('0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'), to_address=string_to_ethereum_address('0xC9cB53B48A2f3A9e75982685644c1870F1405CCb'), timestamp=Timestamp(1609301469), location=Location.UNISWAP, token0=EthereumToken('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'), token1=EthereumToken('0xdAC17F958D2ee523a2206206994597C13D831ec7'), amount0_in=AssetAmount(FVal('2.6455727132446468')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('1936.810111')), ), ]) database.add_balancer_events([ BalancerEvent( tx_hash='0xa54bf4c68d435e3c8f432fd7e62b7f8aca497a831a3d3fca305a954484ddd7b3', log_index=23, address=ChecksumEthAddress('0xa2107FA5B38d9bbd2C461D6EDf11B11A50F6b974'), timestamp=Timestamp(1609301469), event_type=BalancerBPTEventType.MINT, pool_address_token=EthereumToken('0x514910771AF9Ca656af840dff83E8264EcF986CA'), lp_balance=Balance(amount=FVal(2), usd_value=FVal(3)), amounts=[ AssetAmount(FVal(1)), AssetAmount(FVal(2)), ], ), ]) expected_locations = { Location.KRAKEN, Location.BINANCE, Location.BLOCKFI, Location.NEXO, Location.CRYPTOCOM, Location.POLONIEX, Location.COINBASE, Location.EXTERNAL, Location.SUSHISWAP, Location.UNISWAP, Location.BALANCER, } assert set(database.get_associated_locations()) == expected_locations
def get_balancer_test_addr2_expected_trades(): """In a function since the new(unknown) assets needs to have been loaded in the DB""" A_WCRES = EthereumToken.initialize( # noqa: N806 address=string_to_ethereum_address('0xa0afAA285Ce85974c3C881256cB7F225e3A1178a'), decimals=18, symbol='wCRES', ) return [ AMMTrade( trade_type=TradeType.BUY, base_asset=A_WETH, quote_asset=A_AAVE, amount=AssetAmount(FVal('1.616934038985744521')), rate=Price(FVal('6.963972908793392530935439799')), trade_index=1, swaps=[ AMMSwap( tx_hash='0x3c457da9b541ae39a7dc781ab04a03938b98b5649512aec2a2d32635c9bbf589', # noqa: E501 log_index=24, address=string_to_ethereum_address('0x029f388aC4D5C8BfF490550ce0853221030E822b'), # noqa: E501 from_address=string_to_ethereum_address('0x0000000000007F150Bd6f54c40A34d7C3d5e9f56'), # noqa: E501 to_address=string_to_ethereum_address('0x7c90a3cd7Ec80dd2F633ed562480AbbEEd3bE546'), # noqa: E501 timestamp=Timestamp(1607008178), location=Location.BALANCER, token0=A_AAVE, token1=A_WETH, amount0_in=AssetAmount(FVal('11.260284842802604032')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('1.616934038985744521')), ), ], ), AMMTrade( trade_type=TradeType.BUY, base_asset=A_AAVE, quote_asset=A_WETH, amount=AssetAmount(FVal('11.260286362820602094')), rate=Price(FVal('0.1416068599966922676173010716')), trade_index=0, swaps=[ AMMSwap( tx_hash='0x3c457da9b541ae39a7dc781ab04a03938b98b5649512aec2a2d32635c9bbf589', # noqa: E501 log_index=18, address=string_to_ethereum_address('0x029f388aC4D5C8BfF490550ce0853221030E822b'), # noqa: E501 from_address=string_to_ethereum_address('0x0000000000007F150Bd6f54c40A34d7C3d5e9f56'), # noqa: E501 to_address=string_to_ethereum_address('0x70985E557aE0CD6dC88189a532e54FbC61927BAd'), # noqa: E501 timestamp=Timestamp(1607008178), location=Location.BALANCER, token0=A_WETH, token1=A_AAVE, amount0_in=AssetAmount(FVal('1.594533794502600192')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('11.260286362820602094')), ), ], ), AMMTrade( trade_type=TradeType.BUY, base_asset=A_WETH, quote_asset=A_SYN, amount=AssetAmount(FVal('1.352902561458047718')), rate=Price(FVal('724.4303350385182691258363763')), trade_index=0, swaps=[ AMMSwap( tx_hash='0x5e235216cb03e4eb234014f5ccf3efbfddd40c4576424e2a8204f1d12b96ed35', # noqa: E501 log_index=143, address=string_to_ethereum_address('0x029f388aC4D5C8BfF490550ce0853221030E822b'), # noqa: E501 from_address=string_to_ethereum_address('0x0000000000007F150Bd6f54c40A34d7C3d5e9f56'), # noqa: E501 to_address=string_to_ethereum_address('0x8982E9bBf7AC6A49c434aD81D2fF8e16895318e5'), # noqa: E501 timestamp=Timestamp(1607008218), location=Location.BALANCER, token0=A_SYN, token1=A_WETH, amount0_in=AssetAmount(FVal('980.08365587152306176')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('1.352902561458047718')), ), ], ), AMMTrade( trade_type=TradeType.BUY, base_asset=A_WETH, quote_asset=A_WCRES, amount=AssetAmount(FVal('0.205709519074945018')), rate=Price(FVal('232.7409943164679514496089589')), trade_index=0, swaps=[ AMMSwap( tx_hash='0xf54be824b4619777f1db0e3da91b0cd52f6dba730c95a75644e2b085e6ab9824', # noqa: E501 log_index=300, address=string_to_ethereum_address('0x029f388aC4D5C8BfF490550ce0853221030E822b'), # noqa: E501 from_address=string_to_ethereum_address('0x0000000000007F150Bd6f54c40A34d7C3d5e9f56'), # noqa: E501 to_address=string_to_ethereum_address('0x10996eC4f3E7A1b314EbD966Fa8b1ad0fE0f8307'), # noqa: E501 timestamp=Timestamp(1607009877), location=Location.BALANCER, token0=A_WCRES, token1=A_WETH, amount0_in=AssetAmount(FVal('47.87703800986513408')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('0.205709519074945018')), ), ], ), AMMTrade( trade_type=TradeType.BUY, base_asset=A_API3, quote_asset=A_WETH, amount=AssetAmount(FVal('295.881648100500428692')), rate=Price(FVal('0.003346787723157288562491614498')), trade_index=0, swaps=[ AMMSwap( tx_hash='0xfed4e15051e3ce4dc0d2816f719701e5920e40bf41614b5feaa3c5a6a0186c03', # noqa: E501 log_index=22, address=string_to_ethereum_address('0x029f388aC4D5C8BfF490550ce0853221030E822b'), # noqa: E501 from_address=string_to_ethereum_address('0x0000000000007F150Bd6f54c40A34d7C3d5e9f56'), # noqa: E501 to_address=string_to_ethereum_address('0x997c0fc9578a8194EFDdE2E0cD7aa6A69cFCD7c1'), # noqa: E501 timestamp=Timestamp(1607010888), location=Location.BALANCER, token0=A_WETH, token1=A_API3, amount0_in=AssetAmount(FVal('0.990253067370299904')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('295.881648100500428692')), ), ], ), AMMTrade( trade_type=TradeType.BUY, base_asset=A_WETH, quote_asset=A_MFT, amount=AssetAmount(FVal('0.686544199299304057')), rate=Price(FVal('243775.0324093115004367119900')), trade_index=0, swaps=[ AMMSwap( tx_hash='0xf0147c4b81098676c08ae20ae5bf8f8b60d0ad79eec484f3f93ac6ab49a3c51c', # noqa: E501 log_index=97, address=string_to_ethereum_address('0x029f388aC4D5C8BfF490550ce0853221030E822b'), # noqa: E501 from_address=string_to_ethereum_address('0x0000000000007F150Bd6f54c40A34d7C3d5e9f56'), # noqa: E501 to_address=string_to_ethereum_address('0x2Eb6CfbFFC8785Cd0D9f2d233d0a617bF4269eeF'), # noqa: E501 timestamp=Timestamp(1607015059), location=Location.BALANCER, token0=A_MFT, token1=A_WETH, amount0_in=AssetAmount(FVal('167362.334434612660404224')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('0.686544199299304057')), ), ], ), AMMTrade( trade_type=TradeType.BUY, base_asset=A_WETH, quote_asset=A_AAVE, amount=AssetAmount(FVal('3.055412574642681758')), rate=Price(FVal('6.916116208273240607778771150')), trade_index=1, swaps=[ AMMSwap( tx_hash='0x67c0e9a0fdd002d0b9d1cca0c8e4ca4d30435bbf57bbf0091396275efaea414b', # noqa: E501 log_index=37, address=string_to_ethereum_address('0x029f388aC4D5C8BfF490550ce0853221030E822b'), # noqa: E501 from_address=string_to_ethereum_address('0x0000000000007F150Bd6f54c40A34d7C3d5e9f56'), # noqa: E501 to_address=string_to_ethereum_address('0x0E552307659E70bF61f918f96AA880Cdec40d7E2'), # noqa: E501 timestamp=Timestamp(1607015339), location=Location.BALANCER, token0=A_AAVE, token1=A_WETH, amount0_in=AssetAmount(FVal('21.131588430448123904')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('3.055412574642681758')), ), ], ), AMMTrade( trade_type=TradeType.BUY, base_asset=A_AAVE, quote_asset=A_WETH, amount=AssetAmount(FVal('21.131588567541018817')), rate=Price(FVal('0.1435213742524287826717337545')), trade_index=0, swaps=[ AMMSwap( tx_hash='0x67c0e9a0fdd002d0b9d1cca0c8e4ca4d30435bbf57bbf0091396275efaea414b', # noqa: E501 log_index=31, address=string_to_ethereum_address('0x029f388aC4D5C8BfF490550ce0853221030E822b'), # noqa: E501 from_address=string_to_ethereum_address('0x0000000000007F150Bd6f54c40A34d7C3d5e9f56'), # noqa: E501 to_address=string_to_ethereum_address('0x7c90a3cd7Ec80dd2F633ed562480AbbEEd3bE546'), # noqa: E501 timestamp=Timestamp(1607015339), location=Location.BALANCER, token0=A_WETH, token1=A_AAVE, amount0_in=AssetAmount(FVal('3.0328346313504')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('21.131588567541018817')), ), ], ), ]
token1=A_WETH, amount0_in=AssetAmount(FVal('0.01562514')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('0.205421420618533148')), ), ], ).serialize() BALANCER_TEST_ADDR3_EXPECTED_HISTORY_POOL1 = ( BalancerPoolEventsBalance( address=BALANCER_TEST_ADDR3, pool_address_token=BALANCER_TEST_ADDR3_POOL1, profit_loss_amounts=[ AssetAmount(FVal('-0.039312851799093402')), AssetAmount(FVal('0.744372160905819159')), ], usd_profit_loss=FVal('-0.76584117161052920880190053'), events=[ BalancerEvent( tx_hash='0xb9dff9df4e3838c75d354d62c4596d94e5eb8904e07cee07a3b7ffa611c05544', log_index=331, address=BALANCER_TEST_ADDR3, timestamp=Timestamp(1597144247), event_type=BalancerBPTEventType.MINT, pool_address_token=BALANCER_TEST_ADDR3_POOL1, lp_balance=Balance( amount=FVal('0.042569019597126949'), usd_value=FVal('19.779488662371895'), ),
def process_kraken_trades( self, raw_data: List[HistoryBaseEntry], ) -> Tuple[List[Trade], Timestamp]: """ Given a list of history events we process them to create Trade objects. The valid History events type are - Trade - Receive - Spend - Adjustment A pair of receive and spend events can be a trade and kraken uses this kind of event for instant trades and trades made from the phone app. What we do in order to verify that it is a trade is to check if we can find a pair with the same event id. Also in some rare occasions Kraken may forcibly adjust something for you. Example would be delisting of DAO token and forcible exchange to ETH. Returns: - The list of trades processed - The biggest timestamp of all the trades processed May raise: - RemoteError if the pairs couldn't be correctly queried """ trades = [] max_ts = 0 get_attr = operator.attrgetter('event_identifier') adjustments: List[HistoryBaseEntry] = [] # Create a list of lists where each sublist has the events for the same event identifier grouped_events = [ list(g) for k, g in itertools.groupby( sorted(raw_data, key=get_attr), get_attr) ] # noqa: E501 for trade_parts in grouped_events: trade = self.process_kraken_events_for_trade( trade_parts, adjustments) if trade is None: continue trades.append(trade) max_ts = max(max_ts, trade.timestamp) adjustments.sort(key=lambda x: x.timestamp) if len(adjustments) % 2 == 0: for a1, a2 in pairwise(adjustments): if a1.event_subtype is None or a2.event_subtype is None: log.warning( f'Found two kraken adjustment entries without a subtype: {a1} {a2}', ) continue if a1.event_subtype == HistoryEventSubType.SPEND and a2.event_subtype == HistoryEventSubType.RECEIVE: # noqa: E501 spend_event = a1 receive_event = a2 elif a2.event_subtype == HistoryEventSubType.SPEND and a2.event_subtype == HistoryEventSubType.RECEIVE: # noqa: E501 spend_event = a2 receive_event = a1 else: log.warning( f'Found two kraken adjustment with unmatching subtype {a1} {a2}', ) continue rate = Price( abs(receive_event.balance.amount / spend_event.balance.amount)) trade = Trade( timestamp=ts_ms_to_sec(a1.timestamp), location=Location.KRAKEN, base_asset=receive_event.asset, quote_asset=spend_event.asset, trade_type=TradeType.BUY, amount=AssetAmount(receive_event.balance.amount), rate=rate, fee=None, fee_currency=None, link='adjustment' + a1.event_identifier + a2.event_identifier, ) trades.append(trade) else: log.warning( f'Got even number of kraken adjustment historic entries. ' f'Skipping reading them. {adjustments}', ) return trades, Timestamp(max_ts)
def _get_events_graph( self, address: ChecksumEthAddress, start_ts: Timestamp, end_ts: Timestamp, event_type: EventType, ) -> List[LiquidityPoolEvent]: """Get the address' events (mints & burns) querying the AMM's subgraph Each event data is stored in a <LiquidityPoolEvent>. """ address_events: List[LiquidityPoolEvent] = [] if event_type == self.mint_event: query = MINTS_QUERY query_schema = 'mints' elif event_type == self.burn_event: query = BURNS_QUERY query_schema = 'burns' else: log.error( f'Unexpected {self.location} event_type: {event_type}. Skipping events query.', ) return address_events query_id = '0' query_offset = 0 param_types = { '$limit': 'Int!', '$offset': 'Int!', '$address': 'Bytes!', '$start_ts': 'BigInt!', '$end_ts': 'BigInt!', '$id': 'ID!', } param_values = { 'limit': GRAPH_QUERY_LIMIT, 'offset': query_offset, 'address': address.lower(), 'start_ts': str(start_ts), 'end_ts': str(end_ts), 'id': query_id, } querystr = format_query_indentation(query.format()) while True: try: result = self.graph.query( querystr=querystr, param_types=param_types, param_values=param_values, ) except RemoteError as e: self.msg_aggregator.add_error( SUBGRAPH_REMOTE_ERROR_MSG.format(error_msg=str(e), location=self.location), ) raise except AttributeError as e: raise ModuleInitializationFailure( f'{self.location} subgraph remote error') from e result_data = result[query_schema] for event in result_data: token0_ = event['pair']['token0'] token1_ = event['pair']['token1'] try: token0_deserialized = deserialize_ethereum_address( token0_['id']) token1_deserialized = deserialize_ethereum_address( token1_['id']) pool_deserialized = deserialize_ethereum_address( event['pair']['id']) except DeserializationError as e: msg = ( f'Failed to deserialize address involved in liquidity pool event for' f' {self.location}. Token 0: {token0_["id"]}, token 1: {token0_["id"]},' f' pair: {event["pair"]["id"]}.') log.error(msg) raise RemoteError(msg) from e token0 = get_or_create_ethereum_token( userdb=self.database, symbol=token0_['symbol'], ethereum_address=token0_deserialized, name=token0_['name'], decimals=token0_['decimals'], ) token1 = get_or_create_ethereum_token( userdb=self.database, symbol=token1_['symbol'], ethereum_address=token1_deserialized, name=token1_['name'], decimals=int(token1_['decimals']), ) lp_event = LiquidityPoolEvent( tx_hash=event['transaction']['id'], log_index=int(event['logIndex']), address=address, timestamp=Timestamp(int(event['timestamp'])), event_type=event_type, pool_address=pool_deserialized, token0=token0, token1=token1, amount0=AssetAmount(FVal(event['amount0'])), amount1=AssetAmount(FVal(event['amount1'])), usd_price=Price(FVal(event['amountUSD'])), lp_amount=AssetAmount(FVal(event['liquidity'])), ) address_events.append(lp_event) query_id = event['id'] # Check whether an extra request is needed if len(result_data) < GRAPH_QUERY_LIMIT: break # Update pagination step if query_offset == GRAPH_QUERY_SKIP_LIMIT: query_offset = 0 new_query_id = query_id else: query_offset += GRAPH_QUERY_LIMIT new_query_id = '0' param_values = { **param_values, 'id': new_query_id, 'offset': query_offset, } return address_events
def test_query_asset_movements_sandbox( sandbox_kuckoin, inquirer, # pylint: disable=unused-argument ): """Unfortunately the sandbox environment does not support deposits and withdrawals, therefore they must be mocked. Below a list of the movements and their timestamps in ascending mode: Deposits: - deposit 1 - deposit: 1612556651 - deposit 2 - deposit: 1612556652 - deposit 3 - deposit: 1612556653 -> skipped, inner deposit Withdrawals: - withdraw 1: 1612556651 -> skipped, inner withdraw - withdraw 2: 1612556652 - withdraw 3: 1612556656 -> never requested By requesting trades from 1612556651 to 1612556654 and patching the time step as 2s (via MONTHS_IN_SECONDS) we should get back 3 movements. """ deposits_response_1 = (""" { "code":"200000", "data":{ "currentPage":1, "pageSize":2, "totalNum":2, "totalPage":1, "items":[ { "address":"0x5f047b29041bcfdbf0e4478cdfa753a336ba6989", "memo":"5c247c8a03aa677cea2a251d", "amount":1, "fee":0.0001, "currency":"KCS", "isInner":false, "walletTxId":"5bbb57386d99522d9f954c5a", "status":"SUCCESS", "remark":"movement 2 - deposit", "createdAt":1612556652000, "updatedAt":1612556652000 }, { "address":"0x5f047b29041bcfdbf0e4478cdfa753a336ba6989", "memo":"5c247c8a03aa677cea2a251d", "amount":1000, "fee":0.01, "currency":"LINK", "isInner":false, "walletTxId":"5bbb57386d99522d9f954c5b@test", "status":"SUCCESS", "remark":"movement 1 - deposit", "createdAt":1612556651000, "updatedAt":1612556651000 } ] } } """) deposits_response_2 = (""" { "code":"200000", "data":{ "currentPage":1, "pageSize":1, "totalNum":1, "totalPage":1, "items":[ { "address":"1DrT5xUaJ3CBZPDeFR2qdjppM6dzs4rsMt", "memo":"", "currency":"BCHSV", "amount":1, "fee":0.1, "walletTxId":"b893c3ece1b8d7cacb49a39ddd759cf407817f6902f566c443ba16614874ada6", "isInner":true, "status":"SUCCESS", "remark":"movement 4 - deposit", "createdAt":1612556653000, "updatedAt":1612556653000 } ] } } """) withdrawals_response_1 = (""" { "code":"200000", "data":{ "currentPage":1, "pageSize":2, "totalNum":2, "totalPage":1, "items":[ { "id":"5c2dc64e03aa675aa263f1a4", "address":"1DrT5xUaJ3CBZPDeFR2qdjppM6dzs4rsMt", "memo":"", "currency":"BCHSV", "amount":2.5, "fee":0.25, "walletTxId":"b893c3ece1b8d7cacb49a39ddd759cf407817f6902f566c443ba16614874ada4", "isInner":false, "status":"SUCCESS", "remark":"movement 4 - withdraw", "createdAt":1612556652000, "updatedAt":1612556652000 }, { "id":"5c2dc64e03aa675aa263f1a3", "address":"0x5bedb060b8eb8d823e2414d82acce78d38be7fe9", "memo":"", "currency":"ETH", "amount":1, "fee":0.01, "walletTxId":"3e2414d82acce78d38be7fe9", "isInner":true, "status":"SUCCESS", "remark":"movement 3 - withdraw", "createdAt":1612556651000, "updatedAt":1612556651000 } ] } } """) withdrawals_response_2 = (""" { "code":"200000", "data":{ "currentPage":0, "pageSize":0, "totalNum":0, "totalPage":0, "items":[] } } """) withdrawals_response_3 = (""" { "code":"200000", "data":{ "currentPage":1, "pageSize":1, "totalNum":1, "totalPage":1, "items":[ { "id":"5c2dc64e03aa675aa263f1a5", "address":"0x5bedb060b8eb8d823e2414d82acce78d38be7f00", "memo":"", "currency":"KCS", "amount":2.5, "fee":0.25, "walletTxId":"b893c3ece1b8d7cacb49a39ddd759cf407817f6902f566c443ba16614874ada5", "isInner":false, "status":"SUCCESS", "remark":"movement 5 - withdraw", "createdAt":1612556655000, "updatedAt":1612556655000 } ] } } """) expected_asset_movements = [ AssetMovement( location=Location.KUCOIN, category=AssetMovementCategory.DEPOSIT, timestamp=Timestamp(1612556652), address='0x5f047b29041bcfdbf0e4478cdfa753a336ba6989', transaction_id='5bbb57386d99522d9f954c5a', asset=A_KCS, amount=AssetAmount(FVal('1')), fee_asset=A_KCS, fee=Fee(FVal('0.0001')), link='', ), AssetMovement( location=Location.KUCOIN, category=AssetMovementCategory.DEPOSIT, timestamp=Timestamp(1612556651), address='0x5f047b29041bcfdbf0e4478cdfa753a336ba6989', transaction_id='5bbb57386d99522d9f954c5b', asset=A_LINK, amount=AssetAmount(FVal('1000')), fee_asset=A_LINK, fee=Fee(FVal('0.01')), link='', ), AssetMovement( location=Location.KUCOIN, category=AssetMovementCategory.WITHDRAWAL, timestamp=Timestamp(1612556652), address='1DrT5xUaJ3CBZPDeFR2qdjppM6dzs4rsMt', transaction_id= 'b893c3ece1b8d7cacb49a39ddd759cf407817f6902f566c443ba16614874ada4', asset=A_BSV, amount=AssetAmount(FVal('2.5')), fee_asset=A_BSV, fee=Fee(FVal('0.25')), link='5c2dc64e03aa675aa263f1a4', ), ] def get_endpoints_response(): results = [ f'{deposits_response_1}', f'{deposits_response_2}', f'{withdrawals_response_1}', f'{withdrawals_response_2}', # if pagination works as expected and the requesting loop is broken, # the response below won't be processed f'{withdrawals_response_3}', ] for result_ in results: yield result_ def mock_api_query_response(case, options): # pylint: disable=unused-argument return MockResponse(HTTPStatus.OK, next(get_response))
def test_query_trades_sandbox(sandbox_kuckoin, inquirer): # pylint: disable=unused-argument """The sandbox account has 6 trades. Below a list of the trades and their timestamps in ascending mode. - trade 1: 1612556651 -> skipped - trade 2: 1612556693 - trade 3: 1612556765 - trade 4: 1612556765 - trade 5: 1612556765 - trade 6: 1612556794 -> skipped By requesting trades from 1612556693 to 1612556765, the first and last trade should be skipped. """ expected_trades = [ Trade( timestamp=Timestamp(1612556765), location=Location.KUCOIN, base_asset=A_ETH, quote_asset=A_BTC, trade_type=TradeType.BUY, amount=AssetAmount(FVal('0.02934995')), rate=Price(FVal('0.046058')), fee=Fee(FVal('9.4625999797E-7')), fee_currency=A_BTC, link='601da9ddf73c300006194ec6', notes='', ), Trade( timestamp=Timestamp(1612556765), location=Location.KUCOIN, base_asset=A_ETH, quote_asset=A_BTC, trade_type=TradeType.BUY, amount=AssetAmount(FVal('0.02')), rate=Price(FVal('0.04561')), fee=Fee(FVal('6.3854E-7')), fee_currency=A_BTC, link='601da9ddf73c300006194ec5', notes='', ), Trade( timestamp=Timestamp(1612556765), location=Location.KUCOIN, base_asset=A_ETH, quote_asset=A_BTC, trade_type=TradeType.BUY, amount=AssetAmount(FVal('0.06')), rate=Price(FVal('0.0456')), fee=Fee(FVal('0.0000019152')), fee_currency=A_BTC, link='601da9ddf73c300006194ec4', notes='', ), Trade( timestamp=Timestamp(1612556693), location=Location.KUCOIN, base_asset=A_BTC, quote_asset=A_USDT, trade_type=TradeType.SELL, amount=AssetAmount(FVal('0.0013')), rate=Price(FVal('37624.4')), fee=Fee(FVal('0.034238204')), fee_currency=A_USDT, link='601da995e0ee8b00063a075c', notes='', ), ] trades, _ = sandbox_kuckoin.query_online_trade_history( start_ts=Timestamp(1612556693), end_ts=Timestamp(1612556765), ) assert trades == expected_trades
def _read_subgraph_trades( self, address: ChecksumEthAddress, start_ts: Timestamp, end_ts: Timestamp, ) -> List[AMMTrade]: """Get the address' trades data querying the AMM subgraph Each trade (swap) instantiates an <AMMTrade>. The trade pair (i.e. BASE_QUOTE) is determined by `reserve0_reserve1`. Translated to AMM lingo: Trade type BUY: - `asset1In` (QUOTE, reserve1) is gt 0. - `asset0Out` (BASE, reserve0) is gt 0. Trade type SELL: - `asset0In` (BASE, reserve0) is gt 0. - `asset1Out` (QUOTE, reserve1) is gt 0. May raise - RemoteError """ trades: List[AMMTrade] = [] query_id = '0' query_offset = 0 param_types = { '$limit': 'Int!', '$offset': 'Int!', '$address': 'Bytes!', '$start_ts': 'BigInt!', '$end_ts': 'BigInt!', '$id': 'ID!', } param_values = { 'limit': GRAPH_QUERY_LIMIT, 'offset': 0, 'address': address.lower(), 'start_ts': str(start_ts), 'end_ts': str(end_ts), 'id': query_id, } querystr = format_query_indentation(self.swaps_query.format()) while True: try: result = self.graph.query( querystr=querystr, param_types=param_types, param_values=param_values, ) except RemoteError as e: self.msg_aggregator.add_error( SUBGRAPH_REMOTE_ERROR_MSG.format(error_msg=str(e), location=self.location), ) raise for entry in result['swaps']: swaps = [] try: for swap in entry['transaction']['swaps']: timestamp = swap['timestamp'] swap_token0 = swap['pair']['token0'] swap_token1 = swap['pair']['token1'] try: token0_deserialized = deserialize_ethereum_address( swap_token0['id']) token1_deserialized = deserialize_ethereum_address( swap_token1['id']) from_address_deserialized = deserialize_ethereum_address( swap['sender']) # noqa to_address_deserialized = deserialize_ethereum_address( swap['to']) except DeserializationError: msg = ( f'Failed to deserialize addresses in trade from {self.location} graph' # noqa f' with token 0: {swap_token0["id"]}, token 1: {swap_token1["id"]}, ' # noqa f'swap sender: {swap["sender"]}, swap receiver {swap["to"]}' ) log.error(msg) continue token0 = get_or_create_ethereum_token( userdb=self.database, symbol=swap_token0['symbol'], ethereum_address=token0_deserialized, name=swap_token0['name'], decimals=swap_token0['decimals'], ) token1 = get_or_create_ethereum_token( userdb=self.database, symbol=swap_token1['symbol'], ethereum_address=token1_deserialized, name=swap_token1['name'], decimals=int(swap_token1['decimals']), ) try: amount0_in = FVal(swap['amount0In']) amount1_in = FVal(swap['amount1In']) amount0_out = FVal(swap['amount0Out']) amount1_out = FVal(swap['amount1Out']) except ValueError as e: log.error( f'Failed to read amounts in {self.location} swap {str(swap)}. ' f'{str(e)}.', ) continue swaps.append( AMMSwap( tx_hash=swap['id'].split('-')[0], log_index=int(swap['logIndex']), address=address, from_address=from_address_deserialized, to_address=to_address_deserialized, timestamp=Timestamp(int(timestamp)), location=self.location, token0=token0, token1=token1, amount0_in=AssetAmount(amount0_in), amount1_in=AssetAmount(amount1_in), amount0_out=AssetAmount(amount0_out), amount1_out=AssetAmount(amount1_out), )) query_id = entry['id'] except KeyError as e: log.error( f'Failed to read trade in {self.location} swap {str(entry)}. ' f'{str(e)}.', ) continue # with the new logic the list of swaps can be empty, in that case don't try # to make trades from the swaps if len(swaps) == 0: continue # Now that we got all swaps for a transaction, create the trade object trades.extend(self._tx_swaps_to_trades(swaps)) # Check whether an extra request is needed if len(result['swaps']) < GRAPH_QUERY_LIMIT: break # Update pagination step if query_offset == GRAPH_QUERY_SKIP_LIMIT: query_offset = 0 new_query_id = query_id else: query_offset += GRAPH_QUERY_LIMIT new_query_id = '0' param_values = { **param_values, 'id': new_query_id, 'offset': query_offset, } return trades
SushiswapPoolEventsBalance( address=string_to_ethereum_address(TEST_EVENTS_ADDRESS_1), pool_address=string_to_ethereum_address("0xC3f279090a47e80990Fe3a9c30d24Cb117EF91a8"), token0=EthereumToken('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'), token1=EthereumToken('0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF'), events=[ SushiswapPoolEvent( tx_hash='0xb226ddb8cbb286a7a998a35263ad258110eed5f923488f03a8d890572cd4608e', log_index=137, address=string_to_ethereum_address(TEST_EVENTS_ADDRESS_1), timestamp=Timestamp(1627401170), event_type=EventType.MINT_SUSHISWAP, pool_address=string_to_ethereum_address("0xC3f279090a47e80990Fe3a9c30d24Cb117EF91a8"), # noqa: E501 token0=EthereumToken('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'), token1=EthereumToken('0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF'), amount0=AssetAmount(FVal('0.192426688761441618')), amount1=AssetAmount(FVal('1.498665931466140813')), usd_price=Price(FVal('874.684787927721190125529172850727')), lp_amount=AssetAmount(FVal('0.023925092583833892')), ), ], profit_loss0=AssetAmount(FVal('-0.192426688761441618')), profit_loss1=AssetAmount(FVal('-1.498665931466140813')), usd_profit_loss=Price(FVal('-874.6847879277211901255291729')), ), ] @pytest.mark.parametrize('ethereum_accounts', [[TEST_EVENTS_ADDRESS_1]]) @pytest.mark.parametrize('ethereum_modules', [['sushiswap']]) @pytest.mark.parametrize('start_with_valid_premium', [True])
def test_query_owned_assets(data_dir, username): """Test the get_owned_assets with also an unknown asset in the DB""" msg_aggregator = MessagesAggregator() data = DataHandler(data_dir, msg_aggregator) data.unlock(username, '123', create_new=True) balances = deepcopy(asset_balances) balances.extend([ DBAssetBalance( category=BalanceType.ASSET, time=Timestamp(1488326400), asset=A_BTC, amount='1', usd_value='1222.66', ), DBAssetBalance( category=BalanceType.ASSET, time=Timestamp(1489326500), asset=A_XMR, amount='2', usd_value='33.8', ), ]) data.db.add_multiple_balances(balances) data.db.conn.commit() # also make sure that assets from trades are included data.db.add_trades([ Trade( timestamp=Timestamp(1), location=Location.EXTERNAL, base_asset=A_ETH, quote_asset=A_BTC, trade_type=TradeType.BUY, amount=AssetAmount(FVal(1)), rate=Price(FVal(1)), fee=Fee(FVal('0.1')), fee_currency=A_BTC, link='', notes='', ), Trade( timestamp=Timestamp(99), location=Location.EXTERNAL, base_asset=A_ETH, quote_asset=A_BTC, trade_type=TradeType.BUY, amount=AssetAmount(FVal(2)), rate=Price(FVal(1)), fee=Fee(FVal('0.1')), fee_currency=A_BTC, link='', notes='', ), Trade( timestamp=Timestamp(1), location=Location.EXTERNAL, base_asset=A_SDC, quote_asset=A_SDT2, trade_type=TradeType.BUY, amount=AssetAmount(FVal(1)), rate=Price(FVal(1)), fee=Fee(FVal('0.1')), fee_currency=A_BTC, link='', notes='', ), Trade( timestamp=Timestamp(1), location=Location.EXTERNAL, base_asset=A_SUSHI, quote_asset=A_1INCH, trade_type=TradeType.BUY, amount=AssetAmount(FVal(1)), rate=Price(FVal(1)), fee=Fee(FVal('0.1')), fee_currency=A_BTC, link='', notes='', ), Trade( timestamp=Timestamp(3), location=Location.EXTERNAL, base_asset=A_SUSHI, quote_asset=A_1INCH, trade_type=TradeType.BUY, amount=AssetAmount(FVal(2)), rate=Price(FVal(1)), fee=Fee(FVal('0.1')), fee_currency=A_BTC, link='', notes='', ), ]) assets_list = data.db.query_owned_assets() assert set(assets_list) == { A_USD, A_ETH, A_BTC, A_XMR, A_SDC, A_SDT2, A_SUSHI, A_1INCH } # noqa: E501 assert all(isinstance(x, Asset) for x in assets_list) warnings = data.db.msg_aggregator.consume_warnings() assert len(warnings) == 0
def process_kraken_events_for_trade( self, trade_parts: List[HistoryBaseEntry], adjustments: List[HistoryBaseEntry], ) -> Optional[Trade]: """Processes events from trade parts to a trade. If it's an adjustment adds it to a separate list""" if trade_parts[0].event_type == HistoryEventType.ADJUSTMENT: adjustments.append(trade_parts[0]) return None # skip as they don't have same refid event_id = trade_parts[0].event_identifier is_spend_receive = False trade_assets = [] spend_part, receive_part, fee_part, kfee_part = None, None, None, None for trade_part in trade_parts: if trade_part.event_type == HistoryEventType.RECEIVE: is_spend_receive = True receive_part = trade_part elif trade_part.event_type == HistoryEventType.SPEND: if trade_part.event_subtype == HistoryEventSubType.FEE: fee_part = trade_part else: is_spend_receive = True spend_part = trade_part elif trade_part.event_type == HistoryEventType.TRADE: if trade_part.event_subtype == HistoryEventSubType.FEE: fee_part = trade_part elif trade_part.asset == A_KFEE: kfee_part = trade_part elif trade_part.balance.amount < ZERO: spend_part = trade_part else: receive_part = trade_part if (trade_part.balance.amount != ZERO and trade_part.event_subtype != HistoryEventSubType.FEE): trade_assets.append(trade_part.asset) if is_spend_receive and len(trade_parts) < 2: log.warning( f'Found kraken spend/receive events {event_id} with ' f'less than 2 parts. {trade_parts}', ) self.msg_aggregator.add_warning( f'Found kraken spend/receive events {event_id} with ' f'less than 2 parts. Skipping...', ) return None timestamp = ts_ms_to_sec(trade_parts[0].timestamp) exchange_uuid = (str(event_id) + str(timestamp)) if len(trade_assets) != 2: # This can happen some times (for lefteris 5 times since start of kraken usage) # when the other part of a trade is so small it's 0. So it's either a # receive event with no counterpart or a spend event with no counterpart. # This happens for really really small amounts. So we add rate 0 trades if spend_part is not None: base_asset = spend_part.asset trade_type = TradeType.SELL amount = spend_part.balance.amount * -1 elif receive_part is not None: base_asset = receive_part.asset trade_type = TradeType.BUY amount = receive_part.balance.amount else: log.warning( f'Found historic trade entries with no counterpart {trade_parts}' ) return None trade = Trade( timestamp=timestamp, location=Location.KRAKEN, base_asset=base_asset, quote_asset=A_USD, # whatever trade_type=trade_type, amount=AssetAmount(amount), rate=Price(ZERO), fee=None, fee_currency=None, link=exchange_uuid, ) return trade if spend_part is None or receive_part is None: log.error( f'Failed to process {event_id}. Couldnt find spend/receive parts {trade_parts}', ) self.msg_aggregator.add_error( f'Failed to read trades for event {event_id}. ' f'More details are available at the logs', ) return None spend_asset = spend_part.asset receive_asset = receive_part.asset if spend_asset.is_fiat() or trade_parts[0] == receive_part: trade_type = TradeType.BUY base_asset = receive_asset quote_asset = spend_asset amount = receive_part.balance.amount if amount == ZERO: self.msg_aggregator.add_warning( f'Rate for kraken trade couldnt be calculated. Base amount is ZERO ' f'for event {event_id}. Skipping event', ) return None rate = Price((spend_part.balance.amount / amount) * -1) else: trade_type = TradeType.SELL base_asset = spend_asset quote_asset = receive_asset amount = -1 * spend_part.balance.amount if amount == ZERO: self.msg_aggregator.add_warning( f'Rate for kraken trade couldnt be calculated. Base amount is ZERO ' f'for event {event_id}. Skipping event', ) return None rate = Price((receive_part.balance.amount / amount)) # If kfee was found we use it as the fee for the trade if kfee_part is not None and fee_part is None: fee = Fee(kfee_part.balance.amount) fee_asset = A_KFEE elif (None, None) == (fee_part, kfee_part): fee = None fee_asset = None elif fee_part is not None: fee = Fee(fee_part.balance.amount) fee_asset = fee_part.asset trade = Trade( timestamp=timestamp, location=Location.KRAKEN, base_asset=base_asset, quote_asset=quote_asset, trade_type=trade_type, amount=AssetAmount(amount), rate=rate, fee=fee, fee_currency=fee_asset, link=exchange_uuid, ) return trade
def get_expected_trades(): """Function so no price (unknown) assets can be resolved only when existing in the DB""" address = string_to_ethereum_address('0x63BC843b9640c4D79d6aE0105bc39F773172d121') return [AMMTrade( trade_type=TradeType.BUY, base_asset=EthereumToken('0xdAC17F958D2ee523a2206206994597C13D831ec7'), quote_asset=EthereumToken('0x4b4D2e899658FB59b1D518b68fe836B100ee8958'), amount=AssetAmount(FVal('796.857811')), rate=Price(FVal('0.0008323741932057006980885326353')), trade_index=0, swaps=[AMMSwap( tx_hash='0x962d904d75c751fbff316f7a2ed280bd93241d5088d747a4f26fe7437813512f', log_index=141, address=address, from_address=string_to_ethereum_address('0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'), to_address=string_to_ethereum_address('0x63BC843b9640c4D79d6aE0105bc39F773172d121'), timestamp=Timestamp(1609308616), location=Location.SUSHISWAP, token0=EthereumToken('0x4b4D2e899658FB59b1D518b68fe836B100ee8958'), token1=EthereumToken('0xdAC17F958D2ee523a2206206994597C13D831ec7'), amount0_in=AssetAmount(FVal('0.663283877530785731')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('796.857811')), )], ), AMMTrade( trade_type=TradeType.BUY, base_asset=EthereumToken('0x368B3a58B5f49392e5C9E4C998cb0bB966752E51'), quote_asset=EthereumToken('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'), amount=AssetAmount(FVal('2.223721994593087248')), rate=Price(FVal('1124.241252314216598775470692')), trade_index=0, swaps=[AMMSwap( tx_hash='0x90f68af0ebbbb8d4938a4fbd07a70862e806124abd907d1225f25a10afda0180', log_index=26, address=address, from_address=string_to_ethereum_address('0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'), to_address=string_to_ethereum_address('0x06da0fd433C1A5d7a4faa01111c044910A184553'), timestamp=Timestamp(1609303966), location=Location.SUSHISWAP, token0=EthereumToken('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'), token1=EthereumToken('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'), amount0_in=AssetAmount(FVal('2500')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('3.410623314913014194')), ), AMMSwap( tx_hash='0x90f68af0ebbbb8d4938a4fbd07a70862e806124abd907d1225f25a10afda0180', log_index=29, address=address, from_address=string_to_ethereum_address('0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'), to_address=string_to_ethereum_address('0xC9cB53B48A2f3A9e75982685644c1870F1405CCb'), timestamp=Timestamp(1609303966), location=Location.SUSHISWAP, token0=EthereumToken('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'), token1=EthereumToken('0xdAC17F958D2ee523a2206206994597C13D831ec7'), amount0_in=AssetAmount(FVal('3.410623314913014194')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('2474.601464')), ), AMMSwap( tx_hash='0x90f68af0ebbbb8d4938a4fbd07a70862e806124abd907d1225f25a10afda0180', log_index=32, address=address, from_address=string_to_ethereum_address('0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'), to_address=string_to_ethereum_address('0x63BC843b9640c4D79d6aE0105bc39F773172d121'), timestamp=Timestamp(1609303966), location=Location.SUSHISWAP, token0=EthereumToken('0x368B3a58B5f49392e5C9E4C998cb0bB966752E51'), token1=EthereumToken('0xdAC17F958D2ee523a2206206994597C13D831ec7'), amount0_in=AssetAmount(ZERO), amount1_in=AssetAmount(FVal('2474.601464')), amount0_out=AssetAmount(FVal('2.223721994593087248')), amount1_out=AssetAmount(ZERO), )], ), AMMTrade( trade_type=TradeType.BUY, base_asset=EthereumToken('0x368B3a58B5f49392e5C9E4C998cb0bB966752E51'), quote_asset=EthereumToken('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'), amount=AssetAmount(FVal('1.925851508322134245')), rate=Price(FVal('1012.539124420295894184748806')), trade_index=0, swaps=[AMMSwap( tx_hash='0xa54bf4c68d435e3c8f432fd7e62b7f8aca497a831a3d3fca305a954484ddd7b2', log_index=205, address=address, from_address=string_to_ethereum_address('0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'), to_address=string_to_ethereum_address('0x06da0fd433C1A5d7a4faa01111c044910A184553'), timestamp=Timestamp(1609301469), location=Location.SUSHISWAP, token0=EthereumToken('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'), token1=EthereumToken('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'), amount0_in=AssetAmount(FVal('1950')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('2.6455727132446468')), ), AMMSwap( tx_hash='0xa54bf4c68d435e3c8f432fd7e62b7f8aca497a831a3d3fca305a954484ddd7b2', log_index=208, address=address, from_address=string_to_ethereum_address('0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'), to_address=string_to_ethereum_address('0xC9cB53B48A2f3A9e75982685644c1870F1405CCb'), timestamp=Timestamp(1609301469), location=Location.SUSHISWAP, token0=EthereumToken('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'), token1=EthereumToken('0xdAC17F958D2ee523a2206206994597C13D831ec7'), amount0_in=AssetAmount(FVal('2.6455727132446468')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('1936.810111')), ), AMMSwap( tx_hash='0xa54bf4c68d435e3c8f432fd7e62b7f8aca497a831a3d3fca305a954484ddd7b2', log_index=211, address=address, from_address=string_to_ethereum_address('0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'), to_address=string_to_ethereum_address('0x63BC843b9640c4D79d6aE0105bc39F773172d121'), timestamp=Timestamp(1609301469), location=Location.SUSHISWAP, token0=EthereumToken('0x368B3a58B5f49392e5C9E4C998cb0bB966752E51'), token1=EthereumToken('0xdAC17F958D2ee523a2206206994597C13D831ec7'), amount0_in=AssetAmount(ZERO), amount1_in=AssetAmount(FVal('1936.810111')), amount0_out=AssetAmount(FVal('1.925851508322134245')), amount1_out=AssetAmount(ZERO), )], ), AMMTrade( trade_type=TradeType.BUY, base_asset=EthereumToken('0xdAC17F958D2ee523a2206206994597C13D831ec7'), quote_asset=EthereumToken('0x368B3a58B5f49392e5C9E4C998cb0bB966752E51'), amount=AssetAmount(FVal('951.611472')), rate=Price(FVal('0.001050849038104071952592013309')), trade_index=0, swaps=[AMMSwap( tx_hash='0xb3cea8179e8bc349661f265937187403ae4914c108d118889d026bac1fbec4e9', log_index=9, address=address, from_address=string_to_ethereum_address('0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'), to_address=string_to_ethereum_address('0x63BC843b9640c4D79d6aE0105bc39F773172d121'), timestamp=Timestamp(1609296759), location=Location.SUSHISWAP, token0=EthereumToken('0x368B3a58B5f49392e5C9E4C998cb0bB966752E51'), token1=EthereumToken('0xdAC17F958D2ee523a2206206994597C13D831ec7'), amount0_in=AssetAmount(FVal('1')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('951.611472')), )], ), AMMTrade( trade_type=TradeType.BUY, base_asset=EthereumToken('0x368B3a58B5f49392e5C9E4C998cb0bB966752E51'), quote_asset=EthereumToken('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'), amount=AssetAmount(FVal('1')), rate=Price(FVal('511.20342')), trade_index=0, swaps=[AMMSwap( tx_hash='0x0ccec8fd15c8d3ab923ee4a2406778f22769e74da20b19a35e614bfead6bab8d', log_index=242, address=address, from_address=string_to_ethereum_address('0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'), to_address=string_to_ethereum_address('0x06da0fd433C1A5d7a4faa01111c044910A184553'), timestamp=Timestamp(1609294923), location=Location.SUSHISWAP, token0=EthereumToken('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'), token1=EthereumToken('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'), amount0_in=AssetAmount(FVal('511.20342')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('0.690577933591789501')), ), AMMSwap( tx_hash='0x0ccec8fd15c8d3ab923ee4a2406778f22769e74da20b19a35e614bfead6bab8d', log_index=245, address=address, from_address=string_to_ethereum_address('0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'), to_address=string_to_ethereum_address('0xC9cB53B48A2f3A9e75982685644c1870F1405CCb'), timestamp=Timestamp(1609294923), location=Location.SUSHISWAP, token0=EthereumToken('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'), token1=EthereumToken('0xdAC17F958D2ee523a2206206994597C13D831ec7'), amount0_in=AssetAmount(FVal('0.690577933591789501')), amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=AssetAmount(FVal('506.399839')), ), AMMSwap( tx_hash='0x0ccec8fd15c8d3ab923ee4a2406778f22769e74da20b19a35e614bfead6bab8d', log_index=248, address=address, from_address=string_to_ethereum_address('0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'), to_address=string_to_ethereum_address('0x63BC843b9640c4D79d6aE0105bc39F773172d121'), timestamp=Timestamp(1609294923), location=Location.SUSHISWAP, token0=EthereumToken('0x368B3a58B5f49392e5C9E4C998cb0bB966752E51'), token1=EthereumToken('0xdAC17F958D2ee523a2206206994597C13D831ec7'), amount0_in=AssetAmount(ZERO), amount1_in=AssetAmount(FVal('506.399839')), amount0_out=AssetAmount(FVal('1')), amount1_out=AssetAmount(ZERO), )], )]