2022-03-20 10:09:33 +01:00 | | | # IMPORTS
|
| | | import pandas as pd
|
| | | import math
|
| | | import os.path
|
| | | import json
|
| | | import time
|
| | | from datetime import timedelta, datetime
|
| | | from dateutil import parser
|
| | | from tqdm import tqdm_notebook #(Optional, used for progress-bars)
|
| | | import click
|
| | |
|
| | | #import aiohttp
|
| | | import asyncio
|
| | |
|
| | | from binance.client import Client
|
| | | from binance.streams import BinanceSocketManager
|
| | | from binance.enums import *
|
| | |
|
| | | # DO NOT INCLUDE because it has function klines_to_df with the same name but different implementation (name conflict)
|
| | | #from common.utils import *
|
| | | from service.App import *
|
| | |
|
| | | """
|
| | | The script is intended for retrieving data from binance server: klines, server info etc.
|
| | |
|
| | | Links:
|
| | | https://sammchardy.github.io/binance/2018/01/08/historical-data-download-binance.html
|
| | | https://sammchardy.github.io/kucoin/2018/01/14/historical-data-download-kucoin.html
|
| | | """
|
| | |
|
| | | ### CONSTANTS
|
| | | binsizes = {"1m": 1, "5m": 5, "1h": 60, "1d": 1440}
|
| | | batch_size = 750
|
| | |
|
| | | symbols = ["XBTUSD", "ETHUSD", "XRPZ18", "LTCZ18", "EOSZ18", "BCHZ18", "ADAZ18", "TRXZ18"]
|
| | |
|
| | | App.client = Client(api_key=App.config["api_key"], api_secret=App.config["api_secret"])
|
| | |
|
| | |
|
| | | #
|
| | | # Historic data
|
| | | #
|
| | |
|
| | | @click.command()
|
| | | @click.option('--config_file', '-c', type=click.Path(), default='', help='Configuration file name')
|
| | | def main(config_file):
|
| | | """
|
| | | Retrieving historic klines from binance server.
|
| | |
|
| | | Client.get_historical_klines
|
| | | """
|
| | | load_config(config_file)
|
| | |
|
2022-08-14 20:40:43 +02:00 | | | time_column = App.config["time_column"]
|
| | |
|
| | | data_path = Path(App.config["data_folder"])
|
2022-03-20 10:09:33 +01:00 | | |
|
2022-08-14 20:40:43 +02:00 | | | now = datetime.now()
|
| | |
|
| | | freq = "1m"
|
| | | save = True
|
| | |
|
| | | futures = False
|
2022-03-20 10:09:33 +01:00 | | | if futures:
|
| | | App.client.API_URL = "https://fapi.binance.com/fapi"
|
| | | App.client.PRIVATE_API_VERSION = "v1"
|
| | | App.client.PUBLIC_API_VERSION = "v1"
|
| | |
|
2022-08-14 20:40:43 +02:00 | | | data_sources = App.config["data_sources"]
|
| | | for ds in data_sources:
|
| | | # Assumption: folder name is equal to the symbol name we want to download
|
| | | quote = ds.get("folder")
|
| | | if not quote:
|
| | | print(f"ERROR. Folder is not specified.")
|
| | | continue
|
2022-03-20 10:09:33 +01:00 | | |
|
2022-08-14 20:40:43 +02:00 | | | print(f"Start downloading '{quote}' ...")
|
2022-03-20 10:09:33 +01:00 | | |
|
2023-08-19 15:24:06 +02:00 | | | file_path = data_path / quote
|
| | | file_path.mkdir(parents=True, exist_ok=True) # Ensure that folder exists
|
2023-08-07 20:22:57 +02:00 | | |
|
2023-08-19 15:43:04 +02:00 | | | file_name = (file_path / ("futures" if futures else "klines")).with_suffix(".csv")
|
2023-08-07 20:22:57 +02:00 | | |
|
| | | if file_name.is_file():
|
2023-08-19 15:24:06 +02:00 | | | # Load the existing data in order to append newly downloaded data
|
2023-08-07 20:22:57 +02:00 | | | data_df = pd.read_csv(file_name)
|
2023-08-19 15:24:06 +02:00 | | | data_df[time_column] = pd.to_datetime(data_df[time_column], format='ISO8601')
|
2023-08-07 20:22:57 +02:00 | | | print(f"File found. Downloaded data will be appended to the existing file {file_name}")
|
2022-08-14 20:40:43 +02:00 | | | else:
|
2023-08-19 15:24:06 +02:00 | | | # No existing data so we will download all available data and store as a new file
|
2022-08-14 20:40:43 +02:00 | | | data_df = pd.DataFrame()
|
| | | print(f"File not found. All data will be downloaded and stored in newly created file.")
|
2022-03-20 10:09:33 +01:00 | | |
|
2022-08-14 20:40:43 +02:00 | | | oldest_point, newest_point = minutes_of_new_data(quote, freq, data_df)
|
2022-03-20 10:09:33 +01:00 | | |
|
2022-08-14 20:40:43 +02:00 | | | delta_min = (newest_point - oldest_point).total_seconds() / 60
|
2022-03-20 10:09:33 +01:00 | | |
|
2022-08-14 20:40:43 +02:00 | | | available_data = math.ceil(delta_min / binsizes[freq])
|
2022-03-20 10:09:33 +01:00 | | |
|
2022-08-14 20:40:43 +02:00 | | | if oldest_point == datetime.strptime('1 Jan 2017', '%d %b %Y'):
|
| | | print('Downloading all available %s data for %s. Be patient..!' % (freq, quote))
|
| | | else:
|
| | | print('Downloading %d minutes of new data available for %s, i.e. %d instances of %s data.' % (delta_min, quote, available_data, freq))
|
2022-03-20 10:09:33 +01:00 | | |
|
2023-08-19 15:24:06 +02:00 | | | # === Download from the remote server using binance client
|
2022-08-14 20:40:43 +02:00 | | | klines = App.client.get_historical_klines(
|
| | | quote,
|
| | | freq,
|
| | | oldest_point.strftime("%d %b %Y %H:%M:%S"),
|
| | | newest_point.strftime("%d %b %Y %H:%M:%S")
|
| | | )
|
2022-03-20 10:09:33 +01:00 | | |
|
2022-08-14 20:40:43 +02:00 | | | data_df = klines_to_df(klines, data_df)
|
2022-03-20 10:09:33 +01:00 | | |
|
2022-08-14 20:40:43 +02:00 | | | if save:
|
2023-08-07 20:22:57 +02:00 | | | data_df.to_csv(file_name)
|
2022-03-20 10:09:33 +01:00 | | |
|
2023-08-07 20:22:57 +02:00 | | | print(f"Finished downloading '{quote}'. Stored in '{file_name}'")
|
2022-03-20 10:09:33 +01:00 | | |
|
2022-08-14 20:40:43 +02:00 | | | elapsed = datetime.now() - now
|
| | | print(f"Finished downloading data in {str(elapsed).split('.')[0]}")
|
2022-03-20 10:09:33 +01:00 | | |
|
| | | return data_df
|
| | |
|
| | |
|
| | | #
|
| | | # Static data
|
| | | #
|
| | |
|
| | | def get_exchange_info():
|
| | | """
|
| | | Client.get_exchange_info
|
| | | /api/v1/exchangeInfo
|
| | | """
|
| | | exchange_info = App.client.get_exchange_info()
|
| | |
|
| | | with open("exchange_info.json", "w") as file:
|
| | | json.dump(exchange_info, file, indent=4) # , sort_keys=True
|
| | |
|
| | | return
|
| | |
|
| | |
|
| | | def get_account_info():
|
| | | orders = App.client.get_all_orders(symbol='BTCUSDT')
|
| | | trades = App.client.get_my_trades(symbol='BTCUSDT')
|
| | | info = App.client.get_account()
|
| | | status = App.client.get_account_status()
|
| | | details = App.client.get_asset_details()
|
| | |
|
| | |
|
| | | def get_market_info():
|
| | | depth = App.client.get_order_book(symbol='BTCUSDT')
|
| | |
|
| | |
|
| | | #
|
| | | # Utility
|
| | | #
|
| | |
|
| | | def minutes_of_new_data(symbol, freq, data):
|
| | | if len(data) > 0:
|
| | | #old = parser.parse(data["timestamp"].iloc[-1])
|
| | | old = data["timestamp"].iloc[-1]
|
| | | else:
|
| | | #old = datetime.strptime('1 Aug 2019', '%d %b %Y')
|
| | | old = datetime.strptime('1 Jan 2017', '%d %b %Y')
|
| | |
|
| | | # List of tuples like this: [1569728580000, '8156.65000000', '8156.66000000', '8154.75000000', '8155.32000000', '4.63288700', 1569728639999, '37786.23994297', 74, '3.18695100', '25993.68396886', '0']
|
| | | new_info = App.client.get_klines(symbol=symbol, interval=freq)
|
2023-08-25 22:27:28 +02:00 | | | new = pd.to_datetime(new_info[-1][0], unit='ms')
|
2022-03-20 10:09:33 +01:00 | | |
|
| | | return old, new
|
| | |
|
| | |
|
| | | # NOTE: this function is different from same in trade.utils
|
| | | def klines_to_df(klines, df):
|
| | |
|
| | | data = pd.DataFrame(klines, columns = ['timestamp', 'open', 'high', 'low', 'close', 'volume', 'close_time', 'quote_av', 'trades', 'tb_base_av', 'tb_quote_av', 'ignore' ])
|
2023-08-25 22:27:28 +02:00 | | | data['timestamp'] = pd.to_datetime(data['timestamp'], unit='ms')
|
2022-03-20 10:09:33 +01:00 | | | dtypes = {
|
| | | 'open': 'float64', 'high': 'float64', 'low': 'float64', 'close': 'float64', 'volume': 'float64',
|
| | | 'close_time': 'int64',
|
| | | 'quote_av': 'float64',
|
| | | 'trades': 'int64',
|
| | | 'tb_base_av': 'float64',
|
| | | 'tb_quote_av': 'float64',
|
| | | 'ignore': 'float64',
|
| | | }
|
| | | data = data.astype(dtypes)
|
| | |
|
| | | if df is None or len(df) == 0:
|
| | | df = data
|
| | | else:
|
2022-04-16 18:22:55 +02:00 | | | df = pd.concat([df, data])
|
2022-03-20 10:09:33 +01:00 | | |
|
| | | # Drop duplicates
|
| | | df = df.drop_duplicates(subset=["timestamp"])
|
| | | #df = df[~df.index.duplicated(keep='last')] # alternatively, drop duplicates in index
|
| | |
|
| | | df.set_index('timestamp', inplace=True)
|
| | |
|
| | | return df
|
| | |
|
| | |
|
| | | #
|
| | | # Streaming functions (do not work - for test purposes)
|
| | | #
|
| | |
|
| | | # !!! DOES NOT WORK - do not use
|
| | | async def get_futures_klines_all(symbol, freq, save = False):
|
| | | """
|
| | | https://binance-docs.github.io/apidocs/testnet/en/#kline-candlestick-data
|
| | | https://fapi.binance.com - production
|
| | | https://testnet.binancefuture.com - test
|
| | | GET /fapi/v1/exchangeInfo: to get a list of symbolc
|
| | | GET /fapi/v1/klines: symbol*, interval*, startTime, endTime, limit
|
| | | """
|
| | |
|
2022-04-17 11:34:34 +02:00 | | | filename = f"futures.csv"
|
2022-03-20 10:09:33 +01:00 | | |
|
| | | if os.path.isfile(filename):
|
| | | data_df = pd.read_csv(filename)
|
| | | else:
|
| | | data_df = pd.DataFrame()
|
| | |
|
| | | # INFO:
|
| | | # API-keys are passed into the Rest API via the X-MBX-APIKEY header
|
| | | # curl -H "X-MBX-APIKEY: vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A"
|
| | | # API-secret is used to create a HMAC SHA256 signature and then pass it in the signature parameter
|
| | | #[linux]$ echo -n "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantüity=1&price=0.1&recvWindow=5000×tamp=1499827319559" | openssl dgst -sha256 -hmac "API-secret"
|
| | | #(stdin)= c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71 - signature
|
| | |
|
| | | import hmac
|
| | | import hashlib
|
| | | import urllib.parse
|
| | |
|
| | | headers = {"X-MBX-APIKEY": binance_api_key}
|
| | |
|
| | | params = {}
|
| | | query = urllib.parse.urlencode(params) # These our parameters passed in url
|
| | | signature = hmac.new(
|
| | | binance_api_secret.encode("utf8"),
|
| | | query.encode("utf8"),
|
| | | digestmod=hashlib.sha256
|
| | | ).hexdigest()
|
| | | params["signature"] = signature # We simply add signature to the parametes
|
| | |
|
| | | #
|
| | | # Test connection (ping server): GET /fapi/v1/ping
|
| | | #
|
| | | url = "https://fapi.binance.com/fapi/v1/exchangeInfo"
|
| | | async with aiohttp.ClientSession(headers=headers) as session:
|
| | | async with session.get(url, params=params) as response:
|
| | | print(response.status)
|
| | | result = await response.json()
|
| | |
|
| | | print(result)
|
| | |
|
| | | #
|
| | | # /fapi/v1/klines: symbol*, interval*, startTime, endTime, limit
|
| | | #
|
| | |
|
| | | start = 1483228800000 # 1483228800000 2017
|
| | | end = 1569888000000 # 1569888000000 2019-10-7, 1569888000000 - 2019-10-1
|
| | |
|
| | | params = {"symbol": symbol, "interval": freq, "startTime": start, "endTime": end}
|
| | | query = urllib.parse.urlencode(params) # These our parameters passed in url
|
| | | signature = hmac.new(
|
| | | binance_api_secret.encode("utf8"),
|
| | | query.encode("utf8"),
|
| | | digestmod=hashlib.sha256
|
| | | ).hexdigest()
|
| | | params["signature"] = signature # We simply add signature to the parametes
|
| | |
|
| | | url = "https://fapi.binance.com/fapi/v1/klines"
|
| | |
|
| | | async with aiohttp.ClientSession(headers=headers) as session:
|
| | | async with session.get(url, params=params) as response:
|
| | | print(response.status)
|
| | | klines = await response.json()
|
| | |
|
| | | data_df = klines_to_df(klines, None)
|
| | |
|
| | | if save:
|
| | | data_df.to_csv(filename)
|
| | |
|
| | | pass
|
| | |
|
| | |
|
| | | def check_market_stream():
|
| | | """
|
| | | Streams:
|
| | | - depth (order book):
|
| | | 1) Order book price and quantity depth updates used to locally keep an order book
|
| | | 2) Top 20 levels of bids and asks
|
| | | - kline: The kline/candlestick stream pushes updates to the current klines/candlestick every second
|
| | | - ticker,
|
| | | - trade,
|
| | | """
|
| | | bm = BinanceSocketManager(App.client)
|
| | |
|
| | | # trade socket (one event for each new trade - quote intensive)
|
| | | conn_key = bm.start_trade_socket('BTCUSDT', message_fn)
|
| | | # {'e': 'trade', 'E': 1570114844102, 's': 'BTCUSDT', 't': 185892444, 'p': '8090.33000000', 'q': '0.00247300', 'b': 682929714, 'a': 682929708, 'T': 1570114844098, 'm': False, 'M': True}
|
| | |
|
| | | # depths sockets: either partial book or diff response
|
| | | # partial book response
|
| | | #conn_key = bm.start_depth_socket('BTCUSDT', message_fn, depth=BinanceSocketManager.WEBSOCKET_DEPTH_5)
|
| | | # Output: {'lastUpdateId': 1142001769, 'bids': [['8097.09000000', '0.02469400'], ['8097.07000000', '0.13411300'], ['8097.02000000', '2.00000000'], ['8097.01000000', '0.55502000'], ['8096.91000000', '0.29088400']], 'asks': [['8099.01000000', '0.02640000'], ['8099.02000000', '0.06944200'], ['8099.07000000', '0.08537200'], ['8099.28000000', '0.50000000'], ['8100.54000000', '0.00372200']]}
|
| | | # depth diff response
|
| | | #conn_key = bm.start_depth_socket('BTCUSDT', message_fn)
|
| | |
|
| | | # kline socket
|
| | | conn_key = bm.start_kline_socket('BTCUSDT', message_fn, interval=KLINE_INTERVAL_30MINUTE)
|
| | | # {'e': 'kline', 'E': 1570114725899, 's': 'BTCUSDT', 'k': {'t': 1570113000000, 'T': 1570114799999, 's': 'BTCUSDT', 'i': '30m', 'f': 185878257, 'L': 185891547, 'o': '8114.88000000', 'c': '8102.91000000', 'h': '8124.50000000', 'l': '8065.00000000', 'v': '1888.20453300', 'n': 13291, 'x': False, 'q': '15291535.03269335', 'V': '1004.09638500', 'Q': '8131835.50527485', 'B': '0'}}
|
| | |
|
| | | # Aggregated Trade Socket
|
| | | #conn_key = bm.start_aggtrade_socket('BTCUSDT', message_fn)
|
| | | # Trade Socket
|
| | | #conn_key = bm.start_trade_socket('BTCUSDT', message_fn)
|
| | |
|
| | | # Symbol ticker Socket
|
| | | #conn_key = bm.start_symbol_ticker_socket('BTCUSDT', message_fn)
|
| | | # Output: {'e': '24hrTicker', 'E': 1570114641432, 's': 'BTCUSDT', 'p': '-157.27000000', 'P': '-1.907', 'w': '8247.56704052', 'x': '8247.57000000', 'c': '8090.30000000', 'Q': '0.07074700', 'b': '8089.03000000', 'B': '0.09080800', 'a': '8090.29000000', 'A': '2.00000000', 'o': '8247.57000000', 'h': '8393.00000000', 'l': '8060.00000000', 'v': '28067.68120000', 'q': '231490082.36887328', 'O': 1570028241429, 'C': 1570114641429, 'F': 185628703, 'L': 185890956, 'n': 262254}
|
| | |
|
| | | # Ticker Socket
|
| | | #conn_key = bm.start_ticker_socket(message_fn)
|
| | |
|
| | | # Mini Ticker Socket
|
| | | # by default updates every second
|
| | | #conn_key = bm.start_miniticker_socket(message_fn)
|
| | | # this socket can take an update interval parameter
|
| | | # set as 5000 to receive updates every 5 seconds
|
| | | #conn_key = bm.start_miniticker_socket(message_fn, 5000)
|
| | |
|
| | | #
|
| | | # then start the socket manager
|
| | | #
|
| | | bm.start()
|
| | |
|
| | | time.sleep(10)
|
| | |
|
| | | # Close individual socket
|
| | | bm.stop_socket(conn_key)
|
| | |
|
| | | # stop all sockets and end the manager call close after doing this a start call would be required to connect any new sockets.
|
| | | bm.close()
|
| | |
|
| | | # to exist, stop twisted reactor loop - otherwise the code will NOT exist when you expect
|
| | | from twisted.internet import reactor
|
| | | reactor.stop()
|
| | |
|
| | |
|
| | | def message_fn(msg):
|
| | | #print(f"Message type: {msg['e']}")
|
| | | print(msg)
|
| | |
|
| | |
|
| | | def check_market_stream_multiplex():
|
| | | """
|
| | | Symbols in socket name must be lowercase i.e bnbbtc@aggTrade, neobtc@ticker
|
| | | """
|
| | | bm = BinanceSocketManager(App.client)
|
| | | conn_key = bm.start_multiplex_socket(['bnbbtc@aggTrade', 'neobtc@ticker'], multiples_fn)
|
| | |
|
| | |
|
| | | def multiples_fn(msg):
|
| | | print("stream: {} data: {}".format(msg['stream'], msg['data']))
|
| | |
|
| | |
|
| | | def check_user_stream():
|
| | | """
|
| | | User streams (requires extra authentication):
|
| | | - Account Update Event - Return account updates
|
| | | - Order Update Event - Returns individual order updates
|
| | | - Trade Update Event - Returns individual trade updates
|
| | | """
|
| | | bm = BinanceSocketManager(App.client)
|
| | |
|
| | | # The Manager handles keeping the socket alive.
|
| | | bm.start_user_socket(user_message_fn)
|
| | |
|
| | |
|
| | | def user_message_fn(msg):
|
| | | print(f"Message type: {msg['e']}")
|
| | | print(msg)
|
| | |
|
| | |
|
| | | if __name__ == '__main__':
|
| | | main()
|