This commit is contained in:
fxxmr 2021-11-28 15:35:13 +05:30 committed by GitHub
commit ca68984044
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 276 additions and 15 deletions

View File

@ -211,6 +211,12 @@ Sample configuration with inline comments explaining the process:
"RSI": { "RSI": {
'rsi': {'color': 'red'} 'rsi': {'color': 'red'}
} }
},
'volume': {
'showBuySell': 'true',
'showVolumeProfile': 'true',
# 'VolumeProfileHistoryBars': 96, # default: all
# 'VolumeProfilePriceRangeSplices': 100 # default: 50
} }
} }

View File

@ -18,6 +18,13 @@ from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy import IStrategy from freqtrade.strategy import IStrategy
# Possible later improvement:
# just show the volume profile for the zoomed-in area:
# currently the volume profile is always based on all data in the plot
# check if it is possible to bind the y-axis of the volume profile to the x-axis
# of the candels and group by price-read and sum the (buy/sell)-volume together
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -255,7 +262,14 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str],
:return: plot_config - eventually with indicators 1 and 2 :return: plot_config - eventually with indicators 1 and 2
""" """
if plot_config: if plot_config and 'main_plot' not in plot_config and 'subplots' not in plot_config:
# if just the volumeProfile config is set but main_plot and subplots are empty
indicators1 = ['sma', 'ema3', 'ema5']
plot_config['main_plot'] = {ind: {} for ind in indicators1}
indicators2 = ['macd', 'macdsignal']
plot_config['subplots'] = {'Other': {ind: {} for ind in indicators2}}
elif plot_config:
if indicators1: if indicators1:
plot_config['main_plot'] = {ind: {} for ind in indicators1} plot_config['main_plot'] = {ind: {} for ind in indicators1}
if indicators2: if indicators2:
@ -340,6 +354,115 @@ def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots:
return fig return fig
def generateBuySellVolumes(dataframe) -> pd.DataFrame:
candles = dataframe.copy()
candles['volume_buy'] = 0
candles['volume_sell'] = 0
for i in range(len(candles)):
candles['volume_buy'].iat[i] = (candles['volume'].iat[i] *
(candles['close'].iat[i]-candles['low'].iat[i]) /
(candles['high'].iat[i]-candles['low'].iat[i])) \
if (candles['high'].iat[i]-candles['low'].iat[i]) > 0 else 0
candles['volume_sell'].iat[i] = (candles['volume'].iat[i] *
(candles['high'].iat[i]-candles['close'].iat[i]) /
(candles['high'].iat[i]-candles['low'].iat[i])) \
if (candles['high'].iat[i]-candles['low'].iat[i]) > 0 else 0
return candles
def createVolumeProfileData(data: pd.DataFrame, bar_split: int = 50,
history_bars: int = None) -> pd.DataFrame:
"""
Generate the Volume Profile Date for the given dataframe
:param df: DataFrame with candle data
:param bar_split: in how many bars should the PriceRange be spit (default=50)
:param history_bars: how many history bars should be considered (default=all)
:return: DataFrame with Price/Trade Date
"""
df = data.copy()
df["volume_buy"] = generateBuySellVolumes(df)["volume_buy"]
df["volume_sell"] = generateBuySellVolumes(df)["volume_sell"]
df_vol = pd.DataFrame(columns=['price_lower', 'price_upper',
'price_avg', 'volume', 'volume_buy', 'volume_sell'])
div = df['close'].max() - df['close'].min()
step = div/bar_split
for i in range(0, bar_split):
# finding the priceRange
price_lower = df['close'].min() + step*i
price_upper = df['close'].min() + step*(i+1)
price_avg = (price_lower + price_upper)/2
# the volume for the given priceRange
if history_bars is not None:
dt_tail = df.tail(history_bars)
else:
dt_tail = df
volume_comb = dt_tail[(dt_tail['close'] >= price_lower) & (
dt_tail['close'] < price_upper)]['volume'].sum()
volume_buy = dt_tail[(dt_tail['close'] >= price_lower) & (
dt_tail['close'] < price_upper)]['volume_buy'].sum()
volume_sell = dt_tail[(dt_tail['close'] >= price_lower) & (
dt_tail['close'] < price_upper)]['volume_sell'].sum()
df_vol = df_vol.append({'price_lower': price_lower,
'price_upper': price_upper,
'price_avg': price_avg,
'volume': volume_comb,
'volume_buy': volume_buy,
'volume_sell': volume_sell}, ignore_index=True)
return df_vol
def add_volumeProfile(data: pd.DataFrame, fig, plot_config: Dict[str, Dict]):
# load VolumeProfile configurations
volume_config = plot_config['volume'] if 'volume' in plot_config else {}
showVolumeProfile = volume_config['showVolumeProfile'] if 'showVolumeProfile' in \
volume_config else 'false'
VolumeProfileHistoryBars = volume_config['VolumeProfileHistoryBars'] if \
'VolumeProfileHistoryBars' in volume_config else -1
VolumeProfilePriceRangeSplices = volume_config[
'VolumeProfilePriceRangeSplices'] if 'VolumeProfilePriceRangeSplices' in \
volume_config else 50
# Show VolumeProfile
if showVolumeProfile.lower() == 'true':
volumeProfileData = createVolumeProfileData(
data, VolumeProfilePriceRangeSplices, VolumeProfileHistoryBars)
volume_buy = go.Bar(
x=volumeProfileData['volume_buy'],
y=volumeProfileData['price_avg'],
name='VolumeProfile Buy',
marker_color='rgba(99, 146, 52, 0.6)',
marker_line_color='rgba(99, 146, 52, 0.6)',
orientation='h',
)
volume_sell = go.Bar(
x=volumeProfileData['volume_sell'],
y=volumeProfileData['price_avg'],
name='VolumeProfile Sell',
marker_color='rgba(208, 41, 41, 0.6)',
marker_line_color='rgba(208, 41, 41, 0.6)',
orientation='h',
)
fig.add_trace(volume_sell, 1, 2)
fig.add_trace(volume_buy, 1, 2)
fig.update_layout(barmode='stack')
def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *, def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *,
indicators1: List[str] = [], indicators1: List[str] = [],
indicators2: List[str] = [], indicators2: List[str] = [],
@ -359,19 +482,30 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
plot_config = create_plotconfig(indicators1, indicators2, plot_config) plot_config = create_plotconfig(indicators1, indicators2, plot_config)
rows = 2 + len(plot_config['subplots']) rows = 2 + len(plot_config['subplots'])
row_widths = [1 for _ in plot_config['subplots']] row_widths = [1 for _ in plot_config['subplots']]
# load VolumeProfile configurations
volume_config = plot_config['volume'] if 'volume' in plot_config else {}
showBuySell = volume_config['showBuySell'] if 'showBuySell' in volume_config else 'false'
showVolumeProfile = volume_config['showVolumeProfile'] if 'showVolumeProfile' in \
volume_config else 'false'
# Define the graph # Define the graph
fig = make_subplots( fig = make_subplots(
rows=rows, rows=rows,
cols=1, cols=2, # ToDo: Check if 2 columns (instead of one) cause any issues somewhere else
# set the width of the Volume Profile
column_widths=[8, 1 if showVolumeProfile.lower() == 'true' else 0],
shared_xaxes=True, shared_xaxes=True,
shared_yaxes=True,
row_width=row_widths + [1, 4], row_width=row_widths + [1, 4],
vertical_spacing=0.0001, vertical_spacing=0.0001,
horizontal_spacing=0.0001,
) )
fig['layout'].update(title=pair) fig['layout'].update(title=pair)
fig['layout']['yaxis1'].update(title='Price') fig['layout']['yaxis1'].update(title='Price')
fig['layout']['yaxis2'].update(title='Volume') fig['layout']['yaxis3'].update(title='Volume')
for i, name in enumerate(plot_config['subplots']): for i, name in enumerate(plot_config['subplots']):
fig['layout'][f'yaxis{3 + i}'].update(title=name) fig['layout'][f'yaxis{5 + i*2 }'].update(title=name)
fig['layout']['xaxis']['rangeslider'].update(visible=False) fig['layout']['xaxis']['rangeslider'].update(visible=False)
fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"]) fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"])
@ -436,6 +570,32 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data)
fig = add_areas(fig, 1, data, plot_config['main_plot']) fig = add_areas(fig, 1, data, plot_config['main_plot'])
fig = plot_trades(fig, trades) fig = plot_trades(fig, trades)
add_volumeProfile(data, fig, plot_config)
# standard volume plot
if showBuySell.lower() == 'true': # show volume plot split by sell & buy trades
volume_red = go.Bar(
x=data['date'],
y=data['volume'] * (data['high']-data['close']) / (data['high']-data['low']),
name='Volume Sell',
marker_color='rgba(208, 41, 41, 0.6)',
marker_line_color='rgba(208, 41, 41, 0.6)'
)
fig.add_trace(volume_red, 2, 1)
volume_green = go.Bar(
x=data['date'],
y=data['volume'] * (data['close']-data['low']) / (data['high']-data['low']),
name='Volume Buy',
marker_color='rgba(99, 146, 52, 0.6)',
marker_line_color='rgba(99, 146, 52, 0.6)'
)
fig.add_trace(volume_green, 2, 1,)
fig.update_layout(barmode='stack')
else: # show 'normal' gray volume plot
# sub plot: Volume goes to row 2 # sub plot: Volume goes to row 2
volume = go.Bar( volume = go.Bar(
x=data['date'], x=data['date'],
@ -445,6 +605,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
marker_line_color='DarkSlateGrey' marker_line_color='DarkSlateGrey'
) )
fig.add_trace(volume, 2, 1) fig.add_trace(volume, 2, 1)
# add each sub plot to a separate row # add each sub plot to a separate row
for i, label in enumerate(plot_config['subplots']): for i, label in enumerate(plot_config['subplots']):
sub_config = plot_config['subplots'][label] sub_config = plot_config['subplots'][label]

View File

@ -13,8 +13,9 @@ from freqtrade.data import history
from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.plot.plotting import (add_areas, add_indicators, add_profit, create_plotconfig, from freqtrade.plot.plotting import (add_areas, add_indicators, add_profit, create_plotconfig,
generate_candlestick_graph, generate_plot_filename, createVolumeProfileData, generate_candlestick_graph,
generate_profit_graph, init_plotscript, load_and_plot_trades, generate_plot_filename, generate_profit_graph,
generateBuySellVolumes, init_plotscript, load_and_plot_trades,
plot_profit, plot_trades, store_plot_file) plot_profit, plot_trades, store_plot_file)
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from tests.conftest import get_args, log_has, log_has_re, patch_exchange from tests.conftest import get_args, log_has, log_has_re, patch_exchange
@ -60,6 +61,33 @@ def test_init_plotscript(default_conf, mocker, testdatadir):
assert "ADA/BTC" in ret["ohlcv"] assert "ADA/BTC" in ret["ohlcv"]
def test_generate_volumeProfile(default_conf, testdatadir):
pair = "UNITTEST/BTC"
timerange = TimeRange(None, 'line', 0, -1000)
data = history.load_pair_history(pair=pair, timeframe='1m',
datadir=testdatadir, timerange=timerange)
strategy = StrategyResolver.load_strategy(default_conf)
data = strategy.analyze_ticker(data, {'pair': pair})
buysell_volumes = generateBuySellVolumes(data)
assert buysell_volumes['volume_buy'].any()
assert buysell_volumes['volume_sell'].any()
volumeProfile = createVolumeProfileData(buysell_volumes, 50, 100)
assert volumeProfile['price_lower'].any()
assert volumeProfile['price_upper'].any()
assert volumeProfile['price_avg'].any()
assert volumeProfile['volume'].any()
assert volumeProfile['volume_buy'].any()
assert volumeProfile['volume_sell'].any()
assert len(volumeProfile.index) == 50
def test_add_indicators(default_conf, testdatadir, caplog): def test_add_indicators(default_conf, testdatadir, caplog):
pair = "UNITTEST/BTC" pair = "UNITTEST/BTC"
timerange = TimeRange(None, 'line', 0, -1000) timerange = TimeRange(None, 'line', 0, -1000)
@ -273,6 +301,48 @@ def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir)
assert trades_mock.call_count == 1 assert trades_mock.call_count == 1
def test_generate_candlestick_graph_withVolumeProfile(default_conf, mocker, testdatadir, caplog):
row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators',
MagicMock(side_effect=fig_generating_mock))
trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades',
MagicMock(side_effect=fig_generating_mock))
pair = "UNITTEST/BTC"
timerange = TimeRange(None, 'line', 0, -1000)
data = history.load_pair_history(pair=pair, timeframe='1m',
datadir=testdatadir, timerange=timerange)
data['buy'] = 0
data['sell'] = 0
fig = generate_candlestick_graph(pair=pair, data=data, trades=None,
plot_config={'main_plot': {'sma': {}, 'ema200': {}},
'subplots': {'Other': {'macdsignal': {}}},
'volume': {'showBuySell': 'true',
'showVolumeProfile': 'true',
'VolumeProfileHistoryBars': 96,
'VolumeProfilePriceRangeSplices': 60}})
assert isinstance(fig, go.Figure)
assert fig.layout.title.text == pair
figure = fig.layout.figure
assert len(figure.data) == 5
# Candlesticks are plotted first
candles = find_trace_in_fig_data(figure.data, "Price")
assert isinstance(candles, go.Candlestick)
assert isinstance(find_trace_in_fig_data(figure.data, "Volume Sell"), go.Bar)
assert isinstance(find_trace_in_fig_data(figure.data, "Volume Buy"), go.Bar)
assert isinstance(find_trace_in_fig_data(figure.data, "VolumeProfile Sell"), go.Bar)
assert isinstance(find_trace_in_fig_data(figure.data, "VolumeProfile Buy"), go.Bar)
assert row_mock.call_count == 2
assert trades_mock.call_count == 1
assert log_has("No buy-signals found.", caplog)
assert log_has("No sell-signals found.", caplog)
def test_generate_Plot_filename(): def test_generate_Plot_filename():
fn = generate_plot_filename("UNITTEST/BTC", "5m") fn = generate_plot_filename("UNITTEST/BTC", "5m")
assert fn == "freqtrade-plot-UNITTEST_BTC-5m.html" assert fn == "freqtrade-plot-UNITTEST_BTC-5m.html"
@ -468,6 +538,7 @@ def test_plot_profit(default_conf, mocker, testdatadir):
([], [], {}, ([], [], {},
{'main_plot': {'sma': {}, 'ema3': {}, 'ema5': {}}, {'main_plot': {'sma': {}, 'ema3': {}, 'ema5': {}},
'subplots': {'Other': {'macd': {}, 'macdsignal': {}}}}), 'subplots': {'Other': {'macd': {}, 'macdsignal': {}}}}),
# use indicators # use indicators
(['sma', 'ema3'], ['macd'], {}, (['sma', 'ema3'], ['macd'], {},
{'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'Other': {'macd': {}}}}), {'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'Other': {'macd': {}}}}),
@ -495,6 +566,29 @@ def test_plot_profit(default_conf, mocker, testdatadir):
{'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}, {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
{'main_plot': {'sma': {}}, 'subplots': {'Other': {'macd': {}, 'macd_signal': {}}}} {'main_plot': {'sma': {}}, 'subplots': {'Other': {'macd': {}, 'macd_signal': {}}}}
), ),
# No indicators, use plot_conf with just volume profile
([], [],
{
'volume': {'showBuySell': 'true', 'showVolumeProfile': 'true',
'VolumeProfileHistoryBars': 96, 'VolumeProfilePriceRangeSplices': 100},
},
{'main_plot': {'sma': {}, 'ema3': {}, 'ema5': {}},
'subplots': {'Other': {'macd': {}, 'macdsignal': {}}},
'volume': {'showBuySell': 'true', 'showVolumeProfile': 'true',
'VolumeProfileHistoryBars': 96, 'VolumeProfilePriceRangeSplices': 100}},
),
# No indicators, use full plot_conf with volume profile
([], [],
{'main_plot': {'sma': {}, 'ema200': {}},
'subplots': {'Other': {'macdsignal': {}}},
'volume': {'showBuySell': 'true', 'showVolumeProfile': 'true',
'VolumeProfileHistoryBars': 9, 'VolumeProfilePriceRangeSplices': 111}},
{'main_plot': {'sma': {}, 'ema200': {}},
'subplots': {'Other': {'macdsignal': {}}},
'volume': {'showBuySell': 'true', 'showVolumeProfile': 'true',
'VolumeProfileHistoryBars': 9, 'VolumeProfilePriceRangeSplices': 111}},
)
]) ])
def test_create_plotconfig(ind1, ind2, plot_conf, exp): def test_create_plotconfig(ind1, ind2, plot_conf, exp):