Merge pull request #2752 from freqtrade/plotting/indicator_strategy

Allow enhanced plot-dataframe configuration
This commit is contained in:
Matthias 2020-01-13 19:53:15 +01:00 committed by GitHub
commit d12a2a5888
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 278 additions and 52 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

View File

@ -120,16 +120,77 @@ To plot trades from a backtesting result, use `--export-filename <filename>`
freqtrade plot-dataframe --strategy AwesomeStrategy --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH
```
### Plot dataframe basics
![plot-dataframe2](assets/plot-dataframe2.png)
The `plot-dataframe` subcommand requires backtesting data, a strategy and either a backtesting-results file or a database, containing trades corresponding to the strategy.
The resulting plot will have the following elements:
* Green triangles: Buy signals from the strategy. (Note: not every buy signal generates a trade, compare to cyan circles.)
* Red triangles: Sell signals from the strategy. (Also, not every sell signal terminates a trade, compare to red and green squares.)
* Cyan circles: Trade entry points.
* Red squares: Trade exit points for trades with loss or 0% profit.
* Green squares: Trade exit points for profitable trades.
* Indicators with values corresponding to the candle scale (e.g. SMA/EMA), as specified with `--indicators1`.
* Volume (bar chart at the bottom of the main chart).
* Indicators with values in different scales (e.g. MACD, RSI) below the volume bars, as specified with `--indicators2`.
!!! Note "Bollinger Bands"
Bollinger bands are automatically added to the plot if the columns `bb_lowerband` and `bb_upperband` exist, and are painted as a light blue area spanning from the lower band to the upper band.
#### Advanced plot configuration
An advanced plot configuration can be specified in the strategy in the `plot_config` parameter.
Additional features when using plot_config include:
* Specify colors per indicator
* Specify additional subplots
The sample plot configuration below specifies fixed colors for the indicators. Otherwise consecutive plots may produce different colorschemes each time, making comparisons difficult.
It also allows multiple subplots to display both MACD and RSI at the same time.
Sample configuration with inline comments explaining the process:
``` python
plot_config = {
'main_plot': {
# Configuration for main plot indicators.
# Specifies `ema10` to be red, and `ema50` to be a shade of gray
'ema10': {'color': 'red'},
'ema50': {'color': '#CCCCCC'},
# By omitting color, a random color is selected.
'sar': {},
},
'subplots': {
# Create subplot MACD
"MACD": {
'macd': {'color': 'blue'},
'macdsignal': {'color': 'orange'},
},
# Additional subplot RSI
"RSI": {
'rsi': {'color': 'red'},
}
}
}
```
!!! Note
The above configuration assumes that `ema10`, `ema50`, `macd`, `macdsignal` and `rsi` are columns in the DataFrame created by the strategy.
## Plot profit
![plot-profit](assets/plot-profit.png)
The `freqtrade plot-profit` subcommand shows an interactive graph with three plots:
The `plot-profit` subcommand shows an interactive graph with three plots:
1) Average closing price for all pairs
2) The summarized profit made by backtesting.
Note that this is not the real-world profit, but more of an estimate.
3) Profit for each individual pair
* Average closing price for all pairs.
* The summarized profit made by backtesting.
Note that this is not the real-world profit, but more of an estimate.
* Profit for each individual pair.
The first graph is good to get a grip of how the overall market progresses.

View File

@ -363,15 +363,13 @@ AVAILABLE_CLI_OPTIONS = {
"indicators1": Arg(
'--indicators1',
help='Set indicators from your strategy you want in the first row of the graph. '
'Space-separated list. Example: `ema3 ema5`. Default: `%(default)s`.',
default=['sma', 'ema3', 'ema5'],
"Space-separated list. Example: `ema3 ema5`. Default: `['sma', 'ema3', 'ema5']`.",
nargs='+',
),
"indicators2": Arg(
'--indicators2',
help='Set indicators from your strategy you want in the third row of the graph. '
'Space-separated list. Example: `fastd fastk`. Default: `%(default)s`.',
default=['macd', 'macdsignal'],
"Space-separated list. Example: `fastd fastk`. Default: `['macd', 'macdsignal']`.",
nargs='+',
),
"plot_limit": Arg(

View File

@ -54,21 +54,27 @@ def init_plotscript(config):
}
def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> make_subplots:
def add_indicators(fig, row, indicators: Dict[str, Dict], data: pd.DataFrame) -> make_subplots:
"""
Generator all the indicator selected by the user for a specific row
Generate all the indicators selected by the user for a specific row, based on the configuration
:param fig: Plot figure to append to
:param row: row number for this plot
:param indicators: List of indicators present in the dataframe
:param indicators: Dict of Indicators with configuration options.
Dict key must correspond to dataframe column.
:param data: candlestick DataFrame
"""
for indicator in indicators:
for indicator, conf in indicators.items():
logger.debug(f"indicator {indicator} with config {conf}")
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']}})
scatter = go.Scatter(
x=data['date'],
y=data[indicator].values,
mode='lines',
name=indicator
**kwargs
)
fig.add_trace(scatter, row, 1)
else:
@ -107,11 +113,31 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
"""
# Trades can be empty
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)
trade_buys = go.Scatter(
x=trades["open_time"],
y=trades["open_rate"],
mode='markers',
name='trade_buy',
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',
marker=dict(
symbol='square-open',
size=11,
@ -119,16 +145,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
color='green'
)
)
# Create description for sell summarizing the trade
desc = trades.apply(lambda row: f"{round(row['profitperc'] * 100, 1)}%, "
f"{row['sell_reason']}, {row['duration']} min",
axis=1)
trade_sells = go.Scatter(
x=trades["close_time"],
y=trades["close_rate"],
text=desc,
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"],
mode='markers',
name='trade_sell',
name='Sell - Loss',
marker=dict(
symbol='square-open',
size=11,
@ -138,14 +160,53 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
)
fig.add_trace(trade_buys, 1, 1)
fig.add_trace(trade_sells, 1, 1)
fig.add_trace(trade_sells_loss, 1, 1)
else:
logger.warning("No trades found.")
return fig
def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None,
def create_plotconfig(indicators1: List[str], indicators2: List[str],
plot_config: Dict[str, Dict]) -> Dict[str, Dict]:
"""
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}}
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']
# 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
def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *,
indicators1: List[str] = [],
indicators2: List[str] = [],) -> go.Figure:
indicators2: List[str] = [],
plot_config: Dict[str, Dict] = {},
) -> go.Figure:
"""
Generate the graph from the data generated by Backtesting or from DB
Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators
@ -154,21 +215,26 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
:param trades: All trades created
:param indicators1: List containing Main plot indicators
:param indicators2: List containing Sub plot indicators
:return: None
:param plot_config: Dict of Dicts containing advanced plot configuration
:return: Plotly figure
"""
plot_config = create_plotconfig(indicators1, indicators2, plot_config)
rows = 2 + len(plot_config['subplots'])
row_widths = [1 for _ in plot_config['subplots']]
# Define the graph
fig = make_subplots(
rows=3,
rows=rows,
cols=1,
shared_xaxes=True,
row_width=[1, 1, 4],
row_width=row_widths + [1, 4],
vertical_spacing=0.0001,
)
fig['layout'].update(title=pair)
fig['layout']['yaxis1'].update(title='Price')
fig['layout']['yaxis2'].update(title='Volume')
fig['layout']['yaxis3'].update(title='Other')
for i, name in enumerate(plot_config['subplots']):
fig['layout'][f'yaxis{3 + i}'].update(title=name)
fig['layout']['xaxis']['rangeslider'].update(visible=False)
# Common information
@ -238,12 +304,13 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
)
fig.add_trace(bb_lower, 1, 1)
fig.add_trace(bb_upper, 1, 1)
if 'bb_upperband' in indicators1 and 'bb_lowerband' in indicators1:
indicators1.remove('bb_upperband')
indicators1.remove('bb_lowerband')
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']
# Add indicators to main plot
fig = add_indicators(fig=fig, row=1, indicators=indicators1, data=data)
fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data)
fig = plot_trades(fig, trades)
@ -258,7 +325,10 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
fig.add_trace(volume, 2, 1)
# Add indicators to separate row
fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data)
for i, name in enumerate(plot_config['subplots']):
fig = add_indicators(fig=fig, row=3 + i,
indicators=plot_config['subplots'][name],
data=data)
return fig
@ -359,8 +429,9 @@ def load_and_plot_trades(config: Dict[str, Any]):
pair=pair,
data=dataframe,
trades=trades_pair,
indicators1=config["indicators1"],
indicators2=config["indicators2"],
indicators1=config.get("indicators1", []),
indicators2=config.get("indicators2", []),
plot_config=strategy.plot_config if hasattr(strategy, 'plot_config') else {}
)
store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']),

View File

@ -112,6 +112,9 @@ class IStrategy(ABC):
dp: Optional[DataProvider] = None
wallets: Optional[Wallets] = None
# Definition of plot_config. See plotting documentation for more details.
plot_config: Dict = {}
def __init__(self, config: dict) -> None:
self.config = config
# Dict to determine if analysis is necessary

View File

@ -78,7 +78,7 @@ class {{ strategy }}(IStrategy):
'buy': 'gtc',
'sell': 'gtc'
}
{{ plot_config | indent(4) }}
def informative_pairs(self):
"""
Define additional, informative pair/interval combinations to be cached from the exchange.

View File

@ -80,6 +80,22 @@ class SampleStrategy(IStrategy):
'sell': 'gtc'
}
plot_config = {
'main_plot': {
'tema': {},
'sar': {'color': 'white'},
},
'subplots': {
"MACD": {
'macd': {'color': 'blue'},
'macdsignal': {'color': 'orange'},
},
"RSI": {
'rsi': {'color': 'red'},
}
}
}
def informative_pairs(self):
"""
Define additional, informative pair/interval combinations to be cached from the exchange.

View File

@ -0,0 +1,18 @@
plot_config = {
# Main plot indicators (Moving averages, ...)
'main_plot': {
'tema': {},
'sar': {'color': 'white'},
},
'subplots': {
# Subplots - each dict defines one additional plot
"MACD": {
'macd': {'color': 'blue'},
'macdsignal': {'color': 'orange'},
},
"RSI": {
'rsi': {'color': 'red'},
}
}
}

View File

@ -102,12 +102,14 @@ def deploy_new_strategy(strategy_name, strategy_path: Path, subtemplate: str):
indicators = render_template(templatefile=f"subtemplates/indicators_{subtemplate}.j2",)
buy_trend = render_template(templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",)
sell_trend = render_template(templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",)
plot_config = render_template(templatefile=f"subtemplates/plot_config_{subtemplate}.j2",)
strategy_text = render_template(templatefile='base_strategy.py.j2',
arguments={"strategy": strategy_name,
"indicators": indicators,
"buy_trend": buy_trend,
"sell_trend": sell_trend,
"plot_config": plot_config,
})
logger.info(f"Writing strategy to `{strategy_path}`.")

View File

@ -13,6 +13,7 @@ from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
from freqtrade.exceptions import OperationalException
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
from freqtrade.plot.plotting import (add_indicators, add_profit,
create_plotconfig,
generate_candlestick_graph,
generate_plot_filename,
generate_profit_graph, init_plotscript,
@ -66,8 +67,8 @@ def test_add_indicators(default_conf, testdatadir, caplog):
data = history.load_pair_history(pair=pair, timeframe='1m',
datadir=testdatadir, timerange=timerange)
indicators1 = ["ema10"]
indicators2 = ["macd"]
indicators1 = {"ema10": {}}
indicators2 = {"macd": {"color": "red"}}
# Generate buy/sell signals and indicators
strat = DefaultStrategy(default_conf)
@ -86,9 +87,10 @@ def test_add_indicators(default_conf, testdatadir, caplog):
macd = find_trace_in_fig_data(figure.data, "macd")
assert isinstance(macd, go.Scatter)
assert macd.yaxis == "y3"
assert macd.line.color == "red"
# No indicator found
fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data)
fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators={'no_indicator': {}}, data=data)
assert fig == fig3
assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog)
@ -108,18 +110,29 @@ def test_plot_trades(testdatadir, caplog):
figure = fig1.layout.figure
# Check buys - color, should be in first graph, ...
trade_buy = find_trace_in_fig_data(figure.data, "trade_buy")
trade_buy = find_trace_in_fig_data(figure.data, 'Trade buy')
assert isinstance(trade_buy, go.Scatter)
assert trade_buy.yaxis == 'y'
assert len(trades) == len(trade_buy.x)
assert trade_buy.marker.color == 'green'
assert trade_buy.marker.color == 'cyan'
assert trade_buy.marker.symbol == 'circle-open'
assert trade_buy.text[0] == '4.0%, roi, 15 min'
trade_sell = find_trace_in_fig_data(figure.data, "trade_sell")
trade_sell = find_trace_in_fig_data(figure.data, 'Sell - Profit')
assert isinstance(trade_sell, go.Scatter)
assert trade_sell.yaxis == 'y'
assert len(trades) == len(trade_sell.x)
assert trade_sell.marker.color == 'red'
assert trade_sell.text[0] == "4.0%, roi, 15 min"
assert len(trades.loc[trades['profitperc'] > 0]) == len(trade_sell.x)
assert trade_sell.marker.color == 'green'
assert trade_sell.marker.symbol == 'square-open'
assert trade_sell.text[0] == '4.0%, roi, 15 min'
trade_sell_loss = find_trace_in_fig_data(figure.data, 'Sell - Loss')
assert isinstance(trade_sell_loss, go.Scatter)
assert trade_sell_loss.yaxis == 'y'
assert len(trades.loc[trades['profitperc'] <= 0]) == len(trade_sell_loss.x)
assert trade_sell_loss.marker.color == 'red'
assert trade_sell_loss.marker.symbol == 'square-open'
assert trade_sell_loss.text[5] == '-10.4%, stop_loss, 720 min'
def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, testdatadir, caplog):
@ -371,3 +384,47 @@ def test_plot_profit(default_conf, mocker, testdatadir, caplog):
assert profit_mock.call_args_list[0][0][0] == default_conf['pairs']
assert store_mock.call_args_list[0][1]['auto_open'] is True
@pytest.mark.parametrize("ind1,ind2,plot_conf,exp", [
# No indicators, use plot_conf
([], [], {},
{'main_plot': {'sma': {}, 'ema3': {}, 'ema5': {}},
'subplots': {'Other': {'macd': {}, 'macdsignal': {}}}}),
# use indicators
(['sma', 'ema3'], ['macd'], {},
{'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'Other': {'macd': {}}}}),
# only main_plot - adds empty subplots
([], [], {'main_plot': {'sma': {}}},
{'main_plot': {'sma': {}}, 'subplots': {}}),
# Main and subplots
([], [], {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
{'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}),
# no main_plot, adds empty main_plot
([], [], {'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
{'main_plot': {}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}),
# indicator 1 / 2 should have prevelance
(['sma', 'ema3'], ['macd'],
{'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
{'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'Other': {'macd': {}}}}
),
# indicator 1 - overrides plot_config main_plot
(['sma', 'ema3'], [],
{'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
{'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}
),
# indicator 2 - overrides plot_config subplots
([], ['macd', 'macd_signal'],
{'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
{'main_plot': {'sma': {}}, 'subplots': {'Other': {'macd': {}, 'macd_signal': {}}}}
),
])
def test_create_plotconfig(ind1, ind2, plot_conf, exp):
res = create_plotconfig(ind1, ind2, plot_conf)
assert 'main_plot' in res
assert 'subplots' in res
assert isinstance(res['main_plot'], dict)
assert isinstance(res['subplots'], dict)
assert res == exp