def choose_pair( self, timestamp: Timestamp, price_query: Callable[[Asset, Asset, Timestamp], FVal], ) -> TradePair: """Choose a random pair to trade from the available pairs at the selected timestamp""" choices = set(self._symbols_to_pair.keys()) found = False while len(choices) != 0: pair = random.choice(tuple(choices)) choices.remove(pair) binance_pair = self._symbols_to_pair[pair] bbase = binance_pair.binance_base_asset bquote = binance_pair.binance_quote_asset try: base = asset_from_binance(bbase) quote = asset_from_binance(bquote) except UnsupportedAsset: continue if bbase in self.balances_dict or bquote in self.balances_dict: if bbase in DISALLOWED_ASSETS or bquote in DISALLOWED_ASSETS: continue # Before choosing make sure that at the selected timestamp both of # the pair assets exist (had a price) if not assets_exist_at_time(base, quote, timestamp, price_query): continue found = True break if not found: raise ValueError('Could not find a pair to trade with the current funds') return trade_pair_from_assets(base, quote)
def test_binance_assets_are_known( database, inquirer, # pylint: disable=unused-argument ): # use a real binance instance so that we always get the latest data binance = Binance( api_key=make_api_key(), secret=make_api_secret(), database=database, msg_aggregator=MessagesAggregator(), ) mapping = binance.symbols_to_pair binance_assets = set() for _, pair in mapping.items(): binance_assets.add(pair.binance_base_asset) binance_assets.add(pair.binance_quote_asset) sorted_assets = sorted(binance_assets) for binance_asset in sorted_assets: try: _ = asset_from_binance(binance_asset) except UnsupportedAsset: assert binance_asset in UNSUPPORTED_BINANCE_ASSETS except UnknownAsset as e: test_warnings.warn( UserWarning( f'Found unknown asset {e.asset_name} in Binance. Support for it has to be added', )) # also check that lending assets are properly processed assert asset_from_binance('LDBNB') == Asset('BNB') assert asset_from_binance('LDUSDT') == Asset('USDT')
def trade_from_binance( binance_trade: Dict, binance_symbols_to_pair: Dict[str, BinancePair], ) -> Trade: """Turn a binance trade returned from trade history to our common trade history format From the official binance api docs (01/09/18): https://github.com/binance-exchange/binance-official-api-docs/blob/62ff32d27bb32d9cc74d63d547c286bb3c9707ef/rest-api.md#terminology base asset refers to the asset that is the quantity of a symbol. quote asset refers to the asset that is the price of a symbol. Can throw UnsupportedAsset due to asset_from_binance """ amount = FVal(binance_trade['qty']) rate = FVal(binance_trade['price']) binance_pair = binance_symbols_to_pair[binance_trade['symbol']] timestamp = binance_trade['time'] base_asset = asset_from_binance(binance_pair.binance_base_asset) quote_asset = asset_from_binance(binance_pair.binance_quote_asset) if binance_trade['isBuyer']: order_type = TradeType.BUY # e.g. in RDNETH we buy RDN by paying ETH else: order_type = TradeType.SELL fee_currency = asset_from_binance(binance_trade['commissionAsset']) fee = FVal(binance_trade['commission']) log.debug( 'Processing binance Trade', sensitive_log=True, amount=amount, rate=rate, timestamp=timestamp, pair=binance_trade['symbol'], base_asset=base_asset, quote=quote_asset, order_type=order_type, commision_asset=binance_trade['commissionAsset'], fee=fee, ) return Trade( timestamp=timestamp, location='binance', pair=trade_pair_from_assets(base_asset, quote_asset), trade_type=order_type, amount=amount, rate=rate, fee=fee, fee_currency=fee_currency, )
def deserialize_from_db(cls, entry: BINANCE_PAIR_DB_TUPLE) -> 'BinancePair': """Create a BinancePair from data in the database. May raise: - DeserializationError - UnsupportedAsset - UnknownAsset """ return BinancePair( symbol=entry[0], base_asset=asset_from_binance(entry[1]), quote_asset=asset_from_binance(entry[2]), location=Location.deserialize_from_db(entry[3]), )
def test_binance_assets_are_known( database, inquirer, # pylint: disable=unused-argument ): # use a real binance instance so that we always get the latest data binance = Binance( name='binance1', api_key=make_api_key(), secret=make_api_secret(), database=database, msg_aggregator=MessagesAggregator(), uri=BINANCEUS_BASE_URL, ) mapping = binance.symbols_to_pair binance_assets = set() for _, pair in mapping.items(): binance_assets.add(pair.binance_base_asset) binance_assets.add(pair.binance_quote_asset) sorted_assets = sorted(binance_assets) for binance_asset in sorted_assets: try: _ = asset_from_binance(binance_asset) except UnsupportedAsset: assert binance_asset in UNSUPPORTED_BINANCE_ASSETS except UnknownAsset as e: test_warnings.warn( UserWarning( f'Found unknown asset {e.asset_name} in binanceus. ' f'Support for it has to be added', ))
def _query_margined_futures_balances( self, api_type: Literal['fapi', 'dapi'], balances: Dict, ) -> Dict: try: response = self.api_query_list(api_type, 'balance') except BinancePermissionError as e: log.warning( f'Insufficient permission to query {self.name} {api_type} balances.' f'Skipping query. Response details: {str(e)}', ) return balances try: for entry in response: amount = FVal(entry['balance']) if amount == ZERO: continue try: asset = asset_from_binance(entry['asset']) except UnsupportedAsset as e: self.msg_aggregator.add_warning( f'Found unsupported {self.name} asset {e.asset_name}. ' f'Ignoring its margined futures balance query.', ) continue except UnknownAsset as e: self.msg_aggregator.add_warning( f'Found unknown {self.name} asset {e.asset_name}. ' f'Ignoring its margined futures balance query.', ) continue except DeserializationError: self.msg_aggregator.add_error( f'Found {self.name} asset with non-string type ' f'{type(entry["asset"])}. Ignoring its margined futures balance query.', ) continue try: usd_price = Inquirer().find_usd_price(asset) except RemoteError as e: self.msg_aggregator.add_error( f'Error processing {self.name} balance entry due to inability to ' f'query USD price: {str(e)}. Skipping margined futures balance entry', ) continue balance = Balance(amount=amount, usd_value=amount * usd_price) if asset not in balances: balances[asset] = balance.to_dict() else: balances[asset]['amount'] += balance.amount balances[asset]['usd_value'] += balance.usd_value except KeyError as e: self.msg_aggregator.add_error( f'At {self.name} margined futures balance query did not find ' f'expected key {str(e)}. Skipping margined futures query...', ) return balances
def _query_cross_collateral_futures_balances( self, balances: DefaultDict[Asset, Balance], ) -> DefaultDict[Asset, Balance]: """Queries binance collateral future balances and if any found adds them to `balances` May raise: - RemoteError """ futures_response = self.api_query_dict('sapi', 'futures/loan/wallet') try: cross_collaterals = futures_response['crossCollaterals'] for entry in cross_collaterals: amount = deserialize_asset_amount(entry['locked']) if amount == ZERO: continue try: asset = asset_from_binance(entry['collateralCoin']) except UnsupportedAsset as e: self.msg_aggregator.add_warning( f'Found unsupported {self.name} asset {e.asset_name}. ' f'Ignoring its futures balance query.', ) continue except UnknownAsset as e: self.msg_aggregator.add_warning( f'Found unknown {self.name} asset {e.asset_name}. ' f'Ignoring its futures balance query.', ) continue except DeserializationError: self.msg_aggregator.add_error( f'Found {self.name} asset with non-string type ' f'{type(entry["asset"])}. Ignoring its futures balance query.', ) continue try: usd_price = Inquirer().find_usd_price(asset) except RemoteError as e: self.msg_aggregator.add_error( f'Error processing {self.name} balance entry due to inability to ' f'query USD price: {str(e)}. Skipping balance entry', ) continue balances[asset] += Balance( amount=amount, usd_value=amount * usd_price, ) except KeyError as e: self.msg_aggregator.add_error( f'At {self.name} futures balance query did not find expected key ' f'{str(e)}. Skipping futures query...', ) return balances
def _deserialize_asset_movement( self, raw_data: Dict[str, Any]) -> Optional[AssetMovement]: """Processes a single deposit/withdrawal from binance and deserializes it Can log error/warning and return None if something went wrong at deserialization """ try: if 'insertTime' in raw_data: category = AssetMovementCategory.DEPOSIT time_key = 'insertTime' fee = Fee(ZERO) else: category = AssetMovementCategory.WITHDRAWAL time_key = 'applyTime' fee = Fee(deserialize_asset_amount(raw_data['transactionFee'])) timestamp = deserialize_timestamp_from_binance(raw_data[time_key]) asset = asset_from_binance(raw_data['asset']) tx_id = get_key_if_has_val(raw_data, 'txId') internal_id = get_key_if_has_val(raw_data, 'id') link_str = str(internal_id) if internal_id else str( tx_id) if tx_id else '' return AssetMovement( location=self.location, category=category, address=deserialize_asset_movement_address( raw_data, 'address', asset), transaction_id=tx_id, timestamp=timestamp, asset=asset, amount=deserialize_asset_amount_force_positive( raw_data['amount']), fee_asset=asset, fee=fee, link=link_str, ) except UnknownAsset as e: self.msg_aggregator.add_warning( f'Found {str(self.location)} deposit/withdrawal with unknown asset ' f'{e.asset_name}. Ignoring it.', ) except UnsupportedAsset as e: self.msg_aggregator.add_warning( f'Found {str(self.location)} deposit/withdrawal with unsupported asset ' f'{e.asset_name}. Ignoring it.', ) except (DeserializationError, KeyError) as e: msg = str(e) if isinstance(e, KeyError): msg = f'Missing key entry for {msg}.' self.msg_aggregator.add_error( f'Error processing a {str(self.location)} deposit/withdrawal. Check logs ' f'for details. Ignoring it.', ) log.error( f'Error processing a {str(self.location)} deposit/withdrawal', asset_movement=raw_data, error=msg, ) return None
def query_balances(self) -> Tuple[Optional[dict], str]: self.first_connection() try: # account data returns a dict as per binance docs account_data = self.api_query_dict('account') except RemoteError as e: msg = ('Binance API request failed. Could not reach binance due ' 'to {}'.format(e)) log.error(msg) return None, msg returned_balances = {} for entry in account_data['balances']: amount = entry['free'] + entry['locked'] if amount == FVal(0): continue try: asset = asset_from_binance(entry['asset']) except UnsupportedAsset as e: self.msg_aggregator.add_warning( f'Found unsupported binance asset {e.asset_name}. ' f' Ignoring its balance query.', ) continue except UnknownAsset as e: self.msg_aggregator.add_warning( f'Found unknown binance asset {e.asset_name}. ' f' Ignoring its balance query.', ) continue except DeserializationError: self.msg_aggregator.add_error( f'Found binance asset with non-string type {type(entry["asset"])}. ' f' Ignoring its balance query.', ) continue try: usd_price = Inquirer().find_usd_price(asset) except RemoteError as e: self.msg_aggregator.add_error( f'Error processing binance balance entry due to inability to ' f'query USD price: {str(e)}. Skipping balance entry', ) continue balance = {} balance['amount'] = amount balance['usd_value'] = FVal(amount * usd_price) returned_balances[asset] = balance log.debug( 'binance balance query result', sensitive_log=True, asset=asset, amount=amount, usd_value=balance['usd_value'], ) return returned_balances, ''
def _deserialize_asset_movement( self, raw_data: Dict[str, Any]) -> Optional[AssetMovement]: """Processes a single deposit/withdrawal from binance and deserializes it Can log error/warning and return None if something went wrong at deserialization """ try: if 'insertTime' in raw_data: category = AssetMovementCategory.DEPOSIT time_key = 'insertTime' else: category = AssetMovementCategory.WITHDRAWAL time_key = 'applyTime' timestamp = deserialize_timestamp_from_binance(raw_data[time_key]) asset = asset_from_binance(raw_data['asset']) location = Location.BINANCE if self.name == str( Location.BINANCE) else Location.BINANCE_US # noqa: E501 return AssetMovement( location=location, category=category, address=deserialize_asset_movement_address( raw_data, 'address', asset), transaction_id=get_key_if_has_val(raw_data, 'txId'), timestamp=timestamp, asset=asset, amount=deserialize_asset_amount_force_positive( raw_data['amount']), fee_asset=asset, # Binance does not include withdrawal fees neither in the API nor in their UI fee=Fee(ZERO), link=str(raw_data['txId']), ) except UnknownAsset as e: self.msg_aggregator.add_warning( f'Found {self.name} deposit/withdrawal with unknown asset ' f'{e.asset_name}. Ignoring it.', ) except UnsupportedAsset as e: self.msg_aggregator.add_warning( f'Found {self.name} deposit/withdrawal with unsupported asset ' f'{e.asset_name}. Ignoring it.', ) except (DeserializationError, KeyError) as e: msg = str(e) if isinstance(e, KeyError): msg = f'Missing key entry for {msg}.' self.msg_aggregator.add_error( f'Error processing a {self.name} deposit/withdrawal. Check logs ' f'for details. Ignoring it.', ) log.error( f'Error processing a {self.name} deposit_withdrawal', asset_movement=raw_data, error=msg, ) return None
def _query_lending_balances( self, balances: DefaultDict[Asset, Balance], ) -> DefaultDict[Asset, Balance]: """Queries binance lending balances and if any found adds them to `balances` May raise: - RemoteError """ data = self.api_query_dict('sapi', 'lending/union/account') positions = data.get('positionAmountVos', None) if positions is None: raise RemoteError( f'Could not find key positionAmountVos in lending account data ' f'{data} returned by {self.name}.', ) for entry in positions: try: amount = deserialize_asset_amount(entry['amount']) if amount == ZERO: continue asset = asset_from_binance(entry['asset']) except UnsupportedAsset as e: self.msg_aggregator.add_warning( f'Found unsupported {self.name} asset {e.asset_name}. ' f'Ignoring its lending balance query.', ) continue except UnknownAsset as e: self.msg_aggregator.add_warning( f'Found unknown {self.name} asset {e.asset_name}. ' f'Ignoring its lending balance query.', ) continue except (DeserializationError, KeyError) as e: msg = str(e) if isinstance(e, KeyError): msg = f'Missing key entry for {msg}.' self.msg_aggregator.add_error( f'Error at deserializing {self.name} asset. {msg}. ' f'Ignoring its lending balance query.', ) continue try: usd_price = Inquirer().find_usd_price(asset) except RemoteError as e: self.msg_aggregator.add_error( f'Error processing {self.name} balance entry due to inability to ' f'query USD price: {str(e)}. Skipping balance entry', ) continue balances[asset] += Balance( amount=amount, usd_value=amount * usd_price, ) return balances
def _query_cross_collateral_futures_balances(self, balances: Dict) -> Dict: futures_response = self.api_query_dict('sapi', 'futures/loan/wallet') try: cross_collaterals = futures_response['crossCollaterals'] for entry in cross_collaterals: amount = FVal(entry['locked']) if amount == ZERO: continue try: asset = asset_from_binance(entry['collateralCoin']) except UnsupportedAsset as e: self.msg_aggregator.add_warning( f'Found unsupported {self.name} asset {e.asset_name}. ' f'Ignoring its futures balance query.', ) continue except UnknownAsset as e: self.msg_aggregator.add_warning( f'Found unknown {self.name} asset {e.asset_name}. ' f'Ignoring its futures balance query.', ) continue except DeserializationError: self.msg_aggregator.add_error( f'Found {self.name} asset with non-string type ' f'{type(entry["asset"])}. Ignoring its futures balance query.', ) continue try: usd_price = Inquirer().find_usd_price(asset) except RemoteError as e: self.msg_aggregator.add_error( f'Error processing {self.name} balance entry due to inability to ' f'query USD price: {str(e)}. Skipping balance entry', ) continue balance = Balance(amount=amount, usd_value=amount * usd_price) if asset not in balances: balances[asset] = balance.to_dict() else: balances[asset]['amount'] += balance.amount balances[asset]['usd_value'] += balance.usd_value except KeyError as e: self.msg_aggregator.add_error( f'At {self.name} futures balance query did not find expected key ' f'{str(e)}. Skipping futures query...', ) return balances
def _query_spot_balances(self, balances: Dict) -> Dict: account_data = self.api_query_dict('api', 'account') for entry in account_data['balances']: if len(entry['asset']) >= 5 and entry['asset'].startswith('LD'): # Some lending coins also appear to start with the LD prefix. Ignore them continue amount = entry['free'] + entry['locked'] if amount == ZERO: continue try: asset = asset_from_binance(entry['asset']) except UnsupportedAsset as e: self.msg_aggregator.add_warning( f'Found unsupported {self.name} asset {e.asset_name}. ' f'Ignoring its balance query.', ) continue except UnknownAsset as e: self.msg_aggregator.add_warning( f'Found unknown {self.name} asset {e.asset_name}. ' f'Ignoring its balance query.', ) continue except DeserializationError: self.msg_aggregator.add_error( f'Found {self.name} asset with non-string type {type(entry["asset"])}. ' f'Ignoring its balance query.', ) continue try: usd_price = Inquirer().find_usd_price(asset) except RemoteError as e: self.msg_aggregator.add_error( f'Error processing {self.name} balance entry due to inability to ' f'query USD price: {str(e)}. Skipping balance entry', ) continue balance = {} balance['amount'] = amount balance['usd_value'] = FVal(amount * usd_price) if asset not in balances: balances[asset] = balance else: # Some assets may appear twice in binance balance query for different locations # Lending/staking for example balances[asset]['amount'] += balance['amount'] balances[asset]['usd_value'] += balance['usd_value'] return balances
def create_binance_symbols_to_pair( exchange_data: Dict[str, Any], location: Location, ) -> Dict[str, BinancePair]: """Parses the result of 'exchangeInfo' endpoint and creates the symbols_to_pair mapping """ result: Dict[str, BinancePair] = {} for symbol in exchange_data['symbols']: symbol_str = symbol['symbol'] if isinstance(symbol_str, FVal): # the to_int here may raise but should never due to the if check above symbol_str = str(symbol_str.to_int(exact=True)) try: result[symbol_str] = BinancePair( symbol=symbol_str, base_asset=asset_from_binance(symbol['baseAsset']), quote_asset=asset_from_binance(symbol['quoteAsset']), location=location, ) except (UnknownAsset, UnsupportedAsset) as e: log.debug(f'Found binance pair with no processable asset. {str(e)}') return result
def _query_lending_balances(self, balances: Dict) -> Dict: data = self.api_query_dict('lending/union/account') positions = data.get('positionAmountVos', None) if positions is None: raise RemoteError( f'Could not find key positionAmountVos in lending account data ' f'{data} returned by binance', ) for entry in positions: try: amount = FVal(entry['amount']) if amount == ZERO: continue asset = asset_from_binance(entry['asset']) except UnsupportedAsset as e: self.msg_aggregator.add_warning( f'Found unsupported binance asset {e.asset_name}. ' f' Ignoring its lending balance query.', ) continue except UnknownAsset as e: self.msg_aggregator.add_warning( f'Found unknown binance asset {e.asset_name}. ' f' Ignoring its lending balance query.', ) continue except (DeserializationError, KeyError) as e: msg = str(e) if isinstance(e, KeyError): msg = f'Missing key entry for {msg}.' self.msg_aggregator.add_error( f'Error at deserializing binance asset. {msg}.' f' Ignoring its lending balance query.', ) continue try: usd_price = Inquirer().find_usd_price(asset) except RemoteError as e: self.msg_aggregator.add_error( f'Error processing binance balance entry due to inability to ' f'query USD price: {str(e)}. Skipping balance entry', ) continue balance = Balance(amount=amount, usd_value=amount * usd_price) if asset not in balances: balances[asset] = balance.to_dict() else: balances[asset]['amount'] += balance.amount balances[asset]['usd_value'] += balance.usd_value return balances
def _deserialize_asset_movement( self, raw_data: Dict[str, Any]) -> Optional[AssetMovement]: """Processes a single deposit/withdrawal from binance and deserializes it Can log error/warning and return None if something went wrong at deserialization """ try: if 'insertTime' in raw_data: category = 'deposit' time_key = 'insertTime' else: category = 'withdrawal' time_key = 'applyTime' timestamp = deserialize_timestamp_from_binance(raw_data[time_key]) asset = asset_from_binance(raw_data['asset']) amount = deserialize_asset_amount(raw_data['amount']) return AssetMovement( exchange=Exchange.BINANCE, category=cast(Literal['deposit', 'withdrawal'], category), timestamp=timestamp, asset=asset, amount=amount, # Binance does not include withdrawal fees neither in the API nor in their UI fee=Fee(ZERO), ) except UnknownAsset as e: self.msg_aggregator.add_warning( f'Found binance deposit/withdrawal with unknown asset ' f'{e.asset_name}. Ignoring it.', ) except UnsupportedAsset as e: self.msg_aggregator.add_warning( f'Found binance deposit/withdrawal with unsupported asset ' f'{e.asset_name}. Ignoring it.', ) except (DeserializationError, KeyError) as e: msg = str(e) if isinstance(e, KeyError): msg = f'Missing key entry for {msg}.' self.msg_aggregator.add_error( 'Error processing a binance deposit/withdrawal. Check logs ' 'for details. Ignoring it.', ) log.error( 'Error processing a binance deposit_withdrawal', asset_movement=raw_data, error=msg, ) return None
def test_binance_assets_are_known(inquirer): # pylint: disable=unused-argument exchange_data = requests.get('https://api.binance.us/api/v3/exchangeInfo').json() binance_assets = set() for pair_symbol in exchange_data['symbols']: binance_assets.add(pair_symbol['baseAsset']) binance_assets.add(pair_symbol['quoteAsset']) sorted_assets = sorted(binance_assets) for binance_asset in sorted_assets: try: _ = asset_from_binance(binance_asset) except UnsupportedAsset: assert binance_asset in UNSUPPORTED_BINANCE_ASSETS except UnknownAsset as e: test_warnings.warn(UserWarning( f'Found unknown asset {e.asset_name} in binanceus. ' f'Support for it has to be added', ))
def test_binance_assets_are_known( accounting_data_dir, inquirer, # pylint: disable=unused-argument ): # use a real binance instance so that we always get the latest data binance = Binance( api_key=base64.b64encode(make_random_b64bytes(128)), secret=base64.b64encode(make_random_b64bytes(128)), data_dir=accounting_data_dir, msg_aggregator=MessagesAggregator(), ) mapping = binance.symbols_to_pair binance_assets = set() for _, pair in mapping.items(): binance_assets.add(pair.binance_base_asset) binance_assets.add(pair.binance_quote_asset) sorted_assets = sorted(binance_assets) for binance_asset in sorted_assets: _ = asset_from_binance(binance_asset)
def test_binance_assets_are_known(inquirer): # pylint: disable=unused-argument unsupported_assets = set(UNSUPPORTED_BINANCE_ASSETS) common_items = unsupported_assets.intersection(set(WORLD_TO_BINANCE.values())) assert not common_items, f'Binance assets {common_items} should not be unsupported' exchange_data = requests.get('https://api3.binance.com/api/v3/exchangeInfo').json() binance_assets = set() for pair_symbol in exchange_data['symbols']: binance_assets.add(pair_symbol['baseAsset']) binance_assets.add(pair_symbol['quoteAsset']) sorted_assets = sorted(binance_assets) for binance_asset in sorted_assets: try: _ = asset_from_binance(binance_asset) except UnsupportedAsset: assert binance_asset in UNSUPPORTED_BINANCE_ASSETS except UnknownAsset as e: test_warnings.warn(UserWarning( f'Found unknown asset {e.asset_name} in binance. ' f'Support for it has to be added', ))
def process_pool_asset(asset_name: str, asset_amount: FVal) -> None: if asset_amount == ZERO: return None try: asset = asset_from_binance(asset_name) except UnsupportedAsset as e: self.msg_aggregator.add_warning( f'Found unsupported {self.name} asset {asset_name}. ' f'Ignoring its {self.name} pool balance query. {str(e)}', ) return None except UnknownAsset as e: self.msg_aggregator.add_warning( f'Found unknown {self.name} asset {asset_name}. ' f'Ignoring its {self.name} pool balance query. {str(e)}', ) return None except DeserializationError as e: self.msg_aggregator.add_error( f'{self.name} balance deserialization error ' f'for asset {asset_name}: {str(e)}. Skipping entry.', ) return None try: usd_price = Inquirer().find_usd_price(asset) except RemoteError as e: self.msg_aggregator.add_error( f'Error processing {self.name} balance entry due to inability to ' f'query USD price: {str(e)}. Skipping {self.name} pool balance entry', ) return None balances[asset] += Balance( amount=asset_amount, usd_value=asset_amount * usd_price, ) return None
def test_binance_assets_are_known( accounting_data_dir, inquirer, # pylint: disable=unused-argument ): # use a real binance instance so that we always get the latest data binance = Binance( api_key=make_api_key(), secret=make_api_secret(), user_directory=accounting_data_dir, msg_aggregator=MessagesAggregator(), ) mapping = binance.symbols_to_pair binance_assets = set() for _, pair in mapping.items(): binance_assets.add(pair.binance_base_asset) binance_assets.add(pair.binance_quote_asset) sorted_assets = sorted(binance_assets) for binance_asset in sorted_assets: try: _ = asset_from_binance(binance_asset) except UnsupportedAsset: assert binance_asset in UNSUPPORTED_BINANCE_ASSETS
def trade_from_binance( binance_trade: Dict, binance_symbols_to_pair: Dict[str, BinancePair], ) -> Trade: """Turn a binance trade returned from trade history to our common trade history format From the official binance api docs (01/09/18): https://github.com/binance-exchange/binance-official-api-docs/blob/62ff32d27bb32d9cc74d63d547c286bb3c9707ef/rest-api.md#terminology base asset refers to the asset that is the quantity of a symbol. quote asset refers to the asset that is the price of a symbol. Throws: - UnsupportedAsset due to asset_from_binance - DeserializationError due to unexpected format of dict entries - KeyError due to dict entries missing an expected entry """ amount = deserialize_asset_amount(binance_trade['qty']) rate = deserialize_price(binance_trade['price']) if binance_trade['symbol'] not in binance_symbols_to_pair: raise DeserializationError( f'Error reading a binance trade. Could not find ' f'{binance_trade["symbol"]} in binance_symbols_to_pair', ) binance_pair = binance_symbols_to_pair[binance_trade['symbol']] timestamp = deserialize_timestamp_from_binance(binance_trade['time']) base_asset = asset_from_binance(binance_pair.binance_base_asset) quote_asset = asset_from_binance(binance_pair.binance_quote_asset) if binance_trade['isBuyer']: order_type = TradeType.BUY # e.g. in RDNETH we buy RDN by paying ETH else: order_type = TradeType.SELL fee_currency = asset_from_binance(binance_trade['commissionAsset']) fee = deserialize_fee(binance_trade['commission']) log.debug( 'Processing binance Trade', sensitive_log=True, amount=amount, rate=rate, timestamp=timestamp, pair=binance_trade['symbol'], base_asset=base_asset, quote=quote_asset, order_type=order_type, commision_asset=binance_trade['commissionAsset'], fee=fee, ) return Trade( timestamp=timestamp, location=Location.BINANCE, pair=trade_pair_from_assets(base_asset, quote_asset), trade_type=order_type, amount=amount, rate=rate, fee=fee, fee_currency=fee_currency, link=str(binance_trade['id']), )
def _query_spot_balances( self, balances: DefaultDict[Asset, Balance], ) -> DefaultDict[Asset, Balance]: account_data = self.api_query_dict('api', 'account') binance_balances = account_data.get('balances', None) if not binance_balances: raise RemoteError( 'Binance spot balances response did not contain the balances key' ) for entry in binance_balances: try: # force string https://github.com/rotki/rotki/issues/2342 asset_symbol = str(entry['asset']) free = deserialize_asset_amount(entry['free']) locked = deserialize_asset_amount(entry['locked']) except KeyError as e: raise RemoteError( f'Binance spot balance asset entry did not contain key {str(e)}' ) from e # noqa: E501 except DeserializationError as e: raise RemoteError( 'Failed to deserialize an amount from binance spot balance asset entry' ) from e # noqa: E501 if len(asset_symbol) >= 5 and asset_symbol.startswith('LD'): # Some lending coins also appear to start with the LD prefix. Ignore them continue amount = free + locked if amount == ZERO: continue try: asset = asset_from_binance(asset_symbol) except UnsupportedAsset as e: self.msg_aggregator.add_warning( f'Found unsupported {self.name} asset {e.asset_name}. ' f'Ignoring its balance query.', ) continue except UnknownAsset as e: self.msg_aggregator.add_warning( f'Found unknown {self.name} asset {e.asset_name}. ' f'Ignoring its balance query.', ) continue except DeserializationError: self.msg_aggregator.add_error( f'Found {self.name} asset with non-string type {type(entry["asset"])}. ' f'Ignoring its balance query.', ) continue try: usd_price = Inquirer().find_usd_price(asset) except RemoteError as e: self.msg_aggregator.add_error( f'Error processing {self.name} balance entry due to inability to ' f'query USD price: {str(e)}. Skipping balance entry', ) continue balances[asset] += Balance( amount=amount, usd_value=amount * usd_price, ) return balances