diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 646afb5df..1fc4d721e 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,7 +1,7 @@ import logging from datetime import timedelta from pathlib import Path -from typing import Dict +from typing import Any, Dict, List from pandas import DataFrame from tabulate import tabulate @@ -34,118 +34,173 @@ def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame file_dump_json(filename, records) -def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, - results: DataFrame, skip_nan: bool = False) -> str: +def _get_line_floatfmt() -> List[str]: """ - Generates and returns a text table for the given backtest data and the results dataframe + Generate floatformat (goes in line with _generate_result_line()) + """ + return ['s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', 'd', 'd', 'd'] + + +def _get_line_header(first_column: str, stake_currency: str) -> List[str]: + """ + Generate header lines (goes in line with _generate_result_line()) + """ + return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %', + f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', + 'Wins', 'Draws', 'Losses'] + + +def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: str) -> Dict: + """ + Generate one result dict, with "first_column" as key. + """ + return { + 'key': first_column, + 'trades': len(result.index), + 'profit_mean': result.profit_percent.mean(), + 'profit_mean_pct': result.profit_percent.mean() * 100.0, + 'profit_sum': result.profit_percent.sum(), + 'profit_sum_pct': result.profit_percent.sum() * 100.0, + 'profit_total_abs': result.profit_abs.sum(), + 'profit_total_pct': result.profit_percent.sum() * 100.0 / max_open_trades, + 'duration_avg': str(timedelta( + minutes=round(result.trade_duration.mean())) + ) if not result.empty else '0:00', + # 'duration_max': str(timedelta( + # minutes=round(result.trade_duration.max())) + # ) if not result.empty else '0:00', + # 'duration_min': str(timedelta( + # minutes=round(result.trade_duration.min())) + # ) if not result.empty else '0:00', + 'wins': len(result[result.profit_abs > 0]), + 'draws': len(result[result.profit_abs == 0]), + 'losses': len(result[result.profit_abs < 0]), + } + + +def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, + results: DataFrame, skip_nan: bool = False) -> List[Dict]: + """ + Generates and returns a list for the given backtest data and the results dataframe :param data: Dict of containing data that was used during backtesting. :param stake_currency: stake-currency - used to correctly name headers :param max_open_trades: Maximum allowed open trades :param results: Dataframe containing the backtest results :param skip_nan: Print "left open" open trades - :return: pretty printed table with tabulate as string + :return: List of Dicts containing the metrics per pair """ - floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') tabular_data = [] - headers = [ - 'Pair', - 'Buys', - 'Avg Profit %', - 'Cum Profit %', - f'Tot Profit {stake_currency}', - 'Tot Profit %', - 'Avg Duration', - 'Wins', - 'Draws', - 'Losses' - ] + for pair in data: result = results[results.pair == pair] if skip_nan and result.profit_abs.isnull().all(): continue - tabular_data.append([ - pair, - len(result.index), - result.profit_percent.mean() * 100.0, - result.profit_percent.sum() * 100.0, - result.profit_abs.sum(), - result.profit_percent.sum() * 100.0 / max_open_trades, - str(timedelta( - minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00', - len(result[result.profit_abs > 0]), - len(result[result.profit_abs == 0]), - len(result[result.profit_abs < 0]) - ]) + tabular_data.append(_generate_result_line(result, max_open_trades, pair)) # Append Total - tabular_data.append([ - 'TOTAL', - len(results.index), - results.profit_percent.mean() * 100.0, - results.profit_percent.sum() * 100.0, - results.profit_abs.sum(), - results.profit_percent.sum() * 100.0 / max_open_trades, - str(timedelta( - minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', - len(results[results.profit_abs > 0]), - len(results[results.profit_abs == 0]), - len(results[results.profit_abs < 0]) - ]) + tabular_data.append(_generate_result_line(results, max_open_trades, 'TOTAL')) + return tabular_data + + +def generate_text_table(pair_results: List[Dict[str, Any]], stake_currency: str) -> str: + """ + Generates and returns a text table for the given backtest data and the results dataframe + :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row + :param stake_currency: stake-currency - used to correctly name headers + :return: pretty printed table with tabulate as string + """ + + headers = _get_line_header('Pair', stake_currency) + floatfmt = _get_line_floatfmt() + output = [[ + t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], + t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] + ] for t in pair_results] # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate(tabular_data, headers=headers, + return tabulate(output, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore -def generate_text_table_sell_reason(stake_currency: str, max_open_trades: int, - results: DataFrame) -> str: +def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]: """ Generate small table outlining Backtest results - :param stake_currency: Stakecurrency used :param max_open_trades: Max_open_trades parameter - :param results: Dataframe containing the backtest results - :return: pretty printed table with tabulate as string + :param results: Dataframe containing the backtest result for one strategy + :return: List of Dicts containing the metrics per Sell reason """ tabular_data = [] - headers = [ - "Sell Reason", - "Sells", - "Wins", - "Draws", - "Losses", - "Avg Profit %", - "Cum Profit %", - f"Tot Profit {stake_currency}", - "Tot Profit %", - ] + for reason, count in results['sell_reason'].value_counts().iteritems(): result = results.loc[results['sell_reason'] == reason] - wins = len(result[result['profit_abs'] > 0]) - draws = len(result[result['profit_abs'] == 0]) - loss = len(result[result['profit_abs'] < 0]) - profit_mean = round(result['profit_percent'].mean() * 100.0, 2) - profit_sum = round(result["profit_percent"].sum() * 100.0, 2) - profit_tot = result['profit_abs'].sum() + + profit_mean = result['profit_percent'].mean() + profit_sum = result["profit_percent"].sum() profit_percent_tot = round(result['profit_percent'].sum() * 100.0 / max_open_trades, 2) + tabular_data.append( - [ - reason.value, - count, - wins, - draws, - loss, - profit_mean, - profit_sum, - profit_tot, - profit_percent_tot, - ] + { + 'sell_reason': reason.value, + 'trades': count, + 'wins': len(result[result['profit_abs'] > 0]), + 'draws': len(result[result['profit_abs'] == 0]), + 'losses': len(result[result['profit_abs'] < 0]), + 'profit_mean': profit_mean, + 'profit_mean_pct': round(profit_mean * 100, 2), + 'profit_sum': profit_sum, + 'profit_sum_pct': round(profit_sum * 100, 2), + 'profit_total_abs': result['profit_abs'].sum(), + 'profit_pct_total': profit_percent_tot, + } ) - return tabulate(tabular_data, headers=headers, tablefmt="orgtbl", stralign="right") + return tabular_data -def generate_text_table_strategy(stake_currency: str, max_open_trades: str, - all_results: Dict) -> str: +def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], + stake_currency: str) -> str: + """ + Generate small table outlining Backtest results + :param sell_reason_stats: Sell reason metrics + :param stake_currency: Stakecurrency used + :return: pretty printed table with tabulate as string + """ + headers = [ + 'Sell Reason', + 'Sells', + 'Wins', + 'Draws', + 'Losses', + 'Avg Profit %', + 'Cum Profit %', + f'Tot Profit {stake_currency}', + 'Tot Profit %', + ] + + output = [[ + t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'], + t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_pct_total'], + ] for t in sell_reason_stats] + return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") + + +def generate_strategy_metrics(stake_currency: str, max_open_trades: int, + all_results: Dict) -> List[Dict]: + """ + Generate summary per strategy + :param stake_currency: stake-currency - used to correctly name headers + :param max_open_trades: Maximum allowed open trades used for backtest + :param all_results: Dict of containing results for all strategies + :return: List of Dicts containing the metrics per Strategy + """ + + tabular_data = [] + for strategy, results in all_results.items(): + tabular_data.append(_generate_result_line(results, max_open_trades, strategy)) + return tabular_data + + +def generate_text_table_strategy(strategy_results, stake_currency: str) -> str: """ Generate summary table per strategy :param stake_currency: stake-currency - used to correctly name headers @@ -153,34 +208,21 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str, :param all_results: Dict of containing results for all strategies :return: pretty printed table with tabulate as string """ + floatfmt = _get_line_floatfmt() + headers = _get_line_header('Strategy', stake_currency) - floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') - tabular_data = [] - headers = ['Strategy', 'Buys', 'Avg Profit %', 'Cum Profit %', - f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', - 'Wins', 'Draws', 'Losses'] - for strategy, results in all_results.items(): - tabular_data.append([ - strategy, - len(results.index), - results.profit_percent.mean() * 100.0, - results.profit_percent.sum() * 100.0, - results.profit_abs.sum(), - results.profit_percent.sum() * 100.0 / max_open_trades, - str(timedelta( - minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', - len(results[results.profit_abs > 0]), - len(results[results.profit_abs == 0]), - len(results[results.profit_abs < 0]) - ]) + output = [[ + t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], + t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] + ] for t in strategy_results] # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate(tabular_data, headers=headers, + return tabulate(output, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore def generate_edge_table(results: dict) -> str: - floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d') + floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd') tabular_data = [] headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio', 'Required Risk Reward', 'Expectancy', 'Total Number of Trades', @@ -206,38 +248,48 @@ def generate_edge_table(results: dict) -> str: def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], all_results: Dict[str, DataFrame]): - for strategy, results in all_results.items(): + stake_currency = config['stake_currency'] + max_open_trades = config['max_open_trades'] + for strategy, results in all_results.items(): + pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, + max_open_trades=max_open_trades, + results=results, skip_nan=False) + sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, + results=results) + left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, + max_open_trades=max_open_trades, + results=results.loc[results['open_at_end']], + skip_nan=True) + # Print results print(f"Result for strategy {strategy}") - table = generate_text_table(btdata, stake_currency=config['stake_currency'], - max_open_trades=config['max_open_trades'], - results=results) + table = generate_text_table(pair_results, stake_currency=stake_currency) if isinstance(table, str): print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) - table = generate_text_table_sell_reason(stake_currency=config['stake_currency'], - max_open_trades=config['max_open_trades'], - results=results) + table = generate_text_table_sell_reason(sell_reason_stats=sell_reason_stats, + stake_currency=stake_currency, + ) if isinstance(table, str): print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) - table = generate_text_table(btdata, - stake_currency=config['stake_currency'], - max_open_trades=config['max_open_trades'], - results=results.loc[results.open_at_end], skip_nan=True) + table = generate_text_table(left_open_results, stake_currency=stake_currency) if isinstance(table, str): print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) print(table) if isinstance(table, str): print('=' * len(table.splitlines()[0])) print() + if len(all_results) > 1: # Print Strategy summary table - table = generate_text_table_strategy(config['stake_currency'], - config['max_open_trades'], - all_results=all_results) + strategy_results = generate_strategy_metrics(stake_currency=stake_currency, + max_open_trades=max_open_trades, + all_results=all_results) + + table = generate_text_table_strategy(strategy_results, stake_currency) print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '=')) print(table) print('=' * len(table.splitlines()[0])) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 019914720..ace82d28b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -658,10 +658,17 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) gen_table_mock = MagicMock() - mocker.patch('freqtrade.optimize.optimize_reports.generate_text_table', gen_table_mock) + sell_reason_mock = MagicMock() gen_strattable_mock = MagicMock() - mocker.patch('freqtrade.optimize.optimize_reports.generate_text_table_strategy', - gen_strattable_mock) + gen_strat_summary = MagicMock() + + mocker.patch.multiple('freqtrade.optimize.optimize_reports', + generate_text_table=gen_table_mock, + generate_text_table_strategy=gen_strattable_mock, + generate_pair_metrics=MagicMock(), + generate_sell_reason_stats=sell_reason_mock, + generate_strategy_metrics=gen_strat_summary, + ) patched_configuration_load_config_file(mocker, default_conf) args = [ @@ -683,6 +690,8 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): assert backtestmock.call_count == 2 assert gen_table_mock.call_count == 4 assert gen_strattable_mock.call_count == 1 + assert sell_reason_mock.call_count == 2 + assert gen_strat_summary.call_count == 1 # check the logs, that will contain the backtest result exists = [ @@ -703,3 +712,92 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): for line in exists: assert log_has(line, caplog) + + +@pytest.mark.filterwarnings("ignore:deprecated") +def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys): + + patch_exchange(mocker) + backtestmock = MagicMock(side_effect=[ + pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], + 'profit_percent': [0.0, 0.0], + 'profit_abs': [0.0, 0.0], + 'open_time': pd.to_datetime(['2018-01-29 18:40:00', + '2018-01-30 03:30:00', ], utc=True + ), + 'close_time': pd.to_datetime(['2018-01-29 20:45:00', + '2018-01-30 05:35:00', ], utc=True), + 'open_index': [78, 184], + 'close_index': [125, 192], + 'trade_duration': [235, 40], + 'open_at_end': [False, False], + 'open_rate': [0.104445, 0.10302485], + 'close_rate': [0.104969, 0.103541], + 'sell_reason': [SellType.ROI, SellType.ROI] + }), + pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], + 'profit_percent': [0.03, 0.01, 0.1], + 'profit_abs': [0.01, 0.02, 0.2], + 'open_time': pd.to_datetime(['2018-01-29 18:40:00', + '2018-01-30 03:30:00', + '2018-01-30 05:30:00'], utc=True + ), + 'close_time': pd.to_datetime(['2018-01-29 20:45:00', + '2018-01-30 05:35:00', + '2018-01-30 08:30:00'], utc=True), + 'open_index': [78, 184, 185], + 'close_index': [125, 224, 205], + 'trade_duration': [47, 40, 20], + 'open_at_end': [False, False, False], + 'open_rate': [0.104445, 0.10302485, 0.122541], + 'close_rate': [0.104969, 0.103541, 0.123541], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + }), + ]) + mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + PropertyMock(return_value=['UNITTEST/BTC'])) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) + + patched_configuration_load_config_file(mocker, default_conf) + + args = [ + 'backtesting', + '--config', 'config.json', + '--datadir', str(testdatadir), + '--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'), + '--ticker-interval', '1m', + '--timerange', '1510694220-1510700340', + '--enable-position-stacking', + '--disable-max-market-positions', + '--strategy-list', + 'DefaultStrategy', + 'TestStrategyLegacy', + ] + args = get_args(args) + start_backtesting(args) + + # check the logs, that will contain the backtest result + exists = [ + 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', + 'Parameter --timerange detected: 1510694220-1510700340 ...', + f'Using data directory: {testdatadir} ...', + 'Using stake_currency: BTC ...', + 'Using stake_amount: 0.001 ...', + 'Loading data from 2017-11-14T20:57:00+00:00 ' + 'up to 2017-11-14T22:58:00+00:00 (0 days)..', + 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' + 'up to 2017-11-14T22:58:00+00:00 (0 days)..', + 'Parameter --enable-position-stacking detected ...', + 'Running backtesting for Strategy DefaultStrategy', + 'Running backtesting for Strategy TestStrategyLegacy', + ] + + for line in exists: + assert log_has(line, caplog) + + captured = capsys.readouterr() + assert 'BACKTESTING REPORT' in captured.out + assert 'SELL REASON STATS' in captured.out + assert 'LEFT OPEN TRADES REPORT' in captured.out + assert 'STRATEGY SUMMARY' in captured.out diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index e0782146a..8bef6e2cc 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -1,11 +1,13 @@ from pathlib import Path import pandas as pd +import pytest from arrow import Arrow from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import ( - generate_edge_table, generate_text_table, generate_text_table_sell_reason, + generate_pair_metrics, generate_edge_table, generate_sell_reason_stats, + generate_text_table, generate_text_table_sell_reason, generate_strategy_metrics, generate_text_table_strategy, store_backtest_result) from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange @@ -35,12 +37,39 @@ def test_generate_text_table(default_conf, mocker): '| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 |' ' 15.00 | 0:20:00 | 2 | 0 | 0 |' ) - assert generate_text_table(data={'ETH/BTC': {}}, - stake_currency='BTC', max_open_trades=2, - results=results) == result_str + + pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', + max_open_trades=2, results=results) + assert generate_text_table(pair_results, + stake_currency='BTC') == result_str -def test_generate_text_table_sell_reason(default_conf, mocker): +def test_generate_pair_metrics(default_conf, mocker): + + results = pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC'], + 'profit_percent': [0.1, 0.2], + 'profit_abs': [0.2, 0.4], + 'trade_duration': [10, 30], + 'wins': [2, 0], + 'draws': [0, 0], + 'losses': [0, 0] + } + ) + + pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', + max_open_trades=2, results=results) + assert isinstance(pair_results, list) + assert len(pair_results) == 2 + assert pair_results[-1]['key'] == 'TOTAL' + assert ( + pytest.approx(pair_results[-1]['profit_mean_pct']) == pair_results[-1]['profit_mean'] * 100) + assert ( + pytest.approx(pair_results[-1]['profit_sum_pct']) == pair_results[-1]['profit_sum'] * 100) + + +def test_generate_text_table_sell_reason(default_conf): results = pd.DataFrame( { @@ -65,8 +94,46 @@ def test_generate_text_table_sell_reason(default_conf, mocker): '| stop_loss | 1 | 0 | 0 | 1 |' ' -10 | -10 | -0.2 | -5 |' ) - assert generate_text_table_sell_reason(stake_currency='BTC', max_open_trades=2, - results=results) == result_str + + sell_reason_stats = generate_sell_reason_stats(max_open_trades=2, + results=results) + assert generate_text_table_sell_reason(sell_reason_stats=sell_reason_stats, + stake_currency='BTC') == result_str + + +def test_generate_sell_reason_stats(default_conf): + + results = pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_percent': [0.1, 0.2, -0.1], + 'profit_abs': [0.2, 0.4, -0.2], + 'trade_duration': [10, 30, 10], + 'wins': [2, 0, 0], + 'draws': [0, 0, 0], + 'losses': [0, 0, 1], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + } + ) + + sell_reason_stats = generate_sell_reason_stats(max_open_trades=2, + results=results) + roi_result = sell_reason_stats[0] + assert roi_result['sell_reason'] == 'roi' + assert roi_result['trades'] == 2 + assert pytest.approx(roi_result['profit_mean']) == 0.15 + assert roi_result['profit_mean_pct'] == round(roi_result['profit_mean'] * 100, 2) + assert pytest.approx(roi_result['profit_mean']) == 0.15 + assert roi_result['profit_mean_pct'] == round(roi_result['profit_mean'] * 100, 2) + + stop_result = sell_reason_stats[1] + + assert stop_result['sell_reason'] == 'stop_loss' + assert stop_result['trades'] == 1 + assert pytest.approx(stop_result['profit_mean']) == -0.1 + assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2) + assert pytest.approx(stop_result['profit_mean']) == -0.1 + assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2) def test_generate_text_table_strategy(default_conf, mocker): @@ -106,7 +173,12 @@ def test_generate_text_table_strategy(default_conf, mocker): '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |' ' 45.00 | 0:20:00 | 3 | 0 | 0 |' ) - assert generate_text_table_strategy('BTC', 2, all_results=results) == result_str + + strategy_results = generate_strategy_metrics(stake_currency='BTC', + max_open_trades=2, + all_results=results) + + assert generate_text_table_strategy(strategy_results, 'BTC') == result_str def test_generate_edge_table(edge_conf, mocker):