stable/freqtrade/plot/plotting.py

521 lines
18 KiB
Python
Raw Normal View History

2018-06-23 12:18:30 +00:00
import logging
from pathlib import Path
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
from freqtrade.configuration import TimeRange
2020-03-03 06:21:14 +00:00
from freqtrade.data.btanalysis import (calculate_max_drawdown,
combine_dataframes_with_mean,
create_cum_profit,
extract_trades_of_period, load_trades)
2020-01-04 02:07:51 +00:00
from freqtrade.data.converter import trim_dataframe
from freqtrade.data.history import load_data
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_prev_date
2020-01-04 02:07:51 +00:00
from freqtrade.misc import pair_to_filename
from freqtrade.resolvers import StrategyResolver
2018-06-23 12:18:30 +00:00
logger = logging.getLogger(__name__)
try:
from plotly.subplots import make_subplots
2018-06-23 12:18:30 +00:00
from plotly.offline import plot
import plotly.graph_objects as go
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
def init_plotscript(config):
"""
Initialize objects needed for plotting
:return: Dict with candle (OHLCV) data, trades and pairs
"""
if "pairs" in config:
2019-08-16 12:37:10 +00:00
pairs = config["pairs"]
else:
pairs = config["exchange"]["pair_whitelist"]
# Set timerange to use
timerange = TimeRange.parse_timerange(config.get("timerange"))
data = load_data(
2019-12-23 14:13:55 +00:00
datadir=config.get("datadir"),
pairs=pairs,
timeframe=config.get('ticker_interval', '5m'),
timerange=timerange,
2019-12-28 13:57:39 +00:00
data_format=config.get('dataformat_ohlcv', 'json'),
)
2020-03-15 20:20:32 +00:00
no_trades = False
if config.get('no_trades', False):
no_trades = True
elif not config['exportfilename'].is_file() and config['trade_source'] == 'file':
2020-03-14 22:55:13 +00:00
logger.warning("Backtest file is missing skipping trades.")
2020-03-15 20:20:32 +00:00
no_trades = True
2020-03-14 21:15:03 +00:00
trades = load_trades(
config['trade_source'],
db_url=config.get('db_url'),
exportfilename=config.get('exportfilename'),
2020-03-15 20:20:32 +00:00
no_trades=no_trades
2020-03-14 21:15:03 +00:00
)
trades = trim_dataframe(trades, timerange, 'open_time')
2020-03-14 21:15:03 +00:00
return {"ohlcv": data,
"trades": trades,
"pairs": pairs,
}
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
"""
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:
kwargs = {'x': data['date'],
'y': data[indicator].values,
'mode': 'lines',
'name': indicator
}
if 'color' in conf:
kwargs.update({'line': {'color': conf['color']}})
2019-09-24 00:00:07 +00:00
scatter = go.Scatter(
**kwargs
2019-05-28 05:00:57 +00:00
)
2019-09-24 00:00:07 +00:00
fig.add_trace(scatter, 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
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,
)
fig.add_trace(profit, row, 1)
2019-06-30 08:14:33 +00:00
return fig
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:
max_drawdown, highdate, lowdate = calculate_max_drawdown(trades)
drawdown = go.Scatter(
x=[highdate, lowdate],
y=[
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',
2020-03-30 18:08:07 +00:00
name=f"Max drawdown {max_drawdown * 100:.2f}%",
text=f"Max drawdown {max_drawdown * 100:.2f}%",
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
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:
# Create description for sell summarizing the trade
trades['desc'] = trades.apply(lambda row: f"{round(row['profitperc'] * 100, 1)}%, "
f"{row['sell_reason']}, {row['duration']} min",
axis=1)
2019-05-28 05:00:57 +00:00
trade_buys = go.Scatter(
x=trades["open_time"],
y=trades["open_rate"],
mode='markers',
name='Trade buy',
text=trades["desc"],
marker=dict(
symbol='circle-open',
size=11,
line=dict(width=2),
color='cyan'
)
)
trade_sells = go.Scatter(
x=trades.loc[trades['profitperc'] > 0, "close_time"],
y=trades.loc[trades['profitperc'] > 0, "close_rate"],
text=trades.loc[trades['profitperc'] > 0, "desc"],
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'
)
)
trade_sells_loss = go.Scatter(
x=trades.loc[trades['profitperc'] <= 0, "close_time"],
y=trades.loc[trades['profitperc'] <= 0, "close_rate"],
text=trades.loc[trades['profitperc'] <= 0, "desc"],
2019-05-28 05:00:57 +00:00
mode='markers',
name='Sell - Loss',
2019-05-28 05:00:57 +00:00
marker=dict(
symbol='square-open',
size=11,
line=dict(width=2),
color='red'
)
)
fig.add_trace(trade_buys, 1, 1)
fig.add_trace(trade_sells, 1, 1)
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
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
"""
if plot_config:
if indicators1:
plot_config['main_plot'] = {ind: {} for ind in indicators1}
if indicators2:
plot_config['subplots'] = {'Other': {ind: {} for ind in indicators2}}
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:
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}},
}
if 'main_plot' not in plot_config:
plot_config['main_plot'] = {}
if 'subplots' not in plot_config:
plot_config['subplots'] = {}
return plot_config
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
"""
plot_config = create_plotconfig(indicators1, indicators2, plot_config)
2019-05-28 05:00:57 +00:00
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
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)
# Common information
candles = go.Candlestick(
x=data.date,
open=data.open,
high=data.high,
low=data.low,
close=data.close,
name='Price'
)
fig.add_trace(candles, 1, 1)
2019-05-28 05:00:57 +00:00
if 'buy' in data.columns:
df_buy = data[data['buy'] == 1]
2019-05-28 18:23:16 +00:00
if len(df_buy) > 0:
2019-05-29 05:19:21 +00:00
buys = go.Scatter(
2019-05-28 18:23:16 +00:00
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',
)
2019-05-28 05:00:57 +00:00
)
fig.add_trace(buys, 1, 1)
2019-05-28 18:23:16 +00:00
else:
logger.warning("No buy-signals found.")
2019-05-28 05:00:57 +00:00
if 'sell' in data.columns:
df_sell = data[data['sell'] == 1]
2019-05-28 18:23:16 +00:00
if len(df_sell) > 0:
2019-05-29 05:19:21 +00:00
sells = go.Scatter(
2019-05-28 18:23:16 +00:00
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',
)
2019-05-28 05:00:57 +00:00
)
fig.add_trace(sells, 1, 1)
2019-05-28 18:23:16 +00:00
else:
logger.warning("No sell-signals found.")
2019-05-28 05:00:57 +00:00
2019-09-26 03:09:50 +00:00
# TODO: Figure out why scattergl causes problems plotly/plotly.js#2284
2019-05-28 05:00:57 +00:00
if 'bb_lowerband' in data and 'bb_upperband' in data:
2019-09-24 00:00:07 +00:00
bb_lower = go.Scatter(
2019-05-28 05:00:57 +00:00
x=data.date,
y=data.bb_lowerband,
2019-09-24 00:00:07 +00:00
showlegend=False,
2019-05-28 05:00:57 +00:00
line={'color': 'rgba(255,255,255,0)'},
)
2019-09-24 00:00:07 +00:00
bb_upper = go.Scatter(
2019-05-28 05:00:57 +00:00
x=data.date,
y=data.bb_upperband,
2019-09-24 00:00:07 +00:00
name='Bollinger Band',
2019-05-28 05:00:57 +00:00
fill="tonexty",
fillcolor="rgba(0,176,246,0.2)",
line={'color': 'rgba(255,255,255,0)'},
)
fig.add_trace(bb_lower, 1, 1)
fig.add_trace(bb_upper, 1, 1)
2020-01-04 10:13:45 +00:00
if ('bb_upperband' in plot_config['main_plot']
and 'bb_lowerband' in plot_config['main_plot']):
del plot_config['main_plot']['bb_upperband']
del plot_config['main_plot']['bb_lowerband']
2019-10-05 08:32:42 +00:00
2019-05-28 05:00:57 +00:00
# Add indicators to main plot
2020-01-04 10:13:45 +00:00
fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data)
2019-05-28 05:00:57 +00:00
fig = plot_trades(fig, trades)
# Volume goes to row 2
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
)
fig.add_trace(volume, 2, 1)
2019-05-28 05:00:57 +00:00
2019-09-24 00:00:07 +00:00
# Add indicators to separate row
2020-01-04 10:13:45 +00:00
for i, name in enumerate(plot_config['subplots']):
fig = add_indicators(fig=fig, row=3 + i,
indicators=plot_config['subplots'][name],
data=data)
2019-05-28 05:00:57 +00:00
return fig
2019-05-31 04:41:55 +00:00
def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
trades: pd.DataFrame, timeframe: str) -> go.Figure:
2019-06-30 08:31:36 +00:00
# Combine close-values for all pairs, rename columns to "pair"
df_comb = combine_dataframes_with_mean(data, "close")
2019-06-30 08:31:36 +00:00
# Trim trades to available OHLCV data
trades = extract_trades_of_period(df_comb, trades, date_index=True)
2019-06-30 08:31:36 +00:00
# Add combined cumulative profit
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',
)
2019-08-24 12:49:35 +00:00
fig = make_subplots(rows=3, cols=1, shared_xaxes=True,
row_width=[1, 1, 1],
vertical_spacing=0.05,
subplot_titles=["AVG Close Price", "Combined Profit", "Profit per pair"])
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')
fig['layout']['yaxis2'].update(title='Profit')
fig['layout']['yaxis3'].update(title='Profit')
fig['layout']['xaxis']['rangeslider'].update(visible=False)
2019-06-30 08:31:36 +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')
fig = add_max_drawdown(fig, 2, trades, df_comb, timeframe)
2019-06-30 08:31:36 +00:00
for pair in pairs:
profit_col = f'cum_profit_{pair}'
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:31:36 +00:00
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
"""
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'
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
: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
_filename = directory.joinpath(filename)
plot(fig, filename=str(_filename),
auto_open=auto_open)
logger.info(f"Stored plot as {_filename}")
2019-09-05 20:00:16 +00:00
def load_and_plot_trades(config: Dict[str, Any]):
"""
From configuration provided
- Initializes plot-script
- 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
- Generate plot files
:return: None
"""
strategy = StrategyResolver.load_strategy(config)
plot_elements = init_plotscript(config)
trades = plot_elements['trades']
pair_counter = 0
for pair, data in plot_elements["ohlcv"].items():
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})
trades_pair = trades.loc[trades['pair'] == pair]
2020-03-13 01:00:24 +00:00
trades_pair = extract_trades_of_period(df_analyzed, trades_pair)
fig = generate_candlestick_graph(
pair=pair,
2020-03-13 01:00:24 +00:00
data=df_analyzed,
trades=trades_pair,
2020-01-07 06:16:31 +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 {}
)
store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']),
directory=config['user_data_dir'] / "plot")
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.
"""
plot_elements = init_plotscript(config)
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
# 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.
trades = trades[(trades['pair'].isin(plot_elements["pairs"]))
& (~trades['close_time'].isnull())
]
if len(trades) == 0:
raise OperationalException("No trades found, cannot generate Profit-plot without "
"trades from either Backtest result or database.")
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
fig = generate_profit_graph(plot_elements["pairs"], plot_elements["ohlcv"],
trades, config.get('ticker_interval', '5m'))
2019-08-22 14:51:00 +00:00
store_plot_file(fig, filename='freqtrade-profit-plot.html',
directory=config['user_data_dir'] / "plot", auto_open=True)