Do not use ticker where it's not a ticker
This commit is contained in:
@@ -296,7 +296,7 @@ class Arguments:
|
||||
# Add convert-data subcommand
|
||||
convert_data_cmd = subparsers.add_parser(
|
||||
'convert-data',
|
||||
help='Convert OHLCV data from one format to another.',
|
||||
help='Convert candle (OHLCV) data from one format to another.',
|
||||
parents=[_common_parser],
|
||||
)
|
||||
convert_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=True))
|
||||
@@ -305,7 +305,7 @@ class Arguments:
|
||||
# Add convert-trade-data subcommand
|
||||
convert_trade_data_cmd = subparsers.add_parser(
|
||||
'convert-trade-data',
|
||||
help='Convert trade-data from one format to another.',
|
||||
help='Convert trade data from one format to another.',
|
||||
parents=[_common_parser],
|
||||
)
|
||||
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
|
||||
|
@@ -76,7 +76,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
{
|
||||
"type": "text",
|
||||
"name": "ticker_interval",
|
||||
"message": "Please insert your ticker interval:",
|
||||
"message": "Please insert your timeframe (ticker interval):",
|
||||
"default": "5m",
|
||||
},
|
||||
{
|
||||
|
@@ -348,7 +348,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"dataformat_ohlcv": Arg(
|
||||
'--data-format-ohlcv',
|
||||
help='Storage format for downloaded ohlcv data. (default: `%(default)s`).',
|
||||
help='Storage format for downloaded candle (OHLCV) data. (default: `%(default)s`).',
|
||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||
default='json'
|
||||
),
|
||||
|
@@ -45,7 +45,7 @@ class TimeRange:
|
||||
"""
|
||||
Adjust startts by <startup_candles> candles.
|
||||
Applies only if no startup-candles have been available.
|
||||
:param timeframe_secs: Ticker timeframe in seconds e.g. `timeframe_to_seconds('5m')`
|
||||
:param timeframe_secs: Timeframe in seconds e.g. `timeframe_to_seconds('5m')`
|
||||
:param startup_candles: Number of candles to move start-date forward
|
||||
:param min_date: Minimum data date loaded. Key kriterium to decide if start-time
|
||||
has to be moved
|
||||
|
@@ -151,17 +151,17 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> p
|
||||
return trades
|
||||
|
||||
|
||||
def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame],
|
||||
column: str = "close") -> pd.DataFrame:
|
||||
def combine_dataframes_with_mean(data: Dict[str, pd.DataFrame],
|
||||
column: str = "close") -> pd.DataFrame:
|
||||
"""
|
||||
Combine multiple dataframes "column"
|
||||
:param tickers: Dict of Dataframes, dict key should be pair.
|
||||
:param data: Dict of Dataframes, dict key should be pair.
|
||||
:param column: Column in the original dataframes to use
|
||||
:return: DataFrame with the column renamed to the dict key, and a column
|
||||
named mean, containing the mean of all pairs.
|
||||
"""
|
||||
df_comb = pd.concat([tickers[pair].set_index('date').rename(
|
||||
{column: pair}, axis=1)[pair] for pair in tickers], axis=1)
|
||||
df_comb = pd.concat([data[pair].set_index('date').rename(
|
||||
{column: pair}, axis=1)[pair] for pair in data], axis=1)
|
||||
|
||||
df_comb['mean'] = df_comb.mean(axis=1)
|
||||
|
||||
|
@@ -13,12 +13,12 @@ from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_ticker_dataframe(ticker: list, timeframe: str, pair: str, *,
|
||||
fill_missing: bool = True,
|
||||
drop_incomplete: bool = True) -> DataFrame:
|
||||
def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *,
|
||||
fill_missing: bool = True, drop_incomplete: bool = True) -> DataFrame:
|
||||
"""
|
||||
Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe
|
||||
:param ticker: ticker list, as returned by exchange.async_get_candle_history
|
||||
Converts a list with candle (OHLCV) data (in format returned by ccxt.fetch_ohlcv)
|
||||
to a Dataframe
|
||||
:param ohlcv: list with candle (OHLCV) data, as returned by exchange.async_get_candle_history
|
||||
:param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data
|
||||
:param pair: Pair this data is for (used to warn if fillup was necessary)
|
||||
:param fill_missing: fill up missing candles with 0 candles
|
||||
@@ -26,21 +26,18 @@ def parse_ticker_dataframe(ticker: list, timeframe: str, pair: str, *,
|
||||
:param drop_incomplete: Drop the last candle of the dataframe, assuming it's incomplete
|
||||
:return: DataFrame
|
||||
"""
|
||||
logger.debug("Parsing tickerlist to dataframe")
|
||||
logger.debug(f"Converting candle (OHLCV) data to dataframe for pair {pair}.")
|
||||
cols = DEFAULT_DATAFRAME_COLUMNS
|
||||
frame = DataFrame(ticker, columns=cols)
|
||||
df = DataFrame(ohlcv, columns=cols)
|
||||
|
||||
frame['date'] = to_datetime(frame['date'],
|
||||
unit='ms',
|
||||
utc=True,
|
||||
infer_datetime_format=True)
|
||||
df['date'] = to_datetime(df['date'], unit='ms', utc=True, infer_datetime_format=True)
|
||||
|
||||
# Some exchanges return int values for volume and even for ohlc.
|
||||
# Some exchanges return int values for Volume and even for OHLC.
|
||||
# Convert them since TA-LIB indicators used in the strategy assume floats
|
||||
# and fail with exception...
|
||||
frame = frame.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float',
|
||||
'volume': 'float'})
|
||||
return clean_ohlcv_dataframe(frame, timeframe, pair,
|
||||
df = df.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float',
|
||||
'volume': 'float'})
|
||||
return clean_ohlcv_dataframe(df, timeframe, pair,
|
||||
fill_missing=fill_missing,
|
||||
drop_incomplete=drop_incomplete)
|
||||
|
||||
@@ -49,11 +46,11 @@ def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *,
|
||||
fill_missing: bool = True,
|
||||
drop_incomplete: bool = True) -> DataFrame:
|
||||
"""
|
||||
Clense a ohlcv dataframe by
|
||||
Clense a OHLCV dataframe by
|
||||
* Grouping it by date (removes duplicate tics)
|
||||
* dropping last candles if requested
|
||||
* Filling up missing data (if requested)
|
||||
:param data: DataFrame containing ohlcv data.
|
||||
:param data: DataFrame containing candle (OHLCV) data.
|
||||
:param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data
|
||||
:param pair: Pair this data is for (used to warn if fillup was necessary)
|
||||
:param fill_missing: fill up missing candles with 0 candles
|
||||
@@ -88,16 +85,16 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
||||
"""
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
|
||||
ohlc_dict = {
|
||||
ohlcv_dict = {
|
||||
'open': 'first',
|
||||
'high': 'max',
|
||||
'low': 'min',
|
||||
'close': 'last',
|
||||
'volume': 'sum'
|
||||
}
|
||||
ticker_minutes = timeframe_to_minutes(timeframe)
|
||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
||||
# Resample to create "NAN" values
|
||||
df = dataframe.resample(f'{ticker_minutes}min', on='date').agg(ohlc_dict)
|
||||
df = dataframe.resample(f'{timeframe_minutes}min', on='date').agg(ohlcv_dict)
|
||||
|
||||
# Forwardfill close for missing columns
|
||||
df['close'] = df['close'].fillna(method='ffill')
|
||||
@@ -159,20 +156,20 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
|
||||
|
||||
def trades_to_ohlcv(trades: list, timeframe: str) -> DataFrame:
|
||||
"""
|
||||
Converts trades list to ohlcv list
|
||||
Converts trades list to OHLCV list
|
||||
TODO: This should get a dedicated test
|
||||
:param trades: List of trades, as returned by ccxt.fetch_trades.
|
||||
:param timeframe: Ticker timeframe to resample data to
|
||||
:return: ohlcv Dataframe.
|
||||
:param timeframe: Timeframe to resample data to
|
||||
:return: OHLCV Dataframe.
|
||||
"""
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
ticker_minutes = timeframe_to_minutes(timeframe)
|
||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
||||
df = pd.DataFrame(trades)
|
||||
df['datetime'] = pd.to_datetime(df['datetime'])
|
||||
df = df.set_index('datetime')
|
||||
|
||||
df_new = df['price'].resample(f'{ticker_minutes}min').ohlc()
|
||||
df_new['volume'] = df['amount'].resample(f'{ticker_minutes}min').sum()
|
||||
df_new = df['price'].resample(f'{timeframe_minutes}min').ohlc()
|
||||
df_new['volume'] = df['amount'].resample(f'{timeframe_minutes}min').sum()
|
||||
df_new['date'] = df_new.index
|
||||
# Drop 0 volume rows
|
||||
df_new = df_new.dropna()
|
||||
@@ -206,7 +203,7 @@ def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to:
|
||||
|
||||
def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to: str, erase: bool):
|
||||
"""
|
||||
Convert ohlcv from one format to another format.
|
||||
Convert OHLCV from one format to another
|
||||
:param config: Config dictionary
|
||||
:param convert_from: Source format
|
||||
:param convert_to: Target format
|
||||
@@ -216,7 +213,7 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to:
|
||||
src = get_datahandler(config['datadir'], convert_from)
|
||||
trg = get_datahandler(config['datadir'], convert_to)
|
||||
timeframes = config.get('timeframes', [config.get('ticker_interval')])
|
||||
logger.info(f"Converting OHLCV for timeframe {timeframes}")
|
||||
logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}")
|
||||
|
||||
if 'pairs' not in config:
|
||||
config['pairs'] = []
|
||||
@@ -224,7 +221,7 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to:
|
||||
for timeframe in timeframes:
|
||||
config['pairs'].extend(src.ohlcv_get_pairs(config['datadir'],
|
||||
timeframe))
|
||||
logger.info(f"Converting OHLCV for {config['pairs']}")
|
||||
logger.info(f"Converting candle (OHLCV) data for {config['pairs']}")
|
||||
|
||||
for timeframe in timeframes:
|
||||
for pair in config['pairs']:
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Dataprovider
|
||||
Responsible to provide data to the bot
|
||||
including Klines, tickers, historic data
|
||||
including ticker and orderbook data, live and historical candle (OHLCV) data
|
||||
Common Interface for bot and strategy to access data.
|
||||
"""
|
||||
import logging
|
||||
@@ -43,10 +43,10 @@ class DataProvider:
|
||||
|
||||
def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame:
|
||||
"""
|
||||
Get ohlcv data for the given pair as DataFrame
|
||||
Get candle (OHLCV) data for the given pair as DataFrame
|
||||
Please use the `available_pairs` method to verify which pairs are currently cached.
|
||||
:param pair: pair to get the data for
|
||||
:param timeframe: Ticker timeframe to get data for
|
||||
:param timeframe: Timeframe to get data for
|
||||
:param copy: copy dataframe before returning if True.
|
||||
Use False only for read-only operations (where the dataframe is not modified)
|
||||
"""
|
||||
@@ -58,7 +58,7 @@ class DataProvider:
|
||||
|
||||
def historic_ohlcv(self, pair: str, timeframe: str = None) -> DataFrame:
|
||||
"""
|
||||
Get stored historic ohlcv data
|
||||
Get stored historical candle (OHLCV) data
|
||||
:param pair: pair to get the data for
|
||||
:param timeframe: timeframe to get data for
|
||||
"""
|
||||
@@ -69,17 +69,17 @@ class DataProvider:
|
||||
|
||||
def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame:
|
||||
"""
|
||||
Return pair ohlcv data, either live or cached historical -- depending
|
||||
Return pair candle (OHLCV) data, either live or cached historical -- depending
|
||||
on the runmode.
|
||||
:param pair: pair to get the data for
|
||||
:param timeframe: timeframe to get data for
|
||||
:return: Dataframe for this pair
|
||||
"""
|
||||
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||
# Get live ohlcv data.
|
||||
# Get live OHLCV data.
|
||||
data = self.ohlcv(pair=pair, timeframe=timeframe)
|
||||
else:
|
||||
# Get historic ohlcv data (cached on disk).
|
||||
# Get historical OHLCV data (cached on disk).
|
||||
data = self.historic_ohlcv(pair=pair, timeframe=timeframe)
|
||||
if len(data) == 0:
|
||||
logger.warning(f"No data found for ({pair}, {timeframe}).")
|
||||
|
@@ -9,7 +9,7 @@ from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
||||
from freqtrade.data.converter import parse_ticker_dataframe, trades_to_ohlcv
|
||||
from freqtrade.data.converter import ohlcv_to_dataframe, trades_to_ohlcv
|
||||
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
@@ -28,10 +28,10 @@ def load_pair_history(pair: str,
|
||||
data_handler: IDataHandler = None,
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Load cached ticker history for the given pair.
|
||||
Load cached ohlcv history for the given pair.
|
||||
|
||||
:param pair: Pair to load data for
|
||||
:param timeframe: Ticker timeframe (e.g. "5m")
|
||||
:param timeframe: Timeframe (e.g. "5m")
|
||||
:param datadir: Path to the data storage location.
|
||||
:param data_format: Format of the data. Ignored if data_handler is set.
|
||||
:param timerange: Limit data to be loaded to this timerange
|
||||
@@ -63,10 +63,10 @@ def load_data(datadir: Path,
|
||||
data_format: str = 'json',
|
||||
) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
Load ticker history data for a list of pairs.
|
||||
Load ohlcv history data for a list of pairs.
|
||||
|
||||
:param datadir: Path to the data storage location.
|
||||
:param timeframe: Ticker Timeframe (e.g. "5m")
|
||||
:param timeframe: Timeframe (e.g. "5m")
|
||||
: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
|
||||
@@ -104,10 +104,10 @@ def refresh_data(datadir: Path,
|
||||
timerange: Optional[TimeRange] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Refresh ticker history data for a list of pairs.
|
||||
Refresh ohlcv history data for a list of pairs.
|
||||
|
||||
:param datadir: Path to the data storage location.
|
||||
:param timeframe: Ticker Timeframe (e.g. "5m")
|
||||
:param timeframe: Timeframe (e.g. "5m")
|
||||
:param pairs: List of pairs to load
|
||||
:param exchange: Exchange object
|
||||
:param timerange: Limit data to be loaded to this timerange
|
||||
@@ -165,7 +165,7 @@ def _download_pair_history(datadir: Path,
|
||||
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
|
||||
|
||||
:param pair: pair to download
|
||||
:param timeframe: Ticker Timeframe (e.g 5m)
|
||||
:param timeframe: Timeframe (e.g "5m")
|
||||
:param timerange: range of time to download
|
||||
:return: bool with success state
|
||||
"""
|
||||
@@ -194,8 +194,8 @@ def _download_pair_history(datadir: Path,
|
||||
days=-30).float_timestamp) * 1000
|
||||
)
|
||||
# TODO: Maybe move parsing to exchange class (?)
|
||||
new_dataframe = parse_ticker_dataframe(new_data, timeframe, pair,
|
||||
fill_missing=False, drop_incomplete=True)
|
||||
new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair,
|
||||
fill_missing=False, drop_incomplete=True)
|
||||
if data.empty:
|
||||
data = new_dataframe
|
||||
else:
|
||||
@@ -362,7 +362,7 @@ def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime,
|
||||
:param pair: pair used for log output.
|
||||
:param min_date: start-date of the data
|
||||
:param max_date: end-date of the data
|
||||
:param timeframe_min: ticker Timeframe in minutes
|
||||
:param timeframe_min: Timeframe in minutes
|
||||
"""
|
||||
# total difference in minutes / timeframe-minutes
|
||||
expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_min)
|
||||
|
@@ -55,7 +55,7 @@ class IDataHandler(ABC):
|
||||
Implements the loading and conversion to a Pandas dataframe.
|
||||
Timerange trimming and dataframe validation happens outside of this method.
|
||||
:param pair: Pair to load data
|
||||
:param timeframe: Ticker timeframe (e.g. "5m")
|
||||
:param timeframe: Timeframe (e.g. "5m")
|
||||
:param timerange: Limit data to be loaded to this timerange.
|
||||
Optionally implemented by subclasses to avoid loading
|
||||
all data where possible.
|
||||
@@ -67,7 +67,7 @@ class IDataHandler(ABC):
|
||||
"""
|
||||
Remove data for this pair
|
||||
:param pair: Delete data for this pair.
|
||||
:param timeframe: Ticker timeframe (e.g. "5m")
|
||||
:param timeframe: Timeframe (e.g. "5m")
|
||||
:return: True when deleted, false if file did not exist.
|
||||
"""
|
||||
|
||||
@@ -129,10 +129,10 @@ class IDataHandler(ABC):
|
||||
warn_no_data: bool = True
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Load cached ticker history for the given pair.
|
||||
Load cached candle (OHLCV) data for the given pair.
|
||||
|
||||
:param pair: Pair to load data for
|
||||
:param timeframe: Ticker timeframe (e.g. "5m")
|
||||
:param timeframe: Timeframe (e.g. "5m")
|
||||
:param timerange: Limit data to be loaded to this timerange
|
||||
:param fill_missing: Fill missing values with "No action"-candles
|
||||
:param drop_incomplete: Drop last candle assuming it may be incomplete.
|
||||
@@ -145,28 +145,27 @@ class IDataHandler(ABC):
|
||||
if startup_candles > 0 and timerange_startup:
|
||||
timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles)
|
||||
|
||||
pairdf = self._ohlcv_load(pair, timeframe,
|
||||
timerange=timerange_startup)
|
||||
if pairdf.empty:
|
||||
df = self._ohlcv_load(pair, timeframe, timerange=timerange_startup)
|
||||
if df.empty:
|
||||
if warn_no_data:
|
||||
logger.warning(
|
||||
f'No history data for pair: "{pair}", timeframe: {timeframe}. '
|
||||
'Use `freqtrade download-data` to download the data'
|
||||
)
|
||||
return pairdf
|
||||
return df
|
||||
else:
|
||||
enddate = pairdf.iloc[-1]['date']
|
||||
enddate = df.iloc[-1]['date']
|
||||
|
||||
if timerange_startup:
|
||||
self._validate_pairdata(pair, pairdf, timerange_startup)
|
||||
pairdf = trim_dataframe(pairdf, timerange_startup)
|
||||
self._validate_pairdata(pair, df, timerange_startup)
|
||||
df = trim_dataframe(df, timerange_startup)
|
||||
|
||||
# incomplete candles should only be dropped if we didn't trim the end beforehand.
|
||||
return clean_ohlcv_dataframe(pairdf, timeframe,
|
||||
return clean_ohlcv_dataframe(df, timeframe,
|
||||
pair=pair,
|
||||
fill_missing=fill_missing,
|
||||
drop_incomplete=(drop_incomplete and
|
||||
enddate == pairdf.iloc[-1]['date']))
|
||||
enddate == df.iloc[-1]['date']))
|
||||
|
||||
def _validate_pairdata(self, pair, pairdata: DataFrame, timerange: TimeRange):
|
||||
"""
|
||||
|
@@ -60,7 +60,7 @@ class JsonDataHandler(IDataHandler):
|
||||
Implements the loading and conversion to a Pandas dataframe.
|
||||
Timerange trimming and dataframe validation happens outside of this method.
|
||||
:param pair: Pair to load data
|
||||
:param timeframe: Ticker timeframe (e.g. "5m")
|
||||
:param timeframe: Timeframe (e.g. "5m")
|
||||
:param timerange: Limit data to be loaded to this timerange.
|
||||
Optionally implemented by subclasses to avoid loading
|
||||
all data where possible.
|
||||
@@ -83,7 +83,7 @@ class JsonDataHandler(IDataHandler):
|
||||
"""
|
||||
Remove data for this pair
|
||||
:param pair: Delete data for this pair.
|
||||
:param timeframe: Ticker timeframe (e.g. "5m")
|
||||
:param timeframe: Timeframe (e.g. "5m")
|
||||
:return: True when deleted, false if file did not exist.
|
||||
"""
|
||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
||||
|
@@ -119,7 +119,7 @@ class Edge:
|
||||
logger.critical("No data found. Edge is stopped ...")
|
||||
return False
|
||||
|
||||
preprocessed = self.strategy.tickerdata_to_dataframe(data)
|
||||
preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
|
||||
|
||||
# Print timeframe
|
||||
min_date, max_date = history.get_timerange(preprocessed)
|
||||
@@ -137,10 +137,10 @@ class Edge:
|
||||
pair_data = pair_data.sort_values(by=['date'])
|
||||
pair_data = pair_data.reset_index(drop=True)
|
||||
|
||||
ticker_data = self.strategy.advise_sell(
|
||||
dataframe = self.strategy.advise_sell(
|
||||
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
|
||||
|
||||
trades += self._find_trades_for_stoploss_range(ticker_data, pair, self._stoploss_range)
|
||||
trades += self._find_trades_for_stoploss_range(dataframe, pair, self._stoploss_range)
|
||||
|
||||
# If no trade found then exit
|
||||
if len(trades) == 0:
|
||||
@@ -359,11 +359,11 @@ class Edge:
|
||||
# Returning a list of pairs in order of "expectancy"
|
||||
return final
|
||||
|
||||
def _find_trades_for_stoploss_range(self, ticker_data, pair, stoploss_range):
|
||||
buy_column = ticker_data['buy'].values
|
||||
sell_column = ticker_data['sell'].values
|
||||
date_column = ticker_data['date'].values
|
||||
ohlc_columns = ticker_data[['open', 'high', 'low', 'close']].values
|
||||
def _find_trades_for_stoploss_range(self, dataframe, pair, stoploss_range):
|
||||
buy_column = dataframe['buy'].values
|
||||
sell_column = dataframe['sell'].values
|
||||
date_column = dataframe['date'].values
|
||||
ohlc_columns = dataframe[['open', 'high', 'low', 'close']].values
|
||||
|
||||
result: list = []
|
||||
for stoploss in stoploss_range:
|
||||
|
@@ -18,7 +18,7 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE,
|
||||
TRUNCATE, decimal_to_precision)
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.data.converter import parse_ticker_dataframe
|
||||
from freqtrade.data.converter import ohlcv_to_dataframe
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
|
||||
@@ -351,7 +351,7 @@ class Exchange:
|
||||
|
||||
def validate_timeframes(self, timeframe: Optional[str]) -> None:
|
||||
"""
|
||||
Checks if ticker interval from config is a supported timeframe on the exchange
|
||||
Check if timeframe from config is a supported timeframe on the exchange
|
||||
"""
|
||||
if not hasattr(self._api, "timeframes") or self._api.timeframes is None:
|
||||
# If timeframes attribute is missing (or is None), the exchange probably
|
||||
@@ -364,7 +364,7 @@ class Exchange:
|
||||
|
||||
if timeframe and (timeframe not in self.timeframes):
|
||||
raise OperationalException(
|
||||
f"Invalid ticker interval '{timeframe}'. This exchange supports: {self.timeframes}")
|
||||
f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}")
|
||||
|
||||
if timeframe and timeframe_to_minutes(timeframe) < 1:
|
||||
raise OperationalException(
|
||||
@@ -599,7 +599,7 @@ class Exchange:
|
||||
return self._api.fetch_tickers()
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching tickers in batch.'
|
||||
f'Exchange {self._api.name} does not support fetching tickers in batch. '
|
||||
f'Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
@@ -623,13 +623,13 @@ class Exchange:
|
||||
def get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int) -> List:
|
||||
"""
|
||||
Gets candle history using asyncio and returns the list of candles.
|
||||
Handles all async doing.
|
||||
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call.
|
||||
Get candle history using asyncio and returns the list of candles.
|
||||
Handles all async work for this.
|
||||
Async over one pair, assuming we get `self._ohlcv_candle_limit` candles per call.
|
||||
:param pair: Pair to download
|
||||
:param timeframe: Ticker Timeframe to get
|
||||
:param timeframe: Timeframe to get data for
|
||||
:param since_ms: Timestamp in milliseconds to get history from
|
||||
:returns List of tickers
|
||||
:returns List with candle (OHLCV) data
|
||||
"""
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
|
||||
@@ -649,26 +649,27 @@ class Exchange:
|
||||
pair, timeframe, since) for since in
|
||||
range(since_ms, arrow.utcnow().timestamp * 1000, one_call)]
|
||||
|
||||
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
||||
results = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
||||
|
||||
# Combine tickers
|
||||
# Combine gathered results
|
||||
data: List = []
|
||||
for p, timeframe, ticker in tickers:
|
||||
for p, timeframe, res in results:
|
||||
if p == pair:
|
||||
data.extend(ticker)
|
||||
data.extend(res)
|
||||
# Sort data again after extending the result - above calls return in "async order"
|
||||
data = sorted(data, key=lambda x: x[0])
|
||||
logger.info("downloaded %s with length %s.", pair, len(data))
|
||||
logger.info("Downloaded data for %s with length %s.", pair, len(data))
|
||||
return data
|
||||
|
||||
def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]:
|
||||
"""
|
||||
Refresh in-memory ohlcv asynchronously and set `_klines` with the result
|
||||
Refresh in-memory OHLCV asynchronously and set `_klines` with the result
|
||||
Loops asynchronously over pair_list and downloads all pairs async (semi-parallel).
|
||||
Only used in the dataprovider.refresh() method.
|
||||
:param pair_list: List of 2 element tuples containing pair, interval to refresh
|
||||
:return: Returns a List of ticker-dataframes.
|
||||
:return: TODO: return value is only used in the tests, get rid of it
|
||||
"""
|
||||
logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list))
|
||||
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
|
||||
|
||||
input_coroutines = []
|
||||
|
||||
@@ -679,15 +680,15 @@ class Exchange:
|
||||
input_coroutines.append(self._async_get_candle_history(pair, timeframe))
|
||||
else:
|
||||
logger.debug(
|
||||
"Using cached ohlcv data for pair %s, timeframe %s ...",
|
||||
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...",
|
||||
pair, timeframe
|
||||
)
|
||||
|
||||
tickers = asyncio.get_event_loop().run_until_complete(
|
||||
results = asyncio.get_event_loop().run_until_complete(
|
||||
asyncio.gather(*input_coroutines, return_exceptions=True))
|
||||
|
||||
# handle caching
|
||||
for res in tickers:
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||
continue
|
||||
@@ -698,13 +699,14 @@ class Exchange:
|
||||
if ticks:
|
||||
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
|
||||
# keeping parsed dataframe in cache
|
||||
self._klines[(pair, timeframe)] = parse_ticker_dataframe(
|
||||
self._klines[(pair, timeframe)] = ohlcv_to_dataframe(
|
||||
ticks, timeframe, pair=pair, fill_missing=True,
|
||||
drop_incomplete=self._ohlcv_partial_candle)
|
||||
return tickers
|
||||
|
||||
return results
|
||||
|
||||
def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool:
|
||||
# Calculating ticker interval in seconds
|
||||
# Timeframe in seconds
|
||||
interval_in_sec = timeframe_to_seconds(timeframe)
|
||||
|
||||
return not ((self._pairs_last_refresh_time.get((pair, timeframe), 0)
|
||||
@@ -714,11 +716,11 @@ class Exchange:
|
||||
async def _async_get_candle_history(self, pair: str, timeframe: str,
|
||||
since_ms: Optional[int] = None) -> Tuple[str, str, List]:
|
||||
"""
|
||||
Asynchronously gets candle histories using fetch_ohlcv
|
||||
Asynchronously get candle history data using fetch_ohlcv
|
||||
returns tuple: (pair, timeframe, ohlcv_list)
|
||||
"""
|
||||
try:
|
||||
# fetch ohlcv asynchronously
|
||||
# Fetch OHLCV asynchronously
|
||||
s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
|
||||
logger.debug(
|
||||
"Fetching pair %s, interval %s, since %s %s...",
|
||||
@@ -728,9 +730,9 @@ class Exchange:
|
||||
data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe,
|
||||
since=since_ms)
|
||||
|
||||
# Because some exchange sort Tickers ASC and other DESC.
|
||||
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
|
||||
# when GDAX returns a list of tickers DESC (newest first, oldest last)
|
||||
# Some exchanges sort OHLCV in ASC order and others in DESC.
|
||||
# Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
|
||||
# while GDAX returns the list of OHLCV in DESC order (newest first, oldest last)
|
||||
# Only sort if necessary to save computing time
|
||||
try:
|
||||
if data and data[0][0] > data[-1][0]:
|
||||
@@ -743,14 +745,15 @@ class Exchange:
|
||||
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching historical candlestick data.'
|
||||
f'Message: {e}') from e
|
||||
f'Exchange {self._api.name} does not support fetching historical '
|
||||
f'candle (OHLCV) data. Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(f'Could not load ticker history for pair {pair} due to '
|
||||
f'{e.__class__.__name__}. Message: {e}') from e
|
||||
raise TemporaryError(f'Could not fetch historical candle (OHLCV) data '
|
||||
f'for pair {pair} due to {e.__class__.__name__}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(f'Could not fetch ticker data for pair {pair}. '
|
||||
f'Msg: {e}') from e
|
||||
raise OperationalException(f'Could not fetch historical candle (OHLCV) data '
|
||||
f'for pair {pair}. Message: {e}') from e
|
||||
|
||||
@retrier_async
|
||||
async def _async_fetch_trades(self, pair: str,
|
||||
@@ -883,14 +886,14 @@ class Exchange:
|
||||
until: Optional[int] = None,
|
||||
from_id: Optional[str] = None) -> Tuple[str, List]:
|
||||
"""
|
||||
Gets candle history using asyncio and returns the list of candles.
|
||||
Handles all async doing.
|
||||
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call.
|
||||
Get trade history data using asyncio.
|
||||
Handles all async work and returns the list of candles.
|
||||
Async over one pair, assuming we get `self._ohlcv_candle_limit` candles per call.
|
||||
:param pair: Pair to download
|
||||
:param since: Timestamp in milliseconds to get history from
|
||||
:param until: Timestamp in milliseconds. Defaults to current timestamp if not defined.
|
||||
:param from_id: Download data starting with ID (if id is known)
|
||||
:returns List of tickers
|
||||
:returns List of trade data
|
||||
"""
|
||||
if not self.exchange_has("fetchTrades"):
|
||||
raise OperationalException("This exchange does not suport downloading Trades.")
|
||||
|
@@ -172,8 +172,8 @@ class FreqtradeBot:
|
||||
_whitelist = self.edge.adjust(_whitelist)
|
||||
|
||||
if trades:
|
||||
# Extend active-pair whitelist with pairs from open trades
|
||||
# It ensures that tickers are downloaded for open trades
|
||||
# Extend active-pair whitelist with pairs of open trades
|
||||
# It ensures that candle (OHLCV) data are downloaded for open trades as well
|
||||
_whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist])
|
||||
return _whitelist
|
||||
|
||||
@@ -628,7 +628,7 @@ class FreqtradeBot:
|
||||
|
||||
def get_sell_rate(self, pair: str, refresh: bool) -> float:
|
||||
"""
|
||||
Get sell rate - either using get-ticker bid or first bid based on orderbook
|
||||
Get sell rate - either using ticker bid or first bid based on orderbook
|
||||
The orderbook portion is only used for rpc messaging, which would otherwise fail
|
||||
for BitMex (has no bid/ask in fetch_ticker)
|
||||
or remain static in any other case since it's not updating.
|
||||
@@ -1043,7 +1043,7 @@ class FreqtradeBot:
|
||||
"""
|
||||
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||
# Use cached ticker here - it was updated seconds ago.
|
||||
# Use cached rates here - it was updated seconds ago.
|
||||
current_rate = self.get_sell_rate(trade.pair, False)
|
||||
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
||||
gain = "profit" if profit_ratio > 0 else "loss"
|
||||
|
@@ -81,13 +81,13 @@ def file_load_json(file):
|
||||
gzipfile = file
|
||||
# Try gzip file first, otherwise regular json file.
|
||||
if gzipfile.is_file():
|
||||
logger.debug('Loading ticker data from file %s', gzipfile)
|
||||
with gzip.open(gzipfile) as tickerdata:
|
||||
pairdata = json_load(tickerdata)
|
||||
logger.debug(f"Loading historical data from file {gzipfile}")
|
||||
with gzip.open(gzipfile) as datafile:
|
||||
pairdata = json_load(datafile)
|
||||
elif file.is_file():
|
||||
logger.debug('Loading ticker data from file %s', file)
|
||||
with open(file) as tickerdata:
|
||||
pairdata = json_load(tickerdata)
|
||||
logger.debug(f"Loading historical data from file {file}")
|
||||
with open(file) as datafile:
|
||||
pairdata = json_load(datafile)
|
||||
else:
|
||||
return None
|
||||
return pairdata
|
||||
|
@@ -88,8 +88,8 @@ class Backtesting:
|
||||
validate_config_consistency(self.config)
|
||||
|
||||
if "ticker_interval" not in self.config:
|
||||
raise OperationalException("Ticker-interval needs to be set in either configuration "
|
||||
"or as cli argument `--ticker-interval 5m`")
|
||||
raise OperationalException("Timeframe (ticker interval) needs to be set in either "
|
||||
"configuration or as cli argument `--ticker-interval 5m`")
|
||||
self.timeframe = str(self.config.get('ticker_interval'))
|
||||
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
||||
|
||||
@@ -151,32 +151,33 @@ class Backtesting:
|
||||
logger.info(f'Dumping backtest results to {recordfilename}')
|
||||
file_dump_json(recordfilename, records)
|
||||
|
||||
def _get_ticker_list(self, processed: Dict) -> Dict[str, DataFrame]:
|
||||
def _get_ohlcv_as_lists(self, processed: Dict) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
Helper function to convert a processed tickerlist into a list for performance reasons.
|
||||
Helper function to convert a processed dataframes into lists for performance reasons.
|
||||
|
||||
Used by backtest() - so keep this optimized for performance.
|
||||
"""
|
||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
|
||||
ticker: Dict = {}
|
||||
# Create ticker dict
|
||||
data: Dict = {}
|
||||
# Create dict with data
|
||||
for pair, pair_data in processed.items():
|
||||
pair_data.loc[:, 'buy'] = 0 # cleanup from previous run
|
||||
pair_data.loc[:, 'sell'] = 0 # cleanup from previous run
|
||||
|
||||
ticker_data = self.strategy.advise_sell(
|
||||
dataframe = self.strategy.advise_sell(
|
||||
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
|
||||
|
||||
# to avoid using data from future, we buy/sell with signal from previous candle
|
||||
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)
|
||||
ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1)
|
||||
# To avoid using data from future, we use buy/sell signals shifted
|
||||
# from the previous candle
|
||||
dataframe.loc[:, 'buy'] = dataframe['buy'].shift(1)
|
||||
dataframe.loc[:, 'sell'] = dataframe['sell'].shift(1)
|
||||
|
||||
ticker_data.drop(ticker_data.head(1).index, inplace=True)
|
||||
dataframe.drop(dataframe.head(1).index, inplace=True)
|
||||
|
||||
# Convert from Pandas to list for performance reasons
|
||||
# (Looping Pandas is slow.)
|
||||
ticker[pair] = [x for x in ticker_data.itertuples()]
|
||||
return ticker
|
||||
data[pair] = [x for x in dataframe.itertuples()]
|
||||
return data
|
||||
|
||||
def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple,
|
||||
trade_dur: int) -> float:
|
||||
@@ -220,7 +221,7 @@ class Backtesting:
|
||||
|
||||
def _get_sell_trade_entry(
|
||||
self, pair: str, buy_row: DataFrame,
|
||||
partial_ticker: List, trade_count_lock: Dict,
|
||||
partial_ohlcv: List, trade_count_lock: Dict,
|
||||
stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]:
|
||||
|
||||
trade = Trade(
|
||||
@@ -235,7 +236,7 @@ class Backtesting:
|
||||
)
|
||||
logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
|
||||
# calculate win/lose forwards from buy point
|
||||
for sell_row in partial_ticker:
|
||||
for sell_row in partial_ohlcv:
|
||||
if max_open_trades > 0:
|
||||
# Increase trade_count_lock for every iteration
|
||||
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
|
||||
@@ -259,9 +260,9 @@ class Backtesting:
|
||||
close_rate=closerate,
|
||||
sell_reason=sell.sell_type
|
||||
)
|
||||
if partial_ticker:
|
||||
if partial_ohlcv:
|
||||
# no sell condition found - trade stil open at end of backtest period
|
||||
sell_row = partial_ticker[-1]
|
||||
sell_row = partial_ohlcv[-1]
|
||||
bt_res = BacktestResult(pair=pair,
|
||||
profit_percent=trade.calc_profit_ratio(rate=sell_row.open),
|
||||
profit_abs=trade.calc_profit(rate=sell_row.open),
|
||||
@@ -308,8 +309,9 @@ class Backtesting:
|
||||
trades = []
|
||||
trade_count_lock: Dict = {}
|
||||
|
||||
# Dict of ticker-lists for performance (looping lists is a lot faster than dataframes)
|
||||
ticker: Dict = self._get_ticker_list(processed)
|
||||
# Use dict of lists with data for performance
|
||||
# (looping lists is a lot faster than pandas DataFrames)
|
||||
data: Dict = self._get_ohlcv_as_lists(processed)
|
||||
|
||||
lock_pair_until: Dict = {}
|
||||
# Indexes per pair, so some pairs are allowed to have a missing start.
|
||||
@@ -319,12 +321,12 @@ class Backtesting:
|
||||
# Loop timerange and get candle for each pair at that point in time
|
||||
while tmp < end_date:
|
||||
|
||||
for i, pair in enumerate(ticker):
|
||||
for i, pair in enumerate(data):
|
||||
if pair not in indexes:
|
||||
indexes[pair] = 0
|
||||
|
||||
try:
|
||||
row = ticker[pair][indexes[pair]]
|
||||
row = data[pair][indexes[pair]]
|
||||
except IndexError:
|
||||
# missing Data for one pair at the end.
|
||||
# Warnings for this are shown during data loading
|
||||
@@ -352,7 +354,7 @@ class Backtesting:
|
||||
|
||||
# since indexes has been incremented before, we need to go one step back to
|
||||
# also check the buying candle for sell conditions.
|
||||
trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]-1:],
|
||||
trade_entry = self._get_sell_trade_entry(pair, row, data[pair][indexes[pair]-1:],
|
||||
trade_count_lock, stake_amount,
|
||||
max_open_trades)
|
||||
|
||||
@@ -395,7 +397,7 @@ class Backtesting:
|
||||
self._set_strategy(strat)
|
||||
|
||||
# need to reprocess data every time to populate signals
|
||||
preprocessed = self.strategy.tickerdata_to_dataframe(data)
|
||||
preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
|
||||
|
||||
# Trim startup period from analyzed dataframe
|
||||
for pair, df in preprocessed.items():
|
||||
|
@@ -75,8 +75,8 @@ class Hyperopt:
|
||||
|
||||
self.trials_file = (self.config['user_data_dir'] /
|
||||
'hyperopt_results' / 'hyperopt_results.pickle')
|
||||
self.tickerdata_pickle = (self.config['user_data_dir'] /
|
||||
'hyperopt_results' / 'hyperopt_tickerdata.pkl')
|
||||
self.data_pickle_file = (self.config['user_data_dir'] /
|
||||
'hyperopt_results' / 'hyperopt_data.pkl')
|
||||
self.total_epochs = config.get('epochs', 0)
|
||||
|
||||
self.current_best_loss = 100
|
||||
@@ -130,7 +130,7 @@ class Hyperopt:
|
||||
"""
|
||||
Remove hyperopt pickle files to restart hyperopt.
|
||||
"""
|
||||
for f in [self.tickerdata_pickle, self.trials_file]:
|
||||
for f in [self.data_pickle_file, self.trials_file]:
|
||||
p = Path(f)
|
||||
if p.is_file():
|
||||
logger.info(f"Removing `{p}`.")
|
||||
@@ -454,7 +454,7 @@ class Hyperopt:
|
||||
self.backtesting.strategy.trailing_only_offset_is_reached = \
|
||||
d['trailing_only_offset_is_reached']
|
||||
|
||||
processed = load(self.tickerdata_pickle)
|
||||
processed = load(self.data_pickle_file)
|
||||
|
||||
min_date, max_date = get_timerange(processed)
|
||||
|
||||
@@ -570,7 +570,7 @@ class Hyperopt:
|
||||
self.hyperopt_table_header = -1
|
||||
data, timerange = self.backtesting.load_bt_data()
|
||||
|
||||
preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data)
|
||||
preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data)
|
||||
|
||||
# Trim startup period from analyzed dataframe
|
||||
for pair, df in preprocessed.items():
|
||||
@@ -581,7 +581,7 @@ class Hyperopt:
|
||||
'Hyperopting with data from %s up to %s (%s days)..',
|
||||
min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days
|
||||
)
|
||||
dump(preprocessed, self.tickerdata_pickle)
|
||||
dump(preprocessed, self.data_pickle_file)
|
||||
|
||||
# We don't need exchange instance anymore while running hyperopt
|
||||
self.backtesting.exchange = None # type: ignore
|
||||
|
@@ -6,7 +6,7 @@ import pandas as pd
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data.btanalysis import (calculate_max_drawdown,
|
||||
combine_tickers_with_mean,
|
||||
combine_dataframes_with_mean,
|
||||
create_cum_profit,
|
||||
extract_trades_of_period, load_trades)
|
||||
from freqtrade.data.converter import trim_dataframe
|
||||
@@ -29,7 +29,7 @@ except ImportError:
|
||||
def init_plotscript(config):
|
||||
"""
|
||||
Initialize objects needed for plotting
|
||||
:return: Dict with tickers, trades and pairs
|
||||
:return: Dict with candle (OHLCV) data, trades and pairs
|
||||
"""
|
||||
|
||||
if "pairs" in config:
|
||||
@@ -40,7 +40,7 @@ def init_plotscript(config):
|
||||
# Set timerange to use
|
||||
timerange = TimeRange.parse_timerange(config.get("timerange"))
|
||||
|
||||
tickers = load_data(
|
||||
data = load_data(
|
||||
datadir=config.get("datadir"),
|
||||
pairs=pairs,
|
||||
timeframe=config.get('ticker_interval', '5m'),
|
||||
@@ -53,7 +53,7 @@ def init_plotscript(config):
|
||||
exportfilename=config.get('exportfilename'),
|
||||
)
|
||||
trades = trim_dataframe(trades, timerange, 'open_time')
|
||||
return {"tickers": tickers,
|
||||
return {"ohlcv": data,
|
||||
"trades": trades,
|
||||
"pairs": pairs,
|
||||
}
|
||||
@@ -368,10 +368,10 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
|
||||
return fig
|
||||
|
||||
|
||||
def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame],
|
||||
def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
|
||||
trades: pd.DataFrame, timeframe: str) -> go.Figure:
|
||||
# Combine close-values for all pairs, rename columns to "pair"
|
||||
df_comb = combine_tickers_with_mean(tickers, "close")
|
||||
df_comb = combine_dataframes_with_mean(data, "close")
|
||||
|
||||
# Add combined cumulative profit
|
||||
df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe)
|
||||
@@ -439,7 +439,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
||||
"""
|
||||
From configuration provided
|
||||
- Initializes plot-script
|
||||
- Get tickers data
|
||||
- Get candle (OHLCV) data
|
||||
- Generate Dafaframes populated with indicators and signals based on configured strategy
|
||||
- Load trades excecuted during the selected period
|
||||
- Generate Plotly plot objects
|
||||
@@ -451,13 +451,13 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
||||
plot_elements = init_plotscript(config)
|
||||
trades = plot_elements['trades']
|
||||
pair_counter = 0
|
||||
for pair, data in plot_elements["tickers"].items():
|
||||
for pair, data in plot_elements["ohlcv"].items():
|
||||
pair_counter += 1
|
||||
logger.info("analyse pair %s", pair)
|
||||
tickers = {}
|
||||
tickers[pair] = data
|
||||
ohlcv = {}
|
||||
ohlcv[pair] = data
|
||||
|
||||
dataframe = strategy.analyze_ticker(tickers[pair], {'pair': pair})
|
||||
dataframe = strategy.analyze_ticker(ohlcv[pair], {'pair': pair})
|
||||
trades_pair = trades.loc[trades['pair'] == pair]
|
||||
trades_pair = extract_trades_of_period(dataframe, trades_pair)
|
||||
|
||||
@@ -494,7 +494,7 @@ def plot_profit(config: Dict[str, Any]) -> None:
|
||||
|
||||
# Create an average close price of all the pairs that were involved.
|
||||
# this could be useful to gauge the overall market trend
|
||||
fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"],
|
||||
fig = generate_profit_graph(plot_elements["pairs"], plot_elements["ohlcv"],
|
||||
trades, config.get('ticker_interval', '5m'))
|
||||
store_plot_file(fig, filename='freqtrade-profit-plot.html',
|
||||
directory=config['user_data_dir'] / "plot", auto_open=True)
|
||||
|
@@ -59,7 +59,7 @@ class IStrategy(ABC):
|
||||
Attributes you can use:
|
||||
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
||||
stoploss -> float: optimal stoploss designed for the strategy
|
||||
ticker_interval -> str: value of the ticker interval to use for the strategy
|
||||
ticker_interval -> str: value of the timeframe (ticker interval) to use with the strategy
|
||||
"""
|
||||
# Strategy interface version
|
||||
# Default to version 2
|
||||
@@ -125,7 +125,7 @@ class IStrategy(ABC):
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Populate indicators that will be used in the Buy and Sell strategy
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||
:param dataframe: DataFrame with data from the exchange
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
@@ -200,11 +200,11 @@ class IStrategy(ABC):
|
||||
|
||||
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Parses the given ticker history and returns a populated DataFrame
|
||||
Parses the given candle (OHLCV) data and returns a populated DataFrame
|
||||
add several TA indicators and buy signal to it
|
||||
:param dataframe: Dataframe containing ticker data
|
||||
:param dataframe: Dataframe containing data from exchange
|
||||
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
|
||||
:return: DataFrame with ticker data and indicator data
|
||||
:return: DataFrame of candle (OHLCV) data with indicator data and signals added
|
||||
"""
|
||||
logger.debug("TA Analysis Launched")
|
||||
dataframe = self.advise_indicators(dataframe, metadata)
|
||||
@@ -214,12 +214,12 @@ class IStrategy(ABC):
|
||||
|
||||
def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Parses the given ticker history and returns a populated DataFrame
|
||||
Parses the given candle (OHLCV) data and returns a populated DataFrame
|
||||
add several TA indicators and buy signal to it
|
||||
WARNING: Used internally only, may skip analysis if `process_only_new_candles` is set.
|
||||
:param dataframe: Dataframe containing ticker data
|
||||
:param dataframe: Dataframe containing data from exchange
|
||||
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
|
||||
:return: DataFrame with ticker data and indicator data
|
||||
:return: DataFrame of candle (OHLCV) data with indicator data and signals added
|
||||
"""
|
||||
pair = str(metadata.get('pair'))
|
||||
|
||||
@@ -251,21 +251,21 @@ class IStrategy(ABC):
|
||||
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
||||
"""
|
||||
if not isinstance(dataframe, DataFrame) or dataframe.empty:
|
||||
logger.warning('Empty ticker history for pair %s', pair)
|
||||
logger.warning('Empty candle (OHLCV) data for pair %s', pair)
|
||||
return False, False
|
||||
|
||||
try:
|
||||
dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair})
|
||||
except ValueError as error:
|
||||
logger.warning(
|
||||
'Unable to analyze ticker for pair %s: %s',
|
||||
'Unable to analyze candle (OHLCV) data for pair %s: %s',
|
||||
pair,
|
||||
str(error)
|
||||
)
|
||||
return False, False
|
||||
except Exception as error:
|
||||
logger.exception(
|
||||
'Unexpected error when analyzing ticker for pair %s: %s',
|
||||
'Unexpected error when analyzing candle (OHLCV) data for pair %s: %s',
|
||||
pair,
|
||||
str(error)
|
||||
)
|
||||
@@ -440,19 +440,19 @@ class IStrategy(ABC):
|
||||
else:
|
||||
return current_profit > roi
|
||||
|
||||
def tickerdata_to_dataframe(self, tickerdata: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
|
||||
def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
Creates a dataframe and populates indicators for given ticker data
|
||||
Creates a dataframe and populates indicators for given candle (OHLCV) data
|
||||
Used by optimize operations only, not during dry / live runs.
|
||||
"""
|
||||
return {pair: self.advise_indicators(pair_data, {'pair': pair})
|
||||
for pair, pair_data in tickerdata.items()}
|
||||
for pair, pair_data in data.items()}
|
||||
|
||||
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Populate indicators that will be used in the Buy and Sell strategy
|
||||
This method should not be overridden.
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||
:param dataframe: Dataframe with data from the exchange
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
|
@@ -99,7 +99,7 @@ class {{ strategy }}(IStrategy):
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||
:param dataframe: Dataframe with data from the exchange
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
|
@@ -116,7 +116,7 @@ class SampleStrategy(IStrategy):
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||
:param dataframe: Dataframe with data from the exchange
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
|
Reference in New Issue
Block a user