I'm trying to fetch internal asset transfer history from Binance, specifically transfers between my Funding Wallet and Spot Wallet, using the python-binance library. I'm interested in FUNDING_MAIN (Funding to Spot) and MAIN_FUNDING (Spot to Funding) transfer types. I've confirmed with a direct curl command that FUNDING_MAIN transfers for USDT exist for my account and API key within a specific time range. Here's the successful curl command structure (API keys are illustrative): # Illustrative curl command (API keys/secret are examples) api_key="YOUR_API_KEY" api_secret="YOUR_API_SECRET" timestamp=$(date +%s%3N) # Example startTime/endTime (Unix ms) # startTime=1748239900000 (approx May 23, 2025 06:11:40 AM GMT) # endTime=1748253200000 (approx May 23, 2025 09:53:20 AM GMT) query="asset=USDT&type=FUNDING_MAIN&startTime=${startTime_ms}&endTime=${endTime_ms}×tamp=$timestamp" signature=$(echo -n "$query" | openssl dgst -sha256 -hmac "$api_secret" | awk '{print $2}') curl -H "X-MBX-APIKEY: $api_key" -X GET "https://api.binance.com/sapi/v1/asset/transfer?$query&signature=$signature" This curl command successfully returns JSON data with a total count and rows of transfers, for example: {"total":3,"rows":[{"timestamp":1748253169000,"asset":"USDT","amount":"3.25206804","type":"FUNDING_MAIN","status":"CONFIRMED","tranId":264973612117}, /* ...more rows... */ ]} However, when I try to fetch this using python-binance's client.get_universal_transfer_history() method, I consistently get 0 results for these transfer types. Here's a simplified version of my Python function: from binance.client import Client from binance.exceptions import BinanceAPIException import datetime import time import pandas as pd import requests # for requests.exceptions import logging # Assume client is initialized: client = Client(api_key, api_secret) # Assume self.stablecoin_symbols and self.symbol_mappings are defined # Assume self._get_coingecko_historical_price is defined logger = logging.getLogger(__name__) # Basic logger class PortfolioTracker: def __init__(self, client, config): # Simplified self.binance_client = client self.config = config self.stablecoin_symbols = ["USDT", "USDC"] # Example self.symbol_mappings = {"USDT": "tether"} # Example for Coingecko ID self.norm_map = {} # For symbol normalization def _get_coingecko_historical_price(self, coin_id, date_str): # Dummy implementation for this example logger.debug(f"Fetching Coingecko price for {coin_id} on {date_str}") time.sleep(self.config.get("apis", {}).get("coingecko", {}).get("request_delay_ms_internal_transfer", 1000) / 1000.0) if coin_id == "tether": return 1.0 return 0.05 # Dummy price def fetch_internal_transfers(self, days_back: int = 365) -> list: if not self.binance_client: logger.warning("Binance client not initialized.") return [] all_processed_transfers = [] transfer_configs = [ {"api_type": "FUNDING_MAIN", "flow_type": "DEPOSIT", "log_label": "Funding to Spot"}, {"api_type": "MAIN_FUNDING", "flow_type": "WITHDRAWAL", "log_label": "Spot to Funding"} ] batch_days = self.config.get("apis", {}).get("binance", {}).get("transfer_history_batch_days", 29) limit_per_batch = 100 max_retries = self.config.get("apis", {}).get("binance", {}).get("max_retries_per_batch", 3) retry_delay_seconds = self.config.get("apis", {}).get("binance", {}).get("retry_delay_sec", 30) cg_delay_ms = self.config.get("apis", {}).get("coingecko", {}).get("request_delay_ms_internal_transfer", 10000) binance_api_delay_ms = self.config.get("apis", {}).get("binance", {}).get("request_delay_ms", 700) overall_end_time_dt = datetime.datetime.now(datetime.timezone.utc) overall_start_time_dt = overall_end_time_dt - datetime.timedelta(days=days_back) processed_tran_ids = set() for config_entry in transfer_configs: api_type = config_entry["api_type"] transaction_flow_type = config_entry["flow_type"] log_label = config_entry["log_label"] logger.info(f"Fetching internal transfers for type: {api_type} ({log_label}). Lookback: {days_back} days.") current_batch_end_time_dt = overall_end_time_dt while current_batch_end_time_dt > overall_start_time_dt: current_batch_start_time_dt = current_batch_end_time_dt - datetime.timedelta(days=batch_days) if current_batch_start_time_dt < overall_start_time_dt: current_batch_start_time_dt = overall_start_time_dt start_ms = int(current_batch_start_time_dt.timestamp() * 1000) end_ms = int(current_batch_end_time_dt.timestamp() * 1000) if start_ms >= end_ms: break logger.debug(f"Fetching {api_type} batch: {current_batch_start_time_dt.strftime('%Y-%m-%d %H:%M')} to {current_batch_end_time_dt.strftime('%Y-%m-%d %H:%M')}") retries_left = max_retries fetched_rows = None while retries_left > 0: try: # Attempting to fetch for ALL assets for this type and time window transfer_history = self.binance_client.get_universal_transfer_history( type=api_type, startTime=start_ms, endTime=end_ms, size=limit_per_batch ) fetched_rows = transfer_history.get('rows', []) logger.debug(f"Successfully fetched {api_type} batch. Count: {len(fetched_rows) if fetched_rows else 0}") break except (requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError) as e: retries_left -= 1 logger.error(f"Network error fetching {api_type}: {e}. Retries left: {retries_left}.") if retries_left > 0: time.sleep(retry_delay_seconds) else: logger.error(f"Failed to fetch {api_type} batch after multiple retries.") except BinanceAPIException as e: # Make sure to import this logger.error(f"Binance API Error fetching {api_type}: {e}.") retries_left = 0 except Exception as e: logger.error(f"Unexpected error fetching {api_type}: {e}", exc_info=True) retries_left = 0 if fetched_rows: for item in fetched_rows: # ... (processing logic as provided in my script) ... pass # Simplified for brevity current_batch_end_time_dt = current_batch_start_time_dt - datetime.timedelta(milliseconds=1) if fetched_rows is not None: time.sleep(binance_api_delay_ms / 1000.0) logger.info(f"Fetched a total of {len(all_processed_transfers)} internal transactions.") return all_processed_transfers What I've tried/checked: API keys have "Enable Reading" and "Enable Universal Transfer" permissions. The curl test confirms they work for this endpoint. Tested with type="FUNDING_MAIN" and type="MAIN_FUNDING". Ensured startTime and endTime are in milliseconds. The python-binance library is up-to-date. Other API calls (e.g., get_my_trades, get_deposit_history) work fine with the same client instance. *My curl test for FUNDING_MAIN with asset=USDT works, but the Python code iterating assets (including USDT) or trying to fetch all assets for FUNDING_MAIN returns 0. My Questions: Is there a known reason why client.get_universal_transfer_history might return an empty list for type="FUNDING_MAIN" (or MAIN_FUNDING) when a direct API call with similar parameters successfully returns data? Are there specific nuances or required parameters for get_universal_transfer_history with these funding-related types that I might be missing compared to a direct curl construction? (e.g., does asset behave differently if omitted for these types?) Could there be an underlying issue in how python-binance constructs or signs the request for this specific endpoint/type combination? What's the recommended way to reliably fetch all FUNDING_MAIN and MAIN_FUNDING transfers (across all relevant assets like USDT, BTC, ETH, etc.) using python-binance? Should I be iterating assets, or should querying without an asset parameter work for these types? Any insights or suggestions would be greatly appreciated! Continue reading...