diff --git a/python-scripts/ohlcv-fetchers/.gitignore b/python-scripts/ohlcv-fetchers/.gitignore new file mode 100644 index 0000000..416decf --- /dev/null +++ b/python-scripts/ohlcv-fetchers/.gitignore @@ -0,0 +1,3 @@ +/venv/ + +/*.csv diff --git a/python-scripts/ohlcv-fetchers/README.md b/python-scripts/ohlcv-fetchers/README.md new file mode 100644 index 0000000..fde9392 --- /dev/null +++ b/python-scripts/ohlcv-fetchers/README.md @@ -0,0 +1,18 @@ +# ohlcv-fetchers + +Set of scripts that can be used to **download [OHLCV data](https://en.wikipedia.org/wiki/Open-high-low-close_chart)** from various sources (e.g. [_Binance_](https://binance.com/), [_Yahoo Finance_](https://finance.yahoo.com/)). + +## Usage + +Set up a **Python venv** (virtual environment) and install some packages inside it: + +```bash +python3 -mvenv venv +venv/bin/python3 -mpip install -r requirements.txt +``` + +Then you can use the scripts: + +```bash +venv/bin/python3 myscript.py --help +``` diff --git a/python-scripts/ohlcv-fetchers/binance.py b/python-scripts/ohlcv-fetchers/binance.py new file mode 100644 index 0000000..67ace3d --- /dev/null +++ b/python-scripts/ohlcv-fetchers/binance.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +import argparse +import sys + +from datetime import timedelta +from datetime import datetime as dt +from datetime import timezone as tz +from dateutil import parser as dup + +import ccxt + + +def main(argv=None): + if argv is None: + argv = sys.argv + + parser = argparse.ArgumentParser( + description='Binance OHLCV data downloader') + + parser.add_argument('symbol', metavar='SYMBOL', type=str, + help='Ticker symbol (example: BTC/USDT)') + + parser.add_argument('-i', '--interval', type=str, default='5m', + help='Length of time each candle represents' + '(default: 5m)') + + parser.add_argument('-d', '--dt-start', type=lambda x: dup.parse(x), + default=dt.now(tz.utc) - timedelta(hours=1), + help='Start date and time (default: 1 hour ago)') + parser.add_argument('-D', '--dt-end', type=lambda x: dup.parse(x), + default=dt.now(tz.utc), + help='End date and time (default: now)') + + parser.add_argument('-f', '--format', type=str, default='', + help='If specified, formats the float values (such as' + 'the asset prices) with this format string' + '(e.g. "{:.6f}")') + + args = parser.parse_args(argv[1:]) + + args.dt_start = args.dt_start.astimezone(tz.utc) + args.dt_end = args.dt_end.astimezone(tz.utc) + + ############################################################################ + + print(f'Fetching {args.symbol} with CCXT', file=sys.stderr) + + exchange = ccxt.binance() + + # Equivalent of + # https://api.binance.com/api/v3/klines?symbol=...&interval=...&startTime=...&endTime=... + # See + # https://developers.binance.com/docs/binance-spot-api-docs/rest-api#klinecandlestick-data + data = exchange.fetch_ohlcv( + args.symbol, + args.interval, + int(args.dt_start.timestamp() * 1000), + params={ + 'until': int(args.dt_end.timestamp() * 1000), + 'paginate': True, + } + ) + + if args.format != '': + for candle in data: + for i, v in enumerate(candle): + if isinstance(v, float): + candle[i] = args.format.format(v) + + print('Timestamp,Open,High,Low,Close,Volume') + for candle in data: + print(','.join(str(x) for x in candle)) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/python-scripts/ohlcv-fetchers/requirements.txt b/python-scripts/ohlcv-fetchers/requirements.txt new file mode 100644 index 0000000..75db020 --- /dev/null +++ b/python-scripts/ohlcv-fetchers/requirements.txt @@ -0,0 +1,3 @@ +ccxt==4.4.* +python-dateutil==2.9.* +yfinance==0.2.* diff --git a/python-scripts/ohlcv-fetchers/yahoo-finance.py b/python-scripts/ohlcv-fetchers/yahoo-finance.py new file mode 100644 index 0000000..8bfba5d --- /dev/null +++ b/python-scripts/ohlcv-fetchers/yahoo-finance.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +import argparse +import sys + +from datetime import timedelta +from datetime import datetime as dt +from dateutil import parser as dup + +import yfinance as yf +import pandas as pd + + +def is_aware(d: dt): + ''' + Returns true if the datetime object `d` is timezone-aware, false otherwise. + See https://docs.python.org/3/library/datetime.html#determining-if-an-object-is-aware-or-naive + ''' + return d.tzinfo is not None and d.tzinfo.utcoffset(d) is not None + + +def main(argv=None): + if argv is None: + argv = sys.argv + + parser = argparse.ArgumentParser( + description='Yahoo Finance OHLCV data downloader') + + parser.add_argument('symbol', metavar='SYMBOL', type=str, + help='Ticker symbol (example: ^GSPC)') + + parser.add_argument('-i', '--interval', type=str, default='1d', + help='Length of time each candle represents' + '(default: 1d)') + + parser.add_argument('-d', '--dt-start', type=lambda x: dup.parse(x), + default=dt.now().astimezone() - timedelta(days=30), + help='Start date and time (default: 30 days ago)') + parser.add_argument('-D', '--dt-end', type=lambda x: dup.parse(x), + default=dt.now().astimezone(), + help='End date and time (default: now)') + + parser.add_argument('-f', '--format', type=str, default='', + help='If specified, formats the float values (such as' + 'the asset prices) with this format string' + '(e.g. "{:.6f}")') + + args = parser.parse_args(argv[1:]) + + if not is_aware(args.dt_start): + args.dt_start = args.dt_start.astimezone() + if not is_aware(args.dt_end): + args.dt_end = args.dt_end.astimezone() + + ############################################################################ + + print(f'Fetching {args.symbol} with yfinance', file=sys.stderr) + + # Equivalent of + # https://query1.finance.yahoo.com/v8/finance/chart/...?interval=...&period1=...&period2=... + # See https://finance.yahoo.com/quote/.../history + data: pd.DataFrame = yf.download( + args.symbol, args.dt_start, args.dt_end, interval=args.interval) + + if args.format != '': + data = data.apply(lambda col: col.map( + lambda x: args.format.format(x) if isinstance(x, float) else x)) + + data.to_csv(sys.stdout, lineterminator='\n') + + return 0 + + +if __name__ == '__main__': + sys.exit(main())