2018-12-13 05:12:10 +00:00
|
|
|
import logging
|
2019-05-25 14:51:52 +00:00
|
|
|
import operator
|
2023-05-14 07:08:31 +00:00
|
|
|
from datetime import datetime, timedelta
|
2018-12-15 12:54:35 +00:00
|
|
|
from pathlib import Path
|
2019-12-27 09:25:17 +00:00
|
|
|
from typing import Dict, List, Optional, Tuple
|
2018-12-13 05:12:10 +00:00
|
|
|
|
2022-02-01 18:11:51 +00:00
|
|
|
from pandas import DataFrame, concat
|
2018-12-13 05:12:10 +00:00
|
|
|
|
2019-07-11 18:23:23 +00:00
|
|
|
from freqtrade.configuration import TimeRange
|
2024-05-12 13:18:32 +00:00
|
|
|
from freqtrade.constants import (
|
|
|
|
DATETIME_PRINT_FORMAT,
|
|
|
|
DEFAULT_DATAFRAME_COLUMNS,
|
|
|
|
DL_DATA_TIMEFRAMES,
|
|
|
|
DOCS_LINK,
|
|
|
|
Config,
|
|
|
|
)
|
|
|
|
from freqtrade.data.converter import (
|
|
|
|
clean_ohlcv_dataframe,
|
|
|
|
convert_trades_to_ohlcv,
|
|
|
|
ohlcv_to_dataframe,
|
|
|
|
trades_df_remove_duplicates,
|
|
|
|
trades_list_to_df,
|
|
|
|
)
|
2024-03-15 05:49:42 +00:00
|
|
|
from freqtrade.data.history.datahandlers import IDataHandler, get_datahandler
|
2024-03-02 12:10:57 +00:00
|
|
|
from freqtrade.enums import CandleType, TradingMode
|
2019-12-30 14:02:17 +00:00
|
|
|
from freqtrade.exceptions import OperationalException
|
2023-06-17 16:14:58 +00:00
|
|
|
from freqtrade.exchange import Exchange
|
2023-06-17 13:13:56 +00:00
|
|
|
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
2023-08-18 07:07:51 +00:00
|
|
|
from freqtrade.util import dt_ts, format_ms_time
|
|
|
|
from freqtrade.util.datetime_helpers import dt_now
|
2024-01-04 15:25:40 +00:00
|
|
|
from freqtrade.util.migrations import migrate_data
|
2019-04-04 17:56:40 +00:00
|
|
|
|
2020-09-28 17:39:41 +00:00
|
|
|
|
2018-12-13 05:12:10 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
def load_pair_history(
|
|
|
|
pair: str,
|
|
|
|
timeframe: str,
|
|
|
|
datadir: Path,
|
|
|
|
*,
|
|
|
|
timerange: Optional[TimeRange] = None,
|
|
|
|
fill_up_missing: bool = True,
|
|
|
|
drop_incomplete: bool = False,
|
|
|
|
startup_candles: int = 0,
|
|
|
|
data_format: Optional[str] = None,
|
|
|
|
data_handler: Optional[IDataHandler] = None,
|
|
|
|
candle_type: CandleType = CandleType.SPOT,
|
|
|
|
) -> DataFrame:
|
2018-12-15 19:31:25 +00:00
|
|
|
"""
|
2020-03-08 10:35:31 +00:00
|
|
|
Load cached ohlcv history for the given pair.
|
2019-12-17 10:43:42 +00:00
|
|
|
|
2019-06-09 12:40:45 +00:00
|
|
|
:param pair: Pair to load data for
|
2020-03-08 10:35:31 +00:00
|
|
|
:param timeframe: Timeframe (e.g. "5m")
|
2019-06-09 12:40:45 +00:00
|
|
|
:param datadir: Path to the data storage location.
|
2019-12-25 15:12:20 +00:00
|
|
|
:param data_format: Format of the data. Ignored if data_handler is set.
|
2019-06-09 12:40:45 +00:00
|
|
|
:param timerange: Limit data to be loaded to this timerange
|
|
|
|
:param fill_up_missing: Fill missing values with "No action"-candles
|
|
|
|
:param drop_incomplete: Drop last candle assuming it may be incomplete.
|
2019-10-20 12:02:53 +00:00
|
|
|
:param startup_candles: Additional candles to load at the start of the period
|
2019-12-25 14:55:28 +00:00
|
|
|
:param data_handler: Initialized data-handler to use.
|
|
|
|
Will be initialized from data_format if not set
|
2021-12-03 11:23:35 +00:00
|
|
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
2019-12-04 05:57:44 +00:00
|
|
|
:return: DataFrame with ohlcv data, or empty DataFrame
|
2018-12-15 19:31:25 +00:00
|
|
|
"""
|
2019-12-25 15:12:20 +00:00
|
|
|
data_handler = get_datahandler(datadir, data_format, data_handler)
|
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
return data_handler.ohlcv_load(
|
|
|
|
pair=pair,
|
|
|
|
timeframe=timeframe,
|
|
|
|
timerange=timerange,
|
|
|
|
fill_missing=fill_up_missing,
|
|
|
|
drop_incomplete=drop_incomplete,
|
|
|
|
startup_candles=startup_candles,
|
|
|
|
candle_type=candle_type,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def load_data(
|
|
|
|
datadir: Path,
|
|
|
|
timeframe: str,
|
|
|
|
pairs: List[str],
|
|
|
|
*,
|
|
|
|
timerange: Optional[TimeRange] = None,
|
|
|
|
fill_up_missing: bool = True,
|
|
|
|
startup_candles: int = 0,
|
|
|
|
fail_without_data: bool = False,
|
|
|
|
data_format: str = "feather",
|
|
|
|
candle_type: CandleType = CandleType.SPOT,
|
|
|
|
user_futures_funding_rate: Optional[int] = None,
|
|
|
|
) -> Dict[str, DataFrame]:
|
2018-12-13 05:12:10 +00:00
|
|
|
"""
|
2020-03-08 10:35:31 +00:00
|
|
|
Load ohlcv history data for a list of pairs.
|
2019-12-17 10:43:42 +00:00
|
|
|
|
2019-10-20 12:02:53 +00:00
|
|
|
:param datadir: Path to the data storage location.
|
2020-03-08 10:35:31 +00:00
|
|
|
:param timeframe: Timeframe (e.g. "5m")
|
2019-10-20 12:02:53 +00:00
|
|
|
:param pairs: List of pairs to load
|
|
|
|
:param timerange: Limit data to be loaded to this timerange
|
|
|
|
:param fill_up_missing: Fill missing values with "No action"-candles
|
|
|
|
:param startup_candles: Additional candles to load at the start of the period
|
2019-10-23 18:13:43 +00:00
|
|
|
:param fail_without_data: Raise OperationalException if no data is found.
|
2019-12-28 13:57:39 +00:00
|
|
|
:param data_format: Data format which should be used. Defaults to json
|
2021-12-03 11:23:35 +00:00
|
|
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
2019-10-20 12:02:53 +00:00
|
|
|
:return: dict(<pair>:<Dataframe>)
|
2018-12-13 05:12:10 +00:00
|
|
|
"""
|
2019-05-29 18:25:07 +00:00
|
|
|
result: Dict[str, DataFrame] = {}
|
2019-10-31 05:51:36 +00:00
|
|
|
if startup_candles > 0 and timerange:
|
2024-05-12 15:41:55 +00:00
|
|
|
logger.info(f"Using indicator startup period: {startup_candles} ...")
|
2019-09-20 18:16:49 +00:00
|
|
|
|
2019-12-25 15:12:20 +00:00
|
|
|
data_handler = get_datahandler(datadir, data_format)
|
2019-12-25 14:55:28 +00:00
|
|
|
|
2019-09-20 18:16:49 +00:00
|
|
|
for pair in pairs:
|
2024-05-12 15:41:55 +00:00
|
|
|
hist = load_pair_history(
|
|
|
|
pair=pair,
|
|
|
|
timeframe=timeframe,
|
|
|
|
datadir=datadir,
|
|
|
|
timerange=timerange,
|
|
|
|
fill_up_missing=fill_up_missing,
|
|
|
|
startup_candles=startup_candles,
|
|
|
|
data_handler=data_handler,
|
|
|
|
candle_type=candle_type,
|
|
|
|
)
|
2019-12-04 05:57:44 +00:00
|
|
|
if not hist.empty:
|
2019-09-20 18:16:49 +00:00
|
|
|
result[pair] = hist
|
2022-05-17 21:05:33 +00:00
|
|
|
else:
|
|
|
|
if candle_type is CandleType.FUNDING_RATE and user_futures_funding_rate is not None:
|
2024-02-09 12:12:22 +00:00
|
|
|
logger.warn(f"{pair} using user specified [{user_futures_funding_rate}]")
|
2022-08-18 05:20:49 +00:00
|
|
|
elif candle_type not in (CandleType.SPOT, CandleType.FUTURES):
|
2024-02-09 12:12:22 +00:00
|
|
|
result[pair] = DataFrame(columns=["date", "open", "close", "high", "low", "volume"])
|
2019-10-23 18:13:43 +00:00
|
|
|
|
|
|
|
if fail_without_data and not result:
|
|
|
|
raise OperationalException("No data found. Terminating.")
|
2018-12-13 05:12:10 +00:00
|
|
|
return result
|
|
|
|
|
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
def refresh_data(
|
|
|
|
*,
|
|
|
|
datadir: Path,
|
|
|
|
timeframe: str,
|
|
|
|
pairs: List[str],
|
|
|
|
exchange: Exchange,
|
|
|
|
data_format: Optional[str] = None,
|
|
|
|
timerange: Optional[TimeRange] = None,
|
|
|
|
candle_type: CandleType,
|
|
|
|
) -> None:
|
2019-12-17 10:43:42 +00:00
|
|
|
"""
|
2020-03-08 10:35:31 +00:00
|
|
|
Refresh ohlcv history data for a list of pairs.
|
2019-12-17 10:43:42 +00:00
|
|
|
|
|
|
|
:param datadir: Path to the data storage location.
|
2020-03-08 10:35:31 +00:00
|
|
|
:param timeframe: Timeframe (e.g. "5m")
|
2019-12-17 10:43:42 +00:00
|
|
|
:param pairs: List of pairs to load
|
|
|
|
:param exchange: Exchange object
|
2021-06-25 17:13:31 +00:00
|
|
|
:param data_format: dataformat to use
|
2019-12-17 10:43:42 +00:00
|
|
|
:param timerange: Limit data to be loaded to this timerange
|
2021-12-03 11:23:35 +00:00
|
|
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
2019-12-17 10:43:42 +00:00
|
|
|
"""
|
2019-12-25 15:12:20 +00:00
|
|
|
data_handler = get_datahandler(datadir, data_format)
|
2021-08-19 18:34:02 +00:00
|
|
|
for idx, pair in enumerate(pairs):
|
2024-05-12 15:41:55 +00:00
|
|
|
process = f"{idx}/{len(pairs)}"
|
|
|
|
_download_pair_history(
|
|
|
|
pair=pair,
|
|
|
|
process=process,
|
|
|
|
timeframe=timeframe,
|
|
|
|
datadir=datadir,
|
|
|
|
timerange=timerange,
|
|
|
|
exchange=exchange,
|
|
|
|
data_handler=data_handler,
|
|
|
|
candle_type=candle_type,
|
|
|
|
)
|
2019-12-17 10:43:42 +00:00
|
|
|
|
|
|
|
|
2021-11-21 06:21:10 +00:00
|
|
|
def _load_cached_data_for_updating(
|
|
|
|
pair: str,
|
|
|
|
timeframe: str,
|
|
|
|
timerange: Optional[TimeRange],
|
|
|
|
data_handler: IDataHandler,
|
2022-04-30 13:28:01 +00:00
|
|
|
candle_type: CandleType,
|
|
|
|
prepend: bool = False,
|
|
|
|
) -> Tuple[DataFrame, Optional[int], Optional[int]]:
|
2018-12-13 05:12:10 +00:00
|
|
|
"""
|
2019-08-15 18:13:19 +00:00
|
|
|
Load cached data to download more data.
|
2019-10-06 15:10:40 +00:00
|
|
|
If timerange is passed in, checks whether data from an before the stored data will be
|
|
|
|
downloaded.
|
|
|
|
If that's the case then what's available should be completely overwritten.
|
2019-12-27 09:12:56 +00:00
|
|
|
Otherwise downloads always start at the end of the available data to avoid data gaps.
|
|
|
|
Note: Only used by download_pair_history().
|
2018-12-13 05:12:10 +00:00
|
|
|
"""
|
2019-12-27 05:58:50 +00:00
|
|
|
start = None
|
2022-04-30 13:28:01 +00:00
|
|
|
end = None
|
2019-12-27 05:58:50 +00:00
|
|
|
if timerange:
|
2024-05-12 15:41:55 +00:00
|
|
|
if timerange.starttype == "date":
|
2022-11-10 17:11:39 +00:00
|
|
|
start = timerange.startdt
|
2024-05-12 15:41:55 +00:00
|
|
|
if timerange.stoptype == "date":
|
2022-11-10 17:11:39 +00:00
|
|
|
end = timerange.stopdt
|
2019-12-27 05:58:50 +00:00
|
|
|
|
|
|
|
# Intentionally don't pass timerange in - since we need to load the full dataset.
|
2024-05-12 15:41:55 +00:00
|
|
|
data = data_handler.ohlcv_load(
|
|
|
|
pair,
|
|
|
|
timeframe=timeframe,
|
|
|
|
timerange=None,
|
|
|
|
fill_missing=False,
|
|
|
|
drop_incomplete=True,
|
|
|
|
warn_no_data=False,
|
|
|
|
candle_type=candle_type,
|
|
|
|
)
|
2019-12-27 05:58:50 +00:00
|
|
|
if not data.empty:
|
2024-05-12 15:41:55 +00:00
|
|
|
if not prepend and start and start < data.iloc[0]["date"]:
|
2019-12-27 05:58:50 +00:00
|
|
|
# Earlier data than existing data requested, redownload all
|
2019-12-27 09:11:49 +00:00
|
|
|
data = DataFrame(columns=DEFAULT_DATAFRAME_COLUMNS)
|
|
|
|
else:
|
2022-04-30 13:28:01 +00:00
|
|
|
if prepend:
|
2024-05-12 15:41:55 +00:00
|
|
|
end = data.iloc[0]["date"]
|
2022-04-30 13:28:01 +00:00
|
|
|
else:
|
2024-05-12 15:41:55 +00:00
|
|
|
start = data.iloc[-1]["date"]
|
2019-12-27 05:58:50 +00:00
|
|
|
start_ms = int(start.timestamp() * 1000) if start else None
|
2022-04-30 13:28:01 +00:00
|
|
|
end_ms = int(end.timestamp() * 1000) if end else None
|
|
|
|
return data, start_ms, end_ms
|
2019-12-27 05:58:50 +00:00
|
|
|
|
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
def _download_pair_history(
|
|
|
|
pair: str,
|
|
|
|
*,
|
|
|
|
datadir: Path,
|
|
|
|
exchange: Exchange,
|
|
|
|
timeframe: str = "5m",
|
|
|
|
process: str = "",
|
|
|
|
new_pairs_days: int = 30,
|
|
|
|
data_handler: Optional[IDataHandler] = None,
|
|
|
|
timerange: Optional[TimeRange] = None,
|
|
|
|
candle_type: CandleType,
|
|
|
|
erase: bool = False,
|
|
|
|
prepend: bool = False,
|
|
|
|
) -> bool:
|
2018-12-13 05:12:10 +00:00
|
|
|
"""
|
2019-11-13 10:28:26 +00:00
|
|
|
Download latest candles from the exchange for the pair and timeframe passed in parameters
|
2019-11-02 19:19:13 +00:00
|
|
|
The data is downloaded starting from the last correct data that
|
2018-12-13 05:12:10 +00:00
|
|
|
exists in a cache. If timerange starts earlier than the data in the cache,
|
|
|
|
the full data will be redownloaded
|
|
|
|
|
|
|
|
:param pair: pair to download
|
2020-03-08 10:35:31 +00:00
|
|
|
:param timeframe: Timeframe (e.g "5m")
|
2018-12-13 05:12:10 +00:00
|
|
|
:param timerange: range of time to download
|
2021-12-03 11:23:35 +00:00
|
|
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
2022-04-10 07:46:23 +00:00
|
|
|
:param erase: Erase existing data
|
2018-12-16 13:14:17 +00:00
|
|
|
:return: bool with success state
|
2018-12-13 05:12:10 +00:00
|
|
|
"""
|
2019-12-27 12:46:25 +00:00
|
|
|
data_handler = get_datahandler(datadir, data_handler=data_handler)
|
2019-12-25 15:12:20 +00:00
|
|
|
|
2018-12-16 09:29:53 +00:00
|
|
|
try:
|
2022-04-10 07:46:23 +00:00
|
|
|
if erase:
|
|
|
|
if data_handler.ohlcv_purge(pair, timeframe, candle_type=candle_type):
|
2024-05-12 15:41:55 +00:00
|
|
|
logger.info(f"Deleting existing data for pair {pair}, {timeframe}, {candle_type}.")
|
2022-04-10 07:46:23 +00:00
|
|
|
|
2022-04-30 13:28:01 +00:00
|
|
|
data, since_ms, until_ms = _load_cached_data_for_updating(
|
2024-05-12 15:41:55 +00:00
|
|
|
pair,
|
|
|
|
timeframe,
|
|
|
|
timerange,
|
2022-04-30 13:28:01 +00:00
|
|
|
data_handler=data_handler,
|
|
|
|
candle_type=candle_type,
|
2024-05-12 15:41:55 +00:00
|
|
|
prepend=prepend,
|
|
|
|
)
|
2022-04-30 15:24:57 +00:00
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
logger.info(
|
|
|
|
f'({process}) - Download history data for "{pair}", {timeframe}, '
|
2024-05-13 17:49:15 +00:00
|
|
|
f"{candle_type} and store in {datadir}. "
|
2024-05-12 15:41:55 +00:00
|
|
|
f'From {format_ms_time(since_ms) if since_ms else "start"} to '
|
|
|
|
f'{format_ms_time(until_ms) if until_ms else "now"}'
|
|
|
|
)
|
2018-12-13 05:12:10 +00:00
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
logger.debug(
|
|
|
|
"Current Start: %s",
|
|
|
|
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}" if not data.empty else "None",
|
|
|
|
)
|
|
|
|
logger.debug(
|
|
|
|
"Current End: %s",
|
|
|
|
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}" if not data.empty else "None",
|
|
|
|
)
|
2018-12-13 05:12:10 +00:00
|
|
|
|
2018-12-16 09:29:53 +00:00
|
|
|
# Default since_ms to 30 days if nothing is given
|
2024-05-12 15:41:55 +00:00
|
|
|
new_data = exchange.get_historic_ohlcv(
|
|
|
|
pair=pair,
|
|
|
|
timeframe=timeframe,
|
2024-05-13 17:49:15 +00:00
|
|
|
since_ms=(
|
|
|
|
since_ms
|
|
|
|
if since_ms
|
|
|
|
else int((datetime.now() - timedelta(days=new_pairs_days)).timestamp()) * 1000
|
|
|
|
),
|
2024-05-12 15:41:55 +00:00
|
|
|
is_new_pair=data.empty,
|
|
|
|
candle_type=candle_type,
|
|
|
|
until_ms=until_ms if until_ms else None,
|
|
|
|
)
|
2019-12-27 05:58:50 +00:00
|
|
|
# TODO: Maybe move parsing to exchange class (?)
|
2024-05-12 15:41:55 +00:00
|
|
|
new_dataframe = ohlcv_to_dataframe(
|
|
|
|
new_data, timeframe, pair, fill_missing=False, drop_incomplete=True
|
|
|
|
)
|
2019-12-27 05:58:50 +00:00
|
|
|
if data.empty:
|
|
|
|
data = new_dataframe
|
|
|
|
else:
|
2020-07-25 15:06:58 +00:00
|
|
|
# Run cleaning again to ensure there were no duplicate candles
|
|
|
|
# Especially between existing and new data.
|
2024-05-12 15:41:55 +00:00
|
|
|
data = clean_ohlcv_dataframe(
|
|
|
|
concat([data, new_dataframe], axis=0),
|
|
|
|
timeframe,
|
|
|
|
pair,
|
|
|
|
fill_missing=False,
|
|
|
|
drop_incomplete=False,
|
|
|
|
)
|
2018-12-13 05:12:10 +00:00
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
logger.debug(
|
|
|
|
"New Start: %s",
|
|
|
|
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}" if not data.empty else "None",
|
|
|
|
)
|
|
|
|
logger.debug(
|
|
|
|
"New End: %s",
|
|
|
|
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}" if not data.empty else "None",
|
|
|
|
)
|
2018-12-13 05:12:10 +00:00
|
|
|
|
2024-02-09 12:12:22 +00:00
|
|
|
data_handler.ohlcv_store(pair, timeframe, data=data, candle_type=candle_type)
|
2018-12-16 09:29:53 +00:00
|
|
|
return True
|
2019-05-17 16:05:36 +00:00
|
|
|
|
2020-11-25 06:57:23 +00:00
|
|
|
except Exception:
|
|
|
|
logger.exception(
|
|
|
|
f'Failed to download history data for pair: "{pair}", timeframe: {timeframe}.'
|
2019-05-17 16:05:36 +00:00
|
|
|
)
|
2019-01-31 05:51:03 +00:00
|
|
|
return False
|
2019-05-25 14:51:52 +00:00
|
|
|
|
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
def refresh_backtest_ohlcv_data(
|
|
|
|
exchange: Exchange,
|
|
|
|
pairs: List[str],
|
|
|
|
timeframes: List[str],
|
|
|
|
datadir: Path,
|
|
|
|
trading_mode: str,
|
|
|
|
timerange: Optional[TimeRange] = None,
|
|
|
|
new_pairs_days: int = 30,
|
|
|
|
erase: bool = False,
|
|
|
|
data_format: Optional[str] = None,
|
|
|
|
prepend: bool = False,
|
|
|
|
) -> List[str]:
|
2019-08-25 13:01:27 +00:00
|
|
|
"""
|
|
|
|
Refresh stored ohlcv data for backtesting and hyperopt operations.
|
2019-12-16 18:43:33 +00:00
|
|
|
Used by freqtrade download-data subcommand.
|
|
|
|
:return: List of pairs that are not available.
|
2019-08-25 13:01:27 +00:00
|
|
|
"""
|
|
|
|
pairs_not_available = []
|
2019-12-25 15:12:20 +00:00
|
|
|
data_handler = get_datahandler(datadir, data_format)
|
2021-12-08 13:10:08 +00:00
|
|
|
candle_type = CandleType.get_default(trading_mode)
|
2024-05-12 15:41:55 +00:00
|
|
|
process = ""
|
2021-08-19 18:34:02 +00:00
|
|
|
for idx, pair in enumerate(pairs, start=1):
|
2019-08-25 13:01:27 +00:00
|
|
|
if pair not in exchange.markets:
|
|
|
|
pairs_not_available.append(pair)
|
|
|
|
logger.info(f"Skipping pair {pair}...")
|
|
|
|
continue
|
2019-11-02 19:19:13 +00:00
|
|
|
for timeframe in timeframes:
|
2024-05-12 15:41:55 +00:00
|
|
|
logger.debug(f"Downloading pair {pair}, {candle_type}, interval {timeframe}.")
|
|
|
|
process = f"{idx}/{len(pairs)}"
|
|
|
|
_download_pair_history(
|
|
|
|
pair=pair,
|
|
|
|
process=process,
|
|
|
|
datadir=datadir,
|
|
|
|
exchange=exchange,
|
|
|
|
timerange=timerange,
|
|
|
|
data_handler=data_handler,
|
|
|
|
timeframe=str(timeframe),
|
|
|
|
new_pairs_days=new_pairs_days,
|
|
|
|
candle_type=candle_type,
|
|
|
|
erase=erase,
|
|
|
|
prepend=prepend,
|
|
|
|
)
|
|
|
|
if trading_mode == "futures":
|
2021-12-05 09:01:44 +00:00
|
|
|
# Predefined candletype (and timeframe) depending on exchange
|
|
|
|
# Downloads what is necessary to backtest based on futures data.
|
2024-05-12 15:41:55 +00:00
|
|
|
tf_mark = exchange.get_option("mark_ohlcv_timeframe")
|
|
|
|
tf_funding_rate = exchange.get_option("funding_fee_timeframe")
|
2024-01-04 14:52:19 +00:00
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price"))
|
2021-12-18 14:32:02 +00:00
|
|
|
# All exchanges need FundingRate for futures trading.
|
|
|
|
# The timeframe is aligned to the mark-price timeframe.
|
2024-02-09 12:12:22 +00:00
|
|
|
combs = ((CandleType.FUNDING_RATE, tf_funding_rate), (fr_candle_type, tf_mark))
|
2024-01-04 15:03:53 +00:00
|
|
|
for candle_type_f, tf in combs:
|
2024-05-12 15:41:55 +00:00
|
|
|
logger.debug(f"Downloading pair {pair}, {candle_type_f}, interval {tf}.")
|
|
|
|
_download_pair_history(
|
|
|
|
pair=pair,
|
|
|
|
process=process,
|
|
|
|
datadir=datadir,
|
|
|
|
exchange=exchange,
|
|
|
|
timerange=timerange,
|
|
|
|
data_handler=data_handler,
|
|
|
|
timeframe=str(tf),
|
|
|
|
new_pairs_days=new_pairs_days,
|
|
|
|
candle_type=candle_type_f,
|
|
|
|
erase=erase,
|
|
|
|
prepend=prepend,
|
|
|
|
)
|
2021-12-03 13:43:49 +00:00
|
|
|
|
2019-08-25 13:01:27 +00:00
|
|
|
return pairs_not_available
|
|
|
|
|
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
def _download_trades_history(
|
|
|
|
exchange: Exchange,
|
|
|
|
pair: str,
|
|
|
|
*,
|
|
|
|
new_pairs_days: int = 30,
|
|
|
|
timerange: Optional[TimeRange] = None,
|
|
|
|
data_handler: IDataHandler,
|
|
|
|
trading_mode: TradingMode,
|
|
|
|
) -> bool:
|
2019-08-25 12:30:09 +00:00
|
|
|
"""
|
|
|
|
Download trade history from the exchange.
|
|
|
|
Appends to previously downloaded trades data.
|
|
|
|
"""
|
2019-08-16 08:51:04 +00:00
|
|
|
try:
|
2021-05-15 11:15:19 +00:00
|
|
|
until = None
|
2022-08-22 19:18:02 +00:00
|
|
|
since = 0
|
2022-04-30 15:44:57 +00:00
|
|
|
if timerange:
|
2024-05-12 15:41:55 +00:00
|
|
|
if timerange.starttype == "date":
|
2022-04-30 15:44:57 +00:00
|
|
|
since = timerange.startts * 1000
|
2024-05-12 15:41:55 +00:00
|
|
|
if timerange.stoptype == "date":
|
2021-05-15 11:20:36 +00:00
|
|
|
until = timerange.stopts * 1000
|
2019-08-16 08:51:04 +00:00
|
|
|
|
2024-03-02 12:10:57 +00:00
|
|
|
trades = data_handler.trades_load(pair, trading_mode)
|
2019-08-16 08:51:04 +00:00
|
|
|
|
2020-03-31 18:20:10 +00:00
|
|
|
# TradesList columns are defined in constants.DEFAULT_TRADES_COLUMNS
|
|
|
|
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
|
|
|
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
2020-03-31 18:46:42 +00:00
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
if not trades.empty and since > 0 and since < trades.iloc[0]["timestamp"]:
|
2020-06-24 15:40:23 +00:00
|
|
|
# since is before the first trade
|
2024-05-12 15:41:55 +00:00
|
|
|
logger.info(
|
|
|
|
f"Start ({trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}) earlier than "
|
|
|
|
f"available data. Redownloading trades for {pair}..."
|
|
|
|
)
|
2023-08-18 07:07:51 +00:00
|
|
|
trades = trades_list_to_df([])
|
2020-06-24 15:40:23 +00:00
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
from_id = trades.iloc[-1]["id"] if not trades.empty else None
|
|
|
|
if not trades.empty and since < trades.iloc[-1]["timestamp"]:
|
2020-03-31 18:46:42 +00:00
|
|
|
# Reset since to the last available point
|
2020-04-01 18:04:36 +00:00
|
|
|
# - 5 seconds (to ensure we're getting all trades)
|
2024-05-12 15:41:55 +00:00
|
|
|
since = trades.iloc[-1]["timestamp"] - (5 * 1000)
|
|
|
|
logger.info(
|
|
|
|
f"Using last trade date -5s - Downloading trades for {pair} "
|
|
|
|
f"since: {format_ms_time(since)}."
|
|
|
|
)
|
2019-08-16 08:51:04 +00:00
|
|
|
|
2023-08-20 09:44:40 +00:00
|
|
|
if not since:
|
|
|
|
since = dt_ts(dt_now() - timedelta(days=new_pairs_days))
|
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
logger.debug(
|
|
|
|
"Current Start: %s",
|
|
|
|
"None" if trades.empty else f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}",
|
|
|
|
)
|
|
|
|
logger.debug(
|
|
|
|
"Current End: %s",
|
|
|
|
"None" if trades.empty else f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}",
|
|
|
|
)
|
2020-03-31 18:20:10 +00:00
|
|
|
logger.info(f"Current Amount of trades: {len(trades)}")
|
2019-08-16 08:51:04 +00:00
|
|
|
|
2019-12-16 19:12:26 +00:00
|
|
|
# Default since_ms to 30 days if nothing is given
|
2024-05-12 15:41:55 +00:00
|
|
|
new_trades = exchange.get_historic_trades(
|
|
|
|
pair=pair,
|
|
|
|
since=since,
|
|
|
|
until=until,
|
|
|
|
from_id=from_id,
|
|
|
|
)
|
2023-08-18 07:07:51 +00:00
|
|
|
new_trades_df = trades_list_to_df(new_trades[1])
|
|
|
|
trades = concat([trades, new_trades_df], axis=0)
|
2020-04-01 05:58:39 +00:00
|
|
|
# Remove duplicates to make sure we're not storing data we don't need
|
2023-08-18 07:07:51 +00:00
|
|
|
trades = trades_df_remove_duplicates(trades)
|
2024-03-02 12:10:57 +00:00
|
|
|
data_handler.trades_store(pair, trades, trading_mode)
|
2019-08-16 08:51:04 +00:00
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
logger.debug(
|
|
|
|
"New Start: %s",
|
|
|
|
"None" if trades.empty else f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}",
|
|
|
|
)
|
|
|
|
logger.debug(
|
|
|
|
"New End: %s",
|
|
|
|
"None" if trades.empty else f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}",
|
|
|
|
)
|
2019-08-16 08:51:04 +00:00
|
|
|
logger.info(f"New Amount of trades: {len(trades)}")
|
2019-08-29 10:56:10 +00:00
|
|
|
return True
|
2019-08-16 08:51:04 +00:00
|
|
|
|
2020-11-25 06:57:23 +00:00
|
|
|
except Exception:
|
|
|
|
logger.exception(
|
2023-04-26 13:14:45 +00:00
|
|
|
f'Failed to download and store historic trades for pair: "{pair}". '
|
2019-08-16 08:51:04 +00:00
|
|
|
)
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
def refresh_backtest_trades_data(
|
|
|
|
exchange: Exchange,
|
|
|
|
pairs: List[str],
|
|
|
|
datadir: Path,
|
|
|
|
timerange: TimeRange,
|
|
|
|
trading_mode: TradingMode,
|
|
|
|
new_pairs_days: int = 30,
|
|
|
|
erase: bool = False,
|
|
|
|
data_format: str = "feather",
|
|
|
|
) -> List[str]:
|
2019-08-27 05:13:50 +00:00
|
|
|
"""
|
2019-12-16 18:43:33 +00:00
|
|
|
Refresh stored trades data for backtesting and hyperopt operations.
|
|
|
|
Used by freqtrade download-data subcommand.
|
|
|
|
:return: List of pairs that are not available.
|
2019-08-27 05:13:50 +00:00
|
|
|
"""
|
|
|
|
pairs_not_available = []
|
2019-12-25 15:34:27 +00:00
|
|
|
data_handler = get_datahandler(datadir, data_format=data_format)
|
2019-08-27 05:13:50 +00:00
|
|
|
for pair in pairs:
|
|
|
|
if pair not in exchange.markets:
|
|
|
|
pairs_not_available.append(pair)
|
|
|
|
logger.info(f"Skipping pair {pair}...")
|
|
|
|
continue
|
|
|
|
|
2019-12-26 08:51:03 +00:00
|
|
|
if erase:
|
2024-03-02 12:10:57 +00:00
|
|
|
if data_handler.trades_purge(pair, trading_mode):
|
2024-05-12 15:41:55 +00:00
|
|
|
logger.info(f"Deleting existing data for pair {pair}.")
|
|
|
|
|
|
|
|
logger.info(f"Downloading trades for pair {pair}.")
|
|
|
|
_download_trades_history(
|
|
|
|
exchange=exchange,
|
|
|
|
pair=pair,
|
|
|
|
new_pairs_days=new_pairs_days,
|
|
|
|
timerange=timerange,
|
|
|
|
data_handler=data_handler,
|
|
|
|
trading_mode=trading_mode,
|
|
|
|
)
|
2019-08-27 05:13:50 +00:00
|
|
|
return pairs_not_available
|
|
|
|
|
|
|
|
|
2021-05-06 17:34:10 +00:00
|
|
|
def get_timerange(data: Dict[str, DataFrame]) -> Tuple[datetime, datetime]:
|
2019-05-25 14:51:52 +00:00
|
|
|
"""
|
2019-12-17 22:06:03 +00:00
|
|
|
Get the maximum common timerange for the given backtest data.
|
|
|
|
|
2019-05-25 14:51:52 +00:00
|
|
|
:param data: dictionary with preprocessed backtesting data
|
|
|
|
:return: tuple containing min_date, max_date
|
|
|
|
"""
|
2019-12-17 22:06:03 +00:00
|
|
|
timeranges = [
|
2024-05-12 15:41:55 +00:00
|
|
|
(frame["date"].min().to_pydatetime(), frame["date"].max().to_pydatetime())
|
2019-05-25 14:51:52 +00:00
|
|
|
for frame in data.values()
|
|
|
|
]
|
2024-05-12 15:41:55 +00:00
|
|
|
return (
|
|
|
|
min(timeranges, key=operator.itemgetter(0))[0],
|
|
|
|
max(timeranges, key=operator.itemgetter(1))[1],
|
|
|
|
)
|
2019-05-25 14:51:52 +00:00
|
|
|
|
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
def validate_backtest_data(
|
|
|
|
data: DataFrame, pair: str, min_date: datetime, max_date: datetime, timeframe_min: int
|
|
|
|
) -> bool:
|
2019-05-25 14:51:52 +00:00
|
|
|
"""
|
|
|
|
Validates preprocessed backtesting data for missing values and shows warnings about it that.
|
|
|
|
|
2019-06-15 11:31:27 +00:00
|
|
|
:param data: preprocessed backtesting data (as DataFrame)
|
|
|
|
:param pair: pair used for log output.
|
2019-05-25 14:51:52 +00:00
|
|
|
:param min_date: start-date of the data
|
|
|
|
:param max_date: end-date of the data
|
2020-03-08 10:35:31 +00:00
|
|
|
:param timeframe_min: Timeframe in minutes
|
2019-05-25 14:51:52 +00:00
|
|
|
"""
|
2019-11-02 19:19:13 +00:00
|
|
|
# total difference in minutes / timeframe-minutes
|
2024-02-09 12:12:22 +00:00
|
|
|
expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_min)
|
2019-05-25 14:51:52 +00:00
|
|
|
found_missing = False
|
2019-06-15 11:31:27 +00:00
|
|
|
dflen = len(data)
|
|
|
|
if dflen < expected_frames:
|
|
|
|
found_missing = True
|
2024-05-12 15:41:55 +00:00
|
|
|
logger.warning(
|
|
|
|
"%s has missing frames: expected %s, got %s, that's %s missing values",
|
|
|
|
pair,
|
|
|
|
expected_frames,
|
|
|
|
dflen,
|
|
|
|
expected_frames - dflen,
|
|
|
|
)
|
2019-05-25 14:51:52 +00:00
|
|
|
return found_missing
|
2023-06-17 13:13:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
def download_data_main(config: Config) -> None:
|
|
|
|
timerange = TimeRange()
|
2024-05-12 15:41:55 +00:00
|
|
|
if "days" in config:
|
|
|
|
time_since = (datetime.now() - timedelta(days=config["days"])).strftime("%Y%m%d")
|
|
|
|
timerange = TimeRange.parse_timerange(f"{time_since}-")
|
2023-06-17 13:13:56 +00:00
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
if "timerange" in config:
|
|
|
|
timerange = timerange.parse_timerange(config["timerange"])
|
2023-06-17 13:13:56 +00:00
|
|
|
|
|
|
|
# Remove stake-currency to skip checks which are not relevant for datadownload
|
2024-05-12 15:41:55 +00:00
|
|
|
config["stake_currency"] = ""
|
2023-06-17 13:13:56 +00:00
|
|
|
|
|
|
|
pairs_not_available: List[str] = []
|
|
|
|
|
|
|
|
# Init exchange
|
|
|
|
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
2024-05-12 15:41:55 +00:00
|
|
|
|
2023-06-17 13:13:56 +00:00
|
|
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
2023-06-17 16:14:58 +00:00
|
|
|
available_pairs = [
|
2024-05-12 15:41:55 +00:00
|
|
|
p
|
|
|
|
for p in exchange.get_markets(
|
|
|
|
tradable_only=True, active_only=not config.get("include_inactive")
|
|
|
|
).keys()
|
2023-06-17 16:14:58 +00:00
|
|
|
]
|
2023-06-17 13:13:56 +00:00
|
|
|
|
2023-06-17 16:03:57 +00:00
|
|
|
expanded_pairs = dynamic_expand_pairlist(config, available_pairs)
|
2024-05-12 15:41:55 +00:00
|
|
|
if "timeframes" not in config:
|
|
|
|
config["timeframes"] = DL_DATA_TIMEFRAMES
|
2023-06-17 13:13:56 +00:00
|
|
|
|
|
|
|
# Manual validations of relevant settings
|
2024-05-12 15:41:55 +00:00
|
|
|
if not config["exchange"].get("skip_pair_validation", False):
|
2023-06-17 13:13:56 +00:00
|
|
|
exchange.validate_pairs(expanded_pairs)
|
2024-05-12 15:41:55 +00:00
|
|
|
logger.info(
|
|
|
|
f"About to download pairs: {expanded_pairs}, "
|
|
|
|
f"intervals: {config['timeframes']} to {config['datadir']}"
|
|
|
|
)
|
2023-06-17 13:13:56 +00:00
|
|
|
|
2024-01-07 14:22:53 +00:00
|
|
|
if len(expanded_pairs) == 0:
|
|
|
|
logger.warning(
|
|
|
|
"No pairs available for download. "
|
|
|
|
"Please make sure you're using the correct Pair naming for your selected trade mode. \n"
|
2024-05-12 15:41:55 +00:00
|
|
|
f"More info: {DOCS_LINK}/bot-basics/#pair-naming"
|
|
|
|
)
|
2024-01-07 14:22:53 +00:00
|
|
|
|
2024-05-12 15:41:55 +00:00
|
|
|
for timeframe in config["timeframes"]:
|
2023-06-17 13:13:56 +00:00
|
|
|
exchange.validate_timeframes(timeframe)
|
|
|
|
|
|
|
|
# Start downloading
|
|
|
|
try:
|
2024-05-12 15:41:55 +00:00
|
|
|
if config.get("download_trades"):
|
2023-06-17 13:13:56 +00:00
|
|
|
pairs_not_available = refresh_backtest_trades_data(
|
2024-05-12 15:41:55 +00:00
|
|
|
exchange,
|
|
|
|
pairs=expanded_pairs,
|
|
|
|
datadir=config["datadir"],
|
|
|
|
timerange=timerange,
|
|
|
|
new_pairs_days=config["new_pairs_days"],
|
|
|
|
erase=bool(config.get("erase")),
|
|
|
|
data_format=config["dataformat_trades"],
|
|
|
|
trading_mode=config.get("trading_mode", TradingMode.SPOT),
|
|
|
|
)
|
2023-06-17 13:13:56 +00:00
|
|
|
|
|
|
|
# Convert downloaded trade data to different timeframes
|
|
|
|
convert_trades_to_ohlcv(
|
2024-05-12 15:41:55 +00:00
|
|
|
pairs=expanded_pairs,
|
|
|
|
timeframes=config["timeframes"],
|
|
|
|
datadir=config["datadir"],
|
|
|
|
timerange=timerange,
|
|
|
|
erase=bool(config.get("erase")),
|
|
|
|
data_format_ohlcv=config["dataformat_ohlcv"],
|
|
|
|
data_format_trades=config["dataformat_trades"],
|
|
|
|
candle_type=config.get("candle_type_def", CandleType.SPOT),
|
2023-06-17 13:13:56 +00:00
|
|
|
)
|
|
|
|
else:
|
2024-05-12 15:41:55 +00:00
|
|
|
if not exchange.get_option("ohlcv_has_history", True):
|
2023-06-17 13:13:56 +00:00
|
|
|
raise OperationalException(
|
|
|
|
f"Historic klines not available for {exchange.name}. "
|
|
|
|
"Please use `--dl-trades` instead for this exchange "
|
|
|
|
"(will unfortunately take a long time)."
|
2024-05-12 15:41:55 +00:00
|
|
|
)
|
2024-01-04 15:44:17 +00:00
|
|
|
migrate_data(config, exchange)
|
2023-06-17 13:13:56 +00:00
|
|
|
pairs_not_available = refresh_backtest_ohlcv_data(
|
2024-05-12 15:41:55 +00:00
|
|
|
exchange,
|
|
|
|
pairs=expanded_pairs,
|
|
|
|
timeframes=config["timeframes"],
|
|
|
|
datadir=config["datadir"],
|
|
|
|
timerange=timerange,
|
|
|
|
new_pairs_days=config["new_pairs_days"],
|
|
|
|
erase=bool(config.get("erase")),
|
|
|
|
data_format=config["dataformat_ohlcv"],
|
|
|
|
trading_mode=config.get("trading_mode", "spot"),
|
|
|
|
prepend=config.get("prepend_data", False),
|
2023-06-17 13:13:56 +00:00
|
|
|
)
|
|
|
|
finally:
|
|
|
|
if pairs_not_available:
|
2024-05-12 15:41:55 +00:00
|
|
|
logger.info(
|
|
|
|
f"Pairs [{','.join(pairs_not_available)}] not available "
|
|
|
|
f"on exchange {exchange.name}."
|
|
|
|
)
|