diff --git a/BinanceWatch/BinanceManager.py b/BinanceWatch/BinanceManager.py index 219e8a9..428406c 100644 --- a/BinanceWatch/BinanceManager.py +++ b/BinanceWatch/BinanceManager.py @@ -1,7 +1,7 @@ import datetime import math import time -from typing import Optional, Dict +from typing import Optional, Dict, List, Union import dateparser from binance.client import Client @@ -53,17 +53,42 @@ def update_spot(self): def update_cross_margin(self): """ - call all update methods related to cross margin spot account + call all update methods related to cross margin account :return: None :rtype: None """ self.update_all_cross_margin_trades() self.update_cross_margin_loans() - self.update_cross_margin_interests() + self.update_margin_interests() self.update_cross_margin_repays() self.update_universal_transfers(transfer_filter='MARGIN') + def update_isolated_margin(self): + """ + call all update methods related to isolated margin account + + :return: None + :rtype: None + """ + self.update_isolated_margin_transfers() # fetch transfers across all isolated symbols + + # we will now update only the isolated symbols that have been funded + transfers = self.db.get_isolated_transfers() + symbols_info = [] + for _, _, _, symbol, token, _ in transfers: + if symbol.startswith(token): + asset, ref_asset = token, symbol[len(token):] + else: + asset, ref_asset = symbol[:-len(token)], token + symbols_info.append({'asset': asset, 'ref_asset': ref_asset, 'symbol': symbol}) + + self.update_isolated_margin_trades(symbols_info) + self.update_isolated_margin_loans(symbols_info) + self.update_isolated_margin_interests(symbols_info) + self.update_isolated_margin_repays(symbols_info) + + def update_lending(self): """ call all update methods related to lending activities @@ -75,6 +100,61 @@ def update_lending(self): self.update_lending_purchases() self.update_lending_redemptions() + def get_margin_symbol_info(self, isolated: bool) -> List[Dict]: + """ + Return information about margin symbols as provided by the binance API + + + sources: + https://binance-docs.github.io/apidocs/spot/en/#get-all-isolated-margin-symbol-user_data + https://binance-docs.github.io/apidocs/spot/en/#get-all-cross-margin-pairs-market_data + + :param isolated: If isolated data are to be returned, otherwise it will be cross margin data + :type isolated: bool + :return: Info on the trading symbols + :rtype: List[Dict] + + .. code-block:: python + + # cross margin + [ + { + 'id': 351637150141315861, + 'symbol': 'BNBBTC', + 'base': 'BNB', + 'quote': 'BTC', + 'isMarginTrade': True, + 'isBuyAllowed': True, + 'isSellAllowed': True + }, + ... + ] + + # isolated margin + [ + { + 'symbol': '1INCHBTC', + 'base': '1INCH', + 'quote': 'BTC', + 'isMarginTrade': True, + 'isBuyAllowed': True, + 'isSellAllowed': True + }, + ... + ] + + """ + client_params = { + 'method': 'get', + 'data': {} + } + if isolated: + client_params['path'] = 'margin/isolated/allPairs' + client_params['signed'] = True + else: + client_params['path'] = 'margin/allPairs' + return self._call_binance_client('_request_margin_api', client_params) + def update_universal_transfers(self, transfer_filter: Optional[str] = None): """ update the universal transfers database. @@ -131,22 +211,142 @@ def update_universal_transfers(self, transfer_filter: Optional[str] = None): pbar.update() pbar.close() - def update_cross_margin_interests(self): + def update_isolated_margin_transfers(self, symbols_info: Optional[List[Dict]] = None): + """ + Update the transfers to and from isolated symbols + + :param symbols_info: details on the symbols to fetch repays on. Each dictionary needs the fields 'asset' and + 'ref_asset'. If not provided, will update all isolated symbols. + :type symbols_info: Optional[List[Dict]] + :return: None + :rtype: None + """ + asset_key = 'asset' + ref_asset_key = 'ref_asset' + if symbols_info is None: + symbols_info = self.get_margin_symbol_info(isolated=True) + asset_key = 'base' + ref_asset_key = 'quote' + + pbar = tqdm(total=len(symbols_info)) + for symbol_info in symbols_info: + asset = symbol_info[asset_key] + ref_asset = symbol_info[ref_asset_key] + symbol = symbol_info.get('symbol', f"{asset}{ref_asset}") + + pbar.set_description(f"fetching isolated margin transfers for {symbol}") + self.update_isolated_symbol_transfers(isolated_symbol=symbol) + pbar.update() + + pbar.close() + + def update_isolated_symbol_transfers(self, isolated_symbol: str): + """ + Update the transfers made to and from an isolated margin symbol + + sources: + https://binance-docs.github.io/apidocs/spot/en/#get-isolated-margin-transfer-history-user_data + + :param isolated_symbol: isolated margin symbol of trading + :type isolated_symbol: str + :return: + :rtype: + """ + latest_time = self.db.get_last_isolated_transfer_time(isolated_symbol=isolated_symbol) + current = 1 + + while True: + params = { + 'symbol': isolated_symbol, + 'current': current, + 'startTime': latest_time + 1, + 'size': 100, + } + + # no built-in method yet in python-binance for margin/interestHistory + client_params = { + 'method': 'get', + 'path': 'margin/isolated/transfer', + 'signed': True, + 'data': params + } + transfers = self._call_binance_client('_request_margin_api', client_params) + + for transfer in transfers['rows']: + if (transfer['transFrom'], transfer['transTo']) == ('SPOT', 'ISOLATED_MARGIN'): + transfer_type = 'IN' + elif (transfer['transFrom'], transfer['transTo']) == ('SPOT', 'ISOLATED_MARGIN'): + transfer_type = 'OUT' + else: + raise ValueError(f"unrecognised transfer: {transfer['transFrom']} -> {transfer['transTo']}") + + self.db.add_isolated_transfer(transfer_id=transfer['txId'], + transfer_type=transfer_type, + transfer_time=transfer['timestamp'], + isolated_symbol=isolated_symbol, + asset=transfer['asset'], + amount=transfer['amount'], + auto_commit=False) + + if len(transfers['rows']): + current += 1 # next page + self.db.commit() + else: + break + + def update_isolated_margin_interests(self, symbols_info: Optional[List[Dict]] = None): + """ + Update the interests for isolated margin assets + + :param symbols_info: details on the symbols to fetch repays on. Each dictionary needs the fields 'asset' and + 'ref_asset'. If not provided, will update all isolated symbols. + :type symbols_info: Optional[List[Dict]] + :return: None + :rtype: None + """ + asset_key = 'asset' + ref_asset_key = 'ref_asset' + if symbols_info is None: + symbols_info = self.get_margin_symbol_info(isolated=True) + asset_key = 'base' + ref_asset_key = 'quote' + + pbar = tqdm(total=len(symbols_info)) + for symbol_info in symbols_info: + asset = symbol_info[asset_key] + ref_asset = symbol_info[ref_asset_key] + symbol = symbol_info.get('symbol', f"{asset}{ref_asset}") + + pbar.set_description(f"fetching isolated margin interests for {symbol}") + self.update_margin_interests(isolated_symbol=symbol, show_pbar=False) + pbar.update() + + pbar.close() + + def update_margin_interests(self, isolated_symbol: Optional[str] = None, show_pbar: bool = True): """ - update the interests for all cross margin assets + Update the interests for all cross margin assets or for a isolated margin symbol if provided. sources: https://binance-docs.github.io/apidocs/spot/en/#query-repay-record-user_data + :param isolated_symbol: only for isolated margin, provide the trading symbol. Otherwise cross margin data will + be updated + :type isolated_symbol: Optional[str] + :param show_pbar: if the progress bar is displayed + :type show_pbar: bool :return: :rtype: """ - margin_type = 'cross' - latest_time = self.db.get_last_margin_interest_time(margin_type) + margin_type = 'cross' if isolated_symbol is None else 'isolated' + latest_time = self.db.get_last_margin_interest_time(isolated_symbol=isolated_symbol) archived = 1000 * time.time() - latest_time > 1000 * 3600 * 24 * 30 * 3 current = 1 - pbar = tqdm() - pbar.set_description("fetching cross margin interests") + pbar = tqdm(disable=not show_pbar) + desc = f"fetching {margin_type} margin interests" + if isolated_symbol is not None: + desc = desc + f" for {isolated_symbol}" + pbar.set_description(desc) while True: params = { 'current': current, @@ -154,6 +354,8 @@ def update_cross_margin_interests(self): 'size': 100, 'archived': archived } + if isolated_symbol is not None: + params['isolatedSymbol'] = isolated_symbol # no built-in method yet in python-binance for margin/interestHistory client_params = { @@ -165,23 +367,23 @@ def update_cross_margin_interests(self): interests = self._call_binance_client('_request_margin_api', client_params) for interest in interests['rows']: - self.db.add_margin_interest(margin_type=margin_type, - interest_time=interest['interestAccuredTime'], + self.db.add_margin_interest(interest_time=interest['interestAccuredTime'], asset=interest['asset'], interest=interest['interest'], interest_type=interest['type'], + isolated_symbol=interest.get('isolatedSymbol'), auto_commit=False) + pbar.update() if len(interests['rows']): current += 1 # next page self.db.commit() elif archived: # switching to non archived interests current = 1 archived = False - latest_time = self.db.get_last_margin_interest_time(margin_type) + latest_time = self.db.get_last_margin_interest_time(isolated_symbol=isolated_symbol) else: break - pbar.update() pbar.close() def update_cross_margin_repays(self): @@ -209,7 +411,39 @@ def update_cross_margin_repays(self): pbar.update() pbar.close() - def update_margin_asset_repay(self, asset: str, isolated_symbol=''): + def update_isolated_margin_repays(self, symbols_info: Optional[List[Dict]] = None): + """ + Update the repays for isolated margin assets + + :param symbols_info: details on the symbols to fetch repays on. Each dictionary needs the fields 'asset' and + 'ref_asset'. If not provided, will update all isolated symbols. + :type symbols_info: Optional[List[Dict]] + :return: None + :rtype: None + """ + asset_key = 'asset' + ref_asset_key = 'ref_asset' + if symbols_info is None: + symbols_info = self.get_margin_symbol_info(isolated=True) + asset_key = 'base' + ref_asset_key = 'quote' + + pbar = tqdm(total=2 * len(symbols_info)) + for symbol_info in symbols_info: + asset = symbol_info[asset_key] + ref_asset = symbol_info[ref_asset_key] + symbol = symbol_info.get('symbol', f"{asset}{ref_asset}") + + pbar.set_description(f"fetching {asset} isolated margin repays for {symbol}") + self.update_margin_asset_repay(asset=asset, isolated_symbol=symbol) + pbar.update() + + pbar.set_description(f"fetching {ref_asset} isolated margin repays for {symbol}") + self.update_margin_asset_repay(asset=ref_asset, isolated_symbol=symbol) + pbar.update() + pbar.close() + + def update_margin_asset_repay(self, asset: str, isolated_symbol: Optional[str] = None): """ update the repays database for a specified asset. @@ -219,13 +453,13 @@ def update_margin_asset_repay(self, asset: str, isolated_symbol=''): :param asset: asset for the repays :type asset: str - :param isolated_symbol: the symbol must be specified of isolated margin, otherwise cross margin data is returned - :type isolated_symbol: str + :param isolated_symbol: only for isolated margin, provide the trading symbol. Otherwise cross margin data will + be updated + :type isolated_symbol: Optional[str] :return: None :rtype: None """ - margin_type = 'cross' if isolated_symbol == '' else 'isolated' - latest_time = self.db.get_last_repay_time(asset=asset, margin_type=margin_type) + latest_time = self.db.get_last_repay_time(asset=asset, isolated_symbol=isolated_symbol) archived = 1000 * time.time() - latest_time > 1000 * 3600 * 24 * 30 * 3 current = 1 while True: @@ -234,19 +468,20 @@ def update_margin_asset_repay(self, asset: str, isolated_symbol=''): 'current': current, 'startTime': latest_time + 1000, 'archived': archived, - 'isolatedSymbol': isolated_symbol, 'size': 100 } + if isolated_symbol is not None: + client_params['isolatedSymbol'] = isolated_symbol repays = self._call_binance_client('get_margin_repay_details', client_params) for repay in repays['rows']: if repay['status'] == 'CONFIRMED': - self.db.add_repay(margin_type=margin_type, - tx_id=repay['txId'], + self.db.add_repay(tx_id=repay['txId'], repay_time=repay['timestamp'], asset=repay['asset'], principal=repay['principal'], interest=repay['interest'], + isolated_symbol=repay.get('isolatedSymbol', None), auto_commit=False) if len(repays['rows']): @@ -255,7 +490,7 @@ def update_margin_asset_repay(self, asset: str, isolated_symbol=''): elif archived: # switching to non archived repays current = 1 archived = False - latest_time = self.db.get_last_repay_time(asset=asset, margin_type=margin_type) + latest_time = self.db.get_last_repay_time(asset=asset) else: break @@ -284,7 +519,40 @@ def update_cross_margin_loans(self): pbar.update() pbar.close() - def update_margin_asset_loans(self, asset: str, isolated_symbol=''): + def update_isolated_margin_loans(self, symbols_info: Optional[List[Dict]] = None): + """ + Update the loans for isolated margin assets + + :param symbols_info: details on the symbols to fetch loans on. Each dictionary needs the fields 'asset' and + 'ref_asset'. If not provided, will update all isolated symbols. + :type symbols_info: Optional[List[Dict]] + :return: None + :rtype: None + """ + asset_key = 'asset' + ref_asset_key = 'ref_asset' + if symbols_info is None: + symbols_info = self.get_margin_symbol_info(isolated=True) + asset_key = 'base' + ref_asset_key = 'quote' + + pbar = tqdm(total=2 * len(symbols_info)) + for symbol_info in symbols_info: + asset = symbol_info[asset_key] + ref_asset = symbol_info[ref_asset_key] + symbol = symbol_info.get('symbol', f"{asset}{ref_asset}") + + pbar.set_description(f"fetching {asset} isolated margin loans for {symbol}") + self.update_margin_asset_loans(asset=asset, isolated_symbol=symbol) + pbar.update() + + pbar.set_description(f"fetching {ref_asset} isolated margin loans for {symbol}") + self.update_margin_asset_loans(asset=ref_asset, isolated_symbol=symbol) + pbar.update() + + pbar.close() + + def update_margin_asset_loans(self, asset: str, isolated_symbol: Optional[str] = None): """ update the loans database for a specified asset. @@ -294,13 +562,13 @@ def update_margin_asset_loans(self, asset: str, isolated_symbol=''): :param asset: asset for the loans :type asset: str - :param isolated_symbol: the symbol must be specified of isolated margin, otherwise cross margin data is returned - :type isolated_symbol: str + :param isolated_symbol: only for isolated margin, provide the trading symbol. Otherwise cross margin data will + be updated + :type isolated_symbol: Optional[str] :return: None :rtype: None """ - margin_type = 'cross' if isolated_symbol == '' else 'isolated' - latest_time = self.db.get_last_loan_time(asset=asset, margin_type=margin_type) + latest_time = self.db.get_last_loan_time(asset=asset, isolated_symbol=isolated_symbol) archived = 1000 * time.time() - latest_time > 1000 * 3600 * 24 * 30 * 3 current = 1 while True: @@ -309,18 +577,19 @@ def update_margin_asset_loans(self, asset: str, isolated_symbol=''): 'current': current, 'startTime': latest_time + 1000, 'archived': archived, - 'isolatedSymbol': isolated_symbol, 'size': 100 } + if isolated_symbol is not None: + client_params['isolatedSymbol'] = isolated_symbol loans = self._call_binance_client('get_margin_loan_details', client_params) for loan in loans['rows']: if loan['status'] == 'CONFIRMED': - self.db.add_loan(margin_type=margin_type, - tx_id=loan['txId'], + self.db.add_loan(tx_id=loan['txId'], loan_time=loan['timestamp'], asset=loan['asset'], principal=loan['principal'], + isolated_symbol=loan.get('isolatedSymbol'), auto_commit=False) if len(loans['rows']): @@ -329,13 +598,13 @@ def update_margin_asset_loans(self, asset: str, isolated_symbol=''): elif archived: # switching to non archived loans current = 1 archived = False - latest_time = self.db.get_last_loan_time(asset=asset, margin_type=margin_type) + latest_time = self.db.get_last_loan_time(asset=asset, isolated_symbol=isolated_symbol) else: break - def update_cross_margin_symbol_trades(self, asset: str, ref_asset: str, limit: int = 1000): + def update_margin_symbol_trades(self, asset: str, ref_asset: str, is_isolated: bool = False, limit: int = 1000): """ - This update the cross_margin trades in the database for a single trading pair. + This update the margin trades in the database for a single trading pair. It will check the last trade id and will requests the all trades after this trade_id. sources: @@ -346,24 +615,28 @@ def update_cross_margin_symbol_trades(self, asset: str, ref_asset: str, limit: i :type asset: string :param ref_asset: name of the reference asset in the trading pair (ex 'USDT' for 'BTCUSDT') :type ref_asset: string + :param is_isolated: if margin type is isolated, default False + :type is_isolated: bool :param limit: max size of each trade requests :type limit: int :return: None :rtype: None """ + trade_type = 'isolated_margin' if is_isolated else 'cross_margin' limit = min(1000, limit) symbol = asset + ref_asset - last_trade_id = self.db.get_max_trade_id(asset, ref_asset, 'cross_margin') + last_trade_id = self.db.get_max_trade_id(asset, ref_asset, trade_type) while True: client_params = { 'symbol': symbol, 'fromId': last_trade_id + 1, + 'isIsolated': is_isolated, 'limit': limit } new_trades = self._call_binance_client('get_margin_trades', client_params) for trade in new_trades: - self.db.add_trade(trade_type='cross_margin', + self.db.add_trade(trade_type=trade_type, trade_id=int(trade['id']), trade_time=int(trade['time']), asset=asset, @@ -373,6 +646,7 @@ def update_cross_margin_symbol_trades(self, asset: str, ref_asset: str, limit: i fee=float(trade['commission']), fee_asset=trade['commissionAsset'], is_buyer=trade['isBuyer'], + symbol=symbol, auto_commit=False ) last_trade_id = max(last_trade_id, int(trade['id'])) @@ -400,9 +674,44 @@ def update_all_cross_margin_trades(self, limit: int = 1000): pbar = tqdm(total=len(symbols_info)) for symbol_info in symbols_info: pbar.set_description(f"fetching {symbol_info['symbol']} cross margin trades") - self.update_cross_margin_symbol_trades(asset=symbol_info['base'], - ref_asset=symbol_info['quote'], - limit=limit) + self.update_margin_symbol_trades(asset=symbol_info['base'], + ref_asset=symbol_info['quote'], + limit=limit) + pbar.update() + pbar.close() + + def update_isolated_margin_trades(self, symbols_info: Optional[List[Dict]] = None): + """ + This update the isolated margin trades in the database for every trading pairs + + :param symbols_info: details on the symbols to fetch trades on. Each dictionary needs the fields 'asset' and + 'ref_asset'. If not provided, will update all isolated symbols. + :type symbols_info: Optional[List[Dict]] + :return: None + :rtype: None + """ + asset_key = 'asset' + ref_asset_key = 'ref_asset' + if symbols_info is None: + symbols_info = self.get_margin_symbol_info(isolated=True) + asset_key = 'base' + ref_asset_key = 'quote' + + pbar = tqdm(total=len(symbols_info)) + for symbol_info in symbols_info: + asset = symbol_info[asset_key] + ref_asset = symbol_info[ref_asset_key] + symbol = symbol_info.get('symbol', f"{asset}{ref_asset}") + pbar.set_description(f"fetching {symbol} isolated margin trades") + + try: + self.update_margin_symbol_trades(asset=asset, + ref_asset=ref_asset, + limit=1000, + is_isolated=True) + except BinanceAPIException as e: + if e.code != -11001: # -11001 means that this isolated pair has never been used + raise e pbar.update() pbar.close() @@ -553,14 +862,14 @@ def update_spot_dusts(self): for d in dusts['rows']: for sub_dust in d['logs']: date_time = dateparser.parse(sub_dust['operateTime'] + 'Z') - self.db.add_dust(tran_id=sub_dust['tranId'], - time=datetime_to_millistamp(date_time), - asset=sub_dust['fromAsset'], - asset_amount=sub_dust['amount'], - bnb_amount=sub_dust['transferedAmount'], - bnb_fee=sub_dust['serviceChargeAmount'], - auto_commit=False - ) + self.db.add_spot_dust(tran_id=sub_dust['tranId'], + time=datetime_to_millistamp(date_time), + asset=sub_dust['fromAsset'], + asset_amount=sub_dust['amount'], + bnb_amount=sub_dust['transferedAmount'], + bnb_fee=sub_dust['serviceChargeAmount'], + auto_commit=False + ) pbar.update() self.db.commit() pbar.close() @@ -772,7 +1081,8 @@ def update_all_spot_trades(self, limit: int = 1000): pbar.update() pbar.close() - def _call_binance_client(self, method_name: str, params: Optional[Dict] = None, retry_count: int = 0): + def _call_binance_client(self, method_name: str, params: Optional[Dict] = None, + retry_count: int = 0) -> Union[Dict, List]: """ This method is used to handle rate limits: if a rate limits is breached, it will wait the necessary time to call again the API. @@ -784,7 +1094,7 @@ def _call_binance_client(self, method_name: str, params: Optional[Dict] = None, :param retry_count: internal use only to count the number of retry if rate limits are breached :type retry_count: int :return: response of binance.Client method - :rtype: Dict + :rtype: Union[Dict, List] """ if params is None: params = dict() diff --git a/BinanceWatch/__init__.py b/BinanceWatch/__init__.py index df2020d..d05bded 100644 --- a/BinanceWatch/__init__.py +++ b/BinanceWatch/__init__.py @@ -1,2 +1,2 @@ -__version__ = "0.1.3" +__version__ = "0.1.4" __author__ = 'EtWnn' diff --git a/BinanceWatch/storage/BinanceDataBase.py b/BinanceWatch/storage/BinanceDataBase.py index dd6a23d..de9c51d 100644 --- a/BinanceWatch/storage/BinanceDataBase.py +++ b/BinanceWatch/storage/BinanceDataBase.py @@ -23,7 +23,7 @@ def __init__(self, name: str = 'binance_db'): def add_universal_transfer(self, transfer_id: int, transfer_type: str, transfer_time: int, asset: str, amount: float, auto_commit: bool = True): """ - add a universal transfer to the database + Add a universal transfer to the database :param transfer_id: id of the transfer :type transfer_id: int @@ -48,7 +48,7 @@ def add_universal_transfer(self, transfer_id: int, transfer_type: str, transfer_ def get_universal_transfers(self, transfer_type: Optional[str] = None, asset: Optional[str] = None, start_time: Optional[int] = None, end_time: Optional[int] = None): """ - return universal transfers stored in the database. Transfer type, Asset type and time filters can be used + Return universal transfers stored in the database. Transfer type, Asset type and time filters can be used :param transfer_type: enum of the transfer type (ex: 'MAIN_MARGIN') :type transfer_type: Optional[str] @@ -94,7 +94,7 @@ def get_universal_transfers(self, transfer_type: Optional[str] = None, asset: Op def get_last_universal_transfer_time(self, transfer_type: str) -> int: """ - return the latest time when a universal transfer was made + Return the latest time when a universal transfer was made If None, return the millistamp corresponding to 2017/01/01 :param transfer_type: enum of the transfer type (ex: 'MAIN_MARGIN') @@ -119,13 +119,109 @@ def get_last_universal_transfer_time(self, transfer_type: str) -> int: return default return result - def add_margin_interest(self, margin_type: str, interest_time: int, asset: str, interest: float, - interest_type: str, auto_commit: bool = True): + def add_isolated_transfer(self, transfer_id: int, transfer_type: str, transfer_time: int, isolated_symbol: str, + asset: str, amount: float, auto_commit: bool = True): """ - add a repay to the database + Add a universal transfer to the database + + :param transfer_id: id of the transfer + :type transfer_id: int + :param transfer_type: enum of the transfer type (ex: 'MAIN_MARGIN') + :type transfer_type: str + :param transfer_time: millistamp of the operation + :type transfer_time: int + :param isolated_symbol: isolated symbol that received or sent the transfer + :type isolated_symbol: str + :param asset: asset that got transferred + :type asset: str + :param amount: amount transferred + :type amount: float + :param auto_commit: if the database should commit the change made, default True + :type auto_commit: bool + :return: None + :rtype: None + """ + table = tables.ISOLATED_MARGIN_TRANSFER_TABLE + + row = (transfer_id, transfer_type, transfer_time, isolated_symbol, asset, amount) + self.add_row(table, row, auto_commit=auto_commit) + + def get_isolated_transfers(self, isolated_symbol: Optional[str] = None, start_time: Optional[int] = None, + end_time: Optional[int] = None): + """ + Return isolated transfers stored in the database. isolated_symbol and time filters can be used + + :param isolated_symbol: for isolated margin, provided the trading symbol otherwise it will be counted a cross + margin data + :type isolated_symbol: Optional[str] + :param start_time: fetch only transfers after this millistamp + :type start_time: Optional[int] + :param end_time: fetch only transfers before this millistamp + :type end_time: Optional[int] + :return: The raw rows selected as saved in the database + :rtype: List[Tuple] + + .. code-block:: python + + [ + (1206491332, # transfer id + 'IN', # transfer type (IN or OUT) + 1589121841000, # time + 'BTCBUSD', # isolated symbol + 'BTC', # asset + 10.594112), # amount + ] + """ + table = tables.ISOLATED_MARGIN_TRANSFER_TABLE + + conditions_list = [] + if isolated_symbol is not None: + conditions_list.append((table.symbol, + SQLConditionEnum.equal, + isolated_symbol)) + if start_time is not None: + conditions_list.append((table.trfTime, + SQLConditionEnum.greater_equal, + start_time)) + if end_time is not None: + conditions_list.append((table.trfTime, + SQLConditionEnum.lower, + end_time)) + + return self.get_conditions_rows(table, conditions_list=conditions_list) + + def get_last_isolated_transfer_time(self, isolated_symbol: str) -> int: + """ + Return the latest time when a isolated margin transfer was made + If None, return the millistamp corresponding to 2017/01/01 + + :param isolated_symbol: isolated symbol that received or sent the transfers + :type isolated_symbol: str + :return: millistamp + :rtype: int + """ + table = tables.ISOLATED_MARGIN_TRANSFER_TABLE + conditions_list = [(table.symbol, + SQLConditionEnum.equal, + isolated_symbol)] + selection = f"MAX({table.trfTime})" + result = self.get_conditions_rows(table, + selection=selection, + conditions_list=conditions_list) + default = datetime_to_millistamp(datetime.datetime(2017, 1, 1, tzinfo=datetime.timezone.utc)) + try: + result = result[0][0] + except IndexError: + return default + if result is None: + return default + return result + + def add_margin_interest(self, interest_time: int, asset: str, interest: float, interest_type: str, + isolated_symbol: Optional[str] = None, auto_commit: bool = True): + """ + Add a margin interest to the database - :param margin_type: either 'cross' or 'isolated' - :type margin_type: str :param interest_time: millistamp of the operation :type interest_time: int :param asset: asset that got repaid @@ -134,30 +230,34 @@ def add_margin_interest(self, margin_type: str, interest_time: int, asset: str, :type interest: float :param interest_type: one of (PERIODIC, ON_BORROW, PERIODIC_CONVERTED, ON_BORROW_CONVERTED) :type interest_type: str + :param isolated_symbol: for isolated margin, provided the trading symbol otherwise it will be counted a cross + margin data + :type isolated_symbol: Optional[str] :param auto_commit: if the database should commit the change made, default True :type auto_commit: bool :return: None :rtype: None """ - if margin_type == 'cross': + if isolated_symbol is None: table = tables.CROSS_MARGIN_INTEREST_TABLE - elif margin_type == 'isolated': - raise NotImplementedError + row = (interest_time, asset, interest, interest_type) else: - raise ValueError(f"margin type should be 'cross' or 'isolated' but {margin_type} was received") + table = tables.ISOLATED_MARGIN_INTEREST_TABLE + row = (interest_time, isolated_symbol, asset, interest, interest_type) - row = (interest_time, asset, interest, interest_type) self.add_row(table, row, auto_commit=auto_commit) - def get_margin_interests(self, margin_type: str, asset: Optional[str] = None, start_time: Optional[int] = None, - end_time: Optional[int] = None): + def get_margin_interests(self, margin_type: str, asset: Optional[str] = None, isolated_symbol: Optional[str] = None, + start_time: Optional[int] = None, end_time: Optional[int] = None): """ - return margin interests stored in the database. Asset type and time filters can be used + Return margin interests stored in the database. Asset type and time filters can be used :param margin_type: either 'cross' or 'isolated' :type margin_type: :param asset: fetch only interests in this asset :type asset: Optional[str] + :param isolated_symbol: only for isolated margin, provide the trading symbol (otherwise cross data are returned) + :type isolated_symbol: Optional[str] :param start_time: fetch only interests after this millistamp :type start_time: Optional[int] :param end_time: fetch only interests before this millistamp @@ -167,21 +267,36 @@ def get_margin_interests(self, margin_type: str, asset: Optional[str] = None, st .. code-block:: python + # cross margin [ 1559415215400, # time 'BNB', # asset 0.51561, # interest 'PERIODIC_CONVERTED'), # interest type ] + + # isolated margin + [ + 1559415215400, # time + 'BTCBUSD', # symbol + 'BUSD', # asset + 0.51561, # interest + 'PERIODIC'), # interest type + ] """ + conditions_list = [] + if margin_type == 'cross': table = tables.CROSS_MARGIN_INTEREST_TABLE elif margin_type == 'isolated': - raise NotImplementedError + table = tables.ISOLATED_MARGIN_INTEREST_TABLE + if isolated_symbol is not None: + conditions_list.append((table.isolated_symbol, + SQLConditionEnum.equal, + isolated_symbol)) else: raise ValueError(f"margin type should be 'cross' or 'isolated' but {margin_type} was received") - conditions_list = [] if asset is not None: conditions_list.append((table.asset, SQLConditionEnum.equal, @@ -196,30 +311,32 @@ def get_margin_interests(self, margin_type: str, asset: Optional[str] = None, st end_time)) return self.get_conditions_rows(table, conditions_list=conditions_list) - def get_last_margin_interest_time(self, margin_type: str, asset: Optional[str] = None): + def get_last_margin_interest_time(self, asset: Optional[str] = None, isolated_symbol: Optional[str] = None) -> int: """ - return the latest time when a margin interest was accured on a defined asset or on all assets + Return the latest time when a margin interest was accured on a defined asset or on all assets If None, return the millistamp corresponding to 2017/01/01 :param asset: name of the asset charged as interest :type asset: Optional[str] - :param margin_type: either 'cross' or 'isolated' - :type margin_type: + :param isolated_symbol: only for isolated margin, provide the trading symbol (otherwise cross data are returned) + :type isolated_symbol: Optional[str] :return: millistamp :rtype: int """ - if margin_type == 'cross': + conditions_list = [] + if isolated_symbol is None: table = tables.CROSS_MARGIN_INTEREST_TABLE - elif margin_type == 'isolated': - raise NotImplementedError else: - raise ValueError(f"margin type should be 'cross' or 'isolated' but {margin_type} was received") + table = tables.ISOLATED_MARGIN_INTEREST_TABLE + conditions_list.append((table.symbol, + SQLConditionEnum.equal, + isolated_symbol)) - conditions_list = [] if asset is not None: - conditions_list = [(table.asset, - SQLConditionEnum.equal, - asset)] + conditions_list.append((table.asset, + SQLConditionEnum.equal, + asset)) + selection = f"MAX({table.interestTime})" result = self.get_conditions_rows(table, selection=selection, @@ -233,13 +350,11 @@ def get_last_margin_interest_time(self, margin_type: str, asset: Optional[str] = return default return result - def add_repay(self, margin_type: str, tx_id: int, repay_time: int, asset: str, principal: float, - interest: float, auto_commit: bool = True): + def add_repay(self, tx_id: int, repay_time: int, asset: str, principal: float, + interest: float, isolated_symbol: Optional[str] = None, auto_commit: bool = True): """ - add a repay to the database + Add a repay to the database - :param margin_type: either 'cross' or 'isolated' - :type margin_type: :param tx_id: binance id for the transaction (uniqueness?) :type tx_id: int :param repay_time: millitstamp of the operation @@ -249,31 +364,35 @@ def add_repay(self, margin_type: str, tx_id: int, repay_time: int, asset: str, p :param principal: principal amount repaid for the loan :type principal: float :param interest: amount of interest repaid for the loan - :type interest: + :type interest: float + :param isolated_symbol: for isolated margin, provided the trading symbol otherwise it will be counted a cross + margin data + :type isolated_symbol: Optional[str] :param auto_commit: if the database should commit the change made, default True :type auto_commit: bool :return: None :rtype: None """ - if margin_type == 'cross': + if isolated_symbol is None: table = tables.CROSS_MARGIN_REPAY_TABLE - elif margin_type == 'isolated': - raise NotImplementedError + row = (tx_id, repay_time, asset, principal, interest) else: - raise ValueError(f"margin type should be 'cross' or 'isolated' but {margin_type} was received") + table = tables.ISOLATED_MARGIN_REPAY_TABLE + row = (tx_id, repay_time, isolated_symbol, asset, principal, interest) - row = (tx_id, repay_time, asset, principal, interest) self.add_row(table, row, auto_commit=auto_commit) - def get_repays(self, margin_type: str, asset: Optional[str] = None, start_time: Optional[int] = None, - end_time: Optional[int] = None): + def get_repays(self, margin_type: str, asset: Optional[str] = None, isolated_symbol: Optional[str] = None, + start_time: Optional[int] = None, end_time: Optional[int] = None): """ - return repays stored in the database. Asset type and time filters can be used + Return repays stored in the database. Asset type and time filters can be used :param margin_type: either 'cross' or 'isolated' - :type margin_type: + :type margin_type: str :param asset: fetch only repays of this asset :type asset: Optional[str] + :param isolated_symbol: only for isolated margin, provide the trading symbol + :type isolated_symbol: Optional[str] :param start_time: fetch only repays after this millistamp :type start_time: Optional[int] :param end_time: fetch only repays before this millistamp @@ -283,22 +402,38 @@ def get_repays(self, margin_type: str, asset: Optional[str] = None, start_time: .. code-block:: python + # cross margin + [ + (8289451654, # transaction id + 1559415215400, # time + 'USDT', # asset + 145.5491462, # principal + 0.51561), # interest + ] + + # isolated margin [ (8289451654, # transaction id 1559415215400, # time + 'BTCUSDT', # isolated symbol 'USDT', # asset 145.5491462, # principal 0.51561), # interest ] """ + conditions_list = [] + if margin_type == 'cross': table = tables.CROSS_MARGIN_REPAY_TABLE elif margin_type == 'isolated': - raise NotImplementedError + table = tables.ISOLATED_MARGIN_REPAY_TABLE + if isolated_symbol is not None: + conditions_list.append((table.isolated_symbol, + SQLConditionEnum.equal, + isolated_symbol)) else: raise ValueError(f"margin type should be 'cross' or 'isolated' but {margin_type} was received") - conditions_list = [] if asset is not None: conditions_list.append((table.asset, SQLConditionEnum.equal, @@ -313,28 +448,31 @@ def get_repays(self, margin_type: str, asset: Optional[str] = None, start_time: end_time)) return self.get_conditions_rows(table, conditions_list=conditions_list) - def get_last_repay_time(self, asset: str, margin_type: str) -> int: + def get_last_repay_time(self, asset: str, isolated_symbol: Optional[str] = None) -> int: """ - return the latest time when a repay was made on a defined asset + Return the latest time when a repay was made on a defined asset If None, return the millistamp corresponding to 2017/01/01 :param asset: name of the asset repaid :type asset: str - :param margin_type: either 'cross' or 'isolated' - :type margin_type: + :param isolated_symbol: only for isolated margin, provide the trading symbol (otherwise cross data are returned) + :type isolated_symbol: Optional[str] :return: millistamp :rtype: int """ - if margin_type == 'cross': + conditions_list = [] + if isolated_symbol is None: table = tables.CROSS_MARGIN_REPAY_TABLE - elif margin_type == 'isolated': - raise NotImplementedError else: - raise ValueError(f"margin type should be 'cross' or 'isolated' but {margin_type} was received") + table = tables.ISOLATED_MARGIN_REPAY_TABLE + conditions_list.append((table.symbol, + SQLConditionEnum.equal, + isolated_symbol)) + + conditions_list.append((table.asset, + SQLConditionEnum.equal, + asset)) - conditions_list = [(table.asset, - SQLConditionEnum.equal, - asset)] selection = f"MAX({table.repayTime})" result = self.get_conditions_rows(table, selection=selection, @@ -348,13 +486,11 @@ def get_last_repay_time(self, asset: str, margin_type: str) -> int: return default return result - def add_loan(self, margin_type: str, tx_id: int, loan_time: int, asset: str, principal: float, - auto_commit: bool = True): + def add_loan(self, tx_id: int, loan_time: int, asset: str, principal: float, + isolated_symbol: Optional[str] = None, auto_commit: bool = True): """ - add a loan to the database + Add a loan to the database - :param margin_type: either 'cross' or 'isolated' - :type margin_type: :param tx_id: binance id for the transaction (uniqueness?) :type tx_id: int :param loan_time: millitstamp of the operation @@ -363,30 +499,34 @@ def add_loan(self, margin_type: str, tx_id: int, loan_time: int, asset: str, pri :type asset: str :param principal: amount of loaned asset :type principal: float + :param isolated_symbol: for isolated margin, provided the trading symbol otherwise it will be counted a cross + margin data + :type isolated_symbol: Optional[str] :param auto_commit: if the database should commit the change made, default True :type auto_commit: bool :return: None :rtype: None """ - if margin_type == 'cross': + if isolated_symbol is None: table = tables.CROSS_MARGIN_LOAN_TABLE - elif margin_type == 'isolated': - raise NotImplementedError + row = (tx_id, loan_time, asset, principal) else: - raise ValueError(f"margin type should be 'cross' or 'isolated' but {margin_type} was received") + row = (tx_id, loan_time, isolated_symbol, asset, principal) + table = tables.ISOLATED_MARGIN_LOAN_TABLE - row = (tx_id, loan_time, asset, principal) self.add_row(table, row, auto_commit=auto_commit) - def get_loans(self, margin_type: str, asset: Optional[str] = None, start_time: Optional[int] = None, - end_time: Optional[int] = None): + def get_loans(self, margin_type: str, asset: Optional[str] = None, isolated_symbol: Optional[str] = None, + start_time: Optional[int] = None, end_time: Optional[int] = None): """ - return loans stored in the database. Asset type and time filters can be used + Return loans stored in the database. Asset type and time filters can be used :param margin_type: either 'cross' or 'isolated' :type margin_type: :param asset: fetch only loans of this asset :type asset: Optional[str] + :param isolated_symbol: only for isolated margin, provide the trading symbol + :type isolated_symbol: Optional[str] :param start_time: fetch only loans after this millistamp :type start_time: Optional[int] :param end_time: fetch only loans before this millistamp @@ -396,21 +536,37 @@ def get_loans(self, margin_type: str, asset: Optional[str] = None, start_time: O .. code-block:: python + # cross margin + [ + (8289451654, # transaction id + 1559415215400, # time + 'USDT', # asset + 145.5491462), # amount + ] + + # isolated margin [ (8289451654, # transaction id 1559415215400, # time + 'BTCUSDT', # symbol 'USDT', # asset 145.5491462), # amount ] + + """ + conditions_list = [] if margin_type == 'cross': table = tables.CROSS_MARGIN_LOAN_TABLE elif margin_type == 'isolated': - raise NotImplementedError + table = tables.ISOLATED_MARGIN_LOAN_TABLE + if isolated_symbol is not None: + conditions_list.append((table.isolated_symbol, + SQLConditionEnum.equal, + isolated_symbol)) else: raise ValueError(f"margin type should be 'cross' or 'isolated' but {margin_type} was received") - conditions_list = [] if asset is not None: conditions_list.append((table.asset, SQLConditionEnum.equal, @@ -425,28 +581,30 @@ def get_loans(self, margin_type: str, asset: Optional[str] = None, start_time: O end_time)) return self.get_conditions_rows(table, conditions_list=conditions_list) - def get_last_loan_time(self, asset: str, margin_type: str) -> int: + def get_last_loan_time(self, asset: str, isolated_symbol: Optional[str] = None) -> int: """ - return the latest time when an loan was made on a defined asset + Return the latest time when an loan was made on a defined asset If None, return the millistamp corresponding to 2017/01/01 :param asset: name of the asset loaned :type asset: str - :param margin_type: either 'cross' or 'isolated' - :type margin_type: + :param isolated_symbol: only for isolated margin, provide the trading symbol (otherwise cross data are returned) + :type isolated_symbol: Optional[str] :return: millistamp :rtype: int """ - if margin_type == 'cross': + conditions_list = [] + if isolated_symbol is None: table = tables.CROSS_MARGIN_LOAN_TABLE - elif margin_type == 'isolated': - raise NotImplementedError else: - raise ValueError(f"margin type should be 'cross' or 'isolated' but {margin_type} was received") + table = tables.ISOLATED_MARGIN_LOAN_TABLE + conditions_list.append((table.symbol, + SQLConditionEnum.equal, + isolated_symbol)) - conditions_list = [(table.asset, - SQLConditionEnum.equal, - asset)] + conditions_list.append((table.asset, + SQLConditionEnum.equal, + asset)) selection = f"MAX({table.loanTime})" result = self.get_conditions_rows(table, selection=selection, @@ -463,7 +621,7 @@ def get_last_loan_time(self, asset: str, margin_type: str) -> int: def add_lending_redemption(self, redemption_time: int, lending_type: str, asset: str, amount: float, auto_commit: bool = True): """ - add a lending redemption to the database + Add a lending redemption to the database :param redemption_time: millitstamp of the operation :type redemption_time: int @@ -484,7 +642,7 @@ def add_lending_redemption(self, redemption_time: int, lending_type: str, asset: def get_lending_redemptions(self, lending_type: Optional[str] = None, asset: Optional[str] = None, start_time: Optional[int] = None, end_time: Optional[int] = None): """ - return lending redemptions stored in the database. Asset type and time filters can be used + Return lending redemptions stored in the database. Asset type and time filters can be used :param lending_type: fetch only redemptions from this lending type :type lending_type: Optional[str] @@ -528,7 +686,7 @@ def get_lending_redemptions(self, lending_type: Optional[str] = None, asset: Opt def get_last_lending_redemption_time(self, lending_type: Optional[str] = None) -> int: """ - return the latest time when an lending redemption was made. + Return the latest time when an lending redemption was made. If None, return the millistamp corresponding to 2017/01/01 :param lending_type: type of lending @@ -558,7 +716,7 @@ def get_last_lending_redemption_time(self, lending_type: Optional[str] = None) - def add_lending_purchase(self, purchase_id: int, purchase_time: int, lending_type: str, asset: str, amount: float, auto_commit: bool = True): """ - add a lending purchase to the database + Add a lending purchase to the database :param purchase_id: id of the purchase :type purchase_id: int @@ -581,7 +739,7 @@ def add_lending_purchase(self, purchase_id: int, purchase_time: int, lending_typ def get_lending_purchases(self, lending_type: Optional[str] = None, asset: Optional[str] = None, start_time: Optional[int] = None, end_time: Optional[int] = None): """ - return lending purchases stored in the database. Asset type and time filters can be used + Return lending purchases stored in the database. Asset type and time filters can be used :param lending_type: fetch only purchases from this lending type :type lending_type: Optional[str] @@ -626,7 +784,7 @@ def get_lending_purchases(self, lending_type: Optional[str] = None, asset: Optio def get_last_lending_purchase_time(self, lending_type: Optional[str] = None) -> int: """ - return the latest time when an lending purchase was made. + Return the latest time when an lending purchase was made. If None, return the millistamp corresponding to 2017/01/01 :param lending_type: type of lending @@ -656,7 +814,7 @@ def get_last_lending_purchase_time(self, lending_type: Optional[str] = None) -> def add_lending_interest(self, time: int, lending_type: str, asset: str, amount: float, auto_commit: bool = True): """ - add an lending interest to the database + Add an lending interest to the database :param time: millitstamp of the operation :type time: int @@ -677,7 +835,7 @@ def add_lending_interest(self, time: int, lending_type: str, asset: str, amount: def get_lending_interests(self, lending_type: Optional[str] = None, asset: Optional[str] = None, start_time: Optional[int] = None, end_time: Optional[int] = None): """ - return lending interests stored in the database. Asset type and time filters can be used + Return lending interests stored in the database. Asset type and time filters can be used :param lending_type: fetch only interests from this lending type :type lending_type: Optional[str] @@ -722,7 +880,7 @@ def get_lending_interests(self, lending_type: Optional[str] = None, asset: Optio def get_last_lending_interest_time(self, lending_type: Optional[str] = None) -> int: """ - return the latest time when an interest was received. + Return the latest time when an interest was received. If None, return the millistamp corresponding to 2017/01/01 :param lending_type: type of lending @@ -749,10 +907,10 @@ def get_last_lending_interest_time(self, lending_type: Optional[str] = None) -> return default return result - def add_dust(self, tran_id: str, time: int, asset: str, asset_amount: float, bnb_amount: float, bnb_fee: float, - auto_commit: bool = True): + def add_spot_dust(self, tran_id: str, time: int, asset: str, asset_amount: float, bnb_amount: float, bnb_fee: float, + auto_commit: bool = True): """ - add dust operation to the database + Add dust operation to the database :param tran_id: id of the transaction (non unique) :type tran_id: str @@ -778,7 +936,7 @@ def add_dust(self, tran_id: str, time: int, asset: str, asset_amount: float, bnb def get_spot_dusts(self, asset: Optional[str] = None, start_time: Optional[int] = None, end_time: Optional[int] = None): """ - return dusts stored in the database. Asset type and time filters can be used + Return dusts stored in the database. Asset type and time filters can be used :param asset: fetch only dusts from this asset :type asset: Optional[str] @@ -818,7 +976,7 @@ def get_spot_dusts(self, asset: Optional[str] = None, start_time: Optional[int] def add_dividend(self, div_id: int, div_time: int, asset: str, amount: float, auto_commit: bool = True): """ - add a dividend to the database + Add a dividend to the database :param div_id: dividend id :type div_id: int @@ -839,7 +997,7 @@ def add_dividend(self, div_id: int, div_time: int, asset: str, amount: float, au def get_spot_dividends(self, asset: Optional[str] = None, start_time: Optional[int] = None, end_time: Optional[int] = None): """ - return dividends stored in the database. Asset type and time filters can be used + Return dividends stored in the database. Asset type and time filters can be used :param asset: fetch only dividends of this asset :type asset: Optional[str] @@ -878,7 +1036,7 @@ def get_spot_dividends(self, asset: Optional[str] = None, start_time: Optional[i def get_last_spot_dividend_time(self) -> int: """ - fetch the latest time a dividend has been distributed on the spot account. If None is found, + Fetch the latest time a dividend has been distributed on the spot account. If None is found, return the millistamp corresponding to 2017/1/1 :return: @@ -900,7 +1058,7 @@ def get_last_spot_dividend_time(self) -> int: def add_withdraw(self, withdraw_id: str, tx_id: str, apply_time: int, asset: str, amount: float, fee: float, auto_commit: bool = True): """ - add a withdraw to the database + Add a withdraw to the database :param withdraw_id: binance if of the withdraw :type withdraw_id: str @@ -925,7 +1083,7 @@ def add_withdraw(self, withdraw_id: str, tx_id: str, apply_time: int, asset: str def get_spot_withdraws(self, asset: Optional[str] = None, start_time: Optional[int] = None, end_time: Optional[int] = None): """ - return withdraws stored in the database. Asset type and time filters can be used + Return withdraws stored in the database. Asset type and time filters can be used :param asset: fetch only withdraws of this asset :type asset: Optional[str] @@ -966,7 +1124,7 @@ def get_spot_withdraws(self, asset: Optional[str] = None, start_time: Optional[i def get_last_spot_withdraw_time(self) -> int: """ - fetch the latest time a withdraw has been made on the spot account. If None is found, return the millistamp + Fetch the latest time a withdraw has been made on the spot account. If None is found, return the millistamp corresponding to 2017/1/1 :return: @@ -986,7 +1144,7 @@ def get_last_spot_withdraw_time(self) -> int: def add_deposit(self, tx_id: str, insert_time: int, amount: float, asset: str, auto_commit=True): """ - add a deposit to the database + Add a deposit to the database :param tx_id: transaction id :type tx_id: str @@ -1007,7 +1165,7 @@ def add_deposit(self, tx_id: str, insert_time: int, amount: float, asset: str, a def get_spot_deposits(self, asset: Optional[str] = None, start_time: Optional[int] = None, end_time: Optional[int] = None): """ - return deposits stored in the database. Asset type and time filters can be used + Return deposits stored in the database. Asset type and time filters can be used :param asset: fetch only deposits of this asset :type asset: Optional[str] @@ -1046,7 +1204,7 @@ def get_spot_deposits(self, asset: Optional[str] = None, start_time: Optional[in def get_last_spot_deposit_time(self) -> int: """ - fetch the latest time a deposit has been made on the spot account. If None is found, return the millistamp + Fetch the latest time a deposit has been made on the spot account. If None is found, return the millistamp corresponding to 2017/1/1 :return: last deposit millistamp @@ -1067,12 +1225,13 @@ def get_last_spot_deposit_time(self) -> int: return result def add_trade(self, trade_type: str, trade_id: int, trade_time: int, asset: str, ref_asset: str, qty: float, - price: float, fee: float, fee_asset: str, is_buyer: bool, auto_commit=True): + price: float, fee: float, fee_asset: str, is_buyer: bool, symbol: Optional[str] = None, + auto_commit: bool = True): """ - add a trade to the database + Add a trade to the database :param trade_type: type trade executed - :type trade_type: string, must be one of {'spot', 'cross_margin'} + :type trade_type: string, must be one of {'spot', 'cross_margin', 'isolated_margin'} :param trade_id: id of the trade (binance id, unique per trading pair) :type trade_id: int :param trade_time: millistamp of the trade @@ -1091,6 +1250,8 @@ def add_trade(self, trade_type: str, trade_id: int, trade_time: int, asset: str, :type fee_asset: str :param is_buyer: if the trade is a buy or a sell :type is_buyer: bool + :param symbol: trading symbol, mandatory if thr trade_type is isolated margin + :type symbol: Optional[str] :param auto_commit: if the database should commit the change made, default True :type auto_commit: bool :return: None @@ -1101,17 +1262,24 @@ def add_trade(self, trade_type: str, trade_id: int, trade_time: int, asset: str, table = tables.SPOT_TRADE_TABLE elif trade_type == 'cross_margin': table = tables.CROSS_MARGIN_TRADE_TABLE + elif trade_type == 'isolated_margin': + table = tables.ISOLATED_MARGIN_TRADE_TABLE + if symbol is None: + raise ValueError("trade_type was isolated margin but symbol was not provided") + row = (trade_id, trade_time, symbol, asset, ref_asset, qty, price, fee, fee_asset, int(is_buyer)) else: - raise ValueError(f"trade type should be one of ('spot', 'cross_margin') but {trade_type} was received") + msg = f"trade type should be one of ('spot', 'cross_margin', 'isolated_margin') but {trade_type} was" \ + f" received" + raise ValueError(msg) self.add_row(table, row, auto_commit) def get_trades(self, trade_type: str, start_time: Optional[int] = None, end_time: Optional[int] = None, asset: Optional[str] = None, ref_asset: Optional[str] = None): """ - return trades stored in the database. asset type, ref_asset type and time filters can be used + Return trades stored in the database. asset type, ref_asset type and time filters can be used :param trade_type: type trade executed - :type trade_type: string, must be one of ('spot', 'cross_margin') + :type trade_type: string, must be one of ('spot', 'cross_margin', 'isolated_margin') :param start_time: fetch only trades after this millistamp :type start_time: Optional[int] :param end_time: fetch only trades before this millistamp @@ -1123,6 +1291,8 @@ def get_trades(self, trade_type: str, start_time: Optional[int] = None, end_time :return: The raw rows selected as saved in the database :rtype: List[Tuple] + Return for spot and cross margin: + .. code-block:: python [ @@ -1137,13 +1307,35 @@ def get_trades(self, trade_type: str, start_time: Optional[int] = None, end_time 0), # is_buyer ] + Return for isolated margin: + + .. code-block:: python + + [ + (384518832, # trade_id + 1582892988052, # trade time + 'BTCUSDT', # symbol + 'BTC', # asset + 'USDT', # ref asset + 0.0015, # asset quantity + 9011.2, # asset price to ref asset + 0.01425, # fee + 'USDT', # fee asset + 0), # is_buyer + ] + + """ if trade_type == 'spot': table = tables.SPOT_TRADE_TABLE elif trade_type == 'cross_margin': table = tables.CROSS_MARGIN_TRADE_TABLE + elif trade_type == 'isolated_margin': + table = tables.ISOLATED_MARGIN_TRADE_TABLE else: - raise ValueError(f"trade type should be one of ('spot', 'cross_margin') but {trade_type} was received") + msg = f"trade type should be one of ('spot', 'cross_margin', 'isolated_margin') but {trade_type} was" \ + f" received" + raise ValueError(msg) conditions_list = [] if start_time is not None: conditions_list.append((table.tdTime, @@ -1165,7 +1357,7 @@ def get_trades(self, trade_type: str, start_time: Optional[int] = None, end_time def get_max_trade_id(self, asset: str, ref_asset: str, trade_type: str) -> int: """ - return the latest trade id for a trading pair. If none is found, return -1 + Return the latest trade id for a trading pair. If none is found, return -1 :param asset: name of the asset in the trading pair (ex 'BTC' for 'BTCUSDT') :type asset: string @@ -1180,8 +1372,12 @@ def get_max_trade_id(self, asset: str, ref_asset: str, trade_type: str) -> int: table = tables.SPOT_TRADE_TABLE elif trade_type == 'cross_margin': table = tables.CROSS_MARGIN_TRADE_TABLE + elif trade_type == 'isolated_margin': + table = tables.ISOLATED_MARGIN_TRADE_TABLE else: - raise ValueError(f"trade type should be one of {'spot', 'cross_margin'} but {trade_type} was received") + msg = f"trade type should be one of ('spot', 'cross_margin', 'isolated_margin') but {trade_type} was" \ + f" received" + raise ValueError(msg) selection = f"MAX({table.tradeId})" conditions_list = [ diff --git a/BinanceWatch/storage/tables.py b/BinanceWatch/storage/tables.py index dac3cc9..e1312e2 100644 --- a/BinanceWatch/storage/tables.py +++ b/BinanceWatch/storage/tables.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass from typing import List, Optional @@ -6,6 +5,7 @@ class Table: """ This class represent a table in a database. All columns names are dynamic attributes @DynamicAttrs + This class is used to describe the tables that will be used to in the database """ def __init__(self, name: str, columns_names: List[str], columns_sql_types: List[str], @@ -270,6 +270,113 @@ def __init__(self, name: str, columns_names: List[str], columns_sql_types: List[ ] ) +ISOLATED_MARGIN_TRADE_TABLE = Table( + 'isolated_margin_trade', + [ + 'tradeId', + 'tdTime', + 'symbol', + 'asset', + 'refAsset', + 'qty', + 'price', + 'fee', + 'feeAsset', + 'isBuyer' + + ], + [ + 'INTEGER', + 'INTEGER', + 'TEXT', + 'TEXT', + 'TEXT', + 'REAL', + 'REAL', + 'REAL', + 'TEXT', + 'INTEGER' + ] +) + +ISOLATED_MARGIN_LOAN_TABLE = Table( + "isolated_margin_loan_table", + [ + 'loanTime', + 'symbol', + 'asset', + 'principal', + ], + [ + 'INTEGER', + 'TEXT', + 'TEXT', + 'REAL' + ], + primary_key='txId', + primary_key_sql_type='INTEGER' + +) + +ISOLATED_MARGIN_REPAY_TABLE = Table( + "isolated_margin_repay_table", + [ + 'repayTime', + 'symbol', + 'asset', + 'principal', + 'interest', + ], + [ + 'INTEGER', + 'TEXT', + 'TEXT', + 'REAL', + 'REAL' + ], + primary_key='txId', + primary_key_sql_type='INTEGER' + +) + +ISOLATED_MARGIN_INTEREST_TABLE = Table( + "isolated_margin_interest_table", + [ + 'interestTime', + 'symbol', + 'asset', + 'interest', + 'interestType' + ], + [ + 'INTEGER', + 'TEXT', + 'TEXT', + 'REAL', + 'TEXT' + ] +) + +ISOLATED_MARGIN_TRANSFER_TABLE = Table( + "isolated_margin_table", + [ + 'trfType', + 'trfTime', + 'symbol', + 'asset', + 'amount' + ], + [ + 'TEXT', + 'INTEGER', + 'TEXT', + 'TEXT', + 'REAL' + ], + primary_key='tranId', + primary_key_sql_type='INTEGER' +) + UNIVERSAL_TRANSFER_TABLE = Table( "universal_transfer_table", [ diff --git a/README.rst b/README.rst index e222a16..7b87a27 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ =================================== -Welcome to BinanceWatch v0.1.3 +Welcome to BinanceWatch v0.1.4 =================================== Note @@ -49,6 +49,15 @@ It currently supports: - Cross Margin Loans - Cross Margin Interests + +- Isolated Margin Trades +- Isolated Margin Repayment +- Isolated Margin Loans +- Isolated Margin Interests +- Isolated Margin Transfers **\*** + +**\***: see ``Known Issues`` section below. + Quick Tour ---------- @@ -131,3 +140,4 @@ Some endpoints are not yet provided by Binance, so they can't be implemented in - Fiat withdraws and deposits - Locked stacking history - Direct purchases with debit card +- Some isolated margin transfers are not picked up by the API, the reason is unknown at the moment (**I am looking for testers**) diff --git a/docs/source/conf.py b/docs/source/conf.py index d697de0..5b41b7b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -56,4 +56,4 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] \ No newline at end of file +html_static_path = ['_static']