바이낸스 비트코인 선물 Kline/Candle 파일로 다운받아 합치기

Python으로 바이낸스 3년간의 비트코인 선물 1분지표 Candle 데이터를 파일로 빠르게 다운받아 머신러닝에 필요한 데이터를 얻어 낼 수 있게 되었다.

하지만 zip 파일로 다운로드만 빠르게 되었을뿐, 다운로드 받은 zip파일들의 압축을 풀고 하나의 단일 csv파일로 만들어줘야 하는데 이 작업을 수동으로 매번 하는 것은 나의 취향과 맞지 않는다.

다운받은 Kines/Candle 데이터 파일들을 하나의 파일로 묶어 머신러닝에 필요한 큼직한 데이터 파일을 만들어 주도록 파이썬 프로그램을 만들어보자.

Merge to 1m_history.csv
Merge to 1m_history.csv

 

1. 다운로드를 자동화하고 1개의 CSV로 만들자

binance-public-data에 아래에 있는 download-kline.py파일을 실행해서 캔들 데이터를 다운로드 할때 아래와 같이 실행을 하고 있었다.

바이낸스 비트코인 선물 Kline/Candle 파일로 다운받기

 

바이낸스 비트코인 선물 Kline/Candle 파일로 다운받기

머신러닝에 필요한 만큼의 바이낸스 과거 Candlestick data를 API로 가져오려고 하는데... 헉! REST API 1회 호출로가져올수 있는 Candlestick은 최대 1,000개로 제한되어 있고 1일 최대로 호출할 수 있는 API호

axgo.tistory.com

## 최근까지의 바이낸스 선물(Futures) BTCUSDT 1분지표를 다운로드 받을때 이렇게 실행한다.
> python3 download-kline.py -s BTCUSDT -t um -i 1m -skip-daily 1 -startDate 2020-01-01
> python3 download-kline.py -s BTCUSDT -t um -i 1m -skip-monthly 1 -startDate 2023-11-01

바이낸스에서 제공하는 위 스크립트로 다운받은 Historycal Kline/Candle 데이터는 폴더를 여러단계 들어가는 구조로 되어 있다.

재사용이 가능한 파이썬 프로그램으로 형태로 다운로드를 자동화하면서 다운받은 데이터도 써먹기 좋게 1개파일로 최상단에 저장 되도록 하자. (zip파일을 하나씩 압축을 풀어야 하는것 부터가 불편했다.)

 

1-1. Python에서 subprocess로 필요한 작업을 할수 있다.

바이낸스 Historycal Klines/Candles Public Data 다운로드 자동화를 위한 준비를 하자.

  • 파이썬 subprocess로 pip를 실행하면 필요한 라이브러리를 설치 할수 있다.
  • GitPython을 사용하면 파이썬으로 Github 소스를 다운로드할 수 있다.
# -*- coding: utf-8 -*-
#!/usr/bin/env python3
import sys
import tempfile
import subprocess
from datetime import datetime, timedelta
from zipfile import ZipFile
from os import environ, getenv, makedirs, getcwd, walk, remove
from os.path import basename, join, exists, expanduser as home

def pip_install(package):
  subprocess.check_call([sys.executable, "-m", "pip", "install", package])

def pip_install_requirements(requirements_dir):
  subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", requirements_dir.rstrip(".txt")+".txt"])

## GitPython으로 git을 사용할 수 있도록 한다. 없다면 자동으로 GitPython을 설치되도록 한다. 
try:
  from git import Repo
except:
  pip_install("GitPython")
  from git import Repo

## 캔들 데이터를 가공하고 저장하기 위해서는 Pandas가 필요하다.
try:
  import pandas as pd
except:
  pip_install("pandas")
  import pandas as pd

## Pandas 짝꿍 Numpy가 필요하다.
try:
  import numpy as np
except:
  pip_install("numpy")
  import numpy as np

## Binance REST API로 다운로드된 데이터에서 부족한 부분만 가져올 수 있도록 requests를 사용하자.
try:
  import requests
except:
  pip_install("requests")
  import requests

 

1-2. 저장위치 변수 'STORE_DIRECTORY'를 설정 한다.

Kline 캔들 데이터를 저장할 위치는 OS에서 STORE_DIRECTORY로 설정해야 한다. 캔들 데이터가 저장될 위치를 지정하고 binance-public-data를 github에서 다운로드 받아 설치하는것도 자동화 하자.

  • 맥과 리눅스는 export STORE_DIRECTORY="~/binance_data"를 설정해준다.
  • 윈도우는 set STORE_DIRECTORY="%HomePath%/binance_data" 를 설정해주게 된다.
  • 다운로드 도구의 경로는 WORK_PATH로 만들도록 한다.
## 바이낸스 퍼블릭 데이터 다운로드 소스코드를 Temp(임시폴더) 다운로드 받아서 사용하도록 한다.
repo_url = "https://github.com/binance/binance-public-data.git"
temp_path = tempfile.mkdtemp(prefix='candle_download_')

## git으로 소스코드를 임시폴더에 클론(다운로드) 시키고 위치를 저장해두자.
repo_path = Repo.clone_from(repo_url, temp_path)
WORK_PATH = repo_path.working_dir

## STORE_DIRECTORY 환경변수가 없으면 사용자폴더에 binance_data를 사용하도록 설정한다.
STORE_PATH = join(home('~'), "binance_data") if not "STORE_DIRECTORY" in environ.keys() else getenv("STORE_DIRECTORY")
environ["STORE_DIRECTORY"] = STORE_PATH

 

1-3. 파이썬에서 비트코인 1분지표 다운로드를 실행하는 코드

바이낸스에서 다운로드 받을 수 있는 가장 최근까지의 데이터를 STORE_PATH 위치에 다운로드 받자.

## 캔들 데이터를 다운로드 받는 download-kline.py를 실행한다. 
def download_klines(cmd, args):
  subprocess.check_call(cmd + args)

## 저장할 위치가 없으면 만들어주고, download-kline.py에 다운로드 받을 코인 정보등을 입력한다.
def download_binance_datas(symbol="BTCUSDT", interval="1m"):
  # environ["STORE_DIRECTORY"] = "/Users/name/binance_data/"
  if not exists(STORE_PATH):
    makedirs(STORE_PATH)
  # Install requirements library
  pip_install_requirements(join(WORK_PATH, "python", "requirements.txt"))
  # configure download command, 
  kline_cmd = [sys.executable, join(WORK_PATH, "python", "download-kline.py")]
  monthly_args = ["-t", "um", "-s", symbol, "-i", interval, "-skip-daily", "1", "-startDate", "2020-01-01"]
  daily_args = ["-t", "um", "-s", symbol, "-i", interval, "-skip-monthly", "1", "-startDate", f"{datetime.now().strftime('%Y-%m')}-01"]
  # excute download kline
  download_klines(kline_cmd, monthly_args)
  download_klines(kline_cmd, daily_args)

if __name__ == "__main__":
  ## 바이낸스 비트코인 선물 1분지표 다운로드를 실행한다.
  download_binance_datas(symbol="BTCUSDT", interval="1m")

 

지금까지 만들어놓은 내용을 download_test.py 로 저장하고 실행해보자.

> python3 download_test.py

 

다운로드에 필요한 라이브러리와 도구들을 자동으로 설치하고 바이낸스에서 과거 비트코인 선물 Klines/Candles 데이터를 다운로드하는 것까지 자동화가 되었다.

## 설치되어 있지 않은 라이브러리들 자동으로 설치하고 캔들 데이터까지 다운로드 하고 있다.
> python3 download_test.py
Collecting GitPython
  Downloading GitPython-3.1.40-py3-none-any.whl (190 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 190.6/190.6 kB 7.6 MB/s eta 0:00:00
Collecting gitdb<5,>=4.0.1
  Downloading gitdb-4.0.11-py3-none-any.whl (62 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.7/62.7 kB 5.4 MB/s eta 0:00:00
Collecting smmap<6,>=3.0.1
  Downloading smmap-5.0.1-py3-none-any.whl (24 kB)
Installing collected packages: smmap, gitdb, GitPython
Successfully installed GitPython-3.1.40 gitdb-4.0.11 smmap-5.0.1
... (생략)
Found 1 symbols
[1/1] - start download monthly BTCUSDT klines

File Download: /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2020-01.zip
[##################################################]
File Download: /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2020-02.zip
... (생략)
[1/1] - start download daily BTCUSDT klines

File Download: /Users/name/binance_data/data/futures/um/daily/klines/BTCUSDT/1m/BTCUSDT-1m-2023-11-01.zip
[##################################################]
File Download: /Users/name/binance_data/data/futures/um/daily/klines/BTCUSDT/1m/BTCUSDT-1m-2023-11-02.zip
[##################################################]
File Download: /Users/name/binance_data/data/futures/um/daily/klines/BTCUSDT/1m/BTCUSDT-1m-2023-11-03.zip
[##################################################]
File Download: /Users/name/binance_data/data/futures/um/daily/klines/BTCUSDT/1m/BTCUSDT-1m-2023-11-04.zip
[##################################################]
File Download: /Users/name/binance_data/data/futures/um/daily/klines/BTCUSDT/1m/BTCUSDT-1m-2023-11-05.zip
[##################################################]
File not found: https://data.binance.vision/data/futures/um/daily/klines/BTCUSDT/1m/BTCUSDT-1m-2023-11-06.zip

 

다운로드한 데이터 폴더를 확인해보자.

## 홈폴더에 binance_data폴더가 생기고 그 아래로 데이터들이 다운로드 되었다.
> ls -l ~/binance_data
total 0
drwxr-xr-x  3 name  staff  96 11  6 22:03 data

## 맥이나 리눅스에서는 lsd 로 폴더 구조를 볼 수 있다.
~/binance_data> lsd --tree --no-symlink
 .
└──  data
    └──  futures
        └──  um
            ├──  daily
            │   └──  klines
            │       └──  BTCUSDT
            │           └──  1m
            │               ├──  BTCUSDT-1m-2023-11-01.zip
            │               ├──  BTCUSDT-1m-2023-11-02.zip
            │               ├──  BTCUSDT-1m-2023-11-03.zip
            │               ├──  BTCUSDT-1m-2023-11-04.zip
            │               └──  BTCUSDT-1m-2023-11-05.zip
            └──  monthly
                └──  klines
                    └──  BTCUSDT
                        └──  1m
                            ├──  BTCUSDT-1m-2020-01.zip
                            ├──  BTCUSDT-1m-2020-02.zip
                            ├──  BTCUSDT-1m-2020-03.zip
                            ├──  ... (생략)
                            ├──  BTCUSDT-1m-2023-07.zip
                            ├──  BTCUSDT-1m-2023-08.zip
                            ├──  BTCUSDT-1m-2023-09.zip
                            └──  BTCUSDT-1m-2023-10.zip

 

2.  다운받은 데이터를 1개의 단일 csv파일로 만들기

다운로드 받은 폴더를 살펴보면 월별/일별 zip파일이 폴더로 구분되어 저장되어 있다. 우선 다운받은 zip파일들의 압축을 해제하고 csv파일이 나오면 이것들을 하나의 csv파일로 합치면 된다.

zip을 풀면 csv파일들이 나온다
zip을 풀면 csv파일들이 나온다

파이썬으로 zip파일들의 압축을 일괄로 풀어줄 수 있도록 기능을 추가해보자.

 

2-1.  zip 압축 파일들을 일괄 압축해제 하자.

저장된 폴더 밑에 있는 zip파일들을 모두 찾아 압축 해제하는 기능을 download_test.py에 추가하면 된다.

## 확장자가 .zip(소문자)인 파일들을 찾아서 압축을 해제 한다.
## download_test.py 에 추가한다.

def klines_unzip(search_directory=STORE_PATH):
  search_directory = join(search_directory, 'data')
  for root, dirs, files in walk(search_directory):
    for file in files:
      if file.endswith('.zip'):
        zip_file_path = join(root, file)
        # 압축을 풀 디렉토리 선택 (zip 파일이 있는 폴더와 동일한 위치)
        extract_directory = root
        # 압축 파일 열기
        with ZipFile(zip_file_path, 'r') as zip_ref:
          # 압축 해제
          zip_ref.extractall(extract_directory)
        print(f'압축 해제: {zip_file_path} -> {extract_directory}')
        
if __name__ == "__main__":
  ## 바이낸스 비트코인 선물 1분지표 다운로드를 실행한다.
  download_binance_datas(symbol="BTCUSDT", interval="1m")
  ## 다운받은 zip파일들의 압축을 해제한다.
  klines_unzip(search_directory=STORE_PATH)

 

캔들 데이터가 저장된 하위폴더들을 검색해서 zip들을 압축해제 한다. 기능이 추가된 download_test.py를 실행하거나 klines_unzip을 함수로 실행하면 압축을 해제 해준다.

## 지금까지의 내용을 파이썬에서 실행 하면 압축하면서 다음과 같이 출력된다.
>>> klines_unzip(search_directory=STORE_PATH)
압축 해제: /Users/name/binance_data/data/futures/um/daily/klines/BTCUSDT/1m/BTCUSDT-1m-2023-11-04.zip -> /Users/name/binance_data/data/futures/um/daily/klines/BTCUSDT/1m
압축 해제: /Users/name/binance_data/data/futures/um/daily/klines/BTCUSDT/1m/BTCUSDT-1m-2023-11-05.zip -> /Users/name/binance_data/data/futures/um/daily/klines/BTCUSDT/1m
압축 해제: /Users/name/binance_data/data/futures/um/daily/klines/BTCUSDT/1m/BTCUSDT-1m-2023-11-02.zip -> /Users/name/binance_data/data/futures/um/daily/klines/BTCUSDT/1m
... (생략)
압축 해제: /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2020-08.zip -> /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m
압축 해제: /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2022-09.zip -> /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m
압축 해제: /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2022-08.zip -> /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m

 

2-2. csv파일들을 확인 한다.

zip파일들의 압축을 해제되면서 같은 위치에 csv파일들이 만들어지는 것을 확인할 수 있다. 이제 csv파일을 하나로 합쳐 1개의 csv 파일로 만들수 있다.

## lsd 로 압축해제된 상태를 확인할 수 있다.
~/binance_data> lsd --tree --no-symlink
 .
└──  data
    └──  futures
        └──  um
            ├──  daily
            │   └──  klines
            │       └──  BTCUSDT
            │           └──  1m
            │               ├──  BTCUSDT-1m-2023-11-01.csv
            │               ├──  BTCUSDT-1m-2023-11-01.zip
            │               ├──  ... (생략)
            │               ├──  BTCUSDT-1m-2023-11-05.csv
            │               └──  BTCUSDT-1m-2023-11-05.zip
            └──  monthly
                └──  klines
                    └──  BTCUSDT
                        └──  1m
                            ├──  BTCUSDT-1m-2020-01.csv
                            ├──  BTCUSDT-1m-2020-01.zip
                            ├──  BTCUSDT-1m-2023-07.zip
                            ├──  BTCUSDT-1m-2023-08.csv
                            ├──  ... (생략)
                            ├──  BTCUSDT-1m-2023-09.csv
                            ├──  BTCUSDT-1m-2023-09.zip
                            ├──  BTCUSDT-1m-2023-10.csv
                            └──  BTCUSDT-1m-2023-10.zip

 

3. CSV로 저장된 Candle 데이터를 1개의 CSV로 합치자 

흩어져있는 csv 캔들 데이터를 합치기에 앞서 csv를 데이터프레임 형태로 만들때 열의(Field) 속성을 알아야 한다. 다음과 같은 열이름과 속성을 가지고 있다는 것을 알고 넘어가자.

12개의 속성중에서 나에게 필요한 것은 open_time, open, high, low, close, volume 정도가 되겠다. (1열 ~ 6열 이다)

  Field Name (열이름)
Description (설명)
1 open_time
캔들의 시작시간이다. datetime 필드로 변경해서 사용하도록 하자.
2 open
캔들의 시작 가격이다. Open Price
3 high
캔들의 최고 가격이다. High Price
4 low
캔들의 최저 가격이다. Low Price
5 close
캔들의 종료 가격이다. Close Price
6 volume
캔들에 대한 거래량이다. Volume
7 close_time
캔들의 종료시간이다. Kline Close time in unix time format
8 quote_volume
Quote Asset Volume
9 count
Number of Trades
10 taker_buy_volume
Taker buy base asset volume during this period
11 taker_buy_quote_volume
Taker buy quote asset volume during this period
12 ignore
Ignore

참고링크: https://github.com/binance/binance-public-data/tree/master#klines

 

3-1. CSV파일을 합쳐주는 기능을 추가하자.

zip파일들을 찾아 압축을 해제하던 방식으로 흩어져 있는 csv파일들을 찾아 1개의 csv 파일로 합쳐서 저장할 수 있다.

## 대상 폴더의 하위폴더들을 확인해서 Pandas DataFrame으로 불러 들인다.
## 중복된 데이터가 있으면 제거하고 Pandas에서 1개의 CSV파일로 저장한다. 

def klines_history(search_directory=STORE_PATH):
  search_directory = join(search_directory, 'data')
  # 모든 CSV 파일을 저장할 데이터 프레임 초기화
  history = pd.DataFrame()
  for root, dirs, files in walk(search_directory):
    for file in sorted(files):
      if file.endswith('.csv') and file != f'{basename(root)}.csv':
        csv_file_path = join(root, file)
        # 해더가 있는지 확인하고 넘어가야함. 첫 번째 라인 확인
        with open(csv_file_path, 'r') as file:
          first_line = file.readline()
          # 컬럼 이름이 있는 경우 읽을때
          if 'open_time' in first_line or 'Open' in first_line or 'open' in first_line:
            print(f'DataFrame(Header exist): {csv_file_path}')
            df = pd.read_csv(csv_file_path)  # header=0 (기본값)
          # 컬럼 이름이 없는 경우 읽을때
          else:
            print(f'DataFrame(Header empty): {csv_file_path}')
            df = pd.read_csv(csv_file_path, header=None)
            df.columns = ['open_time', 'open','high', 'low', 'close', 'volume', 'close_time', 'quote_volume', 'count', 'taker_buy_volume', 'taker_buy_quote_volume', 'ignore']
        df = df.iloc[:, :6]
        df.columns = ['datetime', 'open','high', 'low', 'close', 'volume']
        history = pd.concat([history, df])
  history.index = pd.to_datetime(history['datetime'], unit='ms', utc=True)
  history = history.astype(float)
  history = history.tz_convert('Asia/Seoul')
  history = history.iloc[np.unique(history.index.values, return_index=True)[1]]
  history_file_path = join(STORE_PATH, f'{basename(root)}_history.csv')
  print("### Save History Klines/Candles (Download) : ", history_file_path)
  history.to_csv(history_file_path, index=False)
  return history, history_file_path, history['datetime'].iloc[-1]
  
if __name__ == "__main__":
  ## 바이낸스 비트코인 선물 1분지표 다운로드를 실행한다.
  download_binance_datas(symbol="BTCUSDT", interval="1m")
  ## 다운받은 zip파일들의 압축을 해제한다.
  klines_unzip(search_directory=STORE_PATH)
  ## csv파일들을 읽어 csv단일 파일로 저장한다.
  klines_history(search_directory=STORE_PATH)

 

CSV파일로 데이터를 저장 할때 첫번째 행에 열이름(Header)이 들어가는 것이 일반적인다. 그런데 드문 드문 헤더가 없는 csv파일들이 있다. 때문에 첫번째 행을 기준으로 csv를 읽어 들이는 옵션을 다르게 처리 해야 한다.

 

위 기능은 아래와 같이 출력된다. ( 1m_history.csv로 모든 csv파일의 내용이 합쳐졌다 ^^)

## 지금까지의 내용을 파이썬에서 실행 하면 압축하면서 다음과 같이 출력된다.
>>> klines_history(search_directory=STORE_PATH)
... (생략)
DataFrame(Header exist): /Users/name/binance_data/data/futures/um/daily/klines/BTCUSDT/1m/BTCUSDT-1m-2023-11-01.csv
DataFrame(Header exist): /Users/name/binance_data/data/futures/um/daily/klines/BTCUSDT/1m/BTCUSDT-1m-2023-11-02.csv
... (생략)
DataFrame(Header empty): /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2022-03.csv
DataFrame(Header exist): /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2022-04.csv
DataFrame(Header empty): /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2022-05.csv
DataFrame(Header exist): /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2022-06.csv
DataFrame(Header exist): /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2022-07.csv
... (생략)
DataFrame(Header exist): /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2023-07.csv
DataFrame(Header exist): /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2023-08.csv
DataFrame(Header exist): /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2023-09.csv
DataFrame(Header exist): /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2023-10.csv
### Save History Klines/Candles (Download) :  /Users/name/binance_data/1m_history.csv
### Download Klines/Candles Count is: 2023200
### Last Klines/Candles Timestamp is: 1699228740000.0

(아마도 2022년 4월~6월 사이에 바이낸스 CSV 익스포트 포맷 정책이 변경되었던것 같다.)

 

3-2.  1개의 CSV파일로 모든 내용을 합쳤다.

과거의 바이낸스 비트코인 선물 캔들 데이터를 모두 다운로드해 1개의 csv파일로 합치기 까지 완료했다.

저장된 1m_history.csv 파일을 확인할 수 있다.

## lsd 생성된 1m_history.csv 파일을 확인할 수 있다.
~/binance_data> lsd --tree --no-symlink
 .
├──  1m_history.csv
└──  data
    └──  futures
        └──  um
            ├──  daily
            │   └──  klines
            │       └──  BTCUSDT
            │           └──  1m
            │               ├──  BTCUSDT-1m-2023-11-01.csv
            │               ├──  BTCUSDT-1m-2023-11-01.zip
            │               ├──  ... (생략)
            │               ├──  BTCUSDT-1m-2023-11-05.csv
            │               └──  BTCUSDT-1m-2023-11-05.zip
            └──  monthly
                └──  klines
                    └──  BTCUSDT
                        └──  1m
                            ├──  BTCUSDT-1m-2020-01.csv
                            ├──  BTCUSDT-1m-2020-01.zip
                            ├──  ... (생략)
                            ├──  BTCUSDT-1m-2023-10.csv
                            └──  BTCUSDT-1m-2023-10.zip
                            
~/binance_data> ls -l
.rw-r--r-- name admin 109 MB Tue Nov  7 00:25:41 2023 1m_history.csv
drwxr-xr-x name admin  96 B  Tue Nov  7 00:24:35 2023 data

 

4. 스크립트를 정리해보자

파이썬으로 바이낸스 비트코인 선물 1분지표를 다운로드하고 1개의 단일 CSV로 만들었다. Pandas 데이터프레임 형태로 데이터 사용할 수 있는 단계까지의 스크립트를 정리하면 아래 <더보기>와 같다.

더보기
# -*- coding: utf-8 -*-
#!/usr/bin/env python3
import sys
import tempfile
import subprocess
from datetime import datetime, timedelta
from zipfile import ZipFile
from os import environ, getenv, makedirs, getcwd, walk, remove
from os.path import basename, join, exists, expanduser as home

def pip_install(package):
  subprocess.check_call([sys.executable, "-m", "pip", "install", package])

def pip_install_requirements(requirements_dir):
  subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", requirements_dir.rstrip(".txt")+".txt"])

## GitPython으로 git을 사용할 수 있도록 한다. 없다면 pip로 GitPython을 설치 한다.
try:
  from git import Repo
except:
  pip_install("GitPython")
  from git import Repo

## 캔들 데이터를 사용하기 위해서는 Pandas가 필요하다.
try:
  import pandas as pd
except:
  pip_install("pandas")
  import pandas as pd

## Pandas 짝꿍 Numpy가 필요하다.
try:
  import numpy as np
except:
  pip_install("numpy")
  import numpy as np

## Binance REST API로 다운로드된 데이터에서 부족한 부분만 가져올 수 있도록 requests를 사용하자.
try:
  import requests
except:
  pip_install("requests")
  import requests

## 바이낸스 퍼블릭 데이터 다운로드 소스코드를 Temp(임시폴더) 다운로드 받아서 사용하도록 한다.
repo_url = "https://github.com/binance/binance-public-data.git"
temp_path = tempfile.mkdtemp(prefix='candle_download_')

## git으로 소스코드를 임시폴더에 클론(다운로드) 시키고 위치를 저장해두자.
repo_path = Repo.clone_from(repo_url, temp_path)
WORK_PATH = repo_path.working_dir

## STORE_DIRECTORY 환경변수가 없으면 사용자폴더에 binance_data를 사용하도록 설정한다.
STORE_PATH = join(home('~'), "binance_data") if not "STORE_DIRECTORY" in environ.keys() else getenv("STORE_DIRECTORY")
environ["STORE_DIRECTORY"] = STORE_PATH

## 캔들 데이터를 다운로드 받는 download-kline.py를 실행한다.
def download_klines(cmd, args):
  subprocess.check_call(cmd + args)

## 저장할 위치가 없으면 만들어주고, download-kline.py에 다운로드 받을 코인 정보등을 입력한다.
def download_binance_datas(symbol="BTCUSDT", interval="1m"):
  # environ["STORE_DIRECTORY"] = "/Users/name/binance_data/"
  if not exists(STORE_PATH):
    makedirs(STORE_PATH)
  # Install requirements library
  pip_install_requirements(join(WORK_PATH, "python", "requirements.txt"))
  # configure download command,
  kline_cmd = [sys.executable, join(WORK_PATH, "python", "download-kline.py")]
  monthly_args = ["-t", "um", "-s", symbol, "-i", interval, "-skip-daily", "1", "-startDate", "2020-01-01"]
  daily_args = ["-t", "um", "-s", symbol, "-i", interval, "-skip-monthly", "1", "-startDate", f"{datetime.now().strftime('%Y-%m')}-01"]
  # excute download kline
  download_klines(kline_cmd, monthly_args)
  download_klines(kline_cmd, daily_args)

def klines_unzip(search_directory=STORE_PATH):
  search_directory = join(search_directory, 'data')
  for root, dirs, files in walk(search_directory):
    for file in files:
      if file.endswith('.zip'):
        zip_file_path = join(root, file)
        # 압축을 풀 디렉토리 선택 (zip 파일이 있는 폴더와 동일한 위치)
        extract_directory = root
        # 압축 파일 열기
        with ZipFile(zip_file_path, 'r') as zip_ref:
          # 압축 해제
          zip_ref.extractall(extract_directory)
        print(f'압축 해제: {zip_file_path} -> {extract_directory}')

def klines_history(search_directory=STORE_PATH):
  search_directory = join(search_directory, 'data')
  # 모든 CSV 파일을 저장할 데이터 프레임 초기화
  history = pd.DataFrame()
  for root, dirs, files in walk(search_directory):
    for file in sorted(files):
      if file.endswith('.csv') and file != f'{basename(root)}.csv':
        csv_file_path = join(root, file)
        # 해더가 있는지 확인하고 넘어가야함. 첫 번째 라인 확인
        with open(csv_file_path, 'r') as file:
          first_line = file.readline()
          # 컬럼 이름이 있는 경우 읽을때
          if 'open_time' in first_line or 'Open' in first_line or 'open' in first_line:
            print(f'DataFrame(Header exist): {csv_file_path}')
            df = pd.read_csv(csv_file_path)  # header=0 (기본값)
          # 컬럼 이름이 없는 경우 읽을때
          else:
            print(f'DataFrame(Header empty): {csv_file_path}')
            df = pd.read_csv(csv_file_path, header=None)
            df.columns = ['open_time', 'open','high', 'low', 'close', 'volume', 'close_time', 'quote_volume', 'count', 'taker_buy_volume', 'taker_buy_quote_volume', 'ignore']
        df = df.iloc[:, :6]
        df.columns = ['datetime', 'open','high', 'low', 'close', 'volume']
        history = pd.concat([history, df])
  history.index = pd.to_datetime(history['datetime'], unit='ms', utc=True)
  history = history.astype(float)
  history = history.tz_convert('Asia/Seoul')
  history = history.iloc[np.unique(history.index.values, return_index=True)[1]]
  history_file_path = join(STORE_PATH, f'{basename(root)}_history.csv')
  print("### Save History Klines/Candles (Download) : ", history_file_path)
  history.to_csv(history_file_path, index=False)
  return history, history_file_path, history['datetime'].iloc[-1]

if __name__ == "__main__":
  ## 바이낸스 비트코인 선물 1분지표 다운로드를 실행한다.
  download_binance_datas(symbol="BTCUSDT", interval="1m")
  ## 다운받은 zip파일들의 압축을 해제한다.
  klines_unzip(search_directory=STORE_PATH)
  ## csv파일들을 읽어 csv단일 파일로 저장한다.
  history_df, history_file_path, history_last_timestamp = klines_history(STORE_PATH)
  print(f"### Download Klines/Candles Count is: {len(history_df)}")
  print(f"### Last Klines/Candles Timestamp is: {history_last_timestamp}")

 

4-1. 몇개의 1분 지표를 다운받았는지 확인할 수 있다

스크립트를 실행하면 홈폴더에 binance_data 폴더를 만들고 월/일로 나누어져 있던 csv들을 1m_history.csv파일 하나로 합쳐 생성해준다. 그리고 끝쪽에 1분 캔들이 몇개인지 출력해주고 있다.

## UTC 2020-01-01 ~ 마지막 날짜 까지의 Candle을 1개의 파일로 생성해준다.
> python3 download_test.py
... (생략)
DataFrame(Header exist): /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2023-07.csv
DataFrame(Header exist): /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2023-08.csv
DataFrame(Header exist): /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2023-09.csv
DataFrame(Header exist): /Users/name/binance_data/data/futures/um/monthly/klines/BTCUSDT/1m/BTCUSDT-1m-2023-10.csv
Save History Klines/Candles (Download) :  /Users/name/binance_data/1m_history.csv
Download Klines/Candles Count is: 2023200
Last Klines/Candles Timestamp is: 1699228740000.0

1m_history.csv 파일에 1분지표 캔들 정보가 2,023,200개 저장되어 있다.

 

4-2. 마지막 캔들의 Timestamp

바이낸스가 하루 단위로 Kilne/Candle 정보를 마감하고 데이터를 업로드하기 때문에 1일 정도의 데이터가 빠진 상태다. 확인을 위해 다운로드한 마지막 캔들의 Timestamp를 파이썬에서 시간으로 표시해보자.

>>> from datetime import datetime
>>> history_last_timestamp = 1699228740000.0
>>> print(datetime.fromtimestamp(history_last_timestamp/1000).strftime("%Y-%m-%d %H:%M:%S"))
'2023-11-06 08:59:00'

마지막 캔들의 Timestamp가 서울시간(UTC +9)으로 표시되고 있다. (UTC+0 시간으로는 '2023년 11월 5일 23시 59분 00초'가 된다)

 

마치며

2백만개 가량의 바이낸스 비트코인 선물 Kline 캔들 데이터를 다운로드 받아 1개의 csv파일로 만드는데 60초도 걸리지 않았다. 이제 다운로드 받은 마지막 캔들의 Timestamp부터 현재까지(Now)의 캔들 데이터를 REST API로 받아서 합치면 될 것 같다. 다음으로는 history_last_timestamp 이후의 캔들만 REST API로 가져와 Pandas 데이터프레임으로 합쳐 단일 파일로 저장 해보겠다.

 

참고자료

How to Download Historical Market Data on Binance: https://www.binance.com/en/support/faq/how-to-download-historical-market-data-on-binance-5810ae42176b4770b880ce1f14932262

Binance Historical Market Data : https://www.binance.com/en/landing/data