2018-06-23 12:18:30 +00:00
|
|
|
import logging
|
2019-06-30 07:41:43 +00:00
|
|
|
from pathlib import Path
|
2022-02-03 23:34:38 +00:00
|
|
|
from typing import Any, Dict, List
|
2019-05-29 05:19:21 +00:00
|
|
|
|
2019-05-28 05:00:57 +00:00
|
|
|
import pandas as pd
|
2020-01-04 02:07:51 +00:00
|
|
|
|
2019-08-14 08:07:32 +00:00
|
|
|
from freqtrade.configuration import TimeRange
|
2022-01-01 13:06:15 +00:00
|
|
|
from freqtrade.data.btanalysis import (analyze_trade_parallelism, calculate_max_drawdown,
|
2022-01-01 13:40:20 +00:00
|
|
|
calculate_underwater, combine_dataframes_with_mean,
|
2020-09-28 17:39:41 +00:00
|
|
|
create_cum_profit, extract_trades_of_period, load_trades)
|
2020-01-04 02:07:51 +00:00
|
|
|
from freqtrade.data.converter import trim_dataframe
|
2020-07-22 13:15:50 +00:00
|
|
|
from freqtrade.data.dataprovider import DataProvider
|
2020-11-14 08:28:00 +00:00
|
|
|
from freqtrade.data.history import get_timerange, load_data
|
2020-05-21 05:13:08 +00:00
|
|
|
from freqtrade.exceptions import OperationalException
|
2020-11-14 08:28:00 +00:00
|
|
|
from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_seconds
|
2020-01-04 02:07:51 +00:00
|
|
|
from freqtrade.misc import pair_to_filename
|
2021-01-12 00:13:58 +00:00
|
|
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
2020-07-22 13:15:50 +00:00
|
|
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
|
|
|
from freqtrade.strategy import IStrategy
|
2018-06-23 12:18:30 +00:00
|
|
|
|
2020-09-28 17:39:41 +00:00
|
|
|
|
2018-06-23 12:18:30 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2019-07-22 18:39:38 +00:00
|
|
|
import plotly.graph_objects as go
|
2020-09-28 17:39:41 +00:00
|
|
|
from plotly.offline import plot
|
|
|
|
from plotly.subplots import make_subplots
|
2018-06-23 12:18:30 +00:00
|
|
|
except ImportError:
|
2019-08-22 14:51:00 +00:00
|
|
|
logger.exception("Module plotly not found \n Please install using `pip3 install plotly`")
|
2019-05-29 05:19:21 +00:00
|
|
|
exit(1)
|
2019-05-28 05:00:57 +00:00
|
|
|
|
|
|
|
|
2021-01-12 00:13:58 +00:00
|
|
|
def init_plotscript(config, markets: List, startup_candles: int = 0):
|
2019-06-30 09:06:51 +00:00
|
|
|
"""
|
|
|
|
Initialize objects needed for plotting
|
2020-03-08 10:35:31 +00:00
|
|
|
:return: Dict with candle (OHLCV) data, trades and pairs
|
2019-06-30 09:06:51 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
if "pairs" in config:
|
2021-01-12 00:13:58 +00:00
|
|
|
pairs = expand_pairlist(config['pairs'], markets)
|
2019-06-30 09:06:51 +00:00
|
|
|
else:
|
2021-01-12 00:13:58 +00:00
|
|
|
pairs = expand_pairlist(config['exchange']['pair_whitelist'], markets)
|
2019-06-30 09:06:51 +00:00
|
|
|
|
|
|
|
# Set timerange to use
|
2020-08-26 18:52:09 +00:00
|
|
|
timerange = TimeRange.parse_timerange(config.get('timerange'))
|
2019-06-30 09:06:51 +00:00
|
|
|
|
2020-03-08 10:35:31 +00:00
|
|
|
data = load_data(
|
2020-08-26 18:52:09 +00:00
|
|
|
datadir=config.get('datadir'),
|
2019-06-30 09:06:51 +00:00
|
|
|
pairs=pairs,
|
2021-06-12 07:03:55 +00:00
|
|
|
timeframe=config['timeframe'],
|
2019-06-30 09:06:51 +00:00
|
|
|
timerange=timerange,
|
2020-11-14 08:28:00 +00:00
|
|
|
startup_candles=startup_candles,
|
2019-12-28 13:57:39 +00:00
|
|
|
data_format=config.get('dataformat_ohlcv', 'json'),
|
2019-06-30 09:06:51 +00:00
|
|
|
)
|
|
|
|
|
2021-02-08 19:08:32 +00:00
|
|
|
if startup_candles and data:
|
2020-11-14 08:28:00 +00:00
|
|
|
min_date, max_date = get_timerange(data)
|
|
|
|
logger.info(f"Loading data from {min_date} to {max_date}")
|
2021-06-12 07:03:55 +00:00
|
|
|
timerange.adjust_start_if_necessary(timeframe_to_seconds(config['timeframe']),
|
2020-11-14 08:28:00 +00:00
|
|
|
startup_candles, min_date)
|
|
|
|
|
2020-03-15 20:20:32 +00:00
|
|
|
no_trades = False
|
2020-06-28 08:17:08 +00:00
|
|
|
filename = config.get('exportfilename')
|
2020-03-15 20:20:32 +00:00
|
|
|
if config.get('no_trades', False):
|
|
|
|
no_trades = True
|
2020-06-28 08:17:08 +00:00
|
|
|
elif config['trade_source'] == 'file':
|
|
|
|
if not filename.is_dir() and not filename.is_file():
|
|
|
|
logger.warning("Backtest file is missing skipping trades.")
|
|
|
|
no_trades = True
|
2021-02-08 19:08:32 +00:00
|
|
|
try:
|
|
|
|
trades = load_trades(
|
|
|
|
config['trade_source'],
|
|
|
|
db_url=config.get('db_url'),
|
|
|
|
exportfilename=filename,
|
|
|
|
no_trades=no_trades,
|
|
|
|
strategy=config.get('strategy'),
|
|
|
|
)
|
|
|
|
except ValueError as e:
|
|
|
|
raise OperationalException(e) from e
|
2021-05-22 15:03:16 +00:00
|
|
|
if not trades.empty:
|
|
|
|
trades = trim_dataframe(trades, timerange, 'open_date')
|
2020-03-14 21:15:03 +00:00
|
|
|
|
2020-03-08 10:35:31 +00:00
|
|
|
return {"ohlcv": data,
|
2019-06-30 09:06:51 +00:00
|
|
|
"trades": trades,
|
|
|
|
"pairs": pairs,
|
2020-11-14 08:28:00 +00:00
|
|
|
"timerange": timerange,
|
2019-06-30 09:06:51 +00:00
|
|
|
}
|
2019-06-30 07:41:43 +00:00
|
|
|
|
|
|
|
|
2020-01-04 10:13:45 +00:00
|
|
|
def add_indicators(fig, row, indicators: Dict[str, Dict], data: pd.DataFrame) -> make_subplots:
|
2019-05-28 05:00:57 +00:00
|
|
|
"""
|
2020-01-03 12:27:22 +00:00
|
|
|
Generate all the indicators selected by the user for a specific row, based on the configuration
|
2019-05-28 05:00:57 +00:00
|
|
|
:param fig: Plot figure to append to
|
|
|
|
:param row: row number for this plot
|
2020-01-03 12:27:22 +00:00
|
|
|
:param indicators: Dict of Indicators with configuration options.
|
|
|
|
Dict key must correspond to dataframe column.
|
2019-05-28 05:00:57 +00:00
|
|
|
:param data: candlestick DataFrame
|
|
|
|
"""
|
2021-05-23 08:22:59 +00:00
|
|
|
plot_kinds = {
|
|
|
|
'scatter': go.Scatter,
|
|
|
|
'bar': go.Bar,
|
|
|
|
}
|
2020-01-03 12:27:22 +00:00
|
|
|
for indicator, conf in indicators.items():
|
2020-01-04 10:13:45 +00:00
|
|
|
logger.debug(f"indicator {indicator} with config {conf}")
|
2019-05-28 05:00:57 +00:00
|
|
|
if indicator in data:
|
2020-01-03 19:10:22 +00:00
|
|
|
kwargs = {'x': data['date'],
|
|
|
|
'y': data[indicator].values,
|
|
|
|
'name': indicator
|
|
|
|
}
|
2021-05-23 08:22:59 +00:00
|
|
|
|
|
|
|
plot_type = conf.get('type', 'scatter')
|
|
|
|
color = conf.get('color')
|
|
|
|
if plot_type == 'bar':
|
|
|
|
kwargs.update({'marker_color': color or 'DarkSlateGrey',
|
|
|
|
'marker_line_color': color or 'DarkSlateGrey'})
|
|
|
|
else:
|
|
|
|
if color:
|
|
|
|
kwargs.update({'line': {'color': color}})
|
|
|
|
kwargs['mode'] = 'lines'
|
|
|
|
if plot_type != 'scatter':
|
2021-05-30 15:39:33 +00:00
|
|
|
logger.warning(f'Indicator {indicator} has unknown plot trace kind {plot_type}'
|
2021-05-23 08:22:59 +00:00
|
|
|
f', assuming "scatter".')
|
|
|
|
|
|
|
|
kwargs.update(conf.get('plotly', {}))
|
|
|
|
trace = plot_kinds[plot_type](**kwargs)
|
|
|
|
fig.add_trace(trace, row, 1)
|
2019-05-28 05:00:57 +00:00
|
|
|
else:
|
|
|
|
logger.info(
|
|
|
|
'Indicator "%s" ignored. Reason: This indicator is not found '
|
|
|
|
'in your strategy.',
|
|
|
|
indicator
|
|
|
|
)
|
|
|
|
|
|
|
|
return fig
|
|
|
|
|
|
|
|
|
2019-07-22 18:39:38 +00:00
|
|
|
def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_subplots:
|
2019-06-30 08:14:33 +00:00
|
|
|
"""
|
|
|
|
Add profit-plot
|
|
|
|
:param fig: Plot figure to append to
|
|
|
|
:param row: row number for this plot
|
|
|
|
:param data: candlestick DataFrame
|
|
|
|
:param column: Column to use for plot
|
|
|
|
:param name: Name to use
|
|
|
|
:return: fig with added profit plot
|
|
|
|
"""
|
2019-10-05 08:32:42 +00:00
|
|
|
profit = go.Scatter(
|
2019-06-30 08:14:33 +00:00
|
|
|
x=data.index,
|
|
|
|
y=data[column],
|
|
|
|
name=name,
|
|
|
|
)
|
2019-07-22 18:39:38 +00:00
|
|
|
fig.add_trace(profit, row, 1)
|
2019-06-30 08:14:33 +00:00
|
|
|
|
|
|
|
return fig
|
|
|
|
|
|
|
|
|
2020-04-05 12:35:53 +00:00
|
|
|
def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
|
|
|
|
timeframe: str) -> make_subplots:
|
2020-03-03 06:21:14 +00:00
|
|
|
"""
|
|
|
|
Add scatter points indicating max drawdown
|
|
|
|
"""
|
|
|
|
try:
|
2022-01-04 14:57:58 +00:00
|
|
|
_, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown(trades)
|
2020-03-03 06:21:14 +00:00
|
|
|
|
|
|
|
drawdown = go.Scatter(
|
|
|
|
x=[highdate, lowdate],
|
|
|
|
y=[
|
2020-04-05 12:35:53 +00:00
|
|
|
df_comb.loc[timeframe_to_prev_date(timeframe, highdate), 'cum_profit'],
|
|
|
|
df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), 'cum_profit'],
|
2020-03-03 06:21:14 +00:00
|
|
|
],
|
|
|
|
mode='markers',
|
2021-11-11 12:55:55 +00:00
|
|
|
name=f"Max drawdown {max_drawdown:.2%}",
|
|
|
|
text=f"Max drawdown {max_drawdown:.2%}",
|
2020-03-03 06:21:14 +00:00
|
|
|
marker=dict(
|
|
|
|
symbol='square-open',
|
|
|
|
size=9,
|
|
|
|
line=dict(width=2),
|
|
|
|
color='green'
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
fig.add_trace(drawdown, row, 1)
|
|
|
|
except ValueError:
|
|
|
|
logger.warning("No trades found - not plotting max drawdown.")
|
|
|
|
return fig
|
|
|
|
|
|
|
|
|
2022-01-01 13:40:20 +00:00
|
|
|
def add_underwater(fig, row, trades: pd.DataFrame) -> make_subplots:
|
|
|
|
"""
|
|
|
|
Add underwater plot
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
underwater = calculate_underwater(trades, value_col="profit_abs")
|
|
|
|
|
|
|
|
underwater = go.Scatter(
|
|
|
|
x=underwater['date'],
|
|
|
|
y=underwater['drawdown'],
|
|
|
|
name="Underwater Plot",
|
2022-01-01 15:40:18 +00:00
|
|
|
fill='tozeroy',
|
|
|
|
fillcolor='#cc362b',
|
|
|
|
line={'color': '#cc362b'},
|
2022-01-01 13:40:20 +00:00
|
|
|
)
|
|
|
|
fig.add_trace(underwater, row, 1)
|
|
|
|
except ValueError:
|
|
|
|
logger.warning("No trades found - not plotting underwater plot")
|
|
|
|
return fig
|
|
|
|
|
|
|
|
|
2022-01-01 13:06:15 +00:00
|
|
|
def add_parallelism(fig, row, trades: pd.DataFrame, timeframe: str) -> make_subplots:
|
|
|
|
"""
|
|
|
|
Add Chart showing trade parallelism
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
result = analyze_trade_parallelism(trades, timeframe)
|
|
|
|
|
2022-01-01 15:40:18 +00:00
|
|
|
drawdown = go.Scatter(
|
2022-01-01 13:06:15 +00:00
|
|
|
x=result.index,
|
|
|
|
y=result['open_trades'],
|
|
|
|
name="Parallel trades",
|
2022-01-01 15:40:18 +00:00
|
|
|
fill='tozeroy',
|
|
|
|
fillcolor='#242222',
|
|
|
|
line={'color': '#242222'},
|
2022-01-01 13:06:15 +00:00
|
|
|
)
|
|
|
|
fig.add_trace(drawdown, row, 1)
|
|
|
|
except ValueError:
|
|
|
|
logger.warning("No trades found - not plotting Parallelism.")
|
|
|
|
return fig
|
|
|
|
|
|
|
|
|
2019-07-22 18:39:38 +00:00
|
|
|
def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
2019-05-28 05:00:57 +00:00
|
|
|
"""
|
2019-06-30 07:47:07 +00:00
|
|
|
Add trades to "fig"
|
2019-05-28 05:00:57 +00:00
|
|
|
"""
|
|
|
|
# Trades can be empty
|
2019-05-28 18:23:16 +00:00
|
|
|
if trades is not None and len(trades) > 0:
|
2020-01-04 19:27:27 +00:00
|
|
|
# Create description for sell summarizing the trade
|
2022-01-20 21:00:33 +00:00
|
|
|
trades['desc'] = trades.apply(
|
|
|
|
lambda row: f"{row['profit_ratio']:.2%}, " +
|
2022-01-22 16:25:21 +00:00
|
|
|
(f"{row['enter_tag']}, " if row['enter_tag'] is not None else "") +
|
2022-01-20 21:00:33 +00:00
|
|
|
f"{row['sell_reason']}, " +
|
|
|
|
f"{row['trade_duration']} min",
|
|
|
|
axis=1)
|
2019-05-28 05:00:57 +00:00
|
|
|
trade_buys = go.Scatter(
|
2020-06-26 07:19:44 +00:00
|
|
|
x=trades["open_date"],
|
2019-05-28 05:00:57 +00:00
|
|
|
y=trades["open_rate"],
|
|
|
|
mode='markers',
|
2020-01-04 19:27:27 +00:00
|
|
|
name='Trade buy',
|
|
|
|
text=trades["desc"],
|
|
|
|
marker=dict(
|
|
|
|
symbol='circle-open',
|
|
|
|
size=11,
|
|
|
|
line=dict(width=2),
|
|
|
|
color='cyan'
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
trade_sells = go.Scatter(
|
2021-01-23 12:02:48 +00:00
|
|
|
x=trades.loc[trades['profit_ratio'] > 0, "close_date"],
|
|
|
|
y=trades.loc[trades['profit_ratio'] > 0, "close_rate"],
|
|
|
|
text=trades.loc[trades['profit_ratio'] > 0, "desc"],
|
2020-01-04 19:27:27 +00:00
|
|
|
mode='markers',
|
|
|
|
name='Sell - Profit',
|
2019-05-28 05:00:57 +00:00
|
|
|
marker=dict(
|
|
|
|
symbol='square-open',
|
|
|
|
size=11,
|
|
|
|
line=dict(width=2),
|
|
|
|
color='green'
|
|
|
|
)
|
|
|
|
)
|
2020-01-04 19:27:27 +00:00
|
|
|
trade_sells_loss = go.Scatter(
|
2021-01-23 12:02:48 +00:00
|
|
|
x=trades.loc[trades['profit_ratio'] <= 0, "close_date"],
|
|
|
|
y=trades.loc[trades['profit_ratio'] <= 0, "close_rate"],
|
|
|
|
text=trades.loc[trades['profit_ratio'] <= 0, "desc"],
|
2019-05-28 05:00:57 +00:00
|
|
|
mode='markers',
|
2020-01-04 19:27:27 +00:00
|
|
|
name='Sell - Loss',
|
2019-05-28 05:00:57 +00:00
|
|
|
marker=dict(
|
|
|
|
symbol='square-open',
|
|
|
|
size=11,
|
|
|
|
line=dict(width=2),
|
|
|
|
color='red'
|
|
|
|
)
|
|
|
|
)
|
2019-07-22 18:39:38 +00:00
|
|
|
fig.add_trace(trade_buys, 1, 1)
|
|
|
|
fig.add_trace(trade_sells, 1, 1)
|
2020-01-04 19:27:27 +00:00
|
|
|
fig.add_trace(trade_sells_loss, 1, 1)
|
2019-06-22 13:45:20 +00:00
|
|
|
else:
|
|
|
|
logger.warning("No trades found.")
|
2019-05-28 05:00:57 +00:00
|
|
|
return fig
|
|
|
|
|
2020-12-19 16:06:21 +00:00
|
|
|
|
2020-01-04 11:54:58 +00:00
|
|
|
def create_plotconfig(indicators1: List[str], indicators2: List[str],
|
|
|
|
plot_config: Dict[str, Dict]) -> Dict[str, Dict]:
|
2020-01-04 10:13:45 +00:00
|
|
|
"""
|
|
|
|
Combines indicators 1 and indicators 2 into plot_config if necessary
|
|
|
|
:param indicators1: List containing Main plot indicators
|
|
|
|
:param indicators2: List containing Sub plot indicators
|
|
|
|
:param plot_config: Dict of Dicts containing advanced plot configuration
|
|
|
|
:return: plot_config - eventually with indicators 1 and 2
|
|
|
|
"""
|
2020-05-21 06:28:58 +00:00
|
|
|
|
2020-12-19 13:14:20 +00:00
|
|
|
if plot_config:
|
2020-01-05 18:50:21 +00:00
|
|
|
if indicators1:
|
2020-12-19 13:14:20 +00:00
|
|
|
plot_config['main_plot'] = {ind: {} for ind in indicators1}
|
2020-01-05 18:50:21 +00:00
|
|
|
if indicators2:
|
2020-12-19 13:14:20 +00:00
|
|
|
plot_config['subplots'] = {'Other': {ind: {} for ind in indicators2}}
|
2020-01-05 18:50:21 +00:00
|
|
|
|
2020-01-04 10:13:45 +00:00
|
|
|
if not plot_config:
|
|
|
|
# If no indicators and no plot-config given, use defaults.
|
|
|
|
if not indicators1:
|
|
|
|
indicators1 = ['sma', 'ema3', 'ema5']
|
|
|
|
if not indicators2:
|
2020-01-04 10:30:21 +00:00
|
|
|
indicators2 = ['macd', 'macdsignal']
|
2020-01-04 10:13:45 +00:00
|
|
|
|
|
|
|
# Create subplot configuration if plot_config is not available.
|
|
|
|
plot_config = {
|
|
|
|
'main_plot': {ind: {} for ind in indicators1},
|
|
|
|
'subplots': {'Other': {ind: {} for ind in indicators2}},
|
|
|
|
}
|
2020-12-19 13:14:20 +00:00
|
|
|
if 'main_plot' not in plot_config:
|
|
|
|
plot_config['main_plot'] = {}
|
2020-05-17 10:24:04 +00:00
|
|
|
|
2020-12-19 13:14:20 +00:00
|
|
|
if 'subplots' not in plot_config:
|
|
|
|
plot_config['subplots'] = {}
|
2020-01-04 10:18:51 +00:00
|
|
|
return plot_config
|
2020-01-04 10:13:45 +00:00
|
|
|
|
2020-12-19 16:06:21 +00:00
|
|
|
|
2020-12-19 20:37:52 +00:00
|
|
|
def plot_area(fig, row: int, data: pd.DataFrame, indicator_a: str,
|
|
|
|
indicator_b: str, label: str = "",
|
|
|
|
fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots:
|
|
|
|
""" Creates a plot for the area between two traces and adds it to fig.
|
2020-12-19 16:06:21 +00:00
|
|
|
:param fig: Plot figure to append to
|
|
|
|
:param row: row number for this plot
|
|
|
|
:param data: candlestick DataFrame
|
2021-06-25 13:45:49 +00:00
|
|
|
:param indicator_a: indicator name as populated in strategy
|
|
|
|
:param indicator_b: indicator name as populated in strategy
|
2020-12-19 16:06:21 +00:00
|
|
|
:param label: label for the filled area
|
|
|
|
:param fill_color: color to be used for the filled area
|
|
|
|
:return: fig with added filled_traces plot
|
|
|
|
"""
|
|
|
|
if indicator_a in data and indicator_b in data:
|
2020-12-19 20:37:52 +00:00
|
|
|
# make lines invisible to get the area plotted, only.
|
2020-12-19 16:48:08 +00:00
|
|
|
line = {'color': 'rgba(255,255,255,0)'}
|
2020-12-19 16:06:21 +00:00
|
|
|
# TODO: Figure out why scattergl causes problems plotly/plotly.js#2284
|
|
|
|
trace_a = go.Scatter(x=data.date, y=data[indicator_a],
|
|
|
|
showlegend=False,
|
2020-12-19 16:48:08 +00:00
|
|
|
line=line)
|
2020-12-19 16:06:21 +00:00
|
|
|
trace_b = go.Scatter(x=data.date, y=data[indicator_b], name=label,
|
|
|
|
fill="tonexty", fillcolor=fill_color,
|
2020-12-19 16:48:08 +00:00
|
|
|
line=line)
|
2020-12-19 16:06:21 +00:00
|
|
|
fig.add_trace(trace_a, row, 1)
|
|
|
|
fig.add_trace(trace_b, row, 1)
|
|
|
|
return fig
|
|
|
|
|
|
|
|
|
2020-12-19 16:42:22 +00:00
|
|
|
def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots:
|
2020-12-19 16:48:08 +00:00
|
|
|
""" Adds all area plots (specified in plot_config) to fig.
|
2020-12-19 16:42:22 +00:00
|
|
|
:param fig: Plot figure to append to
|
|
|
|
:param row: row number for this plot
|
|
|
|
:param data: candlestick DataFrame
|
|
|
|
:param indicators: dict with indicators. ie.: plot_config['main_plot'] or
|
|
|
|
plot_config['subplots'][subplot_label]
|
|
|
|
:return: fig with added filled_traces plot
|
|
|
|
"""
|
|
|
|
for indicator, ind_conf in indicators.items():
|
|
|
|
if 'fill_to' in ind_conf:
|
2020-12-19 19:32:13 +00:00
|
|
|
indicator_b = ind_conf['fill_to']
|
|
|
|
if indicator in data and indicator_b in data:
|
2020-12-19 20:37:52 +00:00
|
|
|
label = ind_conf.get('fill_label',
|
|
|
|
f'{indicator}<>{indicator_b}')
|
2020-12-19 19:32:13 +00:00
|
|
|
fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)')
|
2020-12-19 20:37:52 +00:00
|
|
|
fig = plot_area(fig, row, data, indicator, indicator_b,
|
|
|
|
label=label, fill_color=fill_color)
|
2020-12-19 19:32:13 +00:00
|
|
|
elif indicator not in data:
|
|
|
|
logger.info(
|
|
|
|
'Indicator "%s" ignored. Reason: This indicator is not '
|
|
|
|
'found in your strategy.', indicator
|
|
|
|
)
|
|
|
|
elif indicator_b not in data:
|
|
|
|
logger.info(
|
2021-08-06 22:19:36 +00:00
|
|
|
'fill_to: "%s" ignored. Reason: This indicator is not '
|
|
|
|
'in your strategy.', indicator_b
|
2020-12-19 19:32:13 +00:00
|
|
|
)
|
2020-12-19 16:42:22 +00:00
|
|
|
return fig
|
|
|
|
|
|
|
|
|
2020-01-04 10:13:45 +00:00
|
|
|
def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *,
|
2019-06-30 07:47:07 +00:00
|
|
|
indicators1: List[str] = [],
|
2020-01-04 10:13:45 +00:00
|
|
|
indicators2: List[str] = [],
|
|
|
|
plot_config: Dict[str, Dict] = {},
|
|
|
|
) -> go.Figure:
|
2019-05-28 05:00:57 +00:00
|
|
|
"""
|
|
|
|
Generate the graph from the data generated by Backtesting or from DB
|
2019-06-16 18:14:31 +00:00
|
|
|
Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators
|
2019-05-28 05:00:57 +00:00
|
|
|
:param pair: Pair to Display on the graph
|
|
|
|
:param data: OHLCV DataFrame containing indicators and buy/sell signals
|
|
|
|
:param trades: All trades created
|
|
|
|
:param indicators1: List containing Main plot indicators
|
|
|
|
:param indicators2: List containing Sub plot indicators
|
2020-01-04 10:13:45 +00:00
|
|
|
:param plot_config: Dict of Dicts containing advanced plot configuration
|
|
|
|
:return: Plotly figure
|
2019-05-28 05:00:57 +00:00
|
|
|
"""
|
2020-01-04 10:18:51 +00:00
|
|
|
plot_config = create_plotconfig(indicators1, indicators2, plot_config)
|
2020-01-04 10:13:45 +00:00
|
|
|
rows = 2 + len(plot_config['subplots'])
|
|
|
|
row_widths = [1 for _ in plot_config['subplots']]
|
2019-05-28 05:00:57 +00:00
|
|
|
# Define the graph
|
2019-07-22 18:39:38 +00:00
|
|
|
fig = make_subplots(
|
2020-01-04 10:13:45 +00:00
|
|
|
rows=rows,
|
2019-05-28 05:00:57 +00:00
|
|
|
cols=1,
|
|
|
|
shared_xaxes=True,
|
2020-01-04 10:13:45 +00:00
|
|
|
row_width=row_widths + [1, 4],
|
2019-05-28 05:00:57 +00:00
|
|
|
vertical_spacing=0.0001,
|
|
|
|
)
|
|
|
|
fig['layout'].update(title=pair)
|
|
|
|
fig['layout']['yaxis1'].update(title='Price')
|
|
|
|
fig['layout']['yaxis2'].update(title='Volume')
|
2020-01-04 10:13:45 +00:00
|
|
|
for i, name in enumerate(plot_config['subplots']):
|
|
|
|
fig['layout'][f'yaxis{3 + i}'].update(title=name)
|
2019-05-28 05:00:57 +00:00
|
|
|
fig['layout']['xaxis']['rangeslider'].update(visible=False)
|
2021-06-30 18:11:11 +00:00
|
|
|
fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"])
|
2019-05-28 05:00:57 +00:00
|
|
|
|
|
|
|
# Common information
|
|
|
|
candles = go.Candlestick(
|
|
|
|
x=data.date,
|
|
|
|
open=data.open,
|
|
|
|
high=data.high,
|
|
|
|
low=data.low,
|
|
|
|
close=data.close,
|
|
|
|
name='Price'
|
|
|
|
)
|
2019-07-22 18:39:38 +00:00
|
|
|
fig.add_trace(candles, 1, 1)
|
2019-05-28 05:00:57 +00:00
|
|
|
|
2022-02-03 23:34:38 +00:00
|
|
|
if 'enter_long' in data.columns:
|
|
|
|
df_buy = data[data['enter_long'] == 1]
|
|
|
|
if len(df_buy) > 0:
|
|
|
|
buys = go.Scatter(
|
|
|
|
x=df_buy.date,
|
|
|
|
y=df_buy.close,
|
|
|
|
mode='markers',
|
|
|
|
name='buy',
|
|
|
|
marker=dict(
|
|
|
|
symbol='triangle-up-dot',
|
|
|
|
size=9,
|
|
|
|
line=dict(width=1),
|
|
|
|
color='green',
|
|
|
|
)
|
|
|
|
)
|
|
|
|
fig.add_trace(buys, 1, 1)
|
|
|
|
else:
|
|
|
|
logger.warning("No buy-signals found.")
|
|
|
|
|
|
|
|
if 'exit_long' in data.columns:
|
|
|
|
df_sell = data[data['exit_long'] == 1]
|
|
|
|
if len(df_sell) > 0:
|
|
|
|
sells = go.Scatter(
|
|
|
|
x=df_sell.date,
|
|
|
|
y=df_sell.close,
|
|
|
|
mode='markers',
|
|
|
|
name='sell',
|
|
|
|
marker=dict(
|
|
|
|
symbol='triangle-down-dot',
|
|
|
|
size=9,
|
|
|
|
line=dict(width=1),
|
|
|
|
color='red',
|
|
|
|
)
|
|
|
|
)
|
|
|
|
fig.add_trace(sells, 1, 1)
|
|
|
|
else:
|
|
|
|
logger.warning("No sell-signals found.")
|
2020-12-19 16:42:22 +00:00
|
|
|
# Add Bollinger Bands
|
2020-12-19 20:37:52 +00:00
|
|
|
fig = plot_area(fig, 1, data, 'bb_lowerband', 'bb_upperband',
|
|
|
|
label="Bollinger Band")
|
2020-12-19 16:06:21 +00:00
|
|
|
# prevent bb_lower and bb_upper from plotting
|
|
|
|
try:
|
|
|
|
del plot_config['main_plot']['bb_lowerband']
|
|
|
|
del plot_config['main_plot']['bb_upperband']
|
|
|
|
except KeyError:
|
|
|
|
pass
|
2020-12-19 20:37:52 +00:00
|
|
|
# main plot goes to row 1
|
2020-01-04 10:13:45 +00:00
|
|
|
fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data)
|
2020-12-19 16:42:22 +00:00
|
|
|
fig = add_areas(fig, 1, data, plot_config['main_plot'])
|
2019-05-28 05:00:57 +00:00
|
|
|
fig = plot_trades(fig, trades)
|
2020-12-19 20:37:52 +00:00
|
|
|
# sub plot: Volume goes to row 2
|
2019-05-28 05:00:57 +00:00
|
|
|
volume = go.Bar(
|
|
|
|
x=data['date'],
|
|
|
|
y=data['volume'],
|
2019-09-26 03:09:50 +00:00
|
|
|
name='Volume',
|
|
|
|
marker_color='DarkSlateGrey',
|
|
|
|
marker_line_color='DarkSlateGrey'
|
2020-01-03 12:27:22 +00:00
|
|
|
)
|
2019-07-22 18:39:38 +00:00
|
|
|
fig.add_trace(volume, 2, 1)
|
2020-12-19 21:01:33 +00:00
|
|
|
# add each sub plot to a separate row
|
2020-12-19 16:06:21 +00:00
|
|
|
for i, label in enumerate(plot_config['subplots']):
|
|
|
|
sub_config = plot_config['subplots'][label]
|
|
|
|
row = 3 + i
|
|
|
|
fig = add_indicators(fig=fig, row=row, indicators=sub_config,
|
2020-01-04 10:13:45 +00:00
|
|
|
data=data)
|
2020-12-19 16:06:21 +00:00
|
|
|
# fill area between indicators ( 'fill_to': 'other_indicator')
|
2020-12-19 16:42:22 +00:00
|
|
|
fig = add_areas(fig, row, data, sub_config)
|
2021-06-30 18:11:11 +00:00
|
|
|
|
2019-05-28 05:00:57 +00:00
|
|
|
return fig
|
2019-05-31 04:41:55 +00:00
|
|
|
|
|
|
|
|
2020-03-08 10:35:31 +00:00
|
|
|
def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
|
2021-04-25 08:10:09 +00:00
|
|
|
trades: pd.DataFrame, timeframe: str, stake_currency: str) -> go.Figure:
|
2019-06-30 08:31:36 +00:00
|
|
|
# Combine close-values for all pairs, rename columns to "pair"
|
2021-12-30 09:14:45 +00:00
|
|
|
try:
|
|
|
|
df_comb = combine_dataframes_with_mean(data, "close")
|
|
|
|
except ValueError:
|
|
|
|
raise OperationalException(
|
|
|
|
"No data found. Please make sure that data is available for "
|
|
|
|
"the timerange and pairs selected.")
|
2019-06-30 08:31:36 +00:00
|
|
|
|
2020-04-06 13:49:59 +00:00
|
|
|
# Trim trades to available OHLCV data
|
|
|
|
trades = extract_trades_of_period(df_comb, trades, date_index=True)
|
2020-12-30 07:30:41 +00:00
|
|
|
if len(trades) == 0:
|
|
|
|
raise OperationalException('No trades found in selected timerange.')
|
2020-04-06 13:49:59 +00:00
|
|
|
|
2019-06-30 08:31:36 +00:00
|
|
|
# Add combined cumulative profit
|
2019-10-28 13:24:12 +00:00
|
|
|
df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe)
|
2019-06-30 08:31:36 +00:00
|
|
|
|
|
|
|
# Plot the pairs average close prices, and total profit growth
|
2019-10-05 08:32:42 +00:00
|
|
|
avgclose = go.Scatter(
|
2019-06-30 08:31:36 +00:00
|
|
|
x=df_comb.index,
|
|
|
|
y=df_comb['mean'],
|
|
|
|
name='Avg close price',
|
|
|
|
)
|
|
|
|
|
2022-01-01 13:40:20 +00:00
|
|
|
fig = make_subplots(rows=5, cols=1, shared_xaxes=True,
|
|
|
|
row_heights=[1, 1, 1, 0.5, 1],
|
2019-08-24 12:49:35 +00:00
|
|
|
vertical_spacing=0.05,
|
2022-01-01 13:06:15 +00:00
|
|
|
subplot_titles=[
|
|
|
|
"AVG Close Price",
|
|
|
|
"Combined Profit",
|
|
|
|
"Profit per pair",
|
2022-01-01 13:40:20 +00:00
|
|
|
"Parallelism",
|
|
|
|
"Underwater",
|
2022-02-04 02:39:47 +00:00
|
|
|
])
|
2019-08-24 13:21:16 +00:00
|
|
|
fig['layout'].update(title="Freqtrade Profit plot")
|
2019-08-24 12:49:35 +00:00
|
|
|
fig['layout']['yaxis1'].update(title='Price')
|
2021-04-25 08:10:09 +00:00
|
|
|
fig['layout']['yaxis2'].update(title=f'Profit {stake_currency}')
|
|
|
|
fig['layout']['yaxis3'].update(title=f'Profit {stake_currency}')
|
2022-01-01 13:06:15 +00:00
|
|
|
fig['layout']['yaxis4'].update(title='Trade count')
|
2022-01-01 13:40:20 +00:00
|
|
|
fig['layout']['yaxis5'].update(title='Underwater Plot')
|
2019-08-24 12:49:35 +00:00
|
|
|
fig['layout']['xaxis']['rangeslider'].update(visible=False)
|
2021-06-30 18:11:11 +00:00
|
|
|
fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"])
|
2019-06-30 08:31:36 +00:00
|
|
|
|
2019-07-22 18:39:38 +00:00
|
|
|
fig.add_trace(avgclose, 1, 1)
|
2019-06-30 08:31:36 +00:00
|
|
|
fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit')
|
2020-04-05 12:35:53 +00:00
|
|
|
fig = add_max_drawdown(fig, 2, trades, df_comb, timeframe)
|
2022-01-01 13:06:15 +00:00
|
|
|
fig = add_parallelism(fig, 4, trades, timeframe)
|
2022-01-01 13:40:20 +00:00
|
|
|
fig = add_underwater(fig, 5, trades)
|
2019-06-30 08:31:36 +00:00
|
|
|
|
|
|
|
for pair in pairs:
|
|
|
|
profit_col = f'cum_profit_{pair}'
|
2020-05-28 17:35:32 +00:00
|
|
|
try:
|
|
|
|
df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col,
|
|
|
|
timeframe)
|
|
|
|
fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}")
|
|
|
|
except ValueError:
|
|
|
|
pass
|
2019-06-30 08:47:55 +00:00
|
|
|
return fig
|
2019-06-30 08:31:36 +00:00
|
|
|
|
|
|
|
|
2020-02-02 04:00:40 +00:00
|
|
|
def generate_plot_filename(pair: str, timeframe: str) -> str:
|
2019-06-30 07:47:07 +00:00
|
|
|
"""
|
2019-11-02 19:19:13 +00:00
|
|
|
Generate filenames per pair/timeframe to be used for storing plots
|
2019-06-30 07:47:07 +00:00
|
|
|
"""
|
2020-01-04 02:07:51 +00:00
|
|
|
pair_s = pair_to_filename(pair)
|
|
|
|
file_name = 'freqtrade-plot-' + pair_s + '-' + timeframe + '.html'
|
2019-06-29 18:30:31 +00:00
|
|
|
|
|
|
|
logger.info('Generate plot file for %s', pair)
|
|
|
|
|
|
|
|
return file_name
|
|
|
|
|
|
|
|
|
2019-07-31 04:54:45 +00:00
|
|
|
def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False) -> None:
|
2019-05-31 04:41:55 +00:00
|
|
|
"""
|
|
|
|
Generate a plot html file from pre populated fig plotly object
|
|
|
|
:param fig: Plotly Figure to plot
|
2019-11-02 19:19:13 +00:00
|
|
|
:param filename: Name to store the file as
|
|
|
|
:param directory: Directory to store the file in
|
|
|
|
:param auto_open: Automatically open files saved
|
2019-05-31 04:41:55 +00:00
|
|
|
:return: None
|
|
|
|
"""
|
2019-07-31 04:54:45 +00:00
|
|
|
directory.mkdir(parents=True, exist_ok=True)
|
2019-05-31 04:41:55 +00:00
|
|
|
|
2019-08-05 04:55:51 +00:00
|
|
|
_filename = directory.joinpath(filename)
|
2019-08-04 08:25:46 +00:00
|
|
|
plot(fig, filename=str(_filename),
|
2019-06-29 18:30:31 +00:00
|
|
|
auto_open=auto_open)
|
2019-08-04 08:25:46 +00:00
|
|
|
logger.info(f"Stored plot as {_filename}")
|
2019-08-22 14:02:03 +00:00
|
|
|
|
|
|
|
|
2019-09-05 20:00:16 +00:00
|
|
|
def load_and_plot_trades(config: Dict[str, Any]):
|
2019-08-22 14:02:03 +00:00
|
|
|
"""
|
|
|
|
From configuration provided
|
|
|
|
- Initializes plot-script
|
2020-03-08 10:35:31 +00:00
|
|
|
- Get candle (OHLCV) data
|
2019-08-22 14:21:48 +00:00
|
|
|
- Generate Dafaframes populated with indicators and signals based on configured strategy
|
2021-08-16 12:16:24 +00:00
|
|
|
- Load trades executed during the selected period
|
2019-08-22 14:21:48 +00:00
|
|
|
- Generate Plotly plot objects
|
|
|
|
- Generate plot files
|
2019-08-22 14:02:03 +00:00
|
|
|
:return: None
|
|
|
|
"""
|
2019-12-23 09:23:48 +00:00
|
|
|
strategy = StrategyResolver.load_strategy(config)
|
2019-08-22 18:32:06 +00:00
|
|
|
|
2020-07-22 13:15:50 +00:00
|
|
|
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
|
|
|
IStrategy.dp = DataProvider(config, exchange)
|
2021-01-12 00:13:58 +00:00
|
|
|
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
|
2020-11-14 08:28:00 +00:00
|
|
|
timerange = plot_elements['timerange']
|
2019-08-22 14:02:03 +00:00
|
|
|
trades = plot_elements['trades']
|
|
|
|
pair_counter = 0
|
2020-03-08 10:35:31 +00:00
|
|
|
for pair, data in plot_elements["ohlcv"].items():
|
2019-08-22 14:02:03 +00:00
|
|
|
pair_counter += 1
|
|
|
|
logger.info("analyse pair %s", pair)
|
|
|
|
|
2020-03-13 01:00:24 +00:00
|
|
|
df_analyzed = strategy.analyze_ticker(data, {'pair': pair})
|
2020-11-14 08:28:00 +00:00
|
|
|
df_analyzed = trim_dataframe(df_analyzed, timerange)
|
2021-05-22 15:03:16 +00:00
|
|
|
if not trades.empty:
|
|
|
|
trades_pair = trades.loc[trades['pair'] == pair]
|
|
|
|
trades_pair = extract_trades_of_period(df_analyzed, trades_pair)
|
|
|
|
else:
|
|
|
|
trades_pair = trades
|
2019-08-22 14:02:03 +00:00
|
|
|
|
|
|
|
fig = generate_candlestick_graph(
|
|
|
|
pair=pair,
|
2020-03-13 01:00:24 +00:00
|
|
|
data=df_analyzed,
|
2019-08-22 14:02:03 +00:00
|
|
|
trades=trades_pair,
|
2020-08-26 18:52:09 +00:00
|
|
|
indicators1=config.get('indicators1', []),
|
|
|
|
indicators2=config.get('indicators2', []),
|
2020-01-04 10:13:45 +00:00
|
|
|
plot_config=strategy.plot_config if hasattr(strategy, 'plot_config') else {}
|
2019-08-22 14:02:03 +00:00
|
|
|
)
|
|
|
|
|
2020-06-01 18:49:40 +00:00
|
|
|
store_plot_file(fig, filename=generate_plot_filename(pair, config['timeframe']),
|
2020-08-26 18:52:09 +00:00
|
|
|
directory=config['user_data_dir'] / 'plot')
|
2019-08-22 14:02:03 +00:00
|
|
|
|
2019-08-22 14:43:28 +00:00
|
|
|
logger.info('End of plotting process. %s plots generated', pair_counter)
|
2019-08-22 14:51:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
def plot_profit(config: Dict[str, Any]) -> None:
|
|
|
|
"""
|
|
|
|
Plots the total profit for all pairs.
|
|
|
|
Note, the profit calculation isn't realistic.
|
|
|
|
But should be somewhat proportional, and therefor useful
|
|
|
|
in helping out to find a good algorithm.
|
|
|
|
"""
|
2021-06-12 07:03:55 +00:00
|
|
|
if 'timeframe' not in config:
|
|
|
|
raise OperationalException('Timeframe must be set in either config or via --timeframe.')
|
|
|
|
|
2021-01-12 00:13:58 +00:00
|
|
|
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
|
|
|
plot_elements = init_plotscript(config, list(exchange.markets))
|
2019-11-13 19:44:55 +00:00
|
|
|
trades = plot_elements['trades']
|
2019-08-22 14:51:00 +00:00
|
|
|
# Filter trades to relevant pairs
|
2019-11-13 19:45:16 +00:00
|
|
|
# Remove open pairs - we don't know the profit yet so can't calculate profit for these.
|
|
|
|
# Also, If only one open pair is left, then the profit-generation would fail.
|
2020-08-26 18:52:09 +00:00
|
|
|
trades = trades[(trades['pair'].isin(plot_elements['pairs']))
|
2020-06-26 07:19:44 +00:00
|
|
|
& (~trades['close_date'].isnull())
|
2019-11-13 19:45:16 +00:00
|
|
|
]
|
2020-05-21 05:13:08 +00:00
|
|
|
if len(trades) == 0:
|
|
|
|
raise OperationalException("No trades found, cannot generate Profit-plot without "
|
|
|
|
"trades from either Backtest result or database.")
|
2019-11-13 19:45:16 +00:00
|
|
|
|
2019-08-22 14:51:00 +00:00
|
|
|
# Create an average close price of all the pairs that were involved.
|
|
|
|
# this could be useful to gauge the overall market trend
|
2020-08-26 18:52:09 +00:00
|
|
|
fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'],
|
2021-06-12 07:03:55 +00:00
|
|
|
trades, config['timeframe'],
|
2021-04-25 09:01:04 +00:00
|
|
|
config.get('stake_currency', ''))
|
2019-08-22 14:51:00 +00:00
|
|
|
store_plot_file(fig, filename='freqtrade-profit-plot.html',
|
2021-05-30 14:11:24 +00:00
|
|
|
directory=config['user_data_dir'] / 'plot',
|
|
|
|
auto_open=config.get('plot_auto_open', False))
|