import logging from pathlib import Path from typing import Any, Dict, List import pandas as pd from freqtrade import OperationalException from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import (combine_tickers_with_mean, create_cum_profit, extract_trades_of_period, load_trades) from freqtrade.resolvers import StrategyResolver logger = logging.getLogger(__name__) try: from plotly.subplots import make_subplots from plotly.offline import plot import plotly.graph_objects as go except ImportError: logger.exception("Module plotly not found \n Please install using `pip3 install plotly`") exit(1) def init_plotscript(config): """ Initialize objects needed for plotting :return: Dict with tickers, trades and pairs """ if "pairs" in config: pairs = config["pairs"] else: pairs = config["exchange"]["pair_whitelist"] if pairs is None: raise OperationalException('No pairs available in config.') # Set timerange to use timerange = TimeRange.parse_timerange(config.get("timerange")) if timerange is None: raise OperationalException('Could not parse timerange in config.') tickers = history.load_data( datadir=Path(str(config.get("datadir"))), pairs=pairs, ticker_interval=config.get('ticker_interval', '5m'), timerange=timerange, ) if tickers is None: raise OperationalException('No ticker data available as specified in config.') trades = load_trades(config['trade_source'], db_url=config.get('db_url'), exportfilename=config.get('exportfilename'), ) if trades is None: raise OperationalException('No trades available as specified in config.') return {"tickers": tickers, "trades": trades, "pairs": pairs, } def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> make_subplots: """ Generator all the indicator selected by the user for a specific row :param fig: Plot figure to append to :param row: row number for this plot :param indicators: List of indicators present in the dataframe :param data: candlestick DataFrame """ for indicator in indicators: if indicator in data: # TODO: Figure out why scattergl causes problems scattergl = go.Scatter( x=data['date'], y=data[indicator].values, mode='lines', name=indicator ) fig.add_trace(scattergl, row, 1) 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: """ 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 """ profit = go.Scattergl( x=data.index, y=data[column], name=name, ) fig.add_trace(profit, row, 1) return fig def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: """ Add trades to "fig" """ # Trades can be empty if trades is not None and len(trades) > 0: trade_buys = go.Scatter( x=trades["open_time"], y=trades["open_rate"], mode='markers', name='trade_buy', marker=dict( symbol='square-open', size=11, line=dict(width=2), color='green' ) ) # Create description for sell summarizing the trade desc = trades.apply(lambda row: f"{round(row['profitperc'], 3)}%, {row['sell_reason']}, " f"{row['duration']}min", axis=1) trade_sells = go.Scatter( x=trades["close_time"], y=trades["close_rate"], text=desc, mode='markers', name='trade_sell', 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) else: logger.warning("No trades found.") return fig def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, indicators1: List[str] = [], indicators2: List[str] = [],) -> 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 :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 :return: None """ # Define the graph fig = make_subplots( rows=3, cols=1, shared_xaxes=True, row_width=[1, 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') 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) if 'buy' in data.columns: df_buy = data[data['buy'] == 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 'sell' in data.columns: df_sell = data[data['sell'] == 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.") if 'bb_lowerband' in data and 'bb_upperband' in data: bb_lower = go.Scattergl( x=data.date, y=data.bb_lowerband, name='BB lower', line={'color': 'rgba(255,255,255,0)'}, ) bb_upper = go.Scattergl( x=data.date, y=data.bb_upperband, name='BB upper', 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) # Add indicators to main plot fig = add_indicators(fig=fig, row=1, indicators=indicators1, data=data) fig = plot_trades(fig, trades) # Volume goes to row 2 volume = go.Bar( x=data['date'], y=data['volume'], name='Volume' ) fig.add_trace(volume, 2, 1) # Add indicators to seperate row fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data) return fig def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], trades: pd.DataFrame) -> go.Figure: # Combine close-values for all pairs, rename columns to "pair" df_comb = combine_tickers_with_mean(tickers, "close") # Add combined cumulative profit df_comb = create_cum_profit(df_comb, trades, 'cum_profit') # Plot the pairs average close prices, and total profit growth avgclose = go.Scattergl( x=df_comb.index, y=df_comb['mean'], name='Avg close price', ) 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"]) fig['layout'].update(title="Freqtrade Profit plot") 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) fig.add_trace(avgclose, 1, 1) fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') for pair in pairs: profit_col = f'cum_profit_{pair}' df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") return fig def generate_plot_filename(pair, ticker_interval) -> str: """ Generate filenames per pair/ticker_interval to be used for storing plots """ pair_name = pair.replace("/", "_") file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' logger.info('Generate plot file for %s', pair) return file_name def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False) -> None: """ Generate a plot html file from pre populated fig plotly object :param fig: Plotly Figure to plot :param pair: Pair to plot (used as filename and Plot title) :param ticker_interval: Used as part of the filename :return: None """ directory.mkdir(parents=True, exist_ok=True) _filename = directory.joinpath(filename) plot(fig, filename=str(_filename), auto_open=auto_open) logger.info(f"Stored plot as {_filename}") def load_and_plot_trades(config: Dict[str, Any]): """ From configuration provided - Initializes plot-script - Get tickers 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(config).strategy plot_elements = init_plotscript(config) trades = plot_elements['trades'] # if trades is None: # # raise ValueError('No trades to analyze') # else: pair_counter = 0 for pair, data in plot_elements["tickers"].items(): pair_counter += 1 logger.info("analyse pair %s", pair) tickers = {} tickers[pair] = data dataframe = strategy.analyze_ticker(tickers[pair], {'pair': pair}) logger.debug(f'pair: {pair}') trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(dataframe, trades_pair) fig = generate_candlestick_graph( pair=pair, data=dataframe, trades=trades_pair, indicators1=config["indicators1"], indicators2=config["indicators2"], ) store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']), directory=config['user_data_dir'] / "plot") logger.info('End of plotting process. %s plots generated', pair_counter) 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) trades = load_trades(config['trade_source'], db_url=str(config.get('db_url')), exportfilename=str(config.get('exportfilename')), ) # Filter trades to relevant pairs trades = trades[trades['pair'].isin(plot_elements["pairs"])] # 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"], trades) store_plot_file(fig, filename='freqtrade-profit-plot.html', directory=config['user_data_dir'] / "plot", auto_open=True)